Эх сурвалжийг харах

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

徐福静0235668 19 цаг өмнө
parent
commit
14625f361a

+ 234 - 0
docs/locate/DESIGNER-OPERATIONS-AND-FIELDS.md

@@ -0,0 +1,234 @@
+<!-- @locate: designer-operations -->
+
+## 设计师端功能操作路径(操作步骤)
+
+### 1. 设计师工作台(Dashboard) → 任务与项目
+
+- **入口路径**
+  - 网页端(不带公司ID,通常仅内部调试):`/designer/dashboard`
+  - 企微端(正式使用):`/wxwork/:cid/designer/dashboard`
+- **身份认证与角色校验**
+  - 通过 URL 或 `localStorage` 获取 `cid`(公司ID)。
+  - 初始化企微授权 `WxworkAuth({ cid, appId: 'crm' })` 并调用 `authenticateAndLoadData()`。
+  - 从认证结果中获取 `user` 与 `profile`,并验证角色:
+    - 若 `profile.roleName !== '组员'`,则弹出提示「您不是设计师(组员),无权访问此页面」并跳转首页。
+- **数据加载与视图切换**
+  - 初始化成功后,调用 `loadDashboardData()`:
+    - `loadRealTasks()`:加载当前设计师的任务列表(来自 `DesignerTaskService.getMyTasks(profile.id)`)。
+    - `loadShiftTasks()`:加载代班任务。
+    - `calculateWorkloadPercentage()`:计算个人项目饱和度(工作量百分比)。
+    - `loadProjectTimeline()`:加载项目时间轴信息。
+    - `loadDesignerProjects()`:加载与当前设计师相关的项目负载数据。
+  - 顶部或侧边切换视图:
+    - `activeDashboard`: `'main' | 'skills' | 'personal'`,在主工作台 / 技能雷达 / 个人看板之间切换。
+    - `viewMode`: `'card' | 'list'` 让任务视图在卡片/列表模式之间切换。
+- **任务层面的操作**
+  - 在任务列表中查看:
+    - 超期任务 `overdueTasks`(`isOverdue === true`)。
+    - 紧急任务 `urgentTasks`(例如距截止时间 3 小时内且属于渲染阶段的任务)。
+  - 常规操作路径:
+    - 点击某条任务 → 展开查看详细信息(所属项目、阶段、客户名、截止时间等)。
+    - 通过链接/按钮跳转到项目详情页(通常为 `/wxwork/:cid/project/:projectId` 或设计师端项目详情)。
+
+### 2. 设计师项目详情页(Designer Project Detail)
+
+- **入口路径**
+  - 网页端:`/designer/project-detail/:id`
+  - 企微端:`/wxwork/:cid/designer/project-detail/:id`
+  - 组长/客服端也会复用该组件,并通过 `roleName` 或路径识别视图角色。
+- **角色视图与权限控制**
+  - 角色上下文字段:
+    - `roleContext: 'customer-service' | 'designer' | 'team-leader' | 'technical' = 'designer'`
+  - 通过 URL 查询参数与路径检测角色:
+    - 优先从 `?roleName=` 获取:
+      - `customer-service` → 客服视角
+      - `technical` → 技术视角
+      - `team-leader` → 组长视角
+    - 若查询参数不存在,则根据 URL 路径判断是否包含 `/customer-service/`、`/team-leader/`、`/technical/`。
+    - 若路径也没标识,则根据当前用户角色字段(`currentUser.roleName`)进行判断:
+      - 含「组长」或 `team-leader` → 组长视角
+      - 含「客服」 → 客服视角
+      - 否则默认视为 `designer`。
+  - 常用视图判定方法:
+    - `isDesignerView()` / `isTeamLeaderView()` / `isCustomerServiceView()` / `isTechnicalView()`
+    - `isReadOnly()`:当为客服视角时返回 `true`,限制编辑能力。
+  - 板块与阶段编辑权限控制:
+    - `canEditSection(sectionKey)`:
+      - 客服视角仅可编辑:`order`(订单分配)、`requirements`(确认需求)、`aftercare`(售后)板块;
+      - 设计师与组长视角可以编辑所有板块。
+    - `canEditStage(stage: ProjectStage)`:
+      - 客服仅能编辑阶段:
+        - `订单分配`、`需求沟通`、`方案确认`(订单分配与确认需求板块)
+        - `尾款结算`、`客户评价`、`投诉处理`(售后板块)
+      - 设计师与组长可以编辑全部阶段。
+
+- **交付执行相关操作(设计师核心日常)**
+  - 阶段图片上传字段:
+    - `whiteModelImages`:白模阶段图片数组。
+    - `softDecorImages`:软装阶段图片数组。
+    - `renderLargeImages`:大图渲染阶段图片数组。
+    - `postProcessImages`:后期阶段图片数组。
+    - 每个图片结构中包含:
+      - `id: string`
+      - `name: string`
+      - `url: string`
+      - `size?: string`
+      - `reviewStatus?: 'pending' | 'approved' | 'rejected'`(审核状态,主要由组长操作)
+      - `synced?: boolean`(是否已与后台/组长端同步)
+  - 上传过程:
+    - 通过上传按钮或拖拽上传图片文件(受 `allowedImageTypes='.jpg,.jpeg,.png'` 限制)。
+    - 上传完成后,图片会被加入对应阶段的数组(如 `renderLargeImages`),并在 UI 中展示缩略图和状态。
+    - `showRenderUploadModal` 控制渲染大图上传弹窗;`pendingRenderLargeItems` 暂存待确认大图。
+  - 交付流程结构(对空间与工序的组合管理):
+    - `deliveryProcesses: DeliveryProcess[]`:
+      - 每个流程包括:
+        - `id: 'modeling' | 'softDecor' | 'rendering' | 'postProcess'`
+        - `name: '建模' | '软装' | '渲染' | '后期'`
+        - `isExpanded: boolean`:在 UI 中是否展开。
+        - `spaces`: 空间列表(如卧室/客厅/厨房),包含 `id`, `name`, `order`, `isExpanded`。
+        - `content`: 以空间 ID 为键的状态对象:
+          - `images: any[]`:该空间在该流程下的图片或交付物;
+          - `progress: number`:该流程在该空间下的完成进度;
+          - `status: 'pending' | 'in_progress' | 'completed' | ...`:状态;
+          - `notes: string`:备注;
+          - `lastUpdated: Date`:最后更新时间。
+    - 设计师可在 UI 中:
+      - 针对每一流程(建模/软装/渲染/后期)和空间(卧室/客厅/厨房)分配与查看交付物;
+      - 记录进度与备注,以便组长和客服了解交付状态。
+
+- **质量自查与模型检查**
+  - `modelCheckItems: ModelCheckItem[]`:
+    - 例如:户型匹配度检查、尺寸精度验证、材质贴图检查、光影效果验证、细节完整性检查。
+    - 字段:
+      - `id: string`
+      - `name: string`
+      - `isPassed: boolean`
+      - `notes?: string`
+  - 操作路径:
+    - 在页面中勾选/取消勾选每一项检查 `isPassed`。
+    - 填写备注 `notes` 说明问题或经过。
+
+- **组长视角下的同步与审核(设计师端可见,但只在组长视角可操作)**
+  - 组长视角(在同一组件中)有以下操作,用于审核设计师上传的图片:
+    - `syncUploadedImages(phase)`:
+      - 仅在 `isTeamLeaderView()` 为 true 时生效;
+      - 将指定阶段的图片标记为 `synced=true`,并在未设置 `reviewStatus` 时初始化为 `pending`;
+      - 用提示告知「已同步该阶段的图片信息」。
+    - `reviewImage(imageId, phase, status)`(代码略):
+      - 组长针对单张图片标记为 `approved` 或 `rejected`,同时记录审核结果以便后续统计。
+
+### 3. 个人看板(Personal Board) → 能力与绩效
+
+- **入口路径**
+  - 网页端:`/designer/personal-board`
+  - 企微端:`/wxwork/:cid/designer/personal-board`
+  - 组长端也会通过 `/team-leader/...` 复用该组件查看设计师个人表现。
+- **数据来源与展示内容**
+  - 使用 `ProjectService` 加载以下数据:
+    - `skillTags: SkillTag[]`:技能标签及熟练度与项目数量。
+    - `performanceData: PerformanceData[]`:按月统计的绩效数据:
+      - `month: string`:月份(如 `2024-01`)
+      - `projectCompletionRate: number`:项目完成率
+      - `customerSatisfaction: number`:客户满意度
+      - `deliveryOnTimeRate: number`:交付准时率
+    - `matchingOrders: MatchingOrder[]`:匹配度较高的推荐订单:
+      - `projectName`、`requiredSkills[]`、`matchRate`、`customerLevel` 等。
+    - `settlements: Settlement[]`:结算记录列表:
+      - 结算金额、阶段、状态(待结算/已结算/逾期)、时间等。
+    - 聚合字段:
+      - `totalSettlementAmount`:已结算总金额。
+      - `pendingSettlementAmount`:待结算总金额。
+  - 是否为初级设计师标记:
+    - `isJuniorDesigner: boolean` 用于控制部分功能权限(例如某些高级数据或操作仅限资深设计师)。
+- **图表展示操作**
+  - 在 `ngAfterViewInit` 中,当 `performanceData` 加载完毕后:
+    - 调用 `initCustomChart()` 使用原生 DOM 构建自定义柱状图。
+    - 图表以三个指标为维度:项目完成率、客户满意度、交付准时率。
+  - 用户在 UI 中可以:
+    - 查看不同月份的三项指标对比;
+    - 结合技能标签和匹配订单列表,规划个人能力提升方向。
+
+---
+
+## 设计师端项目与任务字段(数据视角)
+
+> 设计师端沿用 `src/app/models/project.model.ts` 中的通用 `Project`、`Task`、`SkillTag`、`PerformanceData` 等接口,并在各自页面(Dashboard、Project Detail、Personal Board)上做视图层的再封装。
+
+### 1. 通用 Task 模型(设计师任务)
+
+- **字段(参考 `Task` 接口)**
+  - `id: string`:任务 ID。
+  - `projectId: string`:所属项目 ID。
+  - `projectName: string`:所属项目名称。
+  - `title: string`:任务标题,一般会使用项目名称。
+  - `stage: ProjectStage`:任务所在项目阶段(如 `建模`、`渲染`、`后期` 等)。
+  - `deadline: Date`:任务截止时间。
+  - `priority: any`:任务优先级(高/中/低)。
+  - `isOverdue: boolean`:是否逾期。
+  - `isCompleted: boolean`:是否完成。
+  - `assignee: any`:任务负责人(设计师)。
+  - `description: any`:任务描述(例如「渲染阶段 - 客户姓名」)。
+
+### 2. 设计师 Dashboard 内部任务与时间轴数据
+
+- **ShiftTask(代班任务)**
+  - `id: string`
+  - `projectId: string`
+  - `projectName: string`
+  - `taskDescription: string`
+  - `priority: '高' | '中' | '低'`
+  - `shiftDate: string`
+  - `status: '待处理' | '处理中' | '已完成'`
+
+- **ProjectTimelineItem(个人项目时间轴)**
+  - `id: string`
+  - `name: string`:项目名称或阶段名称。
+  - `deadline: string`:截止时间(字符串形式)。
+  - `status: string`:当前状态(进行中/已完成/逾期等)。
+
+- **CurrentUser(顶部用户信息)**
+  - `userid: string`:企微用户ID。
+  - `name: string`:姓名。
+  - `avatar: string`:头像 URL。
+  - `roleName: string`:角色名称(一般为「组员」)。
+  - `department?: string`:部门名称。
+  - `position?: string`:职位。
+
+### 3. 技能标签与绩效数据(个人看板)
+
+- **SkillTag**
+  - `id: string`
+  - `name: string`:技能名称(如风格、软件等)。
+  - `level: number`:技能等级(1–5)。
+  - `count: number`:该技能相关的完成项目数量。
+
+- **PerformanceData**
+  - `month: string`:月份。
+  - `projectCompletionRate: number`:项目完成率。
+  - `customerSatisfaction: number`:客户满意度。
+  - `deliveryOnTimeRate: number`:交付准时率。
+
+- **MatchingOrder**
+  - `id: string`
+  - `projectName: string`:推荐项目名称。
+  - `requiredSkills: string[]`:所需技能。
+  - `matchRate: number`:匹配度(百分比)。
+  - `customerLevel: '优质' | '普通'`:客户等级。
+
+- **Settlement(结算记录)**
+  - `id: string`
+  - `projectId: string`
+  - `stage?: ProjectStage`
+  - `type?: 'deposit' | 'progress' | 'final_payment'`
+  - `amount: number`:金额。
+  - `status: '待结算' | '已结算' | '逾期' | '已取消' | 'pending' | 'completed'`
+  - `createdAt: Date`:创建时间。
+  - 其他字段(如 `settledAt`、`paidDate`、`dueDate` 等)用于详细的财务追踪。
+
+---
+
+如需,我可以再为设计师端补充:
+- 每个页面的「典型操作场景」(例如:收到新任务 → 打开项目 → 上传渲染图 → 填写自查项 → 通知组长审核);  
+- 或者画一份文字版「从任务到交付完成」的完整流程,用于培训文档。
+
+

