- 将项目相关ID类型统一由number改为string,增强一致性 - 新增项目成员、里程碑、任务、资源、风险、时间线节点等详细类型定义 - 添加获取项目详情接口方法getProjectDetail - 在路由中新增项目详情页路由配置 - 实现项目详情页面,支持展示基本信息、成员、任务、风险及资源等数据 - 项目详情页面集成AI助手简易聊天交互功能展示 - 添加项目状态、风险等级及资源状态的辅助文本和样式方法 - 优化甘特图任务条样式计算,基于项目详情任务数据展现
This commit is contained in:
@@ -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 中的定义) ====================
|
||||
|
||||
/** 项目信息 */
|
||||
|
||||
594
src/api/项目详情.openapi.json
Normal file
594
src/api/项目详情.openapi.json
Normal 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": "查询成功"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
865
src/views/project/detail.vue
Normal file
865
src/views/project/detail.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user