Просмотр исходного кода

feat: implement dual-column layout for todo tasks and urgent events

- Introduced a new dual-column layout in the dashboard for better organization of todo tasks and urgent events.
- Enhanced the UI with distinct sections for "待办问题" and "紧急事件", improving visibility and accessibility.
- Added loading states and empty states for better user feedback during data fetching.
- Implemented responsive styles to ensure usability across different screen sizes.
- Updated TypeScript logic to calculate and display urgent events based on project timelines.
0235711 23 часов назад
Родитель
Сommit
b9986d302b
17 измененных файлов с 2679 добавлено и 51 удалено
  1. 161 11
      src/app/pages/team-leader/dashboard/dashboard.html
  2. 193 0
      src/app/pages/team-leader/dashboard/dashboard.scss
  3. 177 4
      src/app/pages/team-leader/dashboard/dashboard.ts
  4. 164 0
      src/app/pages/team-leader/project-timeline/project-progress-modal.html
  5. 493 0
      src/app/pages/team-leader/project-timeline/project-progress-modal.scss
  6. 130 0
      src/app/pages/team-leader/project-timeline/project-progress-modal.ts
  7. 28 4
      src/app/pages/team-leader/project-timeline/project-timeline.html
  8. 115 4
      src/app/pages/team-leader/project-timeline/project-timeline.scss
  9. 146 21
      src/app/pages/team-leader/project-timeline/project-timeline.ts
  10. 24 2
      src/modules/project/components/project-bottom-card/project-bottom-card.component.html
  11. 65 1
      src/modules/project/components/project-bottom-card/project-bottom-card.component.scss
  12. 58 3
      src/modules/project/components/project-bottom-card/project-bottom-card.component.ts
  13. 2 0
      src/modules/project/components/project-progress-modal/index.ts
  14. 164 0
      src/modules/project/components/project-progress-modal/project-progress-modal.component.html
  15. 493 0
      src/modules/project/components/project-progress-modal/project-progress-modal.component.scss
  16. 135 0
      src/modules/project/components/project-progress-modal/project-progress-modal.component.ts
  17. 131 1
      src/modules/project/services/project-space-deliverable.service.ts

+ 161 - 11
src/app/pages/team-leader/dashboard/dashboard.html

@@ -262,26 +262,38 @@
     }
   </section>
 
-  <!-- 待办任务优先级排序(基于项目问题板块) -->
-  <section class="todo-section">
+  <!-- 🆕 待办任务双栏布局(待办问题 + 紧急事件) -->
+  <section class="todo-section todo-section-dual">
     <div class="section-header">
-      <h2>
-        待办任务
-        @if (todoTasksFromIssues.length > 0) {
-          <span class="task-count">({{ todoTasksFromIssues.length }})</span>
-        }
-      </h2>
+      <h2>待办事项</h2>
       <button 
         class="btn-refresh" 
         (click)="refreshTodoTasks()"
-        [disabled]="loadingTodoTasks"
-        title="刷新待办任务">
-        <svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="loadingTodoTasks">
+        [disabled]="loadingTodoTasks || loadingUrgentEvents"
+        title="刷新待办事项">
+        <svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="loadingTodoTasks || loadingUrgentEvents">
           <path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
         </svg>
       </button>
     </div>
     
+    <!-- 🆕 双栏容器 -->
+    <div class="todo-dual-columns">
+      <!-- ========== 左栏:待办问题 ========== -->
+      <div class="todo-column todo-column-issues">
+        <div class="column-header">
+          <h3>
+            <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
+              <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
+            </svg>
+            待办问题
+            @if (todoTasksFromIssues.length > 0) {
+              <span class="task-count">({{ todoTasksFromIssues.length }})</span>
+            }
+          </h3>
+          <span class="column-subtitle">来自项目问题板块</span>
+        </div>
+    
     <!-- 加载状态 -->
     @if (loadingTodoTasks) {
       <div class="loading-state">
@@ -394,6 +406,144 @@
         }
       </div>
     }
+      </div>
+      <!-- ========== 左栏结束 ========== -->
+      
+      <!-- ========== 右栏:紧急事件 ========== -->
+      <div class="todo-column todo-column-urgent">
+        <div class="column-header">
+          <h3>
+            <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
+              <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
+            </svg>
+            紧急事件
+            @if (urgentEvents.length > 0) {
+              <span class="task-count urgent">({{ urgentEvents.length }})</span>
+            }
+          </h3>
+          <span class="column-subtitle">自动计算的截止事件</span>
+        </div>
+        
+        <!-- 加载状态 -->
+        @if (loadingUrgentEvents) {
+          <div class="loading-state">
+            <svg class="spinner" viewBox="0 0 50 50">
+              <circle cx="25" cy="25" r="20" fill="none" stroke-width="4"></circle>
+            </svg>
+            <p>计算紧急事件中...</p>
+          </div>
+        }
+        
+        <!-- 空状态 -->
+        @if (!loadingUrgentEvents && urgentEvents.length === 0) {
+          <div class="empty-state">
+            <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
+              <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
+            </svg>
+            <p>暂无紧急事件</p>
+            <p class="hint">所有项目时间节点正常 ✅</p>
+          </div>
+        }
+        
+        <!-- 紧急事件列表 -->
+        @if (!loadingUrgentEvents && urgentEvents.length > 0) {
+          <div class="todo-list-compact urgent-list">
+            @for (event of urgentEvents; track event.id) {
+              <div class="todo-item-compact urgent-item" [attr.data-urgency]="event.urgencyLevel">
+                <!-- 左侧紧急程度色条 -->
+                <div class="urgency-indicator" [attr.data-urgency]="event.urgencyLevel"></div>
+                
+                <!-- 事件内容 -->
+                <div class="task-content">
+                  <!-- 标题行 -->
+                  <div class="task-header">
+                    <span class="task-title">{{ event.title }}</span>
+                    <div class="task-badges">
+                      <span class="badge badge-urgency" [attr.data-urgency]="event.urgencyLevel">
+                        @if (event.urgencyLevel === 'critical') { 🔴 紧急 }
+                        @else if (event.urgencyLevel === 'high') { 🟠 重要 }
+                        @else { 🟡 注意 }
+                      </span>
+                      <span class="badge badge-event-type">
+                        @if (event.eventType === 'review') { 对图 }
+                        @else if (event.eventType === 'delivery') { 交付 }
+                        @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
+                      </span>
+                    </div>
+                  </div>
+                  
+                  <!-- 描述 -->
+                  <div class="task-description">
+                    {{ event.description }}
+                  </div>
+                  
+                  <!-- 项目信息行 -->
+                  <div class="task-meta">
+                    <span class="project-info">
+                      <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                        <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
+                      </svg>
+                      项目: {{ event.projectName }}
+                    </span>
+                    @if (event.designerName) {
+                      <span class="designer-info">
+                        <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                          <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
+                        </svg>
+                        设计师: {{ event.designerName }}
+                      </span>
+                    }
+                  </div>
+                  
+                  <!-- 底部信息行 -->
+                  <div class="task-footer">
+                    <span class="deadline-info" [class.overdue]="event.overdueDays && event.overdueDays > 0">
+                      <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                        <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/>
+                      </svg>
+                      截止: {{ event.deadline | date:'MM-dd HH:mm' }}
+                      @if (event.overdueDays && event.overdueDays > 0) {
+                        <span class="overdue-label">(逾期{{ event.overdueDays }}天)</span>
+                      }
+                      @else if (event.overdueDays && event.overdueDays < 0) {
+                        <span class="upcoming-label">(还剩{{ -event.overdueDays }}天)</span>
+                      }
+                      @else {
+                        <span class="today-label">(今天)</span>
+                      }
+                    </span>
+                    
+                    @if (event.completionRate !== undefined) {
+                      <span class="completion-info">
+                        <svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor">
+                          <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
+                        </svg>
+                        完成率: {{ event.completionRate }}%
+                      </span>
+                    }
+                  </div>
+                </div>
+                
+                <!-- 右侧操作按钮 -->
+                <div class="task-actions">
+                  <button 
+                    class="btn-action btn-view" 
+                    (click)="onProjectClick(event.projectId)"
+                    title="查看项目">
+                    <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
+                      <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
+                    </svg>
+                    查看项目
+                  </button>
+                </div>
+              </div>
+            }
+          </div>
+        }
+      </div>
+      <!-- ========== 右栏结束 ========== -->
+    </div>
+    <!-- ========== 双栏容器结束 ========== -->
   </section>
 
   <!-- 超期项目提醒 -->

+ 193 - 0
src/app/pages/team-leader/dashboard/dashboard.scss

@@ -1084,6 +1084,185 @@
   }
 }
 
