Jelajahi Sumber

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

徐福静0235668 1 Minggu lalu
induk
melakukan
aec802f8c1
20 mengubah file dengan 4018 tambahan dan 339 penghapusan
  1. 3 3
      src/app/pages/designer/dashboard/dashboard.html
  2. 244 93
      src/app/pages/team-leader/dashboard/dashboard.ts
  3. 75 3
      src/app/pages/team-leader/project-timeline/project-progress-modal.html
  4. 291 0
      src/app/pages/team-leader/project-timeline/project-progress-modal.scss
  5. 136 0
      src/app/pages/team-leader/project-timeline/project-progress-modal.ts
  6. 16 19
      src/app/pages/team-leader/project-timeline/project-timeline.html
  7. 71 61
      src/app/pages/team-leader/project-timeline/project-timeline.scss
  8. 141 131
      src/app/pages/team-leader/project-timeline/project-timeline.ts
  9. 1 0
      src/modules/project/components/project-bottom-card/project-bottom-card.component.html
  10. 10 0
      src/modules/project/components/project-progress-modal/project-progress-modal.component.html
  11. 32 0
      src/modules/project/components/project-progress-modal/project-progress-modal.component.scss
  12. 20 1
      src/modules/project/components/project-progress-modal/project-progress-modal.component.ts
  13. 214 28
      src/modules/project/services/project-space-deliverable.service.ts
  14. 392 0
      未完成空间列表显示设计师名字优化总结.md
  15. 222 0
      组长端项目负载时间轴字段应用总结.md
  16. 576 0
      组长端项目负载时间轴字段需求方案.md
  17. 298 0
      项目进度弹窗修复总结.md
  18. 279 0
      项目进度详情组件字段应用总结.md
  19. 637 0
      项目进度详情组件字段需求方案.md
  20. 360 0
      项目进度详情组件弹窗优化总结.md

+ 3 - 3
src/app/pages/designer/dashboard/dashboard.html

@@ -58,12 +58,12 @@
   <!-- 主要内容区域 - 工作台 -->
   <div *ngIf="activeDashboard === 'main'" class="dashboard-main">
 
-    <!-- 视图切换按钮 -->
-    <div class="view-toggle">
+    <!-- 视图切换按钮 - 已隐藏 -->
+    <!-- <div class="view-toggle">
       <button class="toggle-btn" (click)="toggleView()">
         {{ viewMode === 'card' ? '切换为列表' : '切换为卡片' }}
       </button>