+ 233 - 0
docs/locate/TEAM-LEADER-OPERATIONS-AND-FIELDS.md

@@ -0,0 +1,233 @@
+<!-- @locate: team-leader-operations -->
+
+## 组长端功能操作路径(操作步骤)
+
+### 1. 组长工作台(Dashboard) → 项目详情
+
+- **入口路径**
+  - 网页端:`/team-leader/dashboard`
+  - 企微端:`/wxwork/:cid/team-leader/dashboard`
+- **操作步骤**
+  - 在项目列表/卡片中,通过搜索与筛选(类型、紧急程度、阶段等)定位项目。
+  - 点击项目卡片上的“查看/进入项目”,跳转到企微项目详情对应阶段:
+    - 路径:`/wxwork/:cid/project/:projectId/:stagePath?roleName=team-leader`
+    - 阶段映射(示例逻辑见 `Dashboard` 代码):
+      - `订单分配` → `order`
+      - `确认需求` → `requirements`
+      - `交付执行` → `delivery`
+      - `售后归档` → `aftercare`
+
+### 2. 项目详情四阶段页面(组长视角)
+
+- **入口路径**
+  - `/wxwork/:cid/project/:projectId/:stageId?roleName=team-leader`
+- **顶部阶段切换**
+  - 点击顶部的四个阶段标签「订单分配 / 确认需求 / 交付执行 / 售后归档」:
+    - 前端通过 `goToStage(stageId)` 或 `switchStage(stageId)` 切换子路由。
+    - URL 在 `order / requirements / delivery / aftercare` 之间切换。
+- **订单分配阶段的组长审批**
+  - **展示条件**
+    - 当前用户角色为组长(`roleName` 为 `组长 / 设计组长 / team-leader`)。
+    - 项目当前阶段为 `订单分配`(或等价英文 `order`)。
+    - 项目 `data.approvalStatus === 'pending'`。
+  - **操作路径**
+    - 打开订单分配子页(`order`)。
+    - 组长视角下会显示审批操作区:
+      - 点击「审批通过」:
+        - 更新 `data.approvalStatus = 'approved'`,写入审批记录 `approvalHistory`;
+        - 将 `currentStage` 更新为 `确认需求`;
+        - 保存项目后,派发 `stage:completed` 事件,父组件自动推进阶段并导航到下一个阶段。
+      - 点击「驳回」:
+        - 弹出输入框填写驳回原因;
+        - 更新 `data.approvalStatus = 'rejected'`、`data.lastRejectionReason`,并写入 `approvalHistory`;
+        - `currentStage` 保持(或回退为)`订单分配`,等待客服重新调整并再次提交。
+
+### 3. 团队管理(Team Management) → 项目详情
+
+- **入口路径**
+  - `/team-leader/team-management`
+  - 或:`/wxwork/:cid/team-leader/team-management`
+- **操作步骤**
+  - 在设计师列表中浏览各设计师:
+    - 查看:工作量(轻/中/重)、能力评分、满意度、完成项目数等。
+  - 在某设计师的任务列表中:
+    - 点击任务对应的项目名称 → 调用 `viewProjectDetails(projectId)`:
+      - 路径:`/wxwork/:cid/project/:projectId`
+      - 进入该项目的企微项目详情页(再通过顶部阶段导航进行阶段切换或审批)。
+
+### 4. 质量管理(Quality Management) → 整改任务与能力提升
+
+- **入口路径**
+  - `/team-leader/quality-management`
+- **质量整改任务处理**
+  - 在列表中查看每条整改任务:
+    - 字段包括:项目名、设计师、问题描述、优先级(高/中/低)、状态(待处理/处理中/待审核/已完成)、建议完成时间、SOP 合规得分、组长评语、关联 SOP 等。
+  - 点击「批准」:
+    - 将任务状态置为 `completed`;
+    - 调用同步方法(如 `syncToProjectReview`)把结果同步到项目评审/质量记录中;
+    - 弹出提示(如“已批准任务:XXX”)。
+  - 点击「拒绝」:
+    - 输入拒绝原因;
+    - 将任务状态恢复为 `pending`;
+    - 保存 `managerComment` 为拒绝原因,供后续追踪。
+- **教学与考核**
+  - 视频教程列表:
+    - 可按关键词和阶段筛选(建模/渲染/后期),点击某条教程进行播放(当前实现为模拟弹窗/日志,实际项目可跳播放器页面)。
+  - 实践作业列表:
+    - 对 `submitted` 状态作业进行评审:
+      - 输入 0–100 分数;
+      - 更新作业 `score`、`status='reviewed'`;
+      - 将得分同步至团队能力看板(与 `team-management` 绩效数据联动)。
+
+### 5. 知识库与能力复制(Knowledge Base)
+
+- **入口路径**
+  - `/team-leader/knowledge-base`
+- **SOP 规范管理**
+  - 查看各阶段 SOP 内容(建模/渲染/后期等):
+    - 每条 SOP 包含:阶段说明、详细要求列表、时间轴(Day1/Day2 任务)、质量指标(例如“模型精度 ≥95%”)。
+  - 组长可以:
+    - 新建 SOP(添加新的规范条目);
+    - 编辑现有 SOP;
+    - 导出单阶段或整套 SOP(当前实现为模拟日志,实际可对接 PDF 导出)。
+- **视频教程库**
+  - 使用搜索框与阶段筛选组合过滤教程列表;
+  - 分页浏览教程(按上传时间与播放量等排序),点击教程进行播放。
+- **能力考核任务**
+  - 创建考核任务(标题、描述、阶段、难度、预期耗时等);
+  - 将考核任务分配给指定设计师(设置 `assigneeId` 和 `deadline`);
+  - 查看考核提交与评分结果,将表现反馈到绩效与晋升决策中。
+
+### 6. 负载日历(Workload Calendar) → 排期与项目跳转
+
+- **入口路径**
+  - `/team-leader/workload-calendar`
+- **视图与筛选**
+  - 支持三种视图:日视图 / 周视图 / 月视图;
+  - 支持按设计师(单人或全部)、阶段(待分配/需求方案/执行/收尾验收/归档)、快捷时间范围(全部/今天/3 天内/7 天内)筛选任务;
+  - 日历格子上展示当天任务数、逾期标记等。
+- **操作路径**
+  - 点击某天或某设计师,查看该视图下的任务列表;
+  - 在任务列表中点击项目,调用 `navigateToProject`:
+    - 路径:`/wxwork/:cid/project/:projectId`
+    - 从日历排期直接进入对应的项目详情页面,继续进行阶段操作或审批。
+
+---
+
+## 组长端项目已有数据及字段(数据视角)
+
+### 1. 通用 Project 模型(跨模块使用)
+
+源文件:`src/app/models/project.model.ts`  
+
+- **核心字段**
+  - `id: string`:项目 ID。
+  - `name: string`:项目名称。
+  - `customerName: string`:客户姓名。
+  - `customerPhone?: string`:客户电话。
+  - `customerWechat?: string`:客户微信。
+  - `customerType?: string`:客户类型(如新客户/老客户)。
+  - `customerSource?: string`:客户来源(如朋友圈/信息流等)。
+  - `customerRemark?: string`:客户备注。
+  - `customerInfo?: CustomerInfo`:客户详细偏好信息:
+    - `colorPreference?: string`:色彩偏好。
+    - `spaceRequirements?: string`:空间需求。
+    - `materialPreference?: string`:材质偏好。
+  - `customerTags: CustomerTag[]`:客户标签数组:
+    - `source: '朋友圈' | '信息流'`
+    - `needType: '硬装' | '软装'`
+    - `preference: '现代' | '宋式' | '欧式'`
+    - `colorAtmosphere: string`
+  - `highPriorityNeeds: string[]`:高优先级需求列表。
+  - `requirements?: RequirementItem[]`:项目需求明细:
+    - `id: string`
+    - `description: string`
+    - `status: string`
+    - `priority: 'high' | 'medium' | 'low'`
+  - `status: ProjectStatus`:项目状态:
+    - `'进行中' | '已完成' | '已暂停' | '已延期' | '待分配'`
+  - `currentStage: ProjectStage`:当前阶段(见阶段枚举)。
+  - `stage: ProjectStage`:与 `currentStage` 保持一致,用于兼容。
+  - `createdAt: Date`:项目创建时间。
+  - `deadline: Date`:截至日期。
+  - `assigneeId: string`:当前负责人 ID(设计师 Profile)。
+  - `assigneeName: string`:当前负责人姓名。
+  - `skillsRequired: string[]`:期望的设计师技能标签(用于智能匹配)。
+  - `finalPaymentAmount?: number`:尾款金额。
+  - `customerReviewCompleted?: boolean`:客户评价是否完成。
+  - `customerReviewCompletedAt?: Date`:客户评价完成时间。
+
+- **阶段枚举 `ProjectStage`**
+  - 四大核心阶段(规范化字段):
+    - `订单分配`
+    - `确认需求`
+    - `交付执行`
+    - `售后归档`
+  - 细分阶段(兼容旧数据):
+    - `需求沟通`
+    - `方案确认`
+    - `建模`
+    - `软装`
+    - `渲染`
+    - `后期`
+    - `尾款结算`
+    - `客户评价`
+    - `投诉处理`
+
+### 2. Task 任务模型(负载日历 & 排期)
+
+- **源文件同上 `project.model.ts` 中 `Task` 接口**
+- **核心字段**
+  - `id: string`:任务 ID。
+  - `projectId: string`:所属项目 ID。
+  - `projectName: string`:所属项目名称。
+  - `title: string`:任务标题。
+  - `description: any`:任务描述。
+  - `assignee: any`:负责人(设计师)。
+  - `stage: ProjectStage`:任务所在项目阶段(与上文阶段枚举一致)。
+  - `deadline: Date`:任务截止时间。
+  - `priority: any`:任务优先级(通常为 `high / medium / low`)。
+  - `isOverdue: boolean`:是否逾期。
+  - `isCompleted: boolean`:是否已完成。
+  - `completedDate?: Date`:完成时间。
+
+> 这些字段主要被 `WorkloadCalendarComponent` 与组长工作台用来计算:某设计师在特定时间窗内的任务数量、逾期数量以及整体负载状态(空闲/正常/繁忙/异常),并支撑跳转到项目详情进行下一步操作。
+
+### 3. 组长 Dashboard 本地 Project 视图模型
+
+源文件:`src/app/pages/team-leader/dashboard/dashboard.ts` 中的 `interface Project`(注意:这是前端视图模型,基于后台 `Project` 聚合转换而来)。  
+
+- **核心字段**
+  - `id: string`:项目 ID。
+  - `name: string`:项目名称。
+  - `type: 'soft' | 'hard'`:项目类型(软装/硬装)。
+  - `memberType: 'vip' | 'normal'`:客户等级(VIP/普通)。
+  - `designerName: string`:当前负责设计师。
+  - `status: string`:状态(例如进行中/已完成等,通常由阶段和财务状态推导)。
+  - `expectedEndDate: Date`:预期结束时间(兼容旧字段)。
+  - `deadline: Date`:真实截止时间(新字段)。
+  - `createdAt?: Date`:实际开始时间。
+  - `isOverdue: boolean`:是否逾期。
+  - `overdueDays: number`:逾期天数。
+  - `dueSoon: boolean`:是否临期(例如 3 天内到期)。
+  - `urgency: 'high' | 'medium' | 'low'`:紧急程度标记。
+  - `phases: ProjectPhase[]`:项目阶段进度条数据:
+    - `name: string`:阶段名称(如待确认、需求沟通、建模…)。
+    - `percentage: number`:阶段占比。
+    - `startPercentage: number`:阶段开始位置。
+    - `isCompleted: boolean`:是否完成。
+    - `isCurrent: boolean`:是否当前进行中阶段。
+  - `currentStage: string`:当前阶段(规范化后的中文阶段名,用于映射 URL)。
+  - `qualityRating?: 'excellent' | 'qualified' | 'unqualified' | 'pending'`:质量评级。
+  - `lastCustomerFeedback?: string`:最近客户反馈内容。
+  - `searchIndex?: string`:本地构建的搜索索引(项目名+设计师名小写拼接)。
+
+> 以上字段主要用于组长工作台页面做排序、筛选、搜索以及在项目卡片上展示「阶段进度条」「临期/逾期标识」「质量评级」等信息,并决定点击项目时跳转到哪个阶段的详情页。
+
+---
+
+如需后续扩展,我可以基于本文件继续添加:
+- 按「页面维度」的字段表(例如:工作台/团队管理/质量管理分别用到哪些字段);
+- 或者导出为对外说明文档(仅保留业务含义,不暴露内部实现细节)。
+
+

