浏览代码

feat: add phase deadlines and enhance project timeline visualization

- Implemented dynamic generation of phase deadlines based on delivery date for improved project tracking.
- Updated project timeline to include visual indicators for phase deadlines, enhancing user awareness of project status.
- Refactored event marker logic to streamline the display of project events, including phase deadlines.
- Enhanced legend in the project timeline to clearly represent new phase deadlines and their statuses.
0235711 1 天之前
父节点
当前提交
1ce019b51c

+ 213 - 0
cloud/jobs/migrate-project-phase-deadlines.js

@@ -0,0 +1,213 @@
+/**
+ * 数据迁移任务:为现有项目添加阶段截止时间
+ * 
+ * 使用方法:
+ * Parse.Cloud.startJob('migrateProjectPhaseDeadlines', {
+ *   dryRun: true,  // 是否干跑(只计算不保存)
+ *   batchSize: 100 // 批量处理大小
+ * })
+ */
+
+Parse.Cloud.job("migrateProjectPhaseDeadlines", async (request) => {
+  const { params, message } = request;
+  const { dryRun = false, batchSize = 100 } = params || {};
+  
+  message(`开始迁移项目阶段截止时间数据...`);
+  message(`干跑模式: ${dryRun ? '是' : '否'}`);
+  message(`批处理大小: ${batchSize}`);
+  
+  // 阶段默认工期(天数)
+  const DEFAULT_DURATIONS = {
+    modeling: 1,        // 建模默认1天(临时改为1天便于查看效果)
+    softDecor: 1,       // 软装默认1天(临时改为1天便于查看效果)
+    rendering: 1,       // 渲染默认1天(临时改为1天便于查看效果)
+    postProcessing: 1   // 后期默认1天(临时改为1天便于查看效果)
+  };
+  
+  try {
+    // 查询所有未删除且没有阶段截止时间的项目
+    const projectQuery = new Parse.Query("Project");
+    projectQuery.notEqualTo("isDeleted", true);
+    projectQuery.limit(10000); // 限制最大数量
+    
+    const totalCount = await projectQuery.count({ useMasterKey: true });
+    message(`找到${totalCount}个项目待检查`);
+    
+    let processedCount = 0;
+    let updatedCount = 0;
+    let skippedCount = 0;
+    let errorCount = 0;
+    
+    // 分批处理
+    for (let skip = 0; skip < totalCount; skip += batchSize) {
+      projectQuery.skip(skip);
+      projectQuery.limit(batchSize);
+      
+      const projects = await projectQuery.find({ useMasterKey: true });
+      message(`处理批次: ${skip}-${skip + projects.length}/${totalCount}`);
+      
+      for (const project of projects) {
+        try {
+          processedCount++;
+          
+          const data = project.get("data") || {};
+          
+          // 如果已经有phaseDeadlines,跳过
+          if (data.phaseDeadlines) {
+            skippedCount++;
+            continue;
+          }
+          
+          // 获取项目截止时间
+          const deadline = project.get("deadline");
+          
+          // 如果没有截止时间,也跳过
+          if (!deadline) {
+            message(`  项目 ${project.id} (${project.get("title")}) 没有截止时间,跳过`);
+            skippedCount++;
+            continue;
+          }
+          
+          // 根据项目deadline推算各阶段截止时间
+          const deadlineTime = deadline.getTime();
+          
+          // 计算各阶段截止时间(从后往前推)
+          const postProcessingDeadline = new Date(deadlineTime); // 后期:项目截止日
+          const renderingDeadline = new Date(deadlineTime - DEFAULT_DURATIONS.postProcessing * 24 * 60 * 60 * 1000); // 渲染:提前3天
+          const softDecorDeadline = new Date(deadlineTime - (DEFAULT_DURATIONS.postProcessing + DEFAULT_DURATIONS.rendering) * 24 * 60 * 60 * 1000); // 软装:提前9天
+          const modelingDeadline = new Date(deadlineTime - (DEFAULT_DURATIONS.postProcessing + DEFAULT_DURATIONS.rendering + DEFAULT_DURATIONS.softDecor) * 24 * 60 * 60 * 1000); // 建模:提前13天
+          
+          // 构建phaseDeadlines对象
+          const phaseDeadlines = {
+            modeling: {
+              deadline: modelingDeadline,
+              estimatedDays: DEFAULT_DURATIONS.modeling,
+              status: "not_started",
+              priority: "medium"
+            },
+            softDecor: {
+              deadline: softDecorDeadline,
+              estimatedDays: DEFAULT_DURATIONS.softDecor,
+              status: "not_started",
+              priority: "medium"
+            },
+            rendering: {
+              deadline: renderingDeadline,
+              estimatedDays: DEFAULT_DURATIONS.rendering,
+              status: "not_started",
+              priority: "medium"
+            },
+            postProcessing: {
+              deadline: postProcessingDeadline,
+              estimatedDays: DEFAULT_DURATIONS.postProcessing,
+              status: "not_started",
+              priority: "medium"
+            }
+          };
+          
+          // 如果不是干跑模式,保存数据
+          if (!dryRun) {
+            data.phaseDeadlines = phaseDeadlines;
+            project.set("data", data);
+            await project.save(null, { useMasterKey: true });
+          }
+          
+          updatedCount++;
+          
+          // 每10个项目输出一次详细日志
+          if (updatedCount % 10 === 0) {
+            message(`  ✅ 已更新 ${updatedCount} 个项目`);
+          }
+          
+        } catch (error) {
+          errorCount++;
+          message(`  ❌ 处理项目 ${project.id} 失败: ${error.message}`);
+        }
+      }
+    }
+    
+    // 输出最终统计
+    message('');
+    message('='.repeat(50));
+    message('迁移完成!统计信息:');
+    message(`  总处理数: ${processedCount}`);
+    message(`  已更新: ${updatedCount}`);
+    message(`  已跳过: ${skippedCount}`);
+    message(`  失败数: ${errorCount}`);
+    message(`  干跑模式: ${dryRun ? '是(未实际保存)' : '否(已保存)'}`);
+    message('='.repeat(50));
+    
+  } catch (error) {
+    message(`❌ 迁移失败: ${error.message}`);
+    throw error;
+  }
+});
+
+/**
+ * 测试单个项目的阶段截止时间生成
+ * 
+ * 使用方法:
+ * Parse.Cloud.run('testProjectPhaseDeadlines', { projectId: 'xxx' })
+ */
+Parse.Cloud.define("testProjectPhaseDeadlines", async (request) => {
+  const { projectId } = request.params;
+  
+  if (!projectId) {
+    throw new Error("缺少projectId参数");
+  }
+  
+  const projectQuery = new Parse.Query("Project");
+  const project = await projectQuery.get(projectId, { useMasterKey: true });
+  
+  const deadline = project.get("deadline");
+  if (!deadline) {
+    throw new Error("项目没有截止时间");
+  }
+  
+  const data = project.get("data") || {};
+  
+  // 默认工期
+  const DEFAULT_DURATIONS = {
+    modeling: 1,
+    softDecor: 1,
+    rendering: 1,
+    postProcessing: 1
+  };
+  
+  // 计算阶段截止时间
+  const deadlineTime = deadline.getTime();
+  const postProcessingDeadline = new Date(deadlineTime);
+  const renderingDeadline = new Date(deadlineTime - DEFAULT_DURATIONS.postProcessing * 24 * 60 * 60 * 1000);
+  const softDecorDeadline = new Date(deadlineTime - (DEFAULT_DURATIONS.postProcessing + DEFAULT_DURATIONS.rendering) * 24 * 60 * 60 * 1000);
+  const modelingDeadline = new Date(deadlineTime - (DEFAULT_DURATIONS.postProcessing + DEFAULT_DURATIONS.rendering + DEFAULT_DURATIONS.softDecor) * 24 * 60 * 60 * 1000);
+  
+  return {
+    projectId: project.id,
+    projectTitle: project.get("title"),
+    projectDeadline: deadline,
+    phaseDeadlines: {
+      modeling: {
+        deadline: modelingDeadline,
+        estimatedDays: DEFAULT_DURATIONS.modeling,
+        daysFromNow: Math.ceil((modelingDeadline.getTime() - Date.now()) / (24 * 60 * 60 * 1000))
+      },
+      softDecor: {
+        deadline: softDecorDeadline,
+        estimatedDays: DEFAULT_DURATIONS.softDecor,
+        daysFromNow: Math.ceil((softDecorDeadline.getTime() - Date.now()) / (24 * 60 * 60 * 1000))
+      },
+      rendering: {
+        deadline: renderingDeadline,
+        estimatedDays: DEFAULT_DURATIONS.rendering,
+        daysFromNow: Math.ceil((renderingDeadline.getTime() - Date.now()) / (24 * 60 * 60 * 1000))
+      },
+      postProcessing: {
+        deadline: postProcessingDeadline,
+        estimatedDays: DEFAULT_DURATIONS.postProcessing,
+        daysFromNow: Math.ceil((postProcessingDeadline.getTime() - Date.now()) / (24 * 60 * 60 * 1000))
+      }
+    },
+    currentData: data
+  };
+});
+

