Some checks failed
Lint Code / Lint Code (push) Failing after 9m13s
- 新增风险与工单相关多语言菜单项(英文和中文) - 定义风险相关类型,包括风险分类、风险等级和状态等 - 定义工单相关类型,包括工单类型、优先级及状态等 - 实现风险评估创建、更新、删除、查询及统计接口 - 实现工单创建、更新、删除、查询、处理和分配接口 - 支持批量更新风险状态接口 - 新增我的工单列表及统计接口 - 提供统一的响应结果类型定义 - 更新OpenAPI规范文件以支持新增接口
997 lines
28 KiB
Vue
997 lines
28 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, computed } from "vue";
|
||
import { useRoute, useRouter } from "vue-router";
|
||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||
import {
|
||
getProjectDetail,
|
||
type ProjectDetail,
|
||
type ProjectMember,
|
||
type ProjectMilestone,
|
||
type ProjectTask,
|
||
type ProjectResource,
|
||
type ProjectRisk
|
||
} from "@/api/project";
|
||
import { GGanttChart, GGanttRow } from "@infectoone/vue-ganttastic";
|
||
import { message } from "@/utils/message";
|
||
import dayjs from "dayjs";
|
||
import isoWeek from "dayjs/plugin/isoWeek";
|
||
|
||
// 启用 isoWeek 插件,vue-ganttastic 周精度需要
|
||
dayjs.extend(isoWeek);
|
||
|
||
import ArrowLeftIcon from "~icons/ri/arrow-left-line";
|
||
import DownloadIcon from "~icons/ri/download-line";
|
||
import EditIcon from "~icons/ri/edit-line";
|
||
import UserIcon from "~icons/ri/user-line";
|
||
import AlertIcon from "~icons/ri/alert-line";
|
||
import RobotIcon from "~icons/ri/robot-2-line";
|
||
import RefreshIcon from "~icons/ri/refresh-line";
|
||
import FullscreenIcon from "~icons/ri/fullscreen-line";
|
||
import FileListIcon from "~icons/ri/file-list-line";
|
||
import ArrowRightIcon from "~icons/ri/arrow-right-s-line";
|
||
import CheckIcon from "~icons/ri/check-line";
|
||
|
||
defineOptions({
|
||
name: "ProjectDetail"
|
||
});
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const projectId = ref<string>(route.params.id as string);
|
||
|
||
// 加载状态
|
||
const loading = ref(false);
|
||
const ganttLoading = ref(false);
|
||
|
||
// 边距设置
|
||
const marginSettings = ref({
|
||
top: 16,
|
||
right: 80,
|
||
bottom: 16,
|
||
left: 16
|
||
});
|
||
|
||
// 计算边距样式
|
||
const marginStyle = computed(() => ({
|
||
padding: `${marginSettings.value.top}px ${marginSettings.value.right}px ${marginSettings.value.bottom}px ${marginSettings.value.left}px`
|
||
}));
|
||
|
||
// 项目详情数据
|
||
const projectDetail = ref<ProjectDetail | null>(null);
|
||
|
||
// 项目基本信息(计算属性)
|
||
const projectInfo = computed(() => {
|
||
const data = projectDetail.value;
|
||
return {
|
||
id: data?.id || projectId.value,
|
||
projectName: data?.projectName || "",
|
||
projectCode: data?.projectCode || "",
|
||
status: data?.status || "ongoing",
|
||
statusText: getStatusText(data?.status),
|
||
startDate: data?.planStartDate || "",
|
||
endDate: data?.planEndDate || "",
|
||
progress: data?.progress || 0,
|
||
progressChange: 0,
|
||
budget: data?.budget || 0,
|
||
cost: data?.cost || 0,
|
||
teamSize: data?.memberCount || 0,
|
||
riskLevel: data?.riskLevel || "low",
|
||
riskText: getRiskText(data?.riskLevel),
|
||
riskCount: data?.riskCount || 0,
|
||
description: data?.description || "",
|
||
objectives: data?.objectives || ""
|
||
};
|
||
});
|
||
|
||
// 甘特图数据(使用任务列表)
|
||
|
||
// AI助手消息
|
||
const aiMessages = ref([
|
||
{
|
||
type: "ai",
|
||
content:
|
||
"您好!我是项目AI助手,我可以帮您分析项目进度和流程卡点。请问有什么可以帮助您的?",
|
||
time: "上午 9:30"
|
||
},
|
||
{
|
||
type: "user",
|
||
content: "当前项目进度如何?有没有什么风险点需要注意?",
|
||
time: "上午 9:32"
|
||
},
|
||
{
|
||
type: "ai",
|
||
content: `**项目进度分析:** 当前项目整体进度为68%,比计划进度超前5%。
|
||
|
||
**关键风险点:**
|
||
• 玻璃幕墙材料尚未发货,可能影响后续安装工序
|
||
• 水电安装工序存在资源调配问题,可能导致延期
|
||
• 室内装修团队人力不足,建议增加2名熟练工人
|
||
|
||
**建议措施:** 优先跟进玻璃幕墙供应商发货情况,协调水电安装资源,考虑从其他项目临时调配装修工人。`,
|
||
time: "上午 9:33"
|
||
}
|
||
]);
|
||
|
||
const aiInput = ref("");
|
||
|
||
// 资源清单数据(从API获取)
|
||
const resourceList = computed(() => {
|
||
return projectDetail.value?.resources || [];
|
||
});
|
||
|
||
// 风险列表数据(从API获取)
|
||
const riskList = computed(() => {
|
||
return projectDetail.value?.risks || [];
|
||
});
|
||
|
||
// 成员列表数据(从API获取)
|
||
const memberList = computed(() => {
|
||
return projectDetail.value?.members || [];
|
||
});
|
||
|
||
// 里程碑列表数据(从API获取)
|
||
const milestoneList = computed(() => {
|
||
return projectDetail.value?.milestones || [];
|
||
});
|
||
|
||
// 任务列表数据(从API获取,用于甘特图)
|
||
const taskList = computed(() => {
|
||
return projectDetail.value?.tasks || [];
|
||
});
|
||
|
||
// 返回上一页
|
||
function goBack() {
|
||
router.push("/project");
|
||
}
|
||
|
||
// 获取甘特图数据(现在使用项目详情中的任务数据)
|
||
async function fetchGanttData() {
|
||
// 任务数据已从项目详情API获取,无需单独请求
|
||
ganttLoading.value = true;
|
||
try {
|
||
// 模拟加载延迟
|
||
await new Promise(resolve => setTimeout(resolve, 300));
|
||
} catch (error) {
|
||
console.error("获取甘特图数据失败:", error);
|
||
} finally {
|
||
ganttLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 发送AI消息
|
||
function sendAiMessage() {
|
||
if (!aiInput.value.trim()) return;
|
||
const userMsg = aiInput.value.trim();
|
||
aiMessages.value.push({
|
||
type: "user",
|
||
content: userMsg,
|
||
time: dayjs().format("HH:mm")
|
||
});
|
||
aiInput.value = "";
|
||
|
||
// 模拟AI回复
|
||
setTimeout(() => {
|
||
aiMessages.value.push({
|
||
type: "ai",
|
||
content: "收到您的问题,我正在分析项目数据,请稍候...",
|
||
time: dayjs().format("HH:mm")
|
||
});
|
||
}, 500);
|
||
}
|
||
|
||
// 获取状态标签类型
|
||
function getStatusType(
|
||
status?: string
|
||
): "success" | "warning" | "info" | "primary" | "danger" {
|
||
switch (status) {
|
||
case "completed":
|
||
return "success";
|
||
case "ongoing":
|
||
return "primary";
|
||
case "paused":
|
||
return "warning";
|
||
case "cancelled":
|
||
return "danger";
|
||
default:
|
||
return "info";
|
||
}
|
||
}
|
||
|
||
// 获取物料状态类型
|
||
function getMaterialStatusType(
|
||
status: string
|
||
): "success" | "warning" | "info" | "primary" | "danger" {
|
||
switch (status) {
|
||
case "arrived":
|
||
return "success";
|
||
case "pending":
|
||
return "info";
|
||
case "delayed":
|
||
return "danger";
|
||
default:
|
||
return "info";
|
||
}
|
||
}
|
||
|
||
// 将任务数据转换为 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"
|
||
}
|
||
}
|
||
}));
|
||
});
|
||
|
||
// 获取任务颜色
|
||
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<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")
|
||
};
|
||
});
|
||
|
||
// 计算项目时间跨度(天数)
|
||
const projectDays = computed(() => {
|
||
const start = dayjs(ganttDateRange.value.start);
|
||
const end = dayjs(ganttDateRange.value.end);
|
||
return end.diff(start, "day");
|
||
});
|
||
|
||
// 动态计算甘特图精度:超过两个月(60天)使用周精度,否则使用日精度
|
||
const ganttPrecision = computed(() => {
|
||
return projectDays.value > 60 ? "week" : "day";
|
||
});
|
||
|
||
// 获取项目详情
|
||
async function fetchProjectDetail() {
|
||
if (!projectId.value) return;
|
||
loading.value = true;
|
||
try {
|
||
const res = await getProjectDetail(projectId.value);
|
||
const result = res as any;
|
||
if (result.code === 200 && result.data) {
|
||
projectDetail.value = result.data;
|
||
}
|
||
} catch (error) {
|
||
console.error("获取项目详情失败:", error);
|
||
message("获取项目详情失败", { type: "error" });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// 获取状态文本
|
||
function getStatusText(status?: string): string {
|
||
const statusMap: Record<string, string> = {
|
||
ongoing: "进行中",
|
||
completed: "已完成",
|
||
paused: "已暂停",
|
||
cancelled: "已取消",
|
||
planning: "规划中",
|
||
draft: "草稿"
|
||
};
|
||
return statusMap[status || ""] || "未知";
|
||
}
|
||
|
||
// 获取风险文本
|
||
function getRiskText(risk?: string): string {
|
||
const riskMap: Record<string, string> = {
|
||
low: "低",
|
||
medium: "中等",
|
||
high: "高"
|
||
};
|
||
return riskMap[risk || ""] || "低";
|
||
}
|
||
|
||
// 获取角色文本
|
||
function getRoleText(roleCode?: string): string {
|
||
const roleMap: Record<string, string> = {
|
||
manager: "项目经理",
|
||
leader: "负责人",
|
||
member: "成员",
|
||
sponsor: "发起人"
|
||
};
|
||
return roleMap[roleCode || ""] || roleCode || "成员";
|
||
}
|
||
|
||
// 获取资源类型文本
|
||
function getResourceTypeText(type?: string): string {
|
||
const typeMap: Record<string, string> = {
|
||
equipment: "设备",
|
||
material: "物料",
|
||
human: "人力",
|
||
service: "服务"
|
||
};
|
||
return typeMap[type || ""] || type || "其他";
|
||
}
|
||
|
||
// 获取资源状态类型
|
||
function getResourceStatusType(
|
||
status?: string
|
||
): "success" | "warning" | "info" | "primary" | "danger" {
|
||
switch (status) {
|
||
case "arrived":
|
||
case "actual":
|
||
return "success";
|
||
case "planned":
|
||
return "info";
|
||
case "delayed":
|
||
return "danger";
|
||
default:
|
||
return "info";
|
||
}
|
||
}
|
||
|
||
// 获取资源状态文本
|
||
function getResourceStatusText(status?: string): string {
|
||
const statusMap: Record<string, string> = {
|
||
arrived: "已到货",
|
||
actual: "实际",
|
||
planned: "计划中",
|
||
delayed: "延期"
|
||
};
|
||
return statusMap[status || ""] || status || "计划中";
|
||
}
|
||
|
||
// 获取风险等级标签类型
|
||
function getRiskLevelType(
|
||
level?: string
|
||
): "success" | "warning" | "danger" | "info" {
|
||
switch (level) {
|
||
case "low":
|
||
return "success";
|
||
case "medium":
|
||
return "warning";
|
||
case "high":
|
||
return "danger";
|
||
default:
|
||
return "info";
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchProjectDetail();
|
||
fetchGanttData();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div class="project-detail w-full" :style="marginStyle">
|
||
<!-- 顶部导航 -->
|
||
<div class="flex-bc mb-4">
|
||
<div class="flex items-center gap-3">
|
||
<el-button link @click="goBack">
|
||
<component :is="useRenderIcon(ArrowLeftIcon)" />
|
||
</el-button>
|
||
<div>
|
||
<div class="flex items-center gap-2">
|
||
<h2 class="text-xl font-bold">{{ projectInfo.projectName }}</h2>
|
||
<el-tag :type="getStatusType(projectInfo.status)" size="small">
|
||
{{ projectInfo.statusText }}
|
||
</el-tag>
|
||
</div>
|
||
<p class="text-gray-500 text-sm mt-1">
|
||
项目编号: {{ projectInfo.projectCode }} | 开始日期:
|
||
{{ projectInfo.startDate }} | 预计结束日期:
|
||
{{ projectInfo.endDate }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<el-button>
|
||
<template #icon>
|
||
<component :is="useRenderIcon(DownloadIcon)" />
|
||
</template>
|
||
导出报告
|
||
</el-button>
|
||
<el-button type="primary">
|
||
<template #icon>
|
||
<component :is="useRenderIcon(EditIcon)" />
|
||
</template>
|
||
编辑项目
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计卡片 -->
|
||
<el-row :gutter="16" class="mb-4">
|
||
<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">{{ projectInfo.progress }}%</p>
|
||
<p class="text-xs text-green-500 mt-1">
|
||
<el-icon
|
||
><component :is="useRenderIcon('ri/arrow-up-line')"
|
||
/></el-icon>
|
||
{{ projectInfo.progressChange }}%
|
||
</p>
|
||
</div>
|
||
<div class="stat-icon bg-blue-100">
|
||
<el-icon :size="24" color="#409eff">
|
||
<component :is="useRenderIcon('ri/bar-chart-line')" />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
<el-progress
|
||
:percentage="projectInfo.progress"
|
||
:status="projectInfo.progress === 100 ? 'success' : ''"
|
||
class="mt-3"
|
||
/>
|
||
</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">¥{{ projectInfo.cost }}万</p>
|
||
<p class="text-xs text-gray-400 mt-1">
|
||
/ ¥{{ projectInfo.budget }}万
|
||
</p>
|
||
</div>
|
||
<div class="stat-icon bg-orange-100">
|
||
<el-icon :size="24" color="#e6a23c">
|
||
<component :is="useRenderIcon('ri/money-cny-circle-line')" />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
<el-progress
|
||
:percentage="
|
||
projectInfo.budget > 0
|
||
? Math.round((projectInfo.cost / projectInfo.budget) * 100)
|
||
: 0
|
||
"
|
||
status="warning"
|
||
class="mt-3"
|
||
/>
|
||
</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">
|
||
{{ projectInfo.teamSize }}人
|
||
</p>
|
||
<p class="text-xs text-gray-400 mt-1">项目参与人员</p>
|
||
</div>
|
||
<div class="stat-icon bg-green-100">
|
||
<el-icon :size="24" color="#67c23a">
|
||
<component :is="useRenderIcon(UserIcon)" />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
<div class="flex -space-x-2 mt-3">
|
||
<el-avatar
|
||
v-for="member in memberList.slice(0, 4)"
|
||
:key="member.id"
|
||
:size="28"
|
||
:src="
|
||
member.avatar ||
|
||
`https://api.dicebear.com/7.x/avataaars/svg?seed=${member.id}`
|
||
"
|
||
:title="getRoleText(member.roleCode)"
|
||
/>
|
||
<el-avatar
|
||
v-if="memberList.length > 4"
|
||
:size="28"
|
||
class="bg-gray-200"
|
||
>
|
||
+{{ memberList.length - 4 }}
|
||
</el-avatar>
|
||
</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 text-orange-500">
|
||
{{ projectInfo.riskText }}
|
||
</p>
|
||
<p class="text-xs text-orange-400 mt-1">
|
||
<component :is="useRenderIcon(AlertIcon)" class="inline" />
|
||
{{ projectInfo.riskCount }}个潜在风险需要关注
|
||
</p>
|
||
</div>
|
||
<div class="stat-icon bg-orange-100">
|
||
<el-icon :size="24" color="#e6a23c">
|
||
<component :is="useRenderIcon(AlertIcon)" />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 主要内容区 -->
|
||
<el-row :gutter="16">
|
||
<!-- 左侧:甘特图和物料清单 -->
|
||
<el-col :xs="24" :lg="24">
|
||
<!-- 甘特图 -->
|
||
<el-card shadow="hover" class="mb-4">
|
||
<template #header>
|
||
<div class="flex-bc">
|
||
<span class="font-medium">项目进度甘特图</span>
|
||
<div class="flex gap-2">
|
||
<el-button link @click="fetchGanttData">
|
||
<component :is="useRenderIcon(RefreshIcon)" />
|
||
</el-button>
|
||
<el-button link>
|
||
<component :is="useRenderIcon(FullscreenIcon)" />
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div v-loading="ganttLoading" class="gantt-container">
|
||
<!-- 图例 -->
|
||
<div class="flex gap-4 mb-4 text-sm">
|
||
<div class="flex items-center gap-1">
|
||
<div class="w-4 h-3 bg-blue-400 rounded-sm" />
|
||
<span>计划进度</span>
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<div class="w-4 h-3 bg-green-400 rounded-sm" />
|
||
<span>实际进度</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 任务甘特图主体 - 使用 vue-ganttastic -->
|
||
<div v-if="taskList.length > 0" class="gantt-chart-container mb-6">
|
||
<g-gantt-chart
|
||
:chart-start="ganttDateRange.start"
|
||
:chart-end="ganttDateRange.end"
|
||
:precision="ganttPrecision"
|
||
date-format="YYYY-MM-DD"
|
||
bar-start="startDate"
|
||
bar-end="endDate"
|
||
grid
|
||
highlight-on-hover
|
||
>
|
||
<g-gantt-row
|
||
v-for="task in ganttTasks"
|
||
:key="task.id"
|
||
: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>
|
||
<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
|
||
class="milestone-dot"
|
||
:style="{
|
||
backgroundColor: getMilestoneColor(
|
||
milestone.status,
|
||
milestone.isKey
|
||
)
|
||
}"
|
||
>
|
||
<el-icon
|
||
v-if="milestone.status === 'completed'"
|
||
: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>
|
||
</el-card>
|
||
|
||
<!-- 物料清单 -->
|
||
<el-card shadow="hover">
|
||
<template #header>
|
||
<div class="flex-bc">
|
||
<div class="flex items-center gap-2">
|
||
<component :is="useRenderIcon(FileListIcon)" />
|
||
<span class="font-medium">项目物料清单</span>
|
||
</div>
|
||
<el-button link type="primary">
|
||
查看全部
|
||
<component :is="useRenderIcon(ArrowRightIcon)" />
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
<el-table :data="resourceList" style="width: 100%">
|
||
<el-table-column label="资源名称" min-width="200">
|
||
<template #default="{ row }">
|
||
<div class="flex items-center gap-2">
|
||
<el-avatar :size="32" shape="square" class="bg-gray-100">
|
||
<component :is="useRenderIcon('ri/box-line')" />
|
||
</el-avatar>
|
||
<div>
|
||
<div class="font-medium">{{ row.resourceName }}</div>
|
||
<div class="text-xs text-gray-400">
|
||
{{ getResourceTypeText(row.resourceType) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="规格" prop="specification" width="120" />
|
||
<el-table-column label="数量" width="100">
|
||
<template #default="{ row }">
|
||
{{ row.planQuantity }} {{ row.unit }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" width="100">
|
||
<template #default="{ row }">
|
||
<el-tag :type="getResourceStatusType(row.status)" size="small">
|
||
{{ getResourceStatusText(row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="预计到货" width="120">
|
||
<template #default="{ row }">
|
||
<div class="flex items-center gap-1">
|
||
<span>{{ row.planArriveDate || "--" }}</span>
|
||
<el-icon
|
||
v-if="row.status === 'arrived' || row.status === 'actual'"
|
||
color="#67c23a"
|
||
class="ml-1"
|
||
>
|
||
<component :is="useRenderIcon(CheckIcon)" />
|
||
</el-icon>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="80" fixed="right">
|
||
<template #default>
|
||
<el-button link type="primary" size="small">详情</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped lang="scss">
|
||
.project-detail {
|
||
padding: 16px;
|
||
}
|
||
|
||
.stat-card {
|
||
.stat-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 8px;
|
||
}
|
||
}
|
||
|
||
// 甘特图样式
|
||
.gantt-container {
|
||
min-height: 300px;
|
||
}
|
||
|
||
.gantt-chart-container {
|
||
:deep(.g-gantt-chart) {
|
||
background-color: #fafafa;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
:deep(.g-gantt-row) {
|
||
&:hover {
|
||
background-color: #f0f2f5;
|
||
}
|
||
}
|
||
|
||
:deep(.g-gantt-bar) {
|
||
border-radius: 4px;
|
||
box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
|
||
}
|
||
}
|
||
|
||
.quick-questions {
|
||
padding: 8px 16px;
|
||
border-top: 1px solid #e4e7ed;
|
||
|
||
.el-button {
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
|
||
.ai-input-area {
|
||
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;
|
||
}
|
||
|
||
// 边距设置按钮
|
||
.margin-setting-btn {
|
||
position: fixed;
|
||
top: 80px;
|
||
right: 20px;
|
||
z-index: 100;
|
||
width: 36px;
|
||
height: 36px;
|
||
background-color: #fff;
|
||
border-radius: 50%;
|
||
box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
|
||
|
||
&:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
}
|
||
|
||
// 边距设置面板
|
||
.margin-setting-panel {
|
||
padding: 8px;
|
||
|
||
.setting-title {
|
||
margin-bottom: 12px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
text-align: center;
|
||
}
|
||
|
||
.setting-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
|
||
span {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
}
|
||
|
||
.el-button {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
}
|
||
}
|
||
</style>
|