feat(project): 新增项目管理功能模块
Some checks failed
Lint Code / Lint Code (push) Failing after 6m48s

- 新增项目菜单项及路由配置,支持项目管理入口
- 实现项目相关API接口,包括项目列表、统计、甘特图及项目初始化接口
- 添加项目新建向导组件,支持上传文件预览及确认保存
- 实现项目管理页面,包含项目列表展示、筛选、统计卡片及新建项目操作
- 支持项目基本信息、里程碑、任务、成员及风险等多维度管理数据录入
- 优化页面交互体验,支持上传文件格式校验及数据编辑预览
- 提供状态及风险等级标签显示,辅助项目状态快速识别
This commit is contained in:
2026-03-28 15:25:03 +08:00
parent ce2f4767f1
commit 87bdef6416
11 changed files with 2125 additions and 104 deletions

View File

@@ -69,6 +69,7 @@ panel:
pureMultiTagsCache: MultiTags Cache
menus:
pureHome: Home
pureProject: Project Management
pureLogin: Login
pureEmpty: Empty Page
pureTable: Table

View File

@@ -69,6 +69,7 @@ panel:
pureMultiTagsCache: 页签持久化
menus:
pureHome: 首页
pureProject: 项目管理
pureLogin: 登录
pureEmpty: 无Layout页
pureTable: 表格

241
src/api/project.ts Normal file
View 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 }
);
};

View File

@@ -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"]
}
]
}

View 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,

View 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;

View File

@@ -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",

View 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">
支持 PDFWordTXT 格式的项目资料文件
</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
View 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>

View 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
};
}

View 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
};