feat(project): 支持项目详情页面及相关类型定义
Some checks failed
Lint Code / Lint Code (push) Failing after 23m5s

- 将项目相关ID类型统一由number改为string,增强一致性
- 新增项目成员、里程碑、任务、资源、风险、时间线节点等详细类型定义
- 添加获取项目详情接口方法getProjectDetail
- 在路由中新增项目详情页路由配置
- 实现项目详情页面,支持展示基本信息、成员、任务、风险及资源等数据
- 项目详情页面集成AI助手简易聊天交互功能展示
- 添加项目状态、风险等级及资源状态的辅助文本和样式方法
- 优化甘特图任务条样式计算,基于项目详情任务数据展现
This commit is contained in:
2026-03-28 18:37:03 +08:00
parent c4509b42fa
commit cfa3a57a57
6 changed files with 1641 additions and 15 deletions

View File

@@ -11,7 +11,7 @@ type Result<T = any> = {
/** 项目列表项 - 根据 OpenAPI 定义 */
export type ProjectItem = {
id?: number;
id?: string;
projectCode?: string;
projectName?: string;
projectType?: string;
@@ -56,7 +56,7 @@ export const getProjectList = (params?: ProjectQueryParams) => {
};
/** 删除项目 */
export const deleteProject = (id: number) => {
export const deleteProject = (id: string) => {
return http.request<Result<void>>("delete", `/api/v1/project/${id}`);
};
@@ -90,7 +90,7 @@ export const getProjectStatistics = () => {
/** 甘特图任务 */
export type GanttTask = {
id?: number;
id?: string;
taskName?: string;
type?: string; // task-任务, milestone-里程碑
startDate?: string;
@@ -100,15 +100,15 @@ export type GanttTask = {
progress?: number;
status?: string;
priority?: string;
parentId?: number;
parentId?: string;
sortOrder?: number;
assigneeName?: string;
dependencies?: number[];
dependencies?: string[];
};
/** 项目甘特图数据 */
export type ProjectGantt = {
projectId?: number;
projectId?: string;
projectName?: string;
projectStatus?: string;
projectStartDate?: string;
@@ -119,13 +119,165 @@ export type ProjectGantt = {
};
/** 获取项目甘特图数据 */
export const getProjectGantt = (projectId: number) => {
export const getProjectGantt = (projectId: string) => {
return http.request<Result<ProjectGantt>>(
"get",
`/api/v1/project/${projectId}/gantt`
);
};
// ==================== 项目详情 ====================
/** 项目成员 */
export type ProjectMember = {
id: string;
userId?: string;
userName?: string;
realName?: string;
avatar?: string;
roleCode: string;
department?: string;
responsibility?: string;
weeklyHours?: number;
joinDate?: string;
status?: number;
};
/** 项目里程碑 */
export type ProjectMilestone = {
id: string;
milestoneName: string;
description?: string;
planDate?: string;
actualDate?: string;
status?: string;
progress?: number;
isKey?: number;
deliverables?: string;
sortOrder?: number;
};
/** 项目任务 */
export type ProjectTask = {
id: string;
taskCode?: string;
taskName: string;
description?: string;
taskType?: string;
milestoneId?: string;
assigneeId?: string;
assigneeName?: string;
planStartDate?: string;
planEndDate?: string;
actualStartDate?: string;
actualEndDate?: string;
planHours?: number;
actualHours?: number;
progress?: number;
priority?: string;
status?: string;
sortOrder?: number;
};
/** 项目资源 */
export type ProjectResource = {
id: string;
resourceCode?: string;
resourceType?: string;
resourceName: string;
description?: string;
specification?: string;
unit?: string;
planQuantity?: number;
actualQuantity?: number;
unitPrice?: number;
currency?: string;
supplier?: string;
status?: string;
planArriveDate?: string;
actualArriveDate?: string;
};
/** 项目风险 */
export type ProjectRisk = {
id: string;
riskCode?: string;
category?: string;
riskName: string;
description?: string;
probability?: number;
impact?: number;
riskScore?: number;
riskLevel?: string;
status?: string;
ownerId?: string;
mitigationPlan?: string;
dueDate?: string;
discoverTime?: string;
};
/** 时间线节点 */
export type TimelineNode = {
id: string;
nodeName: string;
nodeType?: string;
planDate?: string;
actualDate?: string;
description?: string;
status?: string;
sortOrder?: number;
kbScope?: string[];
};
/** 项目详情 */
export type ProjectDetail = {
id: string;
projectCode: string;
projectName: string;
projectType: string;
description?: string;
objectives?: string;
managerId?: string;
managerName?: string;
sponsorId?: string;
planStartDate?: string;
planEndDate?: string;
actualStartDate?: string;
actualEndDate?: string;
budget?: number;
cost?: number;
currency?: string;
progress?: number;
status?: string;
priority?: string;
riskLevel?: string;
visibility?: number;
tags?: string[];
createTime?: string;
updateTime?: string;
memberCount?: number;
taskCount?: number;
completedTaskCount?: number;
milestoneCount?: number;
resourceCount?: number;
riskCount?: number;
highRiskCount?: number;
members?: ProjectMember[];
milestones?: ProjectMilestone[];
tasks?: ProjectTask[];
resources?: ProjectResource[];
risks?: ProjectRisk[];
timelineNodes?: TimelineNode[];
};
/** 获取项目详情 */
export const getProjectDetail = (projectId: string) => {
return http.request<Result<ProjectDetail>>(
"get",
`/api/v1/project/${projectId}`
);
};
// ==================== 项目初始化(复用 system.ts 中的定义) ====================
/** 项目信息 */

View File

@@ -0,0 +1,594 @@
{
"code": 200,
"data": {
"id": "2037831996319100930",
"projectCode": "PRJ15F5BF5B5CAD",
"projectName": "AIHR 智能简历筛选系统",
"projectType": "研发项目",
"description": "利用大语言模型LLM和自然语言处理NLP技术实现简历的自动化解析、人岗匹配度评分及候选人排序提升招聘效率。",
"objectives": "1. 效率提升单份处理时间降至10秒内。2. 精准匹配:人岗匹配度评分准确率>90%。3. 系统集成无缝对接主流招聘网站及内部ATS。4. 合规安全:候选人数据脱敏,符合《个人信息保护法》。",
"managerId": null,
"managerName": null,
"sponsorId": null,
"planStartDate": "2026-04-01",
"planEndDate": "2026-09-30",
"actualStartDate": null,
"actualEndDate": null,
"budget": 2850000.0,
"cost": 0.0,
"currency": "CNY",
"progress": 0,
"status": "planning",
"priority": "high",
"riskLevel": "low",
"visibility": 1,
"tags": null,
"createTime": null,
"updateTime": null,
"memberCount": 4,
"taskCount": 8,
"completedTaskCount": 0,
"milestoneCount": 6,
"resourceCount": 6,
"riskCount": 4,
"highRiskCount": 0,
"members": [
{
"id": "2037831996608507907",
"userId": null,
"userName": null,
"realName": null,
"avatar": null,
"roleCode": "manager",
"department": null,
"responsibility": "负责整体项目规划、进度控制、风险管理及跨部门协调",
"weeklyHours": 40.0,
"joinDate": null,
"status": 1
},
{
"id": "2037831996646256642",
"userId": null,
"userName": null,
"realName": null,
"avatar": null,
"roleCode": "leader",
"department": null,
"responsibility": "负责需求梳理、原型设计、用户故事编写及验收测试",
"weeklyHours": 40.0,
"joinDate": null,
"status": 1
},
{
"id": "2037831996646256643",
"userId": null,
"userName": null,
"realName": null,
"avatar": null,
"roleCode": "leader",
"department": null,
"responsibility": "负责系统整体技术选型、架构设计、核心技术难点攻关",
"weeklyHours": 20.0,
"joinDate": null,
"status": 1
},
{
"id": "2037831996646256644",
"userId": null,
"userName": null,
"realName": null,
"avatar": null,
"roleCode": "member",
"department": null,
"responsibility": "负责测试用例编写、功能测试、性能测试及自动化测试脚本",
"weeklyHours": 40.0,
"joinDate": null,
"status": 1
}
],
"milestones": [
{
"id": "2037831996319100931",
"milestoneName": "需求分析与架构设计",
"description": "完成需求评审及技术架构可行性验证",
"planDate": "2026-04-30",
"actualDate": null,
"status": "pending",
"progress": 0,
"isKey": 0,
"deliverables": null,
"sortOrder": 0
},
{
"id": "2037831996319100932",
"milestoneName": "核心算法模型训练与验证",
"description": "模型在测试集上的准确率>85%,解析字段覆盖率>95%",
"planDate": "2026-06-15",
"actualDate": null,
"status": "pending",
"progress": 0,
"isKey": 0,
"deliverables": null,
"sortOrder": 1
},
{
"id": "2037831996319100933",
"milestoneName": "系统功能开发完成 (Alpha 版)",
"description": "核心功能(上传、解析、评分、搜索)闭环跑通",
"planDate": "2026-07-31",
"actualDate": null,
"status": "pending",
"progress": 0,
"isKey": 0,
"deliverables": null,
"sortOrder": 2
},
{
"id": "2037831996386209793",
"milestoneName": "系统集成与内部测试 (Beta 版)",
"description": "支持并发用户数>50响应时间<2秒无严重 Bug",
"planDate": "2026-08-31",
"actualDate": null,
"status": "pending",
"progress": 0,
"isKey": 0,
"deliverables": null,
"sortOrder": 3
},
{
"id": "2037831996386209794",
"milestoneName": "用户验收测试 (UAT) 与试点",
"description": "试点部门(人力资源部)确认功能满足业务需求",
"planDate": "2026-09-15",
"actualDate": null,
"status": "pending",
"progress": 0,
"isKey": 0,
"deliverables": null,
"sortOrder": 4
},
{
"id": "2037831996386209795",
"milestoneName": "正式上线与交付",
"description": "系统正式部署至生产环境,完成全员培训",
"planDate": "2026-09-30",
"actualDate": null,
"status": "pending",
"progress": 0,
"isKey": 0,
"deliverables": null,
"sortOrder": 5
}
],
"tasks": [
{
"id": "2037831996386209796",
"taskCode": null,
"taskName": "需求分析与产品设计",
"description": "梳理业务需求,输出原型设计与需求规格说明书",
"taskType": null,
"milestoneId": "2037831996319100931",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-04-01",
"planEndDate": "2026-04-20",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 120.0,
"actualHours": null,
"progress": 0,
"priority": "high",
"status": "pending",
"sortOrder": 0
},
{
"id": "2037831996449124353",
"taskCode": null,
"taskName": "系统架构与数据库设计",
"description": "完成技术选型、架构设计及数据库表结构设计",
"taskType": null,
"milestoneId": "2037831996319100932",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-04-15",
"planEndDate": "2026-04-30",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 80.0,
"actualHours": null,
"progress": 0,
"priority": "high",
"status": "pending",
"sortOrder": 1
},
{
"id": "2037831996449124354",
"taskCode": null,
"taskName": "数据准备与标注",
"description": "处理10万份历史简历并完成5000对简历-JD匹配标注",
"taskType": null,
"milestoneId": "2037831996319100933",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-04-20",
"planEndDate": "2026-05-20",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 160.0,
"actualHours": null,
"progress": 0,
"priority": "high",
"status": "pending",
"sortOrder": 2
},
{
"id": "2037831996449124355",
"taskCode": null,
"taskName": "核心算法模型训练",
"description": "基于大模型进行简历解析引擎与匹配算法的训练和调优",
"taskType": null,
"milestoneId": "2037831996386209793",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-05-01",
"planEndDate": "2026-06-15",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 240.0,
"actualHours": null,
"progress": 0,
"priority": "high",
"status": "pending",
"sortOrder": 3
},
{
"id": "2037831996449124356",
"taskCode": null,
"taskName": "系统前后端功能开发",
"description": "开发后端服务及前端管理界面并完成API对接",
"taskType": null,
"milestoneId": "2037831996386209794",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-06-01",
"planEndDate": "2026-07-31",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 320.0,
"actualHours": null,
"progress": 0,
"priority": "high",
"status": "pending",
"sortOrder": 4
},
{
"id": "2037831996449124357",
"taskCode": null,
"taskName": "系统集成与内测",
"description": "系统整体集成、性能测试及缺陷修复",
"taskType": null,
"milestoneId": "2037831996386209795",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-08-01",
"planEndDate": "2026-08-31",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 160.0,
"actualHours": null,
"progress": 0,
"priority": "high",
"status": "pending",
"sortOrder": 5
},
{
"id": "2037831996449124358",
"taskCode": null,
"taskName": "用户验收测试与试点",
"description": "人力资源部介入试用系统,收集反馈并进行优化",
"taskType": null,
"milestoneId": "2037831996319100931",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-09-01",
"planEndDate": "2026-09-15",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 80.0,
"actualHours": null,
"progress": 0,
"priority": "medium",
"status": "pending",
"sortOrder": 6
},
{
"id": "2037831996516233217",
"taskCode": null,
"taskName": "生产部署与交付",
"description": "系统部署上线,组织全员培训,输出总结报告",
"taskType": null,
"milestoneId": "2037831996319100932",
"assigneeId": null,
"assigneeName": null,
"planStartDate": "2026-09-16",
"planEndDate": "2026-09-30",
"actualStartDate": null,
"actualEndDate": null,
"planHours": 80.0,
"actualHours": null,
"progress": 0,
"priority": "high",
"status": "pending",
"sortOrder": 7
}
],
"resources": [
{
"id": "2037831996646256645",
"resourceCode": null,
"resourceType": "equipment",
"resourceName": "高性能云服务器 (16核64G)",
"description": null,
"specification": null,
"unit": "台",
"planQuantity": 5.0,
"actualQuantity": null,
"unitPrice": 2000.0,
"currency": "CNY",
"supplier": "云服务提供商",
"status": "planned",
"planArriveDate": null,
"actualArriveDate": null,
"responsibleId": null,
"location": null,
"tags": null
},
{
"id": "2037831996709171202",
"resourceCode": null,
"resourceType": "equipment",
"resourceName": "云端 GPU 算力资源 (NVIDIA A100/A800)",
"description": null,
"specification": null,
"unit": "小时",
"planQuantity": 2000.0,
"actualQuantity": null,
"unitPrice": 50.0,
"currency": "CNY",
"supplier": "云算力平台",
"status": "planned",
"planArriveDate": null,
"actualArriveDate": null,
"responsibleId": null,
"location": null,
"tags": null
},
{
"id": "2037831996709171203",
"resourceCode": null,
"resourceType": "equipment",
"resourceName": "对象存储空间",
"description": null,
"specification": null,
"unit": "TB",
"planQuantity": 5.0,
"actualQuantity": null,
"unitPrice": 300.0,
"currency": "CNY",
"supplier": "云服务提供商",
"status": "planned",
"planArriveDate": null,
"actualArriveDate": null,
"responsibleId": null,
"location": null,
"tags": null
},
{
"id": "2037831996709171204",
"resourceCode": null,
"resourceType": "material",
"resourceName": "历史简历数据",
"description": null,
"specification": null,
"unit": "份",
"planQuantity": 100000.0,
"actualQuantity": null,
"unitPrice": 0.0,
"currency": "CNY",
"supplier": "企业内部",
"status": "planned",
"planArriveDate": null,
"actualArriveDate": null,
"responsibleId": null,
"location": null,
"tags": null
},
{
"id": "2037831996709171205",
"resourceCode": null,
"resourceType": "material",
"resourceName": "高质量匹配标注数据集",
"description": null,
"specification": null,
"unit": "对",
"planQuantity": 5000.0,
"actualQuantity": null,
"unitPrice": 10.0,
"currency": "CNY",
"supplier": "数据标注专员",
"status": "planned",
"planArriveDate": null,
"actualArriveDate": null,
"responsibleId": null,
"location": null,
"tags": null
},
{
"id": "2037831996709171206",
"resourceCode": null,
"resourceType": "human",
"resourceName": "法律合规咨询服务",
"description": null,
"specification": null,
"unit": "项",
"planQuantity": 1.0,
"actualQuantity": null,
"unitPrice": 50000.0,
"currency": "CNY",
"supplier": "外部法律顾问",
"status": "planned",
"planArriveDate": null,
"actualArriveDate": null,
"responsibleId": null,
"location": null,
"tags": null
}
],
"risks": [
{
"id": "2037831996780474370",
"riskCode": null,
"category": "technical",
"riskName": "模型准确率不达标风险",
"description": "大语言模型可能出现幻觉或对复杂简历解析不准导致人岗匹配准确率低于90%",
"probability": 40.0,
"impact": 4.0,
"riskScore": 1.6,
"riskLevel": "medium",
"status": "identified",
"ownerId": null,
"mitigationPlan": "引入高质量标注数据集微调模型,增加人工专家复核机制,持续优化算法",
"dueDate": null,
"discoverTime": "2026-03-28T17:59:43.784116"
},
{
"id": "2037831996780474369",
"riskCode": null,
"category": "external",
"riskName": "数据隐私与合规风险",
"description": "简历数据包含大量个人敏感信息,处理不当可能违反《个人信息保护法》",
"probability": 30.0,
"impact": 5.0,
"riskScore": 1.5,
"riskLevel": "medium",
"status": "identified",
"ownerId": null,
"mitigationPlan": "聘请法律顾问审核全流程合规性,严格实施数据脱敏处理,建立完善的数据访问权限控制",
"dueDate": null,
"discoverTime": "2026-03-28T17:59:43.778244"
},
{
"id": "2037831996780474371",
"riskCode": null,
"category": "technical",
"riskName": "第三方ATS系统集成风险",
"description": "对接主流招聘网站及内部ATS时可能遇到API限制或数据格式不兼容问题",
"probability": 50.0,
"impact": 3.0,
"riskScore": 1.5,
"riskLevel": "medium",
"status": "identified",
"ownerId": null,
"mitigationPlan": "提前获取第三方接口文档并进行技术验证,设计高兼容性的中间件适配层",
"dueDate": null,
"discoverTime": "2026-03-28T17:59:43.786088"
},
{
"id": "2037831996780474372",
"riskCode": null,
"category": "resource",
"riskName": "算力资源短缺风险",
"description": "云端高端GPU如A100/A800可能存在排队或租赁不到位的情况影响训练进度",
"probability": 25.0,
"impact": 4.0,
"riskScore": 1.0,
"riskLevel": "medium",
"status": "identified",
"ownerId": null,
"mitigationPlan": "提前锁定云资源供应商并签订算力保障协议,准备备用算力平台方案",
"dueDate": null,
"discoverTime": "2026-03-28T17:59:43.789088"
}
],
"timelineNodes": [
{
"id": "2037831996843388930",
"nodeName": "项目启动",
"nodeType": "event",
"planDate": "2026-04-01",
"actualDate": null,
"description": "项目正式启动,团队入场",
"status": "pending",
"sortOrder": 0,
"kbScope": null
},
{
"id": "2037831996843388931",
"nodeName": "M1: 需求分析与架构设计完成",
"nodeType": "milestone",
"planDate": "2026-04-30",
"actualDate": null,
"description": "完成需求评审及技术架构可行性验证",
"status": "pending",
"sortOrder": 1,
"kbScope": null
},
{
"id": "2037831996843388932",
"nodeName": "M2: 核心算法验证通过",
"nodeType": "milestone",
"planDate": "2026-06-15",
"actualDate": null,
"description": "模型准确率达标解析引擎V1.0产出",
"status": "pending",
"sortOrder": 2,
"kbScope": null
},
{
"id": "2037831996843388933",
"nodeName": "M3: Alpha版开发完成",
"nodeType": "milestone",
"planDate": "2026-07-31",
"actualDate": null,
"description": "核心功能闭环跑通",
"status": "pending",
"sortOrder": 3,
"kbScope": null
},
{
"id": "2037831996843388934",
"nodeName": "M4: Beta版内测完成",
"nodeType": "milestone",
"planDate": "2026-08-31",
"actualDate": null,
"description": "性能达标,无严重缺陷",
"status": "pending",
"sortOrder": 4,
"kbScope": null
},
{
"id": "2037831996906303490",
"nodeName": "M5: UAT与试点完成",
"nodeType": "milestone",
"planDate": "2026-09-15",
"actualDate": null,
"description": "人力资源部验收确认",
"status": "pending",
"sortOrder": 5,
"kbScope": null
},
{
"id": "2037831996906303491",
"nodeName": "M6: 系统正式上线与交付",
"nodeType": "milestone",
"planDate": "2026-09-30",
"actualDate": null,
"description": "全员培训完成,项目结项",
"status": "pending",
"sortOrder": 6,
"kbScope": null
}
]
},
"message": "查询成功"
}

View File

@@ -17,6 +17,16 @@ export default {
meta: {
title: $t("menus.pureProject")
}
},
{
path: "/project/detail/:id",
name: "ProjectDetail",
component: () => import("@/views/project/detail.vue"),
meta: {
title: "项目详情",
showLink: false,
activePath: "/project"
}
}
]
} satisfies RouteConfigsTable;

View File

@@ -0,0 +1,865 @@
<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 { message } from "@/utils/message";
import dayjs from "dayjs";
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 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";
}
}
// 计算甘特图任务条样式(使用真实任务数据)
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;
return {
left: `${(offsetDays / totalDays) * 100}%`,
width: `${(duration / totalDays) * 100}%`
};
}
// 获取项目详情
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">
<!-- 顶部导航 -->
<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="
Math.round((projectInfo.cost / projectInfo.budget) * 100)
"
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="16">
<!-- 甘特图 -->
<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>
<!-- 甘特图主体 -->
<div class="gantt-chart">
<!-- 时间轴 -->
<div class="gantt-timeline">
<div
v-for="month in 6"
:key="month"
class="timeline-item"
:style="{ left: `${(month - 1) * (100 / 6)}%` }"
>
{{
dayjs(projectInfo.startDate)
.add(month - 1, "month")
.format("YYYY-MM")
}}
</div>
</div>
<!-- 任务列表 -->
<div class="gantt-tasks">
<div
v-for="task in taskList.slice().reverse()"
:key="task.id"
class="gantt-task-row"
>
<div class="task-name">{{ task.taskName }}</div>
<div class="task-bar-container">
<div
class="task-bar"
:style="getTaskBarStyle(task)"
:class="{ completed: task.progress === 100 }"
>
<div
class="task-progress"
:style="{ width: `${task.progress || 0}%` }"
/>
</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>
<!-- 右侧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>
</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 {
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-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;
}
}
}
}
}
// 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;
}
}
}
.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;
}
</style>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useProject } from "./utils/hook";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import CreateProjectWizard from "./components/CreateProjectWizard.vue";
@@ -19,6 +20,7 @@ defineOptions({
name: "Project"
});
const router = useRouter();
const wizardVisible = ref(false);
const {
@@ -50,7 +52,10 @@ function handleWizardSuccess() {
// 查看项目详情
function handleView(row: any) {
console.log("查看项目", row);
router.push({
name: "ProjectDetail",
params: { id: row.id.toString() }
});
}
// 编辑项目
@@ -184,10 +189,10 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
<div>
<p class="text-gray-500 text-sm">平均完成率</p>
<p class="text-2xl font-bold mt-1">
{{ statistics.averageProgress }}%
{{ Math.round(statistics.averageProgress || 0) }}%
</p>
<el-progress
:percentage="statistics.averageProgress"
:percentage="Math.round(statistics.averageProgress || 0)"
:show-text="false"
class="mt-2"
style="width: 100px"
@@ -302,8 +307,8 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
:lg="6"
class="mb-4"
>
<el-card shadow="hover" class="project-card">
<div class="flex justify-between items-start mb-3">
<el-card shadow="hover" class="project-card" @click="handleView(item)">
<div class="flex justify-between items-start mb-3" @click.stop>
<div class="flex-1 min-w-0">
<h4
class="font-medium text-base truncate"
@@ -315,8 +320,8 @@ function getRiskType(risk?: string): "success" | "warning" | "danger" {
{{ item.projectCode || "暂无项目编号" }}
</p>
</div>
<el-dropdown>
<el-button link>
<el-dropdown @click.stop>
<el-button link @click.stop>
<component :is="useRenderIcon(MoreIcon)" />
</el-button>
<template #dropdown>

View File

@@ -96,7 +96,7 @@ export function useProject() {
// 删除项目
async function handleDelete(row: ProjectItem) {
try {
const { code } = await deleteProject(row.id!);
const { code } = await deleteProject(row.id as string);
if (code === 200) {
message(`已删除项目 "${row.projectName}"`, { type: "success" });
onSearch();