Browse Source

feat: enhance project progress modal with incomplete tasks summary and designer assignment visibility

- Added a new section in the project progress modal to display incomplete tasks grouped by designer, improving clarity on task assignments.
- Implemented methods to calculate and display the total number of incomplete tasks and unassigned tasks, enhancing user awareness of project status.
- Optimized the display logic to ensure the current project's data is clearly presented, avoiding confusion with other projects.
- Enhanced the styling for better visual feedback on task completion status and designer assignments, ensuring a more user-friendly interface.
0235711 1 week ago
parent
commit
24faed51d7

+ 184 - 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 || '' // ✅ 添加空间信息
         };
         
         // 添加到映射
@@ -593,56 +609,55 @@ export class Dashboard implements OnInit, OnDestroy {
     
     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项目(忙碌🔵)
-      
-      // 使用项目索引映射到具体天数,跳过第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;
+      // ✅ 应用方案:使用真实字段数据
+      const projectData = project.data || {};
       
-      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);
-      
-      // 项目开始时间:交付前3-7天
-      const projectDuration = 3 + (index % 5); // 3-7天的项目周期
-      const adjustedStartDate = new Date(adjustedEndDate.getTime() - projectDuration * 24 * 60 * 60 * 1000);
+      // 2. 获取真实的交付日期
+      let realEndDate: Date;
+      if (project.deadline) {
+        realEndDate = project.deadline instanceof Date ? project.deadline : new Date(project.deadline);
+      } else {
+        // 降级:如果没有交付日期,使用开始时间 + 7天
+        realEndDate = new Date(realStartDate.getTime() + 7 * 24 * 60 * 60 * 1000);
+      }
       
-      // 🆕 小图对图时间:设置在软装和渲染之间,便于展示
-      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 +667,7 @@ export class Dashboard implements OnInit, OnDestroy {
         status = 'warning';
       }
       
-      // 映射阶段
+      // 6. 映射阶段
       const stageMap: Record<string, 'plan' | 'model' | 'decoration' | 'render' | 'delivery'> = {
         '方案设计': 'plan',
         '方案规划': 'plan',
@@ -669,19 +684,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 +764,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);
         
-        // 后期截止 = 交付日期 - 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);
+        const modelingDeadline = new Date(currentDate);
+        modelingDeadline.setDate(modelingDeadline.getDate() + modelingDays);
         
-        // 🔥 小图对图时间 = 交付日期 - 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(modelingDeadline);
+        const softDecorDeadline = new Date(currentDate);
+        softDecorDeadline.setDate(softDecorDeadline.getDate() + softDecorDays);
+        
+        currentDate = new Date(softDecorDeadline);
+        const renderingDeadline = new Date(currentDate);
+        renderingDeadline.setDate(renderingDeadline.getDate() + renderingDays);
+        
+        currentDate = new Date(renderingDeadline);
+        const postProcessingDeadline = new Date(currentDate);
+        postProcessingDeadline.setDate(postProcessingDeadline.getDate() + postProcessDays);
+        
+        // 更新对图时间(软装和渲染之间)
+        const reviewTime = softDecorDeadline.getTime() + (renderingDeadline.getTime() - softDecorDeadline.getTime()) * 0.5;
+        realReviewDate = new Date(reviewTime);
+        realReviewDate.setHours(14, 0, 0, 0);
         
         phaseDeadlines = {
           modeling: {
-            startDate: 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,26 +837,31 @@ 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 // ✅ 保留原始数据,供后续使用
       };
     });
     

+ 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;
+  }
 }
 

+ 151 - 21
src/modules/project/services/project-space-deliverable.service.ts

@@ -131,11 +131,27 @@ export class ProjectSpaceDeliverableService {
       const project = await projectQuery.get(projectId);
       const projectName = project.get('title') || project.get('name') || '未命名项目';
 
-      // 2. 获取项目的所有空间(Product)
+      // ✅ 应用方案:获取项目的所有空间(Product),包含负责人信息
       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
       const 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)`);
 
@@ -155,8 +171,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 +197,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 +223,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 +251,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,15 +369,17 @@ 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') || {};
@@ -353,16 +422,61 @@ export class ProjectSpaceDeliverableService {
         assignee?: string;
       }> = [];
 
-      // 获取阶段负责人
+      // ✅ 应用方案:获取阶段负责人信息
       const phaseInfo = phaseDeadlines[phaseKey];
       const assignee = phaseInfo?.assignee;
       let assigneeName: string | undefined;
       
-      if (assignee && assignee.objectId) {
-        // 如果有负责人指针,可以在这里查询(为了性能,暂时不实时查询)
-        assigneeName = '待查询';
+      if (assignee) {
+        // 如果 assignee 是对象(Parse Pointer)
+        if (assignee.objectId) {
+          // 为了性能,暂时不实时查询,但保留接口
+          assigneeName = undefined; // 可以后续实现查询逻辑
+        } 
+        // 如果 assignee 是字符串(ID)
+        else if (typeof assignee === 'string') {
+          assigneeName = undefined; // 可以后续实现查询逻辑
+        }
+        // 如果 assignee 是对象且包含 name 字段
+        else if (assignee.name) {
+          assigneeName = assignee.name;
+        }
       }
-
+      
+      // ✅ 应用方案:为每个空间查找负责人
+      // 优先使用空间负责人(Product.profile),其次使用阶段负责人
+      const spaceAssigneeMap = new Map<string, string>();
+      if (products) {
+        products.forEach(product => {
+          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(product.id!, profileName);
+              console.log(`📊 空间 ${product.get('productName')} 的负责人: ${profileName}`);
+            } else {
+              console.warn(`⚠️ 空间 ${product.get('productName')} 的负责人信息无法获取`, profile);
+            }
+          }
+        });
+      }
+      
       spaceInfos.forEach(space => {
         const fileCount = space.deliverableTypes[phaseConfig.key];
         totalFiles += fileCount;
@@ -370,10 +484,26 @@ export class ProjectSpaceDeliverableService {
         if (fileCount > 0) {
           completedSpaces++;
         } else {
+          // ✅ 应用方案:未完成空间列表,优先使用空间负责人,其次使用阶段负责人
+          // 注意:space.spaceId 应该是 product.id,需要确保匹配
+          let spaceAssignee = spaceAssigneeMap.get(space.spaceId);
+          
+          // 如果找不到,尝试使用阶段负责人
+          if (!spaceAssignee) {
+            spaceAssignee = assigneeName;
+          }
+          
+          // 如果还是没有,设置为"未分配"
+          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
+