+// 🆕 双栏布局样式
+.todo-section-dual {
+  // 双栏容器
+  .todo-dual-columns {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 24px;
+    margin-top: 20px;
+  }
+  
+  // 单栏样式
+  .todo-column {
+    display: flex;
+    flex-direction: column;
+    border: 1px solid #e5e7eb;
+    border-radius: 8px;
+    background: #fafafa;
+    overflow: hidden;
+    
+    .column-header {
+      padding: 16px;
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border-bottom: 1px solid #e5e7eb;
+      
+      h3 {
+        margin: 0 0 4px 0;
+        font-size: 16px;
+        font-weight: 600;
+        color: white;
+        display: flex;
+        align-items: center;
+        gap: 8px;
+        
+        svg {
+          flex-shrink: 0;
+        }
+        
+        .task-count {
+          font-size: 13px;
+          font-weight: 500;
+          padding: 2px 8px;
+          background: rgba(255, 255, 255, 0.2);
+          border-radius: 12px;
+          
+          &.urgent {
+            background: rgba(239, 68, 68, 0.9);
+            animation: pulse-glow 2s infinite;
+          }
+        }
+      }
+      
+      .column-subtitle {
+        font-size: 12px;
+        color: rgba(255, 255, 255, 0.8);
+        margin-left: 26px;
+      }
+    }
+    
+    // 左栏特定样式(待办问题)
+    &.todo-column-issues {
+      .column-header {
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      }
+    }
+    
+    // 右栏特定样式(紧急事件)
+    &.todo-column-urgent {
+      .column-header {
+        background: linear-gradient(135deg, #f97316 0%, #dc2626 100%);
+      }
+      
+      // 紧急事件特定样式
+      .urgent-item {
+        background: #fff8f8;
+        border-left-width: 4px;
+        
+        &[data-urgency="critical"] {
+          border-left-color: #dc2626;
+          background: #fef2f2;
+        }
+        
+        &[data-urgency="high"] {
+          border-left-color: #f97316;
+          background: #fff7ed;
+        }
+        
+        &[data-urgency="medium"] {
+          border-left-color: #f59e0b;
+          background: #fffbeb;
+        }
+      }
+      
+      .urgency-indicator {
+        width: 4px;
+        
+        &[data-urgency="critical"] {
+          background: linear-gradient(180deg, #dc2626 0%, #b91c1c 100%);
+          box-shadow: 0 0 10px rgba(220, 38, 38, 0.5);
+        }
+        
+        &[data-urgency="high"] {
+          background: linear-gradient(180deg, #f97316 0%, #ea580c 100%);
+        }
+        
+        &[data-urgency="medium"] {
+          background: linear-gradient(180deg, #f59e0b 0%, #d97706 100%);
+        }
+      }
+      
+      .badge-urgency {
+        &[data-urgency="critical"] {
+          background: #fee2e2;
+          color: #dc2626;
+          font-weight: 700;
+        }
+        
+        &[data-urgency="high"] {
+          background: #ffedd5;
+          color: #f97316;
+        }
+        
+        &[data-urgency="medium"] {
+          background: #fef3c7;
+          color: #f59e0b;
+        }
+      }
+      
+      .badge-event-type {
+        background: #dbeafe;
+        color: #1e40af;
+      }
+      
+      .task-description {
+        font-size: 13px;
+        color: #6b7280;
+        margin: 8px 0;
+        line-height: 1.5;
+      }
+      
+      .deadline-info {
+        &.overdue {
+          color: #dc2626;
+          font-weight: 600;
+        }
+        
+        .overdue-label {
+          color: #dc2626;
+          font-weight: 600;
+        }
+        
+        .upcoming-label {
+          color: #f97316;
+        }
+        
+        .today-label {
+          color: #f59e0b;
+          font-weight: 600;
+        }
+      }
+      
+      .completion-info {
+        font-weight: 500;
+      }
+    }
+  }
+}
+
+// 🆕 紧急事件脉冲动画
+@keyframes pulse-glow {
+  0%, 100% {
+    opacity: 1;
+    box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
+  }
+  50% {
+    opacity: 0.9;
+    box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
+  }
+}
+
 @keyframes rotate {
   from {
     transform: rotate(0deg);
@@ -1121,6 +1300,20 @@
       }
     }
   }
+  
+  // 🆕 双栏布局响应式
+  .todo-section-dual {
+    .todo-dual-columns {
+      grid-template-columns: 1fr;
+      gap: 16px;
+    }
+    
+    .todo-column {
+      .column-header h3 {
+        font-size: 15px;
+      }
+    }
+  }
 }
 
 /* 超期提醒样式 */

+ 177 - 4
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -79,6 +79,25 @@ interface TodoTaskFromIssue {
   tags?: string[];
 }
 
+/**
+ * 🆕 紧急事件接口
+ * 从项目时间轴自动计算,表示截止时间到了但未完成的事件
+ */
+interface UrgentEvent {
+  id: string;
+  title: string;
+  description: string;
+  eventType: 'review' | 'delivery' | 'phase_deadline'; // 事件类型
+  phaseName?: string; // 阶段名称(如果是阶段截止)
+  deadline: Date; // 截止时间
+  projectId: string;
+  projectName: string;
+  designerName?: string;
+  urgencyLevel: 'critical' | 'high' | 'medium'; // 紧急程度
+  overdueDays?: number; // 逾期天数(负数表示还有几天)
+  completionRate?: number; // 完成率(0-100)
+}
+
 // 员工请假记录接口
 interface LeaveRecord {
   id: string;
@@ -145,6 +164,10 @@ export class Dashboard implements OnInit, OnDestroy {
   todoTaskError: string = '';
   private todoTaskRefreshTimer: any;
   
+  // 🆕 紧急事件(从项目时间轴自动计算)
+  urgentEvents: UrgentEvent[] = [];
+  loadingUrgentEvents: boolean = false;
+  
   // 新增:当前用户信息
   currentUser = {
     name: '组长',
@@ -297,6 +320,8 @@ export class Dashboard implements OnInit, OnDestroy {
     
     // 新增:加载待办任务(从问题板块)
     await this.loadTodoTasksFromIssues();
+    // 🆕 计算紧急事件
+    this.calculateUrgentEvents();
     // 启动自动刷新
     this.startAutoRefresh();
   }
@@ -600,15 +625,18 @@ export class Dashboard implements OnInit, OnDestroy {
       const projectDuration = 3 + (index % 5); // 3-7天的项目周期
       const adjustedStartDate = new Date(adjustedEndDate.getTime() - projectDuration * 24 * 60 * 60 * 1000);
       
-      // 🆕 小图对图时间:优先使用真实的 demoday,否则默认为交付前1-2天
+      // 🆕 小图对图时间:设置在软装和渲染之间,便于展示
       let adjustedReviewDate: Date;
       if (project.demoday && project.demoday instanceof Date) {
         // 使用真实的小图对图日期
         adjustedReviewDate = project.demoday;
       } else {
-        // 默认计算:交付前1-2天
-        const reviewDaysBefore = 1 + (index % 2);
-        adjustedReviewDate = new Date(adjustedEndDate.getTime() - reviewDaysBefore * 24 * 60 * 60 * 1000);
+        // 🔥 修改为便于展示:设置在项目时间轴的中间位置(软装完成后)
+        // 计算项目周期的 60% 位置(软装后、渲染前)
+        const projectMidPoint = adjustedStartDate.getTime() + (projectDuration * 0.6 * 24 * 60 * 60 * 1000);
+        adjustedReviewDate = new Date(projectMidPoint);
+        // 设置具体时间为下午2点
+        adjustedReviewDate.setHours(14, 0, 0, 0);
       }
       
       // 计算距离交付还有几天
@@ -3830,6 +3858,139 @@ export class Dashboard implements OnInit, OnDestroy {
   refreshTodoTasks(): void {
     console.log('🔄 手动刷新待办任务...');
     this.loadTodoTasksFromIssues();
+    this.calculateUrgentEvents(); // 🆕 同时刷新紧急事件
+  }
+  
+  /**
+   * 🆕 从项目时间轴数据计算紧急事件
+   * 识别截止时间已到或即将到达但未完成的关键节点
+   */
+  calculateUrgentEvents(): void {
+    this.loadingUrgentEvents = true;
+    const events: UrgentEvent[] = [];
+    const now = new Date();
+    const oneDayMs = 24 * 60 * 60 * 1000;
+    
+    try {
+      // 从 projectTimelineData 中提取数据
+      this.projectTimelineData.forEach(project => {
+        // 1. 检查小图对图事件
+        if (project.reviewDate) {
+          const reviewTime = project.reviewDate.getTime();
+          const timeDiff = reviewTime - now.getTime();
+          const daysDiff = Math.ceil(timeDiff / oneDayMs);
+          
+          // 如果小图对图已经到期或即将到期(1天内),且不在已完成状态
+          if (daysDiff <= 1 && project.currentStage !== 'delivery') {
+            events.push({
+              id: `${project.projectId}-review`,
+              title: `小图对图截止`,
+              description: `项目「${project.projectName}」的小图对图时间已${daysDiff < 0 ? '逾期' : '临近'}`,
+              eventType: 'review',
+              deadline: project.reviewDate,
+              projectId: project.projectId,
+              projectName: project.projectName,
+              designerName: project.designerName,
+              urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
+              overdueDays: -daysDiff
+            });
+          }
+        }
+        
+        // 2. 检查交付事件
+        if (project.deliveryDate) {
+          const deliveryTime = project.deliveryDate.getTime();
+          const timeDiff = deliveryTime - now.getTime();
+          const daysDiff = Math.ceil(timeDiff / oneDayMs);
+          
+          // 如果交付已经到期或即将到期(1天内),且不在已完成状态
+          if (daysDiff <= 1 && project.currentStage !== 'delivery') {
+            const summary = project.spaceDeliverableSummary;
+            const completionRate = summary?.overallCompletionRate || 0;
+            
+            events.push({
+              id: `${project.projectId}-delivery`,
+              title: `项目交付截止`,
+              description: `项目「${project.projectName}」需要在 ${project.deliveryDate.toLocaleDateString()} 交付(当前完成率 ${completionRate}%)`,
+              eventType: 'delivery',
+              deadline: project.deliveryDate,
+              projectId: project.projectId,
+              projectName: project.projectName,
+              designerName: project.designerName,
+              urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
+              overdueDays: -daysDiff,
+              completionRate
+            });
+          }
+        }
+        
+        // 3. 检查各阶段截止时间
+        if (project.phaseDeadlines) {
+          const phaseMap = {
+            modeling: '建模',
+            softDecor: '软装',
+            rendering: '渲染',
+            postProcessing: '后期'
+          };
+          
+          Object.entries(project.phaseDeadlines).forEach(([key, phaseInfo]: [string, any]) => {
+            if (phaseInfo && phaseInfo.deadline) {
+              const deadline = new Date(phaseInfo.deadline);
+              const phaseTime = deadline.getTime();
+              const timeDiff = phaseTime - now.getTime();
+              const daysDiff = Math.ceil(timeDiff / oneDayMs);
+              
+              // 如果阶段已经到期或即将到期(1天内),且状态不是已完成
+              if (daysDiff <= 1 && phaseInfo.status !== 'completed') {
+                const phaseName = phaseMap[key as keyof typeof phaseMap] || key;
+                
+                // 获取该阶段的完成率
+                const summary = project.spaceDeliverableSummary;
+                let completionRate = 0;
+                if (summary && summary.phaseProgress) {
+                  const phaseProgress = summary.phaseProgress[key as keyof typeof summary.phaseProgress];
+                  completionRate = phaseProgress?.completionRate || 0;
+                }
+                
+                events.push({
+                  id: `${project.projectId}-phase-${key}`,
+                  title: `${phaseName}阶段截止`,
+                  description: `项目「${project.projectName}」的${phaseName}阶段截止时间已${daysDiff < 0 ? '逾期' : '临近'}(完成率 ${completionRate}%)`,
+                  eventType: 'phase_deadline',
+                  phaseName,
+                  deadline,
+                  projectId: project.projectId,
+                  projectName: project.projectName,
+                  designerName: project.designerName,
+                  urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
+                  overdueDays: -daysDiff,
+                  completionRate
+                });
+              }
+            }
+          });
+        }
+      });
+      
+      // 按紧急程度和时间排序
+      events.sort((a, b) => {
+        // 首先按紧急程度排序
+        const urgencyOrder = { critical: 0, high: 1, medium: 2 };
+        const urgencyDiff = urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel];
+        if (urgencyDiff !== 0) return urgencyDiff;
+        
+        // 相同紧急程度,按截止时间排序(越早越靠前)
+        return a.deadline.getTime() - b.deadline.getTime();
+      });
+      
+      this.urgentEvents = events;
+      console.log(`✅ 计算紧急事件完成,共 ${events.length} 个紧急事件`);
+      
+    } catch (error) {
+      console.error('❌ 计算紧急事件失败:', error);
+    } finally {
+      this.loadingUrgentEvents = false;
+    }
   }
   
   /**
@@ -3863,6 +4024,18 @@ export class Dashboard implements OnInit, OnDestroy {
     }
   }
   
+  /**
+   * 🆕 从紧急事件点击查看项目
+   */
+  onProjectClick(projectId: string): void {
+    if (!projectId) {
+      console.warn('⚠️ 项目ID为空');
+      return;
+    }
+    console.log(`🔍 查看紧急事件关联项目: ${projectId}`);
+    this.viewProjectDetails(projectId);
+  }
+  
   /**
    * 获取优先级配置
    */

+ 164 - 0
src/app/pages/team-leader/project-timeline/project-progress-modal.html

@@ -0,0 +1,164 @@
+<!-- 弹窗遮罩 -->
+<div class="modal-overlay" *ngIf="visible" (click)="onClose()">
+  <!-- 弹窗内容 -->
+  <div class="modal-content" (click)="onContentClick($event)">
+    <!-- 标题栏 -->
+    <div class="modal-header">
+      <h2 class="modal-title">
+        <span class="icon">📊</span>
+        项目进度详情
+      </h2>
+      <button class="close-btn" (click)="onClose()" aria-label="关闭">
+        <span>✕</span>
+      </button>
+    </div>
+
+    <!-- 项目基本信息 -->
+    <div class="project-info" *ngIf="summary">
+      <div class="info-row">
+        <span class="label">项目名称:</span>
+        <span class="value">{{ summary.projectName }}</span>
+      </div>
+      <div class="info-row">
+        <span class="label">空间总数:</span>
+        <span class="value">{{ summary.totalSpaces }} 个</span>
+      </div>
+      <div class="info-row">
+        <span class="label">已完成空间:</span>
+        <span class="value">{{ summary.spacesWithDeliverables }} / {{ summary.totalSpaces }}</span>
+      </div>
+    </div>
+
+    <!-- 整体进度 -->
+    <div class="overall-progress" *ngIf="summary">
+      <div class="progress-header">
+        <span class="label">整体完成率</span>
+        <span class="percentage" [style.color]="getOverallProgressColor()">
+          {{ summary.overallCompletionRate }}%
+        </span>
+        <span class="status-badge" [style.background-color]="getOverallProgressColor()">
+          {{ getOverallStatusLabel() }}
+        </span>
+      </div>
+      <div class="progress-bar-container">
+        <div class="progress-bar-bg">
+          <div 
+            class="progress-bar-fill"
+            [style.width.%]="summary.overallCompletionRate"
+            [style.background-color]="getOverallProgressColor()">
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 文件统计 -->
+    <div class="file-stats" *ngIf="summary">
+      <div class="stats-title">交付文件统计</div>
+      <div class="stats-grid">
+        <div class="stat-item">
+          <span class="stat-icon">🏗️</span>
+          <span class="stat-label">白模</span>
+          <span class="stat-value">{{ summary.totalByType.whiteModel }}</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-icon">🎨</span>
+          <span class="stat-label">软装</span>
+          <span class="stat-value">{{ summary.totalByType.softDecor }}</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-icon">🖼️</span>
+          <span class="stat-label">渲染</span>
+          <span class="stat-value">{{ summary.totalByType.rendering }}</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-icon">✨</span>
+          <span class="stat-label">后期</span>
+          <span class="stat-value">{{ summary.totalByType.postProcess }}</span>
+        </div>
+      </div>
+      <div class="stats-total">
+        总计:<strong>{{ summary.totalDeliverableFiles }}</strong> 个文件
+      </div>
+    </div>
+
+    <!-- 各阶段进度详情 -->
+    <div class="phase-progress" *ngIf="summary">
+      <div class="section-title">各阶段进度明细</div>
+      
+      <div class="phase-list">
+        <div 
+          *ngFor="let phase of getPhases()"
+          class="phase-item"
+          [class.expanded]="isPhaseExpanded(phase.name)">
+          
+          <!-- 阶段概览(可点击展开/收起) -->
+          <div class="phase-overview" (click)="togglePhase(phase.name)">
+            <div class="phase-header">
+              <span class="phase-icon">{{ PHASE_INFO[phase.name].icon }}</span>
+              <span class="phase-label">{{ phase.info.phaseLabel }}</span>
+              <span class="phase-progress-badge" [style.background-color]="getPhaseProgressColor(phase.info.completionRate)">
+                {{ phase.info.completionRate }}%
+              </span>
+              <span class="expand-icon">{{ isPhaseExpanded(phase.name) ? '▼' : '▶' }}</span>
+            </div>
+            
+            <div class="phase-summary">
+              <span class="summary-text">
+                已完成 {{ phase.info.completedSpaces }} / {{ phase.info.requiredSpaces }} 个空间
+                ({{ phase.info.totalFiles }} 个文件)
+              </span>
+            </div>
+            
+            <!-- 阶段进度条 -->
+            <div class="phase-progress-bar">
+              <div class="progress-bar-bg">
+                <div 
+                  class="progress-bar-fill"
+                  [style.width.%]="phase.info.completionRate"
+                  [style.background-color]="getPhaseProgressColor(phase.info.completionRate)">
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 未完成空间详情(展开时显示) -->
+          <div class="phase-details" *ngIf="isPhaseExpanded(phase.name)">
+            <div class="details-header">
+              <span class="details-title">未完成空间列表</span>
+              <span class="details-count">({{ phase.info.incompleteSpaces.length }} 个)</span>
+            </div>
+            
+            <div class="incomplete-spaces" *ngIf="phase.info.incompleteSpaces.length > 0">
+              <div 
+                *ngFor="let space of phase.info.incompleteSpaces"
+                class="space-item">
+                <span class="space-icon">📦</span>
+                <span class="space-name">{{ space.spaceName }}</span>
+                <span class="assignee" *ngIf="space.assignee">
+                  负责人:{{ space.assignee }}
+                </span>
+              </div>
+            </div>
+            
+            <div class="no-incomplete" *ngIf="phase.info.incompleteSpaces.length === 0">
+              <span class="success-icon">✅</span>
+              <span>所有空间已完成此阶段交付</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="modal-footer">
+      <button class="btn btn-secondary" (click)="onClose()">
+        关闭
+      </button>
+      <button class="btn btn-primary" (click)="onReportIssue()">
+        <span class="btn-icon">🐛</span>
+        提交问题
+      </button>
+    </div>
+  </div>
+</div>
+

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

@@ -0,0 +1,493 @@
+// 项目进度详情弹窗样式
+
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.6);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10000;
+  animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+.modal-content {
+  background: white;
+  border-radius: 12px;
+  width: 90%;
+  max-width: 800px;
+  max-height: 90vh;
+  overflow-y: auto;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
+  animation: slideUp 0.3s ease-out;
+  
+  // 自定义滚动条
+  &::-webkit-scrollbar {
+    width: 8px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 4px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #ccc;
+    border-radius: 4px;
+    
+    &:hover {
+      background: #999;
+    }
+  }
+}
+
+@keyframes slideUp {
+  from {
+    transform: translateY(30px);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+// 标题栏
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e0e0e0;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12px 12px 0 0;
+  
+  .modal-title {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: white;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    
+    .icon {
+      font-size: 24px;
+    }
+  }
+  
+  .close-btn {
+    background: rgba(255, 255, 255, 0.2);
+    border: none;
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    cursor: pointer;
+    color: white;
+    font-size: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+    
+    &:hover {
+      background: rgba(255, 255, 255, 0.3);
+      transform: scale(1.1);
+    }
+  }
+}
+
+// 项目基本信息
+.project-info {
+  padding: 20px 24px;
+  background: #f8f9fa;
+  border-bottom: 1px solid #e0e0e0;
+  
+  .info-row {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    .label {
+      font-weight: 500;
+      color: #666;
+      margin-right: 8px;
+    }
+    
+    .value {
+      color: #333;
+      font-weight: 600;
+    }
+  }
+}
+
+// 整体进度
+.overall-progress {
+  padding: 20px 24px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+  
+  .progress-header {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 12px;
+    
+    .label {
+      font-size: 16px;
+      font-weight: 600;
+      color: #333;
+    }
+    
+    .percentage {
+      font-size: 24px;
+      font-weight: 700;
+    }
+    
+    .status-badge {
+      padding: 4px 12px;
+      border-radius: 12px;
+      font-size: 12px;
+      color: white;
+      font-weight: 600;
+    }
+  }
+  
+  .progress-bar-container {
+    margin-top: 8px;
+  }
+}
+
+// 进度条样式
+.progress-bar-bg {
+  width: 100%;
+  height: 12px;
+  background: #e0e0e0;
+  border-radius: 6px;
+  overflow: hidden;
+  
+  .progress-bar-fill {
+    height: 100%;
+    border-radius: 6px;
+    transition: width 0.6s ease, background-color 0.3s ease;
+    background: linear-gradient(90deg, currentColor 0%, currentColor 100%);
+  }
+}
+
+// 文件统计
+.file-stats {
+  padding: 20px 24px;
+  border-bottom: 1px solid #e0e0e0;
+  
+  .stats-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+    margin-bottom: 16px;
+  }
+  
+  .stats-grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 12px;
+    margin-bottom: 12px;
+    
+    @media (max-width: 600px) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+  }
+  
+  .stat-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 12px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 1px solid #e0e0e0;
+    
+    .stat-icon {
+      font-size: 28px;
+      margin-bottom: 4px;
+    }
+    
+    .stat-label {
+      font-size: 12px;
+      color: #666;
+      margin-bottom: 4px;
+    }
+    
+    .stat-value {
+      font-size: 20px;
+      font-weight: 700;
+      color: #333;
+    }
+  }
+  
+  .stats-total {
+    text-align: center;
+    padding: 12px;
+    background: #e3f2fd;
+    border-radius: 8px;
+    font-size: 14px;
+    color: #1976d2;
+    
+    strong {
+      font-size: 18px;
+      font-weight: 700;
+    }
+  }
+}
+
+// 阶段进度
+.phase-progress {
+  padding: 20px 24px;
+  
+  .section-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+    margin-bottom: 16px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+.phase-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.phase-item {
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+  overflow: hidden;
+  transition: all 0.3s ease;
+  
+  &.expanded {
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+}
+
+.phase-overview {
+  padding: 16px;
+  cursor: pointer;
+  transition: background-color 0.2s;
+  
+  &:hover {
+    background-color: #f8f9fa;
+  }
+  
+  .phase-header {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 8px;
+    
+    .phase-icon {
+      font-size: 24px;
+    }
+    
+    .phase-label {
+      font-size: 16px;
+      font-weight: 600;
+      color: #333;
+      flex: 1;
+    }
+    
+    .phase-progress-badge {
+      padding: 4px 12px;
+      border-radius: 12px;
+      font-size: 14px;
+      color: white;
+      font-weight: 600;
+    }
+    
+    .expand-icon {
+      font-size: 12px;
+      color: #666;
+      transition: transform 0.3s ease;
+    }
+  }
+  
+  .phase-summary {
+    margin-bottom: 8px;
+    
+    .summary-text {
+      font-size: 13px;
+      color: #666;
+    }
+  }
+  
+  .phase-progress-bar {
+    margin-top: 8px;
+    
+    .progress-bar-bg {
+      height: 8px;
+    }
+  }
+}
+
+.phase-item.expanded .phase-overview .expand-icon {
+  transform: rotate(0deg);
+}
+
+.phase-details {
+  padding: 16px;
+  background: #f8f9fa;
+  border-top: 1px solid #e0e0e0;
+  animation: expandDetails 0.3s ease-out;
+  
+  .details-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 12px;
+    
+    .details-title {
+      font-size: 14px;
+      font-weight: 600;
+      color: #666;
+    }
+    
+    .details-count {
+      font-size: 12px;
+      color: #999;
+    }
+  }
+}
+
+@keyframes expandDetails {
+  from {
+    opacity: 0;
+    max-height: 0;
+  }
+  to {
+    opacity: 1;
+    max-height: 500px;
+  }
+}
+
+.incomplete-spaces {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.space-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 12px;
+  background: white;
+  border-radius: 6px;
+  border: 1px solid #e0e0e0;
+  
+  .space-icon {
+    font-size: 16px;
+  }
+  
+  .space-name {
+    flex: 1;
+    font-size: 14px;
+    color: #333;
+  }
+  
+  .assignee {
+    font-size: 12px;
+    color: #666;
+    background: #e3f2fd;
+    padding: 4px 8px;
+    border-radius: 4px;
+  }
+}
+
+.no-incomplete {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 20px;
+  background: #e8f5e9;
+  border-radius: 6px;
+  color: #4caf50;
+  font-size: 14px;
+  
+  .success-icon {
+    font-size: 20px;
+  }
+}
+
+// 底部按钮
+.modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 20px 24px;
+  border-top: 1px solid #e0e0e0;
+  background: #f8f9fa;
+  border-radius: 0 0 12px 12px;
+}
+
+.btn {
+  padding: 10px 20px;
+  border: none;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  
+  &:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  }
+  
+  &:active {
+    transform: translateY(0);
+  }
+}
+
+.btn-secondary {
+  background: #e0e0e0;
+  color: #666;
+  
+  &:hover {
+    background: #d0d0d0;
+  }
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  
+  &:hover {
+    background: linear-gradient(135deg, #5568d3 0%, #65408b 100%);
+  }
+  
+  .btn-icon {
+    font-size: 16px;
+  }
+}
+

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

@@ -0,0 +1,130 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ProjectSpaceDeliverableSummary, PhaseProgressInfo } from '../../../../modules/project/services/project-space-deliverable.service';
+import { PHASE_INFO, PhaseName } from '../../../models/project-phase.model';
+
+/**
+ * 项目进度详情弹窗组件
+ * 
+ * 功能:
+ * - 显示项目的整体进度统计
+ * - 按阶段展示完成情况(建模、软装、渲染、后期)
+ * - 显示未完成的空间列表和负责人
+ * - 提供问题提交入口
+ */
+@Component({
+  selector: 'app-project-progress-modal',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './project-progress-modal.html',
+  styleUrl: './project-progress-modal.scss'
+})
+export class ProjectProgressModalComponent implements OnInit {
+  @Input() summary: ProjectSpaceDeliverableSummary | null = null;
+  @Input() visible: boolean = false;
+  @Output() close = new EventEmitter<void>();
+  @Output() reportIssue = new EventEmitter<string>(); // 提交问题,传递项目ID
+
+  // 当前选中的阶段(用于展开/收起详情)
+  selectedPhase: PhaseName | null = null;
+
+  // 阶段常量
+  readonly PHASE_INFO = PHASE_INFO;
+
+  ngOnInit(): void {
+    console.log('📊 项目进度弹窗初始化', this.summary);
+  }
+
+  /**
+   * 关闭弹窗
+   */
+  onClose(): void {
+    this.close.emit();
+  }
+
+  /**
+   * 阻止点击弹窗内容时关闭
+   */
+  onContentClick(event: MouseEvent): void {
+    event.stopPropagation();
+  }
+
+  /**
+   * 切换阶段展开/收起
+   */
+  togglePhase(phaseName: PhaseName): void {
+    this.selectedPhase = this.selectedPhase === phaseName ? null : phaseName;
+  }
+
+  /**
+   * 判断阶段是否展开
+   */
+  isPhaseExpanded(phaseName: PhaseName): boolean {
+    return this.selectedPhase === phaseName;
+  }
+
+  /**
+   * 获取阶段列表
+   */
+  getPhases(): Array<{ name: PhaseName; info: PhaseProgressInfo }> {
+    if (!this.summary?.phaseProgress) return [];
+
+    return [
+      { name: 'modeling', info: this.summary.phaseProgress.modeling },
+      { name: 'softDecor', info: this.summary.phaseProgress.softDecor },
+      { name: 'rendering', info: this.summary.phaseProgress.rendering },
+      { name: 'postProcessing', info: this.summary.phaseProgress.postProcessing }
+    ];
+  }
+
+  /**
+   * 获取阶段进度颜色
+   */
+  getPhaseProgressColor(completionRate: number): string {
+    if (completionRate === 100) return '#4CAF50'; // 绿色
+    if (completionRate >= 75) return '#8BC34A'; // 浅绿
+    if (completionRate >= 50) return '#FFC107'; // 黄色
+    if (completionRate >= 25) return '#FF9800'; // 橙色
+    if (completionRate > 0) return '#FF5722'; // 深橙
+    return '#9E9E9E'; // 灰色(未开始)
+  }
+
+  /**
+   * 获取阶段状态标签
+   */
+  getPhaseStatusLabel(completionRate: number): string {
+    if (completionRate === 100) return '已完成';
+    if (completionRate >= 75) return '接近完成';
+    if (completionRate >= 50) return '进行中';
+    if (completionRate >= 25) return '刚开始';
+    if (completionRate > 0) return '已启动';
+    return '未开始';
+  }
+
+  /**
+   * 提交问题
+   */
+  onReportIssue(): void {
+    if (this.summary) {
+      this.reportIssue.emit(this.summary.projectId);
+    }
+  }
+
+  /**
+   * 获取整体完成率的颜色
+   */
+  getOverallProgressColor(): string {
+    if (!this.summary) return '#9E9E9E';
+    return this.getPhaseProgressColor(this.summary.overallCompletionRate);
+  }
+
+  /**
+   * 获取整体完成率的状态文字
+   */
+  getOverallStatusLabel(): string {
+    if (!this.summary) return '加载中';
+    return this.getPhaseStatusLabel(this.summary.overallCompletionRate);
+  }
+}
+

+ 28 - 4
src/app/pages/team-leader/project-timeline/project-timeline.html

@@ -123,10 +123,6 @@
           <span class="legend-icon start-icon">▶️</span>
           <span class="legend-label">项目开始</span>
         </div>
-        <div class="legend-item">
-          <span class="legend-icon review-icon">📋</span>
-          <span class="legend-label">小图对图(灰色=已完成)</span>
-        </div>
         <div class="legend-item">
           <span class="legend-icon delivery-icon">📦</span>
           <span class="legend-label">交付日期</span>
@@ -140,6 +136,10 @@
           <span class="legend-icon phase-icon softDecor-icon">软</span>
           <span class="legend-label">软装截止</span>
         </div>
+        <div class="legend-item legend-highlight">
+          <span class="legend-icon review-icon">📸</span>
+          <span class="legend-label">🔥 小图对图(重要)</span>
+        </div>
         <div class="legend-item legend-phase">
           <span class="legend-icon phase-icon rendering-icon">渲</span>
           <span class="legend-label">渲染截止</span>
@@ -251,6 +251,22 @@
                   <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>
+                }
+                
                 <!-- 🆕 使用统一的事件标记方法 -->
                 @for (event of getProjectEvents(project); track event.date) {
                   <div class="event-marker"
@@ -271,3 +287,11 @@
     </div>
   </div>
 </div>
+
+<!-- 🆕 项目进度详情弹窗 -->
+<app-project-progress-modal
+  [visible]="showProgressModal"
+  [summary]="selectedProjectSummary"
+  (close)="onCloseProgressModal()"
+  (reportIssue)="onReportIssueFromModal($event)">
+</app-project-progress-modal>

+ 115 - 4
src/app/pages/team-leader/project-timeline/project-timeline.scss

@@ -267,6 +267,21 @@
   display: flex;
   align-items: center;
   gap: 8px;
+  
+  // 🔥 高亮样式
+  &.legend-highlight {
+    padding: 6px 12px;
+    background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, rgba(99, 102, 241, 0.1) 100%);
+    border-radius: 8px;
+    border: 2px solid #fbbf24;
+    box-shadow: 0 0 10px rgba(251, 191, 36, 0.3);
+    animation: legend-glow 2s ease-in-out infinite;
+    
+    .legend-label {
+      font-weight: 600;
+      color: #7c3aed;
+    }
+  }
 }
 
 .legend-icon {
@@ -285,7 +300,12 @@
   }
   
   &.review-icon {
-    background: #8b5cf6;
+    background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
+    border: 2px solid #fbbf24;
+    width: 28px;
+    height: 28px;
+    font-size: 18px;
+    box-shadow: 0 0 15px rgba(139, 92, 246, 0.5), 0 2px 6px rgba(0, 0, 0, 0.2);
   }
   
   &.delivery-icon {
@@ -683,10 +703,21 @@
   }
 
   &.review {
-    font-size: 18px;
-    width: 26px;
-    height: 26px;
+    font-size: 24px;
+    width: 40px;
+    height: 40px;
     border-radius: 50%;
+    // 🔥 高亮显示:使用金黄色渐变背景和脉冲动画
+    background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%) !important;
+    border: 3px solid #fef3c7;
+    box-shadow: 0 0 25px rgba(245, 158, 11, 0.8), 0 4px 15px rgba(0, 0, 0, 0.3);
+    animation: pulse-highlight 2s ease-in-out infinite;
+    z-index: 15; // 比其他事件更高的层级
+    
+    &:hover {
+      transform: translateY(-50%) scale(1.5);
+      box-shadow: 0 0 35px rgba(245, 158, 11, 1), 0 6px 20px rgba(0, 0, 0, 0.4);
+    }
   }
 
   &.delivery {
@@ -741,6 +772,28 @@
   }
 }
 
