فهرست منبع

refactor: replace Gantt chart with Project Timeline component in dashboard

- Removed the Gantt chart section and integrated the new Project Timeline component for improved project visualization.
- Updated the dashboard to enhance user interaction with project timelines and streamline the display of project data.
0235711 10 ماه پیش
والد
کامیت
5067730328
2فایلهای تغییر یافته به همراه585 افزوده شده و 39 حذف شده
  1. 579 0
      docs/schema/project-phase-deadlines-design.md
  2. 6 39
      src/app/pages/team-leader/dashboard/dashboard.html

+ 579 - 0
docs/schema/project-phase-deadlines-design.md

@@ -0,0 +1,579 @@
+# Project表阶段截止时间字段设计方案
+
+## 📋 需求背景
+
+为了支持项目负载时间轴的阶段查看功能,需要在Project表中添加各个设计阶段的截止时间信息,包括:
+- 建模阶段
+- 软装阶段  
+- 渲染阶段
+- 后期阶段
+
+根据schemas.md说明,这些信息应存储在`Project.data`字段(Object类型)中,可以自由扩展子级属性。
+
+---
+
+## 🎯 数据结构设计
+
+### 1. Project.data.phaseDeadlines 字段结构
+
+```typescript
+interface PhaseDeadlines {
+  modeling?: PhaseInfo;      // 建模阶段
+  softDecor?: PhaseInfo;     // 软装阶段
+  rendering?: PhaseInfo;     // 渲染阶段
+  postProcessing?: PhaseInfo; // 后期阶段
+}
+
+interface PhaseInfo {
+  startDate?: Date;          // 阶段开始时间
+  deadline: Date;            // 阶段截止时间
+  estimatedDays?: number;    // 预计工期(天数)
+  status?: 'not_started' | 'in_progress' | 'completed' | 'delayed'; // 阶段状态
+  completedAt?: Date;        // 实际完成时间
+  assignee?: Pointer<Profile>; // 负责人
+  priority?: 'low' | 'medium' | 'high' | 'urgent'; // 优先级
+  notes?: string;            // 备注信息
+}
+```
+
+---
+
+## 📊 JSON示例
+
+### 完整示例(所有阶段)
+
+```json
+{
+  "phaseDeadlines": {
+    "modeling": {
+      "startDate": "2024-12-01T00:00:00.000Z",
+      "deadline": "2024-12-08T23:59:59.999Z",
+      "estimatedDays": 7,
+      "status": "completed",
+      "completedAt": "2024-12-07T18:30:00.000Z",
+      "assignee": {
+        "__type": "Pointer",
+        "className": "Profile",
+        "objectId": "prof001"
+      },
+      "priority": "high",
+      "notes": "客户要求加急,优先处理"
+    },
+    "softDecor": {
+      "startDate": "2024-12-09T00:00:00.000Z",
+      "deadline": "2024-12-13T23:59:59.999Z",
+      "estimatedDays": 4,
+      "status": "in_progress",
+      "assignee": {
+        "__type": "Pointer",
+        "className": "Profile",
+        "objectId": "prof002"
+      },
+      "priority": "medium"
+    },
+    "rendering": {
+      "startDate": "2024-12-14T00:00:00.000Z",
+      "deadline": "2024-12-20T23:59:59.999Z",
+      "estimatedDays": 6,
+      "status": "not_started",
+      "priority": "high",
+      "notes": "需要高质量渲染,预留充足时间"
+    },
+    "postProcessing": {
+      "startDate": "2024-12-21T00:00:00.000Z",
+      "deadline": "2024-12-24T23:59:59.999Z",
+      "estimatedDays": 3,
+      "status": "not_started",
+      "priority": "medium",
+      "notes": "后期处理与润色"
+    }
+  }
+}
+```
+
+### 简化示例(仅关键字段)
+
+```json
+{
+  "phaseDeadlines": {
+    "modeling": {
+      "deadline": "2024-12-08T23:59:59.999Z",
+      "status": "in_progress"
+    },
+    "softDecor": {
+      "deadline": "2024-12-13T23:59:59.999Z",
+      "status": "not_started"
+    },
+    "rendering": {
+      "deadline": "2024-12-20T23:59:59.999Z",
+      "status": "not_started"
+    },
+    "postProcessing": {
+      "deadline": "2024-12-24T23:59:59.999Z",
+      "status": "not_started"
+    }
+  }
+}
+```
+
+---
+
+## 💾 Parse Cloud Code 使用示例
+
+### 1. 创建项目时设置阶段截止时间
+
+```javascript
+Parse.Cloud.define("createProjectWithPhases", async (request) => {
+  const { projectTitle, contactId, companyId, phaseConfig } = request.params;
+  
+  const Project = Parse.Object.extend("Project");
+  const project = new Project();
+  
+  project.set("title", projectTitle);
+  project.set("contact", { __type: "Pointer", className: "ContactInfo", objectId: contactId });
+  project.set("company", { __type: "Pointer", className: "Company", objectId: companyId });
+  project.set("status", "进行中");
+  project.set("currentStage", "建模");
+  
+  // 设置阶段截止时间
+  const baseDate = new Date();
+  project.set("data", {
+    phaseDeadlines: {
+      modeling: {
+        startDate: new Date(baseDate),
+        deadline: new Date(baseDate.getTime() + 7 * 24 * 60 * 60 * 1000), // 7天后
+        estimatedDays: 7,
+        status: "in_progress",
+        priority: "high"
+      },
+      softDecor: {
+        startDate: new Date(baseDate.getTime() + 7 * 24 * 60 * 60 * 1000),
+        deadline: new Date(baseDate.getTime() + 11 * 24 * 60 * 60 * 1000), // 11天后
+        estimatedDays: 4,
+        status: "not_started",
+        priority: "medium"
+      },
+      rendering: {
+        startDate: new Date(baseDate.getTime() + 11 * 24 * 60 * 60 * 1000),
+        deadline: new Date(baseDate.getTime() + 17 * 24 * 60 * 60 * 1000), // 17天后
+        estimatedDays: 6,
+        status: "not_started",
+        priority: "high"
+      },
+      postProcessing: {
+        startDate: new Date(baseDate.getTime() + 17 * 24 * 60 * 60 * 1000),
+        deadline: new Date(baseDate.getTime() + 20 * 24 * 60 * 60 * 1000), // 20天后
+        estimatedDays: 3,
+        status: "not_started",
+        priority: "medium"
+      }
+    }
+  });
+  
+  await project.save(null, { useMasterKey: true });
+  return project;
+});
+```
+
+### 2. 更新阶段状态
+
+```javascript
+Parse.Cloud.define("updatePhaseStatus", async (request) => {
+  const { projectId, phaseName, status, completedAt } = request.params;
+  
+  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(`Phase ${phaseName} not found`);
+  }
+  
+  phaseDeadlines[phaseName].status = status;
+  if (status === "completed") {
+    phaseDeadlines[phaseName].completedAt = completedAt || new Date();
+  }
+  
+  data.phaseDeadlines = phaseDeadlines;
+  project.set("data", data);
+  
+  await project.save(null, { useMasterKey: true });
+  return project;
+});
+```
+
+### 3. 查询特定阶段的项目
+
+```javascript
+Parse.Cloud.define("getProjectsByPhaseStatus", async (request) => {
+  const { companyId, phaseName, status } = request.params;
+  
+  const projectQuery = new Parse.Query("Project");
+  projectQuery.equalTo("company", { __type: "Pointer", className: "Company", objectId: companyId });
+  projectQuery.exists(`data.phaseDeadlines.${phaseName}`);
+  
+  const projects = await projectQuery.find({ useMasterKey: true });
+  
+  // 前端过滤(Parse Query不支持深层嵌套查询)
+  return projects.filter(project => {
+    const data = project.get("data");
+    return data?.phaseDeadlines?.[phaseName]?.status === status;
+  });
+});
+```
+
+---
+
+## 🎨 前端使用示例(Angular/TypeScript)
+
+### 1. 类型定义
+
+```typescript
+// src/app/models/project-phase.model.ts
+export interface PhaseInfo {
+  startDate?: Date;
+  deadline: Date;
+  estimatedDays?: number;
+  status?: 'not_started' | 'in_progress' | 'completed' | 'delayed';
+  completedAt?: Date;
+  assignee?: {
+    __type: 'Pointer';
+    className: 'Profile';
+    objectId: string;
+  };
+  priority?: 'low' | 'medium' | 'high' | 'urgent';
+  notes?: string;
+}
+
+export interface PhaseDeadlines {
+  modeling?: PhaseInfo;
+  softDecor?: PhaseInfo;
+  rendering?: PhaseInfo;
+  postProcessing?: PhaseInfo;
+}
+
+export interface ProjectData {
+  phaseDeadlines?: PhaseDeadlines;
+  // ... 其他data字段
+}
+```
+
+### 2. 获取阶段信息
+
+```typescript
+// src/app/services/project.service.ts
+getProjectPhaseDeadlines(projectId: string): Observable<PhaseDeadlines | null> {
+  const query = new Parse.Query('Project');
+  return from(query.get(projectId)).pipe(
+    map(project => {
+      const data = project.get('data') as ProjectData;
+      return data?.phaseDeadlines || null;
+    })
+  );
+}
+```
+
+### 3. 项目时间轴组件中使用
+
+```typescript
+// src/app/pages/team-leader/project-timeline/project-timeline.component.ts
+interface TimelineEvent {
+  date: Date;
+  label: string;
+  type: 'start' | 'milestone' | 'deadline';
+  phase: string;
+  status?: string;
+}
+
+generateTimelineEvents(project: any): TimelineEvent[] {
+  const events: TimelineEvent[] = [];
+  const phaseDeadlines = project.get('data')?.phaseDeadlines;
+  
+  if (!phaseDeadlines) return events;
+  
+  const phaseLabels = {
+    modeling: '建模',
+    softDecor: '软装',
+    rendering: '渲染',
+    postProcessing: '后期'
+  };
+  
+  // 为每个阶段生成事件
+  Object.entries(phaseDeadlines).forEach(([phaseName, phaseInfo]: [string, any]) => {
+    // 开始事件
+    if (phaseInfo.startDate) {
+      events.push({
+        date: new Date(phaseInfo.startDate),
+        label: `${phaseLabels[phaseName as keyof typeof phaseLabels]}开始`,
+        type: 'start',
+        phase: phaseName,
+        status: phaseInfo.status
+      });
+    }
+    
+    // 截止事件
+    if (phaseInfo.deadline) {
+      events.push({
+        date: new Date(phaseInfo.deadline),
+        label: `${phaseLabels[phaseName as keyof typeof phaseLabels]}截止`,
+        type: 'deadline',
+        phase: phaseName,
+        status: phaseInfo.status
+      });
+    }
+    
+    // 完成事件
+    if (phaseInfo.completedAt) {
+      events.push({
+        date: new Date(phaseInfo.completedAt),
+        label: `${phaseLabels[phaseName as keyof typeof phaseLabels]}完成`,
+        type: 'milestone',
+        phase: phaseName,
+        status: 'completed'
+      });
+    }
+  });
+  
+  // 按时间排序
+  return events.sort((a, b) => a.date.getTime() - b.date.getTime());
+}
+```
+
+---
+
+## 📈 时间轴可视化建议
+
+### 阶段颜色映射
+
+```typescript
+const phaseColors = {
+  modeling: {
+    bg: '#E3F2FD',      // 浅蓝色
+    border: '#2196F3',  // 蓝色
+    label: '建模'
+  },
+  softDecor: {
+    bg: '#F3E5F5',      // 浅紫色
+    border: '#9C27B0',  // 紫色
+    label: '软装'
+  },
+  rendering: {
+    bg: '#FFF3E0',      // 浅橙色
+    border: '#FF9800',  // 橙色
+    label: '渲染'
+  },
+  postProcessing: {
+    bg: '#E8F5E9',      // 浅绿色
+    border: '#4CAF50',  // 绿色
+    label: '后期'
+  }
+};
+```
+
+### 状态图标映射
+
+```typescript
+const statusIcons = {
+  not_started: '⏸️',
+  in_progress: '▶️',
+  completed: '✅',
+  delayed: '⚠️'
+};
+```
+
+---
+
+## 🔄 数据迁移建议
+
+### 为现有项目添加阶段截止时间
+
+```javascript
+Parse.Cloud.job("migrateProjectPhaseDeadlines", async (request) => {
+  const { params, message } = request;
+  
+  const query = new Parse.Query("Project");
+  query.notEqualTo("isDeleted", true);
+  query.limit(1000);
+  
+  let count = 0;
+  const projects = await query.find({ useMasterKey: true });
+  
+  for (const project of projects) {
+    const data = project.get("data") || {};
+    
+    // 如果已有phaseDeadlines,跳过
+    if (data.phaseDeadlines) continue;
+    
+    // 根据项目deadline推算各阶段截止时间
+    const deadline = project.get("deadline");
+    if (!deadline) continue;
+    
+    const deadlineTime = deadline.getTime();
+    const modelingDeadline = new Date(deadlineTime - 13 * 24 * 60 * 60 * 1000); // 提前13天
+    const softDecorDeadline = new Date(deadlineTime - 9 * 24 * 60 * 60 * 1000);  // 提前9天
+    const renderingDeadline = new Date(deadlineTime - 3 * 24 * 60 * 60 * 1000);  // 提前3天
+    const postProcessingDeadline = deadline;
+    
+    data.phaseDeadlines = {
+      modeling: {
+        deadline: modelingDeadline,
+        estimatedDays: 7,
+        status: "not_started"
+      },
+      softDecor: {
+        deadline: softDecorDeadline,
+        estimatedDays: 4,
+        status: "not_started"
+      },
+      rendering: {
+        deadline: renderingDeadline,
+        estimatedDays: 6,
+        status: "not_started"
+      },
+      postProcessing: {
+        deadline: postProcessingDeadline,
+        estimatedDays: 3,
+        status: "not_started"
+      }
+    };
+    
+    project.set("data", data);
+    await project.save(null, { useMasterKey: true });
+    count++;
+    
+    message(`Migrated ${count} projects`);
+  }
+  
+  message(`Migration completed: ${count} projects updated`);
+});
+```
+
+---
+
+## ⚙️ 配置建议
+
+### 默认工期配置
+
+建议在Company.data中添加默认工期配置:
+
+```json
+{
+  "phaseDefaultDurations": {
+    "modeling": 7,        // 建模默认7天
+    "softDecor": 4,       // 软装默认4天
+    "rendering": 6,       // 渲染默认6天
+    "postProcessing": 3   // 后期默认3天
+  }
+}
+```
+
+### 使用配置创建项目
+
+```typescript
+async createProjectWithDefaultPhases(
+  projectData: any, 
+  companyId: string
+): Promise<Parse.Object> {
+  // 获取公司配置
+  const companyQuery = new Parse.Query('Company');
+  const company = await companyQuery.get(companyId);
+  const companyData = company.get('data') || {};
+  const durations = companyData.phaseDefaultDurations || {
+    modeling: 7,
+    softDecor: 4,
+    rendering: 6,
+    postProcessing: 3
+  };
+  
+  // 计算各阶段截止时间
+  const startDate = new Date();
+  const phaseDeadlines: any = {};
+  let currentDate = new Date(startDate);
+  
+  ['modeling', 'softDecor', 'rendering', 'postProcessing'].forEach(phase => {
+    const days = durations[phase];
+    const deadline = new Date(currentDate.getTime() + days * 24 * 60 * 60 * 1000);
+    
+    phaseDeadlines[phase] = {
+      startDate: new Date(currentDate),
+      deadline: deadline,
+      estimatedDays: days,
+      status: phase === 'modeling' ? 'in_progress' : 'not_started',
+      priority: 'medium'
+    };
+    
+    currentDate = new Date(deadline.getTime() + 1); // 下一阶段从前一阶段结束后开始
+  });
+  
+  // 创建项目
+  const Project = Parse.Object.extend('Project');
+  const project = new Project();
+  project.set('data', { phaseDeadlines, ...projectData.data });
+  // ... 设置其他字段
+  
+  await project.save(null, { useMasterKey: true });
+  return project;
+}
+```
+
+---
+
+## 📝 注意事项
+
+### 1. 数据验证
+- 确保`deadline`字段为有效的Date对象
+- 确保阶段顺序逻辑正确(前一阶段结束 < 下一阶段开始)
+- 状态枚举值必须在允许范围内
+
+### 2. 性能优化
+- Parse Query不支持深层嵌套查询,需要在前端过滤
+- 大量项目查询时建议使用Cloud Code
+- 考虑添加索引优化查询性能
+
+### 3. 兼容性
+- 保持向后兼容,旧项目可能没有`phaseDeadlines`字段
+- 代码中需要做空值检查:`project.get('data')?.phaseDeadlines`
+
+### 4. 扩展性
+- 可以根据项目类型(软装/硬装)调整默认工期
+- 可以添加更多阶段(如量房、方案设计等)
+- 可以为每个阶段添加子任务
+
+---
+
+## 🎯 实施步骤
+
+### 第一步:更新schemas.md文档
+在Project表的data字段说明中添加phaseDeadlines结构说明
+
+### 第二步:编写数据迁移脚本
+为现有项目添加默认的阶段截止时间
+
+### 第三步:更新前端类型定义
+添加PhaseInfo和PhaseDeadlines接口定义
+
+### 第四步:修改项目创建逻辑
+在创建项目时自动生成阶段截止时间
+
+### 第五步:更新时间轴组件
+读取并展示阶段截止时间信息
+
+### 第六步:添加阶段管理界面
+允许手动调整各阶段的截止时间和状态
+
+---
+
+## ✅ 总结
+
+通过在`Project.data.phaseDeadlines`中存储各阶段截止时间信息:
+
+1. ✅ **灵活性**:Object类型允许自由扩展,易于添加新阶段
+2. ✅ **清晰性**:每个阶段的信息结构一致,便于维护
+3. ✅ **可视化**:为项目负载时间轴提供完整的阶段数据支持
+4. ✅ **兼容性**:不影响现有表结构,易于向后兼容
+5. ✅ **扩展性**:支持添加负责人、优先级、备注等更多信息
+
+建议按照本方案实施,可以有效支持项目负载图的阶段查看功能。
+

