浏览代码

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

徐福静0235668 4 天之前
父节点
当前提交
02fbafbeeb

+ 170 - 12
src/app/pages/team-leader/dashboard/dashboard.html

@@ -74,7 +74,6 @@
         @if (selectedStatus !== 'all') {
           <button class="btn-link" (click)="resetStatusFilter()">返回全部项目</button>
         }
-        <button class="btn-toggle-view" (click)="toggleView()">{{ showGanttView ? '返回看板' : '切换视图' }}</button>
       </div>
     </div>
 
@@ -98,6 +97,15 @@
       </div>
       <div class="gantt-container" #workloadGanttContainer></div>
     </div>
+    
+    <!-- 🆕 视图切换按钮(固定在此位置便于切换) -->
+    <div class="view-toggle-bar">
+      <button class="btn-toggle-view" (click)="toggleView()">
+        <span class="toggle-icon">{{ showGanttView ? '📋' : '📊' }}</span>
+        <span class="toggle-text">{{ showGanttView ? '切换到项目看板' : '切换到时间轴视图' }}</span>
+      </button>
+    </div>
+    
     <!-- 项目负载时间轴(切换视图时显示) -->
     @if (showGanttView) {
       <app-project-timeline 
@@ -262,26 +270,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 +414,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>
 
   <!-- 超期项目提醒 -->

+ 265 - 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;
+      }
+    }
+  }
 }
 
 /* 超期提醒样式 */
@@ -2205,3 +2398,75 @@
     }
   }
 }
+
+// 🆕 视图切换按钮栏样式
+.view-toggle-bar {
+  position: sticky;
+  top: 50px; // 根据导航栏高度调整
+  z-index: 99;
+  background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
+  padding: 16px 20px;
+  margin: 20px 0;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  
+  .btn-toggle-view {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 12px 32px;
+    border: 2px solid transparent;
+    border-radius: 8px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    font-size: 15px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+    
+    .toggle-icon {
+      font-size: 20px;
+      transition: transform 0.3s ease;
+    }
+    
+    .toggle-text {
+      letter-spacing: 0.5px;
+    }
+    
+    &:hover {
+      transform: translateY(-2px);
+      box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
+      
+      .toggle-icon {
+        transform: scale(1.2) rotate(5deg);
+      }
+    }
+    
+    &:active {
+      transform: translateY(0);
+      box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+    }
+  }
+}
+
+// 响应式适配
+@media (max-width: 768px) {
+  .view-toggle-bar {
+    top: 45px;
+    padding: 12px 16px;
+    margin: 16px 0;
+    
+    .btn-toggle-view {
+      padding: 10px 24px;
+      font-size: 14px;
+      
+      .toggle-icon {
+        font-size: 18px;
+      }
+    }
+  }
+}

+ 179 - 6
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: '组长',
@@ -211,8 +234,8 @@ export class Dashboard implements OnInit, OnDestroy {
     { id: 'delivery', name: '交付执行', order: 3 },      // 建模、渲染、后期/评审/修改
     { id: 'aftercare', name: '售后', order: 4 }          // 交付完成 → 售后
   ];
-  // 甘特视图开关与实例引用
-  showGanttView: boolean = false;
+  // 甘特视图开关与实例引用(默认显示时间轴视图)
+  showGanttView: boolean = true;
   private ganttChart: any | null = null;
   @ViewChild('ganttChartRef', { static: false }) ganttChartRef!: ElementRef<HTMLDivElement>;
   // 工作负载甘特图引用
@@ -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);
+  }
+}
+

+ 177 - 238
src/app/pages/team-leader/project-timeline/project-timeline.html

@@ -37,50 +37,32 @@
         </button>
       </div>
 
-      <!-- 视图切换 -->
-      <div class="filter-group view-controls">
+      <!-- 时间尺度切换 -->
+      <div class="filter-group time-scale-controls">
+        <label>时间范围:</label>
         <button 
-          class="view-btn"
-          [class.active]="viewMode === 'list'"
-          (click)="toggleViewMode('list')">
-          📋 列表
+          class="scale-btn"
+          [class.active]="timelineScale === 'week'"
+          (click)="toggleTimelineScale('week')">
+          📆 7天
         </button>
         <button 