+// 🔥 小图对图高亮脉冲动画
+@keyframes pulse-highlight {
+  0%, 100% {
+    transform: translateY(-50%) scale(1);
+    box-shadow: 0 0 25px rgba(245, 158, 11, 0.8), 0 4px 15px rgba(0, 0, 0, 0.3);
+  }
+  50% {
+    transform: translateY(-50%) scale(1.15);
+    box-shadow: 0 0 35px rgba(245, 158, 11, 1), 0 6px 20px rgba(0, 0, 0, 0.4);
+  }
+}
+
+// 🔥 图例发光动画
+@keyframes legend-glow {
+  0%, 100% {
+    box-shadow: 0 0 10px rgba(251, 191, 36, 0.3);
+  }
+  50% {
+    box-shadow: 0 0 20px rgba(251, 191, 36, 0.6);
+  }
+}
+
 // 响应式设计
 @media (max-width: 768px) {
   .filter-section {
@@ -839,3 +892,61 @@
     }
   }
 }
+
+// 🆕 项目进度线样式(基于实际完成率)
+.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;
+    transform: translateX(-50%) translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(-50%) translateY(0);
+  }
+}

+ 146 - 21
src/app/pages/team-leader/project-timeline/project-timeline.ts

@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { PhaseDeadlines, PhaseName, PHASE_INFO, isPhaseDelayed } from '../../../models/project-phase.model';
 import { ProjectSpaceDeliverableService, ProjectSpaceDeliverableSummary } from '../../../../modules/project/services/project-space-deliverable.service';