+ 6 - 39
src/app/pages/team-leader/dashboard/dashboard.html

@@ -98,46 +98,13 @@
       </div>
       <div class="gantt-container" #workloadGanttContainer></div>
     </div>
+    <!-- 项目负载时间轴(切换视图时显示) -->
     @if (showGanttView) {
-      <div class="gantt-card">
-        <div class="gantt-header">
-          <div class="title">项目负载时间轴</div>
-          <div class="hint">
-            👤 设计师按负载由高到低排列 | 🎨 每个条形代表一个项目 | 🔴超期 🟠临期 🟢正常
-          </div>
-          <div class="scale-switch">
-            <button [class.active]="ganttScale === 'week'" (click)="setGanttScale('week')">周</button>
-            <button [class.active]="ganttScale === 'month'" (click)="setGanttScale('month')">月</button>
-          </div>
-          <div class="search-box">
-            <input type="search" placeholder="搜索项目/设计师/风格关键词" [(ngModel)]="searchTerm" (input)="onSearchChange()" (focus)="onSearchFocus()" (blur)="onSearchBlur()" />
-            @if (showSuggestions) {
-              <div class="suggestion-panel">
-                @if (searchSuggestions.length > 0) {
-                  <ul>
-                    @for (suggest of searchSuggestions; track suggest.id) {
-                      <li (mousedown)="selectSuggestion(suggest)">
-                        <div class="line-1">
-                          <span class="name">{{ suggest.name }}</span>
-                          <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
-                          <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
-                        </div>
-                        <div class="line-2">
-                          <span class="designer">{{ suggest.designerName || '未分配' }}</span>
-                          <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
-                        </div>
-                      </li>
-                    }
-                  </ul>
-                } @else {
-                  <div class="empty">抱歉,没有检索到哦</div>
-                }
-              </div>
-            }
-          </div>
-        </div>
-        <div #ganttChartRef class="gantt-chart"></div>
-      </div>
+      <app-project-timeline 
+        [projects]="projectTimelineData"
+        [companyId]="currentUser.name"
+        (projectClick)="onProjectTimelineClick($event)">
+      </app-project-timeline>
     }
 
     @if (!showGanttView) {