Преглед изворни кода

refactor: enhance project timeline visualization and status handling

- Updated project timeline HTML to improve project status representation with dynamic background gradients based on project status.
- Refactored CSS for project bars and markers to enhance visual clarity and responsiveness, including new styles for overdue and urgent statuses.
- Simplified TypeScript logic for determining project bar backgrounds and completion rates, improving maintainability and readability.
- Removed outdated progress line logic to streamline the timeline component, focusing on completion markers for better user experience.
0235711 пре 2 дана
родитељ
комит
d8f6254143

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

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

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

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

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

@@ -175,7 +175,7 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
       return {
         left: '0%',
         width: '2px', // 最小可见宽度
-        background: this.getProjectUrgencyColor(project)
+        background: this.getProjectBarBackground(project)
       };
     }
     
@@ -195,15 +195,15 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
       return {
         left: '0%',
         width: '2px',
-        background: this.getProjectUrgencyColor(project)
+        background: this.getProjectBarBackground(project)
       };
     }
     
     const left = ((projectStart - rangeStart) / rangeDuration) * 100;
     const width = ((projectEnd - projectStart) / rangeDuration) * 100;
     
-    // 🆕 根据时间紧急度获取颜色(而不是阶段)
-    const background = this.getProjectUrgencyColor(project);
+    // 🆕 根据状态生成底色
+    const background = this.getProjectBarBackground(project);
     
     return {
       left: `${Math.max(0, left)}%`,
@@ -232,83 +232,62 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   }
   
   /**
-   * 🆕 根据时间紧急度获取项目条颜色
-   * 规则:
-   * - 正常进行(距离最近事件1天+):绿色
-   * - 临近事件前一天(24小时内):黄色
-   * - 事件当天(6小时以上):橙色
-   * - 紧急情况(6小时内):红色
+   * 🆕 根据项目状态生成背景渐变
    */
-  getProjectUrgencyColor(project: ProjectTimeline): string {
-    const now = this.currentTime.getTime();
-    
-    // 找到最近的未来事件(对图或交付)
-    const upcomingEvents: { date: Date; type: string }[] = [];
-    
-    if (project.reviewDate && project.reviewDate.getTime() >= now) {
-      upcomingEvents.push({ date: project.reviewDate, type: 'review' });
-    }
-    if (project.deliveryDate && project.deliveryDate.getTime() >= now) {
-      upcomingEvents.push({ date: project.deliveryDate, type: 'delivery' });
+  private getProjectBarBackground(project: ProjectTimeline): string {
+    if (project.isStalled) {
+      return 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)';
     }
     
-    // 如果没有未来事件,使用默认绿色(项目正常进行)
-    if (upcomingEvents.length === 0) {
-      return 'linear-gradient(135deg, #86EFAC 0%, #4ADE80 100%)'; // 绿色
-    }
+    const backgrounds: Record<string, string> = {
+      normal: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)',
+      warning: 'linear-gradient(135deg, #fef7c3 0%, #fde68a 100%)',
+      urgent: 'linear-gradient(135deg, #ffe7d4 0%, #fdba74 100%)',
+      overdue: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
+      stalled: 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)',
+      critical: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
+      low: 'linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%)',
+      medium: 'linear-gradient(135deg, #fef7c3 0%, #fde68a 100%)',
+      high: 'linear-gradient(135deg, #ffe7d4 0%, #fdba74 100%)'
+    };
     
