- 新增项目菜单项及路由配置,支持项目管理入口 - 实现项目相关API接口,包括项目列表、统计、甘特图及项目初始化接口 - 添加项目新建向导组件,支持上传文件预览及确认保存 - 实现项目管理页面,包含项目列表展示、筛选、统计卡片及新建项目操作 - 支持项目基本信息、里程碑、任务、成员及风险等多维度管理数据录入 - 优化页面交互体验,支持上传文件格式校验及数据编辑预览 - 提供状态及风险等级标签显示,辅助项目状态快速识别
This commit is contained in:
@@ -69,6 +69,7 @@ panel:
|
||||
pureMultiTagsCache: MultiTags Cache
|
||||
menus:
|
||||
pureHome: Home
|
||||
pureProject: Project Management
|
||||
pureLogin: Login
|
||||
pureEmpty: Empty Page
|
||||
pureTable: Table
|
||||
|
||||
@@ -69,6 +69,7 @@ panel:
|
||||
pureMultiTagsCache: 页签持久化
|
||||
menus:
|
||||
pureHome: 首页
|
||||
pureProject: 项目管理
|
||||
pureLogin: 登录
|
||||
pureEmpty: 无Layout页
|
||||
pureTable: 表格
|
||||
|
||||
241
src/api/project.ts
Normal file
241
src/api/project.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { http } from "@/utils/http";
|
||||
|
||||
/** 通用响应结果 */
|
||||
type Result<T = any> = {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
// ==================== 项目列表 ====================
|
||||
|
||||
/** 项目列表项 - 根据 OpenAPI 定义 */
|
||||
export type ProjectItem = {
|
||||
id?: number;
|
||||
projectCode?: string;
|
||||
projectName?: string;
|
||||
projectType?: string;
|
||||
managerId?: number;
|
||||
managerName?: string;
|
||||
planStartDate?: string;
|
||||
planEndDate?: string;
|
||||
progress?: number;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
riskLevel?: string;
|
||||
tags?: string[];
|
||||
budget?: number;
|
||||
cost?: number;
|
||||
createTime?: string;
|
||||
myRole?: string;
|
||||
};
|
||||
|
||||
/** 分页数据结构 */
|
||||
export type TableDataInfo<T> = {
|
||||
total: number;
|
||||
rows: T[];
|
||||
code: number;
|
||||
msg: string;
|
||||
};
|
||||
|
||||
/** 项目查询参数 */
|
||||
export type ProjectQueryParams = {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
};
|
||||
|
||||
/** 分页查询项目列表 */
|
||||
export const getProjectList = (params?: ProjectQueryParams) => {
|
||||
return http.request<Result<TableDataInfo<ProjectItem>>>(
|
||||
"get",
|
||||
"/api/v1/project/list",
|
||||
{ params }
|
||||
);
|
||||
};
|
||||
|
||||
/** 删除项目 */
|
||||
export const deleteProject = (id: number) => {
|
||||
return http.request<Result<void>>("delete", `/api/v1/project/${id}`);
|
||||
};
|
||||
|
||||
// ==================== 项目统计 ====================
|
||||
|
||||
/** 项目统计数据 - 根据 OpenAPI 定义 */
|
||||
export type ProjectStatistics = {
|
||||
totalCount: number;
|
||||
ongoingCount: number;
|
||||
completedCount: number;
|
||||
pausedCount: number;
|
||||
planningCount: number;
|
||||
cancelledCount: number;
|
||||
draftCount: number;
|
||||
statusCountMap: Record<string, number>;
|
||||
newThisMonth: number;
|
||||
aboutToExpireCount: number;
|
||||
averageProgress: number;
|
||||
highRiskCount: number;
|
||||
};
|
||||
|
||||
/** 获取项目统计数据 */
|
||||
export const getProjectStatistics = () => {
|
||||
return http.request<Result<ProjectStatistics>>(
|
||||
"get",
|
||||
"/api/v1/project/statistics"
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 项目甘特图 ====================
|
||||
|
||||
/** 甘特图任务 */
|
||||
export type GanttTask = {
|
||||
id?: number;
|
||||
taskName?: string;
|
||||
type?: string; // task-任务, milestone-里程碑
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
actualStartDate?: string;
|
||||
actualEndDate?: string;
|
||||
progress?: number;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
parentId?: number;
|
||||
sortOrder?: number;
|
||||
assigneeName?: string;
|
||||
dependencies?: number[];
|
||||
};
|
||||
|
||||
/** 项目甘特图数据 */
|
||||
export type ProjectGantt = {
|
||||
projectId?: number;
|
||||
projectName?: string;
|
||||
projectStatus?: string;
|
||||
projectStartDate?: string;
|
||||
projectEndDate?: string;
|
||||
projectProgress?: number;
|
||||
tasks?: GanttTask[];
|
||||
milestones?: GanttTask[];
|
||||
};
|
||||
|
||||
/** 获取项目甘特图数据 */
|
||||
export const getProjectGantt = (projectId: number) => {
|
||||
return http.request<Result<ProjectGantt>>(
|
||||
"get",
|
||||
`/api/v1/project/${projectId}/gantt`
|
||||
);
|
||||
};
|
||||
|
||||
// ==================== 项目初始化(复用 system.ts 中的定义) ====================
|
||||
|
||||
/** 项目信息 */
|
||||
export type ProjectInfo = {
|
||||
project_name?: string;
|
||||
project_type?: string;
|
||||
description?: string;
|
||||
objectives?: string;
|
||||
plan_start_date?: string;
|
||||
plan_end_date?: string;
|
||||
budget?: number;
|
||||
currency?: string;
|
||||
priority?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
|
||||
/** 里程碑信息 */
|
||||
export type MilestoneInfo = {
|
||||
milestone_name?: string;
|
||||
description?: string;
|
||||
plan_date?: string;
|
||||
deliverables?: string;
|
||||
owner_role?: string;
|
||||
};
|
||||
|
||||
/** 任务信息 */
|
||||
export type TaskInfo = {
|
||||
task_id?: string;
|
||||
task_name?: string;
|
||||
parent_task_id?: string;
|
||||
description?: string;
|
||||
plan_start_date?: string;
|
||||
plan_end_date?: string;
|
||||
estimated_hours?: number;
|
||||
priority?: string;
|
||||
assignee_role?: string;
|
||||
dependencies?: string[];
|
||||
deliverables?: string;
|
||||
};
|
||||
|
||||
/** 成员信息 */
|
||||
export type MemberInfo = {
|
||||
name?: string;
|
||||
role_code?: string;
|
||||
responsibility?: string;
|
||||
department?: string;
|
||||
weekly_hours?: number;
|
||||
};
|
||||
|
||||
/** 资源信息 */
|
||||
export type ResourceInfo = {
|
||||
resource_name?: string;
|
||||
resource_type?: string;
|
||||
quantity?: number;
|
||||
unit?: string;
|
||||
unit_price?: number;
|
||||
supplier?: string;
|
||||
};
|
||||
|
||||
/** 风险信息 */
|
||||
export type RiskInfo = {
|
||||
risk_name?: string;
|
||||
category?: string;
|
||||
description?: string;
|
||||
probability?: number;
|
||||
impact?: number;
|
||||
mitigation_plan?: string;
|
||||
};
|
||||
|
||||
/** 时间节点信息 */
|
||||
export type TimelineNodeInfo = {
|
||||
node_name?: string;
|
||||
node_type?: string;
|
||||
plan_date?: string;
|
||||
description?: string;
|
||||
kb_scope?: string[];
|
||||
};
|
||||
|
||||
/** 项目初始化结果 */
|
||||
export type ProjectInitResult = {
|
||||
project: ProjectInfo;
|
||||
milestones: MilestoneInfo[];
|
||||
tasks: TaskInfo[];
|
||||
members: MemberInfo[];
|
||||
resources: ResourceInfo[];
|
||||
risks: RiskInfo[];
|
||||
timeline_nodes: TimelineNodeInfo[];
|
||||
};
|
||||
|
||||
/** 上传文件并生成项目初始化预览数据 */
|
||||
export const previewProjectInit = (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return http.request<Result<ProjectInitResult>>(
|
||||
"post",
|
||||
"/api/v1/project-init/preview",
|
||||
{ data: formData },
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/** 确认并保存项目初始化数据 */
|
||||
export const confirmProjectInit = (data: ProjectInitResult) => {
|
||||
return http.request<Result<ProjectInitResult>>(
|
||||
"post",
|
||||
"/api/v1/project-init/confirm",
|
||||
{ data }
|
||||
);
|
||||
};
|
||||
@@ -1,81 +1,413 @@
|
||||
{
|
||||
"openapi": "3.0.1",
|
||||
"info": {
|
||||
"title": "默认模块",
|
||||
"description": "",
|
||||
"version": "1.0.0"
|
||||
"project": {
|
||||
"project_name": "AIHR 智能简历筛选系统",
|
||||
"project_type": "研发项目",
|
||||
"description": "利用大语言模型(LLM)和自然语言处理(NLP)技术,实现简历的自动化解析、人岗匹配度评分及候选人排序,提升招聘效率。",
|
||||
"objectives": "1. 效率提升:单份简历平均处理时间降低至10秒以内。2. 精准匹配:人岗匹配度评分准确率达到90%以上。3. 系统集成:无缝对接主流招聘网站及企业内部ATS。4. 合规安全:确保候选人数据脱敏处理,符合《个人信息保护法》要求。",
|
||||
"plan_start_date": "2026-04-01",
|
||||
"plan_end_date": "2026-09-30",
|
||||
"budget": 2850000,
|
||||
"currency": "CNY",
|
||||
"priority": "high",
|
||||
"tags": ["AI", "NLP", "LLM", "人力资源", "招聘系统"]
|
||||
},
|
||||
"tags": [],
|
||||
"paths": {
|
||||
"/api/v1/system/role/{id}/menu-ids": {
|
||||
"get": {
|
||||
"summary": "查询角色的菜单权限ID列表(只返回菜单类型的权限)",
|
||||
"deprecated": false,
|
||||
"description": "",
|
||||
"tags": [],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "",
|
||||
"example": "Bearer 000b7e25-53b2-42a3-a39b-9f4cb03644ba",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"default": "Bearer 000b7e25-53b2-42a3-a39b-9f4cb03644ba"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BaseResponseListLong"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"BaseResponseListLong": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer",
|
||||
"description": ""
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": ""
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
"milestones": [
|
||||
{
|
||||
"milestone_name": "M1 需求分析与架构设计",
|
||||
"description": "需求评审通过,技术架构可行性验证完成。",
|
||||
"plan_date": "2026-04-30",
|
||||
"deliverables": "《需求规格说明书》、《系统架构设计文档》、《数据库设计文档》",
|
||||
"owner_role": "产品负责人"
|
||||
},
|
||||
"responses": {},
|
||||
"securitySchemes": {}
|
||||
},
|
||||
"servers": [],
|
||||
"security": []
|
||||
{
|
||||
"milestone_name": "M2 核心算法模型训练与验证",
|
||||
"description": "模型在测试集上的准确率>85%,解析字段覆盖率>95%。",
|
||||
"plan_date": "2026-06-15",
|
||||
"deliverables": "简历解析引擎 V1.0、人岗匹配算法模型、测试报告",
|
||||
"owner_role": "AI算法工程师"
|
||||
},
|
||||
{
|
||||
"milestone_name": "M3 系统功能开发完成 (Alpha 版)",
|
||||
"description": "核心功能(上传、解析、评分、搜索)闭环跑通。",
|
||||
"plan_date": "2026-07-31",
|
||||
"deliverables": "可运行的后端服务、前端管理界面、API 接口文档",
|
||||
"owner_role": "项目经理"
|
||||
},
|
||||
{
|
||||
"milestone_name": "M4 系统集成与内部测试 (Beta 版)",
|
||||
"description": "支持并发用户数>50,响应时间<2秒,无严重 Bug。",
|
||||
"plan_date": "2026-08-31",
|
||||
"deliverables": "集成测试报告、性能测试报告、用户操作手册",
|
||||
"owner_role": "测试工程师"
|
||||
},
|
||||
{
|
||||
"milestone_name": "M5 用户验收测试 (UAT) 与试点",
|
||||
"description": "试点部门(人力资源部)确认功能满足业务需求。",
|
||||
"plan_date": "2026-09-15",
|
||||
"deliverables": "UAT 验收报告、试点运行反馈报告",
|
||||
"owner_role": "产品负责人"
|
||||
},
|
||||
{
|
||||
"milestone_name": "M6 正式上线与交付",
|
||||
"description": "系统正式部署至生产环境,完成全员培训。",
|
||||
"plan_date": "2026-09-30",
|
||||
"deliverables": "最终源代码、部署文档、运维手册、项目总结报告",
|
||||
"owner_role": "项目经理"
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"task_id": "T001",
|
||||
"task_name": "需求分析与产品设计",
|
||||
"parent_task_id": null,
|
||||
"description": "完成需求梳理、原型设计及用户故事编写",
|
||||
"plan_start_date": "2026-04-01",
|
||||
"plan_end_date": "2026-04-15",
|
||||
"estimated_hours": 80,
|
||||
"priority": "high",
|
||||
"assignee_role": "产品负责人",
|
||||
"dependencies": [],
|
||||
"deliverables": "《需求规格说明书》、原型图"
|
||||
},
|
||||
{
|
||||
"task_id": "T002",
|
||||
"task_name": "技术架构与数据库设计",
|
||||
"parent_task_id": null,
|
||||
"description": "系统整体技术选型、架构设计及数据库设计",
|
||||
"plan_start_date": "2026-04-16",
|
||||
"plan_end_date": "2026-04-30",
|
||||
"estimated_hours": 80,
|
||||
"priority": "high",
|
||||
"assignee_role": "技术架构师",
|
||||
"dependencies": ["T001"],
|
||||
"deliverables": "《系统架构设计文档》、《数据库设计文档》"
|
||||
},
|
||||
{
|
||||
"task_id": "T003",
|
||||
"task_name": "数据清洗与标注",
|
||||
"parent_task_id": null,
|
||||
"description": "历史简历数据的清洗、标注及训练数据集构建",
|
||||
"plan_start_date": "2026-04-10",
|
||||
"plan_end_date": "2026-05-15",
|
||||
"estimated_hours": 100,
|
||||
"priority": "high",
|
||||
"assignee_role": "数据标注专员",
|
||||
"dependencies": ["T001"],
|
||||
"deliverables": "训练数据集"
|
||||
},
|
||||
{
|
||||
"task_id": "T004",
|
||||
"task_name": "算法模型训练与调优",
|
||||
"parent_task_id": null,
|
||||
"description": "NLP模型选型、简历解析算法开发及匹配模型训练",
|
||||
"plan_start_date": "2026-05-01",
|
||||
"plan_end_date": "2026-06-15",
|
||||
"estimated_hours": 260,
|
||||
"priority": "high",
|
||||
"assignee_role": "AI算法工程师",
|
||||
"dependencies": ["T003"],
|
||||
"deliverables": "简历解析引擎、算法模型"
|
||||
},
|
||||
{
|
||||
"task_id": "T005",
|
||||
"task_name": "后端功能开发",
|
||||
"parent_task_id": null,
|
||||
"description": "后端API开发、数据库对接及高并发处理优化",
|
||||
"plan_start_date": "2026-05-01",
|
||||
"plan_end_date": "2026-07-15",
|
||||
"estimated_hours": 320,
|
||||
"priority": "high",
|
||||
"assignee_role": "后端开发工程师",
|
||||
"dependencies": ["T002"],
|
||||
"deliverables": "后端服务、API接口文档"
|
||||
},
|
||||
{
|
||||
"task_id": "T006",
|
||||
"task_name": "前端页面开发",
|
||||
"parent_task_id": null,
|
||||
"description": "管理后台前端页面开发、数据可视化展示及交互实现",
|
||||
"plan_start_date": "2026-05-15",
|
||||
"plan_end_date": "2026-07-15",
|
||||
"estimated_hours": 320,
|
||||
"priority": "high",
|
||||
"assignee_role": "前端开发工程师",
|
||||
"dependencies": ["T002"],
|
||||
"deliverables": "前端管理界面"
|
||||
},
|
||||
{
|
||||
"task_id": "T007",
|
||||
"task_name": "系统联调与Alpha版发布",
|
||||
"parent_task_id": null,
|
||||
"description": "前后端联调、算法模型集成及核心功能闭环测试",
|
||||
"plan_start_date": "2026-07-16",
|
||||
"plan_end_date": "2026-07-31",
|
||||
"estimated_hours": 120,
|
||||
"priority": "high",
|
||||
"assignee_role": "后端开发工程师",
|
||||
"dependencies": ["T004", "T005", "T006"],
|
||||
"deliverables": "Alpha版系统"
|
||||
},
|
||||
{
|
||||
"task_id": "T008",
|
||||
"task_name": "系统集成与性能测试",
|
||||
"parent_task_id": null,
|
||||
"description": "功能测试、性能测试及自动化测试脚本编写",
|
||||
"plan_start_date": "2026-08-01",
|
||||
"plan_end_date": "2026-08-31",
|
||||
"estimated_hours": 160,
|
||||
"priority": "high",
|
||||
"assignee_role": "测试工程师",
|
||||
"dependencies": ["T007"],
|
||||
"deliverables": "集成测试报告、性能测试报告"
|
||||
},
|
||||
{
|
||||
"task_id": "T009",
|
||||
"task_name": "UAT验收与试点运行",
|
||||
"parent_task_id": null,
|
||||
"description": "组织人力资源部进行用户验收测试并收集反馈",
|
||||
"plan_start_date": "2026-09-01",
|
||||
"plan_end_date": "2026-09-15",
|
||||
"estimated_hours": 80,
|
||||
"priority": "medium",
|
||||
"assignee_role": "产品负责人",
|
||||
"dependencies": ["T008"],
|
||||
"deliverables": "UAT验收报告"
|
||||
},
|
||||
{
|
||||
"task_id": "T010",
|
||||
"task_name": "正式上线与培训部署",
|
||||
"parent_task_id": null,
|
||||
"description": "系统生产环境部署、全员培训及总结报告编写",
|
||||
"plan_start_date": "2026-09-16",
|
||||
"plan_end_date": "2026-09-30",
|
||||
"estimated_hours": 80,
|
||||
"priority": "high",
|
||||
"assignee_role": "项目经理",
|
||||
"dependencies": ["T009"],
|
||||
"deliverables": "部署文档、运维手册、最终源代码"
|
||||
}
|
||||
],
|
||||
"members": [
|
||||
{
|
||||
"name": "张伟",
|
||||
"role_code": "manager",
|
||||
"responsibility": "负责整体项目规划、进度控制、风险管理及跨部门协调。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 40
|
||||
},
|
||||
{
|
||||
"name": "李娜",
|
||||
"role_code": "leader",
|
||||
"responsibility": "负责需求梳理、原型设计、用户故事编写及验收测试。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 40
|
||||
},
|
||||
{
|
||||
"name": "王强",
|
||||
"role_code": "leader",
|
||||
"responsibility": "负责系统整体技术选型、架构设计、核心技术难点攻关。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 20
|
||||
},
|
||||
{
|
||||
"name": "陈思",
|
||||
"role_code": "member",
|
||||
"responsibility": "负责 NLP 模型选型、简历解析算法开发、匹配模型训练与调优。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 40
|
||||
},
|
||||
{
|
||||
"name": "赵杰",
|
||||
"role_code": "member",
|
||||
"responsibility": "负责后端 API 开发、数据库设计、第三方系统接口对接。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 40
|
||||
},
|
||||
{
|
||||
"name": "刘洋",
|
||||
"role_code": "member",
|
||||
"responsibility": "协助后端开发,负责高并发处理及缓存策略优化。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 40
|
||||
},
|
||||
{
|
||||
"name": "孙丽",
|
||||
"role_code": "member",
|
||||
"responsibility": "负责管理后台前端页面开发、数据可视化展示及交互实现。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 40
|
||||
},
|
||||
{
|
||||
"name": "周敏",
|
||||
"role_code": "member",
|
||||
"responsibility": "负责测试用例编写、功能测试、性能测试及自动化测试脚本。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 40
|
||||
},
|
||||
{
|
||||
"name": "吴凯",
|
||||
"role_code": "member",
|
||||
"responsibility": "负责系统界面设计、用户体验优化及交互规范制定。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 20
|
||||
},
|
||||
{
|
||||
"name": "郑浩",
|
||||
"role_code": "member",
|
||||
"responsibility": "负责历史简历数据的清洗、标注及训练数据集构建。",
|
||||
"department": "未来科技研发部",
|
||||
"weekly_hours": 20
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"resource_name": "高性能云服务器",
|
||||
"resource_type": "equipment",
|
||||
"quantity": 5,
|
||||
"unit": "台",
|
||||
"unit_price": 0,
|
||||
"supplier": "云服务商"
|
||||
},
|
||||
{
|
||||
"resource_name": "云端GPU算力(A100/A800)",
|
||||
"resource_type": "equipment",
|
||||
"quantity": 2000,
|
||||
"unit": "小时",
|
||||
"unit_price": 0,
|
||||
"supplier": "GPU云服务提供商"
|
||||
},
|
||||
{
|
||||
"resource_name": "对象存储",
|
||||
"resource_type": "equipment",
|
||||
"quantity": 5,
|
||||
"unit": "TB",
|
||||
"unit_price": 0,
|
||||
"supplier": "云服务商"
|
||||
},
|
||||
{
|
||||
"resource_name": "历史简历数据",
|
||||
"resource_type": "material",
|
||||
"quantity": 100000,
|
||||
"unit": "份",
|
||||
"unit_price": 0,
|
||||
"supplier": "企业内部"
|
||||
},
|
||||
{
|
||||
"resource_name": "简历-JD匹配对标注数据集",
|
||||
"resource_type": "material",
|
||||
"quantity": 5000,
|
||||
"unit": "对",
|
||||
"unit_price": 0,
|
||||
"supplier": "内部构建"
|
||||
},
|
||||
{
|
||||
"resource_name": "OCR识别 API",
|
||||
"resource_type": "software",
|
||||
"quantity": 1,
|
||||
"unit": "套",
|
||||
"unit_price": 0,
|
||||
"supplier": "第三方服务商"
|
||||
},
|
||||
{
|
||||
"resource_name": "大模型调用 API",
|
||||
"resource_type": "software",
|
||||
"quantity": 1,
|
||||
"unit": "套",
|
||||
"unit_price": 0,
|
||||
"supplier": "第三方大模型服务商"
|
||||
},
|
||||
{
|
||||
"resource_name": "法律合规咨询服务",
|
||||
"resource_type": "human",
|
||||
"quantity": 1,
|
||||
"unit": "项",
|
||||
"unit_price": 0,
|
||||
"supplier": "外部法律顾问"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
{
|
||||
"risk_name": "数据合规与隐私泄露风险",
|
||||
"category": "other",
|
||||
"description": "处理大量包含个人隐私的简历数据,可能违反《个人信息保护法》或引发数据泄露。",
|
||||
"probability": 5,
|
||||
"impact": 5,
|
||||
"mitigation_plan": "聘请专业法律顾问审核数据采集、存储及使用流程;对所有简历数据进行严格的脱敏处理和加密存储。"
|
||||
},
|
||||
{
|
||||
"risk_name": "模型匹配准确率不达标",
|
||||
"category": "technical",
|
||||
"description": "人岗匹配度评分准确率难以达到预期的90%以上目标。",
|
||||
"probability": 40,
|
||||
"impact": 4,
|
||||
"mitigation_plan": "构建高质量的专家标注数据集,引入人力资源专家进行人工抽检复核,持续微调和优化算法模型。"
|
||||
},
|
||||
{
|
||||
"risk_name": "系统并发性能瓶颈",
|
||||
"category": "technical",
|
||||
"description": "在处理大量并发解析请求时,系统响应时间可能超过2秒或出现崩溃。",
|
||||
"probability": 35,
|
||||
"impact": 4,
|
||||
"mitigation_plan": "优化系统架构,引入Redis缓存和负载均衡机制,并在Beta阶段进行充分的压力测试和性能调优。"
|
||||
},
|
||||
{
|
||||
"risk_name": "第三方API依赖及限流风险",
|
||||
"category": "schedule",
|
||||
"description": "过度依赖外部大模型API和OCR服务,可能因网络延迟或服务商限流影响项目进度和系统稳定性。",
|
||||
"probability": 25,
|
||||
"impact": 3,
|
||||
"mitigation_plan": "设计灵活的接口适配层以支持多家API服务商切换,并在必要时考虑本地部署轻量级备用模型。"
|
||||
}
|
||||
],
|
||||
"timeline_nodes": [
|
||||
{
|
||||
"node_name": "项目正式启动",
|
||||
"node_type": "event",
|
||||
"plan_date": "2026-04-01",
|
||||
"description": "项目团队组建完毕,进入需求分析阶段",
|
||||
"kb_scope": ["file"]
|
||||
},
|
||||
{
|
||||
"node_name": "需求与架构设计完成",
|
||||
"node_type": "milestone",
|
||||
"plan_date": "2026-04-30",
|
||||
"description": "完成M1里程碑,架构设计通过评审",
|
||||
"kb_scope": ["report", "file"]
|
||||
},
|
||||
{
|
||||
"node_name": "核心算法就绪",
|
||||
"node_type": "milestone",
|
||||
"plan_date": "2026-06-15",
|
||||
"description": "完成M2里程碑,模型准确率达标",
|
||||
"kb_scope": ["report", "file", "risk"]
|
||||
},
|
||||
{
|
||||
"node_name": "Alpha版发布",
|
||||
"node_type": "milestone",
|
||||
"plan_date": "2026-07-31",
|
||||
"description": "完成M3里程碑,系统核心功能闭环跑通",
|
||||
"kb_scope": ["report", "ticket"]
|
||||
},
|
||||
{
|
||||
"node_name": "Beta版发布",
|
||||
"node_type": "milestone",
|
||||
"plan_date": "2026-08-31",
|
||||
"description": "完成M4里程碑,内部集成测试与性能测试完成",
|
||||
"kb_scope": ["report", "ticket", "risk"]
|
||||
},
|
||||
{
|
||||
"node_name": "UAT验收通过",
|
||||
"node_type": "milestone",
|
||||
"plan_date": "2026-09-15",
|
||||
"description": "完成M5里程碑,试点部门确认功能满足需求",
|
||||
"kb_scope": ["report", "ticket"]
|
||||
},
|
||||
{
|
||||
"node_name": "项目正式上线交付",
|
||||
"node_type": "milestone",
|
||||
"plan_date": "2026-09-30",
|
||||
"description": "完成M6里程碑,生产环境部署及全员培训完成",
|
||||
"kb_scope": ["report", "file"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,36 +2,38 @@
|
||||
|
||||
const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从非 0 开始
|
||||
chatai = 1,
|
||||
vueflow = 2,
|
||||
ganttastic = 3,
|
||||
components = 4,
|
||||
able = 5,
|
||||
table = 6,
|
||||
form = 7,
|
||||
list = 8,
|
||||
result = 9,
|
||||
error = 10,
|
||||
frame = 11,
|
||||
nested = 12,
|
||||
permission = 13,
|
||||
system = 14,
|
||||
monitor = 15,
|
||||
tabs = 16,
|
||||
about = 17,
|
||||
codemirror = 18,
|
||||
markdown = 19,
|
||||
editor = 20,
|
||||
flowchart = 21,
|
||||
formdesign = 22,
|
||||
board = 23,
|
||||
ppt = 24,
|
||||
mind = 25,
|
||||
guide = 26,
|
||||
menuoverflow = 27;
|
||||
project = 2,
|
||||
vueflow = 3,
|
||||
ganttastic = 4,
|
||||
components = 5,
|
||||
able = 6,
|
||||
table = 7,
|
||||
form = 8,
|
||||
list = 9,
|
||||
result = 10,
|
||||
error = 11,
|
||||
frame = 12,
|
||||
nested = 13,
|
||||
permission = 14,
|
||||
system = 15,
|
||||
monitor = 16,
|
||||
tabs = 17,
|
||||
about = 18,
|
||||
codemirror = 19,
|
||||
markdown = 20,
|
||||
editor = 21,
|
||||
flowchart = 22,
|
||||
formdesign = 23,
|
||||
board = 24,
|
||||
ppt = 25,
|
||||
mind = 26,
|
||||
guide = 27,
|
||||
menuoverflow = 28;
|
||||
|
||||
export {
|
||||
home,
|
||||
chatai,
|
||||
project,
|
||||
vueflow,
|
||||
ganttastic,
|
||||
components,
|
||||
|
||||
22
src/router/modules/project.ts
Normal file
22
src/router/modules/project.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { $t } from "@/plugins/i18n";
|
||||
import { project } from "@/router/enums";
|
||||
|
||||
export default {
|
||||
path: "/project",
|
||||
redirect: "/project/index",
|
||||
meta: {
|
||||
icon: "ri:folder-chart-line",
|
||||
title: $t("menus.pureProject"),
|
||||
rank: project
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/project/index",
|
||||
name: "Project",
|
||||
component: () => import("@/views/project/index.vue"),
|
||||
meta: {
|
||||
title: $t("menus.pureProject")
|
||||
}
|
||||
}
|
||||
]
|
||||
} satisfies RouteConfigsTable;
|
||||
@@ -18,7 +18,7 @@ import { useUserStoreHook } from "@/store/modules/user";
|
||||
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
|
||||
const defaultConfig: AxiosRequestConfig = {
|
||||
// 请求超时时间
|
||||
timeout: 10000,
|
||||
timeout: 600000,
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Content-Type": "application/json",
|
||||
|
||||
722
src/views/project/components/CreateProjectWizard.vue
Normal file
722
src/views/project/components/CreateProjectWizard.vue
Normal file
@@ -0,0 +1,722 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { message } from "@/utils/message";
|
||||
import { previewProjectInit, confirmProjectInit } from "@/api/project";
|
||||
import type { ProjectInitResult } from "@/api/project";
|
||||
import {
|
||||
WizardStep,
|
||||
PriorityOptions,
|
||||
ProjectTypeOptions
|
||||
} from "../utils/types";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import UploadIcon from "~icons/ri/upload-cloud-line";
|
||||
import FileIcon from "~icons/ri/file-line";
|
||||
import CheckIcon from "~icons/ri/check-line";
|
||||
import DeleteIcon from "~icons/ri/delete-bin-line";
|
||||
import AddIcon from "~icons/ri/add-line";
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:visible", value: boolean): void;
|
||||
(e: "success"): void;
|
||||
}>();
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: val => emit("update:visible", val)
|
||||
});
|
||||
|
||||
const currentStep = ref<WizardStep>(WizardStep.Upload);
|
||||
const uploading = ref(false);
|
||||
const saving = ref(false);
|
||||
const uploadRef = ref();
|
||||
const fileList = ref<File[]>([]);
|
||||
|
||||
// 项目初始化数据
|
||||
const projectData = reactive<ProjectInitResult>({
|
||||
project: {
|
||||
project_name: "",
|
||||
project_type: "",
|
||||
description: "",
|
||||
objectives: "",
|
||||
plan_start_date: "",
|
||||
plan_end_date: "",
|
||||
budget: 0,
|
||||
currency: "CNY",
|
||||
priority: "medium",
|
||||
tags: []
|
||||
},
|
||||
milestones: [],
|
||||
tasks: [],
|
||||
members: [],
|
||||
resources: [],
|
||||
risks: [],
|
||||
timeline_nodes: []
|
||||
});
|
||||
|
||||
// 步骤标题
|
||||
const stepTitles = ["上传项目资料", "预览项目数据", "确认保存"];
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
dialogVisible.value = false;
|
||||
resetData();
|
||||
}
|
||||
|
||||
// 重置数据
|
||||
function resetData() {
|
||||
currentStep.value = WizardStep.Upload;
|
||||
fileList.value = [];
|
||||
Object.assign(projectData, {
|
||||
project: {
|
||||
project_name: "",
|
||||
project_type: "",
|
||||
description: "",
|
||||
objectives: "",
|
||||
plan_start_date: "",
|
||||
plan_end_date: "",
|
||||
budget: 0,
|
||||
currency: "CNY",
|
||||
priority: "medium",
|
||||
tags: []
|
||||
},
|
||||
milestones: [],
|
||||
tasks: [],
|
||||
members: [],
|
||||
resources: [],
|
||||
risks: [],
|
||||
timeline_nodes: []
|
||||
});
|
||||
}
|
||||
|
||||
// 文件上传前
|
||||
function beforeUpload(file: File) {
|
||||
const validTypes = [
|
||||
"application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/plain"
|
||||
];
|
||||
const isValidType = validTypes.includes(file.type);
|
||||
if (!isValidType) {
|
||||
message("请上传 PDF、Word 或文本文件", { type: "warning" });
|
||||
}
|
||||
return isValidType;
|
||||
}
|
||||
|
||||
// 文件变化
|
||||
function handleFileChange(file: any) {
|
||||
fileList.value = [file.raw];
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
function handleRemove() {
|
||||
fileList.value = [];
|
||||
}
|
||||
|
||||
// 上传并预览
|
||||
async function handleUploadAndPreview() {
|
||||
if (fileList.value.length === 0) {
|
||||
message("请先选择文件", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
uploading.value = true;
|
||||
try {
|
||||
const { code, data } = await previewProjectInit(fileList.value[0]);
|
||||
if (code === 200 && data) {
|
||||
Object.assign(projectData, data);
|
||||
currentStep.value = WizardStep.Preview;
|
||||
message("文件解析成功", { type: "success" });
|
||||
}
|
||||
} catch (error) {
|
||||
message("文件解析失败,请检查文件格式", { type: "error" });
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 上一步
|
||||
function handlePrev() {
|
||||
if (currentStep.value > WizardStep.Upload) {
|
||||
currentStep.value--;
|
||||
}
|
||||
}
|
||||
|
||||
// 下一步
|
||||
function handleNext() {
|
||||
if (currentStep.value < WizardStep.Confirm) {
|
||||
currentStep.value++;
|
||||
}
|
||||
}
|
||||
|
||||
// 确认保存
|
||||
async function handleConfirm() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const { code } = await confirmProjectInit(projectData);
|
||||
if (code === 200) {
|
||||
message("项目创建成功", { type: "success" });
|
||||
emit("success");
|
||||
handleClose();
|
||||
}
|
||||
} catch (error) {
|
||||
message("项目创建失败", { type: "error" });
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加里程碑
|
||||
function addMilestone() {
|
||||
projectData.milestones.push({
|
||||
milestone_name: "",
|
||||
description: "",
|
||||
plan_date: "",
|
||||
deliverables: "",
|
||||
owner_role: ""
|
||||
});
|
||||
}
|
||||
|
||||
// 删除里程碑
|
||||
function removeMilestone(index: number) {
|
||||
projectData.milestones.splice(index, 1);
|
||||
}
|
||||
|
||||
// 添加任务
|
||||
function addTask() {
|
||||
projectData.tasks.push({
|
||||
task_id: `task_${Date.now()}`,
|
||||
task_name: "",
|
||||
parent_task_id: "",
|
||||
description: "",
|
||||
plan_start_date: "",
|
||||
plan_end_date: "",
|
||||
estimated_hours: 0,
|
||||
priority: "medium",
|
||||
assignee_role: "",
|
||||
dependencies: [],
|
||||
deliverables: ""
|
||||
});
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
function removeTask(index: number) {
|
||||
projectData.tasks.splice(index, 1);
|
||||
}
|
||||
|
||||
// 添加成员
|
||||
function addMember() {
|
||||
projectData.members.push({
|
||||
name: "",
|
||||
role_code: "",
|
||||
responsibility: "",
|
||||
department: "",
|
||||
weekly_hours: 40
|
||||
});
|
||||
}
|
||||
|
||||
// 删除成员
|
||||
function removeMember(index: number) {
|
||||
projectData.members.splice(index, 1);
|
||||
}
|
||||
|
||||
// 添加风险
|
||||
function addRisk() {
|
||||
projectData.risks.push({
|
||||
risk_name: "",
|
||||
category: "",
|
||||
description: "",
|
||||
probability: 1,
|
||||
impact: 1,
|
||||
mitigation_plan: ""
|
||||
});
|
||||
}
|
||||
|
||||
// 删除风险
|
||||
function removeRisk(index: number) {
|
||||
projectData.risks.splice(index, 1);
|
||||
}
|
||||
|
||||
// 标签输入
|
||||
const tagInput = ref("");
|
||||
function handleTagInput() {
|
||||
if (tagInput.value && !projectData.project.tags?.includes(tagInput.value)) {
|
||||
projectData.project.tags?.push(tagInput.value);
|
||||
tagInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function removeTag(tag: string) {
|
||||
const index = projectData.project.tags?.indexOf(tag);
|
||||
if (index !== undefined && index > -1) {
|
||||
projectData.project.tags?.splice(index, 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="新建项目"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
:before-close="handleClose"
|
||||
destroy-on-close
|
||||
>
|
||||
<!-- 步骤条 -->
|
||||
<el-steps :active="currentStep" finish-status="success" class="mb-6">
|
||||
<el-step
|
||||
v-for="(title, index) in stepTitles"
|
||||
:key="index"
|
||||
:title="title"
|
||||
/>
|
||||
</el-steps>
|
||||
|
||||
<!-- 步骤 1: 上传文件 -->
|
||||
<div v-if="currentStep === WizardStep.Upload" class="upload-step">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:auto-upload="false"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleRemove"
|
||||
class="upload-area"
|
||||
>
|
||||
<el-icon class="el-icon--upload">
|
||||
<component :is="useRenderIcon(UploadIcon)" />
|
||||
</el-icon>
|
||||
<div class="el-upload__text">拖拽文件到此处或 <em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持 PDF、Word、TXT 格式的项目资料文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="uploading"
|
||||
:disabled="fileList.length === 0"
|
||||
@click="handleUploadAndPreview"
|
||||
>
|
||||
解析文件
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 2: 预览和编辑 -->
|
||||
<div v-if="currentStep === WizardStep.Preview" class="preview-step">
|
||||
<el-scrollbar height="500px">
|
||||
<el-form label-width="100px" class="project-form">
|
||||
<!-- 项目基本信息 -->
|
||||
<el-divider content-position="left">项目基本信息</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目名称">
|
||||
<el-input v-model="projectData.project.project_name" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="项目类型">
|
||||
<el-select
|
||||
v-model="projectData.project.project_type"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in ProjectTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开始日期">
|
||||
<el-date-picker
|
||||
v-model="projectData.project.plan_start_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="结束日期">
|
||||
<el-date-picker
|
||||
v-model="projectData.project.plan_end_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预算">
|
||||
<el-input-number
|
||||
v-model="projectData.project.budget"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="优先级">
|
||||
<el-select
|
||||
v-model="projectData.project.priority"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in PriorityOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="标签">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-tag
|
||||
v-for="tag in projectData.project.tags"
|
||||
:key="tag"
|
||||
closable
|
||||
@close="removeTag(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-model="tagInput"
|
||||
size="small"
|
||||
style="width: 100px"
|
||||
@keyup.enter="handleTagInput"
|
||||
@blur="handleTagInput"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目描述">
|
||||
<el-input
|
||||
v-model="projectData.project.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目目标">
|
||||
<el-input
|
||||
v-model="projectData.project.objectives"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 里程碑 -->
|
||||
<el-divider content-position="left">
|
||||
里程碑
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:icon="useRenderIcon(AddIcon)"
|
||||
@click="addMilestone"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</el-divider>
|
||||
<div
|
||||
v-for="(milestone, index) in projectData.milestones"
|
||||
:key="index"
|
||||
class="sub-form-item"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="7">
|
||||
<el-input
|
||||
v-model="milestone.milestone_name"
|
||||
placeholder="里程碑名称"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<el-date-picker
|
||||
v-model="milestone.plan_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="计划日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<el-input
|
||||
v-model="milestone.owner_role"
|
||||
placeholder="负责人角色"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="3">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
:icon="useRenderIcon(DeleteIcon)"
|
||||
@click="removeMilestone(index)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 任务清单 -->
|
||||
<el-divider content-position="left">
|
||||
任务清单
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:icon="useRenderIcon(AddIcon)"
|
||||
@click="addTask"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</el-divider>
|
||||
<div
|
||||
v-for="(task, index) in projectData.tasks"
|
||||
:key="index"
|
||||
class="sub-form-item"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8">
|
||||
<el-input v-model="task.task_name" placeholder="任务名称" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-date-picker
|
||||
v-model="task.plan_start_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="开始日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-date-picker
|
||||
v-model="task.plan_end_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="结束日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
:icon="useRenderIcon(DeleteIcon)"
|
||||
@click="removeTask(index)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 项目成员 -->
|
||||
<el-divider content-position="left">
|
||||
项目成员
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:icon="useRenderIcon(AddIcon)"
|
||||
@click="addMember"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</el-divider>
|
||||
<div
|
||||
v-for="(member, index) in projectData.members"
|
||||
:key="index"
|
||||
class="sub-form-item"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="6">
|
||||
<el-input v-model="member.name" placeholder="姓名" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-input v-model="member.role_code" placeholder="角色" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-input v-model="member.department" placeholder="部门" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
:icon="useRenderIcon(DeleteIcon)"
|
||||
@click="removeMember(index)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 风险识别 -->
|
||||
<el-divider content-position="left">
|
||||
风险识别
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:icon="useRenderIcon(AddIcon)"
|
||||
@click="addRisk"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</el-divider>
|
||||
<div
|
||||
v-for="(risk, index) in projectData.risks"
|
||||
:key="index"
|
||||
class="sub-form-item"
|
||||
>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8">
|
||||
<el-input v-model="risk.risk_name" placeholder="风险名称" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-select v-model="risk.category" placeholder="类别">
|
||||
<el-option label="技术风险" value="technical" />
|
||||
<el-option label="进度风险" value="schedule" />
|
||||
<el-option label="成本风险" value="cost" />
|
||||
<el-option label="人员风险" value="personnel" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-rate v-model="risk.probability" :max="5" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
:icon="useRenderIcon(DeleteIcon)"
|
||||
@click="removeRisk(index)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 3: 确认保存 -->
|
||||
<div v-if="currentStep === WizardStep.Confirm" class="confirm-step">
|
||||
<el-alert
|
||||
title="请确认以下项目信息"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="项目名称">
|
||||
{{ projectData.project.project_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="项目类型">
|
||||
{{ projectData.project.project_type }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="开始日期">
|
||||
{{ projectData.project.plan_start_date }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="结束日期">
|
||||
{{ projectData.project.plan_end_date }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="预算">
|
||||
{{ projectData.project.budget }} {{ projectData.project.currency }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="优先级">
|
||||
{{ projectData.project.priority }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-row :gutter="20" class="mt-4">
|
||||
<el-col :span="6">
|
||||
<el-statistic
|
||||
title="里程碑数量"
|
||||
:value="projectData.milestones.length"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="任务数量" :value="projectData.tasks.length" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="成员数量" :value="projectData.members.length" />
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="风险数量" :value="projectData.risks.length" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button v-if="currentStep > 0" @click="handlePrev">
|
||||
上一步
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="currentStep < WizardStep.Confirm"
|
||||
type="primary"
|
||||
@click="handleNext"
|
||||
>
|
||||
下一步
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="currentStep === WizardStep.Confirm"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确认创建
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-step {
|
||||
padding: 20px;
|
||||
|
||||
.upload-area {
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-step {
|
||||
.project-form {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.sub-form-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-step {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
467
src/views/project/index.vue
Normal file
467
src/views/project/index.vue
Normal file
@@ -0,0 +1,467 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useProject } from "./utils/hook";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import CreateProjectWizard from "./components/CreateProjectWizard.vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import AddIcon from "~icons/ri/add-line";
|
||||
import SearchIcon from "~icons/ri/search-line";
|
||||
import RefreshIcon from "~icons/ri/refresh-line";
|
||||
import MoreIcon from "~icons/ep/more-filled";
|
||||
import DeleteIcon from "~icons/ep/delete";
|
||||
import EditPenIcon from "~icons/ep/edit-pen";
|
||||
import ViewIcon from "~icons/ri/eye-line";
|
||||
import CalendarIcon from "~icons/ri/calendar-line";
|
||||
import UserIcon from "~icons/ri/user-line";
|
||||
|
||||
defineOptions({
|
||||
name: "Project"
|
||||
});
|
||||
|
||||
const wizardVisible = ref(false);
|
||||
|
||||
const {
|
||||
form,
|
||||
formRef,
|
||||
loading,
|
||||
dataList,
|
||||
pagination,
|
||||
statistics,
|
||||
activeFilter,
|
||||
statusFilterButtons,
|
||||
onSearch,
|
||||
resetForm,
|
||||
handleDelete,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
setFilter
|
||||
} = useProject();
|
||||
|
||||
// 打开新建项目向导
|
||||
function openWizard() {
|
||||
wizardVisible.value = true;
|
||||
}
|
||||
|
||||
// 向导成功回调
|
||||
function handleWizardSuccess() {
|
||||
onSearch();
|
||||
}
|
||||
|
||||
// 查看项目详情
|
||||
function handleView(row: any) {
|
||||
console.log("查看项目", row);
|
||||
}
|
||||
|
||||
// 编辑项目
|
||||
function handleEdit(row: any) {
|
||||
console.log("编辑项目", row);
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
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 getRiskType(risk?: string): "success" | "warning" | "danger" {
|
||||
switch (risk) {
|
||||
case "low":
|
||||
return "success";
|
||||
case "medium":
|
||||
return "warning";
|
||||
case "high":
|
||||
return "danger";
|
||||
default:
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="project-management w-full">
|
||||
<!-- 页面标题 -->
|
||||
<div class="flex-bc mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">项目管理</h2>
|
||||
<p class="text-gray-500 text-sm mt-1">
|
||||
管理所有项目的进度、资源分配和风险管控
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button>
|
||||
<template #icon>
|
||||
<component :is="useRenderIcon('ri/download-line')" />
|
||||
</template>
|
||||
导出报表
|
||||
</el-button>
|
||||
<el-button type="primary" @click="openWizard">
|
||||
<template #icon>
|
||||
<component :is="useRenderIcon(AddIcon)" />
|
||||
</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">
|
||||
{{ statistics.ongoingCount }}
|
||||
</p>
|
||||
<p class="text-xs text-green-500 mt-1">
|
||||
<el-icon
|
||||
><component :is="useRenderIcon('ri/arrow-up-line')"
|
||||
/></el-icon>
|
||||
较上月增加2个
|
||||
</p>
|
||||
</div>
|
||||
<div class="stat-icon bg-blue-100">
|
||||
<el-icon :size="24" color="#409eff">
|
||||
<component :is="useRenderIcon('ri/folder-line')" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</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">
|
||||
{{ statistics.completedCount }}
|
||||
</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('ri/check-line')" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</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">
|
||||
{{ statistics.highRiskCount }}
|
||||
</p>
|
||||
<p class="text-xs text-orange-400 mt-1">需要重点关注</p>
|
||||
</div>
|
||||
<div class="stat-icon bg-orange-100">
|
||||
<el-icon :size="24" color="#e6a23c">
|
||||
<component :is="useRenderIcon('ri/alert-line')" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</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">
|
||||
{{ statistics.averageProgress }}%
|
||||
</p>
|
||||
<el-progress
|
||||
:percentage="statistics.averageProgress"
|
||||
:show-text="false"
|
||||
class="mt-2"
|
||||
style="width: 100px"
|
||||
/>
|
||||
</div>
|
||||
<div class="stat-icon bg-purple-100">
|
||||
<el-icon :size="24" color="#9b59b6">
|
||||
<component :is="useRenderIcon('ri/bar-chart-line')" />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<el-card shadow="never" class="mb-4 filter-card">
|
||||
<div class="flex-bc flex-wrap gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button
|
||||
v-for="btn in statusFilterButtons"
|
||||
:key="btn.value"
|
||||
:type="activeFilter === btn.value ? 'primary' : ''"
|
||||
@click="setFilter(btn.value)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input
|
||||
v-model="form.keyword"
|
||||
placeholder="搜索项目名称..."
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="onSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<component :is="useRenderIcon(SearchIcon)" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="form.status"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="onSearch"
|
||||
>
|
||||
<el-option label="未开始" :value="0" />
|
||||
<el-option label="进行中" :value="1" />
|
||||
<el-option label="已完成" :value="2" />
|
||||
<el-option label="已延期" :value="3" />
|
||||
</el-select>
|
||||
|
||||
<el-button
|
||||
:icon="useRenderIcon(RefreshIcon)"
|
||||
@click="resetForm(formRef)"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 项目列表卡片 -->
|
||||
<div class="flex-bc mb-4">
|
||||
<h3 class="text-lg font-medium">项目列表</h3>
|
||||
<el-button type="primary" @click="openWizard">
|
||||
<template #icon>
|
||||
<component :is="useRenderIcon(AddIcon)" />
|
||||
</template>
|
||||
新建项目
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="!loading && dataList.length === 0"
|
||||
description="暂无参与的项目"
|
||||
class="py-12"
|
||||
>
|
||||
<template #image>
|
||||
<div class="empty-icon">
|
||||
<component
|
||||
:is="useRenderIcon('ri/folder-open-line')"
|
||||
style="font-size: 64px; color: var(--el-text-color-secondary)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="text-center">
|
||||
<p class="text-gray-500 mb-2">暂无参与的项目</p>
|
||||
<p class="text-xs text-gray-400">
|
||||
您还没有参与任何项目,可以创建一个新项目开始
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-button type="primary" @click="openWizard">
|
||||
<template #icon>
|
||||
<component :is="useRenderIcon(AddIcon)" />
|
||||
</template>
|
||||
创建项目
|
||||
</el-button>
|
||||
</el-empty>
|
||||
|
||||
<el-row v-else v-loading="loading" :gutter="16">
|
||||
<el-col
|
||||
v-for="item in dataList"
|
||||
:key="item.id"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
class="mb-4"
|
||||
>
|
||||
<el-card shadow="hover" class="project-card">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4
|
||||
class="font-medium text-base truncate"
|
||||
:title="item.projectName"
|
||||
>
|
||||
{{ item.projectName }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-400 mt-1 truncate">
|
||||
{{ item.projectCode || "暂无项目编号" }}
|
||||
</p>
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<el-button link>
|
||||
<component :is="useRenderIcon(MoreIcon)" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleView(item)">
|
||||
<component :is="useRenderIcon(ViewIcon)" class="mr-2" />
|
||||
查看详情
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="handleEdit(item)">
|
||||
<component :is="useRenderIcon(EditPenIcon)" class="mr-2" />
|
||||
编辑项目
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="handleDelete(item)">
|
||||
<component :is="useRenderIcon(DeleteIcon)" class="mr-2" />
|
||||
<span class="text-red-500">删除项目</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<el-tag
|
||||
:type="getStatusType(item.status)"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ item.status || "未知" }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="getRiskType(item.riskLevel)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ item.riskLevel || "未知" }}风险
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-sm text-gray-500 mb-3 line-clamp-2"
|
||||
style="min-height: 40px"
|
||||
>
|
||||
{{ item.myRole ? `我的角色: ${item.myRole}` : "暂无角色信息" }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 text-xs text-gray-400 mb-3">
|
||||
<span class="flex items-center gap-1">
|
||||
<component :is="useRenderIcon(CalendarIcon)" />
|
||||
{{
|
||||
item.planStartDate
|
||||
? dayjs(item.planStartDate).format("MM-DD")
|
||||
: "--"
|
||||
}}
|
||||
~
|
||||
{{
|
||||
item.planEndDate
|
||||
? dayjs(item.planEndDate).format("MM-DD")
|
||||
: "--"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-bc">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-avatar :size="28">
|
||||
<component :is="useRenderIcon(UserIcon)" />
|
||||
</el-avatar>
|
||||
<span class="text-sm">{{ item.managerName || "未分配" }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-progress
|
||||
:percentage="item.progress || 0"
|
||||
:status="item.progress === 100 ? 'success' : ''"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="flex justify-end mt-4">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[8, 12, 16, 20]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 新建项目向导 -->
|
||||
<CreateProjectWizard
|
||||
v-model:visible="wizardVisible"
|
||||
@success="handleWizardSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.project-management {
|
||||
padding: 16px 80px 16px 16px;
|
||||
|
||||
.stat-card {
|
||||
.stat-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
:deep(.el-card__body) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-card {
|
||||
cursor: pointer;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 10%);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item i) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.el-button:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
154
src/views/project/utils/hook.tsx
Normal file
154
src/views/project/utils/hook.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import { message } from "@/utils/message";
|
||||
import type { PaginationProps } from "@pureadmin/table";
|
||||
import type { ProjectItem, ProjectStatistics } from "@/api/project";
|
||||
import {
|
||||
getProjectList,
|
||||
getProjectStatistics,
|
||||
deleteProject
|
||||
} from "@/api/project";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
|
||||
export function useProject() {
|
||||
const form = reactive({
|
||||
keyword: "",
|
||||
status: ""
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const dataList = ref<ProjectItem[]>([]);
|
||||
const loading = ref(true);
|
||||
const statistics = ref<ProjectStatistics>({
|
||||
totalCount: 0,
|
||||
ongoingCount: 0,
|
||||
completedCount: 0,
|
||||
pausedCount: 0,
|
||||
planningCount: 0,
|
||||
cancelledCount: 0,
|
||||
draftCount: 0,
|
||||
statusCountMap: {},
|
||||
newThisMonth: 0,
|
||||
aboutToExpireCount: 0,
|
||||
averageProgress: 0,
|
||||
highRiskCount: 0
|
||||
});
|
||||
|
||||
const pagination = reactive<PaginationProps>({
|
||||
total: 0,
|
||||
pageSize: 12,
|
||||
currentPage: 1,
|
||||
background: true
|
||||
});
|
||||
|
||||
// 搜索
|
||||
async function onSearch() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { code, data } = await getProjectList({
|
||||
pageNum: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: form.keyword || undefined,
|
||||
status: form.status || undefined
|
||||
});
|
||||
|
||||
if (code === 200 && data) {
|
||||
dataList.value = data.rows;
|
||||
pagination.total = Number(data.total);
|
||||
}
|
||||
} catch {
|
||||
message("获取项目列表失败", { type: "error" });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
async function fetchStatistics() {
|
||||
try {
|
||||
const { code, data } = await getProjectStatistics();
|
||||
if (code === 200 && data) {
|
||||
statistics.value = data;
|
||||
}
|
||||
} catch {
|
||||
console.error("获取统计数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = formEl => {
|
||||
if (!formEl) return;
|
||||
formEl.resetFields();
|
||||
pagination.currentPage = 1;
|
||||
onSearch();
|
||||
};
|
||||
|
||||
// 分页大小变化
|
||||
function handleSizeChange(val: number) {
|
||||
pagination.pageSize = val;
|
||||
onSearch();
|
||||
}
|
||||
|
||||
// 页码变化
|
||||
function handleCurrentChange(val: number) {
|
||||
pagination.currentPage = val;
|
||||
onSearch();
|
||||
}
|
||||
|
||||
// 删除项目
|
||||
async function handleDelete(row: ProjectItem) {
|
||||
try {
|
||||
const { code } = await deleteProject(row.id!);
|
||||
if (code === 200) {
|
||||
message(`已删除项目 "${row.projectName}"`, { type: "success" });
|
||||
onSearch();
|
||||
fetchStatistics();
|
||||
}
|
||||
} catch {
|
||||
message("删除项目失败", { type: "error" });
|
||||
}
|
||||
}
|
||||
|
||||
// 状态筛选按钮
|
||||
const statusFilterButtons = [
|
||||
{ label: "全部项目", value: "" },
|
||||
{ label: "进行中", value: "ongoing" },
|
||||
{ label: "已完成", value: "completed" },
|
||||
{ label: "高风险", value: "high_risk" }
|
||||
];
|
||||
|
||||
// 当前激活的筛选
|
||||
const activeFilter = ref("");
|
||||
|
||||
// 设置筛选
|
||||
function setFilter(filterValue: string) {
|
||||
activeFilter.value = filterValue;
|
||||
if (filterValue === "high_risk") {
|
||||
form.status = "";
|
||||
} else {
|
||||
form.status = filterValue;
|
||||
}
|
||||
pagination.currentPage = 1;
|
||||
onSearch();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onSearch();
|
||||
fetchStatistics();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
formRef,
|
||||
loading,
|
||||
dataList,
|
||||
pagination,
|
||||
statistics,
|
||||
activeFilter,
|
||||
statusFilterButtons,
|
||||
onSearch,
|
||||
resetForm,
|
||||
handleDelete,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
setFilter
|
||||
};
|
||||
}
|
||||
79
src/views/project/utils/types.ts
Normal file
79
src/views/project/utils/types.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type {
|
||||
ProjectInfo,
|
||||
MilestoneInfo,
|
||||
TaskInfo,
|
||||
MemberInfo,
|
||||
ResourceInfo,
|
||||
RiskInfo,
|
||||
TimelineNodeInfo,
|
||||
ProjectItem,
|
||||
ProjectStatistics
|
||||
} from "@/api/project";
|
||||
|
||||
/** 项目查询参数 */
|
||||
export interface ProjectQueryParams {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/** 优先级选项 */
|
||||
export const PriorityOptions = [
|
||||
{ label: "最高", value: "highest" },
|
||||
{ label: "高", value: "high" },
|
||||
{ label: "中", value: "medium" },
|
||||
{ label: "低", value: "low" },
|
||||
{ label: "最低", value: "lowest" }
|
||||
];
|
||||
|
||||
/** 项目类型选项 */
|
||||
export const ProjectTypeOptions = [
|
||||
{ label: "建筑工程", value: "construction" },
|
||||
{ label: "软件开发", value: "software" },
|
||||
{ label: "产品研发", value: "product" },
|
||||
{ label: "市场推广", value: "marketing" },
|
||||
{ label: "其他", value: "other" }
|
||||
];
|
||||
|
||||
/** 甘特图数据项 */
|
||||
export interface GanttItem {
|
||||
id: string;
|
||||
name: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
progress: number;
|
||||
status: number;
|
||||
riskLevel?: number;
|
||||
ownerName?: string;
|
||||
}
|
||||
|
||||
/** 新建项目表单数据 */
|
||||
export interface CreateProjectForm {
|
||||
project: ProjectInfo;
|
||||
milestones: MilestoneInfo[];
|
||||
tasks: TaskInfo[];
|
||||
members: MemberInfo[];
|
||||
resources: ResourceInfo[];
|
||||
risks: RiskInfo[];
|
||||
timeline_nodes: TimelineNodeInfo[];
|
||||
}
|
||||
|
||||
/** 步骤枚举 */
|
||||
export enum WizardStep {
|
||||
Upload = 0,
|
||||
Preview = 1,
|
||||
Confirm = 2
|
||||
}
|
||||
|
||||
export type {
|
||||
ProjectInfo,
|
||||
MilestoneInfo,
|
||||
TaskInfo,
|
||||
MemberInfo,
|
||||
ResourceInfo,
|
||||
RiskInfo,
|
||||
TimelineNodeInfo,
|
||||
ProjectItem,
|
||||
ProjectStatistics
|
||||
};
|
||||
Reference in New Issue
Block a user