Files
ylhp-ai-project-manager-fro…/src/views/project/detail.vue
JiaoTianBo 16f466f666
Some checks failed
Lint Code / Lint Code (push) Failing after 9m13s
feat(api): 新增风险与工单管理接口及多语言支持
- 新增风险与工单相关多语言菜单项(英文和中文)
- 定义风险相关类型,包括风险分类、风险等级和状态等
- 定义工单相关类型,包括工单类型、优先级及状态等
- 实现风险评估创建、更新、删除、查询及统计接口
- 实现工单创建、更新、删除、查询、处理和分配接口
- 支持批量更新风险状态接口
- 新增我的工单列表及统计接口
- 提供统一的响应结果类型定义
- 更新OpenAPI规范文件以支持新增接口
2026-03-30 14:20:01 +08:00

997 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>