+ 4 - 0
src/app/models/project-phase.model.ts

@@ -26,6 +26,10 @@ export interface PhaseInfo {
   startDate?: Date | string;
   /** 阶段截止时间 */
   deadline: Date | string;
+  /** 第一次上传时间 */
+  firstUploadAt?: Date | string;
+  /** 最近一次提交时间 */
+  lastSubmissionAt?: Date | string;
   /** 预计工期(天数) */
   estimatedDays?: number;
   /** 阶段状态 */

+ 77 - 179
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -10,6 +10,9 @@ import { FmodeParse } from 'fmode-ng/parse';
 import { ProjectTimelineComponent } from '../project-timeline';
 import type { ProjectTimeline } from '../project-timeline/project-timeline';
 import { EmployeeDetailPanelComponent } from '../employee-detail-panel';
+import { normalizeDateInput, addDays } from '../../../utils/date-utils';
+import { generatePhaseDeadlines } from '../../../utils/phase-deadline.utils';
+import { PhaseDeadlines, PhaseName } from '../../../models/project-phase.model';
 
 // 项目阶段定义
 interface ProjectStage {
@@ -569,21 +572,6 @@ export class Dashboard implements OnInit, OnDestroy {
    * 将项目数据转换为ProjectTimeline格式(带缓存优化 + 去重)
    */
   private convertToProjectTimeline(): void {
-    // 计算当前数据大小
-    let currentSize = 0;
-    this.designerWorkloadMap.forEach((projects) => {
-      currentSize += projects.length;
-    });
-    
-    console.log(`📊 convertToProjectTimeline: 当前数据大小 = ${currentSize}, 缓存大小 = ${this.lastDesignerWorkloadMapSize}`);
-    
-    // 如果数据没有变化,使用缓存
-    if (currentSize === this.lastDesignerWorkloadMapSize && this.timelineDataCache.length > 0) {
-      console.log(`📊 使用缓存数据,共 ${this.timelineDataCache.length} 个项目`);
-      this.projectTimelineData = this.timelineDataCache;
-      return;
-    }
-    
     // 🔧 不去重,保留所有项目-设计师关联关系(一个项目可能有多个设计师)
     const allDesignerProjects: any[] = [];
     
@@ -626,57 +614,66 @@ export class Dashboard implements OnInit, OnDestroy {
       const projectData = project.data || {};
       
       // 1. 获取真实的项目开始时间
-      // 优先使用 phaseDeadlines.modeling.startDate,其次使用 requirementsConfirmedAt,最后使用 createdAt
-      let realStartDate: Date;
-      if (projectData.phaseDeadlines?.modeling?.startDate) {
-        realStartDate = projectData.phaseDeadlines.modeling.startDate instanceof Date 
-          ? projectData.phaseDeadlines.modeling.startDate
-          : new Date(projectData.phaseDeadlines.modeling.startDate);
-      } else if (projectData.requirementsConfirmedAt) {
-        realStartDate = new Date(projectData.requirementsConfirmedAt);
-      } else if (project.createdAt) {
-        realStartDate = project.createdAt instanceof Date ? project.createdAt : new Date(project.createdAt);
-      } else {
-        // 降级:如果没有开始时间,使用当前时间
-        realStartDate = new Date();
-      }
+      const realStartDate = normalizeDateInput(
+        projectData.phaseDeadlines?.modeling?.startDate ||
+          projectData.requirementsConfirmedAt ||
+          project.createdAt,
+        new Date()
+      );
       
       // 2. 获取真实的交付日期
+      // ✅ 修复:确保 deadline 是未来的日期(不使用过去的初始值或未初始化的值)
+      let proposedEndDate = project.deadline || projectData.phaseDeadlines?.postProcessing?.deadline;
       let realEndDate: Date;
-      if (project.deadline) {
-        realEndDate = project.deadline instanceof Date ? project.deadline : new Date(project.deadline);
-      } else {
-        // ✅ 修复:如果没有交付日期,使用开始时间 + 30天(更合理的默认值)
-        // 避免新项目因为默认7天导致结束日期在过去而被过滤
-        realEndDate = new Date(realStartDate.getTime() + 30 * 24 * 60 * 60 * 1000);
-      }
       
-      // ✅ 调试:检查新项目
-      if (project.id === 'qCV9QHROSH') {
-        console.log(`📊 新项目 qCV9QHROSH 日期计算:`, {
-          projectName: project.name,
-          hasDeadline: !!project.deadline,
-          deadline: project.deadline ? (project.deadline instanceof Date ? project.deadline.toLocaleString('zh-CN') : new Date(project.deadline).toLocaleString('zh-CN')) : '无',
-          realStartDate: realStartDate.toLocaleString('zh-CN'),
-          realEndDate: realEndDate.toLocaleString('zh-CN'),
-          calculatedEndDate: realEndDate.toLocaleString('zh-CN')
-        });
+      // 如果提议的结束日期在过去,或者日期无效,使用默认值
+      if (proposedEndDate) {
+        const proposed = normalizeDateInput(proposedEndDate, realStartDate);
+        // 只有当提议的日期在未来时才使用它
+        if (proposed.getTime() > now.getTime()) {
+          realEndDate = proposed;
+        } else {
+          // 日期在过去,使用默认值(从开始日期起30天)
+          realEndDate = addDays(realStartDate, 30);
+        }
+      } else {
+        // 没有提议的日期,使用默认值
+        realEndDate = addDays(realStartDate, 30);
       }
       
-      // 3. 获取真实的对图时间
-      // 优先使用 demoday,其次使用 phaseDeadlines.softDecor.deadline,最后计算
+      // 3. 获取真实的对图时间(小图对图)
+      // ✅ 逻辑:优先使用 project.demoday,否则在软装截止时间后半天
       let realReviewDate: Date;
+      let reviewDateSource = 'default';
+      
       if (project.demoday) {
-        realReviewDate = project.demoday instanceof Date ? project.demoday : new Date(project.demoday);
+        // 如果有显式设置的小图对图日期,使用它
+        realReviewDate = normalizeDateInput(project.demoday, new Date());
+        reviewDateSource = 'demoday';
       } else if (projectData.phaseDeadlines?.softDecor?.deadline) {
-        const softDecorDeadline = projectData.phaseDeadlines.softDecor.deadline;
-        realReviewDate = softDecorDeadline instanceof Date ? softDecorDeadline : new Date(softDecorDeadline);
+        // 软装截止时间后半天作为小图对图时间
+        const softDecorDeadline = normalizeDateInput(projectData.phaseDeadlines.softDecor.deadline, new Date());
+        realReviewDate = new Date(softDecorDeadline.getTime() + 12 * 60 * 60 * 1000); // 加12小时
+        reviewDateSource = 'softDecor + 12h';
       } else {
-        // 计算:设置在软装和渲染之间(项目周期的 60% 位置)
-        const projectDuration = realEndDate.getTime() - realStartDate.getTime();
-        const projectMidPoint = realStartDate.getTime() + (projectDuration * 0.6);
-        realReviewDate = new Date(projectMidPoint);
-        realReviewDate.setHours(14, 0, 0, 0);
+        // 默认值:项目进度的60%位置,下午2点
+        const defaultReviewPoint = new Date(
+          realStartDate.getTime() + (realEndDate.getTime() - realStartDate.getTime()) * 0.6
+        );
+        defaultReviewPoint.setHours(14, 0, 0, 0);
+        realReviewDate = defaultReviewPoint;
+        reviewDateSource = 'default 60%';
+      }
+      
+      // 调试日志
+      if (project.name?.includes('紫云') || project.name?.includes('自建')) {
+        console.log(`📸 [${project.name}] 小图对图时间计算:`, {
+          source: reviewDateSource,
+          reviewDate: realReviewDate.toLocaleString('zh-CN'),
+          demoday: project.demoday,
+          softDecorDeadline: projectData.phaseDeadlines?.softDecor?.deadline,
+          hasPhaseDeadlines: !!projectData.phaseDeadlines
+        });
       }
       
       // 4. 计算距离交付还有几天(使用真实日期)
@@ -709,59 +706,12 @@ export class Dashboard implements OnInit, OnDestroy {
       const currentStage = stageMap[project.currentStage || '建模阶段'] || 'model';
       const stageName = project.currentStage || '建模阶段';
       
-      // 7. 计算真实的阶段进度(基于 phaseDeadlines)
-      let stageProgress = 50; // 默认值
-      if (projectData.phaseDeadlines) {
-        const phaseDeadlines = projectData.phaseDeadlines;
-        
-        // 根据当前阶段计算进度
-        if (currentStage === 'model' && phaseDeadlines.modeling) {
-          const start = phaseDeadlines.modeling.startDate instanceof Date 
-            ? phaseDeadlines.modeling.startDate 
-            : new Date(phaseDeadlines.modeling.startDate);
-          const end = phaseDeadlines.modeling.deadline instanceof Date 
-            ? phaseDeadlines.modeling.deadline 
-            : new Date(phaseDeadlines.modeling.deadline);
-          const total = end.getTime() - start.getTime();
-          const elapsed = now.getTime() - start.getTime();
-          stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
-        } else if (currentStage === 'decoration' && phaseDeadlines.softDecor) {
-          const start = phaseDeadlines.softDecor.startDate instanceof Date 
-            ? phaseDeadlines.softDecor.startDate 
-            : new Date(phaseDeadlines.softDecor.startDate);
-          const end = phaseDeadlines.softDecor.deadline instanceof Date 
-            ? phaseDeadlines.softDecor.deadline 
-            : new Date(phaseDeadlines.softDecor.deadline);
-          const total = end.getTime() - start.getTime();
-          const elapsed = now.getTime() - start.getTime();
-          stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
-        } else if (currentStage === 'render' && phaseDeadlines.rendering) {
-          const start = phaseDeadlines.rendering.startDate instanceof Date 
-            ? phaseDeadlines.rendering.startDate 
-            : new Date(phaseDeadlines.rendering.startDate);
-          const end = phaseDeadlines.rendering.deadline instanceof Date 
-            ? phaseDeadlines.rendering.deadline 
-            : new Date(phaseDeadlines.rendering.deadline);
-          const total = end.getTime() - start.getTime();
-          const elapsed = now.getTime() - start.getTime();
-          stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
-        } else if (currentStage === 'delivery' && phaseDeadlines.postProcessing) {
-          const start = phaseDeadlines.postProcessing.startDate instanceof Date 
-            ? phaseDeadlines.postProcessing.startDate 
-            : new Date(phaseDeadlines.postProcessing.startDate);
-          const end = phaseDeadlines.postProcessing.deadline instanceof Date 
-            ? phaseDeadlines.postProcessing.deadline 
-            : new Date(phaseDeadlines.postProcessing.deadline);
-          const total = end.getTime() - start.getTime();
-          const elapsed = now.getTime() - start.getTime();
-          stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
-        }
-      } else {
-        // 降级:使用整体项目进度
-        const totalDuration = realEndDate.getTime() - realStartDate.getTime();
-        const elapsed = now.getTime() - realStartDate.getTime();
-        stageProgress = totalDuration > 0 ? Math.min(100, Math.max(0, (elapsed / totalDuration) * 100)) : 50;
-      }
+      // 7. 🆕 阶段任务完成度(由时间轴组件的 getProjectCompletionRate 计算)
+      // ✅ 重要变更:进度条现在表示"任务完成度"而不是"时间百分比"
+      // - 时间轴组件会优先使用交付物完成率(overallCompletionRate)
+      // - 若无交付物数据,则根据 phaseDeadlines.status 推断任务完成度
+      // - stageProgress 保留作为兼容字段,但已弃用
+      let stageProgress = 50; // 默认兼容值(实际进度由时间轴组件计算)
       
       // 8. 检查是否停滞(基于 updatedAt)
       let isStalled = false;
@@ -790,76 +740,24 @@ export class Dashboard implements OnInit, OnDestroy {
       }
       
       // 11. 获取或生成阶段截止时间数据
-      let phaseDeadlines = projectData.phaseDeadlines;
-      
-      // 如果项目没有阶段数据,从交付日期往前推算
-      if (!phaseDeadlines && realEndDate) {
-        const deliveryTime = realEndDate.getTime();
-        const startTime = realStartDate.getTime();
-        const totalDays = Math.ceil((deliveryTime - startTime) / (24 * 60 * 60 * 1000));
-        
-        // 按比例分配:建模30%,软装25%,渲染30%,后期15%
-        const modelingDays = Math.ceil(totalDays * 0.3);
-        const softDecorDays = Math.ceil(totalDays * 0.25);
-        const renderingDays = Math.ceil(totalDays * 0.3);
-        const postProcessDays = totalDays - modelingDays - softDecorDays - renderingDays;
-        
-        let currentDate = new Date(startTime);
-        
-        const modelingDeadline = new Date(currentDate);
-        modelingDeadline.setDate(modelingDeadline.getDate() + modelingDays);
-        
-        currentDate = new Date(modelingDeadline);
-        const softDecorDeadline = new Date(currentDate);
-        softDecorDeadline.setDate(softDecorDeadline.getDate() + softDecorDays);
-        
-        currentDate = new Date(softDecorDeadline);
-        const renderingDeadline = new Date(currentDate);
-        renderingDeadline.setDate(renderingDeadline.getDate() + renderingDays);
-        
-        currentDate = new Date(renderingDeadline);
-        const postProcessingDeadline = new Date(currentDate);
-        postProcessingDeadline.setDate(postProcessingDeadline.getDate() + postProcessDays);
-        
-        // 更新对图时间(软装和渲染之间)
-        const reviewTime = softDecorDeadline.getTime() + (renderingDeadline.getTime() - softDecorDeadline.getTime()) * 0.5;
-        realReviewDate = new Date(reviewTime);
-        realReviewDate.setHours(14, 0, 0, 0);
-        
-        phaseDeadlines = {
-          modeling: {
-            startDate: realStartDate,
-            deadline: modelingDeadline,
-            estimatedDays: modelingDays,
-            status: now.getTime() >= modelingDeadline.getTime() ? 'completed' : 
-                    now.getTime() >= realStartDate.getTime() ? 'in_progress' : 'not_started',
-            priority: 'high'
-          },
-          softDecor: {
-            startDate: modelingDeadline,
-            deadline: softDecorDeadline,
-            estimatedDays: softDecorDays,
-            status: now.getTime() >= softDecorDeadline.getTime() ? 'completed' : 
-                    now.getTime() >= modelingDeadline.getTime() ? 'in_progress' : 'not_started',
-            priority: 'medium'
-          },
-          rendering: {
-            startDate: softDecorDeadline,
-            deadline: renderingDeadline,
-            estimatedDays: renderingDays,
-            status: now.getTime() >= renderingDeadline.getTime() ? 'completed' : 
-                    now.getTime() >= softDecorDeadline.getTime() ? 'in_progress' : 'not_started',
-            priority: 'high'
-          },
-          postProcessing: {
-            startDate: renderingDeadline,
-            deadline: postProcessingDeadline,
-            estimatedDays: postProcessDays,
-            status: now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : 
-                    now.getTime() >= renderingDeadline.getTime() ? 'in_progress' : 'not_started',
-            priority: 'medium'
+      let phaseDeadlines: PhaseDeadlines | undefined = projectData.phaseDeadlines;
+      if (!phaseDeadlines) {
+        phaseDeadlines = generatePhaseDeadlines(realStartDate, realEndDate);
+      }
+      if (phaseDeadlines) {
+        (Object.keys(phaseDeadlines) as PhaseName[]).forEach((phaseKey) => {
+          const info = phaseDeadlines![phaseKey];
+          if (!info) return;
+          const phaseStart = normalizeDateInput(info.startDate, realStartDate);
+          const phaseEnd = normalizeDateInput(info.deadline, realEndDate);
+          if (now >= phaseEnd) {
+            info.status = 'completed';
+          } else if (now >= phaseStart) {
+            info.status = 'in_progress';
+          } else {
+            info.status = info.status || 'not_started';
           }
-        };
+        });
       }
       
       // 12. 获取空间和客户信息
@@ -892,7 +790,7 @@ export class Dashboard implements OnInit, OnDestroy {
     
     // 更新缓存
     this.timelineDataCache = this.projectTimelineData;
-    this.lastDesignerWorkloadMapSize = currentSize;
+    this.lastDesignerWorkloadMapSize = totalProjectsInMap;
     
     console.log(`📊 convertToProjectTimeline 完成: 转换了 ${this.projectTimelineData.length} 个项目`);
     if (this.projectTimelineData.length > 0) {

+ 20 - 7
src/app/pages/team-leader/project-timeline/project-timeline.html

@@ -246,16 +246,29 @@
                      [style.left]="getProjectPosition(project).left"
                      [style.width]="getProjectPosition(project).width"
                      [style.background]="getProjectPosition(project).background"
-                     [title]="project.projectName + ' | ' + project.stageName + ' ' + getProjectCompletionRate(project) + '%'">
-                  <!-- 进度填充 -->
-                  <div class="progress-fill"
-                       [style.width]="getProjectCompletionRate(project) + '%'"
-                       [style.background]="getProjectCompletionColor(project)">
+                     [title]="project.projectName + ' | ' + project.stageName + ' 当前环节进度: ' + getProjectCompletionRate(project) + '%'">
+                  <!-- 🆕 进度条:从现在到最近的一个事件(显示任务完成度) -->
+                  <div class="progress-bar-container"
+                       [style.left]="getProgressBarPosition(project).left"
+                       [style.width]="getProgressBarPosition(project).width"
+                       [title]="'从现在到下一个事件的时间跨度,当前环节任务完成度: ' + getProjectCompletionRate(project) + '%'">
+                    <!-- 任务完成度填充(相对于进度条容器的宽度) -->
+                    <div class="progress-fill"
+                         [style.width.%]="getProjectCompletionRate(project)"
+                         [style.background]="getProjectCompletionColor(project)">
+                      <!-- 任务完成度百分比文本(仅在填充足够宽时显示) -->
+                      @if (getProjectCompletionRate(project) >= 15) {
+                        <span class="progress-text">{{ getProjectCompletionRate(project) }}%</span>
+                      }
+                    </div>
                   </div>
-                  <div class="progress-marker"
+                  
+                  <!-- 任务完成度标记 -->
+                  <div class="completion-marker"
                        [style.left]="getCompletionMarkerLeft(project)">
                     <span class="marker-label"
-                          [style.background]="getProjectCompletionColor(project)">
+                          [style.background]="getProjectCompletionColor(project)"
+                          [title]="'当前环节任务完成度: ' + getProjectCompletionRate(project) + '%'">
                       {{ getProjectCompletionRate(project) }}%
                     </span>
                     <span class="marker-dot"

+ 44 - 6
src/app/pages/team-leader/project-timeline/project-timeline.scss

@@ -692,16 +692,54 @@
   }
 }
 
-// 进度填充
-.progress-fill {
+// 🆕 进度条容器(从现在到下一个事件)
+.progress-bar-container {
   position: absolute;
   top: 0;
-  left: 0;
   bottom: 0;
   border-radius: 8px;
-  transition: width 0.3s ease, background 0.3s ease;
-  box-shadow: inset 0 -8px 12px rgba(0, 0, 0, 0.05);
-  opacity: 0.9;
+  transition: left 0.3s ease, width 0.3s ease;
+  opacity: 0.7;
+  z-index: 5;
+  
+  .progress-fill {
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    border-radius: 8px;
+    transition: background 0.3s ease;
+    box-shadow: inset 0 -8px 12px rgba(0, 0, 0, 0.05);
+    opacity: 0.9;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    overflow: hidden;
+    
+    .progress-text {
+      font-size: 12px;
+      font-weight: 700;
+      color: #ffffff;
+      text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+      letter-spacing: 0.3px;
+      white-space: nowrap;
+      pointer-events: none;
+      opacity: 0.95;
+    }
+  }
+}
+
+// 任务完成度标记
+.completion-marker {
+  position: absolute;
+  top: -40px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 6px;
+  transform: translateX(-50%);
+  pointer-events: none;
 }
 
 .progress-marker {

+ 125 - 5
src/app/pages/team-leader/project-timeline/project-timeline.ts

@@ -169,6 +169,15 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     const rangeEnd = this.timeRangeEnd.getTime();
     const rangeDuration = rangeEnd - rangeStart;
     
+    if (!this.isValidDate(project.startDate) || !this.isValidDate(project.endDate)) {
+      console.warn('⚠️ 项目缺少有效的起止时间,使用占位条:', project.projectName, project.projectId);
+      return {
+        left: '0%',
+        width: '2px',
+        background: this.getProjectBarBackground(project)
+      };
+    }
+    
     // ✅ 修复:如果项目完全在过去,显示一个最小宽度的条
     if (project.endDate.getTime() < rangeStart) {
       // 项目完全在过去,显示在时间轴最左侧,宽度为最小可见宽度
@@ -212,6 +221,64 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     };
   }
   
+  /**
+   * 🆕 获取进度条的位置和宽度(从现在到最近的一个事件)
+   * ✅ 修改逻辑:进度条表示"从当前时间到下一个事件"的时间跨度
+   */
+  getProgressBarPosition(project: ProjectTimeline): { left: string; width: string } {
+    const rangeStart = this.timeRangeStart.getTime();
+    const rangeEnd = this.timeRangeEnd.getTime();
+    const rangeDuration = rangeEnd - rangeStart;
+    const now = this.currentTime.getTime();
+    
+    // 获取项目的所有事件,找到最近的未来事件
+    const events = this.getProjectEvents(project);
+    let nextEventDate: Date | null = null;
+    
+    // 找到距离现在最近的未来事件
+    for (const event of events) {
+      if (event.date.getTime() > now) {
+        if (!nextEventDate || event.date.getTime() < nextEventDate.getTime()) {
+          nextEventDate = event.date;
+        }
+      }
+    }
+    
+    // 如果没有未来事件,使用项目交付日期作为终点
+    if (!nextEventDate && project.deliveryDate && project.deliveryDate.getTime() > now) {
+      nextEventDate = project.deliveryDate;
+    }
+    
+    // 如果没有找到任何未来事件,返回最小宽度
+    if (!nextEventDate) {
+      return {
+        left: '0%',
+        width: '0%' // 无进度条
+      };
+    }
+    
+    // 计算从现在到下一个事件的时间跨度
+    const progressStart = Math.max(now, rangeStart);
+    const progressEnd = Math.min(nextEventDate.getTime(), rangeEnd);
+    
+    if (progressEnd <= progressStart) {
+      return {
+        left: '0%',
+        width: '0%'
+      };
+    }
+    
+    const left = ((progressStart - rangeStart) / rangeDuration) * 100;
+    const width = ((progressEnd - progressStart) / rangeDuration) * 100;
+    
+    return {
+      left: `${Math.max(0, left)}%`,
+      width: `${Math.max(1, width)}%`
+    };
+  }
+  
+
+  
   /**
    * 获取事件标记在时间轴上的位置
    */
@@ -219,6 +286,10 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     const rangeStart = this.timeRangeStart.getTime();
     const rangeEnd = this.timeRangeEnd.getTime();
     const rangeDuration = rangeEnd - rangeStart;
+
+    if (!this.isValidDate(date)) {
+      return '';
+    }
     
     const eventTime = date.getTime();
     
@@ -261,14 +332,58 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     return `status-${project.status || 'normal'}`;
   }
   
+  /**
+   * 🆕 获取项目当前环节的任务完成度(不是时间进度)
+   * 优先级:1. 交付物完成率 2. 阶段状态推断 3. 默认值
+   */
   getProjectCompletionRate(project: ProjectTimeline): number {
+    // ✅ 首先尝试获取交付物完成率(这是最准确的任务完成度)
     const summary = this.getSpaceDeliverableSummary(project.projectId);
-    if (summary) {
+    if (summary && summary.overallCompletionRate !== undefined) {
       return Math.round(summary.overallCompletionRate);
     }
-    if (typeof project.stageProgress === 'number') {
-      return Math.round(Math.max(0, Math.min(100, project.stageProgress)));
+    
+    // ✅ 降级方案:根据阶段状态推断任务完成度
+    if (project.phaseDeadlines) {
+      const phaseDeadlines = project.phaseDeadlines;
+      
+      // 根据当前阶段的状态推断完成度
+      let currentPhaseInfo: any = null;
+      
+      // 映射当前阶段到对应的 phaseDeadlines 字段
+      const stagePhaseMap: Record<string, string> = {
+        'model': 'modeling',
+        'decoration': 'softDecor',
+        'render': 'rendering',
+        'delivery': 'postProcessing'
+      };
+      
+      // 从阶段名称获取对应的 phaseName
+      let phaseName = stagePhaseMap[project.stageName] || stagePhaseMap['model'];
+      currentPhaseInfo = phaseDeadlines[phaseName as any];
+      
+      if (currentPhaseInfo) {
+        const status = currentPhaseInfo.status;
+        
+        // 根据阶段状态推断任务完成度
+        if (status === 'completed') {
+          return 100; // 完成阶段任务
+        } else if (status === 'in_progress') {
+          // 进行中:根据有无上传文件来判断
+          if (currentPhaseInfo.firstUploadAt) {
+            return 50; // 已有上传,表示有进展
+          } else {
+            return 20; // 刚开始,进度较低
+          }
+        } else if (status === 'not_started') {
+          return 0; // 未开始
+        } else if (status === 'delayed') {
+          return 30; // 延期/驳回
+        }
+      }
     }
+    
+    // ✅ 最后的降级:返回 0
     return 0;
   }
   
@@ -338,6 +453,10 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     // 必须同时满足:在时间范围内 + 在当前时间之后
     return this.isEventInRange(date) && date.getTime() >= this.currentTime.getTime();
   }
+
+  private isValidDate(value: Date): boolean {
+    return value instanceof Date && !isNaN(value.getTime());
+  }
   
   /**
    * 🆕 获取项目的所有时间轴事件(含阶段截止时间)
@@ -386,15 +505,16 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
       });
     }
     
-    // 🔥 小图对图事件(始终显示,位于软装和渲染之间,高亮显示)
+    // 🔥 小图对图事件(只要在时间范围内就显示,高亮显示)
     if (project.reviewDate && this.isEventInRange(project.reviewDate)) {
       const isPast = project.reviewDate.getTime() < this.currentTime.getTime();
+      
       events.push({
         date: project.reviewDate,
         label: '小图对图',
         type: 'review',
         projectId: project.projectId,
-        color: isPast ? '#94a3b8' : '#f59e0b', // 🔥 高亮显示:金黄
+        color: isPast ? '#94a3b8' : '#f59e0b', // 🔥 未来显示金黄色,已过去显示灰
         icon: '📸' // 🔥 更醒目的图标
       });
     }

+ 47 - 0
src/app/utils/date-utils.ts

@@ -0,0 +1,47 @@
+export function normalizeDateInput(value: any, fallback?: Date): Date {
+  const parsed = parseDate(value);
+  if (!isNaN(parsed.getTime())) {
+    return parsed;
+  }
+  if (fallback) {
+    const fb = new Date(fallback);
+    return isNaN(fb.getTime()) ? new Date() : fb;
+  }
+  return new Date();
+}
+
+export function addDays(date: Date, days: number): Date {
+  const base = new Date(date);
+  if (isNaN(base.getTime())) {
+    return new Date();
+  }
+  base.setDate(base.getDate() + days);
+  return base;
+}
+
+function parseDate(value: any): Date {
+  if (!value && value !== 0) {
+    return new Date(NaN);
+  }
+
+  if (value instanceof Date) {
+    return new Date(value.getTime());
+  }
+
+  if (typeof value === 'string' || typeof value === 'number') {
+    return new Date(value);
+  }
+
+  if (typeof value === 'object') {
+    if (value.iso) {
+      return new Date(value.iso);
+    }
+
+    if (value.__type === 'Date' && value.iso) {
+      return new Date(value.iso);
+    }
+  }
+
+  return new Date(NaN);
+}
+

+ 200 - 0
src/app/utils/phase-deadline.utils.ts

@@ -0,0 +1,200 @@
+import { PhaseDeadlines, PhaseInfo, PhaseName, PHASE_INFO } from '../models/project-phase.model';
+import { addDays, normalizeDateInput } from './date-utils';
+
+const DAY_MS = 24 * 60 * 60 * 1000;
+
+const DEFAULT_PHASE_RATIOS: Record<PhaseName, number> = {
+  modeling: 0.3,
+  softDecor: 0.25,
+  rendering: 0.3,
+  postProcessing: 0.15
+};
+
+export function generatePhaseDeadlines(
+  startDate: Date,
+  endDate?: Date,
+  ratios: Record<PhaseName, number> = DEFAULT_PHASE_RATIOS
+): PhaseDeadlines {
+  const safeStart = new Date(startDate);
+  const safeEnd = endDate ? new Date(endDate) : addDays(safeStart, 30);
+
+  if (isNaN(safeEnd.getTime()) || safeEnd <= safeStart) {
+    safeEnd.setTime(safeStart.getTime() + 30 * DAY_MS);
+  }
+
+  const totalDays = Math.max(4, Math.ceil((safeEnd.getTime() - safeStart.getTime()) / DAY_MS));
+  const durations = calculatePhaseDurations(totalDays, ratios);
+
+  const deadlines: PhaseDeadlines = {};
+  let cursor = new Date(safeStart);
+
+  (Object.keys(durations) as PhaseName[]).forEach((phase, index) => {
+    const days = Math.max(1, durations[phase]);
+    const deadline = addDays(cursor, days);
+
+    deadlines[phase] = {
+      startDate: cursor.toISOString(),
+      deadline: deadline.toISOString(),
+      estimatedDays: days,
+      status: index === 0 ? 'in_progress' : 'not_started',
+      priority: index === 0 ? 'high' : 'medium'
+    };
+
+    cursor = new Date(deadline.getTime());
+  });
+
+  return deadlines;
+}
+
+export function ensurePhaseDeadlines(
+  existing: PhaseDeadlines | undefined,
+  startDate: Date,
+  endDate?: Date
+): PhaseDeadlines {
+  if (existing) {
+    return existing;
+  }
+  return generatePhaseDeadlines(startDate, endDate);
+}
+
+export function mapDeliveryTypeToPhase(deliveryType: string): PhaseName | null {
+  const map: Record<string, PhaseName> = {
+    white_model: 'modeling',
+    soft_decor: 'softDecor',
+    rendering: 'rendering',
+    post_process: 'postProcessing'
+  };
+  return map[deliveryType] || null;
+}
+
+export function getNextPhaseName(current: PhaseName): PhaseName | null {
+  const order: PhaseName[] = ['modeling', 'softDecor', 'rendering', 'postProcessing'];
+  const index = order.indexOf(current);
+  if (index === -1 || index === order.length - 1) {
+    return null;
+  }
+  return order[index + 1];
+}
+
+export function updatePhaseOnSubmission(
+  deadlines: PhaseDeadlines,
+  phase: PhaseName,
+  submittedAt: Date
+): void {
+  const info = ensurePhaseInfo(deadlines, phase);
+  const iso = submittedAt.toISOString();
+
+  if (!info.startDate) {
+    info.startDate = iso;
+  }
+  if (!(info as any).firstUploadAt) {
+    (info as any).firstUploadAt = iso;
+  }
+  (info as any).lastSubmissionAt = iso;
+
+  if (!info.deadline) {
+    const days = info.estimatedDays || getDefaultPhaseDays(phase);
+    const start = normalizeDateInput(info.startDate, submittedAt);
+    info.deadline = addDays(start, days).toISOString();
+    info.estimatedDays = days;
+  }
+
+  if (info.status !== 'completed') {
+    info.status = 'in_progress';
+  }
+
+  scheduleNextPhase(deadlines, phase, submittedAt);
+}
+
+export function markPhaseStatus(
+  deadlines: PhaseDeadlines,
+  phase: PhaseName,
+  status: 'completed' | 'delayed' | 'in_progress',
+  timestamp: Date
+): void {
+  const info = ensurePhaseInfo(deadlines, phase);
+  info.status = status;
+  if (status === 'completed') {
+    info.completedAt = timestamp.toISOString();
+  } else if (info.completedAt) {
+    delete info.completedAt;
+  }
+}
+
+export function scheduleNextPhase(
+  deadlines: PhaseDeadlines,
+  currentPhase: PhaseName,
+  anchorDate: Date
+): void {
+  const nextPhase = getNextPhaseName(currentPhase);
+  if (!nextPhase) {
+    return;
+  }
+
+  const nextInfo = ensurePhaseInfo(deadlines, nextPhase);
+  if (!nextInfo.startDate) {
+    nextInfo.startDate = anchorDate.toISOString();
+  }
+
+  if (!nextInfo.deadline) {
+    const days = nextInfo.estimatedDays || getDefaultPhaseDays(nextPhase);
+    const start = normalizeDateInput(nextInfo.startDate, anchorDate);
+    nextInfo.deadline = addDays(start, days).toISOString();
+    nextInfo.estimatedDays = days;
+  }
+
+  if (!nextInfo.status || nextInfo.status === 'not_started') {
+    nextInfo.status = 'not_started';
+  }
+}
+
+function ensurePhaseInfo(deadlines: PhaseDeadlines, phase: PhaseName): PhaseInfo {
+  if (!deadlines[phase]) {
+    const start = new Date();
+    const defaultDays = getDefaultPhaseDays(phase);
+    deadlines[phase] = {
+      startDate: start.toISOString(),
+      deadline: addDays(start, defaultDays).toISOString(),
+      estimatedDays: defaultDays,
+      status: 'not_started',
+      priority: 'medium'
+    };
+  }
+  return deadlines[phase]!;
+}
+
+function calculatePhaseDurations(
+  totalDays: number,
+  ratios: Record<PhaseName, number>
+): Record<PhaseName, number> {
+  const result: Record<PhaseName, number> = {
+    modeling: 1,
+    softDecor: 1,
+    rendering: 1,
+    postProcessing: 1
+  };
+
+  let allocated = 0;
+  (Object.keys(result) as PhaseName[]).forEach((phase) => {
+    const ratio = ratios[phase] ?? 0.25;
+    const days = Math.max(1, Math.round(totalDays * ratio));
+    result[phase] = days;
+    allocated += days;
+  });
+
+  const diff = totalDays - allocated;
+  if (diff !== 0) {
+    result.postProcessing = Math.max(1, result.postProcessing + diff);
+  }
+
+  return result;
+}
+
+function getDefaultPhaseDays(phase: PhaseName): number {
+  const meta = PHASE_INFO[phase];
+  if (!meta) {
+    return 3;
+  }
+  return Math.max(1, meta.defaultDays || 3);
+}
+

+ 0 - 1
src/modules/project/pages/project-detail/project-detail.component.ts

@@ -569,7 +569,6 @@ export class ProjectDetailComponent implements OnInit, OnDestroy {
     // 如果是中文名称,转换为英文ID
     if (stageNameToId[workflowCurrent]) {
       workflowCurrent = stageNameToId[workflowCurrent];
-     // console.log('🔄 阶段名称映射:', this.project?.get('currentStage'), '->', workflowCurrent);
     }
 
     // 如果没有当前阶段(新创建的项目),默认订单分配为active(红色)

+ 62 - 1
src/modules/project/pages/project-detail/stages/stage-delivery.component.ts

@@ -9,6 +9,9 @@ import { ProductSpaceService, Project } from '../../../services/product-space.se
 import { WxworkAuth } from 'fmode-ng/social';
 import { DragUploadModalComponent, UploadResult } from '../../../components/drag-upload-modal/drag-upload-modal.component';
 import { ImageAnalysisService } from '../../../services/image-analysis.service';
+import { PhaseDeadlines, PhaseName } from '../../../../../app/models/project-phase.model';
+import { ensurePhaseDeadlines, mapDeliveryTypeToPhase, markPhaseStatus, updatePhaseOnSubmission } from '../../../../../app/utils/phase-deadline.utils';
+import { addDays, normalizeDateInput } from '../../../../../app/utils/date-utils';
 
 const Parse = FmodeParse.with('nova');
 
@@ -814,8 +817,11 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
       this.cdr.markForCheck();
       console.log(`成功上传 ${uploadedFiles} 个交付文件`);
 
-      // ✨ 上传成功后通知组长审批
+      // ✨ 上传成功后通知组长审批,并记录阶段时间线
       if (uploadedFiles > 0) {
+        const projectData = this.project.get('data') || {};
+        this.applyPhaseSubmissionMetadata(projectData, deliveryType, new Date());
+        this.project.set('data', projectData);
         await this.notifyTeamLeaderForApproval(uploadedFiles, deliveryType);
         // 二次校验:从服务器重新查询,确认已写入 ProjectFile 表
         await this.verifyProjectFilesOnServer(targetProjectId, productId, deliveryType);
@@ -1648,6 +1654,8 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
       data.deliveryStageStatus[stageKey].approvedByName = this.currentUser.get('name');
       data.deliveryStageStatus[stageKey].approvedAt = now;
 
+      this.applyPhaseStatusTransition(data, currentType, 'approved');
+
       // 补充:更新空间交付物汇总
       this.updateSpaceDeliverableSummary(data, currentType, 'approved');
       
@@ -1840,6 +1848,8 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
         data.deliveryApproval.rejectionReason = reason;
       }
       
+      this.applyPhaseStatusTransition(data, currentType, 'rejected');
+      
       this.project.set('data', data);
       
       console.log('💾 保存驳回结果...');
@@ -1920,6 +1930,8 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
       data.testMarkedAt = new Date().toISOString();
       data.testMarkedBy = this.currentUser?.get('name') || 'Unknown';
       
+      this.applyPhaseStatusTransition(data, currentType, 'pending');
+      
       this.project.set('data', data);
       await this.project.save();
 
@@ -1939,6 +1951,55 @@ export class StageDeliveryComponent implements OnInit, OnDestroy {
     }
   }
 
+  private getPhaseBaseStartDate(data: any): Date {
+    const source =
+      data?.phaseDeadlines?.modeling?.startDate ||
+      data?.requirementsConfirmedAt ||
+      this.project?.get('createdAt');
+    return normalizeDateInput(source, new Date());
+  }
+
+  private getPhaseEndDate(): Date {
+    const deadline = this.project?.get('deadline');
+    return normalizeDateInput(deadline, addDays(new Date(), 30));
+  }
+
+  private ensureTimelineDeadlines(data: any): PhaseDeadlines {
+    const start = this.getPhaseBaseStartDate(data);
+    const end = this.getPhaseEndDate();
+    data.phaseDeadlines = ensurePhaseDeadlines(data.phaseDeadlines, start, end);
+    return data.phaseDeadlines;
+  }
+
+  private applyPhaseSubmissionMetadata(data: any, deliveryType: string, submittedAt: Date): void {
+    const phase = mapDeliveryTypeToPhase(deliveryType);
+    if (!phase) return;
+    const deadlines = this.ensureTimelineDeadlines(data);
+    updatePhaseOnSubmission(deadlines, phase, submittedAt);
+    data.phaseDeadlines = deadlines;
+  }
+
+  private applyPhaseStatusTransition(
+    data: any,
+    deliveryType: string,
+    status: 'approved' | 'rejected' | 'pending'
+  ): void {
+    const phase = mapDeliveryTypeToPhase(deliveryType);
+    if (!phase) return;
+    const deadlines = this.ensureTimelineDeadlines(data);
+    const timestamp = new Date();
+
+    if (status === 'approved') {
+      markPhaseStatus(deadlines, phase, 'completed', timestamp);
+    } else if (status === 'rejected') {
+      markPhaseStatus(deadlines, phase, 'delayed', timestamp);
+    } else {
+      markPhaseStatus(deadlines, phase, 'in_progress', timestamp);
+    }
+
+    data.phaseDeadlines = deadlines;
+  }
+
   /**
    * 补充:更新空间交付物汇总
    */

+ 17 - 46
src/modules/project/pages/project-detail/stages/stage-requirements.component.ts

@@ -11,6 +11,8 @@ import { ColorGetDialogComponent } from '../../../components/color-get/color-get
 import { completionJSON } from 'fmode-ng/core';
 import { addIcons } from 'ionicons';
 import { add, chevronDown, colorPalette, send, sparkles, trash } from 'ionicons/icons';
+import { generatePhaseDeadlines } from '../../../../../app/utils/phase-deadline.utils';
+import { addDays, normalizeDateInput } from '../../../../../app/utils/date-utils';
 
 addIcons({
   add,sparkles,colorPalette,trash,chevronDown,send
@@ -2386,10 +2388,11 @@ ${context}
       const data = this.project.get('data') || {};
       
       // 保存需求确认数据
+      const confirmedAt = new Date();
       data.requirementsConfirmed = true;
       data.requirementsConfirmedBy = this.currentUser.id;
       data.requirementsConfirmedByName = this.currentUser.get('name');
-      data.requirementsConfirmedAt = new Date().toISOString();
+      data.requirementsConfirmedAt = confirmedAt.toISOString();
 
       // 补充:需求确认详细信息
       data.requirementsDetail = {
@@ -2415,51 +2418,8 @@ ${context}
         confirmedAt: new Date().toISOString()
       };
 
-      // 补充:初始化阶段截止时间 (基于项目交付日期推算)
-      if (!data.phaseDeadlines && this.project.get('deadline')) {
-        const deliveryDate = new Date(this.project.get('deadline'));
-        const startDate = new Date();
-        const totalDays = Math.ceil((deliveryDate.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000));
-        
-        // 按比例分配各阶段时间:建模30%,软装25%,渲染30%,后期15%
-        const modelingDays = Math.ceil(totalDays * 0.3);
-        const softDecorDays = Math.ceil(totalDays * 0.25);
-        const renderingDays = Math.ceil(totalDays * 0.3);
-        const postProcessDays = totalDays - modelingDays - softDecorDays - renderingDays;
-
-        let currentDate = new Date(startDate);
-        
-        data.phaseDeadlines = {
-          modeling: {
-            startDate: new Date(currentDate),
-            deadline: new Date(currentDate.setDate(currentDate.getDate() + modelingDays)),
-            estimatedDays: modelingDays,
-            status: 'not_started',
-            priority: 'high'
-          },
-          softDecor: {
-            startDate: new Date(currentDate),
-            deadline: new Date(currentDate.setDate(currentDate.getDate() + softDecorDays)),
-            estimatedDays: softDecorDays,
-            status: 'not_started',
-            priority: 'medium'
-          },
-          rendering: {
-            startDate: new Date(currentDate),
-            deadline: new Date(currentDate.setDate(currentDate.getDate() + renderingDays)),
-            estimatedDays: renderingDays,
-            status: 'not_started',
-            priority: 'high'
-          },
-          postProcessing: {
-            startDate: new Date(currentDate),
-            deadline: new Date(currentDate.setDate(currentDate.getDate() + postProcessDays)),
-            estimatedDays: postProcessDays,
-            status: 'not_started',
-            priority: 'medium'
-          }
-        };
-      }
+      // 重新生成阶段截止时间 (基于当前确认时间与项目交付日期)
+      this.rebuildPhaseDeadlines(data, confirmedAt);
 
       // 补充:初始化空间交付物汇总
       if (!data.spaceDeliverableSummary && this.projectProducts.length > 0) {
@@ -2518,6 +2478,17 @@ ${context}
     }
   }
 
+  private rebuildPhaseDeadlines(data: any, confirmedAt?: Date): void {
+    const start = confirmedAt ? new Date(confirmedAt) : new Date();
+    const projectDeadline = this.project?.get('deadline');
+    const fallbackDelivery = addDays(start, 30);
+    const deliveryDate = projectDeadline
+      ? normalizeDateInput(projectDeadline, fallbackDelivery)
+      : fallbackDelivery;
+
+    data.phaseDeadlines = generatePhaseDeadlines(start, deliveryDate);
+  }
+
   // ===== 工具方法 =====
 
   /**