# 项目管理模块实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在系统管理菜单下新增项目管理模块,支持甘特图、日历、表格、看板四种视图管理项目进度 **Architecture:** 后端新增 project 模块(5 Entity + 3 Service + 5 Controller),前端新增 project 模块(2 页面 + 7 组件 + 1 Pinia Store)。甘特图使用 dhtmlx-gantt,日历使用 FullCalendar,看板使用 vuedraggable。 **Tech Stack:** Midway.js + TypeORM (后端), Vue 3 + Element Plus + dhtmlx-gantt + FullCalendar + vuedraggable (前端) **Spec:** `docs/superpowers/specs/2026-04-12-project-management-design.md` --- ## 文件结构 ### 后端新建文件 ``` packages/backend/src/modules/project/ ├── config.ts ├── entity/ │ ├── info.ts -- ProjectInfoEntity (project_info) │ ├── phase.ts -- ProjectPhaseEntity (project_phase) │ ├── task.ts -- ProjectTaskEntity (project_task) │ ├── task_dependency.ts -- ProjectTaskDependencyEntity (project_task_dependency) │ └── time_log.ts -- ProjectTimeLogEntity (project_time_log) ├── controller/admin/ │ ├── info.ts -- 项目 CRUD │ ├── phase.ts -- 阶段 CRUD │ ├── task.ts -- 任务 CRUD + ganttData/ganttUpdate/kanban/kanbanSort/tree │ ├── task_dependency.ts -- 依赖关系 add/delete/list │ └── time_log.ts -- 工时记录 CRUD └── service/ ├── info.ts -- 项目进度汇总 ├── task.ts -- 任务树构建、进度计算、看板数据 └── gantt.ts -- 甘特图数据聚合、批量更新 ``` ### 前端新建文件 ``` packages/frontend/src/modules/project/ ├── config.ts -- 模块路由配置 ├── store/ │ └── project.ts -- Pinia store ├── views/ │ ├── list.vue -- 项目列表页 │ ├── detail.vue -- 项目详情页(Tab 容器) │ └── components/ │ ├── gantt.vue -- 甘特图视图 │ ├── calendar.vue -- 日历视图 │ ├── table.vue -- 表格列表视图 │ ├── kanban.vue -- 看板视图 │ ├── task-drawer.vue -- 任务详情抽屉 │ ├── time-log-dialog.vue -- 工时记录弹窗 │ └── phase-manager.vue -- 阶段管理弹窗 ``` --- ## Task 1: 后端 Entity 定义(5 张表) **Files:** - Create: `packages/backend/src/modules/project/entity/info.ts` - Create: `packages/backend/src/modules/project/entity/phase.ts` - Create: `packages/backend/src/modules/project/entity/task.ts` - Create: `packages/backend/src/modules/project/entity/task_dependency.ts` - Create: `packages/backend/src/modules/project/entity/time_log.ts` - [ ] **Step 1: 创建 project 模块目录结构** ```bash cd packages/backend mkdir -p src/modules/project/entity mkdir -p src/modules/project/controller/admin mkdir -p src/modules/project/service ``` - [ ] **Step 2: 创建 ProjectInfoEntity** 创建 `packages/backend/src/modules/project/entity/info.ts`: ```typescript import { BaseEntity } from '../../base/entity/base'; import { Column, Entity, Index } from 'typeorm'; /** * 项目信息 */ @Entity('project_info') export class ProjectInfoEntity extends BaseEntity { @Column({ comment: '项目名称', length: 100 }) name: string; @Column({ comment: '项目描述', type: 'text', nullable: true }) description: string; @Column({ comment: '状态 0未开始 1进行中 2已完成 3已归档', default: 0 }) status: number; @Column({ comment: '计划开始日期', type: 'date', nullable: true }) startDate: string; @Column({ comment: '计划结束日期', type: 'date', nullable: true }) endDate: string; @Column({ comment: '进度百分比 0-100', default: 0 }) progress: number; @Index() @Column({ comment: '项目经理ID', nullable: true }) ownerId: number; @Column({ comment: '项目经理姓名', length: 50, nullable: true }) ownerName: string; @Column({ comment: '主题色', length: 20, nullable: true }) color: string; } ``` - [ ] **Step 3: 创建 ProjectPhaseEntity** 创建 `packages/backend/src/modules/project/entity/phase.ts`: ```typescript import { BaseEntity } from '../../base/entity/base'; import { Column, Entity, Index } from 'typeorm'; /** * 项目阶段 */ @Entity('project_phase') export class ProjectPhaseEntity extends BaseEntity { @Index() @Column({ comment: '所属项目ID' }) projectId: number; @Column({ comment: '阶段名称', length: 100 }) name: string; @Column({ comment: '分类', length: 50, nullable: true }) type: string; @Column({ comment: '状态 0未开始 1进行中 2已完成', default: 0 }) status: number; @Column({ comment: '开始日期', type: 'date', nullable: true }) startDate: string; @Column({ comment: '结束日期', type: 'date', nullable: true }) endDate: string; @Column({ comment: '进度 0-100', default: 0 }) progress: number; @Column({ comment: '排序序号', default: 0 }) sortOrder: number; } ``` - [ ] **Step 4: 创建 ProjectTaskEntity** 创建 `packages/backend/src/modules/project/entity/task.ts`: ```typescript import { BaseEntity } from '../../base/entity/base'; import { Column, Entity, Index } from 'typeorm'; /** * 项目任务 */ @Entity('project_task') export class ProjectTaskEntity extends BaseEntity { @Index() @Column({ comment: '所属项目ID' }) projectId: number; @Index() @Column({ comment: '所属阶段ID', nullable: true }) phaseId: number; @Index() @Column({ comment: '父任务ID', nullable: true }) parentId: number; @Column({ comment: '任务名称', length: 200 }) name: string; @Column({ comment: '任务描述', type: 'text', nullable: true }) description: string; @Column({ comment: '状态 0待办 1进行中 2已完成 3已关闭', default: 0 }) status: number; @Column({ comment: '优先级 0紧急 1高 2中 3低', default: 2 }) priority: number; @Column({ comment: '分类', length: 50, nullable: true }) category: string; @Index() @Column({ comment: '负责人ID', nullable: true }) assigneeId: number; @Column({ comment: '负责人姓名', length: 50, nullable: true }) assigneeName: string; @Column({ comment: '计划开始日期', type: 'date', nullable: true }) startDate: string; @Column({ comment: '计划结束日期', type: 'date', nullable: true }) endDate: string; @Column({ comment: '预估工时(小时)', type: 'decimal', precision: 8, scale: 1, default: 0 }) estimatedHours: number; @Column({ comment: '实际工时(小时)', type: 'decimal', precision: 8, scale: 1, default: 0 }) actualHours: number; @Column({ comment: '进度 0-100', default: 0 }) progress: number; @Column({ comment: '排序序号', default: 0 }) sortOrder: number; @Column({ comment: '自定义颜色', length: 20, nullable: true }) color: string; } ``` - [ ] **Step 5: 创建 ProjectTaskDependencyEntity** 创建 `packages/backend/src/modules/project/entity/task_dependency.ts`: ```typescript import { BaseEntity } from '../../base/entity/base'; import { Column, Entity, Index } from 'typeorm'; /** * 任务依赖关系 */ @Entity('project_task_dependency') export class ProjectTaskDependencyEntity extends BaseEntity { @Index() @Column({ comment: '当前任务ID' }) taskId: number; @Index() @Column({ comment: '前置任务ID' }) dependsOnTaskId: number; @Column({ comment: '依赖类型 0:FS 1:SS 2:FF 3:SF', default: 0 }) type: number; } ``` - [ ] **Step 6: 创建 ProjectTimeLogEntity** 创建 `packages/backend/src/modules/project/entity/time_log.ts`: ```typescript import { BaseEntity } from '../../base/entity/base'; import { Column, Entity, Index } from 'typeorm'; /** * 工时记录 */ @Entity('project_time_log') export class ProjectTimeLogEntity extends BaseEntity { @Index() @Column({ comment: '所属任务ID' }) taskId: number; @Index() @Column({ comment: '记录人ID' }) userId: number; @Column({ comment: '记录人姓名', length: 50 }) userName: string; @Column({ comment: '工作日期', type: 'date' }) logDate: string; @Column({ comment: '工时(小时)', type: 'decimal', precision: 5, scale: 1 }) hours: number; @Column({ comment: '工作内容描述', length: 500, nullable: true }) description: string; } ``` - [ ] **Step 7: 创建模块 config.ts** 创建 `packages/backend/src/modules/project/config.ts`: ```typescript import { ModuleConfig } from '@cool-midway/core'; /** * 模块配置 */ export default () => { return { // 模块名称 name: '项目管理', // 模块描述 description: '项目进度管理,支持甘特图、日历、看板等视图', // 中间件 middlewares: [], // 全局中间件 globalMiddlewares: [], // 模块加载顺序 order: 0, } as ModuleConfig; }; ``` - [ ] **Step 8: 启动后端验证表自动创建** ```bash cd packages/backend pnpm dev ``` 预期:后端启动成功,TypeORM synchronize:true 自动在 neta_test 数据库中创建 5 张表(project_info, project_phase, project_task, project_task_dependency, project_time_log)。检查日志无报错。 - [ ] **Step 9: 提交** ```bash git add packages/backend/src/modules/project/ git commit -m "feat(backend): 项目管理模块 - Entity 定义(5张表)" ``` --- ## Task 2: 后端 Service 层 **Files:** - Create: `packages/backend/src/modules/project/service/info.ts` - Create: `packages/backend/src/modules/project/service/task.ts` - Create: `packages/backend/src/modules/project/service/gantt.ts` - [ ] **Step 1: 创建 ProjectInfoService** 创建 `packages/backend/src/modules/project/service/info.ts`: ```typescript import { Provide } from '@midwayjs/core'; import { BaseService } from '@cool-midway/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { ProjectInfoEntity } from '../entity/info'; import { ProjectPhaseEntity } from '../entity/phase'; import { ProjectTaskEntity } from '../entity/task'; /** * 项目信息服务 */ @Provide() export class ProjectInfoService extends BaseService { @InjectEntityModel(ProjectInfoEntity) projectInfoEntity: Repository; @InjectEntityModel(ProjectPhaseEntity) projectPhaseEntity: Repository; @InjectEntityModel(ProjectTaskEntity) projectTaskEntity: Repository; /** * 重新计算项目进度(由各阶段加权平均) */ async recalcProgress(projectId: number) { const phases = await this.projectPhaseEntity.find({ where: { projectId }, }); if (phases.length === 0) { await this.projectInfoEntity.update(projectId, { progress: 0 }); return; } const total = phases.reduce((sum, p) => sum + p.progress, 0); const progress = Math.round(total / phases.length); await this.projectInfoEntity.update(projectId, { progress }); } /** * 删除项目时级联删除阶段和任务 */ async delete(ids: number[]) { await super.delete(ids); for (const id of ids) { await this.projectPhaseEntity.delete({ projectId: id }); await this.projectTaskEntity.delete({ projectId: id }); } } } ``` - [ ] **Step 2: 创建 ProjectTaskService** 创建 `packages/backend/src/modules/project/service/task.ts`: ```typescript import { Inject, Provide } from '@midwayjs/core'; import { BaseService } from '@cool-midway/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository, In } from 'typeorm'; import { ProjectTaskEntity } from '../entity/task'; import { ProjectPhaseEntity } from '../entity/phase'; import { ProjectTimeLogEntity } from '../entity/time_log'; import { ProjectTaskDependencyEntity } from '../entity/task_dependency'; import { ProjectInfoService } from './info'; /** * 任务服务 */ @Provide() export class ProjectTaskService extends BaseService { @InjectEntityModel(ProjectTaskEntity) projectTaskEntity: Repository; @InjectEntityModel(ProjectPhaseEntity) projectPhaseEntity: Repository; @InjectEntityModel(ProjectTimeLogEntity) projectTimeLogEntity: Repository; @InjectEntityModel(ProjectTaskDependencyEntity) projectTaskDependencyEntity: Repository; @Inject() projectInfoService: ProjectInfoService; /** * 获取任务树(项目下所有任务,按阶段分组,含子任务层级) */ async tree(projectId: number) { const tasks = await this.projectTaskEntity.find({ where: { projectId }, order: { sortOrder: 'ASC', createTime: 'ASC' }, }); return this.buildTree(tasks, null); } /** * 构建树形结构 */ private buildTree(tasks: ProjectTaskEntity[], parentId: number | null) { return tasks .filter(t => t.parentId === parentId) .map(t => ({ ...t, children: this.buildTree(tasks, t.id), })); } /** * 获取看板数据(按状态分组) */ async kanban(projectId: number) { const tasks = await this.projectTaskEntity.find({ where: { projectId }, order: { sortOrder: 'ASC', createTime: 'ASC' }, }); return { todo: tasks.filter(t => t.status === 0), inProgress: tasks.filter(t => t.status === 1), done: tasks.filter(t => t.status === 2), closed: tasks.filter(t => t.status === 3), }; } /** * 看板排序/状态变更 */ async kanbanSort(items: { id: number; status: number; sortOrder: number }[]) { for (const item of items) { await this.projectTaskEntity.update(item.id, { status: item.status, sortOrder: item.sortOrder, }); } } /** * 重新计算阶段进度(由下属任务加权平均) */ async recalcPhaseProgress(phaseId: number) { const tasks = await this.projectTaskEntity.find({ where: { phaseId, parentId: null }, }); if (tasks.length === 0) return; const hasEstimated = tasks.some(t => t.estimatedHours > 0); let progress: number; if (hasEstimated) { const totalWeight = tasks.reduce((sum, t) => sum + (t.estimatedHours || 1), 0); const weightedSum = tasks.reduce( (sum, t) => sum + t.progress * (t.estimatedHours || 1), 0 ); progress = Math.round(weightedSum / totalWeight); } else { progress = Math.round( tasks.reduce((sum, t) => sum + t.progress, 0) / tasks.length ); } await this.projectPhaseEntity.update(phaseId, { progress }); } /** * 重新计算任务工时(汇总工时记录) */ async recalcTaskHours(taskId: number) { const result = await this.projectTimeLogEntity .createQueryBuilder('tl') .select('SUM(tl.hours)', 'total') .where('tl.taskId = :taskId', { taskId }) .getRawOne(); const actualHours = parseFloat(result.total) || 0; await this.projectTaskEntity.update(taskId, { actualHours }); } /** * 任务更新后触发进度向上汇总 */ async afterUpdate(task: ProjectTaskEntity) { if (task.phaseId) { await this.recalcPhaseProgress(task.phaseId); const phase = await this.projectPhaseEntity.findOneBy({ id: task.phaseId }); if (phase) { await this.projectInfoService.recalcProgress(phase.projectId); } } } /** * 删除任务时级联删除子任务、依赖、工时记录 */ async delete(ids: number[]) { await super.delete(ids); for (const id of ids) { // 删除子任务 const children = await this.projectTaskEntity.find({ where: { parentId: id } }); if (children.length > 0) { await this.delete(children.map(c => c.id)); } // 删除依赖关系 await this.projectTaskDependencyEntity .createQueryBuilder() .delete() .where('taskId = :id OR dependsOnTaskId = :id', { id }) .execute(); // 删除工时记录 await this.projectTimeLogEntity.delete({ taskId: id }); } } } ``` - [ ] **Step 3: 创建 ProjectGanttService** 创建 `packages/backend/src/modules/project/service/gantt.ts`: ```typescript import { Provide } from '@midwayjs/core'; import { BaseService } from '@cool-midway/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { ProjectTaskEntity } from '../entity/task'; import { ProjectPhaseEntity } from '../entity/phase'; import { ProjectTaskDependencyEntity } from '../entity/task_dependency'; /** * 甘特图数据服务 */ @Provide() export class ProjectGanttService extends BaseService { @InjectEntityModel(ProjectTaskEntity) projectTaskEntity: Repository; @InjectEntityModel(ProjectPhaseEntity) projectPhaseEntity: Repository; @InjectEntityModel(ProjectTaskDependencyEntity) projectTaskDependencyEntity: Repository; /** * 获取甘特图数据(DHTMLX Gantt 格式) */ async ganttData(projectId: number) { const phases = await this.projectPhaseEntity.find({ where: { projectId }, order: { sortOrder: 'ASC' }, }); const tasks = await this.projectTaskEntity.find({ where: { projectId }, order: { sortOrder: 'ASC', createTime: 'ASC' }, }); const deps = await this.projectTaskDependencyEntity .createQueryBuilder('d') .where('d.taskId IN (:...taskIds)', { taskIds: tasks.length > 0 ? tasks.map(t => t.id) : [0], }) .orWhere('d.dependsOnTaskId IN (:...depIds)', { depIds: tasks.length > 0 ? tasks.map(t => t.id) : [0], }) .getMany(); // 组装 DHTMLX Gantt data 数组 const data = []; // 阶段作为分组节点 for (const phase of phases) { data.push({ id: `p_${phase.id}`, text: phase.name, start_date: phase.startDate || '', end_date: phase.endDate || '', progress: phase.progress / 100, type: 'project', open: true, sortOrder: phase.sortOrder, status: phase.status, phaseType: phase.type, }); } // 任务 for (const task of tasks) { let parent = ''; if (task.parentId) { parent = `t_${task.parentId}`; } else if (task.phaseId) { parent = `p_${task.phaseId}`; } data.push({ id: `t_${task.id}`, text: task.name, start_date: task.startDate || '', end_date: task.endDate || '', progress: task.progress / 100, parent, priority: task.priority, status: task.status, category: task.category, assignee: task.assigneeName, assigneeId: task.assigneeId, estimatedHours: task.estimatedHours, actualHours: task.actualHours, color: task.color, sortOrder: task.sortOrder, }); } // 依赖关系 → links const links = deps.map(d => ({ id: d.id, source: `t_${d.dependsOnTaskId}`, target: `t_${d.taskId}`, type: String(d.type), })); return { data, links }; } /** * 甘特图拖拽批量更新 */ async ganttUpdate( items: { id: string; start_date: string; end_date: string; sortOrder?: number; parent?: string }[] ) { for (const item of items) { if (item.id.startsWith('p_')) { const phaseId = parseInt(item.id.replace('p_', '')); await this.projectPhaseEntity.update(phaseId, { startDate: item.start_date, endDate: item.end_date, ...(item.sortOrder !== undefined ? { sortOrder: item.sortOrder } : {}), }); } else if (item.id.startsWith('t_')) { const taskId = parseInt(item.id.replace('t_', '')); const updateData: any = { startDate: item.start_date, endDate: item.end_date, }; if (item.sortOrder !== undefined) { updateData.sortOrder = item.sortOrder; } if (item.parent) { if (item.parent.startsWith('p_')) { updateData.phaseId = parseInt(item.parent.replace('p_', '')); updateData.parentId = null; } else if (item.parent.startsWith('t_')) { updateData.parentId = parseInt(item.parent.replace('t_', '')); } } await this.projectTaskEntity.update(taskId, updateData); } } } } ``` - [ ] **Step 4: 验证 Service 层编译通过** ```bash cd packages/backend npx tsc --noEmit ``` 预期:无编译错误。 - [ ] **Step 5: 提交** ```bash git add packages/backend/src/modules/project/service/ git commit -m "feat(backend): 项目管理模块 - Service 层(进度计算、甘特图数据、看板)" ``` --- ## Task 3: 后端 Controller 层 **Files:** - Create: `packages/backend/src/modules/project/controller/admin/info.ts` - Create: `packages/backend/src/modules/project/controller/admin/phase.ts` - Create: `packages/backend/src/modules/project/controller/admin/task.ts` - Create: `packages/backend/src/modules/project/controller/admin/task_dependency.ts` - Create: `packages/backend/src/modules/project/controller/admin/time_log.ts` - [ ] **Step 1: 创建 ProjectInfoController** 创建 `packages/backend/src/modules/project/controller/admin/info.ts`: ```typescript import { Provide } from '@midwayjs/core'; import { CoolController, BaseController } from '@cool-midway/core'; import { ProjectInfoEntity } from '../../entity/info'; import { ProjectInfoService } from '../../service/info'; /** * 项目管理 */ @Provide() @CoolController({ api: ['add', 'delete', 'update', 'info', 'list', 'page'], entity: ProjectInfoEntity, service: ProjectInfoService, pageQueryOp: { keyWordLikeFields: ['name', 'ownerName'], fieldEq: ['status'], addOrderBy: { createTime: 'DESC', }, }, }) export class AdminProjectInfoController extends BaseController {} ``` - [ ] **Step 2: 创建 ProjectPhaseController** 创建 `packages/backend/src/modules/project/controller/admin/phase.ts`: ```typescript import { Provide } from '@midwayjs/core'; import { CoolController, BaseController } from '@cool-midway/core'; import { ProjectPhaseEntity } from '../../entity/phase'; /** * 项目阶段 */ @Provide() @CoolController({ api: ['add', 'delete', 'update', 'info', 'list', 'page'], entity: ProjectPhaseEntity, listQueryOp: { fieldEq: ['projectId'], addOrderBy: { sortOrder: 'ASC', }, }, }) export class AdminProjectPhaseController extends BaseController {} ``` - [ ] **Step 3: 创建 ProjectTaskController(含自定义接口)** 创建 `packages/backend/src/modules/project/controller/admin/task.ts`: ```typescript import { Body, Get, Inject, Post, Provide, Query } from '@midwayjs/core'; import { CoolController, BaseController } from '@cool-midway/core'; import { ProjectTaskEntity } from '../../entity/task'; import { ProjectTaskService } from '../../service/task'; import { ProjectGanttService } from '../../service/gantt'; /** * 项目任务 */ @Provide() @CoolController({ api: ['add', 'delete', 'update', 'info', 'list', 'page'], entity: ProjectTaskEntity, service: ProjectTaskService, pageQueryOp: { keyWordLikeFields: ['name', 'assigneeName'], fieldEq: ['projectId', 'phaseId', 'status', 'priority', 'category', 'assigneeId'], addOrderBy: { sortOrder: 'ASC', createTime: 'ASC', }, }, }) export class AdminProjectTaskController extends BaseController { @Inject() projectTaskService: ProjectTaskService; @Inject() projectGanttService: ProjectGanttService; @Get('/tree', { summary: '任务树' }) async tree(@Query('projectId') projectId: number) { return this.ok(await this.projectTaskService.tree(projectId)); } @Get('/ganttData', { summary: '甘特图数据' }) async ganttData(@Query('projectId') projectId: number) { return this.ok(await this.projectGanttService.ganttData(projectId)); } @Post('/ganttUpdate', { summary: '甘特图拖拽更新' }) async ganttUpdate(@Body('items') items: any[]) { await this.projectGanttService.ganttUpdate(items); return this.ok(); } @Get('/kanban', { summary: '看板数据' }) async kanban(@Query('projectId') projectId: number) { return this.ok(await this.projectTaskService.kanban(projectId)); } @Post('/kanbanSort', { summary: '看板排序' }) async kanbanSort(@Body('items') items: any[]) { await this.projectTaskService.kanbanSort(items); return this.ok(); } } ``` - [ ] **Step 4: 创建 ProjectTaskDependencyController** 创建 `packages/backend/src/modules/project/controller/admin/task_dependency.ts`: ```typescript import { Provide } from '@midwayjs/core'; import { CoolController, BaseController } from '@cool-midway/core'; import { ProjectTaskDependencyEntity } from '../../entity/task_dependency'; /** * 任务依赖关系 */ @Provide() @CoolController({ api: ['add', 'delete', 'list'], entity: ProjectTaskDependencyEntity, listQueryOp: { fieldEq: ['taskId'], }, }) export class AdminProjectTaskDependencyController extends BaseController {} ``` - [ ] **Step 5: 创建 ProjectTimeLogController** 创建 `packages/backend/src/modules/project/controller/admin/time_log.ts`: ```typescript import { Inject, Provide } from '@midwayjs/core'; import { CoolController, BaseController } from '@cool-midway/core'; import { ProjectTimeLogEntity } from '../../entity/time_log'; import { ProjectTaskService } from '../../service/task'; /** * 工时记录 */ @Provide() @CoolController({ api: ['add', 'delete', 'page'], entity: ProjectTimeLogEntity, pageQueryOp: { fieldEq: ['taskId', 'userId'], addOrderBy: { logDate: 'DESC', }, }, }) export class AdminProjectTimeLogController extends BaseController { @Inject() projectTaskService: ProjectTaskService; /** * 新增工时后自动汇总 */ async modifyAfter(data: any, type: 'delete' | 'update' | 'add') { if (data.taskId) { await this.projectTaskService.recalcTaskHours(data.taskId); } } } ``` - [ ] **Step 6: 启动后端验证 API** ```bash cd packages/backend pnpm dev ``` 预期:启动成功。访问 `http://localhost:8003/admin/project/info/page` 应返回空数据列表(需要带 token,或用 Postman 测试)。 - [ ] **Step 7: 提交** ```bash git add packages/backend/src/modules/project/controller/ git commit -m "feat(backend): 项目管理模块 - Controller 层(CRUD + 甘特图/看板接口)" ``` --- ## Task 4: 前端依赖安装 + 模块骨架 **Files:** - Modify: `packages/frontend/package.json` - Create: `packages/frontend/src/modules/project/config.ts` - Create: `packages/frontend/src/modules/project/store/project.ts` - [ ] **Step 1: 安装前端依赖** ```bash cd packages/frontend pnpm add dhtmlx-gantt @fullcalendar/vue3 @fullcalendar/core @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction vuedraggable@next ``` - [ ] **Step 2: 创建模块目录结构** ```bash cd packages/frontend mkdir -p src/modules/project/views/components mkdir -p src/modules/project/store ``` - [ ] **Step 3: 创建模块路由配置** 创建 `packages/frontend/src/modules/project/config.ts`: ```typescript import { type ModuleConfig } from '/@/cool'; export default (): ModuleConfig => { return { name: 'project', label: '项目管理', order: 80, views: [ { path: '/project/list', meta: { label: '项目列表' }, component: () => import('./views/list.vue') }, { path: '/project/detail', meta: { label: '项目详情' }, component: () => import('./views/detail.vue') } ] }; }; ``` - [ ] **Step 4: 创建 Pinia Store** 创建 `packages/frontend/src/modules/project/store/project.ts`: ```typescript import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { useCool } from '/@/cool'; export const useProjectStore = defineStore('project', () => { const { service } = useCool(); // 当前项目信息 const currentProject = ref(null); // 甘特图数据 const ganttData = ref<{ data: any[]; links: any[] }>({ data: [], links: [] }); // 任务列表(平铺) const tasks = computed(() => ganttData.value.data.filter((d: any) => String(d.id).startsWith('t_')) ); // 阶段列表 const phases = computed(() => ganttData.value.data.filter((d: any) => String(d.id).startsWith('p_')) ); /** * 加载项目详情 */ async function loadProject(projectId: number) { const res = await service.project.info.info({ id: projectId }); currentProject.value = res; return res; } /** * 加载甘特图数据(所有视图共享) */ async function loadGanttData(projectId: number) { const res = await service.project.task.ganttData({ projectId }); ganttData.value = res; return res; } /** * 刷新数据 */ async function refresh() { if (currentProject.value?.id) { await loadGanttData(currentProject.value.id); } } /** * 甘特图拖拽更新 */ async function ganttUpdate(items: any[]) { await service.project.task.ganttUpdate({ items }); await refresh(); } /** * 看板排序 */ async function kanbanSort(items: any[]) { await service.project.task.kanbanSort({ items }); await refresh(); } /** * 添加任务 */ async function addTask(data: any) { await service.project.task.add(data); await refresh(); } /** * 更新任务 */ async function updateTask(data: any) { await service.project.task.update(data); await refresh(); } /** * 删除任务 */ async function deleteTask(ids: number[]) { await service.project.task.delete({ ids }); await refresh(); } /** * 重置 */ function reset() { currentProject.value = null; ganttData.value = { data: [], links: [] }; } return { currentProject, ganttData, tasks, phases, loadProject, loadGanttData, refresh, ganttUpdate, kanbanSort, addTask, updateTask, deleteTask, reset, }; }); ``` - [ ] **Step 5: 验证前端编译** ```bash cd packages/frontend pnpm dev ``` 预期:前端启动成功,无编译错误。 - [ ] **Step 6: 提交** ```bash git add packages/frontend/package.json pnpm-lock.yaml packages/frontend/src/modules/project/ git commit -m "feat(frontend): 项目管理模块骨架 - 依赖安装 + 路由配置 + Pinia Store" ``` --- ## Task 5: 前端项目列表页 **Files:** - Create: `packages/frontend/src/modules/project/views/list.vue` - [ ] **Step 1: 创建项目列表页** 创建 `packages/frontend/src/modules/project/views/list.vue`: ```vue ``` - [ ] **Step 2: 验证页面渲染** 启动前端 `pnpm dev`,在浏览器后台菜单管理中添加项目管理菜单(路径 `/project/list`),验证页面能正确渲染。 - [ ] **Step 3: 提交** ```bash git add packages/frontend/src/modules/project/views/list.vue git commit -m "feat(frontend): 项目列表页 - 卡片布局 + 新建/编辑/删除" ``` --- ## Task 6: 前端项目详情页(Tab 容器) **Files:** - Create: `packages/frontend/src/modules/project/views/detail.vue` - [ ] **Step 1: 创建项目详情页** 创建 `packages/frontend/src/modules/project/views/detail.vue`: ```vue ``` - [ ] **Step 2: 提交** ```bash git add packages/frontend/src/modules/project/views/detail.vue git commit -m "feat(frontend): 项目详情页 - Tab容器 + 视图切换" ``` --- ## Task 7: 甘特图视图组件 **Files:** - Create: `packages/frontend/src/modules/project/views/components/gantt.vue` - [ ] **Step 1: 创建甘特图组件** 创建 `packages/frontend/src/modules/project/views/components/gantt.vue`: ```vue ``` - [ ] **Step 2: 验证甘特图渲染** 在浏览器中进入项目详情页,确认甘特图 Tab 能正常渲染(即使没有数据也不应报错)。 - [ ] **Step 3: 提交** ```bash git add packages/frontend/src/modules/project/views/components/gantt.vue git commit -m "feat(frontend): 甘特图视图 - DHTMLX Gantt 集成 + 拖拽更新" ``` --- ## Task 8: 日历视图组件 **Files:** - Create: `packages/frontend/src/modules/project/views/components/calendar.vue` - [ ] **Step 1: 创建日历视图组件** 创建 `packages/frontend/src/modules/project/views/components/calendar.vue`: ```vue ``` - [ ] **Step 2: 提交** ```bash git add packages/frontend/src/modules/project/views/components/calendar.vue git commit -m "feat(frontend): 日历视图 - FullCalendar 月/周切换" ``` --- ## Task 9: 表格列表视图组件 **Files:** - Create: `packages/frontend/src/modules/project/views/components/table.vue` - [ ] **Step 1: 创建表格视图组件** 创建 `packages/frontend/src/modules/project/views/components/table.vue`: ```vue ``` - [ ] **Step 2: 提交** ```bash git add packages/frontend/src/modules/project/views/components/table.vue git commit -m "feat(frontend): 表格列表视图 - 筛选 + 分页 + 操作" ``` --- ## Task 10: 看板视图组件 **Files:** - Create: `packages/frontend/src/modules/project/views/components/kanban.vue` - [ ] **Step 1: 创建看板视图组件** 创建 `packages/frontend/src/modules/project/views/components/kanban.vue`: ```vue ``` - [ ] **Step 2: 提交** ```bash git add packages/frontend/src/modules/project/views/components/kanban.vue git commit -m "feat(frontend): 看板视图 - vuedraggable 拖拽 + 状态变更" ``` --- ## Task 11: 任务详情抽屉 + 工时弹窗 + 阶段管理 **Files:** - Create: `packages/frontend/src/modules/project/views/components/task-drawer.vue` - Create: `packages/frontend/src/modules/project/views/components/time-log-dialog.vue` - Create: `packages/frontend/src/modules/project/views/components/phase-manager.vue` - [ ] **Step 1: 创建任务详情抽屉** 创建 `packages/frontend/src/modules/project/views/components/task-drawer.vue`: ```vue ``` - [ ] **Step 2: 创建工时记录弹窗** 创建 `packages/frontend/src/modules/project/views/components/time-log-dialog.vue`: ```vue ``` - [ ] **Step 3: 创建阶段管理弹窗** 创建 `packages/frontend/src/modules/project/views/components/phase-manager.vue`: ```vue ``` - [ ] **Step 4: 提交** ```bash git add packages/frontend/src/modules/project/views/components/task-drawer.vue packages/frontend/src/modules/project/views/components/time-log-dialog.vue packages/frontend/src/modules/project/views/components/phase-manager.vue git commit -m "feat(frontend): 任务详情抽屉 + 工时记录弹窗 + 阶段管理" ``` --- ## Task 12: 端到端验证 + 菜单配置 **Files:** - 无新建文件,在系统中配置菜单 - [ ] **Step 1: 启动后端** ```bash cd packages/backend pnpm dev ``` 预期:启动成功,5 张表已自动创建。 - [ ] **Step 2: 启动前端** ```bash cd packages/frontend pnpm dev ``` 预期:编译成功,无错误。 - [ ] **Step 3: 在系统中配置菜单** 登录后台 → 系统管理 → 菜单管理,添加: 1. 目录:名称「项目管理」,图标自选,排序设置在系统管理下 2. 菜单:名称「项目列表」,路由 `/project/list`,父级「项目管理」 3. 菜单:名称「项目详情」,路由 `/project/detail`,父级「项目管理」,设置为隐藏菜单(不在侧边栏显示) - [ ] **Step 4: 验证核心流程** 1. 项目列表页:新建一个项目 → 显示在卡片列表中 2. 进入项目详情 → 阶段管理:添加 2 个阶段 3. 甘特图 Tab:显示阶段分组(即使暂无任务) 4. 列表 Tab:新建一个任务 → 显示在表格中 5. 看板 Tab:任务卡片出现在「待办」列 6. 日历 Tab:任务显示在对应日期 7. 任务抽屉:编辑任务 → 添加工时记录 - [ ] **Step 5: 提交** ```bash git add -A git commit -m "feat: 项目管理模块完成 - 甘特图/日历/列表/看板四视图" ```