-          class="view-btn"
-          [class.active]="viewMode === 'timeline'"
-          (click)="toggleViewMode('timeline')">
-          📅 时间轴
+          class="scale-btn"
+          [class.active]="timelineScale === 'month'"
+          (click)="toggleTimelineScale('month')">
+          📅 30天
         </button>
       </div>
       
-      <!-- 时间尺度切换(仅在时间轴视图显示) -->
-      @if (viewMode === 'timeline') {
-        <div class="filter-group time-scale-controls">
-          <label>时间范围:</label>
-          <button 
-            class="scale-btn"
-            [class.active]="timelineScale === 'week'"
-            (click)="toggleTimelineScale('week')">
-            📆 7天
-          </button>
-          <button 
-            class="scale-btn"
-            [class.active]="timelineScale === 'month'"
-            (click)="toggleTimelineScale('month')">
-            📅 30天
-          </button>
-        </div>
-        
-        <!-- 🆕 手动刷新按钮 -->
-        <div class="filter-group refresh-controls">
-          <button 
-            class="refresh-btn"
-            (click)="refresh()"
-            title="刷新数据和时间线(自动10分钟刷新一次)">
-            🔄 刷新
-          </button>
-        </div>
-      }
+      <!-- 🆕 手动刷新按钮 -->
+      <div class="filter-group refresh-controls">
+        <button 
+          class="refresh-btn"
+          (click)="refresh()"
+          title="刷新数据和时间线(自动10分钟刷新一次)">
+          🔄 刷新
+        </button>
+      </div>
 
       <!-- 排序方式 -->
       <div class="filter-group sort-controls">
@@ -132,227 +114,184 @@
   </div>
 
   <!-- 时间轴主体 -->