-    // 找到最近的事件
-    upcomingEvents.sort((a, b) => a.date.getTime() - b.date.getTime());
-    const nearestEvent = upcomingEvents[0];
-    const eventTime = nearestEvent.date.getTime();
-    
-    // 计算时间差(毫秒)
-    const timeDiff = eventTime - now;
-    const hoursDiff = timeDiff / (1000 * 60 * 60);
-    const daysDiff = timeDiff / (1000 * 60 * 60 * 24);
-    
-    // 判断是否是同一天
-    const nowDate = new Date(now);
-    const eventDate = nearestEvent.date;
-    const isSameDay = nowDate.getFullYear() === eventDate.getFullYear() &&
-                      nowDate.getMonth() === eventDate.getMonth() &&
-                      nowDate.getDate() === eventDate.getDate();
-    
-    let color = '';
-    let colorName = '';
-    
-    // 🔴 红色:距离事件时间不到6小时(紧急)
-    if (hoursDiff < 6) {
-      color = 'linear-gradient(135deg, #FCA5A5 0%, #EF4444 100%)';
-      colorName = '🔴 红色(紧急 - 6小时内)';
-    }
-    // 🟠 橙色:事件当天但还有6小时以上
-    else if (isSameDay) {
-      color = 'linear-gradient(135deg, #FCD34D 0%, #F59E0B 100%)';
-      colorName = '🟠 橙色(当天 - 6小时+)';
-    }
-    // 🟡 黄色:距离事件前一天(24小时内但不是当天)
-    else if (hoursDiff < 24) {
-      color = 'linear-gradient(135deg, #FEF08A 0%, #EAB308 100%)';
-      colorName = '🟡 黄色(前一天)';
+    return backgrounds[project.status] || 'linear-gradient(135deg, #e2e8f0 0%, #f8fafc 100%)';
+  }
+  
+  getProjectStatusClass(project: ProjectTimeline): string {
+    if (project.isStalled) {
+      return 'status-stalled';
     }
-    // 🟢 绿色:正常进行(距离事件1天+)
-    else {
-      color = 'linear-gradient(135deg, #86EFAC 0%, #4ADE80 100%)';
-      colorName = '🟢 绿色(正常)';
+    return `status-${project.status || 'normal'}`;
+  }
+  
+  getProjectCompletionRate(project: ProjectTimeline): number {
+    const summary = this.getSpaceDeliverableSummary(project.projectId);
+    if (summary) {
+      return Math.round(summary.overallCompletionRate);
     }
-    
-    // 调试日志(只在首次加载时输出,避免刷屏)
-    if (Math.random() < 0.1) { // 10%概率输出
-      console.log(`🎨 项目颜色:${project.projectName}`, {
-        最近事件: `${nearestEvent.type === 'review' ? '对图' : '交付'} - ${nearestEvent.date.toLocaleString('zh-CN')}`,
-        剩余时间: `${hoursDiff.toFixed(1)}小时 (${daysDiff.toFixed(1)}天)`,
-        是否当天: isSameDay,
-        颜色判断: colorName
-      });
+    if (typeof project.stageProgress === 'number') {
+      return Math.round(Math.max(0, Math.min(100, project.stageProgress)));
     }
-    
-    return color;
+    return 0;
+  }
+  
+  getProjectCompletionColor(project: ProjectTimeline): string {
+    return this.getCompletionColor(this.getProjectCompletionRate(project));
+  }
+  
+  getCompletionMarkerLeft(project: ProjectTimeline): string {
+    const rate = this.getProjectCompletionRate(project);
+    const clamped = Math.max(8, Math.min(92, rate));
+    return `calc(${clamped}% - 12px)`;
+  }
+  
+  private getCompletionColor(completionRate: number): string {
+    if (completionRate >= 80) return '#16a34a';
+    if (completionRate >= 60) return '#22c55e';
+    if (completionRate >= 40) return '#f59e0b';
+    if (completionRate >= 20) return '#f97316';
+    return '#ef4444';
   }
   
   /**
@@ -911,63 +890,6 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     return lines.join('\n');
   }
   
-  /**
-   * 🆕 获取进度线标签(基于项目完成情况)
-   */
-  getProgressLineLabel(project: ProjectTimeline): string {
-    const summary = this.getSpaceDeliverableSummary(project.projectId);
-    if (!summary) return '加载中...';
-    return `进度:${summary.overallCompletionRate}%`;
-  }
-  
-  /**
-   * 🆕 获取项目的进度线位置(基于实际完成率)
-   * 进度线表示:在项目时间轴上,根据完成率显示当前进度的位置
-   * @param project 项目信息
-   * @returns CSS left 位置
-   */
-  getProgressLinePosition(project: ProjectTimeline): string {
-    const summary = this.getSpaceDeliverableSummary(project.projectId);
-    if (!summary) return '0%';
-    
-    // 根据完成率计算在项目条上的位置
-    const completionRate = summary.overallCompletionRate;
-    
-    // 获取项目在时间轴上的起始位置和宽度
-    const rangeStart = this.timeRangeStart.getTime();
-    const rangeEnd = this.timeRangeEnd.getTime();
-    const rangeDuration = rangeEnd - rangeStart;
-    
-    const projectStart = Math.max(project.startDate.getTime(), rangeStart);
-    const projectEnd = Math.min(project.endDate.getTime(), rangeEnd);
-    const projectDuration = projectEnd - projectStart;
-    
-    // 项目条的起始位置(百分比)
-    const projectLeft = ((projectStart - rangeStart) / rangeDuration) * 100;
-    
-    // 项目条的宽度(百分比)
-    const projectWidth = (projectDuration / rangeDuration) * 100;
-    
-    // 进度线在项目条内的位置 = 项目起始位置 + 项目宽度 × 完成率
-    const progressPosition = projectLeft + (projectWidth * completionRate / 100);
-    
-    // 🔧 考虑左侧项目名称列的宽度(180px)
-    const result = `calc(180px + (100% - 180px) * ${Math.max(0, Math.min(100, progressPosition)) / 100})`;
-    
-    return result;
-  }
-  
-  /**
-   * 🆕 获取进度线的颜色(基于完成率)
-   */
-  getProgressLineColor(completionRate: number): string {
-    if (completionRate >= 80) return '#4CAF50'; // 绿色
-    if (completionRate >= 60) return '#8BC34A'; // 浅绿
-    if (completionRate >= 40) return '#FFC107'; // 黄色
-    if (completionRate >= 20) return '#FF9800'; // 橙色
-    return '#F44336'; // 红色
-  }
-  
   /**
    * 获取今日标签(含时分)- 保留用于时间参考
    */

+ 119 - 63
src/modules/project/services/project-space-deliverable.service.ts

@@ -132,28 +132,36 @@ export class ProjectSpaceDeliverableService {
       const projectName = project.get('title') || project.get('name') || '未命名项目';
 
       // ✅ 应用方案:获取项目的所有空间(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`);
-        }
-      });
+      let products: FmodeObject[] = [];
+      try {
+        const productQuery = new Parse.Query('Product');
+        productQuery.equalTo('project', project.toPointer());
+        productQuery.ascending('createdAt');
+        productQuery.include('profile'); // ✅ 包含负责人信息(如果存在)
+        // ✅ 优化:如果 profile 是 Pointer,需要 include profile 的 name 字段
+        // 注意:Parse 的 include 会自动包含关联对象的基本字段,但为了确保 name 字段可用,我们显式 include
+        products = await productQuery.find();
+        
+        // ✅ 调试:检查 profile 字段是否正确加载
+        console.log(`📊 查询到 ${products.length} 个 Product,检查 profile 字段:`);
+        products.forEach((product, index) => {
+          const profile = product.get('profile');
+          const productName = product.get('productName') || '未命名';
+          if (profile) {
+            const profileName = profile.get?.('name') || profile.name || '未知';
+            console.log(`  ${index + 1}. ${productName}: profile = ${profileName}`);
+          } else {
+            console.log(`  ${index + 1}. ${productName}: 无 profile`);
+          }
+        });
 
-      console.log(`📊 项目 ${projectName} 共有 ${products.length} 个空间(Product)`);
+        console.log(`📊 项目 ${projectName} 共有 ${products.length} 个空间(Product)`);
+      } catch (productError: any) {
+        // ✅ 容错处理:Product 查询失败时,记录警告但继续处理
+        console.warn(`⚠️ Product 查询失败,将使用空的空间列表:`, productError.message || productError.toString());
+        console.warn(`⚠️ Product 错误详情:`, productError);
+        products = []; // 查询失败时使用空数组
+      }
 
       // 3. 去重:按空间名称去重(忽略大小写和首尾空格)
       const uniqueProducts = this.deduplicateProducts(products);
