Procházet zdrojové kódy

feat: enhance urgent tasks dashboard with customer alert features

- Added new badge styles for task statuses (overdue, upcoming, stagnant, customer) to improve visual feedback.
- Implemented customer alert functionality in the urgent tasks dashboard, allowing for better tracking of customer-related events.
- Updated HTML and TypeScript logic to support filtering and displaying customer service alerts.
- Introduced follow-up tips and action buttons for managing urgent tasks effectively.
0235711 před 20 hodinami
rodič
revize
be54dba92d

+ 70 - 0
src/app/pages/customer-service/dashboard/dashboard-urgent-tasks-enhanced.scss

@@ -133,6 +133,76 @@
   }
 }
 
+.badge-status {
+  display: inline-flex;
+  align-items: center;
+  padding: 2px 8px;
+  border-radius: 999px;
+  font-size: 11px;
+  font-weight: 600;
+  margin-left: 6px;
+  background: #edf2ff;
+  color: #4338ca;
+
+  &.overdue {
+    background: #fee2e2;
+    color: #b91c1c;
+  }
+
+  &.upcoming {
+    background: #fef3c7;
+    color: #b45309;
+  }
+
+  &.stagnant {
+    background: #ffe4e6;
+    color: #be123c;
+  }
+
+  &.customer {
+    background: #e0f2fe;
+    color: #0369a1;
+  }
+}
+
+.followup-tip {
+  margin-top: 8px;
+  padding: 8px 12px;
+  background: #fff7ed;
+  border-radius: 8px;
+  font-size: 12px;
+  color: #c2410c;
+  border: 1px dashed #fb923c;
+}
+
+.task-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+
+  .btn-action {
+    &.btn-muted {
+      background: rgba(79, 70, 229, 0.1);
+      color: #4338ca;
+    }
+
+    &.btn-stagnant {
+      background: rgba(190, 18, 60, 0.12);
+      color: #be123c;
+    }
+
+    &.btn-resolve {
+      background: rgba(16, 185, 129, 0.12);
+      color: #047857;
+    }
+
+    &.btn-todo {
+      background: rgba(14, 116, 144, 0.12);
+      color: #0e7490;
+    }
+  }
+}
+
 // 空状态样式
 .empty-state.filtered {
   display: flex;

+ 60 - 4
src/app/pages/customer-service/dashboard/dashboard.html

@@ -282,7 +282,18 @@
         >
           <span class="tag-icon">📋</span>
           <span class="tag-label">全部</span>
-          <span class="tag-count">{{ urgentEventsList().length || 95 }}</span>
+          <span class="tag-count">{{ urgentEventsList().length }}</span>
+        </button>
+        
+        <button 
+          class="tag-button"
+          [class.active]="urgentEventTagFilter() === 'customer'"
+          (click)="filterUrgentEventsByTag('customer')"
+          title="客户服务预警"
+        >
+          <span class="tag-icon">👥</span>
+          <span class="tag-label">客户服务</span>
+          <span class="tag-count">{{ getTagCount('customer') }}</span>
         </button>
         
         <!-- 工作阶段标签 -->
@@ -294,7 +305,7 @@
         >
           <span class="tag-icon">🔧</span>
           <span class="tag-label">工作阶段</span>
-          <span class="tag-count">{{ getTagCount('phase') || 12 }}</span>
+          <span class="tag-count">{{ getTagCount('phase') }}</span>
         </button>
         
         <!-- 小图截止标签 -->
@@ -306,7 +317,7 @@
         >
           <span class="tag-icon">📐</span>
           <span class="tag-label">小图截止</span>
-          <span class="tag-count">{{ getTagCount('review') || 28 }}</span>
+          <span class="tag-count">{{ getTagCount('review') }}</span>
         </button>
         
         <!-- 交付延期标签 -->
@@ -318,7 +329,7 @@
         >
           <span class="tag-icon">📦</span>
           <span class="tag-label">交付延期</span>
-          <span class="tag-count">{{ getTagCount('delivery') || 55 }}</span>
+          <span class="tag-count">{{ getTagCount('delivery') }}</span>
         </button>
       </div>
       
@@ -366,7 +377,14 @@
                       @if (event.eventType === 'review') { 对图 }
                       @else if (event.eventType === 'delivery') { 交付 }
                       @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
+                      @else if (event.category === 'customer') { 客户 }
                     </span>
+                    <span class="badge badge-status overdue" *ngIf="event.statusType === 'overdue'">逾期</span>
+                    <span class="badge badge-status upcoming" *ngIf="event.statusType === 'dueSoon'">临近</span>
+                    <span class="badge badge-status stagnant" *ngIf="event.statusType === 'stagnant'">
+                      停滞{{ event.stagnationDays || 7 }}天
+                    </span>
+                    <span class="badge badge-status customer" *ngIf="getEventCategory(event) === 'customer'">客户预警</span>
                   </div>
                 </div>
                 
@@ -375,6 +393,12 @@
                   {{ event.description }}
                 </div>
                 
+                @if (event.followUpNeeded) {
+                  <div class="followup-tip">
+                    客户反馈待跟进 · 请尽快处理
+                  </div>
+                }
+                
                 <!-- 项目信息行 -->
                 <div class="task-meta">
                   <span class="project-info">
@@ -424,6 +448,38 @@
               
               <!-- 右侧操作按钮 -->
               <div class="task-actions">
+                <button 
+                  class="btn-action btn-muted" 
+                  *ngIf="event.allowConfirmOnTime"
+                  (click)="confirmEventOnTime(event)"
+                  title="确认可按时交付后隐藏该事件"
+                >
+                  可按时交付
+                </button>
+                <button 
+                  class="btn-action btn-stagnant"
+                  *ngIf="event.statusType !== 'stagnant'"
+                  (click)="markEventAsStagnant(event)"
+                  title="标记为客户停滞期"
+                >
+                  标记停滞
+                </button>
+                <button 
+                  class="btn-action btn-resolve" 
+                  *ngIf="event.allowMarkHandled"
+                  (click)="resolveUrgentEvent(event)"
+                  title="事件已处理,不再提醒"
+                >
+                  事件已处理
+                </button>
+                <button 
+                  class="btn-action btn-todo"
+                  *ngIf="event.allowCreateTodo"
+                  (click)="createTodoFromEvent(event)"
+                  title="将该事件生成代办任务"
+                >
+                  创建代办
+                </button>
                 <button 
                   class="btn-action btn-view" 
                   (click)="onUrgentEventViewProject(event.projectId)"

+ 217 - 42
src/app/pages/customer-service/dashboard/dashboard.ts

@@ -15,7 +15,7 @@ interface UrgentEvent {
   id: string;
   title: string;
   description: string;
-  eventType: 'review' | 'delivery' | 'phase_deadline'; // 事件类型
+  eventType: 'review' | 'delivery' | 'phase_deadline' | 'customer_alert'; // 事件类型
   phaseName?: string; // 阶段名称(如果是阶段截止)
   deadline: Date; // 截止时间
   projectId: string;
@@ -24,6 +24,16 @@ interface UrgentEvent {
   urgencyLevel: 'critical' | 'high' | 'medium'; // 紧急程度
   overdueDays?: number; // 逾期天数(负数表示还有几天)
   completionRate?: number; // 完成率(0-100)
+  category?: 'customer' | 'phase' | 'review' | 'delivery';
+  statusType?: 'dueSoon' | 'overdue' | 'stagnant';
+  followUpNeeded?: boolean;
+  allowConfirmOnTime?: boolean;
+  allowMarkHandled?: boolean;
+  allowCreateTodo?: boolean;
+  stagnationDays?: number;
+  customerIssueType?: 'feedback_pending' | 'complaint' | 'idle';
+  labels?: string[];
+  isMuted?: boolean;
 }
 
 const Parse = FmodeParse.with('nova');
@@ -165,6 +175,8 @@ export class Dashboard implements OnInit, OnDestroy {
   // ⭐ 紧急事件列表(复用组长端逻辑)
   urgentEventsList = signal<UrgentEvent[]>([]);
   loadingUrgentEvents = signal(false);
+  handledUrgentEventIds = signal<Set<string>>(new Set());
+  mutedUrgentEventIds = signal<Set<string>>(new Set());
   
   // 🆕 待办事项标签筛选功能
   urgentEventTagFilter = signal<'all' | 'customer' | 'phase' | 'review' | 'delivery'>('all');
@@ -172,30 +184,23 @@ export class Dashboard implements OnInit, OnDestroy {
   // 标签统计(根据优先级计算)
   urgentEventTags = computed(() => {
     const events = this.urgentEventsList();
-    
-    // 按类型分类统计
-    let customerCount = 0;
-    let phaseCount = 0;
-    let reviewCount = 0;
-    let deliveryCount = 0;
+    const counts = {
+      customer: 0,
+      phase: 0,
+      review: 0,
+      delivery: 0
+    };
     
     events.forEach(event => {
-      if (event.eventType === 'review') {
-        reviewCount++;
-      } else if (event.eventType === 'delivery') {
-        deliveryCount++;
-      } else if (event.eventType === 'phase_deadline') {
-        phaseCount++;
-      }
+      const category = this.getEventCategory(event);
+      counts[category] = (counts[category] ?? 0) + 1;
     });
     
-    // 根据优先级:客户 > 工作阶段 > 小图截止 > 交付延期
-    // 这里构建一个有序的标签列表
     const tags = [
-      { id: 'customer', label: '客户服务', count: customerCount, icon: '👥' },
-      { id: 'phase', label: '工作阶段', count: phaseCount, icon: '🔧' },
-      { id: 'review', label: '小图截止', count: reviewCount, icon: '📐' },
-      { id: 'delivery', label: '交付延期', count: deliveryCount, icon: '📦' }
+      { id: 'customer', label: '客户服务', count: counts.customer, icon: '👥' },
+      { id: 'phase', label: '工作阶段', count: counts.phase, icon: '🔧' },
+      { id: 'review', label: '小图截止', count: counts.review, icon: '📐' },
+      { id: 'delivery', label: '交付延期', count: counts.delivery, icon: '📦' }
     ];
     
     return tags.filter(tag => tag.count > 0);
@@ -210,20 +215,7 @@ export class Dashboard implements OnInit, OnDestroy {
       return events;
     }
     
-    // 按标签过滤
-    switch (filter) {
-      case 'customer':
-        // 客户相关事件:可以根据具体需求判断
-        return events.filter(e => e.eventType === 'review'); // 或其他条件
-      case 'phase':
-        return events.filter(e => e.eventType === 'phase_deadline');
-      case 'review':
-        return events.filter(e => e.eventType === 'review');
-      case 'delivery':
-        return events.filter(e => e.eventType === 'delivery');
-      default:
-        return events;
-    }
+    return events.filter(event => this.getEventCategory(event) === filter);
   });
   
   // 项目时间轴数据(用于计算紧急事件)
@@ -1834,6 +1826,46 @@ onSearchInput(event: Event): void {
     const events: UrgentEvent[] = [];
     const now = new Date();
     const oneDayMs = 24 * 60 * 60 * 1000;
+    const handledSet = this.handledUrgentEventIds();
+    const mutedSet = this.mutedUrgentEventIds();
+    
+    const resolveCategory = (
+      eventType: UrgentEvent['eventType'],
+      category?: 'customer' | 'phase' | 'review' | 'delivery'
+    ): 'customer' | 'phase' | 'review' | 'delivery' => {
+      if (category) return category;
+      switch (eventType) {
+        case 'phase_deadline':
+          return 'phase';
+        case 'delivery':
+          return 'delivery';
+        case 'customer_alert':
+          return 'customer';
+        default:
+          return 'review';
+      }
+    };
+    
+    const addEvent = (
+      partial: Omit<UrgentEvent, 'category' | 'statusType' | 'labels' | 'allowConfirmOnTime' | 'allowMarkHandled' | 'allowCreateTodo' | 'followUpNeeded'> &
+        Partial<UrgentEvent>
+    ) => {
+      const category = resolveCategory(partial.eventType, partial.category);
+      const statusType: UrgentEvent['statusType'] =
+        partial.statusType || (partial.overdueDays && partial.overdueDays > 0 ? 'overdue' : 'dueSoon');
+      const event: UrgentEvent = {
+        ...partial,
+        category,
+        statusType,
+        labels: partial.labels ?? [],
+        followUpNeeded: partial.followUpNeeded ?? false,
+        allowConfirmOnTime:
+          partial.allowConfirmOnTime ?? (category !== 'customer' && statusType === 'dueSoon'),
+        allowMarkHandled: partial.allowMarkHandled ?? true,
+        allowCreateTodo: partial.allowCreateTodo ?? category === 'customer'
+      };
+      events.push(event);
+    };
     
     try {
       // 从 projectTimelineData 中提取数据
@@ -1846,7 +1878,7 @@ onSearchInput(event: Event): void {
           
           // 如果小图对图已经到期或即将到期(1天内),且不在交付完成阶段
           if (daysDiff <= 1 && project.currentStage !== 'delivery') {
-            events.push({
+            addEvent({
               id: `${project.projectId}-review`,
               title: `小图对图截止`,
               description: `项目「${project.projectName}」的小图对图时间已${daysDiff < 0 ? '逾期' : '临近'}`,
@@ -1856,7 +1888,9 @@ onSearchInput(event: Event): void {
               projectName: project.projectName,
               designerName: project.designerName,
               urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
-              overdueDays: -daysDiff
+              overdueDays: -daysDiff,
+              labels: daysDiff < 0 ? ['逾期'] : ['临近'],
+              followUpNeeded: project.currentStage?.includes('图') || project.status === 'warning'
             });
           }
         }
@@ -1872,7 +1906,7 @@ onSearchInput(event: Event): void {
             const summary = project.spaceDeliverableSummary;
             const completionRate = summary?.overallCompletionRate || 0;
             
-            events.push({
+            addEvent({
               id: `${project.projectId}-delivery`,
               title: `项目交付截止`,
               description: `项目「${project.projectName}」需要在 ${project.deliveryDate.toLocaleDateString()} 交付(当前完成率 ${completionRate}%)`,
@@ -1883,7 +1917,8 @@ onSearchInput(event: Event): void {
               designerName: project.designerName,
               urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
               overdueDays: -daysDiff,
-              completionRate
+              completionRate,
+              labels: daysDiff < 0 ? ['逾期'] : ['临近']
             });
           }
         }
@@ -1916,7 +1951,7 @@ onSearchInput(event: Event): void {
                   completionRate = phaseProgress?.completionRate || 0;
                 }
                 
-                events.push({
+                addEvent({
                   id: `${project.projectId}-phase-${key}`,
                   title: `${phaseName}阶段截止`,
                   description: `项目「${project.projectName}」的${phaseName}阶段截止时间已${daysDiff < 0 ? '逾期' : '临近'}(完成率 ${completionRate}%)`,
@@ -1928,12 +1963,78 @@ onSearchInput(event: Event): void {
                   designerName: project.designerName,
                   urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
                   overdueDays: -daysDiff,
-                  completionRate
+                  completionRate,
+                  labels: daysDiff < 0 ? ['逾期'] : ['临近']
                 });
               }
             }
           });
         }
+        
+        // 4. 客户停滞期检测
+        if (project.stalledDays && project.stalledDays >= 7) {
+          addEvent({
+            id: `${project.projectId}-stagnant`,
+            title: project.stalledDays >= 14 ? '客户停滞预警' : '停滞期提醒',
+            description: `项目「${project.projectName}」已有 ${project.stalledDays} 天没有客户反馈,需要主动跟进。`,
+            eventType: 'customer_alert',
+            deadline: new Date(),
+            projectId: project.projectId,
+            projectName: project.projectName,
+            designerName: project.designerName,
+            urgencyLevel: project.stalledDays >= 14 ? 'critical' : 'high',
+            statusType: 'stagnant',
+            stagnationDays: project.stalledDays,
+            followUpNeeded: true,
+            labels: ['停滞期'],
+            allowConfirmOnTime: false,
+            allowCreateTodo: true,
+            allowMarkHandled: true,
+            category: 'customer'
+          });
+        }
+        
+        // 5. 客户反馈/投诉预警
+        const inReviewStage = (project.stageName || '').includes('图') || (project.currentStage || '').includes('图');
+        if (inReviewStage && project.status === 'warning') {
+          addEvent({
+            id: `${project.projectId}-review-followup`,
+            title: '对图反馈待跟进',
+            description: `项目「${project.projectName}」客户反馈尚未得到响应,请尽快跟进处理。`,
+            eventType: 'customer_alert',
+            deadline: project.reviewDate || new Date(),
+            projectId: project.projectId,
+            projectName: project.projectName,
+            designerName: project.designerName,
+            urgencyLevel: 'high',
+            statusType: project.reviewDate && project.reviewDate < now ? 'overdue' : 'dueSoon',
+            followUpNeeded: true,
+            labels: ['对图期'],
+            allowCreateTodo: true,
+            customerIssueType: 'feedback_pending',
+            category: 'customer'
+          });
+        }
+        
+        if (project.priority === 'critical') {
+          addEvent({
+            id: `${project.projectId}-customer-alert`,
+            title: '客户服务预警',
+            description: `项目「${project.projectName}」出现客户强烈不满或投诉,请立即处理并记录归因。`,
+            eventType: 'customer_alert',
+            deadline: new Date(),
+            projectId: project.projectId,
+            projectName: project.projectName,
+            designerName: project.designerName,
+            urgencyLevel: 'critical',
+            statusType: 'dueSoon',
+            followUpNeeded: true,
+            labels: ['客户预警'],
+            allowCreateTodo: true,
+            customerIssueType: 'complaint',
+            category: 'customer'
+          });
+        }
       });
       
       // 按紧急程度和时间排序
@@ -1947,7 +2048,8 @@ onSearchInput(event: Event): void {
         return a.deadline.getTime() - b.deadline.getTime();
       });
       
-      this.urgentEventsList.set(events);
+      const visibleEvents = events.filter(event => !handledSet.has(event.id) && !mutedSet.has(event.id));
+      this.urgentEventsList.set(visibleEvents);
       console.log(`✅ [客服-紧急事件] 计算完成,共 ${events.length} 个紧急事件`);
       
     } catch (error) {
@@ -1986,13 +2088,86 @@ onSearchInput(event: Event): void {
     this.urgentEventTagFilter.set(tag);
   }
   
+  confirmEventOnTime(event: UrgentEvent): void {
+    const next = new Set(this.mutedUrgentEventIds());
+    next.add(event.id);
+    this.mutedUrgentEventIds.set(next);
+    this.calculateUrgentEvents();
+  }
+
+  resolveUrgentEvent(event: UrgentEvent): void {
+    const next = new Set(this.handledUrgentEventIds());
+    next.add(event.id);
+    this.handledUrgentEventIds.set(next);
+    this.calculateUrgentEvents();
+  }
+
+  markEventAsStagnant(event: UrgentEvent): void {
+    const updated = this.urgentEventsList().map(item => {
+      if (item.id !== event.id) {
+        return item;
+      }
+      const labels = new Set(item.labels || []);
+      labels.add('停滞期');
+      return {
+        ...item,
+        category: 'customer' as const,
+        statusType: 'stagnant' as const,
+        stagnationDays: item.stagnationDays || 7,
+        labels: Array.from(labels),
+        followUpNeeded: true
+      };
+    });
+    this.urgentEventsList.set(updated);
+  }
+
+  createTodoFromEvent(event: UrgentEvent): void {
+    const now = new Date();
+    const newTask: TodoTaskFromIssue = {
+      id: `urgent-todo-${event.id}-${now.getTime()}`,
+      title: `【紧急】${event.title}`,
+      description: event.description,
+      priority: event.urgencyLevel === 'critical' ? 'urgent' : event.urgencyLevel === 'high' ? 'high' : 'medium',
+      type: 'feedback',
+      status: 'open',
+      projectId: event.projectId,
+      projectName: event.projectName,
+      relatedStage: event.phaseName,
+      assigneeName: event.designerName || '待分配',
+      creatorName: this.currentUser()?.name || '系统',
+      createdAt: now,
+      updatedAt: now,
+      dueDate: event.deadline,
+      tags: [...(event.labels || []), '来自紧急事件']
+    };
+    this.todoTasksFromIssues.update(tasks => [newTask, ...tasks]);
+    console.log('✅ 已从紧急事件创建代办任务', newTask);
+    this.resolveUrgentEvent(event);
+  }
+
   /**
    * 获取指定标签的计数
    */
-  getTagCount(tagId: string): number {
+  getTagCount(tagId: 'customer' | 'phase' | 'review' | 'delivery'): number {
     const tag = this.urgentEventTags().find(t => t.id === tagId);
     return tag?.count || 0;
   }
+
+  getEventCategory(event: UrgentEvent): 'customer' | 'phase' | 'review' | 'delivery' {
+    if (event.category) return event.category;
+    switch (event.eventType) {
+      case 'phase_deadline':
+        return 'phase';
+      case 'delivery':
+        return 'delivery';
+      case 'review':
+        return 'review';
+      case 'customer_alert':
+        return 'customer';
+      default:
+        return 'review';
+    }
+  }
   
   /**
    * ⭐ 从待办任务面板查看详情(跳转到项目并显示问题弹窗)

+ 55 - 4
src/app/pages/team-leader/dashboard/dashboard.html

@@ -445,6 +445,17 @@
               <span class="tag-count">{{ urgentEvents.length }}</span>
             </button>
             
+            <button 
+              class="tag-button"
+              [class.active]="urgentEventTagFilter === 'customer'"
+              (click)="filterUrgentEventsByTag('customer')"
+              title="客户服务"
+            >
+              <span class="tag-icon">👥</span>
+              <span class="tag-label">客户服务</span>
+              <span class="tag-count">{{ getTagCount('customer') }}</span>
+            </button>
+            
             <button 
               class="tag-button"
               [class.active]="urgentEventTagFilter === 'phase'"
@@ -501,7 +512,7 @@
           </div>
         }
 
-        @if (!loadingUrgentEvents && urgentEvents.length > 0 && filteredUrgentEvents.length === 0) {
+        @if (!loadingUrgentEvents && urgentEvents.length > 0 && filteredUrgentEventsList.length === 0) {
           <div class="empty-state filtered">
             <svg viewBox="0 0 24 24" width="48" height="48" fill="#d1d5db">
               <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
@@ -512,9 +523,9 @@
         }
         
         <!-- 紧急事件列表 -->
-        @if (!loadingUrgentEvents && filteredUrgentEvents.length > 0) {
+        @if (!loadingUrgentEvents && filteredUrgentEventsList.length > 0) {
           <div class="todo-list-compact urgent-list">
-            @for (event of filteredUrgentEvents; track event.id) {
+            @for (event of filteredUrgentEventsList; track trackUrgentEventById($index, event)) {
               <div class="todo-item-compact urgent-item" [attr.data-urgency]="event.urgencyLevel">
                 <!-- 左侧紧急程度色条 -->
                 <div class="urgency-indicator" [attr.data-urgency]="event.urgencyLevel"></div>
@@ -533,8 +544,15 @@
                       <span class="badge badge-event-type">
                         @if (event.eventType === 'review') { 对图 }
                         @else if (event.eventType === 'delivery') { 交付 }
-                        @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
+                      @else if (event.eventType === 'phase_deadline') { {{ event.phaseName }} }
+                      @else if (getEventCategory(event) === 'customer') { 客户 }
                       </span>
+                    <span class="badge-status overdue" *ngIf="event.statusType === 'overdue'">逾期</span>
+                    <span class="badge-status upcoming" *ngIf="event.statusType === 'dueSoon'">临近</span>
+                    <span class="badge-status stagnant" *ngIf="event.statusType === 'stagnant'">
+                      停滞{{ event.stagnationDays || 7 }}天
+                    </span>
+                    <span class="badge-status customer" *ngIf="getEventCategory(event) === 'customer'">客户预警</span>
                     </div>
                   </div>
                   
@@ -542,6 +560,11 @@
                   <div class="task-description">
                     {{ event.description }}
                   </div>
+                @if (event.followUpNeeded) {
+                  <div class="followup-tip">
+                    客户反馈待跟进 · 请及时追踪
+                  </div>
+                }
                   
                   <!-- 项目信息行 -->
                   <div class="task-meta">
@@ -592,6 +615,34 @@
                 
                 <!-- 右侧操作按钮 -->
                 <div class="task-actions">
+                  <button 
+                    class="btn-action btn-muted" 
+                    *ngIf="event.allowConfirmOnTime"
+                    (click)="confirmEventOnTime(event)"
+                  >
+                    可按时交付
+                  </button>
+                  <button 
+                    class="btn-action btn-stagnant" 
+                    *ngIf="event.statusType !== 'stagnant'"
+                    (click)="markEventAsStagnant(event)"
+                  >
+                    标记停滞
+                  </button>
+                  <button 
+                    class="btn-action btn-resolve" 
+                    *ngIf="event.allowMarkHandled"
+                    (click)="resolveUrgentEvent(event)"
+                  >
+                    事件已处理
+                  </button>
+                  <button 
+                    class="btn-action btn-todo" 
+                    *ngIf="event.allowCreateTodo"
+                    (click)="createTodoFromEvent(event)"
+                  >
+                    创建代办
+                  </button>
                   <button 
                     class="btn-action btn-view" 
                     (click)="onProjectClick(event.projectId)"

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

@@ -2567,6 +2567,76 @@
   }
 }
 
+.badge-status {
+  display: inline-flex;
+  align-items: center;
+  padding: 2px 8px;
+  border-radius: 999px;
+  font-size: 11px;
+  font-weight: 600;
+  margin-left: 6px;
+  background: #edf2ff;
+  color: #4338ca;
+
+  &.overdue {
+    background: #fee2e2;
+    color: #b91c1c;
+  }
+
+  &.upcoming {
+    background: #fef3c7;
+    color: #b45309;
+  }
+
+  &.stagnant {
+    background: #ffe4e6;
+    color: #be123c;
+  }
+
+  &.customer {
+    background: #e0f2fe;
+    color: #0369a1;
+  }
+}
+
+.followup-tip {
+  margin-top: 8px;
+  padding: 8px 12px;
+  background: #fff7ed;
+  border-radius: 8px;
+  font-size: 12px;
+  color: #c2410c;
+  border: 1px dashed #fb923c;
+}
+
+.task-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+
+  .btn-action {
+    &.btn-muted {
+      background: rgba(79, 70, 229, 0.1);
+      color: #4338ca;
+    }
+
+    &.btn-stagnant {
+      background: rgba(190, 18, 60, 0.12);
+      color: #be123c;
+    }
+
+    &.btn-resolve {
+      background: rgba(16, 185, 129, 0.12);
+      color: #047857;
+    }
+
+    &.btn-todo {
+      background: rgba(14, 116, 144, 0.12);
+      color: #0e7490;
+    }
+  }
+}
+
 .empty-state.filtered {
   display: flex;
   flex-direction: column;

+ 319 - 39
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -1,7 +1,7 @@
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { Router, RouterModule } from '@angular/router';
-import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
+import { Component, OnInit, OnDestroy, ElementRef, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
 import { ProjectService } from '../../../services/project.service';
 import { DesignerService } from '../services/designer.service';
 import { WxworkAuth } from 'fmode-ng/core';
@@ -90,7 +90,7 @@ interface UrgentEvent {
   id: string;
   title: string;
   description: string;
-  eventType: 'review' | 'delivery' | 'phase_deadline'; // 事件类型
+  eventType: 'review' | 'delivery' | 'phase_deadline' | 'customer_alert'; // 事件类型
   phaseName?: string; // 阶段名称(如果是阶段截止)
   deadline: Date; // 截止时间
   projectId: string;
@@ -99,6 +99,16 @@ interface UrgentEvent {
   urgencyLevel: 'critical' | 'high' | 'medium'; // 紧急程度
   overdueDays?: number; // 逾期天数(负数表示还有几天)
   completionRate?: number; // 完成率(0-100)
+  category?: 'customer' | 'phase' | 'review' | 'delivery';
+  statusType?: 'dueSoon' | 'overdue' | 'stagnant';
+  followUpNeeded?: boolean;
+  allowConfirmOnTime?: boolean;
+  allowMarkHandled?: boolean;
+  allowCreateTodo?: boolean;
+  stagnationDays?: number;
+  customerIssueType?: 'feedback_pending' | 'complaint' | 'idle';
+  labels?: string[];
+  isMuted?: boolean;
 }
 
 // 员工请假记录接口
@@ -147,7 +157,8 @@ declare const echarts: any;
   standalone: true,
   imports: [CommonModule, FormsModule, RouterModule, ProjectTimelineComponent, EmployeeDetailPanelComponent],
   templateUrl: './dashboard.html',
-  styleUrl: './dashboard.scss'
+  styleUrl: './dashboard.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush
 })
 
 export class Dashboard implements OnInit, OnDestroy {
@@ -170,8 +181,14 @@ export class Dashboard implements OnInit, OnDestroy {
   // 🆕 紧急事件(从项目时间轴自动计算)
   urgentEvents: UrgentEvent[] = [];
   loadingUrgentEvents: boolean = false;
+  handledUrgentEventIds: Set<string> = new Set();
+  mutedUrgentEventIds: Set<string> = new Set();
+  filteredUrgentEventsList: UrgentEvent[] = [];
   urgentEventTagFilter: 'all' | 'customer' | 'phase' | 'review' | 'delivery' = 'all';
   
+  // 🔥 性能优化:预计算各标签的事件列表(O(1) 切换)
+  private urgentEventsCache = new Map<string, UrgentEvent[]>();
+  
   // 新增:当前用户信息
   currentUser = {
     name: '组长',
@@ -305,11 +322,13 @@ export class Dashboard implements OnInit, OnDestroy {
     { id: '27', employeeName: '赵六', date: '2024-01-25', isLeave: false },
     { id: '28', employeeName: '赵六', date: '2024-01-26', isLeave: false }
   ];
+  
   constructor(
     private projectService: ProjectService, 
     private router: Router,
     private designerService: DesignerService,
-    private issueService: ProjectIssueService
+    private issueService: ProjectIssueService,
+    private cdr: ChangeDetectorRef
   ) {}
 
   async ngOnInit(): Promise<void> {
@@ -3928,6 +3947,60 @@ export class Dashboard implements OnInit, OnDestroy {
     const events: UrgentEvent[] = [];
     const now = new Date();
     const oneDayMs = 24 * 60 * 60 * 1000;
+    const handledSet = this.handledUrgentEventIds;
+    const mutedSet = this.mutedUrgentEventIds;
+    const projectEventCount = new Map<string, number>(); // 追踪每个项目生成的事件数
+    const MAX_EVENTS_PER_PROJECT = 2; // 每个项目最多生成2个最紧急的事件
+
+    const resolveCategory = (
+      eventType: UrgentEvent['eventType'],
+      category?: 'customer' | 'phase' | 'review' | 'delivery'
+    ): 'customer' | 'phase' | 'review' | 'delivery' => {
+      if (category) return category;
+      switch (eventType) {
+        case 'phase_deadline':
+          return 'phase';
+        case 'delivery':
+          return 'delivery';
+        case 'customer_alert':
+          return 'customer';
+        default:
+          return 'review';
+      }
+    };
+
+    const addEvent = (
+      partial: Omit<UrgentEvent, 'category' | 'statusType' | 'labels' | 'allowConfirmOnTime' | 'allowMarkHandled' | 'allowCreateTodo' | 'followUpNeeded'> &
+        Partial<UrgentEvent>
+    ) => {
+      // 检查该项目是否已达到事件数量上限
+      const currentCount = projectEventCount.get(partial.projectId) || 0;
+      if (currentCount >= MAX_EVENTS_PER_PROJECT) {
+        return; // 跳过,避免单个项目产生过多事件
+      }
+      
+      const category = resolveCategory(partial.eventType, partial.category);
+      const statusType: UrgentEvent['statusType'] =
+        partial.statusType || (partial.overdueDays && partial.overdueDays > 0 ? 'overdue' : 'dueSoon');
+      
+      // 简化描述,避免过长字符串
+      const description = partial.description?.substring(0, 100) || '';
+      
+      const event: UrgentEvent = {
+        ...partial,
+        description,
+        category,
+        statusType,
+        labels: partial.labels ?? [],
+        followUpNeeded: partial.followUpNeeded ?? false,
+        allowConfirmOnTime:
+          partial.allowConfirmOnTime ?? (category !== 'customer' && statusType === 'dueSoon'),
+        allowMarkHandled: partial.allowMarkHandled ?? true,
+        allowCreateTodo: partial.allowCreateTodo ?? category === 'customer'
+      };
+      events.push(event);
+      projectEventCount.set(partial.projectId, currentCount + 1);
+    };
     
     try {
       // 从 projectTimelineData 中提取数据
@@ -3940,7 +4013,7 @@ export class Dashboard implements OnInit, OnDestroy {
           
           // 如果小图对图已经到期或即将到期(1天内),且不在已完成状态
           if (daysDiff <= 1 && project.currentStage !== 'delivery') {
-            events.push({
+            addEvent({
               id: `${project.projectId}-review`,
               title: `小图对图截止`,
               description: `项目「${project.projectName}」的小图对图时间已${daysDiff < 0 ? '逾期' : '临近'}`,
@@ -3950,7 +4023,9 @@ export class Dashboard implements OnInit, OnDestroy {
               projectName: project.projectName,
               designerName: project.designerName,
               urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
-              overdueDays: -daysDiff
+              overdueDays: -daysDiff,
+              labels: daysDiff < 0 ? ['逾期'] : ['临近'],
+              followUpNeeded: (project.stageName || '').includes('图') || project.status === 'warning'
             });
           }
         }
@@ -3966,7 +4041,7 @@ export class Dashboard implements OnInit, OnDestroy {
             const summary = project.spaceDeliverableSummary;
             const completionRate = summary?.overallCompletionRate || 0;
             
-            events.push({
+            addEvent({
               id: `${project.projectId}-delivery`,
               title: `项目交付截止`,
               description: `项目「${project.projectName}」需要在 ${project.deliveryDate.toLocaleDateString()} 交付(当前完成率 ${completionRate}%)`,
@@ -3977,7 +4052,8 @@ export class Dashboard implements OnInit, OnDestroy {
               designerName: project.designerName,
               urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
               overdueDays: -daysDiff,
-              completionRate
+              completionRate,
+              labels: daysDiff < 0 ? ['逾期'] : ['临近']
             });
           }
         }
@@ -4010,7 +4086,7 @@ export class Dashboard implements OnInit, OnDestroy {
                   completionRate = phaseProgress?.completionRate || 0;
                 }
                 
-                events.push({
+                addEvent({
                   id: `${project.projectId}-phase-${key}`,
                   title: `${phaseName}阶段截止`,
                   description: `项目「${project.projectName}」的${phaseName}阶段截止时间已${daysDiff < 0 ? '逾期' : '临近'}(完成率 ${completionRate}%)`,
@@ -4022,12 +4098,75 @@ export class Dashboard implements OnInit, OnDestroy {
                   designerName: project.designerName,
                   urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
                   overdueDays: -daysDiff,
-                  completionRate
+                  completionRate,
+                  labels: daysDiff < 0 ? ['逾期'] : ['临近']
                 });
               }
             }
           });
         }
+        
+        if (project.stalledDays && project.stalledDays >= 7) {
+          addEvent({
+            id: `${project.projectId}-stagnant`,
+            title: project.stalledDays >= 14 ? '客户停滞预警' : '停滞期提醒',
+            description: `项目「${project.projectName}」已有 ${project.stalledDays} 天未收到客户反馈,请主动跟进。`,
+            eventType: 'customer_alert',
+            deadline: new Date(),
+            projectId: project.projectId,
+            projectName: project.projectName,
+            designerName: project.designerName,
+            urgencyLevel: project.stalledDays >= 14 ? 'critical' : 'high',
+            statusType: 'stagnant',
+            stagnationDays: project.stalledDays,
+            labels: ['停滞期'],
+            followUpNeeded: true,
+            allowCreateTodo: true,
+            allowConfirmOnTime: false,
+            category: 'customer'
+          });
+        }
+
+        const inReviewStage = (project.stageName || '').includes('图') || (project.currentStage || '').includes('图');
+        if (inReviewStage && project.status === 'warning') {
+          addEvent({
+            id: `${project.projectId}-review-followup`,
+            title: '对图反馈待跟进',
+            description: `项目「${project.projectName}」客户反馈尚未处理,请尽快跟进。`,
+            eventType: 'customer_alert',
+            deadline: project.reviewDate || new Date(),
+            projectId: project.projectId,
+            projectName: project.projectName,
+            designerName: project.designerName,
+            urgencyLevel: 'high',
+            statusType: project.reviewDate && project.reviewDate < now ? 'overdue' : 'dueSoon',
+            labels: ['对图期'],
+            followUpNeeded: true,
+            allowCreateTodo: true,
+            customerIssueType: 'feedback_pending',
+            category: 'customer'
+          });
+        }
+
+        if (project.priority === 'critical') {
+          addEvent({
+            id: `${project.projectId}-customer-alert`,
+            title: '客户服务预警',
+            description: `项目「${project.projectName}」存在客户不满或抱怨,需要立即处理并记录。`,
+            eventType: 'customer_alert',
+            deadline: new Date(),
+            projectId: project.projectId,
+            projectName: project.projectName,
+            designerName: project.designerName,
+            urgencyLevel: 'critical',
+            statusType: 'dueSoon',
+            labels: ['客户预警'],
+            followUpNeeded: true,
+            allowCreateTodo: true,
+            customerIssueType: 'complaint',
+            category: 'customer'
+          });
+        }
       });
       
       // 按紧急程度和时间排序
@@ -4041,61 +4180,202 @@ export class Dashboard implements OnInit, OnDestroy {
         return a.deadline.getTime() - b.deadline.getTime();
       });
       
-      this.urgentEvents = events;
-      console.log(`✅ 计算紧急事件完成,共 ${events.length} 个紧急事件`);
+      // 过滤已处理和静音的事件
+      const filteredEvents = events.filter(event => !handledSet.has(event.id) && !mutedSet.has(event.id));
+      
+      // 🔥 限制最大显示数量,避免渲染过多 DOM 导致卡顿和 RangeError
+      const MAX_URGENT_EVENTS = 50;
+      this.urgentEvents = filteredEvents.slice(0, MAX_URGENT_EVENTS);
+      
+      if (filteredEvents.length > MAX_URGENT_EVENTS) {
+        console.warn(`⚠️ 紧急事件过多(${filteredEvents.length}个),已限制为前 ${MAX_URGENT_EVENTS} 个最紧急的事件`);
+      }
+      
+      // 🔥 性能关键:预计算所有标签的列表,缓存起来
+      this.precalculateTagCaches();
+      
+      // 初始化显示列表
+      this.updateFilteredUrgentEvents();
+      console.log(`✅ 计算紧急事件完成,共 ${this.urgentEvents.length} 个紧急事件(原始${events.length}个)`);
       
     } catch (error) {
       console.error('❌ 计算紧急事件失败:', error);
+      // 发生错误时清空列表,避免渲染异常数据
+      this.urgentEvents = [];
+      this.filteredUrgentEventsList = [];
     } finally {
       this.loadingUrgentEvents = false;
+      // 确保触发变更检测
+      this.cdr.markForCheck();
     }
   }
 
   /**
-   * 🆕 待办事项标签筛选
+   * 🔥 预计算所有标签的事件列表(一次计算,多次使用)
+   */
+  private precalculateTagCaches(): void {
+    const MAX_PER_TAG = 30;
+    
+    // 清空旧缓存
+    this.urgentEventsCache.clear();
+    
+    // 缓存"全部"标签
+    this.urgentEventsCache.set('all', this.urgentEvents.slice(0, MAX_PER_TAG));
+    
+    // 按类别分组并缓存
+    const categories: Array<'customer' | 'phase' | 'review' | 'delivery'> = ['customer', 'phase', 'review', 'delivery'];
+    
+    for (const category of categories) {
+      const filtered = this.urgentEvents
+        .filter(event => this.getEventCategory(event) === category)
+        .slice(0, MAX_PER_TAG);
+      this.urgentEventsCache.set(category, filtered);
+    }
+    
+    console.log(`✅ 标签缓存已预计算: ${Array.from(this.urgentEventsCache.keys()).map(k => `${k}(${this.urgentEventsCache.get(k)?.length})`).join(', ')}`);
+  }
+
+  /**
+   * 🆕 待办事项标签筛选(O(1) 操作,从缓存直接取)
    */
   filterUrgentEventsByTag(tag: 'all' | 'customer' | 'phase' | 'review' | 'delivery'): void {
     this.urgentEventTagFilter = tag;
+    
+    // 🔥 直接从缓存取出预计算的列表(O(1) 操作)
+    const cached = this.urgentEventsCache.get(tag);
+    if (cached) {
+      this.filteredUrgentEventsList = cached;
+    } else {
+      // 降级:如果缓存不存在,使用旧逻辑
+      this.updateFilteredUrgentEvents();
+    }
+    
+    // 🔥 使用 requestAnimationFrame 在浏览器下一帧更新,确保流畅
+    requestAnimationFrame(() => {
+      this.cdr.markForCheck();
+    });
+  }
+
+  confirmEventOnTime(event: UrgentEvent): void {
+    this.mutedUrgentEventIds.add(event.id);
+    // 立即从当前视图移除
+    this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id);
+    // 重新计算缓存
+    this.precalculateTagCaches();
+    this.updateFilteredUrgentEvents();
+    this.cdr.markForCheck();
+  }
+
+  resolveUrgentEvent(event: UrgentEvent): void {
+    this.handledUrgentEventIds.add(event.id);
+    // 立即从当前视图移除
+    this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id);
+    // 重新计算缓存
+    this.precalculateTagCaches();
+    this.updateFilteredUrgentEvents();
+    this.cdr.markForCheck();
+  }
+
+  markEventAsStagnant(event: UrgentEvent): void {
+    this.urgentEvents = this.urgentEvents.map(item => {
+      if (item.id !== event.id) {
+        return item;
+      }
+      const labels = new Set(item.labels || []);
+      labels.add('停滞期');
+      return {
+        ...item,
+        category: 'customer' as const,
+        statusType: 'stagnant' as const,
+        stagnationDays: item.stagnationDays || 7,
+        labels: Array.from(labels),
+        followUpNeeded: true
+      };
+    });
+    // 重新计算缓存
+    this.precalculateTagCaches();
+    this.updateFilteredUrgentEvents();
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 追踪函数:优化 ngFor 的渲染性能
+   */
+  trackUrgentEventById(index: number, event: UrgentEvent): string {
+    return event.id;
+  }
+
+  /**
+   * 从紧急事件创建待办
+   */
+  createTodoFromEvent(event: UrgentEvent): void {
+    const now = new Date();
+    const newTask: TodoTaskFromIssue = {
+      id: `urgent-todo-${event.id}-${now.getTime()}`,
+      title: `【紧急】${event.title}`,
+      description: event.description,
+      priority: event.urgencyLevel === 'critical' ? 'urgent' : event.urgencyLevel === 'high' ? 'high' : 'medium',
+      type: 'feedback',
+      status: 'open',
+      projectId: event.projectId,
+      projectName: event.projectName,
+      relatedStage: event.phaseName,
+      assigneeName: event.designerName || '待分配',
+      creatorName: this.currentUser.name,
+      createdAt: now,
+      updatedAt: now,
+      dueDate: event.deadline,
+      tags: [...(event.labels || []), '来自紧急事件']
+    };
+    this.todoTasksFromIssues = [newTask, ...this.todoTasksFromIssues];
+    this.resolveUrgentEvent(event);
   }
 
   /**
    * 获取指定标签的事件数量
    */
   getTagCount(tagId: 'customer' | 'phase' | 'review' | 'delivery'): number {
-    switch (tagId) {
-      case 'customer':
-      case 'review':
-        return this.urgentEvents.filter(event => event.eventType === 'review').length;
-      case 'phase':
-        return this.urgentEvents.filter(event => event.eventType === 'phase_deadline').length;
+    return this.urgentEvents.filter(event => this.getEventCategory(event) === tagId).length;
+  }
+
+  getEventCategory(event: UrgentEvent): 'customer' | 'phase' | 'review' | 'delivery' {
+    if (event.category) return event.category;
+    switch (event.eventType) {
+      case 'phase_deadline':
+        return 'phase';
       case 'delivery':
-        return this.urgentEvents.filter(event => event.eventType === 'delivery').length;
+        return 'delivery';
+      case 'customer_alert':
+        return 'customer';
       default:
-        return 0;
+        return 'review';
     }
   }
 
   /**
-   * 计算筛选后的紧急事件列表
+   * 更新筛选后的紧急事件列表(优先从缓存读取)
    */
-  get filteredUrgentEvents(): UrgentEvent[] {
-    if (this.urgentEventTagFilter === 'all') {
-      return this.urgentEvents;
-    }
-    
-    if (this.urgentEventTagFilter === 'customer' || this.urgentEventTagFilter === 'review') {
-      return this.urgentEvents.filter(event => event.eventType === 'review');
-    }
-    
-    if (this.urgentEventTagFilter === 'phase') {
-      return this.urgentEvents.filter(event => event.eventType === 'phase_deadline');
-    }
-    
-    if (this.urgentEventTagFilter === 'delivery') {
-      return this.urgentEvents.filter(event => event.eventType === 'delivery');
+  private updateFilteredUrgentEvents(): void {
+    try {
+      // 🔥 优先从缓存读取
+      const cached = this.urgentEventsCache.get(this.urgentEventTagFilter);
+      if (cached) {
+        this.filteredUrgentEventsList = cached;
+        return;
+      }
+      
+      // 降级:缓存不存在时的旧逻辑
+      if (this.urgentEventTagFilter === 'all') {
+        this.filteredUrgentEventsList = this.urgentEvents.slice(0, 30);
+      } else {
+        this.filteredUrgentEventsList = this.urgentEvents
+          .filter(event => this.getEventCategory(event) === this.urgentEventTagFilter)
+          .slice(0, 30);
+      }
+    } catch (error) {
+      console.error('❌ 更新筛选列表失败:', error);
+      this.filteredUrgentEventsList = [];
     }
-    
-    return this.urgentEvents;
   }
   
   /**