feat(project): 重构甘特图为vue-ganttastic组件并更新项目统计卡片
Some checks failed
Lint Code / Lint Code (push) Failing after 1m35s
Some checks failed
Lint Code / Lint Code (push) Failing after 1m35s
- 替换原有甘特图任务条样式计算,改用vue-ganttastic库渲染任务条和里程碑 - 根据任务状态和进度生成不同颜色,支持关键里程碑标识 - 实现甘特图日期范围计算及任务、里程碑数据格式转换 - 优化甘特图和里程碑的布局与样式,提升交互体验 - 移除右侧项目AI助手相关代码及样式 - 项目首页统计卡片文字和图标调整,展示项目总数、进行中、已完成等状态 - 平均进度条样式优化,补充高风险项目数量显示
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
type ProjectResource,
|
type ProjectResource,
|
||||||
type ProjectRisk
|
type ProjectRisk
|
||||||
} from "@/api/project";
|
} from "@/api/project";
|
||||||
|
import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic";
|
||||||
import { message } from "@/utils/message";
|
import { message } from "@/utils/message";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
@@ -195,24 +196,103 @@ function getMaterialStatusType(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算甘特图任务条样式(使用真实任务数据)
|
// 将任务数据转换为 vue-ganttastic 格式
|
||||||
function getTaskBarStyle(task: ProjectTask) {
|
const ganttTasks = computed(() => {
|
||||||
if (!task.planStartDate || !task.planEndDate) return {};
|
return taskList.value.map(task => ({
|
||||||
const start = dayjs(task.planStartDate);
|
id: task.id,
|
||||||
const end = dayjs(task.planEndDate);
|
label: task.taskName,
|
||||||
const projectStart = dayjs(projectInfo.value.startDate);
|
startDate: task.planStartDate || "",
|
||||||
const projectEnd = dayjs(projectInfo.value.endDate);
|
endDate: task.planEndDate || "",
|
||||||
if (!projectStart.isValid() || !projectEnd.isValid()) return {};
|
progress: task.progress || 0,
|
||||||
const totalDays = projectEnd.diff(projectStart, "day");
|
assignee: task.assigneeName || "未分配",
|
||||||
const offsetDays = start.diff(projectStart, "day");
|
status: task.status || "pending",
|
||||||
const duration = end.diff(start, "day") + 1;
|
ganttBarConfig: {
|
||||||
|
id: task.id,
|
||||||
|
label: `${task.taskName} ${task.assigneeName ? "负责人:" + task.assigneeName : ""}`,
|
||||||
|
hasHandles: false,
|
||||||
|
style: {
|
||||||
|
backgroundColor: getTaskColor(task.status, task.progress),
|
||||||
|
color: "#fff"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
// 获取任务颜色
|
||||||
left: `${(offsetDays / totalDays) * 100}%`,
|
function getTaskColor(status?: string, progress?: number): string {
|
||||||
width: `${(duration / totalDays) * 100}%`
|
if (progress === 100) return "#67c23a"; // 已完成 - 绿色
|
||||||
};
|
if (status === "completed") return "#67c23a"; // 已完成 - 绿色
|
||||||
|
if (status === "in_progress" || status === "ongoing") return "#409eff"; // 进行中 - 蓝色
|
||||||
|
if (status === "pending") return "#e6a23c"; // 待开始 - 橙色
|
||||||
|
if (status === "delayed") return "#f56c6c"; // 延期 - 红色
|
||||||
|
if (status === "paused") return "#909399"; // 暂停 - 灰色
|
||||||
|
return "#409eff"; // 默认 - 蓝色
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将里程碑数据转换为 vue-ganttastic 格式
|
||||||
|
const ganttMilestones = computed(() => {
|
||||||
|
return milestoneList.value.map(milestone => ({
|
||||||
|
id: milestone.id,
|
||||||
|
label: milestone.milestoneName,
|
||||||
|
startDate: milestone.planDate || "",
|
||||||
|
endDate: milestone.planDate || "",
|
||||||
|
status: milestone.status || "pending",
|
||||||
|
isKey: milestone.isKey === 1,
|
||||||
|
ganttBarConfig: {
|
||||||
|
id: milestone.id,
|
||||||
|
label: `${milestone.milestoneName} ${milestone.isKey === 1 ? "【关键】" : ""}`,
|
||||||
|
hasHandles: false,
|
||||||
|
style: {
|
||||||
|
backgroundColor: getMilestoneColor(milestone.status, milestone.isKey),
|
||||||
|
color: "#fff",
|
||||||
|
borderRadius: "50%",
|
||||||
|
width: "16px",
|
||||||
|
height: "16px",
|
||||||
|
minWidth: "16px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取里程碑颜色
|
||||||
|
function getMilestoneColor(status?: string, isKey?: number): string {
|
||||||
|
if (status === "completed") return "#67c23a"; // 已完成 - 绿色
|
||||||
|
if (status === "in_progress") return "#409eff"; // 进行中 - 蓝色
|
||||||
|
if (isKey === 1) return "#f56c6c"; // 关键里程碑 - 红色
|
||||||
|
return "#e6a23c"; // 默认 - 橙色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取里程碑状态文本
|
||||||
|
function getMilestoneStatusText(status?: string): string {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
completed: "已完成",
|
||||||
|
in_progress: "进行中",
|
||||||
|
pending: "待开始",
|
||||||
|
delayed: "已延期"
|
||||||
|
};
|
||||||
|
return statusMap[status || ""] || "待开始";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按日期排序的里程碑列表
|
||||||
|
const sortedMilestones = computed(() => {
|
||||||
|
return [...milestoneList.value].sort((a, b) => {
|
||||||
|
const dateA = new Date(a.planDate || "").getTime();
|
||||||
|
const dateB = new Date(b.planDate || "").getTime();
|
||||||
|
return dateA - dateB;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算甘特图日期范围
|
||||||
|
const ganttDateRange = computed(() => {
|
||||||
|
const start = projectInfo.value.startDate || dayjs().format("YYYY-MM-DD");
|
||||||
|
const end =
|
||||||
|
projectInfo.value.endDate || dayjs().add(6, "month").format("YYYY-MM-DD");
|
||||||
|
return {
|
||||||
|
start: dayjs(start).format("YYYY-MM-DD"),
|
||||||
|
end: dayjs(end).format("YYYY-MM-DD")
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 获取项目详情
|
// 获取项目详情
|
||||||
async function fetchProjectDetail() {
|
async function fetchProjectDetail() {
|
||||||
if (!projectId.value) return;
|
if (!projectId.value) return;
|
||||||
@@ -410,7 +490,9 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<el-progress
|
<el-progress
|
||||||
:percentage="
|
:percentage="
|
||||||
Math.round((projectInfo.cost / projectInfo.budget) * 100)
|
projectInfo.budget > 0
|
||||||
|
? Math.round((projectInfo.cost / projectInfo.budget) * 100)
|
||||||
|
: 0
|
||||||
"
|
"
|
||||||
status="warning"
|
status="warning"
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
@@ -480,7 +562,7 @@ onMounted(() => {
|
|||||||
<!-- 主要内容区 -->
|
<!-- 主要内容区 -->
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<!-- 左侧:甘特图和物料清单 -->
|
<!-- 左侧:甘特图和物料清单 -->
|
||||||
<el-col :xs="24" :lg="16">
|
<el-col :xs="24" :lg="24">
|
||||||
<!-- 甘特图 -->
|
<!-- 甘特图 -->
|
||||||
<el-card shadow="hover" class="mb-4">
|
<el-card shadow="hover" class="mb-4">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -509,42 +591,115 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 甘特图主体 -->
|
<!-- 任务甘特图主体 - 使用 vue-ganttastic -->
|
||||||
<div class="gantt-chart">
|
<div v-if="taskList.length > 0" class="gantt-chart-container mb-6">
|
||||||
<!-- 时间轴 -->
|
<g-gantt-chart
|
||||||
<div class="gantt-timeline">
|
:chart-start="ganttDateRange.start"
|
||||||
<div
|
:chart-end="ganttDateRange.end"
|
||||||
v-for="month in 6"
|
precision="day"
|
||||||
:key="month"
|
date-format="YYYY-MM-DD"
|
||||||
class="timeline-item"
|
bar-start="startDate"
|
||||||
:style="{ left: `${(month - 1) * (100 / 6)}%` }"
|
bar-end="endDate"
|
||||||
>
|
grid
|
||||||
{{
|
highlight-on-hover
|
||||||
dayjs(projectInfo.startDate)
|
>
|
||||||
.add(month - 1, "month")
|
<g-gantt-row
|
||||||
.format("YYYY-MM")
|
v-for="task in ganttTasks"
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 任务列表 -->
|
|
||||||
<div class="gantt-tasks">
|
|
||||||
<div
|
|
||||||
v-for="task in taskList.slice().reverse()"
|
|
||||||
:key="task.id"
|
:key="task.id"
|
||||||
class="gantt-task-row"
|
:bars="[task]"
|
||||||
|
:label="task.label"
|
||||||
|
/>
|
||||||
|
</g-gantt-chart>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无任务数据" class="mb-6" />
|
||||||
|
|
||||||
|
<!-- 里程碑时间线 -->
|
||||||
|
<div v-if="milestoneList.length > 0" class="milestone-section">
|
||||||
|
<div class="flex items-center gap-2 mb-4">
|
||||||
|
<el-icon :size="18" color="#f56c6c">
|
||||||
|
<component :is="useRenderIcon('ri/flag-line')" />
|
||||||
|
</el-icon>
|
||||||
|
<span class="font-medium text-base">项目里程碑</span>
|
||||||
|
<el-tag size="small" type="info"
|
||||||
|
>{{ milestoneList.length }} 个</el-tag
|
||||||
>
|
>
|
||||||
<div class="task-name">{{ task.taskName }}</div>
|
</div>
|
||||||
<div class="task-bar-container">
|
<div class="milestone-timeline">
|
||||||
|
<div
|
||||||
|
v-for="(milestone, index) in sortedMilestones"
|
||||||
|
:key="milestone.id"
|
||||||
|
class="milestone-item"
|
||||||
|
:class="{
|
||||||
|
'is-completed': milestone.status === 'completed',
|
||||||
|
'is-key': milestone.isKey === 1
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="milestone-marker">
|
||||||
<div
|
<div
|
||||||
class="task-bar"
|
class="milestone-dot"
|
||||||
:style="getTaskBarStyle(task)"
|
:style="{
|
||||||
:class="{ completed: task.progress === 100 }"
|
backgroundColor: getMilestoneColor(
|
||||||
|
milestone.status,
|
||||||
|
milestone.isKey
|
||||||
|
)
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<el-icon
|
||||||
class="task-progress"
|
v-if="milestone.status === 'completed'"
|
||||||
:style="{ width: `${task.progress || 0}%` }"
|
:size="12"
|
||||||
/>
|
color="#fff"
|
||||||
|
>
|
||||||
|
<component :is="useRenderIcon('ri/check-line')" />
|
||||||
|
</el-icon>
|
||||||
|
<el-icon
|
||||||
|
v-else-if="milestone.isKey === 1"
|
||||||
|
:size="12"
|
||||||
|
color="#fff"
|
||||||
|
>
|
||||||
|
<component :is="useRenderIcon('ri/star-fill')" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="index < sortedMilestones.length - 1"
|
||||||
|
class="milestone-line"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="milestone-content">
|
||||||
|
<div class="milestone-header">
|
||||||
|
<span class="milestone-name">{{
|
||||||
|
milestone.milestoneName
|
||||||
|
}}</span>
|
||||||
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="
|
||||||
|
milestone.status === 'completed'
|
||||||
|
? 'success'
|
||||||
|
: milestone.status === 'in_progress'
|
||||||
|
? 'primary'
|
||||||
|
: 'info'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ getMilestoneStatusText(milestone.status) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="milestone.isKey === 1"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
effect="dark"
|
||||||
|
>关键</el-tag
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="milestone-date">
|
||||||
|
<el-icon :size="12"
|
||||||
|
><component :is="useRenderIcon('ri/calendar-line')"
|
||||||
|
/></el-icon>
|
||||||
|
<span>计划日期: {{ milestone.planDate }}</span>
|
||||||
|
<span v-if="milestone.actualDate" class="actual-date">
|
||||||
|
(实际: {{ milestone.actualDate }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="milestone.description" class="milestone-desc">
|
||||||
|
{{ milestone.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -618,78 +773,6 @@ onMounted(() => {
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
|
||||||
<!-- 右侧:AI助手 -->
|
|
||||||
<el-col :xs="24" :lg="8">
|
|
||||||
<el-card shadow="hover" class="ai-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<el-icon :size="20" color="#409eff">
|
|
||||||
<component :is="useRenderIcon(RobotIcon)" />
|
|
||||||
</el-icon>
|
|
||||||
<span class="font-medium">项目AI助手</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-gray-400 mt-1">分析项目进度和流程卡点</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
|
||||||
<div class="ai-messages">
|
|
||||||
<div
|
|
||||||
v-for="(msg, index) in aiMessages"
|
|
||||||
:key="index"
|
|
||||||
class="message-item"
|
|
||||||
:class="msg.type"
|
|
||||||
>
|
|
||||||
<div class="message-avatar">
|
|
||||||
<el-avatar
|
|
||||||
v-if="msg.type === 'ai'"
|
|
||||||
:size="32"
|
|
||||||
class="bg-blue-500"
|
|
||||||
>
|
|
||||||
<component :is="useRenderIcon(RobotIcon)" />
|
|
||||||
</el-avatar>
|
|
||||||
<el-avatar
|
|
||||||
v-else
|
|
||||||
:size="32"
|
|
||||||
:src="'https://api.dicebear.com/7.x/avataaars/svg?seed=user'"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="message-content">
|
|
||||||
<div class="message-bubble" v-html="msg.content" />
|
|
||||||
<div class="message-time">{{ msg.time }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 快捷问题 -->
|
|
||||||
<div class="quick-questions">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
@click="
|
|
||||||
aiInput = '帮我分析一下结构施工阶段的流程卡点';
|
|
||||||
sendAiMessage();
|
|
||||||
"
|
|
||||||
>
|
|
||||||
帮我分析一下结构施工阶段的流程卡点
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 输入框 -->
|
|
||||||
<div class="ai-input-area">
|
|
||||||
<el-input
|
|
||||||
v-model="aiInput"
|
|
||||||
placeholder="请输入您的问题..."
|
|
||||||
@keyup.enter="sendAiMessage"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-button type="primary" @click="sendAiMessage">
|
|
||||||
发送
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -715,137 +798,21 @@ onMounted(() => {
|
|||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gantt-chart {
|
.gantt-chart-container {
|
||||||
position: relative;
|
:deep(.g-gantt-chart) {
|
||||||
}
|
background-color: #fafafa;
|
||||||
|
border-radius: 8px;
|
||||||
.gantt-timeline {
|
|
||||||
position: relative;
|
|
||||||
height: 30px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #e4e7ed;
|
|
||||||
|
|
||||||
.timeline-item {
|
|
||||||
position: absolute;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.gantt-tasks {
|
:deep(.g-gantt-row) {
|
||||||
.gantt-task-row {
|
&:hover {
|
||||||
display: flex;
|
background-color: #f0f2f5;
|
||||||
align-items: center;
|
|
||||||
height: 40px;
|
|
||||||
border-bottom: 1px solid #f0f2f5;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-name {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 100px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-bar-container {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-bar {
|
|
||||||
position: absolute;
|
|
||||||
height: 20px;
|
|
||||||
overflow: hidden;
|
|
||||||
background-color: #e6f2ff;
|
|
||||||
border: 1px solid #409eff;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
&.completed {
|
|
||||||
background-color: #e6f7e6;
|
|
||||||
border-color: #67c23a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.task-progress {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #409eff;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
|
|
||||||
.completed & {
|
|
||||||
background-color: #67c23a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// AI助手样式
|
:deep(.g-gantt-bar) {
|
||||||
.ai-card {
|
border-radius: 4px;
|
||||||
display: flex;
|
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||||||
flex-direction: column;
|
|
||||||
height: calc(100vh - 280px);
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-messages {
|
|
||||||
flex: 1;
|
|
||||||
padding: 16px;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.message-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
&.user {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
|
|
||||||
.message-content {
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-bubble {
|
|
||||||
color: white;
|
|
||||||
background-color: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-bubble {
|
|
||||||
padding: 10px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
background-color: #f5f7fa;
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
:deep(strong) {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-time {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,4 +829,92 @@ onMounted(() => {
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
border-top: 1px solid #e4e7ed;
|
border-top: 1px solid #e4e7ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 里程碑区域样式
|
||||||
|
.milestone-section {
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px dashed #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 里程碑时间线样式
|
||||||
|
.milestone-timeline {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
|
||||||
|
&.is-completed {
|
||||||
|
.milestone-content {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-marker {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-dot {
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-line {
|
||||||
|
flex: 1;
|
||||||
|
width: 2px;
|
||||||
|
min-height: 40px;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: linear-gradient(to bottom, #dcdfe6, #e4e7ed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-content {
|
||||||
|
flex: 1;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-date {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
|
||||||
|
.actual-date {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.milestone-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -128,38 +128,43 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
|
|||||||
<el-card shadow="hover" class="stat-card">
|
<el-card shadow="hover" class="stat-card">
|
||||||
<div class="flex-bc">
|
<div class="flex-bc">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 text-sm">进行中项目</p>
|
<p class="text-gray-500 text-sm">项目总数</p>
|
||||||
|
<p class="text-2xl font-bold mt-1">
|
||||||
|
{{ statistics.totalCount }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-blue-500 mt-1">
|
||||||
|
<el-icon
|
||||||
|
><component :is="useRenderIcon('ri/folder-line')"
|
||||||
|
/></el-icon>
|
||||||
|
包含各状态项目
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon bg-blue-100">
|
||||||
|
<el-icon :size="24" color="#409eff">
|
||||||
|
<component :is="useRenderIcon('ri/folders-line')" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="flex-bc">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500 text-sm">进行中</p>
|
||||||
<p class="text-2xl font-bold mt-1">
|
<p class="text-2xl font-bold mt-1">
|
||||||
{{ statistics.ongoingCount }}
|
{{ statistics.ongoingCount }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-green-500 mt-1">
|
<p class="text-xs text-green-500 mt-1">
|
||||||
<el-icon
|
<el-icon
|
||||||
><component :is="useRenderIcon('ri/arrow-up-line')"
|
><component :is="useRenderIcon('ri/play-circle-line')"
|
||||||
/></el-icon>
|
/></el-icon>
|
||||||
较上月增加2个
|
{{ statistics.planningCount }} 个规划中
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon bg-blue-100">
|
|
||||||
<el-icon :size="24" color="#409eff">
|
|
||||||
<component :is="useRenderIcon('ri/folder-line')" />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="24" :sm="12" :md="6">
|
|
||||||
<el-card shadow="hover" class="stat-card">
|
|
||||||
<div class="flex-bc">
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-500 text-sm">已完成项目</p>
|
|
||||||
<p class="text-2xl font-bold mt-1">
|
|
||||||
{{ statistics.completedCount }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-400 mt-1">本年度累计完成</p>
|
|
||||||
</div>
|
|
||||||
<div class="stat-icon bg-green-100">
|
<div class="stat-icon bg-green-100">
|
||||||
<el-icon :size="24" color="#67c23a">
|
<el-icon :size="24" color="#67c23a">
|
||||||
<component :is="useRenderIcon('ri/check-line')" />
|
<component :is="useRenderIcon('ri/play-circle-line')" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,41 +174,50 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
|
|||||||
<el-card shadow="hover" class="stat-card">
|
<el-card shadow="hover" class="stat-card">
|
||||||
<div class="flex-bc">
|
<div class="flex-bc">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-500 text-sm">高风险项目</p>
|
<p class="text-gray-500 text-sm">已完成</p>
|
||||||
<p class="text-2xl font-bold mt-1 text-orange-500">
|
|
||||||
{{ statistics.highRiskCount }}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-orange-400 mt-1">需要重点关注</p>
|
|
||||||
</div>
|
|
||||||
<div class="stat-icon bg-orange-100">
|
|
||||||
<el-icon :size="24" color="#e6a23c">
|
|
||||||
<component :is="useRenderIcon('ri/alert-line')" />
|
|
||||||
</el-icon>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="24" :sm="12" :md="6">
|
|
||||||
<el-card shadow="hover" class="stat-card">
|
|
||||||
<div class="flex-bc">
|
|
||||||
<div>
|
|
||||||
<p class="text-gray-500 text-sm">平均完成率</p>
|
|
||||||
<p class="text-2xl font-bold mt-1">
|
<p class="text-2xl font-bold mt-1">
|
||||||
{{ Math.round(statistics.averageProgress || 0) }}%
|
{{ statistics.completedCount }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">
|
||||||
|
{{ statistics.pausedCount }} 个已暂停 |
|
||||||
|
{{ statistics.cancelledCount }} 个已取消
|
||||||
</p>
|
</p>
|
||||||
<el-progress
|
|
||||||
:percentage="Math.round(statistics.averageProgress || 0)"
|
|
||||||
:show-text="false"
|
|
||||||
class="mt-2"
|
|
||||||
style="width: 100px"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon bg-purple-100">
|
<div class="stat-icon bg-purple-100">
|
||||||
<el-icon :size="24" color="#9b59b6">
|
<el-icon :size="24" color="#9b59b6">
|
||||||
|
<component :is="useRenderIcon('ri/check-double-line')" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<el-card shadow="hover" class="stat-card">
|
||||||
|
<div class="flex-bc">
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-500 text-sm">平均进度</p>
|
||||||
|
<p class="text-2xl font-bold mt-1">
|
||||||
|
{{ Math.round(statistics.averageProgress || 0) }}%
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-orange-400 mt-1">
|
||||||
|
<el-icon
|
||||||
|
><component :is="useRenderIcon('ri/alert-line')"
|
||||||
|
/></el-icon>
|
||||||
|
{{ statistics.highRiskCount }} 个高风险项目
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-icon bg-orange-100">
|
||||||
|
<el-icon :size="24" color="#e6a23c">
|
||||||
<component :is="useRenderIcon('ri/bar-chart-line')" />
|
<component :is="useRenderIcon('ri/bar-chart-line')" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<el-progress
|
||||||
|
:percentage="Math.round(statistics.averageProgress || 0)"
|
||||||
|
:show-text="false"
|
||||||
|
class="mt-2"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
Reference in New Issue
Block a user