diff --git a/src/views/project/detail.vue b/src/views/project/detail.vue index 9bdcf9f..3a7fd1c 100644 --- a/src/views/project/detail.vue +++ b/src/views/project/detail.vue @@ -11,6 +11,7 @@ import { type ProjectResource, type ProjectRisk } from "@/api/project"; +import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic"; import { message } from "@/utils/message"; import dayjs from "dayjs"; @@ -195,24 +196,103 @@ function getMaterialStatusType( } } -// 计算甘特图任务条样式(使用真实任务数据) -function getTaskBarStyle(task: ProjectTask) { - if (!task.planStartDate || !task.planEndDate) return {}; - const start = dayjs(task.planStartDate); - const end = dayjs(task.planEndDate); - const projectStart = dayjs(projectInfo.value.startDate); - const projectEnd = dayjs(projectInfo.value.endDate); - if (!projectStart.isValid() || !projectEnd.isValid()) return {}; - const totalDays = projectEnd.diff(projectStart, "day"); - const offsetDays = start.diff(projectStart, "day"); - const duration = end.diff(start, "day") + 1; +// 将任务数据转换为 vue-ganttastic 格式 +const ganttTasks = computed(() => { + return taskList.value.map(task => ({ + id: task.id, + label: task.taskName, + startDate: task.planStartDate || "", + endDate: task.planEndDate || "", + progress: task.progress || 0, + assignee: task.assigneeName || "未分配", + status: task.status || "pending", + 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}%`, - width: `${(duration / totalDays) * 100}%` - }; +// 获取任务颜色 +function getTaskColor(status?: string, progress?: number): string { + 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 = { + 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() { if (!projectId.value) return; @@ -410,7 +490,9 @@ onMounted(() => { { - + @@ -715,137 +798,21 @@ onMounted(() => { min-height: 300px; } -.gantt-chart { - position: relative; -} - -.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-chart-container { + :deep(.g-gantt-chart) { + background-color: #fafafa; + border-radius: 8px; } -} -.gantt-tasks { - .gantt-task-row { - display: flex; - 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; - } - } + :deep(.g-gantt-row) { + &:hover { + background-color: #f0f2f5; } } -} -// AI助手样式 -.ai-card { - display: flex; - 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; - } + :deep(.g-gantt-bar) { + border-radius: 4px; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); } } @@ -862,4 +829,92 @@ onMounted(() => { padding: 12px 16px; 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; +} diff --git a/src/views/project/index.vue b/src/views/project/index.vue index a3cbae3..eb8db97 100644 --- a/src/views/project/index.vue +++ b/src/views/project/index.vue @@ -128,38 +128,43 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
-

进行中项目

+

项目总数

+

+ {{ statistics.totalCount }} +

+

+ + 包含各状态项目 +

+
+
+ + + +
+
+
+
+ + +
+
+

进行中

{{ statistics.ongoingCount }}

- 较上月增加2个 + {{ statistics.planningCount }} 个规划中

-
- - - -
-
-
-
- - -
-
-

已完成项目

-

- {{ statistics.completedCount }} -

-

本年度累计完成

-
- +
@@ -169,41 +174,50 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
-

高风险项目

-

- {{ statistics.highRiskCount }} -

-

需要重点关注

-
-
- - - -
-
-
-
- - -
-
-

平均完成率

+

已完成

- {{ Math.round(statistics.averageProgress || 0) }}% + {{ statistics.completedCount }} +

+

+ {{ statistics.pausedCount }} 个已暂停 | + {{ statistics.cancelledCount }} 个已取消

-
+ + +
+
+
+
+ + +
+
+

平均进度

+

+ {{ Math.round(statistics.averageProgress || 0) }}% +

+

+ + {{ statistics.highRiskCount }} 个高风险项目 +

+
+
+
+