+ 255 - 0
cloud/utils/project-phase-utils.js

@@ -0,0 +1,255 @@
+/**
+ * 项目阶段截止时间工具函数
+ */
+
+/**
+ * 默认工期配置(天数)
+ */
+const DEFAULT_PHASE_DURATIONS = {
+  modeling: 1,        // 建模默认1天
+  softDecor: 1,       // 软装默认1天
+  rendering: 1,       // 渲染默认1天
+  postProcessing: 1   // 后期默认1天
+};
+
+/**
+ * 生成项目阶段截止时间
+ * @param {Date} projectStartDate - 项目开始日期(可选)
+ * @param {Date} projectDeadline - 项目截止日期(必填)
+ * @param {Object} customDurations - 自定义工期(可选)
+ * @returns {Object} phaseDeadlines对象
+ */
+function generatePhaseDeadlines(projectStartDate, projectDeadline, customDurations = {}) {
+  if (!projectDeadline) {
+    throw new Error("项目截止日期(projectDeadline)是必填参数");
+  }
+  
+  // 合并默认工期和自定义工期
+  const durations = {
+    ...DEFAULT_PHASE_DURATIONS,
+    ...customDurations
+  };
+  
+  const deadlineTime = projectDeadline.getTime();
+  
+  // 从后往前计算各阶段截止时间
+  const postProcessingDeadline = new Date(deadlineTime);
+  const renderingDeadline = new Date(deadlineTime - durations.postProcessing * 24 * 60 * 60 * 1000);
+  const softDecorDeadline = new Date(deadlineTime - (durations.postProcessing + durations.rendering) * 24 * 60 * 60 * 1000);
+  const modelingDeadline = new Date(deadlineTime - (durations.postProcessing + durations.rendering + durations.softDecor) * 24 * 60 * 60 * 1000);
+  
+  // 如果提供了开始日期,使用它作为建模阶段的开始时间
+  const modelingStartDate = projectStartDate || new Date(deadlineTime - (durations.modeling + durations.softDecor + durations.rendering + durations.postProcessing) * 24 * 60 * 60 * 1000);
+  
+  // 构建阶段截止时间对象
+  return {
+    modeling: {
+      startDate: modelingStartDate,
+      deadline: modelingDeadline,
+      estimatedDays: durations.modeling,
+      status: "in_progress", // 默认第一个阶段为进行中
+      priority: "medium"
+    },
+    softDecor: {
+      startDate: modelingDeadline,
+      deadline: softDecorDeadline,
+      estimatedDays: durations.softDecor,
+      status: "not_started",
+      priority: "medium"
+    },
+    rendering: {
+      startDate: softDecorDeadline,
+      deadline: renderingDeadline,
+      estimatedDays: durations.rendering,
+      status: "not_started",
+      priority: "medium"
+    },
+    postProcessing: {
+      startDate: renderingDeadline,
+      deadline: postProcessingDeadline,
+      estimatedDays: durations.postProcessing,
+      status: "not_started",
+      priority: "medium"
+    }
+  };
+}
+
+/**
+ * 从公司配置获取工期设置
+ * @param {String} companyId - 公司ID
+ * @returns {Promise<Object>} 工期配置
+ */
+async function getCompanyPhaseDurations(companyId) {
+  try {
+    const companyQuery = new Parse.Query("Company");
+    const company = await companyQuery.get(companyId, { useMasterKey: true });
+    
+    const companyData = company.get("data") || {};
+    return companyData.phaseDefaultDurations || DEFAULT_PHASE_DURATIONS;
+  } catch (error) {
+    console.warn(`获取公司${companyId}的工期配置失败,使用默认配置:`, error.message);
+    return DEFAULT_PHASE_DURATIONS;
+  }
+}
+
+/**
+ * 更新阶段状态
+ * @param {String} projectId - 项目ID
+ * @param {String} phaseName - 阶段名称 (modeling/softDecor/rendering/postProcessing)
+ * @param {String} status - 新状态 (not_started/in_progress/completed/delayed)
+ * @param {Object} additionalData - 额外数据(如completedAt)
+ * @returns {Promise<Object>} 更新后的项目对象
+ */
+async function updatePhaseStatus(projectId, phaseName, status, additionalData = {}) {
+  const validPhases = ['modeling', 'softDecor', 'rendering', 'postProcessing'];
+  const validStatuses = ['not_started', 'in_progress', 'completed', 'delayed'];
+  
+  if (!validPhases.includes(phaseName)) {
+    throw new Error(`无效的阶段名称: ${phaseName}`);
+  }
+  
+  if (!validStatuses.includes(status)) {
+    throw new Error(`无效的状态: ${status}`);
+  }
+  
+  const projectQuery = new Parse.Query("Project");
+  const project = await projectQuery.get(projectId, { useMasterKey: true });
+  
+  const data = project.get("data") || {};
+  const phaseDeadlines = data.phaseDeadlines || {};
+  
+  if (!phaseDeadlines[phaseName]) {
+    throw new Error(`项目${projectId}没有${phaseName}阶段信息`);
+  }
+  
+  // 更新阶段状态
+  phaseDeadlines[phaseName].status = status;
+  
+  // 如果是完成状态,记录完成时间
+  if (status === 'completed' && !phaseDeadlines[phaseName].completedAt) {
+    phaseDeadlines[phaseName].completedAt = new Date();
+  }
+  
+  // 合并额外数据
+  Object.assign(phaseDeadlines[phaseName], additionalData);
+  
+  data.phaseDeadlines = phaseDeadlines;
+  project.set("data", data);
+  
+  await project.save(null, { useMasterKey: true });
+  
+  return project;
+}
+
+/**
+ * 获取项目当前阶段
+ * @param {Object} phaseDeadlines - 阶段截止时间对象
+ * @returns {String} 当前阶段名称
+ */
+function getCurrentPhase(phaseDeadlines) {
+  if (!phaseDeadlines) {
+    return null;
+  }
+  
+  const phases = ['modeling', 'softDecor', 'rendering', 'postProcessing'];
+  
+  // 找到第一个未完成的阶段
+  for (const phase of phases) {
+    const phaseInfo = phaseDeadlines[phase];
+    if (phaseInfo && phaseInfo.status !== 'completed') {
+      return phase;
+    }
+  }
+  
+  // 所有阶段都完成了,返回最后一个阶段
+  return 'postProcessing';
+}
+
+/**
+ * 检查阶段是否延期
+ * @param {Object} phaseInfo - 阶段信息
+ * @returns {Boolean} 是否延期
+ */
+function isPhaseDelayed(phaseInfo) {
+  if (!phaseInfo || !phaseInfo.deadline) {
+    return false;
+  }
+  
+  // 已完成的阶段不算延期
+  if (phaseInfo.status === 'completed') {
+    return false;
+  }
+  
+  const deadline = new Date(phaseInfo.deadline);
+  const now = new Date();
+  
+  return now > deadline;
+}
+
+/**
+ * 获取阶段剩余天数
+ * @param {Object} phaseInfo - 阶段信息
+ * @returns {Number} 剩余天数(负数表示已逾期)
+ */
+function getPhaseDaysRemaining(phaseInfo) {
+  if (!phaseInfo || !phaseInfo.deadline) {
+    return 0;
+  }
+  
+  const deadline = new Date(phaseInfo.deadline);
+  const now = new Date();
+  const diff = deadline.getTime() - now.getTime();
+  
+  return Math.ceil(diff / (24 * 60 * 60 * 1000));
+}
+
+// 导出函数
+module.exports = {
+  DEFAULT_PHASE_DURATIONS,
+  generatePhaseDeadlines,
+  getCompanyPhaseDurations,
+  updatePhaseStatus,
+  getCurrentPhase,
+  isPhaseDelayed,
+  getPhaseDaysRemaining
+};
+
+// Parse Cloud Code函数注册
+Parse.Cloud.define("generateProjectPhaseDeadlines", async (request) => {
+  const { projectStartDate, projectDeadline, companyId, customDurations } = request.params;
+  
+  if (!projectDeadline) {
+    throw new Error("缺少projectDeadline参数");
+  }
+  
+  // 如果提供了companyId,获取公司的默认工期配置
+  let durations = customDurations || {};
+  if (companyId) {
+    const companyDurations = await getCompanyPhaseDurations(companyId);
+    durations = { ...companyDurations, ...customDurations };
+  }
+  
+  const startDate = projectStartDate ? new Date(projectStartDate) : null;
+  const deadline = new Date(projectDeadline);
+  
+  return generatePhaseDeadlines(startDate, deadline, durations);
+});
+
+Parse.Cloud.define("updateProjectPhaseStatus", async (request) => {
+  const { projectId, phaseName, status, additionalData } = request.params;
+  
+  if (!projectId) {
+    throw new Error("缺少projectId参数");
+  }
+  
+  if (!phaseName) {
+    throw new Error("缺少phaseName参数");
+  }
+  
+  if (!status) {
+    throw new Error("缺少status参数");
+  }
+  
+  return await updatePhaseStatus(projectId, phaseName, status, additionalData);
+});
+