-    </div>
+    </div> -->
 
     <!-- 卡片视图 -->
     @if (viewMode === 'card') {

+ 244 - 93
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -415,6 +415,9 @@ export class Dashboard implements OnInit, OnDestroy {
           finalCreatedAt = project.updatedAt; // Parse 内置属性
         }
         
+        // ✅ 应用方案:获取项目的 data 字段(包含 phaseDeadlines, deliveryStageStatus 等)
+        const projectDataField = project.get('data') || {};
+        
         const projectData = {
           id: project.id,
           name: project.get('title') || '未命名项目',
@@ -423,7 +426,12 @@ export class Dashboard implements OnInit, OnDestroy {
           deadline: deadlineValue || deliveryDateValue || expectedDeliveryDateValue,
           demoday: demodayValue, // 🆕 小图对图日期
           createdAt: finalCreatedAt,
-          designerName: profileName
+          updatedAt: updatedAtValue || project.updatedAt, // ✅ 添加 updatedAt
+          designerName: profileName,
+          designerId: profileId, // ✅ 添加 designerId
+          data: projectDataField, // ✅ 添加 data 字段
+          contact: project.get('contact'), // ✅ 添加客户信息
+          space: projectDataField.quotation?.spaces?.[0]?.name || '' // ✅ 添加空间信息
         };
         
         // 添加到映射 (by ID)
@@ -499,6 +507,9 @@ export class Dashboard implements OnInit, OnDestroy {
           finalCreatedAt = project.updatedAt;
         }
         
+        // ✅ 应用方案:获取项目的 data 字段(包含 phaseDeadlines, deliveryStageStatus 等)
+        const projectDataField = project.get('data') || {};
+        
         const projectData = {
           id: project.id,
           name: project.get('title') || '未命名项目',
@@ -507,7 +518,12 @@ export class Dashboard implements OnInit, OnDestroy {
           deadline: deadlineValue || deliveryDateValue || expectedDeliveryDateValue,
           demoday: demodayValue, // 🆕 小图对图日期
           createdAt: finalCreatedAt,
-          designerName: assigneeName
+          updatedAt: updatedAtValue || project.updatedAt, // ✅ 添加 updatedAt
+          designerName: assigneeName,
+          designerId: assignee.id, // ✅ 添加 designerId
+          data: projectDataField, // ✅ 添加 data 字段
+          contact: project.get('contact'), // ✅ 添加客户信息
+          space: projectDataField.quotation?.spaces?.[0]?.name || '' // ✅ 添加空间信息
         };
         
         // 添加到映射
@@ -517,6 +533,10 @@ export class Dashboard implements OnInit, OnDestroy {
         this.designerWorkloadMap.get(assigneeName)!.push(projectData);
       });
       
+      // ✅ 修复:加载完数据后,转换为时间轴格式
+      console.log(`📊 [降级方案] 加载了 ${projects.length} 个项目,填充到 ${this.designerWorkloadMap.size} 个设计师的工作量映射`);
+      this.convertToProjectTimeline();
+      
     } catch (error) {
       console.error('[降级方案] 加载工作量失败:', error);
     }
@@ -555,8 +575,11 @@ export class Dashboard implements OnInit, OnDestroy {
       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;
     }
@@ -566,10 +589,13 @@ export class Dashboard implements OnInit, OnDestroy {
     
     // 统计项目数量
     let totalProjectsInMap = 0;
-    this.designerWorkloadMap.forEach((projects) => {
+    this.designerWorkloadMap.forEach((projects, key) => {
       totalProjectsInMap += projects.length;
+      console.log(`📊 设计师 "${key}": ${projects.length} 个项目`);
     });
     
+    console.log(`📊 总计 ${totalProjectsInMap} 个项目分布在 ${this.designerWorkloadMap.size} 个设计师中`);
+    
     this.designerWorkloadMap.forEach((projects, designerKey) => {
       // 🔧 改进判断逻辑:跳过明显的 ID 格式(Parse objectId 是10位字母数字组合)
       // 只要包含中文字符,就认为是设计师名称
@@ -591,58 +617,72 @@ export class Dashboard implements OnInit, OnDestroy {
       }
     });
     
+    console.log(`📊 开始转换 ${allDesignerProjects.length} 个项目为时间轴格式`);
+    
     this.projectTimelineData = allDesignerProjects.map((project, index) => {
       const now = new Date();
-      const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
       
-      // 🎨 测试专用:精心设计项目分布,展示不同负载状态
-      // 目标效果:
-      // 第1天(今天)=1项目(忙碌🔵), 第2天(明天)=0项目(空闲🟢), 第3天=3项目(超负荷🔴), 
-      // 第4天=2项目(忙碌🔵), 第5天=1项目(忙碌🔵), 第6天=4项目(超负荷🔴), 第7天=2项目(忙碌🔵)
+      // ✅ 应用方案:使用真实字段数据
+      const projectData = project.data || {};
       
-      // 使用项目索引映射到具体天数,跳过第2天以实现0项目效果
-      const dayMapping = [
-        1,    // 项目0 → 第1天
-        3, 3, 3,  // 项目1,2,3 → 第3天(3个项目,超负荷)
-        4, 4,     // 项目4,5 → 第4天(2个项目)
-        5,        // 项目6 → 第5天(1个项目)
-        6, 6, 6, 6, // 项目7,8,9,10 → 第6天(4个项目,超负荷)
-        7, 7      // 项目11,12 → 第7天(2个项目)
-      ];
-      
-      let dayOffset: number;
-      
-      if (index < dayMapping.length) {
-        dayOffset = dayMapping[index];
+      // 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 {
-        // 超出13个项目后,分配到后续天数
-        dayOffset = 7 + ((index - dayMapping.length) % 7) + 1;
+        // 降级:如果没有开始时间,使用当前时间
+        realStartDate = new Date();
       }
       
-      const adjustedEndDate = new Date(today.getTime() + dayOffset * 24 * 60 * 60 * 1000);
+      // 2. 获取真实的交付日期
+      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);
+      }
       
-      // 项目开始时间:交付前3-7天
-      const projectDuration = 3 + (index % 5); // 3-7天的项目周期
-      const adjustedStartDate = new Date(adjustedEndDate.getTime() - projectDuration * 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')
+        });
+      }
       
-      // 🆕 小图对图时间:设置在软装和渲染之间,便于展示
-      let adjustedReviewDate: Date;
-      if (project.demoday && project.demoday instanceof Date) {
-        // 使用真实的小图对图日期
-        adjustedReviewDate = project.demoday;
+      // 3. 获取真实的对图时间
+      // 优先使用 demoday,其次使用 phaseDeadlines.softDecor.deadline,最后计算
+      let realReviewDate: Date;
+      if (project.demoday) {
+        realReviewDate = project.demoday instanceof Date ? project.demoday : new Date(project.demoday);
+      } else if (projectData.phaseDeadlines?.softDecor?.deadline) {
+        const softDecorDeadline = projectData.phaseDeadlines.softDecor.deadline;
+        realReviewDate = softDecorDeadline instanceof Date ? softDecorDeadline : new Date(softDecorDeadline);
       } else {
-        // 🔥 修改为便于展示:设置在项目时间轴的中间位置(软装完成后)
-        // 计算项目周期的 60% 位置(软装后、渲染前)
-        const projectMidPoint = adjustedStartDate.getTime() + (projectDuration * 0.6 * 24 * 60 * 60 * 1000);
-        adjustedReviewDate = new Date(projectMidPoint);
-        // 设置具体时间为下午2点
-        adjustedReviewDate.setHours(14, 0, 0, 0);
+        // 计算:设置在软装和渲染之间(项目周期的 60% 位置)
+        const projectDuration = realEndDate.getTime() - realStartDate.getTime();
+        const projectMidPoint = realStartDate.getTime() + (projectDuration * 0.6);
+        realReviewDate = new Date(projectMidPoint);
+        realReviewDate.setHours(14, 0, 0, 0);
       }
       
-      // 计算距离交付还有几天
-      const daysUntilDeadline = Math.ceil((adjustedEndDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
+      // 4. 计算距离交付还有几天(使用真实日期)
+      const daysUntilDeadline = Math.ceil((realEndDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
       
-      // 计算项目状态
+      // 5. 计算项目状态
       let status: 'normal' | 'warning' | 'urgent' | 'overdue' = 'normal';
       if (daysUntilDeadline < 0) {
         status = 'overdue';
@@ -652,7 +692,7 @@ export class Dashboard implements OnInit, OnDestroy {
         status = 'warning';
       }
       
-      // 映射阶段
+      // 6. 映射阶段
       const stageMap: Record<string, 'plan' | 'model' | 'decoration' | 'render' | 'delivery'> = {
         '方案设计': 'plan',
         '方案规划': 'plan',
@@ -669,19 +709,75 @@ export class Dashboard implements OnInit, OnDestroy {
       const currentStage = stageMap[project.currentStage || '建模阶段'] || 'model';
       const stageName = project.currentStage || '建模阶段';
       
-      // 计算阶段进度
-      const totalDuration = adjustedEndDate.getTime() - adjustedStartDate.getTime();
-      const elapsed = now.getTime() - adjustedStartDate.getTime();
-      const stageProgress = totalDuration > 0 ? Math.min(100, Math.max(0, (elapsed / totalDuration) * 100)) : 50;
+      // 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;
+      }
       
-      // 检查是否停滞
-      const isStalled = false; // 调整后的项目都是进行中
-      const stalledDays = 0;
+      // 8. 检查是否停滞(基于 updatedAt)
+      let isStalled = false;
+      let stalledDays = 0;
+      if (project.updatedAt) {
+        const updatedAt = project.updatedAt instanceof Date ? project.updatedAt : new Date(project.updatedAt);
+        const daysSinceUpdate = Math.floor((now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24));
+        // 如果超过7天未更新,认为停滞
+        isStalled = daysSinceUpdate > 7;
+        stalledDays = isStalled ? daysSinceUpdate : 0;
+      }
       
-      // 催办次数
+      // 9. 催办次数(基于状态和历史记录)
       const urgentCount = status === 'overdue' ? 2 : status === 'urgent' ? 1 : 0;
       
-      // 优先级
+      // 10. 优先级
       let priority: 'low' | 'medium' | 'high' | 'critical' = 'medium';
       if (status === 'overdue') {
         priority = 'critical';
@@ -693,57 +789,72 @@ export class Dashboard implements OnInit, OnDestroy {
         priority = 'low';
       }
       
-      // 🆕 生成阶段截止时间数据(从交付日期往前推,每个阶段1天)
-      let phaseDeadlines = project.data?.phaseDeadlines;
+      // 11. 获取或生成阶段截止时间数据
+      let phaseDeadlines = projectData.phaseDeadlines;
       
-      // 如果项目没有阶段数据,动态生成(用于演示效果)
-      if (!phaseDeadlines) {
-        // ✅ 关键修复:从交付日期往前推算各阶段截止时间
-        const deliveryTime = adjustedEndDate.getTime();
+      // 如果项目没有阶段数据,从交付日期往前推算
+      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);
         
-        // 后期截止 = 交付日期 - 1天(确保后期事件在交付前显示)
-        const postProcessingDeadline = new Date(deliveryTime - 1 * 24 * 60 * 60 * 1000);
-        // 渲染截止 = 交付日期 - 2天
-        const renderingDeadline = new Date(deliveryTime - 2 * 24 * 60 * 60 * 1000);
-        // 软装截止 = 交付日期 - 3天
-        const softDecorDeadline = new Date(deliveryTime - 3 * 24 * 60 * 60 * 1000);
-        // 建模截止 = 交付日期 - 4天
-        const modelingDeadline = new Date(deliveryTime - 4 * 24 * 60 * 60 * 1000);
+        currentDate = new Date(modelingDeadline);
+        const softDecorDeadline = new Date(currentDate);
+        softDecorDeadline.setDate(softDecorDeadline.getDate() + softDecorDays);
         
-        // 🔥 小图对图时间 = 交付日期 - 2.5天(软装和渲染之间)
-        const reviewTime = deliveryTime - 2.5 * 24 * 60 * 60 * 1000;
-        adjustedReviewDate = new Date(reviewTime); // ⚠️ 不要使用 const,直接赋值给外层变量
-        adjustedReviewDate.setHours(14, 0, 0, 0); // 设置时间为下午2点
+        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: adjustedStartDate,
+            startDate: realStartDate,
             deadline: modelingDeadline,
-            estimatedDays: 1,
-            status: now.getTime() >= modelingDeadline.getTime() && now.getTime() < softDecorDeadline.getTime() ? 'in_progress' : 
-                    now.getTime() >= softDecorDeadline.getTime() ? 'completed' : 'not_started',
+            estimatedDays: modelingDays,
+            status: now.getTime() >= modelingDeadline.getTime() ? 'completed' : 
+                    now.getTime() >= realStartDate.getTime() ? 'in_progress' : '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',
+            estimatedDays: softDecorDays,
+            status: now.getTime() >= softDecorDeadline.getTime() ? 'completed' : 
+                    now.getTime() >= modelingDeadline.getTime() ? 'in_progress' : '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',
+            estimatedDays: renderingDays,
+            status: now.getTime() >= renderingDeadline.getTime() ? 'completed' : 
+                    now.getTime() >= softDecorDeadline.getTime() ? 'in_progress' : 'not_started',
             priority: 'high'
           },
           postProcessing: {
             startDate: renderingDeadline,
             deadline: postProcessingDeadline,
-            estimatedDays: 1,
+            estimatedDays: postProcessDays,
             status: now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : 
                     now.getTime() >= renderingDeadline.getTime() ? 'in_progress' : 'not_started',
             priority: 'medium'
@@ -751,32 +862,72 @@ export class Dashboard implements OnInit, OnDestroy {
         };
       }
       
+      // 12. 获取空间和客户信息
+      const spaceName = project.space || projectData.quotation?.spaces?.[0]?.name || '';
+      const customerName = project.customer || project.contact?.name || '';
+      
       return {
         projectId: project.id || `proj-${Math.random().toString(36).slice(2, 9)}`,
         projectName: project.name || '未命名项目',
-        designerId: project.designerName || '未分配',
+        designerId: project.designerId || project.designerName || '未分配',
         designerName: project.designerName || '未分配',
-        startDate: adjustedStartDate,
-        endDate: adjustedEndDate,
-        deliveryDate: adjustedEndDate,
-        reviewDate: adjustedReviewDate,
+        startDate: realStartDate, // ✅ 使用真实开始时间
+        endDate: realEndDate, // ✅ 使用真实结束时间
+        deliveryDate: realEndDate, // ✅ 使用真实交付日期
+        reviewDate: realReviewDate, // ✅ 使用真实对图时间
         currentStage,
         stageName,
-        stageProgress: Math.round(stageProgress),
-        status,
-        isStalled,
-        stalledDays,
+        stageProgress: Math.round(stageProgress), // ✅ 使用计算的真实进度
+        status, // ✅ 基于真实日期计算的状态
+        isStalled, // ✅ 基于 updatedAt 计算的停滞状态
+        stalledDays, // ✅ 真实的停滞天数
         urgentCount,
         priority,
-        spaceName: project.space || '',
-        customerName: project.customer || '',
-        phaseDeadlines: phaseDeadlines // 🆕 阶段截止时间
+        spaceName, // ✅ 从项目数据获取
+        customerName, // ✅ 从项目数据获取
+        phaseDeadlines: phaseDeadlines, // ✅ 使用真实或计算的阶段截止时间
+        data: projectData // ✅ 保留原始数据,供后续使用
       };
     });
     
     // 更新缓存
     this.timelineDataCache = this.projectTimelineData;
     this.lastDesignerWorkloadMapSize = currentSize;
+    
+    console.log(`📊 convertToProjectTimeline 完成: 转换了 ${this.projectTimelineData.length} 个项目`);
+    if (this.projectTimelineData.length > 0) {
+      const now = new Date();
+      const sampleProject = this.projectTimelineData[0];
+      const daysFromStart = Math.floor((now.getTime() - sampleProject.startDate.getTime()) / (1000 * 60 * 60 * 24));
+      const daysToEnd = Math.floor((sampleProject.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
+      
+      console.log(`📊 第一个项目示例:`, {
+        projectId: sampleProject.projectId,
+        projectName: sampleProject.projectName,
+        designerName: sampleProject.designerName,
+        startDate: sampleProject.startDate.toLocaleString('zh-CN'),
+        endDate: sampleProject.endDate.toLocaleString('zh-CN'),
+        startDateFromNow: `${daysFromStart} 天前`,
+        endDateFromNow: daysToEnd >= 0 ? `${daysToEnd} 天后` : `${Math.abs(daysToEnd)} 天前`,
+        isEndDateInPast: daysToEnd < 0,
+        hasPhaseDeadlines: !!sampleProject.phaseDeadlines
+      });
+      
+      // ✅ 检查日期问题:统计有多少项目的结束日期在过去
+      const projectsWithPastEndDate = this.projectTimelineData.filter(p => {
+        const daysToEnd = Math.floor((p.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
+        return daysToEnd < 0;
+      });
+      
+      if (projectsWithPastEndDate.length > 0) {
+        console.warn(`⚠️ 发现 ${projectsWithPastEndDate.length} 个项目的结束日期在过去,这些项目在时间轴上可能显示为一个点`);
+        console.log(`⚠️ 示例项目:`, projectsWithPastEndDate.slice(0, 3).map(p => ({
+          name: p.projectName,
+          endDate: p.endDate.toLocaleString('zh-CN'),
+          daysAgo: Math.abs(Math.floor((p.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)))
+        })));
+      }
+    }
   }
   
   /**

+ 75 - 3
src/app/pages/team-leader/project-timeline/project-progress-modal.html

@@ -81,6 +81,67 @@
       </div>
     </div>
 
+    <!-- ✅ 应用方案:未完成任务汇总(按设计师分组)- 仅显示当前项目 -->
+    <div class="incomplete-tasks-summary" *ngIf="summary">
+      <div class="summary-header">
+        <div class="summary-title-section">
+          <span class="summary-icon">⚠️</span>
+          <h3 class="summary-title">未完成任务汇总</h3>
+          <span class="summary-badge" *ngIf="hasIncompleteTasks()">{{ getTotalIncompleteTasks() }} 个任务</span>
+          <span class="summary-badge success" *ngIf="!hasIncompleteTasks()">全部完成</span>
+        </div>
+        <div class="summary-subtitle">
+          当前项目:{{ summary.projectName }} - 按设计师分组查看未完成任务
+        </div>
+      </div>
+
+      <!-- 有未完成任务时显示 -->
+      <div class="designer-tasks-list" *ngIf="hasIncompleteTasks()">
+        <div 
+          *ngFor="let designerGroup of getIncompleteTasksByDesigner()"
+          class="designer-group"
+          [class.unassigned]="designerGroup.designerName === '未分配'">
+          
+          <div class="designer-header">
+            <div class="designer-info">
+              <span class="designer-icon">👤</span>
+              <span class="designer-name" [class.unassigned-text]="designerGroup.designerName === '未分配'">
+                {{ designerGroup.designerName }}
+              </span>
+              <span class="task-count-badge">{{ designerGroup.taskCount }} 个未完成</span>
+            </div>
+          </div>
+
+          <div class="tasks-list">
+            <div 
+              *ngFor="let task of designerGroup.tasks"
+              class="task-item">
+              <span class="task-phase-badge" [style.background-color]="getPhaseProgressColor(0)">
+                {{ task.phaseLabel }}
+              </span>
+              <span class="task-space-name">{{ task.spaceName }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 全部完成时显示 -->
+      <div class="all-completed-message" *ngIf="!hasIncompleteTasks()">
+        <span class="success-icon-large">✅</span>
+        <div class="message-content">
+          <h4>恭喜!所有任务已完成</h4>
+          <p>该项目所有空间的交付任务均已完成,无需跟进。</p>
+        </div>
+      </div>
+
+      <div class="summary-footer" *ngIf="getUnassignedTaskCount() > 0">
+        <span class="warning-icon">⚠️</span>
+        <span class="warning-text">
+          有 {{ getUnassignedTaskCount() }} 个任务未分配负责人,请及时分配
+        </span>
+      </div>
+    </div>
+
     <!-- 各阶段进度详情 -->
     <div class="phase-progress" *ngIf="summary">
       <div class="section-title">各阶段进度明细</div>
@@ -131,11 +192,22 @@
             <div class="incomplete-spaces" *ngIf="phase.info.incompleteSpaces.length > 0">
               <div 
                 *ngFor="let space of phase.info.incompleteSpaces"
-                class="space-item">
+                class="space-item"
+                [class.no-assignee]="!space.assignee || space.assignee === '未分配'">
                 <span class="space-icon">📦</span>
                 <span class="space-name">{{ space.spaceName }}</span>
-                <span class="assignee" *ngIf="space.assignee">
-                  负责人:{{ space.assignee }}
+                <!-- ✅ 优化:右侧显示设计师名字 -->
+                <span class="assignee-wrapper">
+                  <!-- 有设计师且不是"未分配"时显示 -->
+                  <span class="assignee" *ngIf="space.assignee && space.assignee !== '未分配'">
+                    <span class="assignee-icon">👤</span>
+                    <span class="assignee-name">{{ space.assignee }}</span>
+                  </span>
+                  <!-- 没有设计师或为"未分配"时显示 -->
+                  <span class="assignee unassigned-badge" *ngIf="!space.assignee || space.assignee === '未分配'">
+                    <span class="warning-icon-small">⚠️</span>
+                    <span>未分配</span>
+                  </span>
                 </span>
               </div>
             </div>

+ 291 - 0
src/app/pages/team-leader/project-timeline/project-progress-modal.scss

@@ -435,6 +435,297 @@
   }
 }
 
+// ✅ 应用方案:未完成任务汇总区域
+.incomplete-tasks-summary {
+  padding: 20px 24px;
+  background: linear-gradient(135deg, #fff3cd 0%, #ffe69c 100%);
+  border-bottom: 2px solid #ffc107;
+  margin-bottom: 0;
+
+  .summary-header {
+    margin-bottom: 16px;
+
+    .summary-title-section {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      margin-bottom: 8px;
+
+      .summary-icon {
+        font-size: 24px;
+      }
+
+      .summary-title {
+        margin: 0;
+        font-size: 18px;
+        font-weight: 700;
+        color: #856404;
+        flex: 1;
+      }
+
+      .summary-badge {
+        background: #ff5722;
+        color: white;
+        padding: 4px 12px;
+        border-radius: 12px;
+        font-size: 14px;
+        font-weight: 700;
+
+        &.success {
+          background: #4caf50;
+        }
+      }
+    }
+
+    .summary-subtitle {
+      font-size: 13px;
+      color: #856404;
+      margin-left: 36px;
+    }
+  }
+
+  .designer-tasks-list {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+  }
+
+  .designer-group {
+    background: white;
+    border-radius: 8px;
+    border: 2px solid #ffc107;
+    overflow: hidden;
+    transition: all 0.3s ease;
+
+    &.unassigned {
+      border-color: #ff5722;
+      background: #fff5f5;
+    }
+
+    &:hover {
+      box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3);
+      transform: translateY(-1px);
+    }
+
+    .designer-header {
+      padding: 12px 16px;
+      background: linear-gradient(135deg, #fff9c4 0%, #fff59d 100%);
+      border-bottom: 1px solid #ffc107;
+
+      .designer-info {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+
+        .designer-icon {
+          font-size: 20px;
+        }
+
+        .designer-name {
+          font-size: 16px;
+          font-weight: 700;
+          color: #333;
+          flex: 1;
+
+          &.unassigned-text {
+            color: #d32f2f;
+          }
+        }
+
+        .task-count-badge {
+          background: #ff5722;
+          color: white;
+          padding: 4px 10px;
+          border-radius: 10px;
+          font-size: 12px;
+          font-weight: 600;
+        }
+      }
+    }
+
+    .tasks-list {
+      padding: 12px 16px;
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+    }
+
+    .task-item {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding: 8px 12px;
+      background: #fffbf0;
+      border-radius: 6px;
+      border-left: 3px solid #ff9800;
+      transition: all 0.2s ease;
+
+      &:hover {
+        background: #fff8e1;
+        border-left-color: #ff5722;
+      }
+
+      .task-phase-badge {
+        padding: 4px 10px;
+        border-radius: 6px;
+        font-size: 12px;
+        font-weight: 600;
+        color: white;
+        white-space: nowrap;
+        min-width: 50px;
+        text-align: center;
+      }
+
+      .task-space-name {
+        font-size: 14px;
+        font-weight: 600;
+        color: #333;
+        flex: 1;
+      }
+    }
+  }
+
+  .summary-footer {
+    margin-top: 16px;
+    padding: 12px 16px;
+    background: #ffebee;
+    border-radius: 8px;
+    border: 1px solid #ffcdd2;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    .warning-icon {
+      font-size: 20px;
+    }
+
+    .warning-text {
+      font-size: 14px;
+      color: #c62828;
+      font-weight: 600;
+    }
+  }
+
+  // ✅ 全部完成时的显示
+  .all-completed-message {
+    padding: 24px;
+    text-align: center;
+    background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
+    border-radius: 8px;
+    border: 2px solid #4caf50;
+
+    .success-icon-large {
+      font-size: 48px;
+      display: block;
+      margin-bottom: 12px;
+    }
+
+    .message-content {
+      h4 {
+        margin: 0 0 8px 0;
+        font-size: 18px;
+        font-weight: 700;
+        color: #2e7d32;
+      }
+
+      p {
+        margin: 0;
+        font-size: 14px;
+        color: #4caf50;
+      }
+    }
+  }
+}
+
+// ✅ 应用方案:优化未完成空间项的样式
+.space-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 10px 12px;
+  background: white;
+  border-radius: 6px;
+  border: 1px solid #e0e0e0;
+  transition: all 0.2s ease;
+
+  &.no-assignee {
+    border-color: #ff9800;
+    background: #fff8e1;
+    border-left: 3px solid #ff9800;
+  }
+
+  &:hover {
+    background: #f8f9fa;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  }
+
+  .space-icon {
+    font-size: 16px;
+    flex-shrink: 0;
+  }
+
+  .space-name {
+    flex: 1;
+    font-size: 14px;
+    color: #333;
+    font-weight: 500;
+    min-width: 0; // 允许文本截断
+  }
+
+  // ✅ 优化:设计师信息包装器,确保右侧对齐
+  .assignee-wrapper {
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    margin-left: auto; // 推送到右侧
+    min-width: 80px; // 确保有最小宽度,避免被压缩
+    justify-content: flex-end; // 右对齐
+  }
+
+  .assignee {
+    display: inline-flex; // 改为 inline-flex,确保显示
+    align-items: center;
+    gap: 6px;
+    font-size: 12px;
+    color: #1976d2;
+    background: #e3f2fd;
+    padding: 4px 10px;
+    border-radius: 12px;
+    font-weight: 600;
+    white-space: nowrap;
+    min-width: fit-content;
+    visibility: visible; // 确保可见
+    opacity: 1; // 确保不透明
+
+    .assignee-icon {
+      font-size: 14px;
+      flex-shrink: 0;
+      display: inline-block; // 确保图标显示
+    }
+
+    .assignee-name {
+      font-weight: 600;
+      white-space: nowrap;
+      display: inline-block; // 确保文字显示
+    }
+
+    &.unassigned-badge {
+      background: #ffebee;
+      color: #d32f2f;
+      border: 1px solid #ffcdd2;
+      display: inline-flex; // 确保显示
+      visibility: visible; // 确保可见
+      opacity: 1; // 确保不透明
+
+      .warning-icon-small {
+        font-size: 12px;
+        flex-shrink: 0;
+        display: inline-block; // 确保图标显示
+      }
+    }
+  }
+}
+
 // 底部按钮
 .modal-footer {
   display: flex;

+ 136 - 0
src/app/pages/team-leader/project-timeline/project-progress-modal.ts

@@ -34,6 +34,26 @@ export class ProjectProgressModalComponent implements OnInit {
 
   ngOnInit(): void {
     console.log('📊 项目进度弹窗初始化', this.summary);
+    if (this.summary) {
+      console.log('📊 项目进度详情:', {
+        projectId: this.summary.projectId,
+        projectName: this.summary.projectName,
+        totalSpaces: this.summary.totalSpaces,
+        phaseProgress: this.summary.phaseProgress,
+        incompleteTasks: this.getTotalIncompleteTasks(),
+        hasIncomplete: this.hasIncompleteTasks()
+      });
+      
+      // ✅ 调试:检查未完成空间的 assignee 字段
+      if (this.summary.phaseProgress) {
+        Object.entries(this.summary.phaseProgress).forEach(([phaseKey, phaseInfo]: [string, any]) => {
+          console.log(`📊 阶段 ${phaseKey} 的未完成空间:`, phaseInfo.incompleteSpaces);
+          phaseInfo.incompleteSpaces?.forEach((space: any, index: number) => {
+            console.log(`  ${index + 1}. ${space.spaceName}: assignee = "${space.assignee}" (类型: ${typeof space.assignee})`);
+          });
+        });
+      }
+    }
   }
 
   /**
@@ -126,5 +146,121 @@ export class ProjectProgressModalComponent implements OnInit {
     if (!this.summary) return '加载中';
     return this.getPhaseStatusLabel(this.summary.overallCompletionRate);
   }
+
+  /**
+   * ✅ 应用方案:获取当前项目的所有未完成的任务,按设计师分组
+   * 注意:这里只显示当前项目(this.summary)的未完成任务,不是所有项目的汇总
+   */
+  getIncompleteTasksByDesigner(): Array<{
+    designerName: string;
+    tasks: Array<{
+      phaseName: string;
+      phaseLabel: string;
+      spaceName: string;
+      spaceId: string;
+    }>;
+    taskCount: number;
+  }> {
+    if (!this.summary?.phaseProgress) {
+      console.warn('⚠️ 项目进度数据不完整,无法获取未完成任务');
+      return [];
+    }
+
+    // ✅ 确保只处理当前项目的数据
+    console.log('📊 获取未完成任务(当前项目):', {
+      projectId: this.summary.projectId,
+      projectName: this.summary.projectName
+    });
+
+    // 收集当前项目的所有未完成的任务
+    const taskMap = new Map<string, Array<{
+      phaseName: string;
+      phaseLabel: string;
+      spaceName: string;
+      spaceId: string;
+    }>>();
+
+    // 遍历当前项目的所有阶段
+    Object.entries(this.summary.phaseProgress).forEach(([phaseKey, phaseInfo]: [string, any]) => {
+      const phaseLabel = phaseInfo.phaseLabel || phaseKey;
+      
+      // 遍历当前项目当前阶段的未完成空间
+      if (phaseInfo.incompleteSpaces && Array.isArray(phaseInfo.incompleteSpaces)) {
+        phaseInfo.incompleteSpaces.forEach((space: any) => {
+          // ✅ 使用空间负责人,如果没有则使用"未分配"
+          const designerName = space.assignee || '未分配';
+          
+          if (!taskMap.has(designerName)) {
+            taskMap.set(designerName, []);
+          }
+          
+          taskMap.get(designerName)!.push({
+            phaseName: phaseKey,
+            phaseLabel: phaseLabel,
+            spaceName: space.spaceName || '未命名空间',
+            spaceId: space.spaceId || ''
+          });
+        });
+      }
+    });
+
+    // 转换为数组并排序(按任务数量降序)
+    const result = Array.from(taskMap.entries()).map(([designerName, tasks]) => ({
+      designerName,
+      tasks,
+      taskCount: tasks.length
+    }));
+
+    // 按任务数量降序排序
+    result.sort((a, b) => b.taskCount - a.taskCount);
+
+    console.log('📊 未完成任务分组结果(当前项目):', {
+      projectId: this.summary.projectId,
+      totalTasks: this.getTotalIncompleteTasks(),
+      designerGroups: result.length,
+      details: result
+    });
+
+    return result;
+  }
+
+  /**
+   * ✅ 应用方案:获取当前项目的未完成任务总数
+   */
+  getTotalIncompleteTasks(): number {
+    if (!this.summary?.phaseProgress) return 0;
+
+    let total = 0;
+    Object.values(this.summary.phaseProgress).forEach((phaseInfo: any) => {
+      if (phaseInfo.incompleteSpaces && Array.isArray(phaseInfo.incompleteSpaces)) {
+        total += phaseInfo.incompleteSpaces.length;
+      }
+    });
+
+    return total;
+  }
+
+  /**
+   * ✅ 应用方案:获取未分配任务数
+   */
+  getUnassignedTaskCount(): number {
+    const tasksByDesigner = this.getIncompleteTasksByDesigner();
+    const unassigned = tasksByDesigner.find(t => t.designerName === '未分配');
+    return unassigned?.taskCount || 0;
+  }
+
+  /**
+   * ✅ 应用方案:判断当前项目是否有未完成的任务
+   */
+  hasIncompleteTasks(): boolean {
+    const total = this.getTotalIncompleteTasks();
+    console.log('📊 检查是否有未完成任务:', {
+      projectId: this.summary?.projectId,
+      projectName: this.summary?.projectName,
+      totalIncompleteTasks: total,
+      hasIncomplete: total > 0
+    });
+    return total > 0;
+  }
 }
 

+ 16 - 19
src/app/pages/team-leader/project-timeline/project-timeline.html

@@ -242,30 +242,26 @@
               <div class="timeline-track">
                 <!-- 项目条形图 -->
                 <div class="project-bar"
+                     [ngClass]="getProjectStatusClass(project)"
                      [style.left]="getProjectPosition(project).left"
                      [style.width]="getProjectPosition(project).width"
                      [style.background]="getProjectPosition(project).background"
-                     [class.status-overdue]="project.status === 'overdue'"
-                     [title]="project.projectName + ' | ' + project.stageName + ' ' + project.stageProgress + '%'">
+                     [title]="project.projectName + ' | ' + project.stageName + ' ' + getProjectCompletionRate(project) + '%'">
                   <!-- 进度填充 -->
-                  <div class="progress-fill" [style.width]="project.stageProgress + '%'"></div>
-                </div>
-                
-                <!-- 🆕 项目进度线(基于实际完成率) -->
-                @if (getSpaceDeliverableSummary(project.projectId); as summary) {
-                  <div class="progress-line" 
-                       [style.left]="getProgressLinePosition(project)"
-                       [style.border-color]="getProgressLineColor(summary.overallCompletionRate)">
-                    <div class="progress-label"
-                         [style.background-color]="getProgressLineColor(summary.overallCompletionRate)">
-                      {{ getProgressLineLabel(project) }}
-                    </div>
-                    <div class="progress-dot"
-                         [style.background-color]="getProgressLineColor(summary.overallCompletionRate)"></div>
-                    <div class="progress-bar-line"
-                         [style.border-color]="getProgressLineColor(summary.overallCompletionRate)"></div>
+                  <div class="progress-fill"
+                       [style.width]="getProjectCompletionRate(project) + '%'"
+                       [style.background]="getProjectCompletionColor(project)">
                   </div>
-                }
+                  <div class="progress-marker"
+                       [style.left]="getCompletionMarkerLeft(project)">
+                    <span class="marker-label"
+                          [style.background]="getProjectCompletionColor(project)">
+                      {{ getProjectCompletionRate(project) }}%
+                    </span>
+                    <span class="marker-dot"
+                          [style.background]="getProjectCompletionColor(project)"></span>
+                  </div>
+                </div>
                 
                 <!-- 🆕 使用统一的事件标记方法 -->
                 @for (event of getProjectEvents(project); track event.date) {
@@ -292,6 +288,7 @@
 <app-project-progress-modal
   [visible]="showProgressModal"
   [summary]="selectedProjectSummary"
+  [companyId]="companyId"
   (close)="onCloseProgressModal()"
   (reportIssue)="onReportIssueFromModal($event)">
 </app-project-progress-modal>

+ 71 - 61
src/app/pages/team-leader/project-timeline/project-timeline.scss

@@ -630,6 +630,7 @@
   position: relative;
   height: 70px;
   padding: 19px 0;
+  overflow: visible;
   background: repeating-linear-gradient(
     90deg,
     transparent,
@@ -644,22 +645,50 @@
   position: absolute;
   top: 50%;
   transform: translateY(-50%);
-  height: 32px;
-  border-radius: 6px;
+  height: 34px;
+  border-radius: 8px;
   transition: all 0.3s;
   overflow: hidden;
-  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
-  border: 2px solid rgba(255, 255, 255, 0.5);
+  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.12);
+  border: 1px solid rgba(255, 255, 255, 0.4);
   opacity: 0.95;
+  
+  &::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    height: 5px;
+    border-radius: 8px 8px 0 0;
+    background: rgba(255, 255, 255, 0.35);
+    transition: background 0.3s ease;
+  }
+
+  &.status-overdue::before {
+    background: #dc2626;
+  }
+  
+  &.status-urgent::before {
+    background: #f97316;
+  }
+  
+  &.status-warning::before {
+    background: #facc15;
+  }
+  
+  &.status-normal::before {
+    background: #22c55e;
+  }
 
-  &.status-overdue {
-    border: 3px solid #dc2626;
-    animation: pulse 2s infinite;
-    box-shadow: 0 0 15px rgba(220, 38, 38, 0.4);
+  &.status-stalled::before {
+    background: #7c3aed;
   }
   
   &:hover {
     opacity: 1;
+    transform: translateY(-50%) scale(1.01);
+    box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
   }
 }
 
@@ -669,12 +698,40 @@
   top: 0;
   left: 0;
   bottom: 0;
-  background: linear-gradient(90deg, 
-    rgba(0, 0, 0, 0.25) 0%, 
-    rgba(0, 0, 0, 0.15) 100%
-  );
-  transition: width 0.3s;
-  border-right: 2px solid rgba(255, 255, 255, 0.6);
+  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;
+}
+
+.progress-marker {
+  position: absolute;
+  top: -40px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 6px;
+  transform: translateX(-50%);
+  pointer-events: none;
+}
+
+.marker-label {
+  color: #ffffff;
+  font-size: 11px;
+  font-weight: 700;
+  padding: 3px 10px;
+  border-radius: 999px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+  letter-spacing: 0.5px;
+  white-space: nowrap;
+}
+
+.marker-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  border: 2px solid #ffffff;
+  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
 }
 
 // 事件标记
@@ -893,53 +950,6 @@
   }
 }
 
-// 🆕 项目进度线样式(基于实际完成率)
-.progress-line {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  z-index: 15;
-  pointer-events: none;
-  
-  .progress-label {
-    position: absolute;
-    top: -30px;
-    left: 50%;
-    transform: translateX(-50%);
-    padding: 4px 8px;
-    border-radius: 4px;
-    color: white;
-    font-size: 11px;
-    font-weight: 600;
-    white-space: nowrap;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
-    animation: fadeInDown 0.3s ease-out;
-  }
-  
-  .progress-dot {
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    width: 12px;
-    height: 12px;
-    border-radius: 50%;
-    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
-    animation: pulse 2s infinite;
-  }
-  
-  .progress-bar-line {
-    position: absolute;
-    top: 0;
-    bottom: 0;
-    left: 50%;
-    transform: translateX(-50%);
-    width: 3px;
-    border-left: 3px dashed;
-    opacity: 0.8;
-  }
-}
-
 @keyframes fadeInDown {
   from {
     opacity: 0;

+ 141 - 131
src/app/pages/team-leader/project-timeline/project-timeline.ts

@@ -162,24 +162,52 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   
   /**
    * 获取项目在时间轴上的位置和宽度
+   * ✅ 修复:处理项目结束日期在过去的情况
    */
   getProjectPosition(project: ProjectTimeline): { left: string; width: string; background: string } {
     const rangeStart = this.timeRangeStart.getTime();
     const rangeEnd = this.timeRangeEnd.getTime();
     const rangeDuration = rangeEnd - rangeStart;
     
+    // ✅ 修复:如果项目完全在过去,显示一个最小宽度的条
+    if (project.endDate.getTime() < rangeStart) {
+      // 项目完全在过去,显示在时间轴最左侧,宽度为最小可见宽度
+      return {
+        left: '0%',
+        width: '2px', // 最小可见宽度
+        background: this.getProjectBarBackground(project)
+      };
+    }
+    
+    // ✅ 修复:如果项目开始日期在过去,但结束日期在未来,从时间轴开始位置显示
     const projectStart = Math.max(project.startDate.getTime(), rangeStart);
     const projectEnd = Math.min(project.endDate.getTime(), rangeEnd);
     
+    // ✅ 修复:确保 projectEnd >= projectStart
+    if (projectEnd < projectStart) {
+      // 这种情况不应该发生,但如果发生了,显示最小宽度
+      console.warn(`⚠️ 项目 ${project.projectName} 的结束日期早于开始日期或时间范围开始`, {
+        startDate: project.startDate.toLocaleString('zh-CN'),
+        endDate: project.endDate.toLocaleString('zh-CN'),
+        rangeStart: new Date(rangeStart).toLocaleString('zh-CN'),
+        rangeEnd: new Date(rangeEnd).toLocaleString('zh-CN')
+      });
+      return {
+        left: '0%',
+        width: '2px',
+        background: this.getProjectBarBackground(project)
+      };
+    }
+    
     const left = ((projectStart - rangeStart) / rangeDuration) * 100;
     const width = ((projectEnd - projectStart) / rangeDuration) * 100;
     
-    // 🆕 根据时间紧急度获取颜色(而不是阶段)
-    const background = this.getProjectUrgencyColor(project);
+    // 🆕 根据状态生成底色
+    const background = this.getProjectBarBackground(project);
     
     return {
       left: `${Math.max(0, left)}%`,
-      width: `${Math.max(1, Math.min(100 - left, width))}%`,
+      width: `${Math.max(2, Math.min(100 - left, width))}%`, // ✅ 修复:最小宽度改为2px(约2%)
       background
     };
   }
@@ -204,83 +232,62 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   }
   
   /**
-   * 🆕 根据时间紧急度获取项目条颜色
-   * 规则:
-   * - 正常进行(距离最近事件1天+):绿色
-   * - 临近事件前一天(24小时内):黄色
-   * - 事件当天(6小时以上):橙色
-   * - 紧急情况(6小时内):红色
+   * 🆕 根据项目状态生成背景渐变
    */
-  getProjectUrgencyColor(project: ProjectTimeline): string {
-    const now = this.currentTime.getTime();
-    
-    // 找到最近的未来事件(对图或交付)
-    const upcomingEvents: { date: Date; type: string }[] = [];
-    
-    if (project.reviewDate && project.reviewDate.getTime() >= now) {
-      upcomingEvents.push({ date: project.reviewDate, type: 'review' });
-    }
-    if (project.deliveryDate && project.deliveryDate.getTime() >= now) {
-      upcomingEvents.push({ date: project.deliveryDate, type: 'delivery' });
+  private getProjectBarBackground(project: ProjectTimeline): string {
+    if (project.isStalled) {
+      return 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)';
     }
     
-    // 如果没有未来事件,使用默认绿色(项目正常进行)
-    if (upcomingEvents.length === 0) {
-      return 'linear-gradient(135deg, #86EFAC 0%, #4ADE80 100%)'; // 绿色
-    }
+    const backgrounds: Record<string, string> = {
+      normal: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)',
+      warning: 'linear-gradient(135deg, #fef7c3 0%, #fde68a 100%)',
+      urgent: 'linear-gradient(135deg, #ffe7d4 0%, #fdba74 100%)',
+      overdue: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
+      stalled: 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)',
+      critical: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
+      low: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)',
+      medium: 'linear-gradient(135deg, #fef7c3 0%, #fde68a 100%)',
+      high: 'linear-gradient(135deg, #ffe7d4 0%, #fdba74 100%)'
+    };
     
-    // 找到最近的事件
-    upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime());
-    const nearestEvent = upcomingEvents[0];
-    const eventTime = nearestEvent.date.getTime();
-    
-    // 计算时间差(毫秒)
-    const timeDiff = eventTime - now;
-    const hoursDiff = timeDiff / (1000 * 60 * 60);
-    const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
-    
-    // 判断是否是同一天
-    const nowDate = new Date(now);
-    const eventDate = nearestEvent.date;
-    const isSameDay = nowDate.getFullYear() === eventDate.getFullYear() &&
-                      nowDate.getMonth() === eventDate.getMonth() &&
-                      nowDate.getDate() === eventDate.getDate();
-    
-    let color = '';
-    let colorName = '';
-    
-    // 🔴 红色:距离事件时间不到6小时(紧急)
-    if (hoursDiff < 6) {
-      color = 'linear-gradient(135deg, #FCA5A5 0%, #EF4444 100%)';
-      colorName = '🔴 红色(紧急 - 6小时内)';
-    }
-    // 🟠 橙色:事件当天但还有6小时以上
-    else if (isSameDay) {
-      color = 'linear-gradient(135deg, #FCD34D 0%, #F59E0B 100%)';
-      colorName = '🟠 橙色(当天 - 6小时+)';
+    return backgrounds[project.status] || 'linear-gradient(135deg, #e2e8f0 0%, #f8fafc 100%)';
+  }
+  
+  getProjectStatusClass(project: ProjectTimeline): string {
+    if (project.isStalled) {
+      return 'status-stalled';
     }
-    // 🟡 黄色:距离事件前一天(24小时内但不是当天)
-    else if (hoursDiff < 24) {
-      color = 'linear-gradient(135deg, #FEF08A 0%, #EAB308 100%)';
-      colorName = '🟡 黄色(前一天)';
+    return `status-${project.status || 'normal'}`;
+  }
+  
+  getProjectCompletionRate(project: ProjectTimeline): number {
+    const summary = this.getSpaceDeliverableSummary(project.projectId);
+    if (summary) {
+      return Math.round(summary.overallCompletionRate);
     }
-    // 🟢 绿色:正常进行(距离事件1天+)
-    else {
-      color = 'linear-gradient(135deg, #86EFAC 0%, #4ADE80 100%)';
-      colorName = '🟢 绿色(正常)';
+    if (typeof project.stageProgress === 'number') {
+      return Math.round(Math.max(0, Math.min(100, project.stageProgress)));
     }
-    
-    // 调试日志(只在首次加载时输出,避免刷屏)
-    if (Math.random() < 0.1) { // 10%概率输出
-      console.log(`🎨 项目颜色:${project.projectName}`, {
-        最近事件: `${nearestEvent.type === 'review' ? '对图' : '交付'} - ${nearestEvent.date.toLocaleString('zh-CN')}`,
-        剩余时间: `${hoursDiff.toFixed(1)}小时 (${daysDiff.toFixed(1)}天)`,
-        是否当天: isSameDay,
-        颜色判断: colorName
-      });
-    }
-    
-    return color;
+    return 0;
+  }
+  
+  getProjectCompletionColor(project: ProjectTimeline): string {
+    return this.getCompletionColor(this.getProjectCompletionRate(project));
+  }
+  
+  getCompletionMarkerLeft(project: ProjectTimeline): string {
+    const rate = this.getProjectCompletionRate(project);
+    const clamped = Math.max(8, Math.min(92, rate));
+    return `calc(${clamped}% - 12px)`;
+  }
+  
+  private getCompletionColor(completionRate: number): string {
+    if (completionRate >= 80) return '#16a34a';
+    if (completionRate >= 60) return '#22c55e';
+    if (completionRate >= 40) return '#f59e0b';
+    if (completionRate >= 20) return '#f97316';
+    return '#ef4444';
   }
   
   /**
@@ -481,8 +488,19 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   private buildDesignerStats(): DesignerInfo[] {
     const designerMap = new Map<string, DesignerInfo>();
     
+    // ✅ 过滤已完成的项目后再统计
+    const now = this.currentTime.getTime();
+    const activeProjects = this.projects.filter(project => {
+      // ✅ 修复:使用相同的过滤逻辑
+      const endDatePast = project.endDate.getTime() < now;
+      const daysPast = endDatePast ? Math.floor((now - project.endDate.getTime()) / (1000 * 60 * 60 * 24)) : 0;
+      const isCompleted = (project.currentStage === 'delivery' && endDatePast) || 
+                          (endDatePast && daysPast > 7);
+      return !isCompleted;
+    });
+    
     // 🔧 统计每个设计师负责的项目(不去重,因为项目列表已经包含了所有设计师-项目关联)
-    this.projects.forEach(project => {
+    activeProjects.forEach(project => {
       const designerName = project.designerName || '未分配';
       
       if (!designerMap.has(designerName)) {
@@ -525,6 +543,55 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   applyFilters(): void {
     let result = [...this.projects];
     
+    // ✅ 过滤已完成的项目
+    const now = this.currentTime.getTime();
+    const filteredOut: any[] = [];
+    
+    result = result.filter(project => {
+      // 判断项目是否已完成:
+      // 1. currentStage 为 'delivery'(交付阶段)且 endDate 在过去
+      // 2. 或者 endDate 在过去超过7天(给一个缓冲期)
+      const endDatePast = project.endDate.getTime() < now;
+      const daysPast = endDatePast ? Math.floor((now - project.endDate.getTime()) / (1000 * 60 * 60 * 24)) : 0;
+      const isCompleted = (project.currentStage === 'delivery' && endDatePast) || 
+                          (endDatePast && daysPast > 7); // ✅ 修复:只过滤结束日期在过去超过7天的项目
+      
+      if (isCompleted) {
+        filteredOut.push({
+          projectId: project.projectId,
+          projectName: project.projectName,
+          currentStage: project.currentStage,
+          endDate: project.endDate.toLocaleString('zh-CN'),
+          daysPast: daysPast
+        });
+      }
+      
+      return !isCompleted; // 只保留未完成的项目
+    });
+    
+    console.log(`📊 过滤已完成项目后剩余: ${result.length} 个项目(原始: ${this.projects.length})`);
+    if (filteredOut.length > 0) {
+      console.log(`📊 已过滤的项目(${filteredOut.length}个):`, filteredOut.slice(0, 5));
+    }
+    
+    // ✅ 调试:检查特定项目
+    const targetProject = this.projects.find(p => p.projectId === 'qCV9QHROSH');
+    if (targetProject) {
+      const endDatePast = targetProject.endDate.getTime() < now;
+      const daysPast = endDatePast ? Math.floor((now - targetProject.endDate.getTime()) / (1000 * 60 * 60 * 24)) : 0;
+      console.log(`📊 新项目 qCV9QHROSH 详情:`, {
+        projectName: targetProject.projectName,
+        currentStage: targetProject.currentStage,
+        startDate: targetProject.startDate.toLocaleString('zh-CN'),
+        endDate: targetProject.endDate.toLocaleString('zh-CN'),
+        endDatePast: endDatePast,
+        daysPast: daysPast,
+        isFiltered: filteredOut.some(p => p.projectId === 'qCV9QHROSH')
+      });
+    } else {
+      console.warn(`⚠️ 未找到项目 qCV9QHROSH,可能数据还未加载`);
+    }
+    
     // 设计师筛选
     if (this.selectedDesigner !== 'all') {
       // 选择单个设计师:显示该设计师的所有项目
@@ -823,63 +890,6 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     return lines.join('\n');
   }
   
-  /**
-   * 🆕 获取进度线标签(基于项目完成情况)
-   */
-  getProgressLineLabel(project: ProjectTimeline): string {
-    const summary = this.getSpaceDeliverableSummary(project.projectId);
-    if (!summary) return '加载中...';
-    return `进度:${summary.overallCompletionRate}%`;
-  }
-  
-  /**
-   * 🆕 获取项目的进度线位置(基于实际完成率)
-   * 进度线表示:在项目时间轴上,根据完成率显示当前进度的位置
-   * @param project 项目信息
-   * @returns CSS left 位置
-   */
-  getProgressLinePosition(project: ProjectTimeline): string {
-    const summary = this.getSpaceDeliverableSummary(project.projectId);
-    if (!summary) return '0%';
-    
-    // 根据完成率计算在项目条上的位置
-    const completionRate = summary.overallCompletionRate;
-    
-    // 获取项目在时间轴上的起始位置和宽度
-    const rangeStart = this.timeRangeStart.getTime();
-    const rangeEnd = this.timeRangeEnd.getTime();
-    const rangeDuration = rangeEnd - rangeStart;
-    
-    const projectStart = Math.max(project.startDate.getTime(), rangeStart);
-    const projectEnd = Math.min(project.endDate.getTime(), rangeEnd);
-    const projectDuration = projectEnd - projectStart;
-    
-    // 项目条的起始位置(百分比)
-    const projectLeft = ((projectStart - rangeStart) / rangeDuration) * 100;
-    
-    // 项目条的宽度(百分比)
-    const projectWidth = (projectDuration / rangeDuration) * 100;
-    
-    // 进度线在项目条内的位置 = 项目起始位置 + 项目宽度 × 完成率
-    const progressPosition = projectLeft + (projectWidth * completionRate / 100);
-    
-    // 🔧 考虑左侧项目名称列的宽度(180px)
-    const result = `calc(180px + (100% - 180px) * ${Math.max(0, Math.min(100, progressPosition)) / 100})`;
-    
-    return result;
-  }
-  
-  /**
-   * 🆕 获取进度线的颜色(基于完成率)
-   */
-  getProgressLineColor(completionRate: number): string {
-    if (completionRate >= 80) return '#4CAF50'; // 绿色
-    if (completionRate >= 60) return '#8BC34A'; // 浅绿
-    if (completionRate >= 40) return '#FFC107'; // 黄色
-    if (completionRate >= 20) return '#FF9800'; // 橙色
-    return '#F44336'; // 红色
-  }
-  
   /**
    * 获取今日标签(含时分)- 保留用于时间参考
    */

+ 1 - 0
src/modules/project/components/project-bottom-card/project-bottom-card.component.html

@@ -104,6 +104,7 @@
 <app-project-progress-modal
   [visible]="showProgressModal"
   [summary]="projectProgressSummary"
+  [companyId]="cid"
   (close)="onCloseProgressModal()"
   (reportIssue)="onReportIssueFromModal($event)">
 </app-project-progress-modal>

+ 10 - 0
src/modules/project/components/project-progress-modal/project-progress-modal.component.html

@@ -18,6 +18,16 @@
       <div class="info-row">
         <span class="label">项目名称:</span>
         <span class="value">{{ summary.projectName }}</span>
+        <!-- 🆕 进入项目按钮 -->
+        <a 
+          *ngIf="canNavigateToProject()"
+          [routerLink]="getProjectDetailRoute()"
+          class="enter-project-btn"
+          (click)="onClose()"
+          title="进入项目详情页">
+          <span class="btn-icon">🚀</span>
+          进入项目
+        </a>
       </div>
       <div class="info-row">
         <span class="label">空间总数:</span>

+ 32 - 0
src/modules/project/components/project-progress-modal/project-progress-modal.component.scss

@@ -133,6 +133,38 @@
     .value {
       color: #333;
       font-weight: 600;
+      flex: 1;
+    }
+
+    // 🆕 进入项目按钮样式
+    .enter-project-btn {
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+      padding: 8px 16px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      color: white;
+      text-decoration: none;
+      border-radius: 6px;
+      font-size: 14px;
+      font-weight: 600;
+      transition: all 0.2s;
+      margin-left: 12px;
+      white-space: nowrap;
+      
+      &:hover {
+        background: linear-gradient(135deg, #5568d3 0%, #65408b 100%);
+        transform: translateY(-1px);
+        box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
+      }
+      
+      &:active {
+        transform: translateY(0);
+      }
+      
+      .btn-icon {
+        font-size: 16px;
+      }
     }
   }
 }

+ 20 - 1
src/modules/project/components/project-progress-modal/project-progress-modal.component.ts

@@ -1,6 +1,7 @@
 import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
+import { RouterModule } from '@angular/router';
 import { ProjectSpaceDeliverableSummary, PhaseProgressInfo } from '../../services/project-space-deliverable.service';
 import { PHASE_INFO, PhaseName } from '../../../../app/models/project-phase.model';
 
@@ -21,13 +22,14 @@ import { PHASE_INFO, PhaseName } from '../../../../app/models/project-phase.mode
 @Component({
   selector: 'app-project-progress-modal',
   standalone: true,
-  imports: [CommonModule, FormsModule],
+  imports: [CommonModule, FormsModule, RouterModule],
   templateUrl: './project-progress-modal.component.html',
   styleUrl: './project-progress-modal.component.scss'
 })
 export class ProjectProgressModalComponent implements OnInit {
   @Input() summary: ProjectSpaceDeliverableSummary | null = null;
   @Input() visible: boolean = false;
+  @Input() companyId: string = ''; // 🆕 公司ID,用于项目详情页导航
   @Output() close = new EventEmitter<void>();
   @Output() reportIssue = new EventEmitter<string>(); // 提交问题,传递项目ID
 
@@ -131,5 +133,22 @@ export class ProjectProgressModalComponent implements OnInit {
     if (!this.summary) return '加载中';
     return this.getPhaseStatusLabel(this.summary.overallCompletionRate);
   }
+
+  /**
+   * 🆕 获取项目详情页路由
+   */
+  getProjectDetailRoute(): string[] {
+    if (!this.summary || !this.companyId) {
+      return [];
+    }
+    return ['/wxwork', this.companyId, 'project', this.summary.projectId];
+  }
+
+  /**
+   * 🆕 检查是否可以导航到项目详情页
+   */
+  canNavigateToProject(): boolean {
+    return !!(this.summary?.projectId && this.companyId);
+  }
 }
 

+ 214 - 28
src/modules/project/services/project-space-deliverable.service.ts

@@ -131,13 +131,37 @@ export class ProjectSpaceDeliverableService {
       const project = await projectQuery.get(projectId);
       const projectName = project.get('title') || project.get('name') || '未命名项目';
 
-      // 2. 获取项目的所有空间(Product)
-      const productQuery = new Parse.Query('Product');
-      productQuery.equalTo('project', project.toPointer());
-      productQuery.ascending('createdAt');
-      const products = await productQuery.find();
-
-      console.log(`📊 项目 ${projectName} 共有 ${products.length} 个空间(Product)`);
+      // ✅ 应用方案:获取项目的所有空间(Product),包含负责人信息
+      let products: FmodeObject[] = [];
+      try {
+        const productQuery = new Parse.Query('Product');
+        productQuery.equalTo('project', project.toPointer());
+        productQuery.ascending('createdAt');
+        productQuery.include('profile'); // ✅ 包含负责人信息(如果存在)
+        // ✅ 优化:如果 profile 是 Pointer,需要 include profile 的 name 字段
+        // 注意:Parse 的 include 会自动包含关联对象的基本字段,但为了确保 name 字段可用,我们显式 include
+        products = await productQuery.find();
+        
+        // ✅ 调试:检查 profile 字段是否正确加载
+        console.log(`📊 查询到 ${products.length} 个 Product,检查 profile 字段:`);
+        products.forEach((product, index) => {
+          const profile = product.get('profile');
+          const productName = product.get('productName') || '未命名';
+          if (profile) {
+            const profileName = profile.get?.('name') || profile.name || '未知';
+            console.log(`  ${index + 1}. ${productName}: profile = ${profileName}`);
+          } else {
+            console.log(`  ${index + 1}. ${productName}: 无 profile`);
+          }
+        });
+
+        console.log(`📊 项目 ${projectName} 共有 ${products.length} 个空间(Product)`);
+      } catch (productError: any) {
+        // ✅ 容错处理:Product 查询失败时,记录警告但继续处理
+        console.warn(`⚠️ Product 查询失败,将使用空的空间列表:`, productError.message || productError.toString());
+        console.warn(`⚠️ Product 错误详情:`, productError);
+        products = []; // 查询失败时使用空数组
+      }
 
       // 3. 去重:按空间名称去重(忽略大小写和首尾空格)
       const uniqueProducts = this.deduplicateProducts(products);
@@ -155,8 +179,15 @@ export class ProjectSpaceDeliverableService {
         postProcess: 0
       };
 
+      // ✅ 应用方案:优化性能,一次性查询所有交付文件
+      const allDeliveryFiles = await this.projectFileService.getProjectFiles(projectId, {
+        stage: 'delivery'
+      });
+      console.log(`📊 项目 ${projectName} 共有 ${allDeliveryFiles.length} 个交付文件`);
+
       for (const product of uniqueProducts) {
-        const spaceInfo = await this.getSpaceDeliverableInfo(projectId, product);
+        // ✅ 应用方案:传入所有文件列表,避免重复查询
+        const spaceInfo = await this.getSpaceDeliverableInfo(projectId, product, allDeliveryFiles);
         spaceInfos.push(spaceInfo);
 
         // 累加统计
@@ -174,8 +205,8 @@ export class ProjectSpaceDeliverableService {
       // 5. 计算整体完成率
       const overallCompletionRate = this.calculateOverallCompletionRate(spaceInfos);
 
-      // 🆕 6. 计算各阶段进度详情
-      const phaseProgress = this.calculatePhaseProgress(spaceInfos, project);
+      // ✅ 应用方案:计算各阶段进度详情,传入 products 以获取负责人信息
+      const phaseProgress = this.calculatePhaseProgress(spaceInfos, project, uniqueProducts);
 
       return {
         projectId,
@@ -200,11 +231,13 @@ export class ProjectSpaceDeliverableService {
    * 
    * @param projectId 项目ID
    * @param product Product对象
+   * @param allDeliveryFiles 所有交付文件列表(可选,用于性能优化)
    * @returns 空间交付物信息
    */
   private async getSpaceDeliverableInfo(
     projectId: string,
-    product: FmodeObject
+    product: FmodeObject,
+    allDeliveryFiles?: FmodeObject[]
   ): Promise<SpaceDeliverableInfo> {
     const spaceId = product.id!;
     const spaceName = product.get('productName') || '未命名空间';
@@ -226,17 +259,59 @@ export class ProjectSpaceDeliverableService {
       postProcess: 0
     };
 
-    // 查询每种类型的交付文件
-    for (const [key, fileType] of Object.entries(deliveryTypeMap)) {
-      const files = await this.projectFileService.getProjectFiles(projectId, {
-        fileType: fileType,
+    // ✅ 应用方案:优化文件查询逻辑,支持 deliveryType 字段
+    // 如果已传入文件列表,直接使用;否则查询
+    let deliveryFiles: FmodeObject[];
+    if (allDeliveryFiles) {
+      deliveryFiles = allDeliveryFiles;
+    } else {
+      deliveryFiles = await this.projectFileService.getProjectFiles(projectId, {
         stage: 'delivery'
       });
+    }
 
-      // 过滤当前空间的文件
-      const spaceFiles = files.filter(file => {
-        const data = file.get('data');
-        return data?.productId === spaceId || data?.spaceId === spaceId;
+    // 定义交付类型映射(支持多种字段格式)
+    const deliveryTypeMappings = {
+      whiteModel: {
+        fileType: 'delivery_white_model',
+        deliveryType: ['white_model', 'delivery_white_model', 'whiteModel']
+      },
+      softDecor: {
+        fileType: 'delivery_soft_decor',
+        deliveryType: ['soft_decor', 'delivery_soft_decor', 'softDecor']
+      },
+      rendering: {
+        fileType: 'delivery_rendering',
+        deliveryType: ['rendering', 'delivery_rendering']
+      },
+      postProcess: {
+        fileType: 'delivery_post_process',
+        deliveryType: ['post_process', 'delivery_post_process', 'postProcess']
+      }
+    };
+
+    // 统计各类型文件数量
+    for (const [key, mapping] of Object.entries(deliveryTypeMappings)) {
+      // 过滤当前空间的该类型文件
+      const spaceFiles = deliveryFiles.filter(file => {
+        const data = file.get('data') || {};
+        const fileType = file.get('fileType') || '';
+        const deliveryType = data.deliveryType || '';
+        const uploadStage = data.uploadStage || '';
+        
+        // ✅ 应用方案:优先使用 data.spaceId,其次使用 data.productId
+        const isSpaceMatch = data.spaceId === spaceId || data.productId === spaceId;
+        
+        // ✅ 应用方案:支持多种字段格式匹配
+        const isTypeMatch = 
+          fileType === mapping.fileType || 
+          mapping.deliveryType.includes(deliveryType) ||
+          mapping.deliveryType.includes(fileType);
+        
+        // ✅ 应用方案:确认是交付阶段文件(推荐检查 uploadStage)
+        const isDeliveryStage = uploadStage === 'delivery' || !uploadStage || fileType.includes('delivery');
+        
+        return isSpaceMatch && isTypeMatch && isDeliveryStage;
       });
 
       deliverableTypes[key as keyof typeof deliverableTypes] = spaceFiles.length;
@@ -302,19 +377,28 @@ export class ProjectSpaceDeliverableService {
   }
 
   /**
-   * 🆕 计算各阶段进度详情
+   * ✅ 应用方案:计算各阶段进度详情
    * 
    * @param spaceInfos 空间信息列表
    * @param project 项目对象
+   * @param products Product对象数组(可选,用于获取空间负责人)
    * @returns 各阶段进度信息
    */
   private calculatePhaseProgress(
     spaceInfos: SpaceDeliverableInfo[],
-    project: FmodeObject
+    project: FmodeObject,
+    products?: FmodeObject[]
   ): ProjectSpaceDeliverableSummary['phaseProgress'] {
     // 获取项目阶段截止信息中的负责人
     const projectData = project.get('data') || {};
     const phaseDeadlines = projectData.phaseDeadlines || {};
+    
+    // ✅ 🆕 获取 designerAssignmentStats 统计数据(主要数据源)
+    const projectDate = project.get('date') || {};
+    const designerAssignmentStats = projectDate.designerAssignmentStats || {};
+    const projectLeader = designerAssignmentStats.projectLeader || null;
+    const teamMembers = designerAssignmentStats.teamMembers || [];
+    const crossTeamCollaborators = designerAssignmentStats.crossTeamCollaborators || [];
 
     // 阶段映射:交付物类型 -> 阶段名称
     const phaseMap = {
@@ -342,6 +426,81 @@ export class ProjectSpaceDeliverableService {
 
     const result: any = {};
 
+    // ✅ 🆕 构建空间ID到负责人姓名的映射(优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee)
+    const spaceAssigneeMap = new Map<string, string>();
+    
+    // 优先级1:从 designerAssignmentStats 获取空间分配信息(最准确)
+    // 1.1 项目负责人的空间分配
+    if (projectLeader && projectLeader.assignedSpaces && Array.isArray(projectLeader.assignedSpaces)) {
+      projectLeader.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
+        if (space.id && projectLeader.name) {
+          spaceAssigneeMap.set(space.id, projectLeader.name);
+          console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${projectLeader.name} (项目负责人)`);
+        }
+      });
+    }
+    
+    // 1.2 团队成员的空间分配
+    teamMembers.forEach((member: { id: string; name: string; isProjectLeader?: boolean; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
+      if (member.assignedSpaces && Array.isArray(member.assignedSpaces) && member.name) {
+        member.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
+          if (space.id && !spaceAssigneeMap.has(space.id)) {
+            // 如果该空间还没有分配负责人,则使用当前成员
+            spaceAssigneeMap.set(space.id, member.name);
+            console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${member.name}${member.isProjectLeader ? ' (项目负责人)' : ''}`);
+          }
+        });
+      }
+    });
+    
+    // 1.3 跨组合作者的空间分配
+    crossTeamCollaborators.forEach((collaborator: { id: string; name: string; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
+      if (collaborator.assignedSpaces && Array.isArray(collaborator.assignedSpaces) && collaborator.name) {
+        collaborator.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
+          if (space.id && !spaceAssigneeMap.has(space.id)) {
+            spaceAssigneeMap.set(space.id, collaborator.name);
+            console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${collaborator.name} (跨组合作)`);
+          }
+        });
+      }
+    });
+    
+    // 优先级2:从 Product.profile 获取空间负责人(如果 designerAssignmentStats 中没有)
+    if (products) {
+      products.forEach(product => {
+        const spaceId = product.id!;
+        // 如果该空间还没有分配负责人,才尝试使用 Product.profile
+        if (!spaceAssigneeMap.has(spaceId)) {
+          const profile = product.get('profile');
+          if (profile) {
+            // ✅ 优化:支持多种方式获取设计师姓名
+            let profileName: string | undefined;
+            
+            // 方式1:profile 是 Parse Object,使用 get('name')
+            if (profile.get && typeof profile.get === 'function') {
+              profileName = profile.get('name') || profile.get('username') || '';
+            }
+            // 方式2:profile 是普通对象,直接访问 name 属性
+            else if (profile.name) {
+              profileName = profile.name;
+            }
+            // 方式3:profile 是字符串(ID),需要查询(暂时跳过,性能考虑)
+            else if (typeof profile === 'string') {
+              // 可以后续实现查询逻辑
+              profileName = undefined;
+            }
+            
+            if (profileName) {
+              spaceAssigneeMap.set(spaceId, profileName);
+              console.log(`📊 [Product.profile] 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人: ${profileName}`);
+            } else {
+              console.warn(`⚠️ 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人信息无法获取`, profile);
+            }
+          }
+        }
+      });
+    }
+
     // 计算每个阶段的进度
     Object.entries(phaseMap).forEach(([phaseKey, phaseConfig]) => {
       const requiredSpaces = spaceInfos.length; // 假设所有空间都需要各阶段交付物
@@ -353,16 +512,27 @@ export class ProjectSpaceDeliverableService {
         assignee?: string;
       }> = [];
 
-      // 获取阶段负责人
+      // ✅ 优先级3:获取阶段负责人信息(作为最后备选)
       const phaseInfo = phaseDeadlines[phaseKey];
       const assignee = phaseInfo?.assignee;
-      let assigneeName: string | undefined;
+      let phaseAssigneeName: string | undefined;
       
-      if (assignee && assignee.objectId) {
-        // 如果有负责人指针,可以在这里查询(为了性能,暂时不实时查询)
-        assigneeName = '待查询';
+      if (assignee) {
+        // 如果 assignee 是对象(Parse Pointer)
+        if (assignee.objectId) {
+          // 为了性能,暂时不实时查询,但保留接口
+          phaseAssigneeName = undefined; // 可以后续实现查询逻辑
+        } 
+        // 如果 assignee 是字符串(ID)
+        else if (typeof assignee === 'string') {
+          phaseAssigneeName = undefined; // 可以后续实现查询逻辑
+        }
+        // 如果 assignee 是对象且包含 name 字段
+        else if (assignee.name) {
+          phaseAssigneeName = assignee.name;
+        }
       }
-
+      
       spaceInfos.forEach(space => {
         const fileCount = space.deliverableTypes[phaseConfig.key];
         totalFiles += fileCount;
@@ -370,10 +540,26 @@ export class ProjectSpaceDeliverableService {
         if (fileCount > 0) {
           completedSpaces++;
         } else {
+          // ✅ 应用方案:未完成空间列表,按优先级获取负责人
+          // 优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee
+          let spaceAssignee = spaceAssigneeMap.get(space.spaceId);
+          
+          // 如果找不到,尝试使用阶段负责人
+          if (!spaceAssignee) {
+            spaceAssignee = phaseAssigneeName;
+          }
+          
+          // 如果还是没有,设置为"未分配"
+          if (!spaceAssignee) {
+            spaceAssignee = '未分配';
+          }
+          
+          console.log(`📊 未完成空间: ${space.spaceName} (ID: ${space.spaceId}), 负责人: ${spaceAssignee}`);
+          
           incompleteSpaces.push({
             spaceId: space.spaceId,
             spaceName: space.spaceName,
-            assignee: assigneeName
+            assignee: spaceAssignee
           });
         }
       });

+ 392 - 0
未完成空间列表显示设计师名字优化总结.md

@@ -0,0 +1,392 @@
+# 未完成空间列表显示设计师名字优化总结
+
+**优化日期**: 2025-11-13  
+**需求**: 在项目进度详情弹窗的"未完成空间列表"中,为每个未完成的空间右侧显示负责的设计师名字  
+**修改文件**: 
+- `src/modules/project/services/project-space-deliverable.service.ts`
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.html`
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.scss`
+
+---
+
+## ✅ 已实现的优化
+
+### 1. 优化设计师信息获取逻辑
+
+#### 1.1 支持多种方式获取设计师姓名
+
+**问题**: 原代码只支持一种方式获取 `profile.name`,可能无法正确获取设计师信息
+
+**修复**: 支持多种方式获取设计师姓名
+
+```typescript
+// ✅ 优化:支持多种方式获取设计师姓名
+let profileName: string | undefined;
+
+// 方式1:profile 是 Parse Object,使用 get('name')
+if (profile.get && typeof profile.get === 'function') {
+  profileName = profile.get('name') || profile.get('username') || '';
+}
+// 方式2:profile 是普通对象,直接访问 name 属性
+else if (profile.name) {
+  profileName = profile.name;
+}
+// 方式3:profile 是字符串(ID),需要查询(暂时跳过,性能考虑)
+else if (typeof profile === 'string') {
+  // 可以后续实现查询逻辑
+  profileName = undefined;
+}
+```
+
+**效果**: 
+- ✅ 兼容 Parse Object 格式的 profile
+- ✅ 兼容普通对象格式的 profile
+- ✅ 为未来支持字符串 ID 格式预留接口
+
+---
+
+### 2. 添加调试日志
+
+#### 2.1 查询 Product 时检查 profile 字段
+
+**新增**: 在查询 Product 后,检查每个 Product 的 profile 字段是否正确加载
+
+```typescript
+// ✅ 调试:检查 profile 字段是否正确加载
+console.log(`📊 查询到 ${products.length} 个 Product,检查 profile 字段:`);
+products.forEach((product, index) => {
+  const profile = product.get('profile');
+  const productName = product.get('productName') || '未命名';
+  if (profile) {
+    const profileName = profile.get?.('name') || profile.name || '未知';
+    console.log(`  ${index + 1}. ${productName}: profile = ${profileName}`);
+  } else {
+    console.log(`  ${index + 1}. ${productName}: 无 profile`);
+  }
+});
+```
+
+#### 2.2 记录空间负责人信息
+
+**新增**: 在设置空间负责人时,记录日志
+
+```typescript
+if (profileName) {
+  spaceAssigneeMap.set(product.id!, profileName);
+  console.log(`📊 空间 ${product.get('productName')} 的负责人: ${profileName}`);
+} else {
+  console.warn(`⚠️ 空间 ${product.get('productName')} 的负责人信息无法获取`, profile);
+}
+```
+
+**效果**: 
+- ✅ 方便排查数据问题
+- ✅ 确认 profile 字段是否正确加载
+- ✅ 确认空间负责人是否正确设置
+
+---
+
+### 3. 优化 HTML 模板结构
+
+#### 3.1 添加设计师信息包装器
+
+**优化**: 添加 `assignee-wrapper` 确保设计师名字显示在右侧
+
+```html
+<!-- 修复前 -->
+<span class="assignee" *ngIf="space.assignee && space.assignee !== '未分配'">
+  <span class="assignee-icon">👤</span>
+  <span class="assignee-name">{{ space.assignee }}</span>
+</span>
+
+<!-- 修复后 -->
+<span class="assignee-wrapper">
+  <span class="assignee" *ngIf="space.assignee && space.assignee !== '未分配'">
+    <span class="assignee-icon">👤</span>
+    <span class="assignee-name">{{ space.assignee }}</span>
+  </span>
+  <span class="assignee unassigned-badge" *ngIf="!space.assignee || space.assignee === '未分配'">
+    <span class="warning-icon-small">⚠️</span>
+    <span>未分配</span>
+  </span>
+</span>
+```
+
+**效果**: 
+- ✅ 设计师名字始终显示在右侧
+- ✅ 布局更加清晰
+- ✅ 支持未分配状态的显示
+
+---
+
+### 4. 优化样式布局
+
+#### 4.1 右侧对齐设计师信息
+
+**优化**: 使用 `margin-left: auto` 将设计师信息推送到右侧
+
+```scss
+// ✅ 优化:设计师信息包装器,确保右侧对齐
+.assignee-wrapper {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  margin-left: auto; // 推送到右侧
+}
+```
+
+#### 4.2 优化空间名称显示
+
+**优化**: 允许空间名称文本截断,避免挤压设计师信息
+
+```scss
+.space-name {
+  flex: 1;
+  font-size: 14px;
+  color: #333;
+  font-weight: 500;
+  min-width: 0; // 允许文本截断
+}
+```
+
+#### 4.3 优化设计师标签样式
+
+**优化**: 确保设计师标签不换行,保持整洁
+
+```scss
+.assignee {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  color: #1976d2;
+  background: #e3f2fd;
+  padding: 4px 10px;
+  border-radius: 12px;
+  font-weight: 600;
+  white-space: nowrap; // 不换行
+  min-width: fit-content;
+}
+```
+
+**效果**: 
+- ✅ 设计师名字始终在右侧对齐
+- ✅ 空间名称可以截断,不会挤压设计师信息
+- ✅ 布局更加美观和清晰
+
+---
+
+## 📊 显示效果
+
+### 有设计师的空间
+
+```
+┌─────────────────────────────────────────────────┐
+│ 📦 门厅                              👤 张三    │
+│ 📦 主要空间                          👤 李四    │
+│ 📦 辅助空间                          👤 王五    │
+│ 📦 办公区                            👤 赵六    │
+└─────────────────────────────────────────────────┘
+```
+
+### 未分配的空间
+
+```
+┌─────────────────────────────────────────────────┐
+│ 📦 门厅                              ⚠️ 未分配  │
+│ 📦 主要空间                          ⚠️ 未分配  │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+## 🔍 字段使用说明
+
+### 使用的字段
+
+| 字段路径 | 说明 | 优先级 |
+|---------|------|--------|
+| `Product.profile.name` | 空间负责人(设计师)姓名 | 1(最高) |
+| `Product.profile.username` | 空间负责人用户名(降级) | 2 |
+| `Project.data.phaseDeadlines[phase].assignee.name` | 阶段负责人姓名 | 3(降级) |
+
+### 数据流程
+
+```
+查询 Product
+  ↓
+include('profile') 包含 profile 字段
+  ↓
+获取 profile.get('name') 或 profile.name
+  ↓
+存储到 spaceAssigneeMap
+  ↓
+填充到 incompleteSpaces[].assignee
+  ↓
+在模板中显示
+```
+
+---
+
+## 🧪 测试建议
+
+### 1. 功能测试
+
+- [ ] 测试有 profile 的空间显示设计师名字
+- [ ] 测试没有 profile 的空间显示"未分配"
+- [ ] 测试 profile.name 为空的情况
+- [ ] 测试 profile 是 Parse Object 的情况
+- [ ] 测试 profile 是普通对象的情况
+
+### 2. 布局测试
+
+- [ ] 测试空间名称较长时的显示
+- [ ] 测试设计师名字较长时的显示
+- [ ] 测试响应式布局(不同屏幕尺寸)
+- [ ] 测试未分配状态的显示
+
+### 3. 数据测试
+
+- [ ] 检查控制台日志,确认 profile 字段正确加载
+- [ ] 检查控制台日志,确认空间负责人正确设置
+- [ ] 测试多个空间的情况
+- [ ] 测试所有空间都没有 profile 的情况
+
+---
+
+## ⚠️ 注意事项
+
+### 1. 数据依赖
+
+- ✅ 依赖 `Product.profile` 字段
+- ✅ 依赖 `profile.name` 或 `profile.username` 字段
+- ⚠️ 如果 `profile` 为空,会降级使用阶段负责人
+- ⚠️ 如果阶段负责人也为空,会显示"未分配"
+
+### 2. 性能考虑
+
+- ✅ 使用 `include('profile')` 一次性加载所有 profile
+- ✅ 使用 Map 数据结构,查找性能良好
+- ⚠️ 如果 profile 是字符串 ID,暂时不查询(性能考虑)
+
+### 3. 兼容性
+
+- ✅ 兼容 Parse Object 格式的 profile
+- ✅ 兼容普通对象格式的 profile
+- ✅ 兼容没有 profile 的情况
+- ⚠️ 暂时不支持 profile 是字符串 ID 的情况(需要额外查询)
+
+---
+
+## 🚀 后续优化建议
+
+### 阶段一:功能增强(优先级:高)
+
+1. ⚠️ 支持 profile 是字符串 ID 的情况(需要查询 Profile 表)
+2. ⚠️ 添加点击设计师名字跳转到设计师详情页
+3. ⚠️ 添加快速分配设计师功能
+
+### 阶段二:性能优化(优先级:中)
+
+1. ⚠️ 批量查询 Profile(如果 profile 是 ID)
+2. ⚠️ 添加缓存机制,避免重复查询
+3. ⚠️ 优化查询逻辑,减少数据库查询次数
+
+### 阶段三:视觉优化(优先级:低)
+
+1. ⚠️ 添加设计师头像显示
+2. ⚠️ 添加设计师状态指示(在线/离线)
+3. ⚠️ 添加悬停提示(显示设计师详细信息)
+
+---
+
+## 📝 代码变更总结
+
+### 1. 服务层 (`project-space-deliverable.service.ts`)
+
+**变更**:
+- ✅ 优化 `calculatePhaseProgress` 方法中的 profile 获取逻辑
+- ✅ 添加调试日志,检查 profile 字段
+- ✅ 支持多种方式获取设计师姓名
+
+**关键代码**:
+```typescript
+// 方式1:profile 是 Parse Object
+if (profile.get && typeof profile.get === 'function') {
+  profileName = profile.get('name') || profile.get('username') || '';
+}
+// 方式2:profile 是普通对象
+else if (profile.name) {
+  profileName = profile.name;
+}
+```
+
+### 2. 模板层 (`project-progress-modal.html`)
+
+**变更**:
+- ✅ 添加 `assignee-wrapper` 包装器
+- ✅ 优化设计师信息显示结构
+
+**关键代码**:
+```html
+<span class="assignee-wrapper">
+  <span class="assignee" *ngIf="space.assignee && space.assignee !== '未分配'">
+    <span class="assignee-icon">👤</span>
+    <span class="assignee-name">{{ space.assignee }}</span>
+  </span>
+  <span class="assignee unassigned-badge" *ngIf="!space.assignee || space.assignee === '未分配'">
+    <span class="warning-icon-small">⚠️</span>
+    <span>未分配</span>
+  </span>
+</span>
+```
+
+### 3. 样式层 (`project-progress-modal.scss`)
+
+**变更**:
+- ✅ 添加 `assignee-wrapper` 样式
+- ✅ 优化空间名称显示(允许截断)
+- ✅ 优化设计师标签样式(不换行)
+
+**关键代码**:
+```scss
+.assignee-wrapper {
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
+  margin-left: auto; // 推送到右侧
+}
+```
+
+---
+
+## 📌 总结
+
+### 关键改进
+
+1. **数据获取优化**
+   - ✅ 支持多种方式获取设计师姓名
+   - ✅ 添加调试日志,方便排查问题
+
+2. **布局优化**
+   - ✅ 设计师名字始终显示在右侧
+   - ✅ 空间名称可以截断,不会挤压设计师信息
+
+3. **用户体验**
+   - ✅ 清晰显示每个空间的负责人
+   - ✅ 未分配状态有明确的视觉提示
+
+### 效果对比
+
+| 指标 | 优化前 | 优化后 |
+|------|--------|--------|
+| 设计师名字显示 | ⚠️ 可能不显示 | ✅ 始终显示 |
+| 布局对齐 | ⚠️ 可能不对齐 | ✅ 右侧对齐 |
+| 数据获取 | ⚠️ 单一方式 | ✅ 多种方式 |
+| 调试能力 | ❌ 无日志 | ✅ 详细日志 |
+
+---
+
+**文档维护**: 本文档应随功能变更及时更新  
+**最后更新**: 2025-11-13
+

+ 222 - 0
组长端项目负载时间轴字段应用总结.md

@@ -0,0 +1,222 @@
+# 组长端项目负载时间轴字段应用总结
+
+**应用日期**: 2025-11-13  
+**基于方案**: 组长端项目负载时间轴字段需求方案.md  
+**修改文件**: `src/app/pages/team-leader/dashboard/dashboard.ts`
+
+---
+
+## ✅ 已应用的更改
+
+### 1. 数据加载增强
+
+#### 1.1 `loadDesignerWorkload()` 方法
+- ✅ 添加了 `data` 字段的获取:`project.get('data')`
+- ✅ 添加了 `updatedAt` 字段:用于计算停滞状态
+- ✅ 添加了 `designerId` 字段:用于设计师关联
+- ✅ 添加了 `contact` 字段:用于客户信息
+- ✅ 添加了 `space` 字段:从 `data.quotation.spaces[0].name` 提取
+
+#### 1.2 `loadDesignerWorkloadFromProjects()` 方法
+- ✅ 同样添加了上述字段的获取逻辑
+
+---
+
+### 2. 时间轴数据转换优化
+
+#### 2.1 移除测试数据逻辑
+- ✅ 移除了 `dayMapping` 测试数据映射
+- ✅ 移除了基于索引的日期计算逻辑
+- ✅ 改为使用真实项目数据
+
+#### 2.2 真实时间节点获取
+
+**项目开始时间** (`realStartDate`):
+```typescript
+优先级顺序:
+1. projectData.phaseDeadlines.modeling.startDate (最高优先级)
+2. projectData.requirementsConfirmedAt
+3. project.createdAt
+4. 当前时间 (降级)
+```
+
+**项目结束时间** (`realEndDate`):
+```typescript
+优先级顺序:
+1. project.deadline (最高优先级)
+2. realStartDate + 7天 (降级)
+```
+
+**对图时间** (`realReviewDate`):
+```typescript
+优先级顺序:
+1. project.demoday (最高优先级)
+2. projectData.phaseDeadlines.softDecor.deadline
+3. 计算值:项目周期的 60% 位置 (降级)
+```
+
+---
+
+### 3. 阶段进度计算
+
+#### 3.1 基于 `phaseDeadlines` 的真实进度计算
+- ✅ 根据当前阶段 (`currentStage`) 选择对应的阶段数据
+- ✅ 使用阶段的实际开始时间和截止时间计算进度
+- ✅ 降级方案:如果没有 `phaseDeadlines`,使用整体项目进度
+
+**支持的阶段**:
+- `model` → `phaseDeadlines.modeling`
+- `decoration` → `phaseDeadlines.softDecor`
+- `render` → `phaseDeadlines.rendering`
+- `delivery` → `phaseDeadlines.postProcessing`
+
+---
+
+### 4. 停滞状态计算
+
+#### 4.1 基于 `updatedAt` 的真实停滞判断
+```typescript
+- 如果超过7天未更新,认为停滞
+- stalledDays = 距离最后更新的天数
+- isStalled = stalledDays > 7
+```
+
+---
+
+### 5. 阶段截止时间处理
+
+#### 5.1 使用真实 `phaseDeadlines`
+- ✅ 优先使用 `project.data.phaseDeadlines`
+- ✅ 如果不存在,从交付日期往前推算
+- ✅ 按比例分配:建模30%,软装25%,渲染30%,后期15%
+
+#### 5.2 阶段状态计算
+- ✅ 根据当前时间和阶段截止时间计算状态
+- ✅ 状态值:`'not_started' | 'in_progress' | 'completed'`
+
+---
+
+### 6. 返回数据增强
+
+#### 6.1 时间轴对象字段
+```typescript
+{
+  projectId: string,
+  projectName: string,
+  designerId: string,        // ✅ 新增
+  designerName: string,
+  startDate: Date,          // ✅ 使用真实开始时间
+  endDate: Date,             // ✅ 使用真实结束时间
+  deliveryDate: Date,       // ✅ 使用真实交付日期
+  reviewDate: Date,         // ✅ 使用真实对图时间
+  currentStage: string,
+  stageName: string,
+  stageProgress: number,     // ✅ 基于真实数据计算
+  status: string,            // ✅ 基于真实日期计算
+  isStalled: boolean,        // ✅ 基于 updatedAt 计算
+  stalledDays: number,       // ✅ 真实的停滞天数
+  urgentCount: number,
+  priority: string,
+  spaceName: string,         // ✅ 从项目数据获取
+  customerName: string,       // ✅ 从项目数据获取
+  phaseDeadlines: object,    // ✅ 使用真实或计算的阶段数据
+  data: object               // ✅ 保留原始数据
+}
+```
+
+---
+
+## 📊 字段映射关系
+
+### 已实现的字段映射
+
+| 功能点 | 字段路径 | 状态 |
+|--------|---------|------|
+| 项目开始时间 | `data.phaseDeadlines.modeling.startDate` | ✅ 已应用 |
+| 需求确认时间 | `data.requirementsConfirmedAt` | ✅ 已应用 |
+| 交付日期 | `project.deadline` | ✅ 已应用 |
+| 对图时间 | `project.demoday` | ✅ 已应用 |
+| 阶段截止时间 | `data.phaseDeadlines` | ✅ 已应用 |
+| 阶段进度 | 基于 `phaseDeadlines` 计算 | ✅ 已应用 |
+| 停滞状态 | 基于 `project.updatedAt` 计算 | ✅ 已应用 |
+| 空间信息 | `data.quotation.spaces[0].name` | ✅ 已应用 |
+| 客户信息 | `project.contact.name` | ✅ 已应用 |
+
+---
+
+## 🔄 降级处理方案
+
+### 1. 时间节点降级
+- **开始时间**: `phaseDeadlines` → `requirementsConfirmedAt` → `createdAt` → 当前时间
+- **结束时间**: `deadline` → `startDate + 7天`
+- **对图时间**: `demoday` → `phaseDeadlines.softDecor.deadline` → 计算值
+
+### 2. 阶段进度降级
+- **有 phaseDeadlines**: 使用对应阶段的真实进度
+- **无 phaseDeadlines**: 使用整体项目进度(基于开始和结束时间)
+
+### 3. 阶段截止时间降级
+- **有 phaseDeadlines**: 直接使用
+- **无 phaseDeadlines**: 从交付日期往前推算,按比例分配
+
+---
+
+## ⚠️ 注意事项
+
+### 1. 数据完整性
+- 确保项目数据中包含 `data` 字段
+- 确保 `data.phaseDeadlines` 在确认需求后已初始化
+- 确保 `data.deliveryStageStatus` 在交付执行阶段已更新
+
+### 2. 性能考虑
+- 数据加载时已包含 `data` 字段,避免额外查询
+- 使用缓存机制减少重复计算
+
+### 3. 兼容性
+- 保留了降级处理逻辑,确保旧数据也能正常显示
+- 新字段缺失时使用合理的默认值
+
+---
+
+## 🚀 后续优化建议
+
+### 1. 数据补全
+- [ ] 为旧项目补充 `phaseDeadlines` 数据
+- [ ] 为旧项目补充 `deliveryStageStatus` 数据
+- [ ] 确保所有项目都有 `requirementsConfirmedAt` 时间
+
+### 2. 功能增强
+- [ ] 使用 `deliveryStageStatus` 显示各子阶段的真实状态
+- [ ] 使用 `spaceDeliverableSummary` 显示空间交付物统计
+- [ ] 添加阶段状态的可视化展示
+
+### 3. 性能优化
+- [ ] 缓存空间交付物统计数据
+- [ ] 优化查询性能,减少数据库请求
+- [ ] 添加数据更新监听,实时刷新时间轴
+
+---
+
+## 📝 测试建议
+
+### 1. 功能测试
+- [ ] 测试有完整 `phaseDeadlines` 的项目显示
+- [ ] 测试只有 `deadline` 的项目显示(降级场景)
+- [ ] 测试停滞状态的计算准确性
+- [ ] 测试阶段进度的计算准确性
+
+### 2. 数据测试
+- [ ] 测试不同阶段的项目显示
+- [ ] 测试有/无 `demoday` 的项目显示
+- [ ] 测试有/无 `space` 信息的项目显示
+
+### 3. 边界测试
+- [ ] 测试没有 `data` 字段的项目
+- [ ] 测试没有 `deadline` 的项目
+- [ ] 测试没有 `createdAt` 的项目
+
+---
+
+**文档维护**: 本文档应随代码变更及时更新  
+**最后更新**: 2025-11-13
+

+ 576 - 0
组长端项目负载时间轴字段需求方案.md

@@ -0,0 +1,576 @@
+# 组长端项目负载时间轴字段需求方案
+
+**分析日期**: 2025-11-13  
+**基于 Git Commit**: 56ce1468fe45ff0a928bf500a9d331dfe8c9bb2c  
+**文档版本**: v1.0
+
+---
+
+## 📋 概述
+
+本文档基于 Git commit `56ce1468fe45ff0a928bf500a9d331dfe8c9bb2c` 中更新的字段内容,分析组长端项目负载时间轴图的所有功能实现所需的数据字段。
+
+---
+
+## 🔍 Git Commit 更新的字段内容
+
+### 1. 确认需求阶段字段
+
+#### 1.1 需求确认时间字段
+**位置**: `Project.data.requirementsConfirmed*`
+
+```typescript
+data.requirementsConfirmed = true;                          // 是否已确认需求
+data.requirementsConfirmedBy = string;                     // 确认人ID
+data.requirementsConfirmedByName = string;                // 确认人姓名
+data.requirementsConfirmedAt = string;                     // 确认时间 (ISO字符串)
+```
+
+**用途**: 标记需求确认完成时间,作为项目时间轴的起始节点
+
+---
+
+### 2. 交付执行阶段字段
+
+#### 2.1 阶段标识符
+**位置**: `Project.data.deliveryStageStatus` 对象的键
+
+```typescript
+// 四个子阶段标识符
+'white_model'    // 白模阶段
+'soft_decor'     // 软装阶段
+'rendering'      // 渲染阶段
+'post_process'   // 后期阶段
+```
+
+#### 2.2 阶段状态字段
+**位置**: `Project.data.deliveryStageStatus[stageId]`
+
+```typescript
+deliveryStageStatus: {
+  [stageId: string]: {
+    status: 'not_started' | 'in_progress' | 'completed' | 'approved' | 'rejected';
+    startedAt?: string;        // 开始时间 (ISO字符串)
+    completedAt?: string;      // 完成时间 (ISO字符串)
+    approvedAt?: string;       // 审批通过时间 (ISO字符串)
+    rejectedAt?: string;       // 审批驳回时间 (ISO字符串)
+    approvedBy?: string;       // 审批人ID
+    rejectionReason?: string;  // 驳回原因
+  }
+}
+```
+
+**用途**: 跟踪每个交付子阶段的详细状态和时间节点
+
+---
+
+### 3. 阶段截止时间字段
+
+#### 3.1 阶段截止时间结构
+**位置**: `Project.data.phaseDeadlines`
+
+```typescript
+data.phaseDeadlines = {
+  modeling: {
+    startDate: Date;                    // 阶段开始日期
+    deadline: Date;                     // 阶段截止日期
+    estimatedDays: number;              // 预计天数
+    status: 'not_started' | 'in_progress' | 'completed';
+    priority: 'high' | 'medium' | 'low';
+  };
+  softDecor: { /* 同上结构 */ };
+  rendering: { /* 同上结构 */ };
+  postProcessing: { /* 同上结构 */ };
+}
+```
+
+**用途**: 为时间轴提供各阶段的计划时间范围,用于可视化展示
+
+---
+
+### 4. 空间交付物汇总字段
+
+#### 4.1 空间交付物汇总结构
+**位置**: `Project.data.spaceDeliverableSummary`
+
+```typescript
+data.spaceDeliverableSummary = {
+  [productId: string]: {
+    spaceName: string;                 // 空间名称
+    totalDeliverables: number;         // 总交付物数量
+    completedDeliverables: number;     // 已完成交付物数量
+    completionRate: number;             // 完成率 (0-100)
+    lastUpdateTime: string;            // 最后更新时间 (ISO字符串)
+    phaseProgress: {
+      white_model: number;             // 0-100 (审批通过=100, 待审批=80, 驳回=50)
+      soft_decor: number;
+      rendering: number;
+      post_process: number;
+    };
+  };
+  overallCompletionRate: number;       // 整体完成率 (0-100)
+}
+```
+
+**用途**: 展示每个空间的交付物完成情况,用于进度详情弹窗
+
+---
+
+### 5. 需求确认详细信息字段
+
+#### 5.1 需求详情结构
+**位置**: `Project.data.requirementsDetail`
+
+```typescript
+data.requirementsDetail = {
+  globalRequirements: object;          // 全局需求
+  spaceRequirements: object[];         // 空间需求
+  crossSpaceRequirements: object[];    // 跨空间需求
+  referenceImages: object[];           // 参考图片信息
+  cadFiles: object[];                 // CAD文件信息
+  aiAnalysisResults: object;           // AI分析结果
+  confirmedAt: string;                // 确认时间 (ISO字符串)
+}
+```
+
+**用途**: 记录需求确认时的完整信息,用于追溯和展示
+
+---
+
+### 6. ProjectFile 扩展字段
+
+#### 6.1 文件关联信息
+**位置**: `ProjectFile.data`
+
+```typescript
+data.spaceId = string;                // 关联的空间/产品ID
+data.deliveryType = string;            // 交付类型标识
+data.uploadedFor = string;             // 上传用途
+data.uploadStage = string;             // 上传阶段 ('requirements' | 'delivery')
+data.approvalStatus = string;          // 审批状态 ('unverified' | 'pending' | 'approved' | 'rejected')
+```
+
+**用途**: 关联文件与空间、阶段的关系,用于统计和展示
+
+---
+
+## 🎯 组长端项目负载时间轴功能需求
+
+### 功能清单
+
+1. **项目列表展示**
+   - 项目名称
+   - 设计师信息
+   - 客户信息
+   - 空间信息
+
+2. **时间轴可视化**
+   - 项目开始时间
+   - 项目结束时间
+   - 交付日期
+   - 对图时间(demoday)
+   - 各阶段时间节点
+
+3. **阶段进度展示**
+   - 当前阶段
+   - 阶段进度百分比
+   - 各子阶段状态
+
+4. **状态标识**
+   - 项目状态(正常/警告/紧急/逾期)
+   - 是否停滞
+   - 停滞天数
+   - 催办次数
+
+5. **优先级展示**
+   - 优先级等级(低/中/高/紧急)
+
+6. **筛选和排序**
+   - 按设计师筛选
+   - 按状态筛选
+   - 按优先级筛选
+   - 按时间排序
+
+7. **进度详情弹窗**
+   - 空间数量统计
+   - 交付物完成情况
+   - 各阶段完成率
+
+---
+
+## 📊 字段映射关系
+
+### 时间轴展示所需字段
+
+| 功能点 | 所需字段 | 数据来源 | 字段路径 |
+|--------|---------|---------|---------|
+| **项目基本信息** |
+| 项目ID | `projectId` | Project | `project.id` |
+| 项目名称 | `projectName` | Project | `project.title` |
+| 设计师ID | `designerId` | Project | `project.assignee.id` |
+| 设计师名称 | `designerName` | Project | `project.assignee.name` |
+| 客户名称 | `customerName` | Project | `project.contact.name` |
+| 空间名称 | `spaceName` | Project | `project.data.quotation.spaces[].name` |
+| **时间节点** |
+| 项目开始时间 | `startDate` | Project | `project.data.phaseDeadlines.modeling.startDate` |
+| 项目结束时间 | `endDate` | Project | `project.deadline` |
+| 交付日期 | `deliveryDate` | Project | `project.deadline` |
+| 对图时间 | `reviewDate` | Project | `project.demoday` 或 `project.data.phaseDeadlines.softDecor.deadline` |
+| **阶段信息** |
+| 当前阶段 | `currentStage` | Project | `project.currentStage` |
+| 阶段名称 | `stageName` | Project | `project.currentStage` (映射显示名称) |
+| 阶段进度 | `stageProgress` | 计算 | 基于 `phaseDeadlines` 和当前时间计算 |
+| **阶段截止时间** |
+| 建模阶段开始 | `phaseDeadlines.modeling.startDate` | Project | `project.data.phaseDeadlines.modeling.startDate` |
+| 建模阶段截止 | `phaseDeadlines.modeling.deadline` | Project | `project.data.phaseDeadlines.modeling.deadline` |
+| 软装阶段开始 | `phaseDeadlines.softDecor.startDate` | Project | `project.data.phaseDeadlines.softDecor.startDate` |
+| 软装阶段截止 | `phaseDeadlines.softDecor.deadline` | Project | `project.data.phaseDeadlines.softDecor.deadline` |
+| 渲染阶段开始 | `phaseDeadlines.rendering.startDate` | Project | `project.data.phaseDeadlines.rendering.startDate` |
+| 渲染阶段截止 | `phaseDeadlines.rendering.deadline` | Project | `project.data.phaseDeadlines.rendering.deadline` |
+| 后期阶段开始 | `phaseDeadlines.postProcessing.startDate` | Project | `project.data.phaseDeadlines.postProcessing.startDate` |
+| 后期阶段截止 | `phaseDeadlines.postProcessing.deadline` | Project | `project.data.phaseDeadlines.postProcessing.deadline` |
+| **交付阶段状态** |
+| 白模阶段状态 | `deliveryStageStatus.white_model.status` | Project | `project.data.deliveryStageStatus.white_model.status` |
+| 白模开始时间 | `deliveryStageStatus.white_model.startedAt` | Project | `project.data.deliveryStageStatus.white_model.startedAt` |
+| 白模完成时间 | `deliveryStageStatus.white_model.completedAt` | Project | `project.data.deliveryStageStatus.white_model.completedAt` |
+| 白模审批时间 | `deliveryStageStatus.white_model.approvedAt` | Project | `project.data.deliveryStageStatus.white_model.approvedAt` |
+| 软装阶段状态 | `deliveryStageStatus.soft_decor.status` | Project | `project.data.deliveryStageStatus.soft_decor.status` |
+| 软装开始时间 | `deliveryStageStatus.soft_decor.startedAt` | Project | `project.data.deliveryStageStatus.soft_decor.startedAt` |
+| 软装完成时间 | `deliveryStageStatus.soft_decor.completedAt` | Project | `project.data.deliveryStageStatus.soft_decor.completedAt` |
+| 软装审批时间 | `deliveryStageStatus.soft_decor.approvedAt` | Project | `project.data.deliveryStageStatus.soft_decor.approvedAt` |
+| 渲染阶段状态 | `deliveryStageStatus.rendering.status` | Project | `project.data.deliveryStageStatus.rendering.status` |
+| 渲染开始时间 | `deliveryStageStatus.rendering.startedAt` | Project | `project.data.deliveryStageStatus.rendering.startedAt` |
+| 渲染完成时间 | `deliveryStageStatus.rendering.completedAt` | Project | `project.data.deliveryStageStatus.rendering.completedAt` |
+| 渲染审批时间 | `deliveryStageStatus.rendering.approvedAt` | Project | `project.data.deliveryStageStatus.rendering.approvedAt` |
+| 后期阶段状态 | `deliveryStageStatus.post_process.status` | Project | `project.data.deliveryStageStatus.post_process.status` |
+| 后期开始时间 | `deliveryStageStatus.post_process.startedAt` | Project | `project.data.deliveryStageStatus.post_process.startedAt` |
+| 后期完成时间 | `deliveryStageStatus.post_process.completedAt` | Project | `project.data.deliveryStageStatus.post_process.completedAt` |
+| 后期审批时间 | `deliveryStageStatus.post_process.approvedAt` | Project | `project.data.deliveryStageStatus.post_process.approvedAt` |
+| **状态计算** |
+| 项目状态 | `status` | 计算 | 基于 `deadline` 和当前时间计算 |
+| 是否停滞 | `isStalled` | 计算 | 基于最后更新时间判断 |
+| 停滞天数 | `stalledDays` | 计算 | 基于最后更新时间计算 |
+| 催办次数 | `urgentCount` | 计算 | 基于状态和历史记录 |
+| 优先级 | `priority` | 计算 | 基于状态和截止时间 |
+| **进度详情** |
+| 空间总数 | `totalSpaces` | 计算 | `Object.keys(spaceDeliverableSummary).length` |
+| 已完成空间数 | `spacesWithDeliverables` | 计算 | 统计 `completionRate === 100` 的空间 |
+| 总交付物文件数 | `totalDeliverableFiles` | 计算 | 统计所有空间的 `totalDeliverables` |
+| 整体完成率 | `overallCompletionRate` | Project | `project.data.spaceDeliverableSummary.overallCompletionRate` |
+| 各空间完成情况 | `spaceDeliverableSummary` | Project | `project.data.spaceDeliverableSummary` |
+
+---
+
+## 🔧 字段获取和计算逻辑
+
+### 1. 项目基本信息获取
+
+```typescript
+// 从 Project 对象获取基本信息
+const projectId = project.id;
+const projectName = project.get('title');
+const designerId = project.get('assignee')?.id;
+const designerName = project.get('assignee')?.get('name');
+const customerName = project.get('contact')?.get('name');
+const spaceName = project.get('data')?.quotation?.spaces?.[0]?.name || '';
+```
+
+### 2. 时间节点获取
+
+```typescript
+const data = project.get('data') || {};
+
+// 项目开始时间:优先使用 phaseDeadlines.modeling.startDate
+const startDate = data.phaseDeadlines?.modeling?.startDate 
+  ? new Date(data.phaseDeadlines.modeling.startDate)
+  : project.get('createdAt') || new Date();
+
+// 项目结束时间:使用 deadline
+const endDate = project.get('deadline') 
+  ? new Date(project.get('deadline'))
+  : new Date();
+
+// 对图时间:优先使用 demoday,否则使用软装阶段截止时间
+const reviewDate = project.get('demoday')
+  ? new Date(project.get('demoday'))
+  : data.phaseDeadlines?.softDecor?.deadline
+  ? new Date(data.phaseDeadlines.softDecor.deadline)
+  : null;
+```
+
+### 3. 阶段信息获取
+
+```typescript
+const currentStage = project.get('currentStage') || 'modeling';
+
+// 阶段名称映射
+const stageNameMap = {
+  'plan': '方案设计',
+  'model': '建模阶段',
+  'decoration': '软装阶段',
+  'render': '渲染阶段',
+  'delivery': '交付阶段'
+};
+
+const stageName = stageNameMap[currentStage] || currentStage;
+
+// 阶段进度计算
+const phaseDeadlines = data.phaseDeadlines || {};
+const now = new Date();
+
+// 根据当前阶段和时间计算进度
+let stageProgress = 0;
+if (currentStage === 'model' && phaseDeadlines.modeling) {
+  const start = new Date(phaseDeadlines.modeling.startDate);
+  const end = new Date(phaseDeadlines.modeling.deadline);
+  const total = end.getTime() - start.getTime();
+  const elapsed = now.getTime() - start.getTime();
+  stageProgress = Math.min(100, Math.max(0, (elapsed / total) * 100));
+}
+// ... 其他阶段类似计算
+```
+
+### 4. 状态计算逻辑
+
+```typescript
+const deadline = project.get('deadline') ? new Date(project.get('deadline')) : null;
+const now = new Date();
+
+if (!deadline) {
+  status = 'normal';
+} else {
+  const daysUntilDeadline = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
+  
+  if (daysUntilDeadline < 0) {
+    status = 'overdue';        // 逾期
+    priority = 'critical';
+  } else if (daysUntilDeadline <= 1) {
+    status = 'urgent';         // 紧急(1天内)
+    priority = 'high';
+  } else if (daysUntilDeadline <= 3) {
+    status = 'warning';        // 警告(3天内)
+    priority = 'medium';
+  } else {
+    status = 'normal';         // 正常
+    priority = 'low';
+  }
+}
+```
+
+### 5. 停滞判断逻辑
+
+```typescript
+const updatedAt = project.get('updatedAt') ? new Date(project.get('updatedAt')) : null;
+const now = new Date();
+
+if (updatedAt) {
+  const daysSinceUpdate = Math.floor((now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24));
+  
+  // 如果超过7天未更新,认为停滞
+  isStalled = daysSinceUpdate > 7;
+  stalledDays = isStalled ? daysSinceUpdate : 0;
+} else {
+  isStalled = false;
+  stalledDays = 0;
+}
+```
+
+### 6. 空间交付物统计获取
+
+```typescript
+const spaceDeliverableSummary = data.spaceDeliverableSummary || {};
+
+// 计算统计数据
+const totalSpaces = Object.keys(spaceDeliverableSummary).filter(
+  key => key !== 'overallCompletionRate'
+).length;
+
+const spacesWithDeliverables = Object.values(spaceDeliverableSummary)
+  .filter((summary: any) => summary.completionRate === 100).length;
+
+const totalDeliverableFiles = Object.values(spaceDeliverableSummary)
+  .reduce((sum: number, summary: any) => {
+    return sum + (summary.totalDeliverables || 0);
+  }, 0);
+
+const overallCompletionRate = spaceDeliverableSummary.overallCompletionRate || 0;
+```
+
+---
+
+## 📝 必需字段清单
+
+### 核心必需字段(必须存在)
+
+1. **Project 基础字段**
+   - `id` - 项目ID
+   - `title` - 项目名称
+   - `deadline` - 交付日期
+   - `currentStage` - 当前阶段
+   - `assignee` - 设计师(Pointer)
+   - `contact` - 客户(Pointer)
+   - `createdAt` - 创建时间
+   - `updatedAt` - 更新时间
+
+2. **Project.data 必需字段**
+   - `phaseDeadlines` - 阶段截止时间(必需)
+   - `deliveryStageStatus` - 交付阶段状态(必需)
+   - `spaceDeliverableSummary` - 空间交付物汇总(必需)
+   - `requirementsConfirmedAt` - 需求确认时间(推荐)
+
+### 推荐字段(增强功能)
+
+1. **Project 推荐字段**
+   - `demoday` - 对图时间
+   - `status` - 项目状态
+
+2. **Project.data 推荐字段**
+   - `requirementsDetail` - 需求详情
+   - `quotation.spaces` - 空间列表
+
+3. **ProjectFile.data 推荐字段**
+   - `spaceId` - 关联空间ID
+   - `deliveryType` - 交付类型
+   - `approvalStatus` - 审批状态
+
+---
+
+## ⚠️ 字段缺失处理方案
+
+### 1. phaseDeadlines 缺失
+
+**处理方案**: 从 `deadline` 往前推算各阶段时间
+
+```typescript
+if (!data.phaseDeadlines && project.get('deadline')) {
+  const deliveryDate = new Date(project.get('deadline'));
+  const startDate = project.get('createdAt') || 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;
+  
+  // 生成 phaseDeadlines
+  // ...
+}
+```
+
+### 2. deliveryStageStatus 缺失
+
+**处理方案**: 根据 `currentStage` 推断状态
+
+```typescript
+if (!data.deliveryStageStatus) {
+  const currentStage = project.get('currentStage');
+  const stageMap = {
+    'modeling': 'white_model',
+    'decoration': 'soft_decor',
+    'rendering': 'rendering',
+    'postProduction': 'post_process'
+  };
+  
+  // 根据当前阶段设置状态
+  // ...
+}
+```
+
+### 3. spaceDeliverableSummary 缺失
+
+**处理方案**: 从 Product 表查询并初始化
+
+```typescript
+if (!data.spaceDeliverableSummary) {
+  // 查询项目的所有 Product
+  const products = await productSpaceService.getProjectProductSpaces(projectId);
+  
+  // 初始化汇总数据
+  data.spaceDeliverableSummary = {};
+  products.forEach(product => {
+    data.spaceDeliverableSummary[product.id] = {
+      spaceName: product.name,
+      totalDeliverables: 4,
+      completedDeliverables: 0,
+      completionRate: 0,
+      lastUpdateTime: new Date().toISOString(),
+      phaseProgress: {
+        white_model: 0,
+        soft_decor: 0,
+        rendering: 0,
+        post_process: 0
+      }
+    };
+  });
+}
+```
+
+---
+
+## 🚀 实施建议
+
+### 阶段一:基础字段验证(优先级:高)
+
+1. ✅ 验证 `phaseDeadlines` 字段是否在所有项目中存在
+2. ✅ 验证 `deliveryStageStatus` 字段是否在交付阶段项目中存在
+3. ✅ 验证 `spaceDeliverableSummary` 字段是否在确认需求后的项目中存在
+
+### 阶段二:数据补全(优先级:高)
+
+1. ✅ 为缺失 `phaseDeadlines` 的项目补充数据
+2. ✅ 为缺失 `deliveryStageStatus` 的项目补充数据
+3. ✅ 为缺失 `spaceDeliverableSummary` 的项目补充数据
+
+### 阶段三:功能增强(优先级:中)
+
+1. ✅ 优化时间轴展示,使用真实的时间节点
+2. ✅ 添加阶段状态的可视化展示
+3. ✅ 优化进度详情弹窗的数据展示
+
+### 阶段四:性能优化(优先级:低)
+
+1. ✅ 缓存空间交付物统计数据
+2. ✅ 优化查询性能,减少数据库请求
+3. ✅ 添加数据更新监听,实时刷新时间轴
+
+---
+
+## 📌 总结
+
+### 关键字段
+
+1. **时间节点字段**
+   - `phaseDeadlines` - 各阶段计划时间
+   - `deliveryStageStatus` - 各阶段实际时间
+   - `deadline` - 交付日期
+   - `demoday` - 对图时间
+
+2. **状态字段**
+   - `currentStage` - 当前阶段
+   - `deliveryStageStatus[].status` - 各子阶段状态
+
+3. **进度字段**
+   - `spaceDeliverableSummary` - 空间交付物汇总
+   - `phaseProgress` - 各阶段进度
+
+### 数据流
+
+```
+Project 对象
+  ├── 基础字段 (id, title, deadline, currentStage)
+  ├── data.phaseDeadlines (阶段计划时间)
+  ├── data.deliveryStageStatus (阶段实际状态)
+  ├── data.spaceDeliverableSummary (空间交付物统计)
+  └── assignee, contact (关联对象)
+      ↓
+转换为 ProjectTimeline 格式
+      ↓
+展示在时间轴组件中
+```
+
+---
+
+**文档维护**: 本文档应随字段结构变化及时更新  
+**最后更新**: 2025-11-13
+

+ 298 - 0
项目进度弹窗修复总结.md

@@ -0,0 +1,298 @@
+# 项目进度弹窗修复总结
+
+**修复日期**: 2025-11-13  
+**问题描述**: 
+1. 项目进度弹窗中没有显示"未完成任务汇总"区域
+2. 需要确保只显示当前项目的完成情况,而不是所有项目的汇总
+
+**修改文件**: 
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.ts`
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.html`
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.scss`
+
+---
+
+## ✅ 修复内容
+
+### 1. 修复显示条件
+
+**问题**: 原条件 `*ngIf="summary && hasIncompleteTasks()"` 导致当没有未完成任务时,整个汇总区域不显示
+
+**修复**: 改为 `*ngIf="summary"`,确保汇总区域始终显示
+
+```html
+<!-- 修复前 -->
+<div class="incomplete-tasks-summary" *ngIf="summary && hasIncompleteTasks()">
+
+<!-- 修复后 -->
+<div class="incomplete-tasks-summary" *ngIf="summary">
+```
+
+**效果**: 
+- ✅ 有未完成任务时:显示任务列表
+- ✅ 无未完成任务时:显示"全部完成"消息
+
+---
+
+### 2. 明确显示当前项目信息
+
+**问题**: 用户可能误以为显示的是所有项目的汇总
+
+**修复**: 在 subtitle 中明确显示当前项目名称
+
+```html
+<div class="summary-subtitle">
+  当前项目:{{ summary.projectName }} - 按设计师分组查看未完成任务
+</div>
+```
+
+**效果**: 
+- ✅ 用户清楚知道这是当前项目的汇总
+- ✅ 避免误解为所有项目的汇总
+
+---
+
+### 3. 添加"全部完成"状态显示
+
+**新增**: 当没有未完成任务时,显示成功消息
+
+```html
+<!-- 全部完成时显示 -->
+<div class="all-completed-message" *ngIf="!hasIncompleteTasks()">
+  <span class="success-icon-large">✅</span>
+  <div class="message-content">
+    <h4>恭喜!所有任务已完成</h4>
+    <p>该项目所有空间的交付任务均已完成,无需跟进。</p>
+  </div>
+</div>
+```
+
+**效果**: 
+- ✅ 用户可以看到项目已完成的状态
+- ✅ 提供清晰的视觉反馈
+
+---
+
+### 4. 优化任务数量徽章
+
+**新增**: 区分"有未完成任务"和"全部完成"两种状态
+
+```html
+<span class="summary-badge" *ngIf="hasIncompleteTasks()">{{ getTotalIncompleteTasks() }} 个任务</span>
+<span class="summary-badge success" *ngIf="!hasIncompleteTasks()">全部完成</span>
+```
+
+**样式**:
+- 有未完成任务:红色背景 `#ff5722`
+- 全部完成:绿色背景 `#4caf50`
+
+---
+
+### 5. 增强调试日志
+
+**新增**: 添加详细的调试日志,帮助排查问题
+
+```typescript
+console.log('📊 获取未完成任务(当前项目):', {
+  projectId: this.summary.projectId,
+  projectName: this.summary.projectName
+});
+
+console.log('📊 未完成任务分组结果(当前项目):', {
+  projectId: this.summary.projectId,
+  totalTasks: this.getTotalIncompleteTasks(),
+  designerGroups: result.length,
+  details: result
+});
+```
+
+**效果**: 
+- ✅ 方便排查数据问题
+- ✅ 确认只处理当前项目的数据
+
+---
+
+### 6. 明确代码注释
+
+**优化**: 在关键方法中添加注释,说明只处理当前项目
+
+```typescript
+/**
+ * ✅ 应用方案:获取当前项目的所有未完成的任务,按设计师分组
+ * 注意:这里只显示当前项目(this.summary)的未完成任务,不是所有项目的汇总
+ */
+getIncompleteTasksByDesigner(): Array<{...}>
+```
+
+---
+
+## 🔍 逻辑确认
+
+### 数据流程
+
+```
+用户点击项目
+  ↓
+onProjectClick(projectId)
+  ↓
+获取项目的统计摘要: selectedProjectSummary = spaceDeliverableCache.get(projectId)
+  ↓
+传递给弹窗组件: [summary]="selectedProjectSummary"
+  ↓
+弹窗组件处理: getIncompleteTasksByDesigner()
+  ↓
+基于 this.summary(单个项目)处理数据
+  ↓
+返回当前项目的未完成任务列表
+```
+
+### 数据范围确认
+
+**✅ 只处理当前项目**:
+- `this.summary` 是单个项目的 `ProjectSpaceDeliverableSummary`
+- `this.summary.phaseProgress` 只包含当前项目的阶段数据
+- `incompleteSpaces` 只包含当前项目的未完成空间
+
+**❌ 不会处理其他项目**:
+- 不会遍历所有项目
+- 不会汇总多个项目的数据
+- 只显示当前选中项目的信息
+
+---
+
+## 📊 显示效果
+
+### 有未完成任务时
+
+```
+┌─────────────────────────────────────────────────┐
+│ ⚠️ 未完成任务汇总                    [5 个任务] │
+│    当前项目:测试:十二月组-家装-客厅+厨房        │
+│    按设计师分组,清晰查看每个设计师的未完成任务  │
+├─────────────────────────────────────────────────┤
+│ ┌───────────────────────────────────────────┐ │
+│ │ 👤 张三                     [3 个未完成]  │ │
+│ ├───────────────────────────────────────────┤ │
+│ │ [建模] 客厅                                │ │
+│ │ [软装] 主卧                                │ │
+│ │ [渲染] 厨房                                │ │
+│ └───────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────┘
+```
+
+### 全部完成时
+
+```
+┌─────────────────────────────────────────────────┐
+│ ⚠️ 未完成任务汇总                    [全部完成] │
+│    当前项目:测试:十二月组-家装-客厅+厨房        │
+│    按设计师分组,清晰查看每个设计师的未完成任务  │
+├─────────────────────────────────────────────────┤
+│                                                 │
+│              ✅                                 │
+│        恭喜!所有任务已完成                      │
+│   该项目所有空间的交付任务均已完成,无需跟进。   │
+│                                                 │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+## 🧪 测试建议
+
+### 1. 功能测试
+
+- [ ] 测试有未完成任务的项目的弹窗显示
+- [ ] 测试全部完成项目的弹窗显示
+- [ ] 测试未分配任务的项目显示
+- [ ] 测试多个设计师的项目分组显示
+
+### 2. 数据测试
+
+- [ ] 确认只显示当前项目的数据
+- [ ] 确认项目名称正确显示
+- [ ] 确认任务数量统计正确
+- [ ] 确认设计师分组正确
+
+### 3. 边界测试
+
+- [ ] 测试没有空间的项目
+- [ ] 测试没有阶段数据的项目
+- [ ] 测试数据加载中的状态
+
+---
+
+## ⚠️ 注意事项
+
+### 1. 数据依赖
+
+- ✅ 依赖 `summary.phaseProgress` 数据
+- ✅ 依赖 `incompleteSpaces` 数组中的 `assignee` 字段
+- ⚠️ 如果 `assignee` 为空,会显示为"未分配"
+
+### 2. 性能考虑
+
+- ✅ 数据聚合在组件内完成,无需额外查询
+- ✅ 使用 Map 数据结构,性能良好
+- ✅ 仅在弹窗打开时计算,不影响列表性能
+
+### 3. 兼容性
+
+- ✅ 兼容旧数据(没有 assignee 的情况)
+- ✅ 兼容未分配任务的情况
+- ✅ 兼容所有阶段的数据
+
+---
+
+## 📝 后续优化建议
+
+### 阶段一:功能增强(优先级:高)
+
+1. ⚠️ 添加点击任务项跳转到项目详情页
+2. ⚠️ 添加快速分配负责人功能
+3. ⚠️ 添加任务完成时间预估
+
+### 阶段二:交互优化(优先级:中)
+
+1. ⚠️ 添加筛选功能(按阶段、按设计师)
+2. ⚠️ 添加排序功能(按任务数、按阶段)
+3. ⚠️ 添加导出功能(导出未完成任务列表)
+
+### 阶段三:视觉优化(优先级:低)
+
+1. ⚠️ 添加动画效果
+2. ⚠️ 优化移动端显示
+3. ⚠️ 添加图表展示(任务分布饼图)
+
+---
+
+## 📌 总结
+
+### 关键修复
+
+1. **显示条件优化**
+   - ✅ 从条件渲染改为始终显示
+   - ✅ 区分"有未完成任务"和"全部完成"两种状态
+
+2. **信息明确化**
+   - ✅ 显示当前项目名称
+   - ✅ 明确说明这是当前项目的汇总
+
+3. **逻辑确认**
+   - ✅ 确认只处理当前项目的数据
+   - ✅ 添加调试日志帮助排查问题
+
+### 效果对比
+
+| 指标 | 修复前 | 修复后 |
+|------|--------|--------|
+| 未完成任务汇总显示 | ❌ 不显示 | ✅ 始终显示 |
+| 项目信息明确性 | ⚠️ 不明确 | ✅ 明确显示项目名称 |
+| 全部完成状态 | ❌ 不显示 | ✅ 显示成功消息 |
+| 数据范围 | ⚠️ 可能混淆 | ✅ 明确只显示当前项目 |
+
+---
+
+**文档维护**: 本文档应随功能变更及时更新  
+**最后更新**: 2025-11-13
+

+ 279 - 0
项目进度详情组件字段应用总结.md

@@ -0,0 +1,279 @@
+# 项目进度详情组件字段应用总结
+
+**应用日期**: 2025-11-13  
+**基于方案**: 项目进度详情组件字段需求方案.md  
+**修改文件**: `src/modules/project/services/project-space-deliverable.service.ts`
+
+---
+
+## ✅ 已应用的更改
+
+### 1. 文件查询优化
+
+#### 1.1 性能优化
+- ✅ **一次性查询所有交付文件**:避免为每个空间和每个类型重复查询
+- ✅ **文件列表传递**:将查询结果传递给 `getSpaceDeliverableInfo` 方法,避免重复查询
+
+**代码位置**: `project-space-deliverable.service.ts:159-167`
+
+```typescript
+// ✅ 应用方案:优化性能,一次性查询所有交付文件
+const allDeliveryFiles = await this.projectFileService.getProjectFiles(projectId, {
+  stage: 'delivery'
+});
+
+for (const product of uniqueProducts) {
+  // ✅ 应用方案:传入所有文件列表,避免重复查询
+  const spaceInfo = await this.getSpaceDeliverableInfo(projectId, product, allDeliveryFiles);
+  spaceInfos.push(spaceInfo);
+}
+```
+
+---
+
+### 2. 字段匹配逻辑增强
+
+#### 2.1 支持多种字段格式
+- ✅ **优先使用 `data.spaceId`**:符合 Git Commit 更新的字段
+- ✅ **降级使用 `data.productId`**:兼容旧数据
+- ✅ **支持 `data.deliveryType` 字段**:使用 Git Commit 中更新的字段
+- ✅ **支持 `data.uploadStage` 字段**:确认是交付阶段文件
+
+**代码位置**: `project-space-deliverable.service.ts:273-290`
+
+```typescript
+// ✅ 应用方案:支持多种字段格式匹配
+const spaceFiles = deliveryFiles.filter(file => {
+  const data = file.get('data') || {};
+  const fileType = file.get('fileType') || '';
+  const deliveryType = data.deliveryType || '';
+  const uploadStage = data.uploadStage || '';
+  
+  // ✅ 应用方案:优先使用 data.spaceId,其次使用 data.productId
+  const isSpaceMatch = data.spaceId === spaceId || data.productId === spaceId;
+  
+  // ✅ 应用方案:支持多种字段格式匹配
+  const isTypeMatch = 
+    fileType === mapping.fileType || 
+    mapping.deliveryType.includes(deliveryType) ||
+    mapping.deliveryType.includes(fileType);
+  
+  // ✅ 应用方案:确认是交付阶段文件(推荐检查 uploadStage)
+  const isDeliveryStage = uploadStage === 'delivery' || !uploadStage || fileType.includes('delivery');
+  
+  return isSpaceMatch && isTypeMatch && isDeliveryStage;
+});
+```
+
+#### 2.2 交付类型映射增强
+- ✅ **支持多种 deliveryType 值格式**:
+  - `white_model` / `delivery_white_model` / `whiteModel`
+  - `soft_decor` / `delivery_soft_decor` / `softDecor`
+  - `rendering` / `delivery_rendering`
+  - `post_process` / `delivery_post_process` / `postProcess`
+
+**代码位置**: `project-space-deliverable.service.ts:250-268`
+
+---
+
+### 3. 负责人信息获取优化
+
+#### 3.1 空间负责人优先
+- ✅ **优先使用空间负责人**:从 `Product.profile` 获取
+- ✅ **降级使用阶段负责人**:从 `Project.data.phaseDeadlines[phase].assignee` 获取
+- ✅ **包含 profile 查询**:在查询 Product 时使用 `include('profile')`
+
+**代码位置**: `project-space-deliverable.service.ts:134-138, 431-444`
+
+```typescript
+// ✅ 应用方案:获取项目的所有空间(Product),包含负责人信息
+const productQuery = new Parse.Query('Product');
+productQuery.equalTo('project', project.toPointer());
+productQuery.ascending('createdAt');
+productQuery.include('profile'); // ✅ 包含负责人信息(如果存在)
+
+// ✅ 应用方案:为每个空间查找负责人
+// 优先使用空间负责人(Product.profile),其次使用阶段负责人
+const spaceAssigneeMap = new Map<string, string>();
+if (products) {
+  products.forEach(product => {
+    const profile = product.get('profile');
+    if (profile) {
+      const profileName = profile.get?.('name') || profile.name || '';
+      if (profileName) {
+        spaceAssigneeMap.set(product.id!, profileName);
+      }
+    }
+  });
+}
+
+// 未完成空间列表,优先使用空间负责人,其次使用阶段负责人
+const spaceAssignee = spaceAssigneeMap.get(space.spaceId) || assigneeName;
+```
+
+#### 3.2 阶段负责人获取
+- ✅ **支持多种 assignee 格式**:
+  - Parse Pointer 对象(`assignee.objectId`)
+  - 字符串 ID
+  - 包含 name 字段的对象
+
+**代码位置**: `project-space-deliverable.service.ts:393-429`
+
+---
+
+### 4. 方法签名优化
+
+#### 4.1 getSpaceDeliverableInfo 方法
+- ✅ **添加可选参数 `allDeliveryFiles`**:支持传入文件列表,避免重复查询
+
+```typescript
+private async getSpaceDeliverableInfo(
+  projectId: string,
+  product: FmodeObject,
+  allDeliveryFiles?: FmodeObject[]  // ✅ 新增可选参数
+): Promise<SpaceDeliverableInfo>
+```
+
+#### 4.2 calculatePhaseProgress 方法
+- ✅ **添加可选参数 `products`**:支持传入 Product 列表,获取空间负责人信息
+
+```typescript
+private calculatePhaseProgress(
+  spaceInfos: SpaceDeliverableInfo[],
+  project: FmodeObject,
+  products?: FmodeObject[]  // ✅ 新增可选参数
+): ProjectSpaceDeliverableSummary['phaseProgress']
+```
+
+---
+
+## 📊 性能优化效果
+
+### 优化前
+- 每个空间 × 4 种类型 = **4N 次数据库查询**(N 为空间数)
+- 例如:10 个空间 = 40 次查询
+
+### 优化后
+- 1 次查询所有交付文件 + N 次内存过滤 = **1 次数据库查询**
+- 例如:10 个空间 = 1 次查询
+
+**性能提升**: 约 **40 倍**(对于 10 个空间的项目)
+
+---
+
+## 🔍 字段使用对照表
+
+### Git Commit 更新的字段应用
+
+| Git Commit 字段 | 应用位置 | 用途 |
+|----------------|---------|------|
+| `ProjectFile.data.spaceId` | `getSpaceDeliverableInfo()` | ✅ 优先用于关联空间和文件 |
+| `ProjectFile.data.deliveryType` | `getSpaceDeliverableInfo()` | ✅ 用于识别交付物类型 |
+| `ProjectFile.data.uploadStage` | `getSpaceDeliverableInfo()` | ✅ 用于确认是交付阶段文件 |
+| `Project.data.phaseDeadlines[phase].assignee` | `calculatePhaseProgress()` | ✅ 用于获取阶段负责人 |
+
+---
+
+## ⚠️ 字段缺失处理
+
+### 1. data.spaceId 缺失
+**处理方案**: 降级使用 `data.productId`
+
+```typescript
+const isSpaceMatch = data.spaceId === spaceId || data.productId === spaceId;
+```
+
+### 2. data.deliveryType 缺失
+**处理方案**: 从 `fileType` 推断
+
+```typescript
+const isTypeMatch = 
+  fileType === mapping.fileType || 
+  mapping.deliveryType.includes(deliveryType) ||
+  mapping.deliveryType.includes(fileType);
+```
+
+### 3. data.uploadStage 缺失
+**处理方案**: 从 `fileType` 推断(包含 'delivery' 关键字)
+
+```typescript
+const isDeliveryStage = uploadStage === 'delivery' || !uploadStage || fileType.includes('delivery');
+```
+
+### 4. Product.profile 缺失
+**处理方案**: 降级使用阶段负责人
+
+```typescript
+const spaceAssignee = spaceAssigneeMap.get(space.spaceId) || assigneeName;
+```
+
+---
+
+## 🚀 后续优化建议
+
+### 阶段一:数据补全(优先级:高)
+
+1. ✅ 为旧项目的 ProjectFile 补充 `data.spaceId` 字段
+2. ✅ 为旧项目的 ProjectFile 补充 `data.deliveryType` 字段
+3. ✅ 为旧项目的 ProjectFile 补充 `data.uploadStage` 字段
+
+### 阶段二:功能增强(优先级:中)
+
+1. ⚠️ 实现阶段负责人的实时查询(当前为占位符)
+2. ⚠️ 添加缓存机制,避免重复查询
+3. ⚠️ 添加数据更新监听,实时刷新统计
+
+### 阶段三:性能优化(优先级:低)
+
+1. ⚠️ 使用 `Project.data.spaceDeliverableSummary` 进行缓存
+2. ⚠️ 批量查询负责人信息
+3. ⚠️ 添加查询结果缓存
+
+---
+
+## 📝 测试建议
+
+### 1. 功能测试
+- [ ] 测试有完整字段的项目统计
+- [ ] 测试只有部分字段的项目统计(降级场景)
+- [ ] 测试没有 `data.spaceId` 但有 `data.productId` 的文件
+- [ ] 测试没有 `data.deliveryType` 但能从 `fileType` 推断的文件
+
+### 2. 性能测试
+- [ ] 测试大量空间项目的查询性能
+- [ ] 测试大量文件项目的查询性能
+- [ ] 对比优化前后的查询次数
+
+### 3. 数据测试
+- [ ] 测试有空间负责人的项目
+- [ ] 测试只有阶段负责人的项目
+- [ ] 测试没有负责人的项目
+
+---
+
+## 📌 总结
+
+### 关键改进
+
+1. **性能优化**
+   - ✅ 从 4N 次查询优化为 1 次查询
+   - ✅ 约 40 倍性能提升(10 个空间的项目)
+
+2. **字段支持**
+   - ✅ 优先使用 Git Commit 更新的字段
+   - ✅ 支持多种字段格式,确保兼容性
+
+3. **负责人信息**
+   - ✅ 优先使用空间负责人
+   - ✅ 降级使用阶段负责人
+
+4. **代码质量**
+   - ✅ 添加详细注释
+   - ✅ 改进方法签名
+   - ✅ 优化错误处理
+
+---
+
+**文档维护**: 本文档应随代码变更及时更新  
+**最后更新**: 2025-11-13
+

+ 637 - 0
项目进度详情组件字段需求方案.md

@@ -0,0 +1,637 @@
+# 项目进度详情组件字段需求方案
+
+**分析日期**: 2025-11-13  
+**组件路径**: `src/app/pages/team-leader/project-timeline/project-progress-modal.ts`  
+**服务路径**: `src/modules/project/services/project-space-deliverable.service.ts`  
+**文档版本**: v1.0
+
+---
+
+## 📋 概述
+
+本文档分析项目进度详情组件(`ProjectProgressModalComponent`)实现所需的所有数据字段,包括从数据库表到组件展示的完整数据流。
+
+---
+
+## 🎯 组件功能清单
+
+### 1. 项目基本信息展示
+- 项目名称
+- 空间总数
+- 已完成空间数
+
+### 2. 整体进度展示
+- 整体完成率(百分比)
+- 完成状态标签(未开始/刚开始/进行中/接近完成/已完成)
+
+### 3. 文件统计展示
+- 白模文件数
+- 软装文件数
+- 渲染文件数
+- 后期文件数
+- 总文件数
+
+### 4. 各阶段进度明细
+- 建模阶段进度
+- 软装阶段进度
+- 渲染阶段进度
+- 后期阶段进度
+- 每个阶段的完成空间数/应完成空间数
+- 每个阶段的文件总数
+- 每个阶段的未完成空间列表(含负责人)
+
+---
+
+## 📊 数据流分析
+
+### 数据获取流程
+
+```
+Project 表
+  ├── id (项目ID)
+  ├── title (项目名称)
+  └── data.phaseDeadlines (阶段负责人信息)
+      ↓
+Product 表 (查询条件: project = Project)
+  ├── id (空间ID)
+  ├── productName (空间名称)
+  └── productType (空间类型)
+      ↓
+ProjectFile 表 (查询条件: project = Project, stage = 'delivery')
+  ├── data.spaceId / data.productId (关联空间)
+  ├── data.deliveryType (交付类型)
+  └── fileType (文件类型)
+      ↓
+ProjectSpaceDeliverableService
+  ├── 统计空间数量
+  ├── 统计各类型文件数量
+  ├── 计算完成率
+  └── 生成 ProjectSpaceDeliverableSummary
+      ↓
+ProjectProgressModalComponent
+  └── 展示统计信息
+```
+
+---
+
+## 🔍 字段映射关系
+
+### 1. Project 表字段
+
+| 组件展示 | 字段路径 | 数据类型 | 必需性 | 说明 |
+|---------|---------|---------|--------|------|
+| 项目ID | `project.id` | string | ✅ 必需 | 用于查询关联数据 |
+| 项目名称 | `project.title` 或 `project.name` | string | ✅ 必需 | 显示在弹窗标题 |
+| 阶段负责人 | `project.data.phaseDeadlines[phase].assignee` | Pointer/objectId | ⚠️ 推荐 | 用于显示未完成空间的负责人 |
+
+**代码位置**: `project-space-deliverable.service.ts:130-132`
+
+```typescript
+const project = await projectQuery.get(projectId);
+const projectName = project.get('title') || project.get('name') || '未命名项目';
+const projectData = project.get('data') || {};
+const phaseDeadlines = projectData.phaseDeadlines || {};
+```
+
+---
+
+### 2. Product 表字段
+
+| 组件展示 | 字段路径 | 数据类型 | 必需性 | 说明 |
+|---------|---------|---------|--------|------|
+| 空间ID | `product.id` | string | ✅ 必需 | 用于关联 ProjectFile |
+| 空间名称 | `product.productName` | string | ✅ 必需 | 显示在空间列表中 |
+| 空间类型 | `product.productType` | string | ⚠️ 推荐 | 用于分类展示 |
+
+**查询条件**:
+```typescript
+const productQuery = new Parse.Query('Product');
+productQuery.equalTo('project', project.toPointer());
+productQuery.ascending('createdAt');
+const products = await productQuery.find();
+```
+
+**代码位置**: `project-space-deliverable.service.ts:134-138`
+
+**去重逻辑**: 按 `productName` 去重(忽略大小写和首尾空格)
+
+---
+
+### 3. ProjectFile 表字段
+
+| 组件展示 | 字段路径 | 数据类型 | 必需性 | 说明 |
+|---------|---------|---------|--------|------|
+| 关联空间ID | `projectFile.data.spaceId` 或 `projectFile.data.productId` | string | ✅ 必需 | 用于关联空间和文件 |
+| 交付类型 | `projectFile.data.deliveryType` | string | ✅ 必需 | 用于分类统计 |
+| 文件类型 | `projectFile.fileType` | string | ✅ 必需 | 用于查询过滤 |
+| 上传阶段 | `projectFile.data.uploadStage` | string | ⚠️ 推荐 | 用于确认是交付阶段文件 |
+
+**查询条件**:
+```typescript
+// 查询交付阶段的文件
+const files = await this.projectFileService.getProjectFiles(projectId, {
+  fileType: fileType,  // 'delivery_white_model' | 'delivery_soft_decor' | 'delivery_rendering' | 'delivery_post_process'
+  stage: 'delivery'
+});
+
+// 过滤当前空间的文件
+const spaceFiles = files.filter(file => {
+  const data = file.get('data');
+  return data?.productId === spaceId || data?.spaceId === spaceId;
+});
+```
+
+**交付类型映射**:
+```typescript
+const deliveryTypeMap = {
+  whiteModel: 'delivery_white_model',    // 白模
+  softDecor: 'delivery_soft_decor',      // 软装
+  rendering: 'delivery_rendering',       // 渲染
+  postProcess: 'delivery_post_process'   // 后期
+};
+```
+
+**代码位置**: `project-space-deliverable.service.ts:229-243`
+
+---
+
+### 4. Project.data 字段
+
+| 组件展示 | 字段路径 | 数据类型 | 必需性 | 说明 |
+|---------|---------|---------|--------|------|
+| 阶段负责人 | `data.phaseDeadlines[phase].assignee` | Pointer/objectId | ⚠️ 推荐 | 用于显示未完成空间的负责人 |
+| 空间交付物汇总 | `data.spaceDeliverableSummary` | object | ⚠️ 推荐 | 可用于缓存优化 |
+
+**代码位置**: `project-space-deliverable.service.ts:316-364`
+
+```typescript
+// 获取阶段负责人
+const phaseInfo = phaseDeadlines[phaseKey];
+const assignee = phaseInfo?.assignee;
+let assigneeName: string | undefined;
+
+if (assignee && assignee.objectId) {
+  // 如果有负责人指针,可以在这里查询(为了性能,暂时不实时查询)
+  assigneeName = '待查询';
+}
+```
+
+---
+
+## 📝 数据结构定义
+
+### ProjectSpaceDeliverableSummary 接口
+
+```typescript
+interface ProjectSpaceDeliverableSummary {
+  projectId: string;                    // 项目ID
+  projectName: string;                  // 项目名称
+  totalSpaces: number;                  // 空间总数
+  spacesWithDeliverables: number;       // 已上传交付物的空间数
+  spaces: SpaceDeliverableInfo[];       // 空间详细列表
+  totalDeliverableFiles: number;        // 总交付文件数
+  totalByType: {                        // 各类型总计
+    whiteModel: number;
+    softDecor: number;
+    rendering: number;
+    postProcess: number;
+  };
+  overallCompletionRate: number;        // 整体完成率(0-100)
+  phaseProgress: {                      // 各阶段进度详情
+    modeling: PhaseProgressInfo;
+    softDecor: PhaseProgressInfo;
+    rendering: PhaseProgressInfo;
+    postProcessing: PhaseProgressInfo;
+  };
+}
+```
+
+### SpaceDeliverableInfo 接口
+
+```typescript
+interface SpaceDeliverableInfo {
+  spaceId: string;                      // 空间ID(Product ID)
+  spaceName: string;                    // 空间名称
+  spaceType: string;                    // 空间类型
+  deliverableTypes: {                   // 交付物类型统计
+    whiteModel: number;                 // 白模文件数量
+    softDecor: number;                 // 软装文件数量
+    rendering: number;                 // 渲染文件数量
+    postProcess: number;                // 后期文件数量
+  };
+  totalFiles: number;                   // 总文件数
+  hasDeliverables: boolean;            // 是否已上传交付物
+  completionRate: number;               // 完成度(0-100)
+}
+```
+
+### PhaseProgressInfo 接口
+
+```typescript
+interface PhaseProgressInfo {
+  phaseName: 'modeling' | 'softDecor' | 'rendering' | 'postProcessing';
+  phaseLabel: string;                   // 阶段中文标签
+  requiredSpaces: number;              // 应完成空间数
+  completedSpaces: number;             // 已完成空间数
+  completionRate: number;               // 完成率(0-100)
+  totalFiles: number;                   // 总文件数
+  incompleteSpaces: Array<{            // 未完成空间列表
+    spaceId: string;
+    spaceName: string;
+    assignee?: string;                  // 负责人
+  }>;
+}
+```
+
+---
+
+## 🔧 字段获取和计算逻辑
+
+### 1. 空间信息获取
+
+**来源**: Product 表
+
+```typescript
+// 查询项目的所有空间
+const productQuery = new Parse.Query('Product');
+productQuery.equalTo('project', project.toPointer());
+productQuery.ascending('createdAt');
+const products = await productQuery.find();
+
+// 去重:按空间名称去重
+const uniqueProducts = this.deduplicateProducts(products);
+
+// 提取空间信息
+products.forEach(product => {
+  const spaceId = product.id;
+  const spaceName = product.get('productName') || '未命名空间';
+  const spaceType = product.get('productType') || 'other';
+});
+```
+
+**必需字段**:
+- ✅ `Product.id` - 空间ID
+- ✅ `Product.productName` - 空间名称
+- ⚠️ `Product.productType` - 空间类型(推荐)
+
+---
+
+### 2. 交付文件统计
+
+**来源**: ProjectFile 表
+
+```typescript
+// 定义交付物类型映射
+const deliveryTypeMap = {
+  whiteModel: 'delivery_white_model',
+  softDecor: 'delivery_soft_decor',
+  rendering: 'delivery_rendering',
+  postProcess: 'delivery_post_process'
+};
+
+// 查询每种类型的交付文件
+for (const [key, fileType] of Object.entries(deliveryTypeMap)) {
+  const files = await this.projectFileService.getProjectFiles(projectId, {
+    fileType: fileType,
+    stage: 'delivery'
+  });
+
+  // 过滤当前空间的文件
+  const spaceFiles = files.filter(file => {
+    const data = file.get('data');
+    return data?.productId === spaceId || data?.spaceId === spaceId;
+  });
+
+  deliverableTypes[key] = spaceFiles.length;
+}
+```
+
+**必需字段**:
+- ✅ `ProjectFile.data.spaceId` 或 `ProjectFile.data.productId` - 关联空间ID
+- ✅ `ProjectFile.data.deliveryType` - 交付类型标识
+- ✅ `ProjectFile.fileType` - 文件类型(用于查询)
+- ⚠️ `ProjectFile.data.uploadStage` - 上传阶段(推荐,用于确认是交付阶段)
+
+**字段值说明**:
+- `deliveryType` 值: `'white_model'` | `'soft_decor'` | `'rendering'` | `'post_process'`
+- `fileType` 值: `'delivery_white_model'` | `'delivery_soft_decor'` | `'delivery_rendering'` | `'delivery_post_process'`
+
+---
+
+### 3. 完成率计算
+
+**空间完成率**:
+```typescript
+// 计算完成度(假设每种类型至少需要1个文件才算完成)
+const completedTypes = Object.values(deliverableTypes).filter(count => count > 0).length;
+const completionRate = Math.round((completedTypes / 4) * 100);
+```
+
+**整体完成率**:
+```typescript
+const totalCompletionRate = spaceInfos.reduce(
+  (sum, space) => sum + space.completionRate,
+  0
+);
+const overallCompletionRate = Math.round(totalCompletionRate / spaceInfos.length);
+```
+
+**阶段完成率**:
+```typescript
+const completionRate = requiredSpaces > 0
+  ? Math.round((completedSpaces / requiredSpaces) * 100)
+  : 0;
+```
+
+---
+
+### 4. 阶段进度详情计算
+
+**来源**: 空间信息和 ProjectFile 统计
+
+```typescript
+// 阶段映射
+const phaseMap = {
+  modeling: {
+    key: 'whiteModel',
+    label: '建模',
+    phaseName: 'modeling'
+  },
+  softDecor: {
+    key: 'softDecor',
+    label: '软装',
+    phaseName: 'softDecor'
+  },
+  rendering: {
+    key: 'rendering',
+    label: '渲染',
+    phaseName: 'rendering'
+  },
+  postProcessing: {
+    key: 'postProcess',
+    label: '后期',
+    phaseName: 'postProcessing'
+  }
+};
+
+// 计算每个阶段的进度
+Object.entries(phaseMap).forEach(([phaseKey, phaseConfig]) => {
+  const requiredSpaces = spaceInfos.length; // 假设所有空间都需要各阶段交付物
+  let completedSpaces = 0;
+  let totalFiles = 0;
+  const incompleteSpaces = [];
+
+  spaceInfos.forEach(space => {
+    const fileCount = space.deliverableTypes[phaseConfig.key];
+    totalFiles += fileCount;
+
+    if (fileCount > 0) {
+      completedSpaces++;
+    } else {
+      incompleteSpaces.push({
+        spaceId: space.spaceId,
+        spaceName: space.spaceName,
+        assignee: assigneeName // 从 phaseDeadlines 获取
+      });
+    }
+  });
+});
+```
+
+**负责人信息获取**:
+```typescript
+// 获取阶段负责人
+const phaseInfo = phaseDeadlines[phaseKey];
+const assignee = phaseInfo?.assignee;
+
+if (assignee && assignee.objectId) {
+  // 可以查询 Profile 表获取负责人姓名
+  // 为了性能,暂时不实时查询
+  assigneeName = '待查询';
+}
+```
+
+---
+
+## ⚠️ 字段缺失处理方案
+
+### 1. Product.productName 缺失
+
+**处理方案**: 使用默认值
+
+```typescript
+const spaceName = product.get('productName') || '未命名空间';
+```
+
+### 2. ProjectFile.data.spaceId 缺失
+
+**处理方案**: 尝试使用 `data.productId`
+
+```typescript
+const spaceFiles = files.filter(file => {
+  const data = file.get('data');
+  return data?.productId === spaceId || data?.spaceId === spaceId;
+});
+```
+
+### 3. ProjectFile.data.deliveryType 缺失
+
+**处理方案**: 使用 `fileType` 推断
+
+```typescript
+// 如果 deliveryType 缺失,从 fileType 推断
+if (!data.deliveryType && fileType) {
+  if (fileType.includes('white_model')) {
+    deliveryType = 'white_model';
+  } else if (fileType.includes('soft_decor')) {
+    deliveryType = 'soft_decor';
+  }
+  // ... 其他类型
+}
+```
+
+### 4. phaseDeadlines.assignee 缺失
+
+**处理方案**: 不显示负责人信息
+
+```typescript
+const assigneeName = assignee ? '待查询' : undefined;
+```
+
+---
+
+## 📌 必需字段清单
+
+### 核心必需字段(必须存在)
+
+1. **Project 表**
+   - ✅ `id` - 项目ID
+   - ✅ `title` 或 `name` - 项目名称
+
+2. **Product 表**
+   - ✅ `id` - 空间ID
+   - ✅ `productName` - 空间名称
+   - ✅ `project` - 关联项目(Pointer)
+
+3. **ProjectFile 表**
+   - ✅ `project` - 关联项目(Pointer)
+   - ✅ `data.spaceId` 或 `data.productId` - 关联空间ID
+   - ✅ `data.deliveryType` - 交付类型标识
+   - ✅ `fileType` - 文件类型(用于查询)
+
+### 推荐字段(增强功能)
+
+1. **Product 表**
+   - ⚠️ `productType` - 空间类型
+
+2. **ProjectFile 表**
+   - ⚠️ `data.uploadStage` - 上传阶段(用于确认是交付阶段)
+
+3. **Project.data**
+   - ⚠️ `phaseDeadlines[phase].assignee` - 阶段负责人(用于显示未完成空间的负责人)
+
+---
+
+## 🚀 实施建议
+
+### 阶段一:核心字段验证(优先级:高)
+
+1. ✅ 验证 `Product.productName` 字段是否在所有空间中存在
+2. ✅ 验证 `ProjectFile.data.spaceId` 或 `data.productId` 字段是否在所有交付文件中存在
+3. ✅ 验证 `ProjectFile.data.deliveryType` 字段是否在所有交付文件中存在
+
+### 阶段二:数据补全(优先级:高)
+
+1. ✅ 为缺失 `data.spaceId` 的 ProjectFile 补充数据
+2. ✅ 为缺失 `data.deliveryType` 的 ProjectFile 补充数据
+3. ✅ 确保所有交付文件都有正确的 `fileType` 值
+
+### 阶段三:功能增强(优先级:中)
+
+1. ✅ 添加阶段负责人信息的实时查询
+2. ✅ 使用 `data.spaceDeliverableSummary` 进行缓存优化
+3. ✅ 添加空间类型的分类展示
+
+### 阶段四:性能优化(优先级:低)
+
+1. ✅ 缓存空间交付物统计数据
+2. ✅ 优化查询性能,减少数据库请求
+3. ✅ 添加数据更新监听,实时刷新统计
+
+---
+
+## 📊 字段使用示例
+
+### 示例1:获取项目进度统计
+
+```typescript
+import { ProjectSpaceDeliverableService } from '@modules/project/services/project-space-deliverable.service';
+
+constructor(
+  private projectSpaceDeliverableService: ProjectSpaceDeliverableService
+) {}
+
+async loadProjectProgress(projectId: string) {
+  const summary = await this.projectSpaceDeliverableService
+    .getProjectSpaceDeliverableSummary(projectId);
+  
+  console.log('项目名称:', summary.projectName);
+  console.log('空间总数:', summary.totalSpaces);
+  console.log('已完成空间:', summary.spacesWithDeliverables);
+  console.log('整体完成率:', summary.overallCompletionRate + '%');
+  console.log('白模文件数:', summary.totalByType.whiteModel);
+  console.log('软装文件数:', summary.totalByType.softDecor);
+  console.log('渲染文件数:', summary.totalByType.rendering);
+  console.log('后期文件数:', summary.totalByType.postProcess);
+}
+```
+
+### 示例2:检查项目是否完成
+
+```typescript
+const isCompleted = await this.projectSpaceDeliverableService
+  .isAllSpacesDelivered(projectId);
+
+if (isCompleted) {
+  console.log('✅ 项目所有空间已完成交付');
+} else {
+  const incompleteSpaces = await this.projectSpaceDeliverableService
+    .getIncompleteSpaces(projectId);
+  console.log('⚠️ 未完成空间:', incompleteSpaces);
+}
+```
+
+### 示例3:获取阶段进度详情
+
+```typescript
+const summary = await this.projectSpaceDeliverableService
+  .getProjectSpaceDeliverableSummary(projectId);
+
+// 建模阶段进度
+const modelingProgress = summary.phaseProgress.modeling;
+console.log('建模阶段完成率:', modelingProgress.completionRate + '%');
+console.log('已完成空间:', modelingProgress.completedSpaces);
+console.log('未完成空间:', modelingProgress.incompleteSpaces);
+
+// 软装阶段进度
+const softDecorProgress = summary.phaseProgress.softDecor;
+// ... 类似
+```
+
+---
+
+## 🔄 与 Git Commit 字段的关联
+
+### 已更新的字段(来自 commit 56ce1468)
+
+1. **ProjectFile.data 扩展字段**
+   - ✅ `spaceId` - 关联的空间/产品ID
+   - ✅ `deliveryType` - 交付类型标识
+   - ✅ `uploadStage` - 上传阶段
+
+2. **Project.data 扩展字段**
+   - ✅ `phaseDeadlines` - 阶段截止时间(包含负责人信息)
+   - ✅ `spaceDeliverableSummary` - 空间交付物汇总(可用于缓存)
+
+这些字段与进度详情组件的需求完全匹配,可以直接使用。
+
+---
+
+## 📝 总结
+
+### 关键字段
+
+1. **空间信息字段**
+   - `Product.id` - 空间ID
+   - `Product.productName` - 空间名称
+   - `Product.productType` - 空间类型
+
+2. **文件关联字段**
+   - `ProjectFile.data.spaceId` / `data.productId` - 关联空间ID
+   - `ProjectFile.data.deliveryType` - 交付类型标识
+   - `ProjectFile.fileType` - 文件类型
+
+3. **阶段信息字段**
+   - `Project.data.phaseDeadlines[phase].assignee` - 阶段负责人
+
+### 数据流
+
+```
+Project 表 (项目基本信息)
+  ↓
+Product 表 (空间列表)
+  ↓
+ProjectFile 表 (交付文件,通过 data.spaceId 关联)
+  ↓
+ProjectSpaceDeliverableService (统计计算)
+  ↓
+ProjectSpaceDeliverableSummary (统计结果)
+  ↓
+ProjectProgressModalComponent (展示)
+```
+
+---
+
+**文档维护**: 本文档应随字段结构变化及时更新  
+**最后更新**: 2025-11-13
+

+ 360 - 0
项目进度详情组件弹窗优化总结.md

@@ -0,0 +1,360 @@
+# 项目进度详情组件弹窗优化总结
+
+**优化日期**: 2025-11-13  
+**优化目标**: 清楚显示哪个空间的设计师没完成任务  
+**修改文件**: 
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.ts`
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.html`
+- `src/app/pages/team-leader/project-timeline/project-progress-modal.scss`
+
+---
+
+## ✅ 已实现的优化
+
+### 1. 新增"未完成任务汇总"区域
+
+#### 1.1 功能特性
+- ✅ **按设计师分组显示**:将所有未完成的任务按负责人分组
+- ✅ **任务数量统计**:显示每个设计师的未完成任务数量
+- ✅ **任务详情展示**:显示每个任务的阶段和空间名称
+- ✅ **未分配任务提醒**:突出显示未分配负责人的任务
+
+#### 1.2 显示位置
+- 位置:在"文件统计"和"各阶段进度明细"之间
+- 条件:仅在有未完成任务时显示
+
+---
+
+### 2. 组件方法增强
+
+#### 2.1 新增方法
+
+**`getIncompleteTasksByDesigner()`**
+```typescript
+// 获取所有未完成的任务,按设计师分组
+getIncompleteTasksByDesigner(): Array<{
+  designerName: string;
+  tasks: Array<{
+    phaseName: string;
+    phaseLabel: string;
+    spaceName: string;
+    spaceId: string;
+  }>;
+  taskCount: number;
+}>
+```
+
+**功能**:
+- 遍历所有阶段的未完成空间
+- 按负责人(设计师)分组
+- 按任务数量降序排序
+- 返回分组后的任务列表
+
+**`getTotalIncompleteTasks()`**
+```typescript
+// 获取未完成任务总数
+getTotalIncompleteTasks(): number
+```
+
+**`getUnassignedTaskCount()`**
+```typescript
+// 获取未分配任务数
+getUnassignedTaskCount(): number
+```
+
+**`hasIncompleteTasks()`**
+```typescript
+// 判断是否有未完成的任务
+hasIncompleteTasks(): boolean
+```
+
+---
+
+### 3. 模板优化
+
+#### 3.1 未完成任务汇总区域
+
+**HTML 结构**:
+```html
+<div class="incomplete-tasks-summary" *ngIf="summary && hasIncompleteTasks()">
+  <!-- 标题区域 -->
+  <div class="summary-header">
+    <h3>未完成任务汇总</h3>
+    <span class="summary-badge">{{ getTotalIncompleteTasks() }} 个任务</span>
+  </div>
+
+  <!-- 设计师分组列表 -->
+  <div class="designer-tasks-list">
+    <div *ngFor="let designerGroup of getIncompleteTasksByDesigner()" class="designer-group">
+      <!-- 设计师信息 -->
+      <div class="designer-header">
+        <span class="designer-name">{{ designerGroup.designerName }}</span>
+        <span class="task-count-badge">{{ designerGroup.taskCount }} 个未完成</span>
+      </div>
+
+      <!-- 任务列表 -->
+      <div class="tasks-list">
+        <div *ngFor="let task of designerGroup.tasks" class="task-item">
+          <span class="task-phase-badge">{{ task.phaseLabel }}</span>
+          <span class="task-space-name">{{ task.spaceName }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 未分配任务提醒 -->
+  <div class="summary-footer" *ngIf="getUnassignedTaskCount() > 0">
+    <span>有 {{ getUnassignedTaskCount() }} 个任务未分配负责人</span>
+  </div>
+</div>
+```
+
+#### 3.2 优化未完成空间项显示
+
+**改进**:
+- ✅ 突出显示未分配负责人的空间(橙色边框和背景)
+- ✅ 优化负责人信息显示(图标 + 姓名)
+- ✅ 未分配任务显示警告图标和"未分配"标签
+
+---
+
+### 4. 样式优化
+
+#### 4.1 未完成任务汇总区域样式
+
+**视觉特点**:
+- 🟡 **黄色渐变背景**:突出显示未完成任务区域
+- 🔴 **红色边框**:未分配任务组使用红色边框
+- 📊 **卡片式布局**:每个设计师一个卡片
+- 🎯 **任务数量徽章**:红色背景,白色文字
+
+**样式类**:
+- `.incomplete-tasks-summary` - 汇总区域容器
+- `.designer-group` - 设计师分组卡片
+- `.designer-group.unassigned` - 未分配任务组(特殊样式)
+- `.task-item` - 单个任务项
+- `.summary-footer` - 未分配任务提醒
+
+#### 4.2 未完成空间项样式优化
+
+**改进**:
+- ✅ 未分配负责人的空间:橙色边框 + 浅黄色背景
+- ✅ 有负责人的空间:正常样式 + 蓝色负责人标签
+- ✅ 悬停效果:提升交互体验
+
+---
+
+## 📊 显示效果
+
+### 未完成任务汇总区域
+
+```
+┌─────────────────────────────────────────────────┐
+│ ⚠️ 未完成任务汇总                    [5 个任务] │
+│    按设计师分组,清晰查看每个设计师的未完成任务  │
+├─────────────────────────────────────────────────┤
+│ ┌───────────────────────────────────────────┐ │
+│ │ 👤 张三                     [3 个未完成]  │ │
+│ ├───────────────────────────────────────────┤ │
+│ │ [建模] 客厅                                │ │
+│ │ [软装] 主卧                                │ │
+│ │ [渲染] 厨房                                │ │
+│ └───────────────────────────────────────────┘ │
+│ ┌───────────────────────────────────────────┐ │
+│ │ 👤 李四                     [2 个未完成]  │ │
+│ ├───────────────────────────────────────────┤ │
+│ │ [建模] 次卧                                │ │
+│ │ [后期] 卫生间                              │ │
+│ └───────────────────────────────────────────┘ │
+│ ┌───────────────────────────────────────────┐ │
+│ │ ⚠️ 未分配                   [1 个未完成]  │ │
+│ ├───────────────────────────────────────────┤ │
+│ │ [软装] 书房                                │ │
+│ └───────────────────────────────────────────┘ │
+├─────────────────────────────────────────────────┤
+│ ⚠️ 有 1 个任务未分配负责人,请及时分配         │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+## 🎯 优化效果
+
+### 1. 信息清晰度提升
+
+**优化前**:
+- ❌ 需要展开每个阶段才能看到未完成空间
+- ❌ 无法快速了解哪个设计师任务最多
+- ❌ 未分配任务容易被忽略
+
+**优化后**:
+- ✅ 一眼看到所有未完成任务
+- ✅ 按设计师分组,快速定位责任人
+- ✅ 未分配任务突出显示,不会遗漏
+
+### 2. 视觉层次优化
+
+**优化前**:
+- ❌ 所有信息平铺,没有重点
+- ❌ 负责人信息不够醒目
+
+**优化后**:
+- ✅ 未完成任务汇总区域使用醒目的黄色背景
+- ✅ 未分配任务使用红色边框和背景
+- ✅ 负责人信息使用蓝色标签,清晰可见
+
+### 3. 交互体验提升
+
+**优化前**:
+- ❌ 需要多次点击展开才能看到完整信息
+- ❌ 无法快速了解整体情况
+
+**优化后**:
+- ✅ 顶部汇总区域提供全局视图
+- ✅ 按任务数量排序,优先显示任务最多的设计师
+- ✅ 悬停效果提升交互体验
+
+---
+
+## 📝 使用场景
+
+### 场景1:快速查看未完成任务
+组长打开项目进度详情弹窗,顶部"未完成任务汇总"区域立即显示:
+- 哪些设计师有未完成任务
+- 每个设计师有多少个未完成任务
+- 哪些任务未分配负责人
+
+### 场景2:按设计师分配任务
+组长看到未分配任务后,可以:
+- 快速识别需要分配的任务
+- 根据设计师当前任务量合理分配
+- 避免任务分配不均
+
+### 场景3:跟踪设计师工作进度
+组长可以:
+- 查看每个设计师的未完成任务列表
+- 了解哪些阶段的任务未完成
+- 及时跟进和督促
+
+---
+
+## 🔧 技术实现细节
+
+### 1. 数据聚合逻辑
+
+```typescript
+// 遍历所有阶段的未完成空间
+Object.entries(this.summary.phaseProgress).forEach(([phaseKey, phaseInfo]) => {
+  phaseInfo.incompleteSpaces?.forEach((space) => {
+    const designerName = space.assignee || '未分配';
+    // 按设计师分组
+    // ...
+  });
+});
+
+// 按任务数量降序排序
+result.sort((a, b) => b.taskCount - a.taskCount);
+```
+
+### 2. 条件渲染
+
+```html
+<!-- 仅在有未完成任务时显示 -->
+<div *ngIf="summary && hasIncompleteTasks()">
+  <!-- 未完成任务汇总 -->
+</div>
+
+<!-- 未分配任务提醒 -->
+<div *ngIf="getUnassignedTaskCount() > 0">
+  <!-- 提醒信息 -->
+</div>
+```
+
+### 3. 样式条件应用
+
+```html
+<!-- 未分配任务组特殊样式 -->
+<div 
+  class="designer-group"
+  [class.unassigned]="designerGroup.designerName === '未分配'">
+
+<!-- 未分配空间特殊样式 -->
+<div 
+  class="space-item"
+  [class.no-assignee]="!space.assignee || space.assignee === '未分配'">
+```
+
+---
+
+## ⚠️ 注意事项
+
+### 1. 数据依赖
+- ✅ 依赖 `summary.phaseProgress` 数据
+- ✅ 依赖 `incompleteSpaces` 数组中的 `assignee` 字段
+- ⚠️ 如果 `assignee` 为空,会显示为"未分配"
+
+### 2. 性能考虑
+- ✅ 数据聚合在组件内完成,无需额外查询
+- ✅ 使用 Map 数据结构,性能良好
+- ✅ 仅在弹窗打开时计算,不影响列表性能
+
+### 3. 兼容性
+- ✅ 兼容旧数据(没有 assignee 的情况)
+- ✅ 兼容未分配任务的情况
+- ✅ 兼容所有阶段的数据
+
+---
+
+## 🚀 后续优化建议
+
+### 阶段一:功能增强(优先级:高)
+
+1. ⚠️ 添加点击任务项跳转到项目详情页
+2. ⚠️ 添加快速分配负责人功能
+3. ⚠️ 添加任务完成时间预估
+
+### 阶段二:交互优化(优先级:中)
+
+1. ⚠️ 添加筛选功能(按阶段、按设计师)
+2. ⚠️ 添加排序功能(按任务数、按阶段)
+3. ⚠️ 添加导出功能(导出未完成任务列表)
+
+### 阶段三:视觉优化(优先级:低)
+
+1. ⚠️ 添加动画效果
+2. ⚠️ 优化移动端显示
+3. ⚠️ 添加图表展示(任务分布饼图)
+
+---
+
+## 📌 总结
+
+### 关键改进
+
+1. **信息聚合**
+   - ✅ 将所有未完成任务集中展示
+   - ✅ 按设计师分组,清晰明了
+
+2. **视觉优化**
+   - ✅ 使用醒目的颜色和样式
+   - ✅ 突出显示未分配任务
+
+3. **用户体验**
+   - ✅ 快速了解整体情况
+   - ✅ 快速定位责任人
+   - ✅ 及时发现问题
+
+### 效果对比
+
+| 指标 | 优化前 | 优化后 |
+|------|--------|--------|
+| 查看未完成任务时间 | 需要展开多个阶段 | 一眼看到汇总 |
+| 定位责任人 | 需要逐个查看 | 按设计师分组 |
+| 发现未分配任务 | 容易遗漏 | 突出显示 |
+| 信息清晰度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
+
+---
+
+**文档维护**: 本文档应随功能变更及时更新  
+**最后更新**: 2025-11-13
+