@@ -384,6 +392,13 @@ export class ProjectSpaceDeliverableService {
     // 获取项目阶段截止信息中的负责人
     const projectData = project.get('data') || {};
     const phaseDeadlines = projectData.phaseDeadlines || {};
+    
+    // ✅ 🆕 获取 designerAssignmentStats 统计数据(主要数据源)
+    const projectDate = project.get('date') || {};
+    const designerAssignmentStats = projectDate.designerAssignmentStats || {};
+    const projectLeader = designerAssignmentStats.projectLeader || null;
+    const teamMembers = designerAssignmentStats.teamMembers || [];
+    const crossTeamCollaborators = designerAssignmentStats.crossTeamCollaborators || [];
 
     // 阶段映射:交付物类型 -> 阶段名称
     const phaseMap = {
@@ -411,43 +426,51 @@ export class ProjectSpaceDeliverableService {
 
     const result: any = {};
 
-    // 计算每个阶段的进度
-    Object.entries(phaseMap).forEach(([phaseKey, phaseConfig]) => {
-      const requiredSpaces = spaceInfos.length; // 假设所有空间都需要各阶段交付物
-      let completedSpaces = 0;
-      let totalFiles = 0;
-      const incompleteSpaces: Array<{
-        spaceId: string;
-        spaceName: string;
-        assignee?: string;
-      }> = [];
-
-      // ✅ 应用方案:获取阶段负责人信息
-      const phaseInfo = phaseDeadlines[phaseKey];
-      const assignee = phaseInfo?.assignee;
-      let assigneeName: string | undefined;
-      
-      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;
+    // ✅ 🆕 构建空间ID到负责人姓名的映射(优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee)
+    const spaceAssigneeMap = new Map<string, string>();
+    
+    // 优先级1:从 designerAssignmentStats 获取空间分配信息(最准确)
+    // 1.1 项目负责人的空间分配
+    if (projectLeader && projectLeader.assignedSpaces && Array.isArray(projectLeader.assignedSpaces)) {
+      projectLeader.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
+        if (space.id && projectLeader.name) {
+          spaceAssigneeMap.set(space.id, projectLeader.name);
+          console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${projectLeader.name} (项目负责人)`);
         }
+      });
+    }
+    
+    // 1.2 团队成员的空间分配
+    teamMembers.forEach((member: { id: string; name: string; isProjectLeader?: boolean; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
+      if (member.assignedSpaces && Array.isArray(member.assignedSpaces) && member.name) {
+        member.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
+          if (space.id && !spaceAssigneeMap.has(space.id)) {
+            // 如果该空间还没有分配负责人,则使用当前成员
+            spaceAssigneeMap.set(space.id, member.name);
+            console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${member.name}${member.isProjectLeader ? ' (项目负责人)' : ''}`);
+          }
+        });
       }
-      
-      // ✅ 应用方案:为每个空间查找负责人
-      // 优先使用空间负责人(Product.profile),其次使用阶段负责人
-      const spaceAssigneeMap = new Map<string, string>();
-      if (products) {
-        products.forEach(product => {
+    });
+    
+    // 1.3 跨组合作者的空间分配
+    crossTeamCollaborators.forEach((collaborator: { id: string; name: string; assignedSpaces?: Array<{ id: string; name: string; area?: number }> }) => {
+      if (collaborator.assignedSpaces && Array.isArray(collaborator.assignedSpaces) && collaborator.name) {
+        collaborator.assignedSpaces.forEach((space: { id: string; name: string; area?: number }) => {
+          if (space.id && !spaceAssigneeMap.has(space.id)) {
+            spaceAssigneeMap.set(space.id, collaborator.name);
+            console.log(`📊 [designerAssignmentStats] 空间 ${space.name} (ID: ${space.id}) 的负责人: ${collaborator.name} (跨组合作)`);
+          }
+        });
+      }
+    });
+    
+    // 优先级2:从 Product.profile 获取空间负责人(如果 designerAssignmentStats 中没有)
+    if (products) {
+      products.forEach(product => {
+        const spaceId = product.id!;
+        // 如果该空间还没有分配负责人,才尝试使用 Product.profile
+        if (!spaceAssigneeMap.has(spaceId)) {
           const profile = product.get('profile');
           if (profile) {
             // ✅ 优化:支持多种方式获取设计师姓名
@@ -468,13 +491,46 @@ export class ProjectSpaceDeliverableService {
             }
             
             if (profileName) {
-              spaceAssigneeMap.set(product.id!, profileName);
-              console.log(`📊 空间 ${product.get('productName')} 的负责人: ${profileName}`);
+              spaceAssigneeMap.set(spaceId, profileName);
+              console.log(`📊 [Product.profile] 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人: ${profileName}`);
             } else {
-              console.warn(`⚠️ 空间 ${product.get('productName')} 的负责人信息无法获取`, profile);
+              console.warn(`⚠️ 空间 ${product.get('productName')} (ID: ${spaceId}) 的负责人信息无法获取`, profile);
             }
           }
-        });
+        }
+      });
+    }
+
+    // 计算每个阶段的进度
+    Object.entries(phaseMap).forEach(([phaseKey, phaseConfig]) => {
+      const requiredSpaces = spaceInfos.length; // 假设所有空间都需要各阶段交付物
+      let completedSpaces = 0;
+      let totalFiles = 0;
+      const incompleteSpaces: Array<{
+        spaceId: string;
+        spaceName: string;
+        assignee?: string;
+      }> = [];
+
+      // ✅ 优先级3:获取阶段负责人信息(作为最后备选)
+      const phaseInfo = phaseDeadlines[phaseKey];
+      const assignee = phaseInfo?.assignee;
+      let phaseAssigneeName: string | undefined;
+      
+      if (assignee) {
+        // 如果 assignee 是对象(Parse Pointer)
+        if (assignee.objectId) {
+          // 为了性能,暂时不实时查询,但保留接口
+          phaseAssigneeName = undefined; // 可以后续实现查询逻辑
+        } 
+        // 如果 assignee 是字符串(ID)
+        else if (typeof assignee === 'string') {
+          phaseAssigneeName = undefined; // 可以后续实现查询逻辑
+        }
+        // 如果 assignee 是对象且包含 name 字段
+        else if (assignee.name) {
+          phaseAssigneeName = assignee.name;
+        }
       }
       
       spaceInfos.forEach(space => {
@@ -484,13 +540,13 @@ export class ProjectSpaceDeliverableService {
         if (fileCount > 0) {
           completedSpaces++;
         } else {
-          // ✅ 应用方案:未完成空间列表,优先使用空间负责人,其次使用阶段负责人
-          // 注意:space.spaceId 应该是 product.id,需要确保匹配
+          // ✅ 应用方案:未完成空间列表,按优先级获取负责人
+          // 优先级:designerAssignmentStats > Product.profile > phaseDeadlines.assignee
           let spaceAssignee = spaceAssigneeMap.get(space.spaceId);
           
           // 如果找不到,尝试使用阶段负责人
           if (!spaceAssignee) {
-            spaceAssignee = assigneeName;
+            spaceAssignee = phaseAssigneeName;
           }
           
           // 如果还是没有,设置为"未分配"