- 新增项目菜单项及路由配置,支持项目管理入口 - 实现项目相关API接口,包括项目列表、统计、甘特图及项目初始化接口 - 添加项目新建向导组件,支持上传文件预览及确认保存 - 实现项目管理页面,包含项目列表展示、筛选、统计卡片及新建项目操作 - 支持项目基本信息、里程碑、任务、成员及风险等多维度管理数据录入 - 优化页面交互体验,支持上传文件格式校验及数据编辑预览 - 提供状态及风险等级标签显示,辅助项目状态快速识别
This commit is contained in:
@@ -69,6 +69,7 @@ panel:
|
|||||||
pureMultiTagsCache: MultiTags Cache
|
pureMultiTagsCache: MultiTags Cache
|
||||||
menus:
|
menus:
|
||||||
pureHome: Home
|
pureHome: Home
|
||||||
|
pureProject: Project Management
|
||||||
pureLogin: Login
|
pureLogin: Login
|
||||||
pureEmpty: Empty Page
|
pureEmpty: Empty Page
|
||||||
pureTable: Table
|
pureTable: Table
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ panel:
|
|||||||
pureMultiTagsCache: 页签持久化
|
pureMultiTagsCache: 页签持久化
|
||||||
menus:
|
menus:
|
||||||
pureHome: 首页
|
pureHome: 首页
|
||||||
|
pureProject: 项目管理
|
||||||
pureLogin: 登录
|
pureLogin: 登录
|
||||||
pureEmpty: 无Layout页
|
pureEmpty: 无Layout页
|
||||||
pureTable: 表格
|
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",
|
"project": {
|
||||||
"info": {
|
"project_name": "AIHR 智能简历筛选系统",
|
||||||
"title": "默认模块",
|
"project_type": "研发项目",
|
||||||
"description": "",
|
"description": "利用大语言模型(LLM)和自然语言处理(NLP)技术,实现简历的自动化解析、人岗匹配度评分及候选人排序,提升招聘效率。",
|
||||||
"version": "1.0.0"
|
"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": [],
|
"milestones": [
|
||||||
"paths": {
|
{
|
||||||
"/api/v1/system/role/{id}/menu-ids": {
|
"milestone_name": "M1 需求分析与架构设计",
|
||||||
"get": {
|
"description": "需求评审通过,技术架构可行性验证完成。",
|
||||||
"summary": "查询角色的菜单权限ID列表(只返回菜单类型的权限)",
|
"plan_date": "2026-04-30",
|
||||||
"deprecated": false,
|
"deliverables": "《需求规格说明书》、《系统架构设计文档》、《数据库设计文档》",
|
||||||
"description": "",
|
"owner_role": "产品负责人"
|
||||||
"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": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"responses": {},
|
{
|
||||||
"securitySchemes": {}
|
"milestone_name": "M2 核心算法模型训练与验证",
|
||||||
},
|
"description": "模型在测试集上的准确率>85%,解析字段覆盖率>95%。",
|
||||||
"servers": [],
|
"plan_date": "2026-06-15",
|
||||||
"security": []
|
"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 开始
|
const home = 0, // 平台规定只有 home 路由的 rank 才能为 0 ,所以后端在返回 rank 的时候需要从非 0 开始
|
||||||
chatai = 1,
|
chatai = 1,
|
||||||
vueflow = 2,
|
project = 2,
|
||||||
ganttastic = 3,
|
vueflow = 3,
|
||||||
components = 4,
|
ganttastic = 4,
|
||||||
able = 5,
|
components = 5,
|
||||||
table = 6,
|
able = 6,
|
||||||
form = 7,
|
table = 7,
|
||||||
list = 8,
|
form = 8,
|
||||||
result = 9,
|
list = 9,
|
||||||
error = 10,
|
result = 10,
|
||||||
frame = 11,
|
error = 11,
|
||||||
nested = 12,
|
frame = 12,
|
||||||
permission = 13,
|
nested = 13,
|
||||||
system = 14,
|
permission = 14,
|
||||||
monitor = 15,
|
system = 15,
|
||||||
tabs = 16,
|
monitor = 16,
|
||||||
about = 17,
|
tabs = 17,
|
||||||
codemirror = 18,
|
about = 18,
|
||||||
markdown = 19,
|
codemirror = 19,
|
||||||
editor = 20,
|
markdown = 20,
|
||||||
flowchart = 21,
|
editor = 21,
|
||||||
formdesign = 22,
|
flowchart = 22,
|
||||||
board = 23,
|
formdesign = 23,
|
||||||
ppt = 24,
|
board = 24,
|
||||||
mind = 25,
|
ppt = 25,
|
||||||
guide = 26,
|
mind = 26,
|
||||||
menuoverflow = 27;
|
guide = 27,
|
||||||
|
menuoverflow = 28;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
home,
|
home,
|
||||||
chatai,
|
chatai,
|
||||||
|
project,
|
||||||
vueflow,
|
vueflow,
|
||||||
ganttastic,
|
ganttastic,
|
||||||
components,
|
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
|
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
|
||||||
const defaultConfig: AxiosRequestConfig = {
|
const defaultConfig: AxiosRequestConfig = {
|
||||||
// 请求超时时间
|
// 请求超时时间
|
||||||
timeout: 10000,
|
timeout: 600000,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json, text/plain, */*",
|
Accept: "application/json, text/plain, */*",
|
||||||
"Content-Type": "application/json",
|
"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