+import { ProjectProgressModalComponent } from '../../../../modules/project/components/project-progress-modal';
 
 export interface ProjectTimeline {
   projectId: string;
@@ -50,7 +51,7 @@ interface DesignerInfo {
 @Component({
   selector: 'app-project-timeline',
   standalone: true,
-  imports: [CommonModule, FormsModule],
+  imports: [CommonModule, FormsModule, ProjectProgressModalComponent],
   templateUrl: './project-timeline.html',
   styleUrl: './project-timeline.scss',
   changeDetection: ChangeDetectionStrategy.OnPush
@@ -84,6 +85,10 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   // 🆕 空间与交付物统计缓存
   spaceDeliverableCache: Map<string, ProjectSpaceDeliverableSummary> = new Map();
 
+  // 🆕 进度详情弹窗
+  showProgressModal: boolean = false;
+  selectedProjectSummary: ProjectSpaceDeliverableSummary | null = null;
+
   constructor(
     private cdr: ChangeDetectorRef,
     private projectSpaceDeliverableService: ProjectSpaceDeliverableService
@@ -345,7 +350,36 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
       });
     }
     
-    // 小图对图事件(显示所有小图事件,不受今日线限制)
+    // 🆕 阶段截止事件
+    if (project.phaseDeadlines) {
+      // 定义阶段顺序:建模 -> 软装
+      const phaseOrder: PhaseName[] = ['modeling', 'softDecor'];
+      
+      phaseOrder.forEach(phaseName => {
+        const phaseInfo = project.phaseDeadlines?.[phaseName];
+        if (phaseInfo && phaseInfo.deadline) {
+          const deadline = new Date(phaseInfo.deadline);
+          
+          // 只显示未来的阶段截止事件
+          if (this.isEventInFuture(deadline)) {
+            const phaseConfig = PHASE_INFO[phaseName];
+            const isDelayed = isPhaseDelayed(phaseInfo);
+            
+            events.push({
+              date: deadline,
+              label: `${phaseConfig.label}截止`,
+              type: 'phase_deadline',
+              phase: phaseName,
+              projectId: project.projectId,
+              color: isDelayed ? '#dc2626' : phaseConfig.color,
+              icon: phaseConfig.label.charAt(0)
+            });
+          }
+        }
+      });
+    }
+    
+    // 🔥 小图对图事件(始终显示,位于软装和渲染之间,高亮显示)
     if (project.reviewDate && this.isEventInRange(project.reviewDate)) {
       const isPast = project.reviewDate.getTime() < this.currentTime.getTime();
       events.push({
@@ -353,39 +387,30 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
         label: '小图对图',
         type: 'review',
         projectId: project.projectId,
-        color: isPast ? '#94a3b8' : '#8b5cf6', // 过去的事件显示为灰
-        icon: '📋'
+        color: isPast ? '#94a3b8' : '#f59e0b', // 🔥 高亮显示:金黄
+        icon: '📸' // 🔥 更醒目的图标
       });
     }
     
-    // 交付事件
-    if (project.deliveryDate && this.isEventInFuture(project.deliveryDate)) {
-      events.push({
-        date: project.deliveryDate,
-        label: '交付',
-        type: 'delivery',
-        projectId: project.projectId,
-        color: this.getEventColor('delivery', project),
-        icon: '📦'
-      });
-    }
-    
-    // 🆕 阶段截止事件
+    // 继续添加剩余阶段:渲染 -> 后期
     if (project.phaseDeadlines) {
-      Object.entries(project.phaseDeadlines).forEach(([phaseName, phaseInfo]) => {
+      const remainingPhases: PhaseName[] = ['rendering', 'postProcessing'];
+      
+      remainingPhases.forEach(phaseName => {
+        const phaseInfo = project.phaseDeadlines?.[phaseName];
         if (phaseInfo && phaseInfo.deadline) {
           const deadline = new Date(phaseInfo.deadline);
           
           // 只显示未来的阶段截止事件
           if (this.isEventInFuture(deadline)) {
-            const phaseConfig = PHASE_INFO[phaseName as PhaseName];
+            const phaseConfig = PHASE_INFO[phaseName];
             const isDelayed = isPhaseDelayed(phaseInfo);
             
             events.push({
               date: deadline,
               label: `${phaseConfig.label}截止`,
               type: 'phase_deadline',
-              phase: phaseName as PhaseName,
+              phase: phaseName,
               projectId: project.projectId,
               color: isDelayed ? '#dc2626' : phaseConfig.color,
               icon: phaseConfig.label.charAt(0)
@@ -395,6 +420,18 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
       });
     }
     
+    // 交付事件
+    if (project.deliveryDate && this.isEventInFuture(project.deliveryDate)) {
+      events.push({
+        date: project.deliveryDate,
+        label: '交付',
+        type: 'delivery',
+        projectId: project.projectId,
+        color: this.getEventColor('delivery', project),
+        icon: '📦'
+      });
+    }
+    
     // 按时间排序
     return events.sort((a, b) => a.date.getTime() - b.date.getTime());
   }
@@ -528,7 +565,38 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     this.applyFilters();
   }
 
+  /**
+   * 🆕 点击项目进度条时打开详情弹窗(替代原来的跳转)
+   */
   onProjectClick(projectId: string): void {
+    // 获取项目的统计摘要
+    const summary = this.spaceDeliverableCache.get(projectId);
+    if (summary) {
+      this.selectedProjectSummary = summary;
+      this.showProgressModal = true;
+      this.cdr.markForCheck();
+    } else {
+      console.warn('⚠️ 项目统计数据未加载:', projectId);
+    }
+  }
+
+  /**
+   * 🆕 关闭进度详情弹窗
+   */
+  onCloseProgressModal(): void {
+    this.showProgressModal = false;
+    this.selectedProjectSummary = null;
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 🆕 从弹窗提交问题
+   */
+  onReportIssueFromModal(projectId: string): void {
+    // 关闭弹窗
+    this.onCloseProgressModal();
+    
+    // 发出事件,让父组件处理问题提交
     this.projectClick.emit(projectId);
   }
 
@@ -735,7 +803,64 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   }
   
   /**
-   * 获取今日标签(含时分)
+   * 🆕 获取进度线标签(基于项目完成情况)
+   */
+  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'; // 红色
+  }
+  
+  /**
+   * 获取今日标签(含时分)- 保留用于时间参考
    */
   getTodayLabel(): string {
     const dateStr = this.currentTime.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });

+ 24 - 2
src/modules/project/components/project-bottom-card/project-bottom-card.component.html

@@ -13,7 +13,21 @@
     <div class="card-content">
       <!-- 左侧:项目标题和状态 -->
       <div class="project-info">
-        <h2 class="project-title">{{ getProjectTitle() }}</h2>
+        <div class="title-row">
+          <h2 class="project-title">{{ getProjectTitle() }}</h2>
+          <!-- 🆕 项目进度按钮 -->
+          <button
+            class="progress-button"
+            (click)="onShowProgress()"
+            [disabled]="loading"
+            title="查看项目进度详情">
+            <svg class="button-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <circle cx="12" cy="12" r="10"></circle>
+              <path d="M12 6v6l4 2"></path>
+            </svg>
+            <span class="button-text">进度</span>
+          </button>
+        </div>
         <div class="project-meta">
           <span class="status-badge" [class]="getStatusClass()">
             {{ getProjectStatus() }}
@@ -84,4 +98,12 @@
       </div>
     </div>
   }
-</div>
+</div>
+
+<!-- 🆕 项目进度详情弹窗 -->
+<app-project-progress-modal
+  [visible]="showProgressModal"
+  [summary]="projectProgressSummary"
+  (close)="onCloseProgressModal()"
+  (reportIssue)="onReportIssueFromModal($event)">
+</app-project-progress-modal>

+ 65 - 1
src/modules/project/components/project-bottom-card/project-bottom-card.component.scss

@@ -52,17 +52,64 @@
     flex: 1;
     max-width: 65px;
     
+    .title-row {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      margin-bottom: 4px;
+    }
 
     .project-title {
       font-size: 18px;
       font-weight: 600;
       color: #1f2937;
-      margin: 0 0 4px 0;
+      margin: 0;
       white-space: nowrap;
       overflow: hidden;
       text-overflow: ellipsis;
     }
 
+    .progress-button {
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border: none;
+      border-radius: 6px;
+      padding: 6px 12px;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      color: white;
+      flex-shrink: 0;
+      box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
+
+      &:hover:not(:disabled) {
+        transform: translateY(-2px);
+        box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4);
+      }
+
+      &:active:not(:disabled) {
+        transform: translateY(0);
+        box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
+      }
+
+      &:disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+      }
+
+      .button-icon {
+        width: 16px;
+        height: 16px;
+        flex-shrink: 0;
+      }
+
+      .button-text {
+        font-size: 13px;
+        font-weight: 500;
+      }
+    }
+
     .project-meta {
       display: flex;
       align-items: center;
@@ -224,11 +271,28 @@
     .project-info {
       text-align: center;
 
+      .title-row {
+        justify-content: center;
+      }
+
       .project-title {
         font-size: 16px;
         display: none;
       }
 
+      .progress-button {
+        padding: 5px 10px;
+
+        .button-icon {
+          width: 14px;
+          height: 14px;
+        }
+
+        .button-text {
+          font-size: 12px;
+        }
+      }
+
       .project-meta {
         justify-content: center;
       }

+ 58 - 3
src/modules/project/components/project-bottom-card/project-bottom-card.component.ts

@@ -1,15 +1,17 @@
-import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnInit, inject } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FmodeObject } from 'fmode-ng/parse';
+import { ProjectProgressModalComponent } from '../project-progress-modal';
+import { ProjectSpaceDeliverableService, ProjectSpaceDeliverableSummary } from '../../services/project-space-deliverable.service';
 
 @Component({
   selector: 'app-project-bottom-card',
   standalone: true,
-  imports: [CommonModule],
+  imports: [CommonModule, ProjectProgressModalComponent],
   templateUrl: './project-bottom-card.component.html',
   styleUrls: ['./project-bottom-card.component.scss']
 })
-export class ProjectBottomCardComponent {
+export class ProjectBottomCardComponent implements OnInit {
   @Input() project: FmodeObject | null = null;
   @Input() groupChat: FmodeObject | null = null;
   @Input() currentUser: FmodeObject | null = null;
@@ -23,8 +25,17 @@ export class ProjectBottomCardComponent {
   @Output() showMembers = new EventEmitter<void>();
   @Output() showIssues = new EventEmitter<void>();
 
+  // 🆕 项目进度详情
+  private deliverableService = inject(ProjectSpaceDeliverableService);
+  showProgressModal: boolean = false;
+  projectProgressSummary: ProjectSpaceDeliverableSummary | null = null;
+
   constructor() {}
 
+  ngOnInit(): void {
+    // 如果需要预加载进度数据,可以在这里添加逻辑
+  }
+
   onShowFiles() {
     this.showFiles.emit();
   }
@@ -37,6 +48,50 @@ export class ProjectBottomCardComponent {
     this.showIssues.emit();
   }
 
+  /**
+   * 🆕 显示项目进度详情
+   */
+  async onShowProgress() {
+    if (!this.project) {
+      console.warn('⚠️ 项目信息为空');
+      return;
+    }
+
+    const projectId = this.project.id;
+    if (!projectId) {
+      console.warn('⚠️ 项目ID为空');
+      return;
+    }
+
+    console.log('📊 加载项目进度详情:', projectId);
+
+    // 加载项目进度数据
+    try {
+      this.projectProgressSummary = await this.deliverableService.getProjectSpaceDeliverableSummary(projectId);
+      this.showProgressModal = true;
+    } catch (error) {
+      console.error('❌ 加载项目进度失败:', error);
+      // 可以在这里添加错误提示
+    }
+  }
+
+  /**
+   * 🆕 关闭进度详情弹窗
+   */
+  onCloseProgressModal() {
+    this.showProgressModal = false;
+  }
+
+  /**
+   * 🆕 从进度详情弹窗提交问题
+   */
+  onReportIssueFromModal(projectId: string) {
+    console.log('🐛 提交项目问题:', projectId);
+    // 关闭弹窗并触发问题提交(可以扩展这个功能)
+    this.showProgressModal = false;
+    this.onShowIssues();
+  }
+
   getProjectTitle(): string {
     return this.project?.get('title') || '项目详情';
   }

+ 2 - 0
src/modules/project/components/project-progress-modal/index.ts

@@ -0,0 +1,2 @@
+export { ProjectProgressModalComponent } from './project-progress-modal.component';
+

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

@@ -0,0 +1,164 @@
+<!-- 弹窗遮罩 -->
+<div class="modal-overlay" *ngIf="visible" (click)="onClose()">
+  <!-- 弹窗内容 -->
+  <div class="modal-content" (click)="onContentClick($event)">
+    <!-- 标题栏 -->
+    <div class="modal-header">
+      <h2 class="modal-title">
+        <span class="icon">📊</span>
+        项目进度详情
+      </h2>
+      <button class="close-btn" (click)="onClose()" aria-label="关闭">
+        <span>✕</span>
+      </button>
+    </div>
+
+    <!-- 项目基本信息 -->
+    <div class="project-info" *ngIf="summary">
+      <div class="info-row">
+        <span class="label">项目名称:</span>
+        <span class="value">{{ summary.projectName }}</span>
+      </div>
+      <div class="info-row">
+        <span class="label">空间总数:</span>
+        <span class="value">{{ summary.totalSpaces }} 个</span>
+      </div>
+      <div class="info-row">
+        <span class="label">已完成空间:</span>
+        <span class="value">{{ summary.spacesWithDeliverables }} / {{ summary.totalSpaces }}</span>
+      </div>
+    </div>
+
+    <!-- 整体进度 -->
+    <div class="overall-progress" *ngIf="summary">
+      <div class="progress-header">
+        <span class="label">整体完成率</span>
+        <span class="percentage" [style.color]="getOverallProgressColor()">
+          {{ summary.overallCompletionRate }}%
+        </span>
+        <span class="status-badge" [style.background-color]="getOverallProgressColor()">
+          {{ getOverallStatusLabel() }}
+        </span>
+      </div>
+      <div class="progress-bar-container">
+        <div class="progress-bar-bg">
+          <div 
+            class="progress-bar-fill"
+            [style.width.%]="summary.overallCompletionRate"
+            [style.background-color]="getOverallProgressColor()">
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 文件统计 -->
+    <div class="file-stats" *ngIf="summary">
+      <div class="stats-title">交付文件统计</div>
+      <div class="stats-grid">
+        <div class="stat-item">
+          <span class="stat-icon">🏗️</span>
+          <span class="stat-label">白模</span>
+          <span class="stat-value">{{ summary.totalByType.whiteModel }}</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-icon">🎨</span>
+          <span class="stat-label">软装</span>
+          <span class="stat-value">{{ summary.totalByType.softDecor }}</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-icon">🖼️</span>
+          <span class="stat-label">渲染</span>
+          <span class="stat-value">{{ summary.totalByType.rendering }}</span>
+        </div>
+        <div class="stat-item">
+          <span class="stat-icon">✨</span>
+          <span class="stat-label">后期</span>
+          <span class="stat-value">{{ summary.totalByType.postProcess }}</span>
+        </div>
+      </div>
+      <div class="stats-total">
+        总计:<strong>{{ summary.totalDeliverableFiles }}</strong> 个文件
+      </div>
+    </div>
+
+    <!-- 各阶段进度详情 -->
+    <div class="phase-progress" *ngIf="summary">
+      <div class="section-title">各阶段进度明细</div>
+      
+      <div class="phase-list">
+        <div 
+          *ngFor="let phase of getPhases()"
+          class="phase-item"
+          [class.expanded]="isPhaseExpanded(phase.name)">
+          
+          <!-- 阶段概览(可点击展开/收起) -->
+          <div class="phase-overview" (click)="togglePhase(phase.name)">
+            <div class="phase-header">
+              <span class="phase-icon">{{ PHASE_INFO[phase.name].icon }}</span>
+              <span class="phase-label">{{ phase.info.phaseLabel }}</span>
+              <span class="phase-progress-badge" [style.background-color]="getPhaseProgressColor(phase.info.completionRate)">
+                {{ phase.info.completionRate }}%
+              </span>
+              <span class="expand-icon">{{ isPhaseExpanded(phase.name) ? '▼' : '▶' }}</span>
+            </div>
+            
+            <div class="phase-summary">
+              <span class="summary-text">
+                已完成 {{ phase.info.completedSpaces }} / {{ phase.info.requiredSpaces }} 个空间
+                ({{ phase.info.totalFiles }} 个文件)
+              </span>
+            </div>
+            
+            <!-- 阶段进度条 -->
+            <div class="phase-progress-bar">
+              <div class="progress-bar-bg">
+                <div 
+                  class="progress-bar-fill"
+                  [style.width.%]="phase.info.completionRate"
+                  [style.background-color]="getPhaseProgressColor(phase.info.completionRate)">
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 未完成空间详情(展开时显示) -->
+          <div class="phase-details" *ngIf="isPhaseExpanded(phase.name)">
+            <div class="details-header">
+              <span class="details-title">未完成空间列表</span>
+              <span class="details-count">({{ phase.info.incompleteSpaces.length }} 个)</span>
+            </div>
+            
+            <div class="incomplete-spaces" *ngIf="phase.info.incompleteSpaces.length > 0">
+              <div 
+                *ngFor="let space of phase.info.incompleteSpaces"
+                class="space-item">
+                <span class="space-icon">📦</span>
+                <span class="space-name">{{ space.spaceName }}</span>
+                <span class="assignee" *ngIf="space.assignee">
+                  负责人:{{ space.assignee }}
+                </span>
+              </div>
+            </div>
+            
+            <div class="no-incomplete" *ngIf="phase.info.incompleteSpaces.length === 0">
+              <span class="success-icon">✅</span>
+              <span>所有空间已完成此阶段交付</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="modal-footer">
+      <button class="btn btn-secondary" (click)="onClose()">
+        关闭
+      </button>
+      <button class="btn btn-primary" (click)="onReportIssue()">
+        <span class="btn-icon">🐛</span>
+        提交问题
+      </button>
+    </div>
+  </div>
+</div>
+

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

@@ -0,0 +1,493 @@
+// 项目进度详情弹窗样式
+
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.6);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 10000;
+  animation: fadeIn 0.2s ease-out;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+.modal-content {
+  background: white;
+  border-radius: 12px;
+  width: 90%;
+  max-width: 800px;
+  max-height: 90vh;
+  overflow-y: auto;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
+  animation: slideUp 0.3s ease-out;
+  
+  // 自定义滚动条
+  &::-webkit-scrollbar {
+    width: 8px;
+  }
+  
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 4px;
+  }
+  
+  &::-webkit-scrollbar-thumb {
+    background: #ccc;
+    border-radius: 4px;
+    
+    &:hover {
+      background: #999;
+    }
+  }
+}
+
+@keyframes slideUp {
+  from {
+    transform: translateY(30px);
+    opacity: 0;
+  }
+  to {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+// 标题栏
+.modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e0e0e0;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12px 12px 0 0;
+  
+  .modal-title {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: white;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    
+    .icon {
+      font-size: 24px;
+    }
+  }
+  
+  .close-btn {
+    background: rgba(255, 255, 255, 0.2);
+    border: none;
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    cursor: pointer;
+    color: white;
+    font-size: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s;
+    
+    &:hover {
+      background: rgba(255, 255, 255, 0.3);
+      transform: scale(1.1);
+    }
+  }
+}
+
+// 项目基本信息
+.project-info {
+  padding: 20px 24px;
+  background: #f8f9fa;
+  border-bottom: 1px solid #e0e0e0;
+  
+  .info-row {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    .label {
+      font-weight: 500;
+      color: #666;
+      margin-right: 8px;
+    }
+    
+    .value {
+      color: #333;
+      font-weight: 600;
+    }
+  }
+}
+
+// 整体进度
+.overall-progress {
+  padding: 20px 24px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+  
+  .progress-header {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 12px;
+    
+    .label {
+      font-size: 16px;
+      font-weight: 600;
+      color: #333;
+    }
+    
+    .percentage {
+      font-size: 24px;
+      font-weight: 700;
+    }
+    
+    .status-badge {
+      padding: 4px 12px;
+      border-radius: 12px;
+      font-size: 12px;
+      color: white;
+      font-weight: 600;
+    }
+  }
+  
+  .progress-bar-container {
+    margin-top: 8px;
+  }
+}
+
+// 进度条样式
+.progress-bar-bg {
+  width: 100%;
+  height: 12px;
+  background: #e0e0e0;
+  border-radius: 6px;
+  overflow: hidden;
+  
+  .progress-bar-fill {
+    height: 100%;
+    border-radius: 6px;
+    transition: width 0.6s ease, background-color 0.3s ease;
+    background: linear-gradient(90deg, currentColor 0%, currentColor 100%);
+  }
+}
+
+// 文件统计
+.file-stats {
+  padding: 20px 24px;
+  border-bottom: 1px solid #e0e0e0;
+  
+  .stats-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+    margin-bottom: 16px;
+  }
+  
+  .stats-grid {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 12px;
+    margin-bottom: 12px;
+    
+    @media (max-width: 600px) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+  }
+  
+  .stat-item {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 12px;
+    background: #f8f9fa;
+    border-radius: 8px;
+    border: 1px solid #e0e0e0;
+    
+    .stat-icon {
+      font-size: 28px;
+      margin-bottom: 4px;
+    }
+    
+    .stat-label {
+      font-size: 12px;
+      color: #666;
+      margin-bottom: 4px;
+    }
+    
+    .stat-value {
+      font-size: 20px;
+      font-weight: 700;
+      color: #333;
+    }
+  }
+  
+  .stats-total {
+    text-align: center;
+    padding: 12px;
+    background: #e3f2fd;
+    border-radius: 8px;
+    font-size: 14px;
+    color: #1976d2;
+    
+    strong {
+      font-size: 18px;
+      font-weight: 700;
+    }
+  }
+}
+
+// 阶段进度
+.phase-progress {
+  padding: 20px 24px;
+  
+  .section-title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+    margin-bottom: 16px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+.phase-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.phase-item {
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+  overflow: hidden;
+  transition: all 0.3s ease;
+  
+  &.expanded {
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  }
+}
+
+.phase-overview {
+  padding: 16px;
+  cursor: pointer;
+  transition: background-color 0.2s;
+  
+  &:hover {
+    background-color: #f8f9fa;
+  }
+  
+  .phase-header {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 8px;
+    
+    .phase-icon {
+      font-size: 24px;
+    }
+    
+    .phase-label {
+      font-size: 16px;
+      font-weight: 600;
+      color: #333;
+      flex: 1;
+    }
+    
+    .phase-progress-badge {
+      padding: 4px 12px;
+      border-radius: 12px;
+      font-size: 14px;
+      color: white;
+      font-weight: 600;
+    }
+    
+    .expand-icon {
+      font-size: 12px;
+      color: #666;
+      transition: transform 0.3s ease;
+    }
+  }
+  
+  .phase-summary {
+    margin-bottom: 8px;
+    
+    .summary-text {
+      font-size: 13px;
+      color: #666;
+    }
+  }
+  
+  .phase-progress-bar {
+    margin-top: 8px;
+    
+    .progress-bar-bg {
+      height: 8px;
+    }
+  }
+}
+
+.phase-item.expanded .phase-overview .expand-icon {
+  transform: rotate(0deg);
+}
+
+.phase-details {
+  padding: 16px;
+  background: #f8f9fa;
+  border-top: 1px solid #e0e0e0;
+  animation: expandDetails 0.3s ease-out;
+  
+  .details-header {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    margin-bottom: 12px;
+    
+    .details-title {
+      font-size: 14px;
+      font-weight: 600;
+      color: #666;
+    }
+    
+    .details-count {
+      font-size: 12px;
+      color: #999;
+    }
+  }
+}
+
+@keyframes expandDetails {
+  from {
+    opacity: 0;
+    max-height: 0;
+  }
+  to {
+    opacity: 1;
+    max-height: 500px;
+  }
+}
+
+.incomplete-spaces {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.space-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 12px;
+  background: white;
+  border-radius: 6px;
+  border: 1px solid #e0e0e0;
+  
+  .space-icon {
+    font-size: 16px;
+  }
+  
+  .space-name {
+    flex: 1;
+    font-size: 14px;
+    color: #333;
+  }
+  
+  .assignee {
+    font-size: 12px;
+    color: #666;
+    background: #e3f2fd;
+    padding: 4px 8px;
+    border-radius: 4px;
+  }
+}
+
+.no-incomplete {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 20px;
+  background: #e8f5e9;
+  border-radius: 6px;
+  color: #4caf50;
+  font-size: 14px;
+  
+  .success-icon {
+    font-size: 20px;
+  }
+}
+
+// 底部按钮
+.modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 20px 24px;
+  border-top: 1px solid #e0e0e0;
+  background: #f8f9fa;
+  border-radius: 0 0 12px 12px;
+}
+
+.btn {
+  padding: 10px 20px;
+  border: none;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  
+  &:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  }
+  
+  &:active {
+    transform: translateY(0);
+  }
+}
+
+.btn-secondary {
+  background: #e0e0e0;
+  color: #666;
+  
+  &:hover {
+    background: #d0d0d0;
+  }
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  
+  &:hover {
+    background: linear-gradient(135deg, #5568d3 0%, #65408b 100%);
+  }
+  
+  .btn-icon {
+    font-size: 16px;
+  }
+}
+

+ 135 - 0
src/modules/project/components/project-progress-modal/project-progress-modal.component.ts

@@ -0,0 +1,135 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { ProjectSpaceDeliverableSummary, PhaseProgressInfo } from '../../services/project-space-deliverable.service';
+import { PHASE_INFO, PhaseName } from '../../../../app/models/project-phase.model';
+
+/**
+ * 项目进度详情弹窗组件(独立组件)
+ * 
+ * 功能:
+ * - 显示项目的整体进度统计
+ * - 按阶段展示完成情况(建模、软装、渲染、后期)
+ * - 显示未完成的空间列表和负责人
+ * - 提供问题提交入口
+ * 
+ * 使用场景:
+ * - 项目时间轴页面
+ * - 项目详情页面
+ * - 其他需要查看项目进度的地方
+ */
+@Component({
+  selector: 'app-project-progress-modal',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './project-progress-modal.component.html',
+  styleUrl: './project-progress-modal.component.scss'
+})
+export class ProjectProgressModalComponent implements OnInit {
+  @Input() summary: ProjectSpaceDeliverableSummary | null = null;
+  @Input() visible: boolean = false;
+  @Output() close = new EventEmitter<void>();
+  @Output() reportIssue = new EventEmitter<string>(); // 提交问题,传递项目ID
+
+  // 当前选中的阶段(用于展开/收起详情)
+  selectedPhase: PhaseName | null = null;
+
+  // 阶段常量
+  readonly PHASE_INFO = PHASE_INFO;
+
+  ngOnInit(): void {
+    console.log('📊 项目进度弹窗初始化', this.summary);
+  }
+
+  /**
+   * 关闭弹窗
+   */
+  onClose(): void {
+    this.close.emit();
+  }
+
+  /**
+   * 阻止点击弹窗内容时关闭
+   */
+  onContentClick(event: MouseEvent): void {
+    event.stopPropagation();
+  }
+
+  /**
+   * 切换阶段展开/收起
+   */
+  togglePhase(phaseName: PhaseName): void {
+    this.selectedPhase = this.selectedPhase === phaseName ? null : phaseName;
+  }
+
+  /**
+   * 判断阶段是否展开
+   */
+  isPhaseExpanded(phaseName: PhaseName): boolean {
+    return this.selectedPhase === phaseName;
+  }
+
+  /**
+   * 获取阶段列表
+   */
+  getPhases(): Array<{ name: PhaseName; info: PhaseProgressInfo }> {
+    if (!this.summary?.phaseProgress) return [];
+
+    return [
+      { name: 'modeling', info: this.summary.phaseProgress.modeling },
+      { name: 'softDecor', info: this.summary.phaseProgress.softDecor },
+      { name: 'rendering', info: this.summary.phaseProgress.rendering },
+      { name: 'postProcessing', info: this.summary.phaseProgress.postProcessing }
+    ];
+  }
+
+  /**
+   * 获取阶段进度颜色
+   */
+  getPhaseProgressColor(completionRate: number): string {
+    if (completionRate === 100) return '#4CAF50'; // 绿色
+    if (completionRate >= 75) return '#8BC34A'; // 浅绿
+    if (completionRate >= 50) return '#FFC107'; // 黄色
+    if (completionRate >= 25) return '#FF9800'; // 橙色
+    if (completionRate > 0) return '#FF5722'; // 深橙
+    return '#9E9E9E'; // 灰色(未开始)
+  }
+
+  /**
+   * 获取阶段状态标签
+   */
+  getPhaseStatusLabel(completionRate: number): string {
+    if (completionRate === 100) return '已完成';
+    if (completionRate >= 75) return '接近完成';
+    if (completionRate >= 50) return '进行中';
+    if (completionRate >= 25) return '刚开始';
+    if (completionRate > 0) return '已启动';
+    return '未开始';
+  }
+
+  /**
+   * 提交问题
+   */
+  onReportIssue(): void {
+    if (this.summary) {
+      this.reportIssue.emit(this.summary.projectId);
+    }
+  }
+
+  /**
+   * 获取整体完成率的颜色
+   */
+  getOverallProgressColor(): string {
+    if (!this.summary) return '#9E9E9E';
+    return this.getPhaseProgressColor(this.summary.overallCompletionRate);
+  }
+
+  /**
+   * 获取整体完成率的状态文字
+   */
+  getOverallStatusLabel(): string {
+    if (!this.summary) return '加载中';
+    return this.getPhaseStatusLabel(this.summary.overallCompletionRate);
+  }
+}
+

+ 131 - 1
src/modules/project/services/project-space-deliverable.service.ts

@@ -33,6 +33,30 @@ export interface SpaceDeliverableInfo {
   completionRate: number;
 }
 
+/**
+ * 🆕 阶段进度信息
+ */
+export interface PhaseProgressInfo {
+  /** 阶段名称 */
+  phaseName: 'modeling' | 'softDecor' | 'rendering' | 'postProcessing';
+  /** 阶段中文标签 */
+  phaseLabel: string;
+  /** 应完成空间数(有此类型交付物要求的空间数) */
+  requiredSpaces: number;
+  /** 已完成空间数(已上传此类型交付物的空间数) */
+  completedSpaces: number;
+  /** 完成率(0-100) */
+  completionRate: number;
+  /** 总文件数 */
+  totalFiles: number;
+  /** 未完成空间列表 */
+  incompleteSpaces: Array<{
+    spaceId: string;
+    spaceName: string;
+    assignee?: string; // 负责人
+  }>;
+}
+
 /**
  * 项目空间与交付物统计信息
  */
@@ -58,6 +82,13 @@ export interface ProjectSpaceDeliverableSummary {
   };
   /** 整体完成率(0-100) */
   overallCompletionRate: number;
+  /** 🆕 各阶段进度详情 */
+  phaseProgress: {
+    modeling: PhaseProgressInfo;
+    softDecor: PhaseProgressInfo;
+    rendering: PhaseProgressInfo;
+    postProcessing: PhaseProgressInfo;
+  };
 }
 
 /**
@@ -143,6 +174,9 @@ export class ProjectSpaceDeliverableService {
       // 5. 计算整体完成率
       const overallCompletionRate = this.calculateOverallCompletionRate(spaceInfos);
 
+      // 🆕 6. 计算各阶段进度详情
+      const phaseProgress = this.calculatePhaseProgress(spaceInfos, project);
+
       return {
         projectId,
         projectName,
@@ -151,7 +185,8 @@ export class ProjectSpaceDeliverableService {
         spaces: spaceInfos,
         totalDeliverableFiles,
         totalByType,
-        overallCompletionRate
+        overallCompletionRate,
+        phaseProgress
       };
 
     } catch (error) {
@@ -266,6 +301,101 @@ export class ProjectSpaceDeliverableService {
     return Math.round(totalCompletionRate / spaceInfos.length);
   }
 
+  /**
+   * 🆕 计算各阶段进度详情
+   * 
+   * @param spaceInfos 空间信息列表
+   * @param project 项目对象
+   * @returns 各阶段进度信息
+   */
+  private calculatePhaseProgress(
+    spaceInfos: SpaceDeliverableInfo[],
+    project: FmodeObject
+  ): ProjectSpaceDeliverableSummary['phaseProgress'] {
+    // 获取项目阶段截止信息中的负责人
+    const projectData = project.get('data') || {};
+    const phaseDeadlines = projectData.phaseDeadlines || {};
+
+    // 阶段映射:交付物类型 -> 阶段名称
+    const phaseMap = {
+      modeling: {
+        key: 'whiteModel' as const,
+        label: '建模',
+        phaseName: 'modeling' as const
+      },
+      softDecor: {
+        key: 'softDecor' as const,
+        label: '软装',
+        phaseName: 'softDecor' as const
+      },
+      rendering: {
+        key: 'rendering' as const,
+        label: '渲染',
+        phaseName: 'rendering' as const
+      },
+      postProcessing: {
+        key: 'postProcess' as const,
+        label: '后期',
+        phaseName: 'postProcessing' as const
+      }
+    };
+
+    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.objectId) {
+        // 如果有负责人指针,可以在这里查询(为了性能,暂时不实时查询)
+        assigneeName = '待查询';
+      }
+
+      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
+          });
+        }
+      });
+
+      const completionRate = requiredSpaces > 0
+        ? Math.round((completedSpaces / requiredSpaces) * 100)
+        : 0;
+
+      result[phaseKey] = {
+        phaseName: phaseConfig.phaseName,
+        phaseLabel: phaseConfig.label,
+        requiredSpaces,
+        completedSpaces,
+        completionRate,
+        totalFiles,
+        incompleteSpaces
+      };
+    });
+
+    return result;
+  }
+
   /**
    * 检查项目是否所有空间都已上传交付物
    *