+ 396 - 0
docs/schema/project-phase-implementation-guide.md

@@ -0,0 +1,396 @@
+# 项目阶段截止时间功能实施指南
+
+## ✅ 已完成的实施内容
+
+### 1. 前端类型定义 ✅
+
+**文件位置**: `src/app/models/project-phase.model.ts`
+
+包含内容:
+- `PhaseInfo` - 单个阶段信息接口
+- `PhaseDeadlines` - 阶段截止时间集合接口
+- `ProjectData` - Project.data字段类型定义
+- `PHASE_INFO` - 阶段常量配置(建模/软装/渲染/后期)
+- `PHASE_STATUS_INFO` - 阶段状态信息
+- `PHASE_PRIORITY_INFO` - 优先级信息
+- 工具函数:
+  - `generateDefaultPhaseDeadlines()` - 生成默认阶段截止时间
+  - `isPhaseDelayed()` - 检查阶段是否延期
+  - `getPhaseDaysRemaining()` - 获取阶段剩余天数
+  - `getPhaseProgress()` - 获取阶段进度百分比
+
+### 2. 项目时间轴组件更新 ✅
+
+**文件位置**: 
+- `src/app/pages/team-leader/project-timeline/project-timeline.ts`
+- `src/app/pages/team-leader/project-timeline/project-timeline.html`
+
+更新内容:
+- ✅ 导入阶段类型定义和工具函数
+- ✅ 扩展 `ProjectTimeline` 接口添加 `phaseDeadlines` 字段
+- ✅ 添加 `TimelineEvent` 接口支持多种事件类型
+- ✅ 新增 `getProjectEvents()` 方法统一获取所有事件(含阶段截止)
+- ✅ 新增阶段相关工具方法:
+  - `getPhaseLabel()` - 获取阶段标签
+  - `getPhaseIcon()` - 获取阶段图标
+  - `getPhaseColor()` - 获取阶段颜色
+- ✅ 模板更新:
+  - 使用统一的事件标记循环显示所有事件
+  - 图例添加阶段截止时间说明(🎨建模/🪑软装/🖼️渲染/✨后期)
+
+### 3. Dashboard组件数据传递 ✅
+
+**文件位置**: `src/app/pages/team-leader/dashboard/dashboard.ts`
+
+更新内容:
+- ✅ `convertToProjectTimeline()` 方法中读取 `project.data.phaseDeadlines`
+- ✅ 将阶段截止时间数据传递给时间轴组件
+
+### 4. Cloud Code数据迁移脚本 ✅
+
+**文件位置**: `cloud/jobs/migrate-project-phase-deadlines.js`
+
+功能:
+- ✅ `migrateProjectPhaseDeadlines` - 批量为现有项目添加阶段截止时间
+  - 支持干跑模式(`dryRun: true`)
+  - 支持批量处理(`batchSize: 100`)
+  - 根据项目deadline反推各阶段时间
+  - 详细的进度和统计信息
+- ✅ `testProjectPhaseDeadlines` - 测试单个项目的阶段时间生成
+
+### 5. Cloud Code工具函数 ✅
+
+**文件位置**: `cloud/utils/project-phase-utils.js`
+
+功能:
+- ✅ `generatePhaseDeadlines()` - 生成阶段截止时间
+- ✅ `getCompanyPhaseDurations()` - 获取公司级默认工期配置
+- ✅ `updatePhaseStatus()` - 更新阶段状态
+- ✅ `getCurrentPhase()` - 获取当前阶段
+- ✅ `isPhaseDelayed()` - 检查是否延期
+- ✅ `getPhaseDaysRemaining()` - 获取剩余天数
+
+Cloud Function:
+- ✅ `generateProjectPhaseDeadlines` - 生成阶段截止时间
+- ✅ `updateProjectPhaseStatus` - 更新阶段状态
+
+### 6. 设计方案文档 ✅
+
+**文件位置**: `docs/schema/project-phase-deadlines-design.md`
+
+包含完整的:
+- 数据结构设计
+- JSON示例
+- 前后端代码示例
+- 可视化建议
+- 实施步骤
+
+---
+
+## 🚀 如何使用
+
+### 前端使用示例
+
+#### 1. 在组件中使用类型定义
+
+```typescript
+import { PhaseDeadlines, PHASE_INFO, generateDefaultPhaseDeadlines } from '../models/project-phase.model';
+
+// 生成默认阶段截止时间
+const phaseDeadlines = generateDefaultPhaseDeadlines(new Date());
+
+// 访问阶段信息
+const modelingPhase = phaseDeadlines.modeling;
+console.log('建模截止时间:', modelingPhase?.deadline);
+
+// 使用常量获取阶段信息
+const phaseConfig = PHASE_INFO.modeling;
+console.log('建模阶段:', phaseConfig.label, phaseConfig.icon, phaseConfig.color);
+```
+
+#### 2. 在项目时间轴中展示
+
+时间轴组件已自动支持阶段截止时间展示,只需确保传入的项目数据包含 `phaseDeadlines` 字段:
+
+```typescript
+const projectData: ProjectTimeline = {
+  projectId: 'xxx',
+  projectName: '李总现代简约全案',
+  // ... 其他字段
+  phaseDeadlines: {
+    modeling: {
+      deadline: new Date('2024-12-08'),
+      status: 'in_progress',
+      estimatedDays: 7
+    },
+    // ... 其他阶段
+  }
+};
+```
+
+### Cloud Code使用示例
+
+#### 1. 数据迁移(为现有项目添加阶段时间)
+
+```javascript
+// 干跑模式(只计算不保存)
+Parse.Cloud.startJob('migrateProjectPhaseDeadlines', {
+  dryRun: true,
+  batchSize: 50
+});
+
+// 正式迁移
+Parse.Cloud.startJob('migrateProjectPhaseDeadlines', {
+  dryRun: false,
+  batchSize: 100
+});
+```
+
+#### 2. 创建项目时生成阶段截止时间
+
+```javascript
+// 在afterSave钩子中自动生成
+Parse.Cloud.afterSave("Project", async (request) => {
+  const project = request.object;
+  
+  // 检查是否是新项目且有deadline
+  if (project.existed() || !project.get("deadline")) {
+    return;
+  }
+  
+  const data = project.get("data") || {};
+  
+  // 如果已经有phaseDeadlines,跳过
+  if (data.phaseDeadlines) {
+    return;
+  }
+  
+  // 生成阶段截止时间
+  const { generatePhaseDeadlines } = require('./utils/project-phase-utils');
+  const phaseDeadlines = generatePhaseDeadlines(
+    project.get("createdAt"),
+    project.get("deadline")
+  );
+  
+  data.phaseDeadlines = phaseDeadlines;
+  project.set("data", data);
+  
+  await project.save(null, { useMasterKey: true });
+});
+```
+
+#### 3. 更新阶段状态
+
+```javascript
+// 方式1:使用Cloud Function
+await Parse.Cloud.run('updateProjectPhaseStatus', {
+  projectId: 'xxx',
+  phaseName: 'modeling',
+  status: 'completed',
+  additionalData: {
+    completedAt: new Date(),
+    notes: '建模阶段已完成'
+  }
+});
+
+// 方式2:直接使用工具函数
+const { updatePhaseStatus } = require('./utils/project-phase-utils');
+await updatePhaseStatus('projectId', 'modeling', 'completed', {
+  completedAt: new Date()
+});
+```
+
+#### 4. 设置公司默认工期
+
+```javascript
+// 在Company.data中添加配置
+const company = await new Parse.Query("Company").get(companyId);
+const data = company.get("data") || {};
+
+data.phaseDefaultDurations = {
+  modeling: 8,        // 建模8天
+  softDecor: 5,       // 软装5天
+  rendering: 7,       // 渲染7天
+  postProcessing: 4   // 后期4天
+};
+
+company.set("data", data);
+await company.save(null, { useMasterKey: true });
+```
+
+---
+
+## 📊 数据结构说明
+
+### Project.data.phaseDeadlines 字段结构
+
+```json
+{
+  "phaseDeadlines": {
+    "modeling": {
+      "startDate": "2024-12-01T00:00:00.000Z",
+      "deadline": "2024-12-08T23:59:59.999Z",
+      "estimatedDays": 7,
+      "status": "in_progress",
+      "completedAt": "2024-12-07T18:30:00.000Z",
+      "assignee": {
+        "__type": "Pointer",
+        "className": "Profile",
+        "objectId": "prof001"
+      },
+      "priority": "high",
+      "notes": "客户要求加急"
+    },
+    "softDecor": {
+      "deadline": "2024-12-13T23:59:59.999Z",
+      "estimatedDays": 4,
+      "status": "not_started",
+      "priority": "medium"
+    },
+    "rendering": {
+      "deadline": "2024-12-20T23:59:59.999Z",
+      "estimatedDays": 6,
+      "status": "not_started",
+      "priority": "high"
+    },
+    "postProcessing": {
+      "deadline": "2024-12-24T23:59:59.999Z",
+      "estimatedDays": 3,
+      "status": "not_started",
+      "priority": "medium"
+    }
+  }
+}
+```
+
+### 字段说明
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| startDate | Date | 否 | 阶段开始时间 |
+| deadline | Date | 是 | 阶段截止时间 |
+| estimatedDays | Number | 否 | 预计工期(天数) |
+| status | String | 否 | 阶段状态 (not_started/in_progress/completed/delayed) |
+| completedAt | Date | 否 | 实际完成时间 |
+| assignee | Pointer | 否 | 负责人 |
+| priority | String | 否 | 优先级 (low/medium/high/urgent) |
+| notes | String | 否 | 备注信息 |
+
+---
+
+## 🎨 可视化效果
+
+### 阶段图标和颜色
+
+- 🎨 **建模** - 蓝色 (#2196F3)
+- 🪑 **软装** - 紫色 (#9C27B0)
+- 🖼️ **渲染** - 橙色 (#FF9800)
+- ✨ **后期** - 绿色 (#4CAF50)
+
+### 时间轴展示
+
+- 项目条形图按紧急程度显示颜色
+- 阶段截止时间在时间轴上显示为彩色标记
+- 悬停显示详细信息(阶段名称、截止时间)
+- 只显示今日线之后的未来事件
+
+---
+
+## 🧪 测试建议
+
+### 1. 单元测试
+
+```typescript
+describe('PhaseDeadlines', () => {
+  it('should generate default phase deadlines', () => {
+    const startDate = new Date('2024-12-01');
+    const phaseDeadlines = generateDefaultPhaseDeadlines(startDate);
+    
+    expect(phaseDeadlines.modeling).toBeDefined();
+    expect(phaseDeadlines.softDecor).toBeDefined();
+    expect(phaseDeadlines.rendering).toBeDefined();
+    expect(phaseDeadlines.postProcessing).toBeDefined();
+  });
+  
+  it('should check if phase is delayed', () => {
+    const pastPhase: PhaseInfo = {
+      deadline: new Date('2024-01-01'),
+      status: 'in_progress'
+    };
+    expect(isPhaseDelayed(pastPhase)).toBe(true);
+    
+    const futurePhase: PhaseInfo = {
+      deadline: new Date('2025-12-31'),
+      status: 'in_progress'
+    };
+    expect(isPhaseDelayed(futurePhase)).toBe(false);
+  });
+});
+```
+
+### 2. E2E测试
+
+1. 创建新项目,验证自动生成阶段截止时间
+2. 切换到时间轴视图,验证阶段标记显示
+3. 悬停阶段标记,验证工具提示信息
+4. 更新阶段状态,验证UI更新
+
+---
+
+## 📋 待办事项
+
+### 已完成 ✅
+- [x] 创建前端类型定义文件
+- [x] 更新项目时间轴组件
+- [x] 创建数据迁移脚本
+- [x] 创建工具函数
+- [x] 编写使用文档
+
+### 可选增强功能 🔮
+- [ ] 添加阶段管理界面(手动调整截止时间)
+- [ ] 阶段延期自动提醒功能
+- [ ] 阶段进度追踪报表
+- [ ] 支持自定义阶段(不限于4个阶段)
+- [ ] 阶段依赖关系管理
+
+---
+
+## 🐛 故障排查
+
+### 问题1:时间轴没有显示阶段标记
+
+**原因**:项目数据中没有 `phaseDeadlines` 字段
+
+**解决方案**:
+1. 运行数据迁移脚本为现有项目添加数据
+2. 或手动为项目添加 `phaseDeadlines` 数据
+
+### 问题2:阶段时间计算不正确
+
+**原因**:项目没有 `deadline` 字段
+
+**解决方案**:
+确保所有项目都设置了 `deadline` 字段
+
+### 问题3:时间轴性能问题
+
+**原因**:项目数量过多
+
+**解决方案**:
+1. 使用筛选功能减少显示的项目数量
+2. 考虑添加分页或虚拟滚动
+
+---
+
+## 📞 技术支持
+
+如有问题,请参考:
+1. 设计方案文档:`docs/schema/project-phase-deadlines-design.md`
+2. schemas.md数据范式文档:`rules/schemas.md`
+3. 代码注释和类型定义
+
+---
+
+**最后更新**: 2024年11月6日  
+**版本**: 1.0.0
+

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

@@ -0,0 +1,247 @@
+/**
+ * 项目阶段信息模型
+ * 用于Project.data.phaseDeadlines字段
+ */
+
+/**
+ * 阶段状态枚举
+ */
+export type PhaseStatus = 'not_started' | 'in_progress' | 'completed' | 'delayed';
+
+/**
+ * 优先级枚举
+ */
+export type PhasePriority = 'low' | 'medium' | 'high' | 'urgent';
+
+/**
+ * 阶段名称枚举
+ */
+export type PhaseName = 'modeling' | 'softDecor' | 'rendering' | 'postProcessing';
+
+/**
+ * 单个阶段信息
+ */
+export interface PhaseInfo {
+  /** 阶段开始时间 */
+  startDate?: Date | string;
+  /** 阶段截止时间 */
+  deadline: Date | string;
+  /** 预计工期(天数) */
+  estimatedDays?: number;
+  /** 阶段状态 */
+  status?: PhaseStatus;
+  /** 实际完成时间 */
+  completedAt?: Date | string;
+  /** 负责人 */
+  assignee?: {
+    __type: 'Pointer';
+    className: 'Profile';
+    objectId: string;
+  };
+  /** 优先级 */
+  priority?: PhasePriority;
+  /** 备注信息 */
+  notes?: string;
+}
+
+/**
+ * 项目阶段截止时间集合
+ */
+export interface PhaseDeadlines {
+  /** 建模阶段 */
+  modeling?: PhaseInfo;
+  /** 软装阶段 */
+  softDecor?: PhaseInfo;
+  /** 渲染阶段 */
+  rendering?: PhaseInfo;
+  /** 后期阶段 */
+  postProcessing?: PhaseInfo;
+}
+
+/**
+ * Project.data字段类型
+ */
+export interface ProjectData {
+  /** 阶段截止时间 */
+  phaseDeadlines?: PhaseDeadlines;
+  /** 其他扩展数据 */
+  [key: string]: any;
+}
+
+/**
+ * 阶段信息常量
+ */
+export const PHASE_INFO = {
+  modeling: {
+    key: 'modeling' as PhaseName,
+    label: '建模',
+    icon: '🎨',
+    color: '#2196F3',
+    bgColor: '#E3F2FD',
+    defaultDays: 1  // 🆕 临时改为1天便于查看效果
+  },
+  softDecor: {
+    key: 'softDecor' as PhaseName,
+    label: '软装',
+    icon: '🪑',
+    color: '#9C27B0',
+    bgColor: '#F3E5F5',
+    defaultDays: 1  // 🆕 临时改为1天便于查看效果
+  },
+  rendering: {
+    key: 'rendering' as PhaseName,
+    label: '渲染',
+    icon: '🖼️',
+    color: '#FF9800',
+    bgColor: '#FFF3E0',
+    defaultDays: 1  // 🆕 临时改为1天便于查看效果
+  },
+  postProcessing: {
+    key: 'postProcessing' as PhaseName,
+    label: '后期',
+    icon: '✨',
+    color: '#4CAF50',
+    bgColor: '#E8F5E9',
+    defaultDays: 1  // 🆕 临时改为1天便于查看效果
+  }
+} as const;
+
+/**
+ * 阶段状态信息
+ */
+export const PHASE_STATUS_INFO = {
+  not_started: {
+    label: '未开始',
+    icon: '⏸️',
+    color: '#9E9E9E'
+  },
+  in_progress: {
+    label: '进行中',
+    icon: '▶️',
+    color: '#2196F3'
+  },
+  completed: {
+    label: '已完成',
+    icon: '✅',
+    color: '#4CAF50'
+  },
+  delayed: {
+    label: '已延期',
+    icon: '⚠️',
+    color: '#F44336'
+  }
+} as const;
+
+/**
+ * 优先级信息
+ */
+export const PHASE_PRIORITY_INFO = {
+  low: {
+    label: '低',
+    color: '#9E9E9E'
+  },
+  medium: {
+    label: '中',
+    color: '#FF9800'
+  },
+  high: {
+    label: '高',
+    color: '#F44336'
+  },
+  urgent: {
+    label: '紧急',
+    color: '#D32F2F'
+  }
+} as const;
+
+/**
+ * 工具函数:生成默认阶段截止时间
+ * @param startDate 项目开始时间
+ * @returns 默认的阶段截止时间对象
+ */
+export function generateDefaultPhaseDeadlines(startDate: Date = new Date()): PhaseDeadlines {
+  const result: PhaseDeadlines = {};
+  let currentDate = new Date(startDate);
+
+  // 按顺序生成各阶段
+  const phases: PhaseName[] = ['modeling', 'softDecor', 'rendering', 'postProcessing'];
+  
+  phases.forEach((phaseName, index) => {
+    const phaseConfig = PHASE_INFO[phaseName];
+    const days = phaseConfig.defaultDays;
+    const deadline = new Date(currentDate.getTime() + days * 24 * 60 * 60 * 1000);
+
+    result[phaseName] = {
+      startDate: new Date(currentDate),
+      deadline: deadline,
+      estimatedDays: days,
+      status: index === 0 ? 'in_progress' : 'not_started',
+      priority: 'medium'
+    };
+
+    // 下一阶段从当前阶段结束后开始
+    currentDate = new Date(deadline.getTime() + 1);
+  });
+
+  return result;
+}
+
+/**
+ * 工具函数:检查阶段是否延期
+ * @param phase 阶段信息
+ * @returns 是否延期
+ */
+export function isPhaseDelayed(phase: PhaseInfo): boolean {
+  if (phase.status === 'completed') {
+    return false;
+  }
+  
+  const deadline = new Date(phase.deadline);
+  const now = new Date();
+  
+  return now > deadline;
+}
+
+/**
+ * 工具函数:获取阶段剩余天数
+ * @param phase 阶段信息
+ * @returns 剩余天数(负数表示已逾期)
+ */
+export function getPhaseDaysRemaining(phase: PhaseInfo): number {
+  const deadline = new Date(phase.deadline);
+  const now = new Date();
+  const diff = deadline.getTime() - now.getTime();
+  return Math.ceil(diff / (24 * 60 * 60 * 1000));
+}
+
+/**
+ * 工具函数:获取阶段进度百分比
+ * @param phase 阶段信息
+ * @returns 进度百分比 (0-100)
+ */
+export function getPhaseProgress(phase: PhaseInfo): number {
+  if (phase.status === 'completed') {
+    return 100;
+  }
+  
+  if (!phase.startDate || phase.status === 'not_started') {
+    return 0;
+  }
+
+  const start = new Date(phase.startDate).getTime();
+  const deadline = new Date(phase.deadline).getTime();
+  const now = new Date().getTime();
+
+  if (now <= start) {
+    return 0;
+  }
+
+  if (now >= deadline) {
+    return 100;
+  }
+
+  const total = deadline - start;
+  const elapsed = now - start;
+  return Math.round((elapsed / total) * 100);
+}
+

+ 55 - 1
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -649,6 +649,59 @@ export class Dashboard implements OnInit, OnDestroy {
         priority = 'low';
       }
       
+      // 🆕 生成阶段截止时间数据(从交付日期往前推,每个阶段1天)
+      let phaseDeadlines = project.data?.phaseDeadlines;
+      
+      // 如果项目没有阶段数据,动态生成(用于演示效果)
+      if (!phaseDeadlines) {
+        // ✅ 关键修复:从交付日期往前推算各阶段截止时间
+        const deliveryTime = adjustedEndDate.getTime();
+        
+        // 后期截止 = 交付日期
+        const postProcessingDeadline = new Date(deliveryTime);
+        // 渲染截止 = 交付日期 - 1天
+        const renderingDeadline = new Date(deliveryTime - 1 * 24 * 60 * 60 * 1000);
+        // 软装截止 = 交付日期 - 2天
+        const softDecorDeadline = new Date(deliveryTime - 2 * 24 * 60 * 60 * 1000);
+        // 建模截止 = 交付日期 - 3天
+        const modelingDeadline = new Date(deliveryTime - 3 * 24 * 60 * 60 * 1000);
+        
+        phaseDeadlines = {
+          modeling: {
+            startDate: adjustedStartDate,
+            deadline: modelingDeadline,
+            estimatedDays: 1,
+            status: now.getTime() >= modelingDeadline.getTime() && now.getTime() < softDecorDeadline.getTime() ? 'in_progress' : 
+                    now.getTime() >= softDecorDeadline.getTime() ? 'completed' : 'not_started',
+            priority: 'high'
+          },
+          softDecor: {
+            startDate: modelingDeadline,
+            deadline: softDecorDeadline,
+            estimatedDays: 1,
+            status: now.getTime() >= softDecorDeadline.getTime() && now.getTime() < renderingDeadline.getTime() ? 'in_progress' : 
+                    now.getTime() >= renderingDeadline.getTime() ? 'completed' : 'not_started',
+            priority: 'medium'
+          },
+          rendering: {
+            startDate: softDecorDeadline,
+            deadline: renderingDeadline,
+            estimatedDays: 1,
+            status: now.getTime() >= renderingDeadline.getTime() && now.getTime() < postProcessingDeadline.getTime() ? 'in_progress' : 
+                    now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : 'not_started',
+            priority: 'high'
+          },
+          postProcessing: {
+            startDate: renderingDeadline,
+            deadline: postProcessingDeadline,
+            estimatedDays: 1,
+            status: now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : 
+                    now.getTime() >= renderingDeadline.getTime() ? 'in_progress' : 'not_started',
+            priority: 'medium'
+          }
+        };
+      }
+      
       return {
         projectId: project.id || `proj-${Math.random().toString(36).slice(2, 9)}`,
         projectName: project.name || '未命名项目',
@@ -667,7 +720,8 @@ export class Dashboard implements OnInit, OnDestroy {
         urgentCount,
         priority,
         spaceName: project.space || '',
-        customerName: project.customer || ''
+        customerName: project.customer || '',
+        phaseDeadlines: phaseDeadlines // 🆕 阶段截止时间
       };
     });
     

+ 29 - 32
src/app/pages/team-leader/project-timeline/project-timeline.html

@@ -139,17 +139,31 @@
         <!-- 图例说明 -->
         <div class="timeline-legend">
           <div class="legend-item">
-            <span class="legend-icon start-icon"></span>
+            <span class="legend-icon start-icon">▶️</span>
             <span class="legend-label">项目开始</span>
           </div>
           <div class="legend-item">
-            <span class="legend-icon review-icon"></span>
+            <span class="legend-icon review-icon">📋</span>
             <span class="legend-label">对图时间</span>
           </div>
           <div class="legend-item">
-            <span class="legend-icon delivery-icon"></span>
+            <span class="legend-icon delivery-icon">📦</span>
             <span class="legend-label">交付日期</span>
           </div>
+          <div class="legend-separator"></div>
+          <div class="legend-item legend-phase">
+            <span class="legend-label">🎨 建模截止</span>
+          </div>
+          <div class="legend-item legend-phase">
+            <span class="legend-label">🪑 软装截止</span>
+          </div>
+          <div class="legend-item legend-phase">
+            <span class="legend-label">🖼️ 渲染截止</span>
+          </div>
+          <div class="legend-item legend-phase">
+            <span class="legend-label">✨ 后期截止</span>
+          </div>
+          <div class="legend-separator"></div>
           <div class="legend-item">
             <div class="legend-bar-demo legend-bar-green"></div>
             <span class="legend-label">🟢 正常进行(2天+)</span>
@@ -160,14 +174,14 @@
           </div>
           <div class="legend-item">
             <div class="legend-bar-demo legend-bar-orange"></div>
-            <span class="legend-label">🟠 事件当天(2小时+)</span>
+            <span class="legend-label">🟠 事件当天(6小时+)</span>
           </div>
           <div class="legend-item">
             <div class="legend-bar-demo legend-bar-red"></div>
-            <span class="legend-label">🔴 紧急(2小时内)</span>
+            <span class="legend-label">🔴 紧急(6小时内)</span>
           </div>
           <div class="legend-item legend-note">
-            <span class="legend-label">💡 仅显示今日线之后的关键事件</span>
+            <span class="legend-label">💡 仅显示今日线之后的关键事件和阶段截止时间</span>
           </div>
         </div>
         
@@ -244,32 +258,15 @@
                     <div class="progress-fill" [style.width]="project.stageProgress + '%'"></div>
                   </div>
                   
-                  <!-- 关键事件标记(只显示未来的事件)-->
-                  @if (isEventInFuture(project.startDate)) {
-                    <div class="event-marker start"
-                         [style.left]="getEventPosition(project.startDate)"
-                         [style.background]="getEventColor('start', project)"
-                         [title]="'开始:' + formatTime(project.startDate)">
-                      ●
-                    </div>
-                  }
-                  
-                  @if (project.reviewDate && isEventInFuture(project.reviewDate)) {
-                    <div class="event-marker review"
-                         [style.left]="getEventPosition(project.reviewDate)"
-                         [style.background]="getEventColor('review', project)"
-                         [title]="'对图:' + formatTime(project.reviewDate)">
-                      ○
-                    </div>
-                  }
-                  
-                  @if (isEventInFuture(project.deliveryDate)) {
-                    <div class="event-marker delivery"
-                         [style.left]="getEventPosition(project.deliveryDate)"
-                         [style.background]="getEventColor('delivery', project)"
-                         [class.blink]="project.status === 'overdue'"
-                         [title]="'交付:' + formatTime(project.deliveryDate)">
-                      ◆
+                  <!-- 🆕 使用统一的事件标记方法 -->
+                  @for (event of getProjectEvents(project); track event.date) {
+                    <div class="event-marker"
+                         [class]="event.type"
+                         [style.left]="getEventPosition(event.date)"
+                         [style.background]="event.color"
+                         [class.blink]="project.status === 'overdue' && event.type === 'delivery'"
+                         [title]="event.label + ':' + formatTime(event.date) + (event.phase ? ' (' + getPhaseLabel(event.phase) + ')' : '')">
+                      {{ event.icon }}
                     </div>
                   }
                 </div>

+ 115 - 10
src/app/pages/team-leader/project-timeline/project-timeline.ts

@@ -1,6 +1,7 @@
 import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
+import { PhaseDeadlines, PhaseName, PHASE_INFO, isPhaseDelayed } from '../../../models/project-phase.model';
 
 export interface ProjectTimeline {
   projectId: string;
@@ -21,6 +22,18 @@ export interface ProjectTimeline {
   priority: 'low' | 'medium' | 'high' | 'critical';
   spaceName?: string;
   customerName?: string;
+  phaseDeadlines?: PhaseDeadlines; // 🆕 阶段截止时间信息
+}
+
+/** 🆕 时间轴事件 */
+interface TimelineEvent {
+  date: Date;
+  label: string;
+  type: 'start' | 'review' | 'delivery' | 'phase_deadline';
+  phase?: PhaseName;
+  projectId: string;
+  color: string;
+  icon: string;
 }
 
 interface DesignerInfo {
@@ -173,10 +186,10 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   /**
    * 🆕 根据时间紧急度获取项目条颜色
    * 规则:
-   * - 正常进行(距离最近事件2天+):绿色
-   * - 临近事件前一天:黄色
-   * - 事件当天(未到时间):橙色
-   * - 事件当天(已过时间):红色
+   * - 正常进行(距离最近事件1天+):绿色
+   * - 临近事件前一天(24小时内):黄色
+   * - 事件当天(6小时以上):橙色
+   * - 紧急情况(6小时内):红色
    */
   getProjectUrgencyColor(project: ProjectTimeline): string {
     const now = this.currentTime.getTime();
@@ -216,22 +229,22 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     let color = '';
     let colorName = '';
     
-    // 🔴 红色:事件当天且已过事件时间(或距离事件时间不到2小时
-    if (isSameDay && hoursDiff < 2) {
+    // 🔴 红色:距离事件时间不到6小时(紧急
+    if (hoursDiff < 6) {
       color = 'linear-gradient(135deg, #FCA5A5 0%, #EF4444 100%)';
-      colorName = '🔴 红色(紧急)';
+      colorName = '🔴 红色(紧急 - 6小时内)';
     }
-    // 🟠 橙色:事件当天但还有2小时以上
+    // 🟠 橙色:事件当天但还有6小时以上
     else if (isSameDay) {
       color = 'linear-gradient(135deg, #FCD34D 0%, #F59E0B 100%)';
-      colorName = '🟠 橙色(当天)';
+      colorName = '🟠 橙色(当天 - 6小时+)';
     }
     // 🟡 黄色:距离事件前一天(24小时内但不是当天)
     else if (hoursDiff < 24) {
       color = 'linear-gradient(135deg, #FEF08A 0%, #EAB308 100%)';
       colorName = '🟡 黄色(前一天)';
     }
-    // 🟢 绿色:正常进行(距离事件2天+)
+    // 🟢 绿色:正常进行(距离事件1天+)
     else {
       color = 'linear-gradient(135deg, #86EFAC 0%, #4ADE80 100%)';
       colorName = '🟢 绿色(正常)';
@@ -298,6 +311,98 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     // 必须同时满足:在时间范围内 + 在当前时间之后
     return this.isEventInRange(date) && date.getTime() >= this.currentTime.getTime();
   }
+  
+  /**
+   * 🆕 获取项目的所有时间轴事件(含阶段截止时间)
+   */
+  getProjectEvents(project: ProjectTimeline): TimelineEvent[] {
+    const events: TimelineEvent[] = [];
+    
+    // 项目开始事件
+    if (this.isEventInFuture(project.startDate)) {
+      events.push({
+        date: project.startDate,
+        label: '开始',
+        type: 'start',
+        projectId: project.projectId,
+        color: '#10b981',
+        icon: '▶️'
+      });
+    }
+    
+    // 评审事件
+    if (project.reviewDate && this.isEventInFuture(project.reviewDate)) {
+      events.push({
+        date: project.reviewDate,
+        label: '评审',
+        type: 'review',
+        projectId: project.projectId,
+        color: '#3b82f6',
+        icon: '📋'
+      });
+    }
+    
+    // 交付事件
+    if (project.deliveryDate && this.isEventInFuture(project.deliveryDate)) {
+      events.push({
+        date: project.deliveryDate,
+        label: '交付',
+        type: 'delivery',
+        projectId: project.projectId,
+        color: this.getEventColor('delivery', project),
+        icon: '📦'
+      });
+    }
+    
+    // 🆕 阶段截止事件
+    if (project.phaseDeadlines) {
+      Object.entries(project.phaseDeadlines).forEach(([phaseName, phaseInfo]) => {
+        if (phaseInfo && phaseInfo.deadline) {
+          const deadline = new Date(phaseInfo.deadline);
+          
+          // 只显示未来的阶段截止事件
+          if (this.isEventInFuture(deadline)) {
+            const phaseConfig = PHASE_INFO[phaseName as PhaseName];
+            const isDelayed = isPhaseDelayed(phaseInfo);
+            
+            events.push({
+              date: deadline,
+              label: `${phaseConfig.label}截止`,
+              type: 'phase_deadline',
+              phase: phaseName as PhaseName,
+              projectId: project.projectId,
+              color: isDelayed ? '#dc2626' : phaseConfig.color,
+              icon: phaseConfig.icon
+            });
+          }
+        }
+      });
+    }
+    
+    // 按时间排序
+    return events.sort((a, b) => a.date.getTime() - b.date.getTime());
+  }
+  
+  /**
+   * 🆕 获取阶段信息(用于工具提示)
+   */
+  getPhaseLabel(phaseName: PhaseName): string {
+    return PHASE_INFO[phaseName]?.label || phaseName;
+  }
+  
+  /**
+   * 🆕 获取阶段图标
+   */
+  getPhaseIcon(phaseName: PhaseName): string {
+    return PHASE_INFO[phaseName]?.icon || '📌';
+  }
+  
+  /**
+   * 🆕 获取阶段颜色
+   */
+  getPhaseColor(phaseName: PhaseName): string {
+    return PHASE_INFO[phaseName]?.color || '#6b7280';
+  }
 
   private buildDesignerStats(): DesignerInfo[] {
     const designerMap = new Map<string, DesignerInfo>();