-  <div class="timeline-body" [class.timeline-view]="viewMode === 'timeline'">
-    @if (viewMode === 'timeline') {
-      <!-- 时间轴视图 -->
-      <div class="timeline-view-container">
-        <!-- 图例说明 -->
-        <div class="timeline-legend">
-          <div class="legend-item">
-            <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>
-          </div>
-          <div class="legend-separator"></div>
-          <div class="legend-item legend-phase">
-            <span class="legend-label">🎨 建模截止</span>
-          </div>
-          <div class="legend-item legend-phase">
-            <span class="legend-label">🪑 软装截止</span>
-          </div>
-          <div class="legend-item legend-phase">
-            <span class="legend-label">🖼️ 渲染截止</span>
-          </div>
-          <div class="legend-item legend-phase">
-            <span class="legend-label">✨ 后期截止</span>
-          </div>
-          <div class="legend-separator"></div>
-          <div class="legend-item">
-            <div class="legend-bar-demo legend-bar-green"></div>
-            <span class="legend-label">🟢 正常进行(2天+)</span>
-          </div>
-          <div class="legend-item">
-            <div class="legend-bar-demo legend-bar-yellow"></div>
-            <span class="legend-label">🟡 前一天(24小时内)</span>
-          </div>
-          <div class="legend-item">
-            <div class="legend-bar-demo legend-bar-orange"></div>
-            <span class="legend-label">🟠 事件当天(6小时+)</span>
-          </div>
-          <div class="legend-item">
-            <div class="legend-bar-demo legend-bar-red"></div>
-            <span class="legend-label">🔴 紧急(6小时内)</span>
-          </div>
-          <div class="legend-item legend-note">
-            <span class="legend-label">💡 仅显示今日线之后的关键事件和阶段截止时间</span>
-          </div>
+  <div class="timeline-body timeline-view">
+    <!-- 时间轴视图 -->
+    <div class="timeline-view-container">
+      <!-- 图例说明 -->
+      <div class="timeline-legend">
+        <div class="legend-item">
+          <span class="legend-icon start-icon">▶️</span>
+          <span class="legend-label">项目开始</span>
         </div>
-        
-        <!-- 时间刻度尺 -->
-        <div class="timeline-ruler">
-          <div class="ruler-header">
-            <span class="project-name-header">项目名称</span>
-          </div>
-          <div class="ruler-ticks">
-            @for (date of timeRange; track date; let i = $index) {
-              <div class="ruler-tick" [class.first]="i === 0">
-                <div class="tick-date">{{ date.getMonth() + 1 }}/{{ date.getDate() }}</div>
-                @if (timelineScale === 'week') {
-                  <div class="tick-weekday">
-                    @switch (date.getDay()) {
-                      @case (0) { 周日 }
-                      @case (1) { 周一 }
-                      @case (2) { 周二 }
-                      @case (3) { 周三 }
-                      @case (4) { 周四 }
-                      @case (5) { 周五 }
-                      @case (6) { 周六 }
-                    }
-                  </div>
-                }
-              </div>
-            }
-          </div>
+        <div class="legend-item">
+          <span class="legend-icon delivery-icon">📦</span>
+          <span class="legend-label">交付日期</span>
         </div>
-        
-        <!-- 今日标记线(实时移动,精确到分钟) -->
-        <div class="today-line" 
-             [style.left]="getTodayPosition()">
-          <div class="today-label">
-            {{ getTodayLabel() }}
-          </div>
-          <div class="today-dot"></div>
-          <div class="today-bar"></div>
+        <div class="legend-separator"></div>
+        <div class="legend-item legend-phase">
+          <span class="legend-icon phase-icon modeling-icon">建</span>
+          <span class="legend-label">建模截止</span>
+        </div>
+        <div class="legend-item legend-phase">
+          <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>
+        </div>
+        <div class="legend-item legend-phase">
+          <span class="legend-icon phase-icon postProcessing-icon">后</span>
+          <span class="legend-label">后期截止</span>
+        </div>
+        <div class="legend-separator"></div>
+        <div class="legend-item">
+          <div class="legend-bar-demo legend-bar-green"></div>
+          <span class="legend-label">🟢 正常进行(2天+)</span>
+        </div>
+        <div class="legend-item">
+          <div class="legend-bar-demo legend-bar-yellow"></div>
+          <span class="legend-label">🟡 前一天(24小时内)</span>
+        </div>
+        <div class="legend-item">
+          <div class="legend-bar-demo legend-bar-orange"></div>
+          <span class="legend-label">🟠 事件当天(6小时+)</span>
+        </div>
+        <div class="legend-item">
+          <div class="legend-bar-demo legend-bar-red"></div>
+          <span class="legend-label">🔴 紧急(6小时内)</span>
+        </div>
+        <div class="legend-item legend-note">
+          <span class="legend-label">💡 仅显示今日线之后的关键事件和阶段截止时间</span>
+        </div>
+      </div>
         
-        <!-- 项目时间轴 -->
-        <div class="timeline-projects">
-          @if (filteredProjects.length === 0) {
-            <div class="empty-state">
-              <p>暂无项目数据</p>
-            </div>
-          } @else {
-            @for (project of filteredProjects; track project.projectId) {
-              <div class="timeline-row" (click)="onProjectClick(project.projectId)">
-                <!-- 项目名称标签 -->
-                <div class="project-label">
-                  <span class="project-name-label" [title]="project.projectName">
-                    {{ project.projectName }}
-                  </span>
-                  <span class="designer-label">{{ project.designerName }}</span>
-                  @if (project.priority === 'critical' || project.priority === 'high') {
-                    <span class="priority-badge" [class]="'badge-' + project.priority">
-                      @if (project.priority === 'critical') { ‼️ }
-                      @else { 🔥 }
-                    </span>
-                  }
-                  <!-- 🆕 空间与交付物统计徽章 -->
-                  @if (getSpaceDeliverableSummary(project.projectId); as summary) {
-                    <span class="space-deliverable-badge" 
-                          [title]="formatSpaceDeliverableTooltip(project.projectId)"
-                          [style.background-color]="getProjectDeliveryStatusColor(project.projectId)">
-                      📦 {{ summary.spacesWithDeliverables }}/{{ summary.totalSpaces }}
-                    </span>
-                  }
-                </div>
-                
-                <!-- 时间轴区域 -->
-                <div class="timeline-track">
-                  <!-- 项目条形图 -->
-                  <div class="project-bar"
-                       [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 + '%'">
-                    <!-- 进度填充 -->
-                    <div class="progress-fill" [style.width]="project.stageProgress + '%'"></div>
-                  </div>
-                  
-                  <!-- 🆕 使用统一的事件标记方法 -->
-                  @for (event of getProjectEvents(project); track event.date) {
-                    <div class="event-marker"
-                         [class]="event.type"
-                         [style.left]="getEventPosition(event.date)"
-                         [style.background]="event.color"
-                         [class.blink]="project.status === 'overdue' && event.type === 'delivery'"
-                         [title]="event.label + ':' + formatTime(event.date) + (event.phase ? ' (' + getPhaseLabel(event.phase) + ')' : '')">
-                      {{ event.icon }}
-                    </div>
+      <!-- 时间刻度尺 -->
+      <div class="timeline-ruler">
+        <div class="ruler-header">
+          <span class="project-name-header">项目名称</span>
+        </div>
+        <div class="ruler-ticks">
+          @for (date of timeRange; track date; let i = $index) {
+            <div class="ruler-tick" [class.first]="i === 0">
+              <div class="tick-date">{{ date.getMonth() + 1 }}/{{ date.getDate() }}</div>
+              @if (timelineScale === 'week') {
+                <div class="tick-weekday">
+                  @switch (date.getDay()) {
+                    @case (0) { 周日 }
+                    @case (1) { 周一 }
+                    @case (2) { 周二 }
+                    @case (3) { 周三 }
+                    @case (4) { 周四 }
+                    @case (5) { 周五 }
+                    @case (6) { 周六 }
                   }
                 </div>
-              </div>
-            }
+              }
+            </div>
           }
         </div>
       </div>
-    } @else {
-      <!-- 列表视图 -->
-      <div class="projects-list">
+        
+      <!-- 今日标记线(实时移动,精确到分钟) -->
+      <div class="today-line" 
+           [style.left]="getTodayPosition()">
+        <div class="today-label">
+          {{ getTodayLabel() }}
+        </div>
+        <div class="today-dot"></div>
+        <div class="today-bar"></div>
+      </div>
+        
+      <!-- 项目时间轴 -->
+      <div class="timeline-projects">
         @if (filteredProjects.length === 0) {
           <div class="empty-state">
             <p>暂无项目数据</p>
           </div>
         } @else {
-          @for (project of filteredProjects; track project.projectId; let i = $index) {
-            <div 
-              class="project-item"
-              [class]="'status-' + project.status"
-              (click)="onProjectClick(project.projectId)">
-              
-              <!-- 优先级指示条 -->
-              <div class="priority-bar" [class]="'priority-' + project.priority"></div>
-              
-              <!-- 项目信息 -->
-              <div class="project-content">
-                <div class="project-header">
-                  <h4 class="project-name">{{ project.projectName }}</h4>
-                  <div class="project-badges">
-                    @if (project.isStalled) {
-                      <span class="badge badge-stalled">⏸️ 停滞{{ project.stalledDays }}天</span>
-                    }
-                    @if (project.urgentCount > 0) {
-                      <span class="badge badge-urgent">🔥 催办{{ project.urgentCount }}次</span>
-                    }
-                    @if (project.status === 'overdue') {
-                      <span class="badge badge-overdue">⚠️ 逾期</span>
-                    } @else if (project.status === 'urgent') {
-                      <span class="badge badge-warning">⏰ 紧急</span>
-                    }
-                    <!-- 🆕 空间与交付物统计徽章 -->
-                    @if (getSpaceDeliverableSummary(project.projectId); as summary) {
-                      <span class="badge badge-deliverable"
-                            [title]="formatSpaceDeliverableTooltip(project.projectId)"
-                            [style.background-color]="getProjectDeliveryStatusColor(project.projectId)"
-                            [style.color]="'white'">
-                        📦 空间 {{ summary.spacesWithDeliverables }}/{{ summary.totalSpaces }} | 文件 {{ summary.totalDeliverableFiles }}
-                      </span>
-                    }
-                  </div>
-                </div>
-                
-                <div class="project-meta">
-                  <span class="meta-item">
-                    <span class="meta-label">设计师:</span>
-                    <span class="meta-value">{{ project.designerName }}</span>
-                  </span>
-                  <span class="meta-item">
-                    <span class="meta-label">阶段:</span>
-                    <span class="meta-value">{{ project.stageName }}</span>
+          @for (project of filteredProjects; track project.projectId) {
+            <div class="timeline-row" (click)="onProjectClick(project.projectId)">
+              <!-- 项目名称标签 -->
+              <div class="project-label">
+                <span class="project-name-label" [title]="project.projectName">
+                  {{ project.projectName }}
+                </span>
+                <span class="designer-label">{{ project.designerName }}</span>
+                @if (project.priority === 'critical' || project.priority === 'high') {
+                  <span class="priority-badge" [class]="'badge-' + project.priority">
+                    @if (project.priority === 'critical') { ‼️ }
+                    @else { 🔥 }
                   </span>
-                  <span class="meta-item">
-                    <span class="meta-label">进度:</span>
-                    <span class="meta-value">{{ project.stageProgress }}%</span>
-                  </span>
-                  <span class="meta-item">
-                    <span class="meta-label">截止:</span>
-                    <span class="meta-value" [class.text-danger]="project.status === 'overdue'">
-                      {{ formatDate(project.endDate) }}
-                    </span>
+                }
+                <!-- 🆕 空间与交付物统计徽章 -->
+                @if (getSpaceDeliverableSummary(project.projectId); as summary) {
+                  <span class="space-deliverable-badge" 
+                        [title]="formatSpaceDeliverableTooltip(project.projectId)"
+                        [style.background-color]="getProjectDeliveryStatusColor(project.projectId)">
+                    📦 {{ summary.spacesWithDeliverables }}/{{ summary.totalSpaces }}
                   </span>
+                }
+              </div>
+              
+              <!-- 时间轴区域 -->
+              <div class="timeline-track">
+                <!-- 项目条形图 -->
+                <div class="project-bar"
+                     [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 + '%'">
+                  <!-- 进度填充 -->
+                  <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"
+                       [class]="event.type"
+                       [class.phase-deadline]="event.type === 'phase_deadline'" 
+                       [style.left]="getEventPosition(event.date)"
+                       [style.background]="event.color"
+                       [class.blink]="project.status === 'overdue' && event.type === 'delivery'"
+                       [title]="event.label + ':' + formatTime(event.date) + (event.phase ? ' (' + getPhaseLabel(event.phase) + ')' : '')">
+                    {{ event.icon }}
+                  </div>
+                }
               </div>
             </div>
           }
         }
       </div>
-    }
+    </div>
   </div>
 </div>
+
+<!-- 🆕 项目进度详情弹窗 -->
+<app-project-progress-modal
+  [visible]="showProgressModal"
+  [summary]="selectedProjectSummary"
+  (close)="onCloseProgressModal()"
+  (reportIssue)="onReportIssueFromModal($event)">
+</app-project-progress-modal>

+ 138 - 171
src/app/pages/team-leader/project-timeline/project-timeline.scss

@@ -79,14 +79,12 @@
   }
 }
 
-.view-controls,
 .sort-controls,
 .time-scale-controls {
   border-left: 1px solid #e5e7eb;
   padding-left: 16px;
 }
 
-.view-btn,
 .sort-btn,
 .scale-btn,
 .refresh-btn {
@@ -230,160 +228,6 @@
   padding: 16px;
 }
 
-// 列表视图
-.projects-list {
-  display: flex;
-  flex-direction: column;
-  gap: 12px;
-}
-
-.project-item {
-  position: relative;
-  display: flex;
-  padding: 16px;
-  background: #ffffff;
-  border: 1px solid #e5e7eb;
-  border-radius: 8px;
-  cursor: pointer;
-  transition: all 0.2s;
-
-  &:hover {
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
-    transform: translateY(-2px);
-  }
-
-  &.status-overdue {
-    border-left-width: 4px;
-    border-left-color: #dc2626;
-  }
-
-  &.status-urgent {
-    border-left-width: 4px;
-    border-left-color: #ea580c;
-  }
-
-  &.status-warning {
-    border-left-width: 4px;
-    border-left-color: #f59e0b;
-  }
-}
-
-.priority-bar {
-  position: absolute;
-  left: 0;
-  top: 0;
-  bottom: 0;
-  width: 4px;
-  border-radius: 8px 0 0 8px;
-
-  &.priority-critical {
-    background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%);
-  }
-
-  &.priority-high {
-    background: linear-gradient(180deg, #ea580c 0%, #c2410c 100%);
-  }
-
-  &.priority-medium {
-    background: linear-gradient(180deg, #f59e0b 0%, #d97706 100%);
-  }
-
-  &.priority-low {
-    background: linear-gradient(180deg, #10b981 0%, #059669 100%);
-  }
-}
-
-.project-content {
-  flex: 1;
-  padding-left: 8px;
-}
-
-.project-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 12px;
-}
-
-.project-name {
-  margin: 0;
-  font-size: 16px;
-  font-weight: 600;
-  color: #111827;
-}
-
-.project-badges {
-  display: flex;
-  gap: 8px;
-}
-
-.badge {
-  padding: 4px 8px;
-  border-radius: 4px;
-  font-size: 12px;
-  font-weight: 500;
-  white-space: nowrap;
-
-  &.badge-stalled {
-    background: #f3f4f6;
-    color: #6b7280;
-  }
-
-  &.badge-urgent {
-    background: #fee2e2;
-    color: #991b1b;
-  }
-
-  &.badge-overdue {
-    background: #fecaca;
-    color: #7f1d1d;
-  }
-
-  &.badge-warning {
-    background: #fed7aa;
-    color: #9a3412;
-  }
-  
-  // 🆕 空间与交付物统计徽章(列表视图)
-  &.badge-deliverable {
-    padding: 4px 10px;
-    border-radius: 12px;
-    font-size: 11px;
-    font-weight: 600;
-    cursor: help;
-    transition: transform 0.2s, box-shadow 0.2s;
-    
-    &:hover {
-      transform: scale(1.05);
-      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
-    }
-  }
-}
-
-.project-meta {
-  display: flex;
-  gap: 16px;
-  flex-wrap: wrap;
-}
-
-.meta-item {
-  display: flex;
-  gap: 4px;
-  font-size: 13px;
-}
-
-.meta-label {
-  color: #6b7280;
-}
-
-.meta-value {
-  color: #374151;
-  font-weight: 500;
-
-  &.text-danger {
-    color: #dc2626;
-  }
-}
 
 // 空状态
 .empty-state {
@@ -423,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 {
@@ -441,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 {
@@ -449,6 +313,19 @@
     border-radius: 4px;
     transform: rotate(45deg);
   }
+
+  // 🆕 阶段文本图标样式
+  &.phase-icon {
+    font-size: 14px; // 调整字体大小使其在小圆圈中居中
+    font-weight: bold;
+    width: 28px;
+    height: 28px;
+    border-radius: 50%;
+    background: rgba(255, 255, 255, 0.2); // 半透明背景
+    border: 1px solid rgba(255, 255, 255, 0.4); // 描边
+    color: #ffffff;
+    box-shadow: none; // 移除阴影
+  }
 }
 
 .legend-bar-demo {
@@ -826,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 {
@@ -848,6 +736,16 @@
     animation: blink 1s infinite;
   }
 
+  &.phase-deadline {
+    font-size: 14px; // 调整字体大小使其在小圆圈中居中
+    font-weight: bold;
+    width: 28px;
+    height: 28px;
+    border-radius: 50%;
+    // background 会由 [style.background] 动态设置
+    color: #ffffff;
+  }
+
   &:hover {
     transform: translateY(-50%) scale(1.4);
     z-index: 20;
@@ -874,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 {
@@ -900,17 +820,6 @@
   .stats-body {
     grid-template-columns: 1fr;
   }
-
-  .project-header {
-    flex-direction: column;
-    align-items: flex-start;
-    gap: 8px;
-  }
-
-  .project-meta {
-    flex-direction: column;
-    gap: 8px;
-  }
   
   // 时间轴视图响应式
   .timeline-legend {
@@ -983,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);
+  }
+}

+ 147 - 28
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
@@ -65,7 +66,6 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
   selectedDesigner: string = 'all';
   selectedStatus: 'all' | 'normal' | 'warning' | 'urgent' | 'overdue' | 'stalled' = 'all';
   selectedPriority: 'all' | 'low' | 'medium' | 'high' | 'critical' = 'all';
-  viewMode: 'timeline' | 'list' = 'list';
   sortBy: 'priority' | 'time' = 'priority';
   
   // 设计师统计
@@ -85,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
@@ -346,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({
@@ -354,48 +387,51 @@ 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.icon
+              icon: phaseConfig.label.charAt(0)
             });
           }
         }
       });
     }
     
+    // 交付事件
+    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());
   }
@@ -515,11 +551,6 @@ export class ProjectTimelineComponent implements OnInit, OnDestroy {
     this.cdr.markForCheck(); // 触发变更检测
   }
 
-  toggleViewMode(mode: 'timeline' | 'list'): void {
-    this.viewMode = mode;
-    this.calculateTimeRange(); // 重新计算时间范围
-  }
-
   toggleSortBy(sortBy: 'priority' | 'time'): void {
     this.sortBy = sortBy;
     this.applyFilters();
@@ -534,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);
   }
 
@@ -741,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;
+  }
+
   /**
    * 检查项目是否所有空间都已上传交付物
    *