浏览代码

```
docs: add stagnation/modification implementation guide and enhance project marking workflow

- Created STAGNATION-MODIFICATION-IMPLEMENTATION.md with comprehensive technical documentation covering data flow, component methods, UI templates, and user experience workflows
- Enhanced project-kanban component to prioritize stagnation/modification status over regular stage mapping in getProjectsByCorePhase() method
- Added StagnationReasonModalComponent integration to dashboard with modal controls

0235711 16 小时之前
父节点
当前提交
be1d62a958

+ 217 - 0
STAGNATION-MODIFICATION-IMPLEMENTATION.md

@@ -0,0 +1,217 @@
+# 停滞期和改图期功能实现说明
+
+## 功能概述
+在组长端的紧急事件中标记项目为"停滞"或"改图"后,该项目会在项目监控看板中的"停滞期"或"改图期"列以卡片形式显示,并注明停滞或改图的原因。
+
+## 实现的关键修改
+
+### 1. Dashboard主组件 (`dashboard.ts`)
+
+#### 新增变量
+```typescript
+// 停滞/改图原因弹窗控制
+showStagnationModal: boolean = false;
+stagnationModalType: 'stagnation' | 'modification' = 'stagnation';
+stagnationModalProject: Project | null = null;
+```
+
+#### 核心方法
+
+**标记紧急事件时同步更新项目对象**
+- `markEventAsStagnant()`: 标记紧急事件为停滞时,调用 `updateProjectMarkStatus()` 同步更新对应的项目对象
+- `markEventAsModification()`: 标记紧急事件为改图时,调用 `updateProjectMarkStatus()` 同步更新对应的项目对象
+
+**从看板直接标记时弹出原因输入弹窗**
+- `markProjectAsStalled()`: 弹出停滞原因弹窗
+- `markProjectAsModification()`: 弹出改图原因弹窗
+
+**处理原因输入**
+- `onStagnationReasonConfirm()`: 确认原因后调用 `updateProjectMarkStatus()` 更新项目
+- `closeStagnationModal()`: 关闭原因输入弹窗
+
+**项目状态更新核心方法**
+```typescript
+private updateProjectMarkStatus(projectId: string, type: 'stagnation' | 'modification', reason: any): void {
+  // 更新项目的 isStalled/isModification 标志
+  // 保存原因类型、自定义原因、预计恢复时间、备注等信息
+  // 重新应用筛选
+}
+```
+
+### 2. Dashboard HTML (`dashboard.html`)
+
+新增停滞/改图原因弹窗组件:
+```html
+<app-stagnation-reason-modal
+  [isOpen]="showStagnationModal"
+  [eventType]="stagnationModalType"
+  [projectName]="stagnationModalProject?.name || ''"
+  (confirm)="onStagnationReasonConfirm($event)"
+  (cancel)="closeStagnationModal()">
+</app-stagnation-reason-modal>
+```
+
+### 3. 项目看板组件 (`project-kanban.component.ts`)
+
+修改了 `getProjectsByCorePhase()` 方法,实现了以下逻辑:
+
+```typescript
+getProjectsByCorePhase(coreId: string): Project[] {
+  return this.projects.filter(p => {
+    // 优先判断是否被标记为停滞或改图
+    if (p.isStalled && coreId === 'stalled') {
+      return true;
+    }
+    if (p.isModification && coreId === 'modification') {
+      return true;
+    }
+    
+    // 如果被标记为停滞或改图,不应该出现在其他常规列中
+    if (p.isStalled || p.isModification) {
+      return false;
+    }
+    
+    // 否则,根据 currentStage 映射到常规核心阶段
+    return this.mapStageToCorePhase(p.currentStage) === coreId;
+  });
+}
+```
+
+### 4. 看板卡片显示 (`project-kanban.component.html`)
+
+卡片中已有完善的原因显示逻辑(第42-77行):
+
+**停滞原因显示**
+```html
+@if (project.isStalled && project.stagnationReasonType) {
+  <div class="reason-label stagnant">
+    <span class="reason-text">
+      @if (project.stagnationReasonType === 'designer') { 设计师原因停滞 }
+      @if (project.stagnationReasonType === 'customer') { 客户原因停滞 }
+      @if (project.stagnationReasonType === 'custom') { {{ project.stagnationCustomReason }} }
+    </span>
+    @if (project.estimatedResumeDate) {
+      <span class="resume-date">({{ project.estimatedResumeDate | date:'MM-dd' }}恢复)</span>
+    }
+  </div>
+}
+```
+
+**改图原因显示**
+```html
+@if (project.isModification && project.modificationReasonType) {
+  <div class="reason-label modification">
+    <span class="reason-text">
+      @if (project.modificationReasonType === 'customer') { 客户要求改图 }
+      @if (project.modificationReasonType === 'designer') { 设计师优化 }
+      @if (project.modificationReasonType === 'custom') { {{ project.modificationCustomReason }} }
+    </span>
+  </div>
+}
+```
+
+**备注显示**
+```html
+@if ((project.isStalled || project.isModification) && project.reasonNotes) {
+  <div class="reason-notes">
+    {{ project.reasonNotes }}
+  </div>
+}
+```
+
+### 5. 样式 (`project-kanban.component.scss`)
+
+已有完善的样式(第214-269行):
+- `.reason-label.stagnant`: 红色背景,用于停滞原因
+- `.reason-label.modification`: 黄色背景,用于改图原因
+- `.reason-notes`: 灰色背景,用于备注
+
+### 6. 常量定义 (`dashboard.constants.ts`)
+
+核心阶段已包含停滞期和改图期:
+```typescript
+export const CORE_PHASES: ProjectStage[] = [
+  { id: 'order', name: '订单分配', order: 1 },
+  { id: 'requirements', name: '确认需求', order: 2 },
+  { id: 'delivery', name: '交付执行', order: 3 },
+  { id: 'stalled', name: '停滞期', order: 3.5 },       // 停滞期
+  { id: 'modification', name: '改图期', order: 3.8 },  // 改图期
+  { id: 'aftercare', name: '售后', order: 4 }
+];
+```
+
+## 数据流
+
+### 从紧急事件标记
+1. 用户在"紧急事件"中点击"标记为停滞/改图"
+2. 弹出 `StagnationReasonModalComponent` 原因输入弹窗
+3. 用户填写原因信息并确认
+4. 触发 `markEventAsStagnant()` 或 `markEventAsModification()`
+5. 调用 `updateProjectMarkStatus()` 更新项目对象的以下字段:
+   - `isStalled` / `isModification`
+   - `stagnationReasonType` / `modificationReasonType`
+   - `stagnationCustomReason` / `modificationCustomReason`
+   - `estimatedResumeDate`(仅停滞)
+   - `reasonNotes`
+   - `markedAt`, `markedBy`
+6. 重新应用筛选,项目出现在对应的看板列中
+
+### 从看板直接标记
+1. 用户在项目卡片上点击"⏸️"(停滞)或"✏️"(改图)按钮
+2. 触发 `markProjectAsStalled()` 或 `markProjectAsModification()`
+3. 弹出 `StagnationReasonModalComponent` 原因输入弹窗
+4. 用户填写原因信息并确认
+5. 触发 `onStagnationReasonConfirm()`
+6. 直接调用 `updateProjectMarkStatus()` 更新项目对象
+7. 重新应用筛选,项目出现在对应的看板列中
+
+## 项目数据模型
+
+`Project` 接口中包含以下相关字段(在 `interfaces.ts` 中定义):
+
+```typescript
+export interface Project {
+  // ... 其他字段
+  isStalled?: boolean;                      // 是否停滞
+  isModification?: boolean;                 // 是否改图
+  stagnationReasonType?: 'designer' | 'customer' | 'custom';
+  stagnationCustomReason?: string;
+  modificationReasonType?: 'designer' | 'customer' | 'custom';
+  modificationCustomReason?: string;
+  estimatedResumeDate?: Date;               // 预计恢复时间(仅停滞)
+  reasonNotes?: string;                     // 备注说明
+  markedAt?: Date;                          // 标记时间
+  markedBy?: string;                        // 标记人
+}
+```
+
+## 用户体验流程
+
+1. **标记操作**
+   - 从紧急事件标记:点击"标记为停滞/改图" → 弹窗填写原因 → 确认
+   - 从看板标记:点击卡片上的按钮 → 弹窗填写原因 → 确认
+
+2. **原因选择**
+   - 停滞原因:设计师原因 / 客户原因 / 自定义
+   - 改图原因:客户要求 / 设计师优化 / 自定义
+   - 可选填写:预计恢复时间(停滞)、备注说明
+
+3. **看板显示**
+   - 项目自动移动到"停滞期"或"改图期"列
+   - 卡片显示原因标签(红色/黄色)
+   - 显示预计恢复时间(如有)
+   - 显示备注说明(如有)
+
+## 注意事项
+
+1. **互斥关系**:项目不能同时处于停滞期和改图期
+2. **列显示控制**:默认情况下,"停滞期"和"改图期"列始终可见(订单和需求阶段可通过切换隐藏)
+3. **数据持久化**:目前通过 `saveEventMarkToDatabase()` 方法预留了持久化接口,需要后续实现真正的数据库保存逻辑
+
+## 后续优化建议
+
+1. 实现 `saveEventMarkToDatabase()` 方法,将标记信息保存到 Parse 数据库
+2. 在项目加载时从数据库读取停滞/改图标记信息
+3. 添加"取消标记"功能,允许将项目移出停滞期/改图期
+4. 添加停滞/改图历史记录追踪
+5. 在项目详情页显示完整的标记历史

+ 17 - 1
src/app/pages/team-leader/dashboard/components/project-kanban/project-kanban.component.ts

@@ -30,7 +30,23 @@ export class ProjectKanbanComponent {
   getProjectsByCorePhase(coreId: string): Project[] {
     if (!this.projects) return [];
     
-    return this.projects.filter(p => this.mapStageToCorePhase(p.currentStage) === coreId);
+    return this.projects.filter(p => {
+      // 🆕 优先判断是否被标记为停滞或改图
+      if (p.isStalled && coreId === 'stalled') {
+        return true;
+      }
+      if (p.isModification && coreId === 'modification') {
+        return true;
+      }
+      
+      // 如果被标记为停滞或改图,不应该出现在其他常规列中
+      if (p.isStalled || p.isModification) {
+        return false;
+      }
+      
+      // 否则,根据 currentStage 映射到常规核心阶段
+      return this.mapStageToCorePhase(p.currentStage) === coreId;
+    });
   }
 
   private mapStageToCorePhase(stageId: string): 'order' | 'requirements' | 'delivery' | 'aftercare' {

+ 10 - 1
src/app/pages/team-leader/dashboard/dashboard.html

@@ -145,4 +145,13 @@
   [recommendations]="recommendations"
   (close)="closeSmartMatch()"
   (assign)="assignToDesigner($event)">
-</app-smart-match-modal>
+</app-smart-match-modal>
+
+<!-- 停滞/改图原因弹窗 -->
+<app-stagnation-reason-modal
+  [isOpen]="showStagnationModal"
+  [eventType]="stagnationModalType"
+  [projectName]="stagnationModalProject?.name || ''"
+  (confirm)="onStagnationReasonConfirm($event)"
+  (cancel)="closeStagnationModal()">
+</app-stagnation-reason-modal>

+ 91 - 10
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -13,6 +13,7 @@ import { DashboardNavbarComponent } from './components/dashboard-navbar/dashboar
 import { WorkloadGanttComponent } from './components/workload-gantt/workload-gantt.component';
 import { TodoSectionComponent } from './components/todo-section/todo-section.component';
 import { SmartMatchModalComponent } from './components/smart-match-modal/smart-match-modal.component';
+import { StagnationReasonModalComponent } from './components/stagnation-reason-modal/stagnation-reason-modal.component';
 import { DashboardFilterBarComponent } from './components/dashboard-filter-bar/dashboard-filter-bar.component';
 import { ProjectKanbanComponent } from './components/project-kanban/project-kanban.component';
 import { DashboardAlertsComponent } from './components/dashboard-alerts/dashboard-alerts.component';
@@ -51,6 +52,7 @@ import {
     WorkloadGanttComponent,
     TodoSectionComponent,
     SmartMatchModalComponent,
+    StagnationReasonModalComponent,
     DashboardFilterBarComponent,
     ProjectKanbanComponent,
     DashboardAlertsComponent
@@ -100,6 +102,11 @@ export class Dashboard implements OnInit, OnDestroy {
   showSmartMatch: boolean = false;
   selectedProject: any = null;
   recommendations: any[] = [];
+  
+  // 🆕 停滞/改图原因弹窗
+  showStagnationModal: boolean = false;
+  stagnationModalType: 'stagnation' | 'modification' = 'stagnation';
+  stagnationModalProject: Project | null = null;
   // 新增:关键词搜索
   searchTerm: string = '';
   
@@ -949,6 +956,8 @@ export class Dashboard implements OnInit, OnDestroy {
 
   markEventAsStagnant(payload: {event: UrgentEvent, reason: any}): void {
     const { event, reason } = payload;
+    
+    // 更新紧急事件
     this.urgentEvents = this.urgentEvents.map(item => {
       if (item.id !== event.id) return item;
       const labels = new Set(item.labels || []);
@@ -969,6 +978,10 @@ export class Dashboard implements OnInit, OnDestroy {
         followUpNeeded: true
       };
     });
+    
+    // 🆕 同步更新对应的项目对象
+    this.updateProjectMarkStatus(event.projectId, 'stagnation', reason);
+    
     this.cdr.markForCheck();
     
     // TODO: 持久化到后端数据库
@@ -977,6 +990,8 @@ export class Dashboard implements OnInit, OnDestroy {
 
   markEventAsModification(payload: {event: UrgentEvent, reason: any}): void {
     const { event, reason } = payload;
+    
+    // 更新紧急事件
     this.urgentEvents = this.urgentEvents.map(item => {
       if (item.id !== event.id) return item;
       const labels = new Set(item.labels || []);
@@ -993,12 +1008,53 @@ export class Dashboard implements OnInit, OnDestroy {
         labels: Array.from(labels)
       };
     });
+    
+    // 🆕 同步更新对应的项目对象
+    this.updateProjectMarkStatus(event.projectId, 'modification', reason);
+    
     this.cdr.markForCheck();
     
     // TODO: 持久化到后端数据库
     this.saveEventMarkToDatabase(event, 'modification', reason);
   }
 
+  /**
+   * 更新项目的停滞/改图标记及原因信息
+   */
+  private updateProjectMarkStatus(projectId: string, type: 'stagnation' | 'modification', reason: any): void {
+    this.projects = this.projects.map(project => {
+      if (project.id !== projectId) return project;
+      
+      if (type === 'stagnation') {
+        return {
+          ...project,
+          isStalled: true,
+          isModification: false,
+          stagnationReasonType: reason.reasonType,
+          stagnationCustomReason: reason.customReason,
+          estimatedResumeDate: reason.estimatedResumeDate,
+          reasonNotes: reason.notes,
+          markedAt: new Date(),
+          markedBy: this.currentUser.name
+        };
+      } else {
+        return {
+          ...project,
+          isModification: true,
+          isStalled: false,
+          modificationReasonType: reason.reasonType,
+          modificationCustomReason: reason.customReason,
+          reasonNotes: reason.notes,
+          markedAt: new Date(),
+          markedBy: this.currentUser.name
+        };
+      }
+    });
+    
+    // 重新应用筛选
+    this.applyFilters();
+  }
+  
   private saveEventMarkToDatabase(event: UrgentEvent, type: 'stagnation' | 'modification', reason: any): void {
     // TODO: 实现数据持久化逻辑
     // 可以保存到 Parse 数据库的 ProjectEvent 或 UrgentEventMark 表
@@ -1044,20 +1100,45 @@ export class Dashboard implements OnInit, OnDestroy {
     console.log(`✅ 标记问题为已读: ${task.title}`);
   }
 
-  // 标记项目为停滞
+  // 标记项目为停滞(从看板触发)
   markProjectAsStalled(project: Project): void {
-    project.isStalled = true;
-    project.isModification = false; // 互斥
-    this.applyFilters();
-    window?.fmode?.alert('已标记为停滞项目');
+    // 🆕 弹出原因输入弹窗
+    this.stagnationModalType = 'stagnation';
+    this.stagnationModalProject = project;
+    this.showStagnationModal = true;
   }
 
-  // 标记项目为改图
+  // 标记项目为改图(从看板触发)
   markProjectAsModification(project: Project): void {
-    project.isModification = true;
-    project.isStalled = false; // 互斥
-    this.applyFilters();
-    window?.fmode?.alert('已标记为改图项目');
+    // 🆕 弹出原因输入弹窗
+    this.stagnationModalType = 'modification';
+    this.stagnationModalProject = project;
+    this.showStagnationModal = true;
+  }
+  
+  // 🆕 确认标记停滞/改图原因
+  onStagnationReasonConfirm(reason: any): void {
+    if (!this.stagnationModalProject) return;
+    
+    // 直接调用 updateProjectMarkStatus 更新项目
+    this.updateProjectMarkStatus(
+      this.stagnationModalProject.id,
+      this.stagnationModalType,
+      reason
+    );
+    
+    // 关闭弹窗
+    this.closeStagnationModal();
+    
+    // 显示确认消息
+    const message = this.stagnationModalType === 'stagnation' ? '已标记为停滞项目' : '已标记为改图项目';
+    window?.fmode?.alert(message);
+  }
+  
+  // 🆕 关闭停滞/改图原因弹窗
+  closeStagnationModal(): void {
+    this.showStagnationModal = false;
+    this.stagnationModalProject = null;
   }
   
   // 切换前期阶段显示

+ 19 - 5
src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.html

@@ -386,8 +386,8 @@
         
         <div class="project-list">
           @for (project of selectedDayProjects; track project.id) {
-            <div class="project-item" (click)="onProjectClick(project.id); closeCalendarProjectList()">
-              <div class="project-info">
+            <div class="project-item">
+              <div class="project-info" (click)="onProjectClick(project.id); closeCalendarProjectList()">
                 <svg class="project-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                   <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
                   <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
@@ -401,9 +401,17 @@
                   }
                 </div>
               </div>
-              <svg class="arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                <path d="M5 12h14M12 5l7 7-7 7"/>
-              </svg>
+              <div class="project-actions">
+                <button class="btn-view-progress" (click)="onViewProgress(project.id, $event)" title="查看项目进度">
+                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+                    <path d="M18 20V10M12 20V4M6 20v-6" />
+                  </svg>
+                  <span>进度</span>
+                </button>
+                <svg class="arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" (click)="onProjectClick(project.id); closeCalendarProjectList()">
+                  <path d="M5 12h14M12 5l7 7-7 7"/>
+                </svg>
+              </div>
             </div>
           }
         </div>
@@ -442,3 +450,9 @@
   </div>
 }
 
+<!-- 项目进度详情弹窗 -->
+<app-project-progress-modal
+  [visible]="showProgressModal"
+  [summary]="progressSummary"
+  (close)="closeProgressModal()">
+</app-project-progress-modal>

+ 57 - 12
src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.scss

@@ -943,7 +943,6 @@
         border: 1px solid #e2e8f0;
         border-radius: 12px;
         margin-bottom: 12px;
-        cursor: pointer;
         transition: all 0.2s;
 
         &:last-child {
@@ -953,7 +952,6 @@
         &:hover {
           border-color: #667eea;
           background: #f0f3ff;
-          transform: translateX(4px);
           box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
         }
 
@@ -962,6 +960,7 @@
           align-items: center;
           gap: 12px;
           flex: 1;
+          cursor: pointer;
 
           .project-icon {
             width: 32px;
@@ -988,17 +987,63 @@
           }
         }
 
-        .arrow-icon {
-          width: 20px;
-          height: 20px;
-          stroke: #94a3b8;
-          transition: all 0.2s;
-          flex-shrink: 0;
-        }
+        .project-actions {
+          display: flex;
+          align-items: center;
+          gap: 16px; // 增加间距
 
-        &:hover .arrow-icon {
-          stroke: #667eea;
-          transform: translateX(4px);
+          .btn-view-progress {
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            padding: 6px 12px;
+            background: #f0f3ff; // 淡紫色背景
+            color: #667eea; // 主题色文字
+            border: 1px solid rgba(102, 126, 234, 0.2); // 轻微边框
+            border-radius: 16px; // 胶囊形状
+            font-size: 12px;
+            font-weight: 600;
+            cursor: pointer;
+            transition: all 0.2s;
+            white-space: nowrap;
+
+            svg {
+              width: 14px;
+              height: 14px;
+              stroke: #667eea;
+              stroke-width: 2;
+            }
+
+            &:hover {
+              background: #667eea;
+              color: white;
+              border-color: #667eea;
+              transform: translateY(-1px);
+              box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
+
+              svg {
+                stroke: white;
+              }
+            }
+
+            &:active {
+              transform: translateY(0);
+            }
+          }
+
+          .arrow-icon {
+            width: 20px;
+            height: 20px;
+            stroke: #94a3b8;
+            transition: all 0.2s;
+            flex-shrink: 0;
+            cursor: pointer;
+
+            &:hover {
+              stroke: #667eea;
+              transform: translateX(4px);
+            }
+          }
         }
       }
     }

+ 45 - 2
src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.ts

@@ -2,6 +2,8 @@ import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChange
 import { CommonModule } from '@angular/common';
 import { Router } from '@angular/router';
 import { DesignerCalendarComponent, Designer as CalendarDesigner } from '../../customer-service/consultation-order/components/designer-calendar/designer-calendar.component';
+import { ProjectProgressModalComponent } from '../project-timeline/project-progress-modal';
+import { ProjectSpaceDeliverableService, ProjectSpaceDeliverableSummary } from '../../../../modules/project/services/project-space-deliverable.service';
 import { normalizeDateInput, addDays } from '../../../utils/date-utils';
 
 // 员工详情面板数据接口
@@ -47,7 +49,7 @@ export interface EmployeeCalendarDay {
 @Component({
   selector: 'app-employee-detail-panel',
   standalone: true,
-  imports: [CommonModule, DesignerCalendarComponent],
+  imports: [CommonModule, DesignerCalendarComponent, ProjectProgressModalComponent],
   templateUrl: './employee-detail-panel.html',
   styleUrls: ['./employee-detail-panel.scss']
 })
@@ -93,10 +95,17 @@ export class EmployeeDetailPanelComponent implements OnInit, OnChanges {
   showDesignerCalendar: boolean = false;
   calendarDesigners: CalendarDesigner[] = [];
   calendarViewMode: 'week' | 'month' | 'quarter' = 'month';
+
+  // 项目进度详情弹窗
+  showProgressModal: boolean = false;
+  selectedProjectForProgress: string = '';
+  progressSummary: ProjectSpaceDeliverableSummary | null = null;
+  loadingProgress: boolean = false;
   
   constructor(
     private router: Router,
-    private cdr: ChangeDetectorRef
+    private cdr: ChangeDetectorRef,
+    private projectSpaceDeliverableService: ProjectSpaceDeliverableService
   ) {}
   
   ngOnInit(): void {
@@ -577,6 +586,40 @@ export class EmployeeDetailPanelComponent implements OnInit, OnChanges {
     return typeMap[leaveType || ''] || '未知';
   }
   
+  /**
+   * 查看项目进度
+   */
+  async onViewProgress(projectId: string, event?: Event): Promise<void> {
+    if (event) {
+      event.stopPropagation();
+    }
+
+    this.selectedProjectForProgress = projectId;
+    this.loadingProgress = true;
+    this.showProgressModal = true;
+
+    try {
+      console.log('📊 加载项目进度数据:', projectId);
+      this.progressSummary = await this.projectSpaceDeliverableService.getProjectSpaceDeliverableSummary(projectId);
+      console.log('✅ 项目进度数据加载成功:', this.progressSummary);
+    } catch (error) {
+      console.error('❌ 加载项目进度数据失败:', error);
+      this.progressSummary = null;
+    } finally {
+      this.loadingProgress = false;
+      this.cdr.markForCheck();
+    }
+  }
+
+  /**
+   * 关闭进度弹窗
+   */
+  closeProgressModal(): void {
+    this.showProgressModal = false;
+    this.selectedProjectForProgress = '';
+    this.progressSummary = null;
+  }
+
   /**
    * 阻止事件冒泡
    */