Browse Source

```
feat: refactor dashboard with component extraction and label updates

- Extracted dashboard alerts into standalone DashboardAlertsComponent with overdue project warnings and urgent pinned tasks display
- Extracted filter bar into standalone DashboardFilterBarComponent with search, dropdowns, and time window buttons
- Extracted project list into standalone DashboardProjectListComponent with sorting, grouping, and item rendering
- Extracted statistics cards into standalone DashboardStatsComponent with metrics

0235711 11 tháng trước cách đây
mục cha
commit
576b9f1a00
29 tập tin đã thay đổi với 4261 bổ sung và 2611 xóa
  1. 2 2
      src/app/pages/customer-service/dashboard/dashboard.html
  2. 1 1
      src/app/pages/customer-service/dashboard/dashboard.ts
  3. 37 0
      src/app/pages/team-leader/dashboard/components/dashboard-alerts/dashboard-alerts.component.html
  4. 126 0
      src/app/pages/team-leader/dashboard/components/dashboard-alerts/dashboard-alerts.component.scss
  5. 33 0
      src/app/pages/team-leader/dashboard/components/dashboard-alerts/dashboard-alerts.component.ts
  6. 96 0
      src/app/pages/team-leader/dashboard/components/dashboard-filter-bar/dashboard-filter-bar.component.html
  7. 142 0
      src/app/pages/team-leader/dashboard/components/dashboard-filter-bar/dashboard-filter-bar.component.scss
  8. 195 0
      src/app/pages/team-leader/dashboard/components/dashboard-filter-bar/dashboard-filter-bar.component.ts
  9. 46 0
      src/app/pages/team-leader/dashboard/components/dashboard-metrics/dashboard-metrics.component.html
  10. 78 0
      src/app/pages/team-leader/dashboard/components/dashboard-metrics/dashboard-metrics.component.scss
  11. 24 0
      src/app/pages/team-leader/dashboard/components/dashboard-metrics/dashboard-metrics.component.ts
  12. 69 0
      src/app/pages/team-leader/dashboard/components/project-kanban/project-kanban.component.html
  13. 299 0
      src/app/pages/team-leader/dashboard/components/project-kanban/project-kanban.component.scss
  14. 137 0
      src/app/pages/team-leader/dashboard/components/project-kanban/project-kanban.component.ts
  15. 50 0
      src/app/pages/team-leader/dashboard/components/smart-match-modal/smart-match-modal.component.html
  16. 273 0
      src/app/pages/team-leader/dashboard/components/smart-match-modal/smart-match-modal.component.scss
  17. 36 0
      src/app/pages/team-leader/dashboard/components/smart-match-modal/smart-match-modal.component.ts
  18. 349 0
      src/app/pages/team-leader/dashboard/components/todo-section/todo-section.component.html
  19. 724 0
      src/app/pages/team-leader/dashboard/components/todo-section/todo-section.component.scss
  20. 194 0
      src/app/pages/team-leader/dashboard/components/todo-section/todo-section.component.ts
  21. 19 0
      src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.html
  22. 115 0
      src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.scss
  23. 382 0
      src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.ts
  24. 76 1120
      src/app/pages/team-leader/dashboard/dashboard.html
  25. 134 0
      src/app/pages/team-leader/dashboard/dashboard.model.ts
  26. 110 1447
      src/app/pages/team-leader/dashboard/dashboard.ts
  27. 134 0
      src/app/pages/team-leader/dashboard/interfaces.ts
  28. 20 20
      src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.html
  29. 360 21
      src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.ts

+ 2 - 2
src/app/pages/customer-service/dashboard/dashboard.html

@@ -301,10 +301,10 @@
           class="tag-button"
           [class.active]="urgentEventTagFilter() === 'phase'"
           (click)="filterUrgentEventsByTag('phase')"
-          title="工作阶段"
+          title="制图阶段"
         >
           <span class="tag-icon">🔧</span>
-          <span class="tag-label">工作阶段</span>
+          <span class="tag-label">制图阶段</span>
           <span class="tag-count">{{ getTagCount('phase') }}</span>
         </button>
         

+ 1 - 1
src/app/pages/customer-service/dashboard/dashboard.ts

@@ -198,7 +198,7 @@ export class Dashboard implements OnInit, OnDestroy {
     
     const tags = [
       { id: 'customer', label: '客户服务', count: counts.customer, icon: '👥' },
-      { id: 'phase', label: '工作阶段', count: counts.phase, icon: '🔧' },
+      { id: 'phase', label: '制图阶段', count: counts.phase, icon: '🔧' },
       { id: 'review', label: '小图截止', count: counts.review, icon: '📐' },
       { id: 'delivery', label: '交付延期', count: counts.delivery, icon: '📦' }
     ];

+ 37 - 0
src/app/pages/team-leader/dashboard/components/dashboard-alerts/dashboard-alerts.component.html

@@ -0,0 +1,37 @@
+<!-- 超期项目提醒 -->
+@if (showAlert && overdueProjects.length > 0) {
+  <div class="overdue-alert">
+    <div class="alert-content">
+      <h3>⚠️ 超期项目提醒</h3>
+      <ul>
+        @for (project of overdueProjects.slice(0, 3); track $index) {
+          <li>
+            {{ project.name }} ({{ project.designerName }} 负责) - 超期{{ project.overdueDays }}天
+          </li>
+        }
+      </ul>
+      <div class="alert-actions">
+        <button (click)="viewAllOverdueProjects()" class="btn-view-all">查看全部</button>
+        <button (click)="closeAlertModal()" class="btn-close">关闭</button>
+      </div>
+    </div>
+  </div>
+}
+
+@if (urgentPinnedProjects && urgentPinnedProjects.length > 0) {
+  <div class="urgent-pinned">
+    <div class="pinned-title">紧急任务固定区(超期 + 高紧急)</div>
+    <div class="pinned-list">
+      @for (p of urgentPinnedProjects.slice(0, 3); track $index) {
+        <div class="pinned-item" (click)="onFilterByStatus('overdue')">
+          <span class="dot dot-high"></span>
+          <span class="name">{{ p.name }}</span>
+          <span class="meta">{{ p.designerName || '未分配' }} · 超期{{ p.overdueDays }}天</span>
+        </div>
+      }
+      @if (urgentPinnedProjects.length > 3) {
+        <button class="btn-view-all" (click)="viewAllOverdueProjects()">更多…</button>
+      }
+    </div>
+  </div>
+}

+ 126 - 0
src/app/pages/team-leader/dashboard/components/dashboard-alerts/dashboard-alerts.component.scss

@@ -0,0 +1,126 @@
+@use "sass:color";
+@use '../../../../../shared/styles/ios-theme' as ios;
+@use '../../../ios-theme.scss' as local;
+
+/* 超期提醒样式 */
+.overdue-alert {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background-color: local.$ios-card-background;
+  border-radius: local.$ios-radius-lg;
+  padding: local.$ios-spacing-xl;
+  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 400px;
+  animation: slideIn 0.3s ease-out;
+  
+  @keyframes slideIn {
+    from { opacity: 0; transform: translate(-50%, -60%); }
+    to { opacity: 1; transform: translate(-50%, -50%); }
+  }
+  
+  .alert-content {
+    h3 {
+      font-size: local.$ios-font-size-lg;
+      font-weight: local.$ios-font-weight-semibold;
+      color: local.$ios-danger;
+      margin: 0 0 local.$ios-spacing-lg 0;
+      display: flex;
+      align-items: center;
+      gap: local.$ios-spacing-sm;
+    }
+    
+    ul {
+      margin: 0 0 local.$ios-spacing-lg 0;
+      padding-left: local.$ios-spacing-xl;
+      
+      li {
+        font-size: local.$ios-font-size-base;
+        color: local.$ios-text-primary;
+        margin-bottom: local.$ios-spacing-sm;
+        
+        &:last-child { margin-bottom: 0; }
+      }
+    }
+    
+    .alert-actions {
+      display: flex;
+      gap: local.$ios-spacing-md;
+      justify-content: flex-end;
+      
+      .btn-view-all {
+        background-color: local.$ios-primary;
+        color: local.$ios-background;
+        border: none;
+        border-radius: local.$ios-radius-md;
+        padding: local.$ios-spacing-sm local.$ios-spacing-lg;
+        font-size: local.$ios-font-size-sm;
+        font-weight: local.$ios-font-weight-medium;
+        cursor: pointer;
+        transition: local.$ios-feedback-tap;
+        
+        &:hover { background-color: local.$ios-primary-light; }
+      }
+      
+      .btn-close {
+        background-color: local.$ios-text-tertiary;
+        color: local.$ios-text-primary;
+        border: none;
+        border-radius: local.$ios-radius-md;
+        padding: local.$ios-spacing-sm local.$ios-spacing-lg;
+        font-size: local.$ios-font-size-sm;
+        font-weight: local.$ios-font-weight-medium;
+        cursor: pointer;
+        transition: local.$ios-feedback-tap;
+        
+        &:hover { background-color: local.$ios-text-secondary; color: local.$ios-background; }
+      }
+    }
+  }
+}
+
+.urgent-pinned {
+  margin: 8px 16px 0;
+  padding: 8px 12px;
+  background: rgba(255, 241, 241, 0.8);
+  border: 1px solid #fecaca;
+  border-radius: 8px;
+  .pinned-title {
+    font-size: 12px;
+    color: #b91c1c;
+    margin-bottom: 6px;
+    font-weight: 600;
+  }
+  .pinned-list {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    flex-wrap: wrap;
+  }
+  .pinned-item {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 6px 10px;
+    border-radius: 6px;
+    background: #fff;
+    border: 1px solid #fee2e2;
+    cursor: pointer;
+    transition: box-shadow .2s ease;
+    &:hover { box-shadow: 0 1px 4px rgba(0,0,0,.1); }
+    .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
+    .dot-high { background: #ef4444; }
+    .name { font-weight: 600; color: #111827; }
+    .meta { color: #6b7280; font-size: 12px; }
+  }
+  .btn-view-all {
+    padding: 6px 10px;
+    border: none;
+    background: #ef4444;
+    color: #fff;
+    border-radius: 6px;
+    cursor: pointer;
+  }
+}

+ 33 - 0
src/app/pages/team-leader/dashboard/components/dashboard-alerts/dashboard-alerts.component.ts

@@ -0,0 +1,33 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Project } from '../../dashboard.model';
+
+@Component({
+  selector: 'app-dashboard-alerts',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './dashboard-alerts.component.html',
+  styleUrl: './dashboard-alerts.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class DashboardAlertsComponent {
+  @Input() showAlert: boolean = false;
+  @Input() overdueProjects: Project[] = [];
+  @Input() urgentPinnedProjects: Project[] = [];
+
+  @Output() viewAllOverdue = new EventEmitter<void>();
+  @Output() closeAlert = new EventEmitter<void>();
+  @Output() filterStatus = new EventEmitter<string>();
+
+  viewAllOverdueProjects() {
+    this.viewAllOverdue.emit();
+  }
+
+  closeAlertModal() {
+    this.closeAlert.emit();
+  }
+
+  onFilterByStatus(status: string) {
+    this.filterStatus.emit(status);
+  }
+}

+ 96 - 0
src/app/pages/team-leader/dashboard/components/dashboard-filter-bar/dashboard-filter-bar.component.html

@@ -0,0 +1,96 @@
+<div class="section-filters">
+  <div class="search-box">
+    <input 
+      type="search" 
+      class="input-search" 
+      placeholder="搜索项目/设计师/风格关键词" 
+      [(ngModel)]="searchTerm" 
+      (input)="onSearchChange()" 
+      (focus)="onSearchFocus()" 
+      (blur)="onSearchBlur()" 
+    />
+    @if (showSuggestions) {
+      <div class="suggestion-panel">
+        @if (searchSuggestions.length > 0) {
+          <ul>
+            @for (suggest of searchSuggestions; track suggest.id) {
+              <li (mousedown)="selectSuggestion(suggest)">
+                <div class="line-1">
+                  <span class="name">{{ suggest.name }}</span>
+                  <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
+                  <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
+                </div>
+                <div class="line-2">
+                  <span class="designer">{{ suggest.designerName || '未分配' }}</span>
+                  <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
+                </div>
+              </li>
+            }
+          </ul>
+        } @else {
+          <div class="empty">抱歉,没有检索到哦</div>
+        }
+      </div>
+    }
+  </div>
+
+  <select [(ngModel)]="selectedType" (change)="onFilterChange()" class="custom-select">
+    <option value="all">全部项目</option>
+    <option value="soft">软装项目</option>
+    <option value="hard">硬装项目</option>
+  </select>
+
+  <select [(ngModel)]="selectedUrgency" (change)="onFilterChange()" class="custom-select">
+    <option value="all">全部紧急程度</option>
+    <option value="high">高</option>
+    <option value="medium">中</option>
+    <option value="low">低</option>
+  </select>
+
+  <select [(ngModel)]="selectedStatus" (change)="onFilterChange()" class="custom-select">
+    <option value="all">全部状态</option>
+    <option value="progress">进行中</option>
+    <option value="completed">已完成</option>
+    <option value="overdue">已延期</option>
+    <option value="dueSoon">临期(3天内)</option>
+    <option value="pendingApproval">待确认</option>
+    <option value="pendingAssignment">待分配</option>
+  </select>
+
+  <select [(ngModel)]="selectedDesigner" (change)="onFilterChange()" class="custom-select">
+    <option value="all">全部设计师</option>
+    @for (d of designers; track d) {
+      <option [value]="d">{{ d }}</option>
+    }
+  </select>
+
+  <select [(ngModel)]="selectedMemberType" (change)="onFilterChange()" class="custom-select">
+    <option value="all">全部会员</option>
+    <option value="vip">VIP会员</option>
+    <option value="normal">普通会员</option>
+  </select>
+
+  <!-- 四大板块筛选 -->
+  <select [(ngModel)]="selectedCorePhase" (change)="onFilterChange()" class="custom-select">
+    <option value="all">全部板块</option>
+    @for (core of corePhases; track core.id) {
+      <option [value]="core.id">{{ core.name }}</option>
+    }
+  </select>
+
+  <!-- 支持数百项目的下拉筛选 -->
+  <select [(ngModel)]="selectedProjectId" (change)="onProjectSelect()" class="custom-select project-selector">
+    <option value="">选择项目</option>
+    @for (project of projects; track project.id) {
+      <option [value]="project.id">{{ project.name }}</option>
+    }
+  </select>
+
+  <!-- 时间窗快捷筛选按钮组 -->
+  <div class="time-window-buttons">
+    <button [class.active]="isTimeWindowActive('all')" (click)="filterByTimeWindow('all')">全部</button>
+    <button [class.active]="isTimeWindowActive('today')" (click)="filterByTimeWindow('today')">今天到期</button>
+    <button [class.active]="isTimeWindowActive('threeDays')" (click)="filterByTimeWindow('threeDays')">3天内</button>
+    <button [class.active]="isTimeWindowActive('sevenDays')" (click)="filterByTimeWindow('sevenDays')">7天内</button>
+  </div>
+</div>

+ 142 - 0
src/app/pages/team-leader/dashboard/components/dashboard-filter-bar/dashboard-filter-bar.component.scss

@@ -0,0 +1,142 @@
+@use "sass:color";
+@use '../../../../../shared/styles/ios-theme' as ios;
+@use '../../../ios-theme.scss' as local;
+
+.section-filters {
+  display: flex;
+  gap: local.$ios-spacing-md;
+  flex-wrap: wrap;
+  align-items: center;
+  overflow: visible;
+  
+  .custom-select {
+    appearance: none;
+    padding: local.$ios-spacing-sm local.$ios-spacing-md;
+    border: 1px solid local.$ios-border;
+    border-radius: local.$ios-radius-md;
+    background-color: local.$ios-background;
+    font-size: local.$ios-font-size-sm;
+    color: local.$ios-text-primary;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    min-width: 140px;
+    height: 36px; /* Explicit height alignment */
+    line-height: 1.2;
+    
+    /* Add chevron icon for select */
+    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
+    background-position: right 0.5rem center;
+    background-repeat: no-repeat;
+    background-size: 1.5em 1.5em;
+    padding-right: 2.5rem;
+
+    &:hover {
+      border-color: ios.$ios-primary;
+    }
+    
+    &:focus {
+      outline: none;
+      border-color: ios.$ios-primary;
+      box-shadow: 0 0 0 2px rgba(ios.$ios-primary, 0.1);
+    }
+  }
+
+  .search-box {
+    margin-right: 8px;
+    position: relative; 
+    display: flex;
+    align-items: center;
+
+    .input-search {
+      appearance: none;
+      width: 260px;
+      padding: 8px 12px;
+      height: 36px; /* Match select height */
+      border: 1px solid #e5e7eb;
+      border-radius: 8px;
+      font-size: 14px;
+      outline: none;
+      transition: border-color .2s ease, box-shadow .2s ease;
+      &:focus {
+        border-color: #3b82f6;
+        box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
+      }
+    }
+
+    .suggestion-panel {
+      position: absolute;
+      top: calc(100% + 6px);
+      right: 0;
+      min-width: 260px;
+      width: max(100%, 360px);
+      max-width: 520px;
+      background: #fff;
+      border: 1px solid #e5e7eb;
+      border-radius: 10px;
+      box-shadow: 0 12px 28px rgba(0,0,0,.12), 0 2px 8px rgba(0,0,0,.06);
+      z-index: 20;
+      padding: 6px;
+
+      ul { list-style: none; margin: 0; padding: 0; max-height: 320px; overflow-y: auto; }
+      li {
+        padding: 8px 10px;
+        border-radius: 8px;
+        cursor: pointer;
+        transition: background .15s ease;
+        display: flex;
+        flex-direction: column;
+        gap: 4px;
+        &:hover { background: #f3f4f6; }
+      }
+
+      .line-1 { display: flex; align-items: center; gap: 8px; 
+        .name { font-weight: 600; color: #111827; flex: 1; min-width: 0; }
+        .badge { font-size: 12px; padding: 2px 6px; border-radius: 999px; background: #eef2ff; color: #4f46e5; 
+            &.vip { background: #ede9fe; color: #7c3aed; }
+        }
+        .urgency { font-size: 12px; 
+            &.u-high { color: #dc2626; }
+            &.u-medium { color: #f59e0b; }
+            &.u-low { color: #22c55e; }
+        }
+      }
+      .line-2 { display: flex; align-items: center; justify-content: space-between; color: #6b7280; font-size: 12px; }
+      .empty { padding: 10px 12px; color: #6b7280; font-size: 13px; }
+    }
+  }
+
+  .time-window-buttons {
+    display: inline-flex;
+    gap: 8px;
+    /* margin-left: 8px; Removed to rely on gap */
+    button {
+        height: 36px;
+        display: flex;
+        align-items: center;
+        padding: 0 12px;
+        border: 1px solid #d0d7de;
+        border-radius: 6px;
+        background: #fff;
+        color: #24292f;
+        cursor: pointer;
+        transition: all .15s ease;
+        &:hover { background: #f6f8fa; }
+        &.active { background: #0969da; color: #fff; border-color: #0969da; }
+    }
+  }
+}
+
+// Media Queries
+@media (max-width: 1024px) {
+  .section-filters {
+    .search-box { width: 100%; }
+    .input-search { width: min(100%, 520px); }
+  }
+}
+
+@media (max-width: 640px) {
+  .section-filters {
+    gap: 6px;
+    .input-search { width: 100%; }
+  }
+}

+ 195 - 0
src/app/pages/team-leader/dashboard/components/dashboard-filter-bar/dashboard-filter-bar.component.ts

@@ -0,0 +1,195 @@
+import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Project } from '../../dashboard.model';
+
+export interface FilterState {
+  searchTerm: string;
+  type: 'all' | 'soft' | 'hard';
+  urgency: 'all' | 'high' | 'medium' | 'low';
+  status: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon';
+  designer: string;
+  memberType: 'all' | 'vip' | 'normal';
+  corePhase: 'all' | 'order' | 'requirements' | 'delivery' | 'aftercare';
+  projectId: string;
+  timeWindow: 'all' | 'today' | 'threeDays' | 'sevenDays';
+}
+
+@Component({
+  selector: 'app-dashboard-filter-bar',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './dashboard-filter-bar.component.html',
+  styleUrls: ['./dashboard-filter-bar.component.scss']
+})
+export class DashboardFilterBarComponent implements OnChanges {
+  @Input() projects: Project[] = [];
+  @Input() designers: string[] = [];
+  @Input() corePhases: any[] = [];
+  
+  @Output() filterChange = new EventEmitter<FilterState>();
+  @Output() viewProject = new EventEmitter<string>(); // Emits projectId
+
+  // Filter States (Two-way binding)
+  @Input() searchTerm: string = '';
+  @Output() searchTermChange = new EventEmitter<string>();
+
+  @Input() selectedType: 'all' | 'soft' | 'hard' = 'all';
+  @Output() selectedTypeChange = new EventEmitter<'all' | 'soft' | 'hard'>();
+
+  @Input() selectedUrgency: 'all' | 'high' | 'medium' | 'low' = 'all';
+  @Output() selectedUrgencyChange = new EventEmitter<'all' | 'high' | 'medium' | 'low'>();
+
+  @Input() selectedStatus: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon' = 'all';
+  @Output() selectedStatusChange = new EventEmitter<string>();
+
+  @Input() selectedDesigner: string = 'all';
+  @Output() selectedDesignerChange = new EventEmitter<string>();
+
+  @Input() selectedMemberType: 'all' | 'vip' | 'normal' = 'all';
+  @Output() selectedMemberTypeChange = new EventEmitter<'all' | 'vip' | 'normal'>();
+
+  @Input() selectedCorePhase: 'all' | 'order' | 'requirements' | 'delivery' | 'aftercare' = 'all';
+  @Output() selectedCorePhaseChange = new EventEmitter<string>();
+
+  @Input() selectedProjectId: string = '';
+  @Output() selectedProjectIdChange = new EventEmitter<string>();
+
+  @Input() selectedTimeWindow: 'all' | 'today' | 'threeDays' | 'sevenDays' = 'all';
+  @Output() selectedTimeWindowChange = new EventEmitter<'all' | 'today' | 'threeDays' | 'sevenDays'>();
+
+  // Search Suggestions State
+  searchSuggestions: Project[] = [];
+  showSuggestions: boolean = false;
+  isSearchFocused: boolean = false;
+  
+  private searchDebounceTimer: any;
+  private hideSuggestionsTimer: any;
+  private readonly SEARCH_DEBOUNCE_MS = 200;
+  private readonly MIN_SEARCH_LEN = 2;
+  private readonly MAX_SUGGESTIONS = 8;
+
+  ngOnChanges(changes: SimpleChanges): void {
+    // If projects change, we might need to re-run search suggestions if search is active
+    if (changes['projects'] && this.searchTerm) {
+      this.updateSearchSuggestions();
+    }
+  }
+
+  // Search Handling
+  onSearchChange(): void {
+    this.searchTermChange.emit(this.searchTerm); // Emit change immediately for binding
+    
+    if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer);
+    this.searchDebounceTimer = setTimeout(() => {
+      this.updateSearchSuggestions();
+      this.emitFilterChange();
+    }, this.SEARCH_DEBOUNCE_MS);
+  }
+
+  onSearchFocus(): void {
+    if (this.hideSuggestionsTimer) clearTimeout(this.hideSuggestionsTimer);
+    this.isSearchFocused = true;
+    this.updateSearchSuggestions();
+  }
+
+  onSearchBlur(): void {
+    this.isSearchFocused = false;
+    this.hideSuggestionsTimer = setTimeout(() => {
+      this.showSuggestions = false;
+    }, 150);
+  }
+
+  private updateSearchSuggestions(): void {
+    const q = (this.searchTerm || '').trim().toLowerCase();
+    if (q.length < this.MIN_SEARCH_LEN) {
+      this.searchSuggestions = [];
+      this.showSuggestions = false;
+      return;
+    }
+
+    const scored = this.projects
+      .filter(p => (p.searchIndex || `${(p.name || '')}|${(p.designerName || '')}`.toLowerCase()).includes(q))
+      .map(p => {
+        const dl = p.deadline || p.expectedEndDate;
+        const dlTime = dl ? new Date(dl).getTime() : NaN;
+        const daysToDl = Math.ceil(((isNaN(dlTime) ? 0 : dlTime) - Date.now()) / (1000 * 60 * 60 * 24));
+        const urgencyScore = p.urgency === 'high' ? 3 : p.urgency === 'medium' ? 2 : 1;
+        const overdueScore = p.isOverdue ? 10 : 0;
+        const score = overdueScore + (4 - urgencyScore) * 2 - (isNaN(daysToDl) ? 0 : daysToDl);
+        return { p, score };
+      })
+      .sort((a, b) => b.score - a.score)
+      .slice(0, this.MAX_SUGGESTIONS)
+      .map(x => x.p);
+
+    this.searchSuggestions = scored;
+    this.showSuggestions = this.isSearchFocused && this.searchSuggestions.length > 0;
+  }
+
+  selectSuggestion(project: Project): void {
+    this.searchTerm = project.name;
+    this.searchTermChange.emit(this.searchTerm);
+    this.showSuggestions = false;
+    this.viewProject.emit(project.id);
+  }
+
+  getUrgencyLabel(urgency: string): string {
+    const labels: Record<string, string> = {
+      'high': '高',
+      'medium': '中',
+      'low': '低'
+    };
+    return labels[urgency] || '未知';
+  }
+
+  onProjectSelect(): void {
+    if (this.selectedProjectId) {
+      this.viewProject.emit(this.selectedProjectId);
+      // Reset selection so the same project can be selected again if needed, 
+      // or just to keep the "Select Project" placeholder active.
+      this.selectedProjectId = '';
+      this.selectedProjectIdChange.emit(this.selectedProjectId);
+    }
+  }
+
+  // Filter Changes
+  onFilterChange(): void {
+    // Emit property changes
+    this.selectedTypeChange.emit(this.selectedType);
+    this.selectedUrgencyChange.emit(this.selectedUrgency);
+    this.selectedStatusChange.emit(this.selectedStatus);
+    this.selectedDesignerChange.emit(this.selectedDesigner);
+    this.selectedMemberTypeChange.emit(this.selectedMemberType);
+    this.selectedCorePhaseChange.emit(this.selectedCorePhase as string);
+    this.selectedProjectIdChange.emit(this.selectedProjectId);
+    
+    this.emitFilterChange();
+  }
+
+  filterByTimeWindow(timeWindow: 'all' | 'today' | 'threeDays' | 'sevenDays'): void {
+    this.selectedTimeWindow = timeWindow;
+    this.selectedTimeWindowChange.emit(this.selectedTimeWindow);
+    this.emitFilterChange();
+  }
+  
+  // Main Emit Method
+  private emitFilterChange(): void {
+    this.filterChange.emit({
+      searchTerm: this.searchTerm,
+      type: this.selectedType,
+      urgency: this.selectedUrgency,
+      status: this.selectedStatus,
+      designer: this.selectedDesigner,
+      memberType: this.selectedMemberType,
+      corePhase: this.selectedCorePhase,
+      projectId: this.selectedProjectId,
+      timeWindow: this.selectedTimeWindow
+    });
+  }
+  
+  // Helper for template to check active status
+  isTimeWindowActive(window: string): boolean {
+    return this.selectedTimeWindow === window;
+  }
+}

+ 46 - 0
src/app/pages/team-leader/dashboard/components/dashboard-metrics/dashboard-metrics.component.html

@@ -0,0 +1,46 @@
+<div class="dashboard-metrics">
+  <div class="metric-card" (click)="onFilterStatus('overdue')">
+    <div class="metric-icon warning">⚠️</div>
+    <div class="metric-content">
+      <div class="metric-count">{{ overdueCount }}</div>
+      <div class="metric-label">已延期项目</div>
+    </div>
+  </div>
+  <div class="metric-card" (click)="onFilterStatus('dueSoon')">
+    <div class="metric-icon info">⏳</div>
+    <div class="metric-content">
+      <div class="metric-count">{{ dueSoonCount }}</div>
+      <div class="metric-label">临期项目(3天内)</div>
+    </div>
+  </div>
+  <div class="metric-card" (click)="onFilterStatus('pendingApproval')">
+    <div class="metric-icon info">📋</div>
+    <div class="metric-content">
+      <div class="metric-count">{{ pendingApprovalCount }}</div>
+      <div class="metric-label">待组长确认项目</div>
+    </div>
+  </div>
+  <div class="metric-card" (click)="onFilterStatus('pendingAssignment')">
+    <div class="metric-icon primary">🎯</div>
+    <div class="metric-content">
+      <div class="metric-count">{{ pendingAssignmentCount }}</div>
+      <div class="metric-label">待分配方案项目</div>
+    </div>
+  </div>
+  <!-- 新增:超负荷设计师数量 -->
+  <div class="metric-card">
+    <div class="metric-icon danger">🔥</div>
+    <div class="metric-content">
+      <div class="metric-count">{{ overloadedDesignersCount }}</div>
+      <div class="metric-label">超负荷设计师</div>
+    </div>
+  </div>
+  <!-- 新增:平均负载率 -->
+  <div class="metric-card">
+    <div class="metric-icon success">📊</div>
+    <div class="metric-content">
+      <div class="metric-count">{{ averageWorkloadRate.toFixed(0) }}%</div>
+      <div class="metric-label">平均负载率</div>
+    </div>
+  </div>
+</div>

+ 78 - 0
src/app/pages/team-leader/dashboard/components/dashboard-metrics/dashboard-metrics.component.scss

@@ -0,0 +1,78 @@
+@use "sass:color";
+
+// iOS主题变量
+$ios-spacing-md: 12px;
+$ios-spacing-lg: 16px;
+$ios-radius-lg: 12px;
+$ios-radius-full: 9999px;
+$ios-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+$ios-shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+$ios-font-weight-bold: 700;
+$ios-font-size-sm: 14px;
+$ios-text-primary: #1f2937;
+$ios-text-secondary: #6b7280;
+$ios-card-background: #ffffff;
+$ios-background: #f9fafb;
+$ios-border: #e5e7eb;
+
+:host {
+  display: block;
+}
+
+.dashboard-metrics {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+  gap: $ios-spacing-lg;
+  margin-top: $ios-spacing-lg;
+  
+  .metric-card {
+    display: flex;
+    align-items: center;
+    gap: $ios-spacing-md;
+    background: linear-gradient(135deg, $ios-card-background, #f8f9fa);
+    border-radius: $ios-radius-lg;
+    padding: $ios-spacing-lg;
+    border: 1px solid $ios-border;
+    box-shadow: $ios-shadow-sm;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    
+    &:hover {
+      transform: translateY(-2px);
+      box-shadow: $ios-shadow-card;
+    }
+    
+    .metric-icon {
+      font-size: 2rem;
+      width: 50px;
+      height: 50px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: $ios-radius-full;
+      background-color: $ios-background;
+    }
+    
+    .metric-icon.warning { background-color: rgba(255, 149, 0, 0.1); }
+    .metric-icon.info { background-color: rgba(59, 130, 246, 0.1); }
+    .metric-icon.primary { background-color: rgba(124, 58, 237, 0.1); }
+    .metric-icon.danger { background-color: rgba(239, 68, 68, 0.1); }
+    .metric-icon.success { background-color: rgba(16, 185, 129, 0.1); }
+    
+    .metric-content { flex: 1; }
+    
+    .metric-count {
+      font-size: 2rem;
+      font-weight: $ios-font-weight-bold;
+      color: $ios-text-primary;
+      line-height: 1.2;
+    }
+    
+    .metric-label {
+      font-size: $ios-font-size-sm;
+      color: $ios-text-secondary;
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+    }
+  }
+}

+ 24 - 0
src/app/pages/team-leader/dashboard/components/dashboard-metrics/dashboard-metrics.component.ts

@@ -0,0 +1,24 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+@Component({
+  selector: 'app-dashboard-metrics',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './dashboard-metrics.component.html',
+  styleUrls: ['./dashboard-metrics.component.scss']
+})
+export class DashboardMetricsComponent {
+  @Input() overdueCount: number = 0;
+  @Input() dueSoonCount: number = 0;
+  @Input() pendingApprovalCount: number = 0;
+  @Input() pendingAssignmentCount: number = 0;
+  @Input() overloadedDesignersCount: number = 0;
+  @Input() averageWorkloadRate: number = 0;
+
+  @Output() filterStatus = new EventEmitter<string>();
+
+  onFilterStatus(status: string): void {
+    this.filterStatus.emit(status);
+  }
+}

+ 69 - 0
src/app/pages/team-leader/dashboard/components/project-kanban/project-kanban.component.html

@@ -0,0 +1,69 @@
+<div class="project-kanban">
+  <!-- 新增:公共横向滚动容器,保证表头与表体同步滚动 -->
+  <div class="kanban-scroll">
+    <!-- 阶段标题 -->
+    <div class="kanban-header">
+      @for (core of corePhases; track core.id) {
+        <div class="kanban-column-header">
+          <h3>{{ core.name }}</h3>
+          <span class="stage-count">{{ getProjectCountByCorePhase(core.id) }}</span>
+        </div>
+      }
+    </div>
+    <!-- 项目卡片 -->
+    <div class="kanban-body">
+      @for (core of corePhases; track core.id) {
+        <div class="kanban-column">
+          @for (project of getProjectsByCorePhase(core.id); track project.id) {
+            <div class="project-card" 
+                 (click)="onViewProject(project.id, core.id)"
+                 [class.overdue]="project.isOverdue" 
+                 [class.high-urgency]="project.urgency === 'high'"
+                 [class.due-soon]="project.dueSoon && !project.isOverdue"
+                 [class.pending-approval]="isPendingApproval(project)">
+              <!-- 待审批徽章 -->
+              @if (isPendingApproval(project)) {
+                <div class="approval-badge">
+                  <span class="badge-icon">📋</span>
+                  <span class="badge-text">待审批</span>
+                </div>
+              }
+              <div class="project-card-header">
+                <h4 (click)="onViewProject(project.id, core.id, $event)" style="cursor: pointer;">{{ project.name }}</h4>
+                <div class="right-badges">
+                  <span class="member-badge" [class.vip]="project.memberType === 'vip'">{{ project.memberType === 'vip' ? 'VIP' : '普通' }}</span>
+                  <span class="project-urgency" [class]="'urgency-' + project.urgency">{{ getUrgencyLabel(project.urgency) }}</span>
+                </div>
+              </div>
+              <div class="project-card-content">
+                <p>负责人: {{ project.designerName || '未分配' }}</p>
+                <p class="deadline">{{ project.isOverdue ? '超期' + project.overdueDays + '天' : (project.dueSoon ? '临期: ' + (project.deadline | date:'MM-dd') : '截止: ' + (project.deadline | date:'MM-dd')) }}</p>
+              </div>
+              <div class="project-card-footer">
+                <button (click)="onViewProject(project.id, core.id, $event)" class="btn-view">查看详情</button>
+                @if (project.currentStage === 'pendingAssignment') {
+                  <button (click)="onOpenSmartMatch(project, $event)" class="btn-smart">🤖 智能推荐</button>
+                  <button (click)="onAssignProject(project.id, $event)" class="btn-assign">手动分配</button>
+                }
+                <!-- 新增:质量评审快捷操作 -->
+                @if (project.currentStage === 'review' || project.currentStage === 'delivery') {
+                  <div class="inline-actions">
+                    <button class="btn-secondary" (click)="onReviewProject(project.id, 'excellent', $event)">评为优秀</button>
+                    <button class="btn-secondary" (click)="onReviewProject(project.id, 'qualified', $event)">评为合格</button>
+                    <button class="btn-secondary" (click)="onReviewProject(project.id, 'unqualified', $event)">评为不合格</button>
+                  </div>
+                }
+              </div>
+            </div>
+          }
+          @if (getProjectsByCorePhase(core.id).length === 0) {
+            <div class="empty-column">
+              <span class="empty-icon">📦</span>
+              <p>暂无项目</p>
+            </div>
+          }
+        </div>
+      }
+    </div>
+  </div>
+</div>

+ 299 - 0
src/app/pages/team-leader/dashboard/components/project-kanban/project-kanban.component.scss

@@ -0,0 +1,299 @@
+@use '../../../ios-theme.scss' as local;
+
+:host {
+  display: block;
+}
+
+.inline-actions {
+  display: inline-flex;
+  gap: 6px;
+  margin-left: 8px;
+  .btn-secondary {
+    padding: 4px 8px;
+    font-size: 12px;
+    border: 1px solid #d0d7de;
+    border-radius: 6px;
+    background: #fff;
+    color: #24292f;
+    cursor: pointer;
+    &:hover { background: #f3f4f6; }
+  }
+}
+
+// 待审批项目卡片样式
+.project-card {
+  &.pending-approval {
+    border: 2px solid #ff9800 !important;
+    box-shadow: 0 0 10px rgba(255, 152, 0, 0.3);
+    position: relative;
+    animation: pulse 2s ease-in-out infinite;
+    
+    .approval-badge {
+      position: absolute;
+      top: 10px;
+      right: 10px;
+      background: linear-gradient(135deg, #ff9800, #ff6b00);
+      color: white;
+      padding: 4px 12px;
+      border-radius: 20px;
+      font-size: 12px;
+      font-weight: bold;
+      display: flex;
+      align-items: center;
+      gap: 4px;
+      box-shadow: 0 2px 8px rgba(255, 152, 0, 0.4);
+      z-index: 10;
+      
+      .badge-icon {
+        font-size: 14px;
+      }
+    }
+  }
+}
+
+@keyframes pulse {
+  0%, 100% {
+    transform: scale(1);
+  }
+  50% {
+    transform: scale(1.02);
+  }
+}
+
+// 项目卡片与看板样式
+.project-kanban {
+  position: relative;
+  z-index: 1;
+  
+  // 公共横向滚动容器
+  .kanban-scroll {
+    overflow-x: auto;
+    padding-bottom: local.$ios-spacing-md;
+    -webkit-overflow-scrolling: touch;
+    width: 100%; // Ensure full width
+
+    &::-webkit-scrollbar { height: 6px; }
+    &::-webkit-scrollbar-track {
+      background: local.$ios-background;
+      border-radius: local.$ios-radius-full;
+    }
+    &::-webkit-scrollbar-thumb {
+      background: local.$ios-border;
+      border-radius: local.$ios-radius-full;
+    }
+    &::-webkit-scrollbar-thumb:hover { background: local.$ios-text-tertiary; }
+
+    .kanban-header, .kanban-body { 
+      width: 100%; 
+      min-width: 800px; // Ensure a minimum width for the entire board to prevent squashing on small screens
+    }
+  }
+  
+  // 看板标题栏
+  .kanban-header {
+    position: sticky;
+    top: 0;
+    z-index: 2;
+    background: local.$ios-card-background;
+    border-bottom: 1px solid local.$ios-border;
+    display: flex;
+    gap: local.$ios-spacing-md;
+    margin-bottom: local.$ios-spacing-md;
+    
+    .kanban-column-header {
+      flex: 1; // Allow to grow and shrink
+      // Removed fixed min/max width
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: local.$ios-spacing-sm local.$ios-spacing-md;
+      background-color: local.$ios-background;
+      border-radius: local.$ios-radius-md;
+      border: 1px solid local.$ios-border;
+      
+      h3 {
+        font-size: local.$ios-font-size-sm;
+        font-weight: local.$ios-font-weight-medium;
+        color: local.$ios-text-primary;
+        margin: 0;
+      }
+      
+      .stage-count {
+        font-size: local.$ios-font-size-xs;
+        background-color: local.$ios-primary;
+        color: local.$ios-background;
+        padding: 2px 8px;
+        border-radius: local.$ios-radius-full;
+        font-weight: local.$ios-font-weight-medium;
+      }
+    }
+  }
+  
+  // 看板主体内容
+  .kanban-body {
+    display: flex;
+    gap: local.$ios-spacing-md;
+    
+    .kanban-column {
+      flex: 1; // Allow to grow and shrink
+      // Removed fixed min/max width
+      height: 500px; // Increased height slightly for better visibility
+      background-color: local.$ios-background;
+      border-radius: local.$ios-radius-md;
+      border: 1px solid local.$ios-border;
+      padding: local.$ios-spacing-sm;
+      overflow-y: auto;
+      -webkit-overflow-scrolling: touch;
+      
+      &::-webkit-scrollbar { width: 4px; }
+      &::-webkit-scrollbar-track { background: transparent; }
+      &::-webkit-scrollbar-thumb {
+        background: local.$ios-border;
+        border-radius: local.$ios-radius-full;
+      }
+      
+      .project-card {
+        background-color: local.$ios-card-background;
+        border-radius: local.$ios-radius-md;
+        padding: local.$ios-spacing-md;
+        margin-bottom: local.$ios-spacing-sm;
+        border: 1px solid local.$ios-border;
+        box-shadow: local.$ios-shadow-sm;
+        transition: all 0.2s ease;
+        cursor: pointer;
+        user-select: none;
+        
+        &:hover {
+          transform: translateY(-2px);
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+        }
+        
+        &:active { transform: translateY(-1px); opacity: 0.98; }
+        &.overdue { border-left: 4px solid local.$ios-danger; }
+        &.high-urgency { border-left: 4px solid local.$ios-warning; }
+        &.due-soon { border-left: 4px solid local.$ios-warning; }
+        
+        .project-card-header {
+          display: flex;
+          justify-content: space-between;
+          align-items: flex-start;
+          margin-bottom: local.$ios-spacing-sm;
+          
+          h4 {
+            font-size: local.$ios-font-size-sm;
+            font-weight: local.$ios-font-weight-medium;
+            color: local.$ios-primary;
+            margin: 0;
+            cursor: pointer;
+            &:hover { text-decoration: underline; }
+          }
+          
+          .project-urgency {
+            font-size: 10px;
+            padding: 2px 6px;
+            border-radius: local.$ios-radius-full;
+            font-weight: local.$ios-font-weight-medium;
+          }
+          
+          .urgency-high { background-color: rgba(239, 68, 68, 0.1); color: local.$ios-danger; }
+          .urgency-medium { background-color: rgba(255, 149, 0, 0.1); color: local.$ios-warning; }
+          .urgency-low { background-color: rgba(59, 130, 246, 0.1); color: local.$ios-info; }
+        }
+        
+        .project-card-content {
+          margin-bottom: local.$ios-spacing-sm;
+          
+          p {
+            font-size: 11px;
+            color: local.$ios-text-secondary;
+            margin: 0 0 4px 0;
+          }
+          
+          .deadline { font-size: 10px; color: local.$ios-text-tertiary; }
+        }
+        
+        .project-card-footer {
+          display: flex;
+          gap: 4px;
+          
+          button {
+            flex: 1;
+            font-size: 10px;
+            padding: 4px 6px;
+            border: none;
+            border-radius: local.$ios-radius-sm;
+            cursor: pointer;
+            transition: all 0.2s ease;
+          }
+          
+          .btn-view { background-color: local.$ios-primary; color: local.$ios-background; }
+          .btn-assign { background-color: local.$ios-success; color: local.$ios-background; }
+          button:hover { opacity: 0.9; }
+        }
+        
+        .right-badges {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+        }
+        .member-badge {
+          font-size: 10px;
+          padding: 2px 6px;
+          border-radius: local.$ios-radius-full;
+          background-color: rgba(59, 130, 246, 0.08);
+          color: local.$ios-info;
+          &.vip {
+            background-color: rgba(124, 58, 237, 0.12);
+            color: local.$ios-primary;
+            font-weight: local.$ios-font-weight-semibold;
+          }
+        }
+      }
+      
+      .empty-column {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        height: 100%;
+        
+        .empty-icon {
+          font-size: 2rem;
+          margin-bottom: local.$ios-spacing-sm;
+          opacity: 0.3;
+        }
+        
+        p {
+          font-size: local.$ios-font-size-sm;
+          color: local.$ios-text-tertiary;
+          margin: 0;
+        }
+      }
+    }
+  }
+}
+
+// 极窄屏样式优化:减小列间距与列宽,保证可视密度
+@media (max-width: 640px) {
+  .project-kanban {
+    .kanban-header { gap: local.$ios-spacing-sm; }
+    .kanban-body {
+      gap: local.$ios-spacing-sm;
+      .kanban-column {
+        min-width: 160px;
+        max-width: 160px;
+      }
+    }
+  }
+}
+
+@media (max-width: 480px) {
+  .project-kanban {
+    .kanban-body {
+      .kanban-column {
+        min-width: 150px;
+        max-width: 150px;
+      }
+    }
+  }
+}

+ 137 - 0
src/app/pages/team-leader/dashboard/components/project-kanban/project-kanban.component.ts

@@ -0,0 +1,137 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Project } from '../../interfaces';
+
+@Component({
+  selector: 'app-project-kanban',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './project-kanban.component.html',
+  styleUrls: ['./project-kanban.component.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class ProjectKanbanComponent {
+  @Input() corePhases: any[] = [];
+  @Input() projects: Project[] = [];
+  
+  // Outputs for actions that need to be handled by the parent
+  @Output() viewProject = new EventEmitter<{projectId: string, phaseId: string}>();
+  @Output() openSmartMatch = new EventEmitter<Project>();
+  @Output() assignProject = new EventEmitter<string>(); // projectId
+  @Output() reviewProject = new EventEmitter<{projectId: string, rating: 'excellent' | 'qualified' | 'unqualified'}>();
+
+  getProjectCountByCorePhase(coreId: string): number {
+    return this.getProjectsByCorePhase(coreId).length;
+  }
+
+  getProjectsByCorePhase(coreId: string): Project[] {
+    if (!this.projects) return [];
+    
+    return this.projects.filter(p => this.mapStageToCorePhase(p.currentStage) === coreId);
+  }
+
+  private mapStageToCorePhase(stageId: string): 'order' | 'requirements' | 'delivery' | 'aftercare' {
+    if (!stageId) return 'order';
+    
+    const normalizedStage = stageId.trim().toLowerCase();
+    
+    // 1. 订单分配阶段
+    if (normalizedStage === 'order' || 
+        normalizedStage === 'pendingapproval' || 
+        normalizedStage === 'pendingassignment' ||
+        normalizedStage === '订单分配' ||
+        normalizedStage === '待审批' ||
+        normalizedStage === '待分配') {
+      return 'order';
+    }
+    
+    // 2. 确认需求阶段
+    if (normalizedStage === 'requirements' ||
+        normalizedStage === 'requirement' || 
+        normalizedStage === 'planning' ||
+        normalizedStage === '确认需求' ||
+        normalizedStage === '需求沟通' ||
+        normalizedStage === '方案规划') {
+      return 'requirements';
+    }
+    
+    // 3. 交付执行阶段
+    if (normalizedStage === 'delivery' ||
+        normalizedStage === 'modeling' || 
+        normalizedStage === 'rendering' || 
+        normalizedStage === 'postproduction' || 
+        normalizedStage === 'review' || 
+        normalizedStage === 'revision' ||
+        normalizedStage === '交付执行' ||
+        normalizedStage === '建模' ||
+        normalizedStage === '建模阶段' ||
+        normalizedStage === '渲染' ||
+        normalizedStage === '渲染阶段' ||
+        normalizedStage === '后期制作' ||
+        normalizedStage === '评审' ||
+        normalizedStage === '修改' ||
+        normalizedStage === '修订' ||
+        normalizedStage === '白模' ||
+        normalizedStage === '软装' ||
+        normalizedStage === '后期') {
+      return 'delivery';
+    }
+    
+    // 4. 售后归档阶段
+    if (normalizedStage === 'aftercare' ||
+        normalizedStage === 'completed' ||
+        normalizedStage === 'archived' ||
+        normalizedStage === '售后归档' ||
+        normalizedStage === '售后' ||
+        normalizedStage === '归档' ||
+        normalizedStage === '已完成' ||
+        normalizedStage === '已交付') {
+      return 'aftercare';
+    }
+    
+    return 'delivery';
+  }
+
+  isPendingApproval(project: Project): boolean {
+    return project.currentStage === 'pendingApproval' || 
+           (project.currentStage === 'review' && project.status === 'warning');
+  }
+
+  getUrgencyLabel(urgency: string): string {
+    const labels: Record<string, string> = {
+      'high': '高',
+      'medium': '中',
+      'low': '低'
+    };
+    return labels[urgency] || '无';
+  }
+
+  onViewProject(projectId: string, phaseId: string, event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.viewProject.emit({ projectId, phaseId });
+  }
+
+  onOpenSmartMatch(project: Project, event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.openSmartMatch.emit(project);
+  }
+
+  onAssignProject(projectId: string, event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.assignProject.emit(projectId);
+  }
+
+  onReviewProject(projectId: string, rating: 'excellent' | 'qualified' | 'unqualified', event?: Event): void {
+    if (event) {
+      event.stopPropagation();
+    }
+    this.reviewProject.emit({ projectId, rating });
+  }
+}

+ 50 - 0
src/app/pages/team-leader/dashboard/components/smart-match-modal/smart-match-modal.component.html

@@ -0,0 +1,50 @@
+<div class="smart-match-modal" *ngIf="visible">
+  <div class="modal-backdrop" (click)="onClose()"></div>
+  <div class="modal-content">
+    <div class="modal-header">
+      <h3>🤖 智能推荐设计师</h3>
+      <button class="btn-close" (click)="onClose()">×</button>
+    </div>
+    
+    <div class="project-info" *ngIf="selectedProject">
+      <h4>{{ selectedProject.name }}</h4>
+      <div class="tags">
+        <span class="tag">{{ selectedProject.type === 'hard' ? '硬装' : '软装' }}</span>
+        <span class="tag">{{ selectedProject.memberType === 'vip' ? 'VIP' : '普通' }}</span>
+        <span class="tag urgency u-{{ selectedProject.urgency }}">
+          {{ getUrgencyLabel(selectedProject.urgency) }}
+        </span>
+      </div>
+    </div>
+    
+    <div class="recommendations-list">
+      <div class="rec-card" *ngFor="let rec of recommendations; let i = index">
+        <div class="rank" [class.gold]="i===0" [class.silver]="i===1" [class.bronze]="i===2">
+          {{ i + 1 }}
+        </div>
+        <div class="designer-info">
+          <h4>{{ rec.designer.name }}</h4>
+          <div class="match-score-bar">
+            <div class="score-fill" [style.width.%]="rec.matchScore">
+              <span>{{ rec.matchScore }}分</span>
+            </div>
+          </div>
+        </div>
+        <div class="details">
+          <p><strong>擅长:</strong>{{ rec.designer.tags.expertise.styles.join('、') || '暂无标签' }}</p>
+          <p><strong>负载:</strong>{{ rec.loadRate.toFixed(0) }}% ({{ rec.currentProjects }}个项目)</p>
+          <p><strong>评分:</strong>⭐ {{ rec.designer.tags.history.avgRating || '暂无' }}</p>
+          <p class="reason"><strong>推荐理由:</strong>{{ rec.reason }}</p>
+        </div>
+        <button class="btn-assign" (click)="onAssign(rec.designer.id)">
+          分配给TA
+        </button>
+      </div>
+      
+      <div class="empty" *ngIf="recommendations.length === 0">
+        <p>未找到合适的设计师</p>
+        <p>您可以手动分配或调整项目参数</p>
+      </div>
+    </div>
+  </div>
+</div>

+ 273 - 0
src/app/pages/team-leader/dashboard/components/smart-match-modal/smart-match-modal.component.scss

@@ -0,0 +1,273 @@
+@use "sass:color";
+
+// iOS主题变量
+$ios-spacing-sm: 8px;
+$ios-spacing-md: 12px;
+$ios-spacing-lg: 16px;
+$ios-spacing-xl: 24px;
+$ios-radius-lg: 12px;
+$ios-radius-full: 9999px;
+$ios-shadow-card: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+$ios-font-weight-medium: 500;
+$ios-font-weight-semibold: 600;
+$ios-font-weight-bold: 700;
+$ios-font-size-sm: 14px;
+$ios-font-size-md: 16px;
+$ios-font-size-lg: 18px;
+$ios-font-size-xl: 20px;
+$ios-text-primary: #1f2937;
+$ios-text-secondary: #6b7280;
+$ios-card-background: #ffffff;
+$ios-border: #e5e7eb;
+$ios-background: #f9fafb;
+$ios-primary: #6366f1;
+$ios-primary-light: #818cf8;
+$ios-success: #10b981;
+$ios-danger: #ef4444;
+$ios-warning: #f59e0b;
+$ios-info: #3b82f6;
+
+/* ========== 智能推荐弹窗样式 ========== */
+.smart-match-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  
+  .modal-backdrop {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    animation: fadeIn 0.2s ease;
+  }
+  
+  .modal-content {
+    position: relative;
+    width: 90%;
+    max-width: 800px;
+    max-height: 90vh;
+    background: white;
+    border-radius: 16px;
+    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    animation: slideUp 0.3s ease;
+    
+    .modal-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 20px;
+      border-bottom: 1px solid $ios-border;
+      background: linear-gradient(135deg, #f0f9ff, #ffffff);
+      
+      h3 {
+        font-size: $ios-font-size-xl;
+        font-weight: $ios-font-weight-bold;
+        color: $ios-text-primary;
+        margin: 0;
+      }
+      
+      .btn-close {
+        width: 36px;
+        height: 36px;
+        border: none;
+        background: #f3f4f6;
+        border-radius: 50%;
+        font-size: 24px;
+        color: $ios-text-secondary;
+        cursor: pointer;
+        transition: all 0.2s ease;
+        
+        &:hover {
+          background: $ios-border;
+          color: $ios-text-primary;
+        }
+      }
+    }
+    
+    .project-info {
+      padding: 16px 20px;
+      background: $ios-background;
+      border-bottom: 1px solid $ios-border;
+      
+      h4 {
+        font-size: $ios-font-size-lg;
+        font-weight: $ios-font-weight-semibold;
+        color: $ios-text-primary;
+        margin: 0 0 8px 0;
+      }
+      
+      .tags {
+        display: flex;
+        gap: 8px;
+        
+        .tag {
+          padding: 4px 12px;
+          border-radius: 8px;
+          font-size: $ios-font-size-sm;
+          background: white;
+          border: 1px solid $ios-border;
+          
+          &.urgency {
+            &.u-high { background: #fecaca; color: $ios-danger; border-color: $ios-danger; }
+            &.u-medium { background: #fed7aa; color: $ios-warning; border-color: $ios-warning; }
+            &.u-low { background: #d1fae5; color: $ios-success; border-color: $ios-success; }
+          }
+        }
+      }
+    }
+    
+    .recommendations-list {
+      flex: 1;
+      overflow-y: auto;
+      padding: 20px;
+      
+      .rec-card {
+        display: flex;
+        gap: 16px;
+        padding: 16px;
+        background: white;
+        border: 1px solid $ios-border;
+        border-radius: 12px;
+        margin-bottom: 16px;
+        transition: all 0.2s ease;
+        
+        &:hover {
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+        }
+        
+        .rank {
+          width: 40px;
+          height: 40px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: $ios-font-size-lg;
+          font-weight: $ios-font-weight-bold;
+          border-radius: 50%;
+          background: #f3f4f6;
+          color: $ios-text-secondary;
+          flex-shrink: 0;
+          
+          &.gold { background: linear-gradient(135deg, #fbbf24, #f59e0b); color: white; }
+          &.silver { background: linear-gradient(135deg, #d1d5db, #9ca3af); color: white; }
+          &.bronze { background: linear-gradient(135deg, #f97316, #ea580c); color: white; }
+        }
+        
+        .designer-info {
+          flex: 1;
+          
+          h4 {
+            font-size: $ios-font-size-md;
+            font-weight: $ios-font-weight-semibold;
+            color: $ios-text-primary;
+            margin: 0 0 8px 0;
+          }
+          
+          .match-score-bar {
+            position: relative;
+            height: 20px;
+            background: #f3f4f6;
+            border-radius: 12px;
+            overflow: hidden;
+            
+            .score-fill {
+              height: 100%;
+              background: linear-gradient(90deg, $ios-info, $ios-primary);
+              display: flex;
+              align-items: center;
+              justify-content: flex-end;
+              padding-right: 8px;
+              transition: width 0.3s ease;
+              
+              span {
+                font-size: 12px;
+                font-weight: $ios-font-weight-semibold;
+                color: white;
+              }
+            }
+          }
+        }
+        
+        .details {
+          flex: 1;
+          font-size: $ios-font-size-sm;
+          
+          p {
+            margin: 4px 0;
+            color: $ios-text-secondary;
+            
+            strong {
+              color: $ios-text-primary;
+            }
+            
+            &.reason {
+              color: #2563eb;
+              font-weight: $ios-font-weight-medium;
+            }
+          }
+        }
+        
+        .btn-assign {
+          align-self: center;
+          padding: 8px 20px;
+          background: linear-gradient(135deg, $ios-info, $ios-primary);
+          color: white;
+          border: none;
+          border-radius: 8px;
+          font-size: $ios-font-size-sm;
+          font-weight: $ios-font-weight-semibold;
+          cursor: pointer;
+          transition: all 0.2s ease;
+          
+          &:hover {
+            box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
+            transform: translateY(-2px);
+          }
+        }
+      }
+      
+      .empty {
+        text-align: center;
+        padding: 48px;
+        color: $ios-text-secondary;
+        
+        p {
+          margin: 8px 0;
+          
+          &:first-child {
+            font-size: $ios-font-size-lg;
+            font-weight: $ios-font-weight-semibold;
+            color: $ios-text-primary;
+          }
+        }
+      }
+    }
+  }
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes slideUp {
+  from {
+    opacity: 0;
+    transform: translateY(30px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}

+ 36 - 0
src/app/pages/team-leader/dashboard/components/smart-match-modal/smart-match-modal.component.ts

@@ -0,0 +1,36 @@
+import { Component, Input, Output, EventEmitter } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Project } from '../../interfaces';
+
+@Component({
+  selector: 'app-smart-match-modal',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './smart-match-modal.component.html',
+  styleUrls: ['./smart-match-modal.component.scss']
+})
+export class SmartMatchModalComponent {
+  @Input() visible: boolean = false;
+  @Input() selectedProject: Project | null = null;
+  @Input() recommendations: any[] = [];
+
+  @Output() close = new EventEmitter<void>();
+  @Output() assign = new EventEmitter<string>();
+
+  onClose(): void {
+    this.close.emit();
+  }
+
+  onAssign(designerId: string): void {
+    this.assign.emit(designerId);
+  }
+
+  getUrgencyLabel(urgency: string): string {
+    const labels: Record<string, string> = {
+      'high': '高',
+      'medium': '中',
+      'low': '低'
+    };
+    return labels[urgency] || '未知';
+  }
+}

+ 349 - 0
src/app/pages/team-leader/dashboard/components/todo-section/todo-section.component.html

@@ -0,0 +1,349 @@
+<section class="todo-section todo-section-dual">
+  <div class="section-header">
+    <h2>待办事项</h2>
+    <button 
+      class="btn-refresh" 
+      (click)="refreshTodoTasks()"
+      [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>
+          待办问题
+          <span class="task-count" *ngIf="todoTasksFromIssues.length > 0">({{ todoTasksFromIssues.length }})</span>
+        </h3>
+        <span class="column-subtitle">来自项目问题板块</span>
+      </div>
+  
+      <!-- 加载状态 -->
+      <div class="loading-state" *ngIf="loadingTodoTasks">
+        <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>
+      
+      <!-- 错误状态 -->
+      <div class="error-state" *ngIf="!loadingTodoTasks && todoTaskError">
+        <svg viewBox="0 0 24 24" width="48" height="48" fill="#ef4444">
+          <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>
+        <p>{{ todoTaskError }}</p>
+        <button class="btn-retry" (click)="refreshTodoTasks()">重试</button>
+      </div>
+      
+      <!-- 空状态 -->
+      <div class="empty-state" *ngIf="!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length === 0">
+        <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
+          <path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
+        </svg>
+        <p>暂无待办任务</p>
+        <p class="hint">所有项目问题都已处理完毕 🎉</p>
+      </div>
+      
+      <!-- 待办任务列表 -->
+      <div class="todo-list-compact" *ngIf="!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length > 0">
+        <div class="todo-item-compact" *ngFor="let task of todoTasksFromIssues" [attr.data-priority]="task.priority">
+          <!-- 左侧优先级色条 -->
+          <div class="priority-indicator" [attr.data-priority]="task.priority"></div>
+          
+          <!-- 任务内容 -->
+          <div class="task-content">
+            <!-- 标题行 -->
+            <div class="task-header">
+              <span class="task-title">{{ task.title }}</span>
+              <div class="task-badges">
+                <span class="badge badge-priority" [attr.data-priority]="task.priority">
+                  {{ getPriorityConfig(task.priority).label }}
+                </span>
+                <span class="badge badge-type">{{ getIssueTypeLabel(task.type) }}</span>
+              </div>
+            </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>
+                项目: {{ task.projectName }}
+                <ng-container *ngIf="task.relatedSpace"> | {{ task.relatedSpace }}</ng-container>
+                <ng-container *ngIf="task.relatedStage"> | {{ task.relatedStage }}</ng-container>
+              </span>
+            </div>
+            
+            <!-- 底部信息行 -->
+            <div class="task-footer">
+              <span class="time-info" [title]="formatExactTime(task.createdAt)">
+                <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>
+                创建于 {{ formatRelativeTime(task.createdAt) }}
+              </span>
+              
+              <span class="assignee-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>
+                指派给: {{ task.assigneeName }}
+              </span>
+            </div>
+          </div>
+          
+          <!-- 右侧操作按钮 -->
+          <div class="task-actions">
+            <button 
+              class="btn-action btn-view" 
+              (click)="onNavigateToIssue(task)"
+              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>
+            <button 
+              class="btn-action btn-mark-read" 
+              (click)="onMarkAsRead(task)"
+              title="标记已读">
+              <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
+                <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
+              </svg>
+              标记已读
+            </button>
+          </div>
+        </div>
+      </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>
+          紧急事件
+          <span class="task-count urgent" *ngIf="urgentEvents.length > 0">({{ urgentEvents.length }})</span>
+        </h3>
+        <span class="column-subtitle">自动计算的截止事件</span>
+      </div>
+      
+      <div class="tag-filter-bar" *ngIf="!loadingUrgentEvents && urgentEvents.length > 0">
+        <button 
+          class="tag-button"
+          [class.active]="urgentEventTagFilter === 'all'"
+          (click)="filterUrgentEventsByTag('all')"
+          title="显示所有紧急事件"
+        >
+          <span class="tag-icon">📋</span>
+          <span class="tag-label">全部</span>
+          <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'"
+          (click)="filterUrgentEventsByTag('phase')"
+          title="工作阶段"
+        >
+          <span class="tag-icon">🔧</span>
+          <span class="tag-label">工作阶段</span>
+          <span class="tag-count">{{ getTagCount('phase') }}</span>
+        </button>
+        
+        <button 
+          class="tag-button"
+          [class.active]="urgentEventTagFilter === 'review'"
+          (click)="filterUrgentEventsByTag('review')"
+          title="小图截止"
+        >
+          <span class="tag-icon">📐</span>
+          <span class="tag-label">小图截止</span>
+          <span class="tag-count">{{ getTagCount('review') }}</span>
+        </button>
+        
+        <button 
+          class="tag-button"
+          [class.active]="urgentEventTagFilter === 'delivery'"
+          (click)="filterUrgentEventsByTag('delivery')"
+          title="交付延期"
+        >
+          <span class="tag-icon">📦</span>
+          <span class="tag-label">交付延期</span>
+          <span class="tag-count">{{ getTagCount('delivery') }}</span>
+        </button>
+      </div>
+      
+      <!-- 加载状态 -->
+      <div class="loading-state" *ngIf="loadingUrgentEvents">
+        <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>
+      
+      <!-- 空状态 -->
+      <div class="empty-state" *ngIf="!loadingUrgentEvents && urgentEvents.length === 0">
+        <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>
+
+      <div class="empty-state filtered" *ngIf="!loadingUrgentEvents && urgentEvents.length > 0 && filteredUrgentEventsList.length === 0">
+        <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"/>
+        </svg>
+        <p>该筛选条件下暂无事件</p>
+        <p class="hint">尝试切换其他标签</p>
+      </div>
+      
+      <!-- 紧急事件列表 -->
+      <div class="todo-list-compact urgent-list" *ngIf="!loadingUrgentEvents && filteredUrgentEventsList.length > 0">
+        <div class="todo-item-compact urgent-item" *ngFor="let event of filteredUrgentEventsList; trackBy: trackUrgentEventById" [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">
+                  <ng-container *ngIf="event.urgencyLevel === 'critical'">🔴 紧急</ng-container>
+                  <ng-container *ngIf="event.urgencyLevel === 'high'">🟠 重要</ng-container>
+                  <ng-container *ngIf="event.urgencyLevel === 'medium'">🟡 注意</ng-container>
+                </span>
+                <span class="badge badge-event-type">
+                  <ng-container *ngIf="event.eventType === 'review'">对图</ng-container>
+                  <ng-container *ngIf="event.eventType === 'delivery'">交付</ng-container>
+                  <ng-container *ngIf="event.eventType === 'phase_deadline'">{{ event.phaseName }}</ng-container>
+                  <ng-container *ngIf="getEventCategory(event) === 'customer'">客户</ng-container>
+                </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>
+            
+            <!-- 描述 -->
+            <div class="task-description">
+              {{ event.description }}
+            </div>
+            <div class="followup-tip" *ngIf="event.followUpNeeded">
+              客户反馈待跟进 · 请及时追踪
+            </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>
+              <span class="designer-info" *ngIf="event.designerName">
+                <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' }}
+                <span class="overdue-label" *ngIf="event.overdueDays && event.overdueDays > 0">(逾期{{ event.overdueDays }}天)</span>
+                <span class="upcoming-label" *ngIf="event.overdueDays && event.overdueDays < 0">(还剩{{ -event.overdueDays }}天)</span>
+                <span class="today-label" *ngIf="!event.overdueDays">(今天)</span>
+              </span>
+              
+              <span class="completion-info" *ngIf="event.completionRate !== undefined">
+                <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-muted" 
+              *ngIf="event.allowConfirmOnTime"
+              (click)="onConfirmEventOnTime(event)"
+            >
+              可按时交付
+            </button>
+            <button 
+              class="btn-action btn-stagnant" 
+              *ngIf="event.statusType !== 'stagnant'"
+              (click)="onMarkEventAsStagnant(event)"
+            >
+              标记停滞
+            </button>
+            <button 
+              class="btn-action btn-resolve" 
+              *ngIf="event.allowMarkHandled"
+              (click)="onResolveUrgentEvent(event)"
+            >
+              事件已处理
+            </button>
+            <button 
+              class="btn-action btn-todo" 
+              *ngIf="event.allowCreateTodo"
+              (click)="onCreateTodoFromEvent(event)"
+            >
+              创建代办
+            </button>
+            <button 
+              class="btn-action btn-view" 
+              (click)="onProjectClickHandler(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>

+ 724 - 0
src/app/pages/team-leader/dashboard/components/todo-section/todo-section.component.scss

@@ -0,0 +1,724 @@
+@use "sass:color";
+
+/* 待办任务样式(重构版 - 基于项目问题板块) */
+.todo-section {
+  background-color: white;
+  border-radius: 12px;
+  padding: 24px;
+  margin-bottom: 24px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  
+  .section-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    
+    h2 {
+      font-size: 20px;
+      font-weight: 600;
+      color: #111827;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      margin: 0;
+      
+      .task-count {
+        font-size: 16px;
+        color: #6b7280;
+        font-weight: 400;
+      }
+    }
+    
+    .section-actions {
+      display: flex;
+      gap: 12px;
+      
+      .btn-refresh {
+        display: flex;
+        align-items: center;
+        gap: 6px;
+        padding: 8px 16px;
+        background: #f3f4f6;
+        border: 1px solid #e5e7eb;
+        border-radius: 6px;
+        font-size: 14px;
+        color: #374151;
+        cursor: pointer;
+        transition: all 0.2s;
+        
+        &:hover:not(:disabled) {
+          background: #e5e7eb;
+          border-color: #d1d5db;
+        }
+        
+        &:disabled {
+          opacity: 0.6;
+          cursor: not-allowed;
+        }
+        
+        svg.rotating {
+          animation: rotate 1s linear infinite;
+        }
+      }
+    }
+  }
+  
+  // 加载/错误/空状态
+  .loading-state,
+  .error-state,
+  .empty-state {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 48px 24px;
+    text-align: center;
+    
+    .spinner {
+      width: 40px;
+      height: 40px;
+      border: 4px solid #f3f4f6;
+      border-top-color: #667eea;
+      border-radius: 50%;
+      animation: rotate 1s linear infinite;
+    }
+    
+    p {
+      margin-top: 16px;
+      font-size: 14px;
+      color: #6b7280;
+      
+      &.hint {
+        font-size: 13px;
+        color: #9ca3af;
+        margin-top: 8px;
+      }
+    }
+    
+    .btn-retry {
+      margin-top: 16px;
+      padding: 8px 20px;
+      background: #667eea;
+      color: white;
+      border: none;
+      border-radius: 6px;
+      font-size: 14px;
+      cursor: pointer;
+      transition: background 0.2s;
+      
+      &:hover {
+        background: #5568d3;
+      }
+    }
+  }
+  
+  // 紧凑列表
+  .todo-list-compact {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    
+    .todo-item-compact {
+      position: relative;
+      display: flex;
+      align-items: stretch;
+      background: #fafafa;
+      border: 1px solid #e5e7eb;
+      border-radius: 8px;
+      overflow: hidden;
+      transition: all 0.2s;
+      cursor: pointer;
+      
+      &:hover {
+        background: #f9fafb;
+        border-color: #d1d5db;
+        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+        transform: translateY(-1px);
+      }
+      
+      // 优先级指示条
+      .priority-indicator {
+        width: 4px;
+        flex-shrink: 0;
+        border-radius: 2px 0 0 2px;
+        
+        // 根据优先级设置颜色
+        &[data-priority="critical"] {
+          background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%);
+        }
+        
+        &[data-priority="high"] {
+          background: linear-gradient(180deg, #f97316 0%, #ea580c 100%);
+        }
+        
+        &[data-priority="medium"] {
+          background: linear-gradient(180deg, #eab308 0%, #ca8a04 100%);
+        }
+        
+        &[data-priority="low"] {
+          background: linear-gradient(180deg, #d1d5db 0%, #9ca3af 100%);
+        }
+      }
+      
+      // 主要内容区
+      .task-content {
+        flex: 1;
+        padding: 16px;
+        min-width: 0;
+        
+        .task-header {
+          display: flex;
+          align-items: center;
+          gap: 8px;
+          margin-bottom: 8px;
+          
+          .priority-icon {
+            font-size: 16px;
+            flex-shrink: 0;
+          }
+          
+          .priority-label {
+            font-size: 12px;
+            font-weight: 600;
+            flex-shrink: 0;
+          }
+          
+          .task-title {
+            font-size: 15px;
+            font-weight: 500;
+            color: #111827;
+            flex: 1;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+            margin: 0;
+          }
+          
+          .task-badges {
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            flex-shrink: 0;
+          }
+          
+          .badge {
+            padding: 3px 8px;
+            border-radius: 4px;
+            font-size: 11px;
+            font-weight: 600;
+            white-space: nowrap;
+            
+            &.badge-priority {
+              // 根据优先级设置颜色
+              &[data-priority="critical"] {
+                background: #fee2e2;
+                color: #dc2626;
+              }
+              
+              &[data-priority="high"] {
+                background: #ffedd5;
+                color: #f97316;
+              }
+              
+              &[data-priority="medium"] {
+                background: #fef3c7;
+                color: #ca8a04;
+              }
+              
+              &[data-priority="low"] {
+                background: #f3f4f6;
+                color: #6b7280;
+              }
+            }
+            
+            &.badge-type {
+              background: #e0e7ff;
+              color: #4f46e5;
+            }
+          }
+          
+          .issue-type-badge {
+            padding: 2px 8px;
+            background: #e0e7ff;
+            color: #4f46e5;
+            border-radius: 4px;
+            font-size: 11px;
+            font-weight: 500;
+            flex-shrink: 0;
+          }
+        }
+        
+        .task-meta {
+          display: flex;
+          align-items: center;
+          gap: 16px;
+          margin-bottom: 8px;
+          flex-wrap: wrap;
+          
+          .meta-item {
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            font-size: 13px;
+            color: #6b7280;
+            
+            svg {
+              opacity: 0.6;
+            }
+          }
+        }
+        
+        .task-footer {
+          display: flex;
+          align-items: center;
+          gap: 16px;
+          flex-wrap: wrap;
+          
+          .time-info,
+          .assignee-info {
+            display: flex;
+            align-items: center;
+            gap: 4px;
+            font-size: 12px;
+            color: #9ca3af;
+            
+            svg {
+              opacity: 0.7;
+            }
+          }
+          
+          // 时间信息的特殊样式(支持 tooltip 显示精确时间)
+          .time-info {
+            cursor: help; // 鼠标悬停时显示帮助图标
+            position: relative;
+            padding: 2px 4px;
+            margin: -2px -4px; // 抵消padding,保持原有位置
+            border-radius: 3px;
+            transition: all 0.2s ease;
+            
+            &:hover {
+              background-color: rgba(102, 126, 234, 0.1);
+              color: #667eea;
+              
+              svg {
+                opacity: 1;
+              }
+            }
+          }
+          
+          .tags {
+            display: flex;
+            align-items: center;
+            gap: 6px;
+            
+            .tag {
+              padding: 2px 6px;
+              background: #f3f4f6;
+              color: #6b7280;
+              border-radius: 3px;
+              font-size: 11px;
+            }
+            
+            .tag-more {
+              font-size: 11px;
+              color: #9ca3af;
+            }
+          }
+        }
+      }
+      
+      // 操作按钮区
+      .task-actions {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        padding: 16px;
+        border-left: 1px solid #e5e7eb;
+        background: white;
+        
+        button {
+          padding: 6px 12px;
+          border: 1px solid #d1d5db;
+          border-radius: 4px;
+          font-size: 12px;
+          cursor: pointer;
+          transition: all 0.2s;
+          white-space: nowrap;
+          
+          &.btn-view {
+            background: #667eea;
+            color: white;
+            border-color: #667eea;
+            
+            &:hover {
+              background: #5568d3;
+              border-color: #5568d3;
+            }
+          }
+          
+          &.btn-mark-read {
+            background: white;
+            color: #6b7280;
+            
+            &:hover {
+              background: #f9fafb;
+              border-color: #9ca3af;
+            }
+          }
+          
+          // 新增的按钮样式
+          &.btn-muted {
+            background: #f0fdf4;
+            color: #16a34a;
+            border-color: #86efac;
+            &:hover { background: #dcfce7; }
+          }
+          
+          &.btn-stagnant {
+            background: #fef2f2;
+            color: #dc2626;
+            border-color: #fca5a5;
+            &:hover { background: #fee2e2; }
+          }
+          
+          &.btn-resolve {
+            background: #eff6ff;
+            color: #2563eb;
+            border-color: #93c5fd;
+            &:hover { background: #dbeafe; }
+          }
+          
+          &.btn-todo {
+            background: #fff7ed;
+            color: #ea580c;
+            border-color: #fdba74;
+            &:hover { background: #ffedd5; }
+          }
+        }
+      }
+    }
+  }
+}
+
+// 🆕 双栏布局样式
+.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%);
+      }
+      
+      // 标签过滤器样式
+      .tag-filter-bar {
+        display: flex;
+        gap: 8px;
+        padding: 12px;
+        background: #fff7ed;
+        border-bottom: 1px solid #fed7aa;
+        overflow-x: auto;
+        
+        .tag-button {
+          display: flex;
+          align-items: center;
+          gap: 4px;
+          padding: 4px 10px;
+          background: white;
+          border: 1px solid #fed7aa;
+          border-radius: 16px;
+          font-size: 12px;
+          color: #7c2d12;
+          cursor: pointer;
+          transition: all 0.2s;
+          white-space: nowrap;
+          
+          &:hover {
+            background: #fff2cc;
+          }
+          
+          &.active {
+            background: #f97316;
+            color: white;
+            border-color: #ea580c;
+            
+            .tag-count {
+              background: rgba(255,255,255,0.2);
+              color: white;
+            }
+          }
+          
+          .tag-icon {
+            font-size: 12px;
+          }
+          
+          .tag-count {
+            background: #fed7aa;
+            color: #9a3412;
+            padding: 0 6px;
+            border-radius: 10px;
+            font-size: 10px;
+            font-weight: 600;
+          }
+        }
+      }
+      
+      // 紧急事件特定样式
+      .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;
+          font-weight: 600;
+        }
+        
+        &[data-urgency="medium"] {
+          background: #fef3c7;
+          color: #f59e0b;
+        }
+      }
+      
+      .badge-event-type {
+        background: #dbeafe;
+        color: #1e40af;
+      }
+      
+      .badge-status {
+        padding: 2px 6px;
+        border-radius: 4px;
+        font-size: 11px;
+        
+        &.overdue {
+          background: #fee2e2;
+          color: #991b1b;
+        }
+        
+        &.upcoming {
+          background: #ffedd5;
+          color: #9a3412;
+        }
+        
+        &.stagnant {
+          background: #f3f4f6;
+          color: #4b5563;
+        }
+        
+        &.customer {
+          background: #e0e7ff;
+          color: #4338ca;
+        }
+      }
+      
+      .task-description {
+        font-size: 13px;
+        color: #6b7280;
+        margin: 8px 0;
+        line-height: 1.5;
+      }
+      
+      .followup-tip {
+        font-size: 12px;
+        color: #b91c1c;
+        background: #fef2f2;
+        padding: 4px 8px;
+        border-radius: 4px;
+        margin-bottom: 8px;
+        display: inline-block;
+      }
+      
+      .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);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+// 响应式布局
+@media (max-width: 768px) {
+  .todo-section {
+    padding: 16px;
+    
+    .section-header {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 12px;
+    }
+    
+    .todo-list-compact {
+      .todo-item-compact {
+        flex-direction: column;
+        
+        .task-actions {
+          flex-direction: row;
+          border-left: none;
+          border-top: 1px solid #e5e7eb;
+          padding: 12px;
+          
+          button {
+            flex: 1;
+          }
+        }
+      }
+    }
+  }
+  
+  // 🆕 双栏布局响应式
+  .todo-section-dual {
+    .todo-dual-columns {
+      grid-template-columns: 1fr;
+      gap: 16px;
+    }
+    
+    .todo-column {
+      .column-header h3 {
+        font-size: 15px;
+      }
+    }
+  }
+}

+ 194 - 0
src/app/pages/team-leader/dashboard/components/todo-section/todo-section.component.ts

@@ -0,0 +1,194 @@
+import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { TodoTaskFromIssue, UrgentEvent, Project } from '../../interfaces';
+import { ProjectIssue, IssuePriority, IssueType } from '../../../../../../modules/project/services/project-issue.service';
+
+@Component({
+  selector: 'app-todo-section',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './todo-section.component.html',
+  styleUrls: ['./todo-section.component.scss']
+})
+export class TodoSectionComponent implements OnInit, OnDestroy {
+  @Input() todoTasksFromIssues: TodoTaskFromIssue[] = [];
+  @Input() loadingTodoTasks: boolean = false;
+  @Input() todoTaskError: string = '';
+  
+  @Input() urgentEvents: UrgentEvent[] = [];
+  @Input() loadingUrgentEvents: boolean = false;
+  
+  @Output() refresh = new EventEmitter<void>();
+  @Output() navigateToIssue = new EventEmitter<TodoTaskFromIssue>();
+  @Output() markAsRead = new EventEmitter<TodoTaskFromIssue>();
+  @Output() projectClick = new EventEmitter<string>();
+  
+  // Urgent Event Actions
+  @Output() confirmEventOnTime = new EventEmitter<UrgentEvent>();
+  @Output() markEventAsStagnant = new EventEmitter<UrgentEvent>();
+  @Output() resolveUrgentEvent = new EventEmitter<UrgentEvent>();
+  @Output() createTodoFromEvent = new EventEmitter<UrgentEvent>();
+
+  filteredUrgentEventsList: UrgentEvent[] = [];
+  urgentEventTagFilter: 'all' | 'customer' | 'phase' | 'review' | 'delivery' = 'all';
+  
+  private urgentEventsCache = new Map<string, UrgentEvent[]>();
+
+  constructor(private cdr: ChangeDetectorRef) {}
+
+  ngOnInit(): void {
+    this.filterUrgentEvents();
+  }
+
+  // 当输入数据变化时更新过滤列表
+  ngOnChanges(): void {
+    this.urgentEventsCache.clear();
+    this.filterUrgentEvents();
+  }
+
+  ngOnDestroy(): void {
+    // Cleanup if needed
+  }
+
+  refreshTodoTasks(): void {
+    this.refresh.emit();
+  }
+
+  // 过滤紧急事件
+  filterUrgentEventsByTag(tag: 'all' | 'customer' | 'phase' | 'review' | 'delivery'): void {
+    if (this.urgentEventTagFilter === tag) return;
+    this.urgentEventTagFilter = tag;
+    this.filterUrgentEvents();
+  }
+
+  private filterUrgentEvents(): void {
+    if (this.urgentEventTagFilter === 'all') {
+      this.filteredUrgentEventsList = [...this.urgentEvents];
+      return;
+    }
+
+    // Check cache
+    if (this.urgentEventsCache.has(this.urgentEventTagFilter)) {
+      this.filteredUrgentEventsList = this.urgentEventsCache.get(this.urgentEventTagFilter)!;
+      return;
+    }
+
+    let filtered: UrgentEvent[] = [];
+    
+    if (this.urgentEventTagFilter === 'customer') {
+      filtered = this.urgentEvents.filter(e => this.getEventCategory(e) === 'customer');
+    } else if (this.urgentEventTagFilter === 'phase') {
+      filtered = this.urgentEvents.filter(e => e.eventType === 'phase_deadline');
+    } else if (this.urgentEventTagFilter === 'review') {
+      filtered = this.urgentEvents.filter(e => e.eventType === 'review');
+    } else if (this.urgentEventTagFilter === 'delivery') {
+      filtered = this.urgentEvents.filter(e => e.eventType === 'delivery');
+    }
+
+    // Update cache
+    this.urgentEventsCache.set(this.urgentEventTagFilter, filtered);
+    this.filteredUrgentEventsList = filtered;
+  }
+
+  getTagCount(tag: string): number {
+    if (tag === 'all') return this.urgentEvents.length;
+    
+    if (tag === 'customer') {
+      return this.urgentEvents.filter(e => this.getEventCategory(e) === 'customer').length;
+    } else if (tag === 'phase') {
+      return this.urgentEvents.filter(e => e.eventType === 'phase_deadline').length;
+    } else if (tag === 'review') {
+      return this.urgentEvents.filter(e => e.eventType === 'review').length;
+    } else if (tag === 'delivery') {
+      return this.urgentEvents.filter(e => e.eventType === 'delivery').length;
+    }
+    return 0;
+  }
+
+  getEventCategory(event: UrgentEvent): string {
+    if (event.category) return event.category;
+    
+    // Fallback logic if category is missing
+    if (event.eventType === 'customer_alert' || event.title.includes('客户') || event.description.includes('客户')) {
+      return 'customer';
+    }
+    if (event.eventType === 'review') return 'review';
+    if (event.eventType === 'delivery') return 'delivery';
+    if (event.eventType === 'phase_deadline') return 'phase';
+    
+    return 'other';
+  }
+
+  trackUrgentEventById(index: number, event: UrgentEvent): string {
+    return event.id;
+  }
+
+  // Helpers for template
+  getPriorityConfig(priority: string): { label: string, class: string } {
+    const config: Record<string, { label: string, class: string }> = {
+      'critical': { label: '紧急', class: 'badge-critical' },
+      'high': { label: '高', class: 'badge-high' },
+      'medium': { label: '中', class: 'badge-medium' },
+      'low': { label: '低', class: 'badge-low' }
+    };
+    return config[priority] || { label: '普通', class: 'badge-normal' };
+  }
+
+  getIssueTypeLabel(type: string): string {
+    const labels: Record<string, string> = {
+      'design': '设计',
+      'construction': '施工',
+      'material': '材料',
+      'customer': '客户',
+      'other': '其他'
+    };
+    return labels[type] || type;
+  }
+
+  formatExactTime(date: Date | string): string {
+    return new Date(date).toLocaleString('zh-CN');
+  }
+
+  formatRelativeTime(date: Date | string): string {
+    const now = new Date();
+    const target = new Date(date);
+    const diffMs = now.getTime() - target.getTime();
+    const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+    
+    if (diffDays === 0) {
+      return '今天';
+    } else if (diffDays === 1) {
+      return '昨天';
+    } else {
+      return `${diffDays}天前`;
+    }
+  }
+
+  onNavigateToIssue(task: TodoTaskFromIssue): void {
+    this.navigateToIssue.emit(task);
+  }
+
+  onMarkAsRead(task: TodoTaskFromIssue): void {
+    this.markAsRead.emit(task);
+  }
+
+  onConfirmEventOnTime(event: UrgentEvent): void {
+    this.confirmEventOnTime.emit(event);
+  }
+
+  onMarkEventAsStagnant(event: UrgentEvent): void {
+    this.markEventAsStagnant.emit(event);
+  }
+
+  onResolveUrgentEvent(event: UrgentEvent): void {
+    this.resolveUrgentEvent.emit(event);
+  }
+
+  onCreateTodoFromEvent(event: UrgentEvent): void {
+    this.createTodoFromEvent.emit(event);
+  }
+
+  onProjectClickHandler(projectId: string): void {
+    this.projectClick.emit(projectId);
+  }
+}

+ 19 - 0
src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.html

@@ -0,0 +1,19 @@
+<div class="workload-gantt-card">
+  <div class="gantt-header">
+    <h3>工作量负载概览</h3>
+    <p class="gantt-subtitle">设计师每日工作状态一目了然</p>
+    <div class="gantt-controls">
+      <div class="scale-switch">
+        <button [class.active]="workloadGanttScale === 'week'" (click)="setWorkloadGanttScale('week')">周视图</button>
+        <button [class.active]="workloadGanttScale === 'month'" (click)="setWorkloadGanttScale('month')">月视图</button>
+      </div>
+      <div class="legend">
+        <span class="legend-item"><span class="dot idle"></span>空闲</span>
+        <span class="legend-item"><span class="dot busy"></span>忙碌</span>
+        <span class="legend-item"><span class="dot overload"></span>超负荷</span>
+        <span class="legend-item"><span class="dot leave"></span>请假</span>
+      </div>
+    </div>
+  </div>
+  <div class="gantt-container" #workloadGanttContainer></div>
+</div>

+ 115 - 0
src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.scss

@@ -0,0 +1,115 @@
+/* ========== 工作负载甘特图样式 ========== */
+.workload-gantt-card {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+  margin-bottom: 24px;
+  
+  .gantt-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 16px;
+    flex-wrap: wrap;
+    gap: 12px;
+    
+    h3 {
+      font-size: 18px;
+      font-weight: 700;
+      color: #1f2937;
+      margin: 0;
+    }
+    
+    .gantt-subtitle {
+      width: 100%;
+      font-size: 13px;
+      color: #6b7280;
+      margin: 4px 0 8px 0;
+    }
+    
+    .gantt-controls {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      
+      .scale-switch {
+        display: flex;
+        background: #f3f4f6;
+        border-radius: 8px;
+        padding: 2px;
+        
+        button {
+          padding: 8px 16px;
+          border: none;
+          background: transparent;
+          border-radius: 6px;
+          font-size: 14px;
+          font-weight: 500;
+          color: #6b7280;
+          cursor: pointer;
+          transition: all 0.2s ease;
+          
+          &:hover {
+            color: #1f2937;
+          }
+          
+          &.active {
+            background: white;
+            color: #3b82f6;
+            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+          }
+        }
+      }
+      
+      .legend {
+        display: flex;
+        gap: 16px;
+        align-items: center;
+        
+        .legend-item {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          font-size: 14px;
+          color: #6b7280;
+          
+          .dot {
+            width: 20px;
+            height: 16px;
+            border-radius: 4px;
+            border: 1px solid rgba(0, 0, 0, 0.1);
+            
+            &.idle {
+              background: #d1fae5; // 空闲-浅绿色(0个项目)
+            }
+            
+            &.busy {
+              background: #bfdbfe; // 忙碌-浅蓝色(1-2个项目)
+            }
+            
+            &.overload {
+              background: #fecaca; // 超负荷-浅红色(≥3个项目)
+            }
+            
+            &.leave {
+              background: #e5e7eb; // 请假-灰色
+            }
+          }
+        }
+      }
+    }
+  }
+  
+  .gantt-container {
+    width: 100%;
+    height: 500px;
+    min-height: 400px;
+    cursor: pointer; // 提示可点击
+    
+    // ECharts会覆盖cursor,所以在全局添加
+    canvas {
+      cursor: pointer !important;
+    }
+  }
+}

+ 382 - 0
src/app/pages/team-leader/dashboard/components/workload-gantt/workload-gantt.component.ts

@@ -0,0 +1,382 @@
+import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, OnDestroy, OnChanges, SimpleChanges, AfterViewInit, ChangeDetectorRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Project } from '../../interfaces';
+
+declare const echarts: any;
+
+@Component({
+  selector: 'app-workload-gantt',
+  standalone: true,
+  imports: [CommonModule],
+  templateUrl: './workload-gantt.component.html',
+  styleUrls: ['./workload-gantt.component.scss']
+})
+export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewInit {
+  @Input() designerWorkloadMap: Map<string, any[]> = new Map();
+  @Input() realDesigners: any[] = [];
+  @Input() filteredProjects: Project[] = [];
+  
+  @Output() employeeClick = new EventEmitter<string>();
+
+  @ViewChild('workloadGanttContainer', { static: false }) workloadGanttContainer!: ElementRef<HTMLDivElement>;
+  
+  private workloadGanttChart: any | null = null;
+  workloadGanttScale: 'week' | 'month' = 'week';
+
+  constructor(private cdr: ChangeDetectorRef) {}
+
+  ngOnChanges(changes: SimpleChanges): void {
+    if (changes['designerWorkloadMap'] || changes['realDesigners'] || changes['filteredProjects']) {
+      this.updateWorkloadGantt();
+    }
+  }
+
+  ngAfterViewInit(): void {
+    // Initial update
+    setTimeout(() => this.updateWorkloadGantt(), 0);
+    
+    // Handle resize
+    window.addEventListener('resize', this.handleResize);
+  }
+
+  ngOnDestroy(): void {
+    window.removeEventListener('resize', this.handleResize);
+    if (this.workloadGanttChart) {
+      this.workloadGanttChart.dispose();
+      this.workloadGanttChart = null;
+    }
+  }
+
+  private handleResize = () => {
+    if (this.workloadGanttChart) {
+      this.workloadGanttChart.resize();
+    }
+  }
+
+  setWorkloadGanttScale(scale: 'week' | 'month'): void {
+    if (this.workloadGanttScale !== scale) {
+      this.workloadGanttScale = scale;
+      this.updateWorkloadGantt();
+    }
+  }
+
+  private updateWorkloadGantt(): void {
+    if (!this.workloadGanttContainer?.nativeElement) {
+      // If called too early, retry slightly later
+      // setTimeout(() => this.updateWorkloadGantt(), 100); 
+      return;
+    }
+
+    if (!this.workloadGanttChart) {
+      this.workloadGanttChart = echarts.init(this.workloadGanttContainer.nativeElement);
+      
+      // Add click event listener
+      this.workloadGanttChart.on('click', (params: any) => {
+        if (params.componentType === 'series' && params.seriesType === 'custom') {
+          const designerName = params.value[3]; // value[3] is designer name
+          if (designerName && designerName !== '未分配') {
+            this.employeeClick.emit(designerName);
+          }
+        }
+      });
+    }
+
+    const DAY = 24 * 60 * 60 * 1000;
+    const now = new Date();
+    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+    const todayTs = today.getTime();
+
+    // Time range
+    let xMin: number;
+    let xMax: number;
+    let xSplitNumber: number;
+    let xLabelFormatter: (value: number) => string;
+
+    if (this.workloadGanttScale === 'week') {
+      // Week view: next 7 days
+      xMin = todayTs;
+      xMax = todayTs + 7 * DAY;
+      xSplitNumber = 7;
+      xLabelFormatter = (val: any) => {
+        const date = new Date(val);
+        const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
+        return `${date.getMonth() + 1}/${date.getDate()}\n${weekDays[date.getDay()]}`;
+      };
+    } else {
+      // Month view: next 30 days
+      xMin = todayTs;
+      xMax = todayTs + 30 * DAY;
+      xSplitNumber = 30;
+      xLabelFormatter = (val: any) => {
+        const date = new Date(val);
+        return `${date.getMonth() + 1}/${date.getDate()}`;
+      };
+    }
+
+    // Get all real designers
+    let designers: string[] = [];
+    
+    if (this.realDesigners && this.realDesigners.length > 0) {
+      designers = this.realDesigners.map(d => d.name);
+    } else {
+      // Fallback: extract from filtered projects
+      const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
+      designers = Array.from(new Set(assigned.map(p => p.designerName)));
+    }
+    
+    if (designers.length === 0) {
+      // Empty state
+      const emptyOption = {
+        title: {
+          text: '暂无组员数据',
+          subtext: '请先在系统中添加设计师(组员角色)',
+          left: 'center',
+          top: 'center',
+          textStyle: { fontSize: 16, color: '#9ca3af' },
+          subtextStyle: { fontSize: 13, color: '#d1d5db' }
+        }
+      };
+      this.workloadGanttChart.setOption(emptyOption, true);
+      return;
+    }
+    
+    const workloadByDesigner: Record<string, any[]> = {};
+    designers.forEach(name => {
+      workloadByDesigner[name] = [];
+    });
+
+    // Calculate total load for sorting
+    const designerTotalLoad: Record<string, number> = {};
+    designers.forEach(name => {
+      const projects = this.designerWorkloadMap.get(name) || [];
+      designerTotalLoad[name] = projects.length;
+    });
+    
+    // Sort designers by total load descending
+    const sortedDesigners = designers.sort((a, b) => {
+      return designerTotalLoad[b] - designerTotalLoad[a];
+    });
+    
+    // Generate time slot data for each designer
+    sortedDesigners.forEach((designerName, yIndex) => {
+      const designerProjects = this.designerWorkloadMap.get(designerName) || [];
+      
+      // Calculate status for each day
+      const days = this.workloadGanttScale === 'week' ? 7 : 30;
+      for (let i = 0; i < days; i++) {
+        const dayStart = todayTs + i * DAY;
+        const dayEnd = dayStart + DAY - 1;
+        
+        // Find projects for this day
+        const dayProjects = designerProjects.filter(p => {
+          const isCompleted = p.status === '已完成' || p.status === '已交付';
+          
+          // Completed projects do not count towards future load
+          if (isCompleted) {
+            return false;
+          }
+          
+          // If project has no deadline, assume it's in progress
+          if (!p.deadline) {
+            return true; 
+          }
+          
+          const pEnd = new Date(p.deadline).getTime();
+          
+          // Check if time is valid
+          if (isNaN(pEnd)) {
+            return true; 
+          }
+          
+          // Project only displayed before its deadline
+          if (dayStart > pEnd) {
+            return false; 
+          }
+          
+          // Project start time
+          const pStart = p.createdAt ? new Date(p.createdAt).getTime() : todayTs;
+          
+          // Project overlaps with this day
+          return !(pEnd < dayStart || pStart > dayEnd);
+        });
+
+        let status: 'idle' | 'busy' | 'overload' | 'leave' = 'idle';
+        let color = '#d1fae5'; // Idle - Light Green
+        
+        const projectCount = dayProjects.length;
+        
+        if (projectCount === 0) {
+          status = 'idle';
+          color = '#d1fae5'; 
+        } else if (projectCount >= 3) {
+          status = 'overload';
+          color = '#fecaca'; // Overload - Light Red
+        } else {
+          status = 'busy';
+          color = '#bfdbfe'; // Busy - Light Blue
+        }
+
+        workloadByDesigner[designerName].push({
+          name: `${designerName}-${i}`,
+          value: [yIndex, dayStart, dayEnd, designerName, status, projectCount, dayProjects.map(p => p.name)],
+          itemStyle: { color }
+        });
+      }
+    });
+
+    // Merge all data
+    const data = Object.values(workloadByDesigner).flat();
+
+    const option = {
+      backgroundColor: '#fff',
+      title: {
+        text: this.workloadGanttScale === 'week' ? '未来7天工作状态' : '未来30天工作状态',
+        subtext: '🟢空闲  🔵忙碌  🔴超负荷',
+        left: 'center',
+        textStyle: { fontSize: 14, color: '#374151', fontWeight: 600 },
+        subtextStyle: { fontSize: 12, color: '#6b7280' }
+      },
+      tooltip: {
+        formatter: (params: any) => {
+          const [yIndex, start, end, name, status, projectCount, projectNames = []] = params.value;
+          const startDate = new Date(start);
+          const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
+          
+          let statusText = '';
+          let statusColor = '';
+          let statusBadge = '';
+          
+          if (status === 'leave') {
+            statusText = '请假';
+            statusColor = '#6b7280';
+            statusBadge = '<span style="background:#e5e7eb;color:#374151;padding:2px 8px;border-radius:4px;font-size:11px;">请假</span>';
+          } else if (projectCount === 0) {
+            statusText = '空闲';
+            statusColor = '#10b981';
+            statusBadge = '<span style="background:#d1fae5;color:#059669;padding:2px 8px;border-radius:4px;font-size:11px;">🟢 空闲</span>';
+          } else if (projectCount >= 3) {
+            statusText = '超负荷';
+            statusColor = '#dc2626';
+            statusBadge = '<span style="background:#fecaca;color:#dc2626;padding:2px 8px;border-radius:4px;font-size:11px;">🔴 超负荷</span>';
+          } else {
+            statusText = '忙碌';
+            statusColor = '#3b82f6';
+            statusBadge = '<span style="background:#bfdbfe;color:#1d4ed8;padding:2px 8px;border-radius:4px;font-size:11px;">🔵 忙碌</span>';
+          }
+          
+          let projectListHtml = '';
+          if (projectNames && projectNames.length > 0) {
+            projectListHtml = `
+              <div style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed #e5e7eb;">
+                <div style="font-size: 12px; color: #6b7280; margin-bottom: 4px;">项目列表:</div>
+                ${projectNames.slice(0, 5).map((pName: string, idx: number) => 
+                  `<div style="font-size: 12px; color: #374151; margin-left: 8px;">
+                    ${idx + 1}. ${pName.length > 20 ? pName.substring(0, 20) + '...' : pName}
+                  </div>`
+                ).join('')}
+                ${projectNames.length > 5 ? `<div style="font-size: 12px; color: #6b7280; margin-left: 8px;">...及${projectNames.length - 5}个其他项目</div>` : ''}
+              </div>
+            `;
+          }
+          
+          return `<div style="padding: 12px; min-width: 220px;">
+                    <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
+                      <strong style="font-size: 15px; color: #1f2937;">${name}</strong>
+                      ${statusBadge}
+                    </div>
+                    <div style="color: #6b7280; font-size: 13px;">
+                      📅 ${startDate.getMonth() + 1}月${startDate.getDate()}日 ${weekDays[startDate.getDay()]}<br/>
+                      📊 项目数量: <span style="font-weight: 600; color: ${statusColor};">${projectCount}个</span>
+                    </div>
+                    ${projectListHtml}
+                    <div style="margin-top: 10px; padding-top: 8px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 11px; text-align: center;">
+                      💡 点击查看设计师详细信息
+                    </div>
+                  </div>`;
+        }
+      },
+      grid: {
+        left: 100,
+        right: 50,
+        top: 60,
+        bottom: 60
+      },
+      xAxis: {
+        type: 'time',
+        min: xMin,
+        max: xMax,
+        boundaryGap: false,
+        axisLine: { lineStyle: { color: '#e5e7eb' } },
+        axisLabel: {
+          color: '#6b7280',
+          formatter: xLabelFormatter,
+          interval: 0,
+          rotate: this.workloadGanttScale === 'week' ? 0 : 45,
+          showMinLabel: true,
+          showMaxLabel: true
+        },
+        axisTick: {
+          alignWithLabel: true,
+          interval: 0
+        },
+        splitLine: { 
+          show: true,
+          lineStyle: { color: '#f1f5f9' }
+        },
+        splitNumber: xSplitNumber,
+        minInterval: DAY
+      },
+      yAxis: {
+        type: 'category',
+        data: sortedDesigners,
+        inverse: true,
+        axisLabel: { 
+          color: '#374151', 
+          margin: 8,
+          fontSize: 13,
+          fontWeight: 500,
+          formatter: (value: string) => {
+            const totalProjects = designerTotalLoad[value] || 0;
+            const icon = totalProjects >= 3 ? '🔥' : totalProjects >= 2 ? '⚡' : totalProjects >= 1 ? '✓' : '○';
+            return `${icon} ${value} (${totalProjects})`;
+          }
+        },
+        axisTick: { show: false },
+        axisLine: { lineStyle: { color: '#e5e7eb' } }
+      },
+      series: [
+        {
+          type: 'custom',
+          name: '工作负载',
+          renderItem: (params: any, api: any) => {
+            const categoryIndex = api.value(0);
+            const start = api.coord([api.value(1), categoryIndex]);
+            const end = api.coord([api.value(2), categoryIndex]);
+            const height = api.size([0, 1])[1] * 0.6;
+            const rectShape = echarts.graphic.clipRectByRect({
+              x: start[0],
+              y: start[1] - height / 2,
+              width: Math.max(end[0] - start[0], 2),
+              height
+            }, {
+              x: params.coordSys.x,
+              y: params.coordSys.y,
+              width: params.coordSys.width,
+              height: params.coordSys.height
+            });
+            return rectShape ? { 
+              type: 'rect', 
+              shape: rectShape, 
+              style: api.style() 
+            } : undefined;
+          },
+          encode: { x: [1, 2], y: 0 },
+          data,
+          z: 2
+        }
+      ]
+    } as any;
+
+    this.workloadGanttChart.setOption(option, true);
+  }
+}

+ 76 - 1120
src/app/pages/team-leader/dashboard/dashboard.html

@@ -17,52 +17,15 @@
 
 <header class="dashboard-header">
   <!-- 核心数据指标卡片(扩展为6个) -->
-  <div class="dashboard-metrics">
-    <div class="metric-card" (click)="filterByStatus('overdue')">
-      <div class="metric-icon warning">⚠️</div>
-      <div class="metric-content">
-        <div class="metric-count">{{ overdueProjects.length }}</div>
-        <div class="metric-label">已延期项目</div>
-      </div>
-    </div>
-    <div class="metric-card" (click)="filterByStatus('dueSoon')">
-      <div class="metric-icon info">⏳</div>
-      <div class="metric-content">
-        <div class="metric-count">{{ dueSoonProjects.length }}</div>
-        <div class="metric-label">临期项目(3天内)</div>
-      </div>
-    </div>
-    <div class="metric-card" (click)="filterByStatus('pendingApproval')">
-      <div class="metric-icon info">📋</div>
-      <div class="metric-content">
-        <div class="metric-count">{{ pendingApprovalProjects.length }}</div>
-        <div class="metric-label">待组长确认项目</div>
-      </div>
-    </div>
-    <div class="metric-card" (click)="filterByStatus('pendingAssignment')">
-      <div class="metric-icon primary">🎯</div>
-      <div class="metric-content">
-        <div class="metric-count">{{ pendingAssignmentProjects.length }}</div>
-        <div class="metric-label">待分配方案项目</div>
-      </div>
-    </div>
-    <!-- 新增:超负荷设计师数量 -->
-    <div class="metric-card">
-      <div class="metric-icon danger">🔥</div>
-      <div class="metric-content">
-        <div class="metric-count">{{ overloadedDesignersCount }}</div>
-        <div class="metric-label">超负荷设计师</div>
-      </div>
-    </div>
-    <!-- 新增:平均负载率 -->
-    <div class="metric-card">
-      <div class="metric-icon success">📊</div>
-      <div class="metric-content">
-        <div class="metric-count">{{ averageWorkloadRate.toFixed(0) }}%</div>
-        <div class="metric-label">平均负载率</div>
-      </div>
-    </div>
-  </div>
+  <app-dashboard-metrics
+    [overdueCount]="overdueProjects.length"
+    [dueSoonCount]="dueSoonProjects.length"
+    [pendingApprovalCount]="pendingApprovalProjects.length"
+    [pendingAssignmentCount]="pendingAssignmentProjects.length"
+    [overloadedDesignersCount]="overloadedDesignersCount"
+    [averageWorkloadRate]="averageWorkloadRate"
+    (filterStatus)="filterByStatus($event)">
+  </app-dashboard-metrics>
 </header>
 
 <main class="dashboard-main">
@@ -78,27 +41,14 @@
     </div>
 
     <!-- 工作量负载概览 -->
-    <div class="workload-gantt-card">
-      <div class="gantt-header">
-        <h3>工作量负载概览</h3>
-        <p class="gantt-subtitle">设计师每日工作状态一目了然</p>
-        <div class="gantt-controls">
-          <div class="scale-switch">
-            <button [class.active]="workloadGanttScale === 'week'" (click)="setWorkloadGanttScale('week')">周视图</button>
-            <button [class.active]="workloadGanttScale === 'month'" (click)="setWorkloadGanttScale('month')">月视图</button>
-          </div>
-          <div class="legend">
-            <span class="legend-item"><span class="dot idle"></span>空闲</span>
-            <span class="legend-item"><span class="dot busy"></span>忙碌</span>
-            <span class="legend-item"><span class="dot overload"></span>超负荷</span>
-            <span class="legend-item"><span class="dot leave"></span>请假</span>
-          </div>
-        </div>
-      </div>
-      <div class="gantt-container" #workloadGanttContainer></div>
-    </div>
+    <app-workload-gantt
+      [designerWorkloadMap]="designerWorkloadMap"
+      [realDesigners]="realDesigners"
+      [filteredProjects]="filteredProjects"
+      (employeeClick)="onEmployeeClick($event)">
+    </app-workload-gantt>
     
-    <!-- 🆕 视图切换按钮(固定在此位置便于切换) -->
+    <!-- 视图切换按钮(固定在此位置便于切换) -->
     <div class="view-toggle-bar">
       <button class="btn-toggle-view" (click)="toggleView()">
         <span class="toggle-icon">{{ showGanttView ? '📋' : '📊' }}</span>
@@ -116,1071 +66,77 @@
     }
 
     @if (!showGanttView) {
-      <div class="section-filters">
-        <div class="search-box">
-          <input type="search" class="input-search" placeholder="搜索项目/设计师/风格关键词" [(ngModel)]="searchTerm" (input)="onSearchChange()" (focus)="onSearchFocus()" (blur)="onSearchBlur()" />
-          @if (showSuggestions) {
-            <div class="suggestion-panel">
-              @if (searchSuggestions.length > 0) {
-                <ul>
-                  @for (suggest of searchSuggestions; track suggest.id) {
-                    <li (mousedown)="selectSuggestion(suggest)">
-                      <div class="line-1">
-                        <span class="name">{{ suggest.name }}</span>
-                        <span class="badge" [class.vip]="suggest.memberType==='vip'">{{ suggest.memberType==='vip' ? 'VIP' : '普通' }}</span>
-                        <span class="urgency" [class]="'u-' + suggest.urgency">{{ getUrgencyLabel(suggest.urgency) }}</span>
-                      </div>
-                      <div class="line-2">
-                        <span class="designer">{{ suggest.designerName || '未分配' }}</span>
-                        <span class="deadline">{{ suggest.deadline | date:'MM-dd' }}</span>
-                      </div>
-                    </li>
-                  }
-                </ul>
-              } @else {
-                <div class="empty">抱歉,没有检索到哦</div>
-              }
-            </div>
-          }
-        </div>
-        <select (change)="filterProjects($event)" class="custom-select">
-          <option value="all">全部项目</option>
-          <option value="soft">软装项目</option>
-          <option value="hard">硬装项目</option>
-        </select>
-        <select (change)="filterByUrgency($event)" class="custom-select">
-          <option value="all">全部紧急程度</option>
-          <option value="high">高</option>
-          <option value="medium">中</option>
-          <option value="low">低</option>
-        </select>
-        <select (change)="onStatusChange($event)" class="custom-select">
-          <option value="all">全部状态</option>
-          <option value="progress">进行中</option>
-          <option value="completed">已完成</option>
-          <option value="overdue">已延期</option>
-          <option value="dueSoon">临期(3天内)</option>
-          <option value="pendingApproval">待确认</option>
-          <option value="pendingAssignment">待分配</option>
-        </select>
-        <select (change)="onDesignerChange($event)" class="custom-select">
-          <option value="all">全部设计师</option>
-          @for (d of designers; track d) {
-            <option [value]="d">{{ d }}</option>
-          }
-        </select>
-        <select (change)="onMemberTypeChange($event)" class="custom-select">
-          <option value="all">全部会员</option>
-          <option value="vip">VIP会员</option>
-          <option value="normal">普通会员</option>
-        </select>
-        <!-- 新增:四大板块筛选 -->
-        <select [(ngModel)]="selectedCorePhase" (change)="onCorePhaseChange($event)" class="custom-select">
-          <option value="all">全部板块</option>
-          @for (core of corePhases; track core.id) {
-            <option [value]="core.id">{{ core.name }}</option>
-          }
-        </select>
-        <!-- 支持数百项目的下拉筛选 -->
-        <select [(ngModel)]="selectedProjectId" (change)="selectProject()" class="custom-select project-selector">
-          <option value="">选择项目</option>
-          @for (project of projects; track project.id) {
-            <option [value]="project.id">{{ project.name }}</option>
-          }
-        </select>
-        <!-- 新增:时间窗快捷筛选按钮组 -->
-        <div class="time-window-buttons">
-          <button [class.active]="selectedTimeWindow === 'all'" (click)="filterByTimeWindow('all')">全部</button>
-          <button [class.active]="selectedTimeWindow === 'today'" (click)="filterByTimeWindow('today')">今天到期</button>
-          <button [class.active]="selectedTimeWindow === 'threeDays'" (click)="filterByTimeWindow('threeDays')">3天内</button>
-          <button [class.active]="selectedTimeWindow === 'sevenDays'" (click)="filterByTimeWindow('sevenDays')">7天内</button>
-        </div>
-      </div>
+      <app-dashboard-filter-bar
+        [projects]="projects"
+        [designers]="designers"
+        [corePhases]="corePhases"
+        [(searchTerm)]="searchTerm"
+        [(selectedType)]="selectedType"
+        [(selectedUrgency)]="selectedUrgency"
+        [(selectedStatus)]="selectedStatus"
+        [(selectedDesigner)]="selectedDesigner"
+        [(selectedMemberType)]="selectedMemberType"
+        [(selectedCorePhase)]="selectedCorePhase"
+        [(selectedProjectId)]="selectedProjectId"
+        [(selectedTimeWindow)]="selectedTimeWindow"
+        (filterChange)="applyFilters()"
+        (viewProject)="viewProjectDetails($event)">
+      </app-dashboard-filter-bar>
       
-      <!-- 项目看板 - 横向展开10个项目阶段 -->
-      <div class="project-kanban">
-        <!-- 新增:公共横向滚动容器,保证表头与表体同步滚动 -->
-        <div class="kanban-scroll">
-          <!-- 阶段标题 -->
-          <div class="kanban-header">
-            @for (core of corePhases; track core.id) {
-              <div class="kanban-column-header">
-                <h3>{{ core.name }}</h3>
-                <span class="stage-count">{{ getProjectCountByCorePhase(core.id) }}</span>
-              </div>
-            }
-          </div>
-          <!-- 项目卡片 -->
-          <div class="kanban-body">
-            @for (core of corePhases; track core.id) {
-              <div class="kanban-column">
-                @for (project of getProjectsByCorePhase(core.id); track project.id) {
-                  <div class="project-card" 
-                       (click)="viewProjectDetailsByPhase(project.id, core.id)"
-                       [class.overdue]="project.isOverdue" 
-                       [class.high-urgency]="project.urgency === 'high'"
-                       [class.due-soon]="project.dueSoon && !project.isOverdue"
-                       [class.pending-approval]="isPendingApproval(project)">
-                    <!-- 待审批徽章 -->
-                    @if (isPendingApproval(project)) {
-                      <div class="approval-badge">
-                        <span class="badge-icon">📋</span>
-                        <span class="badge-text">待审批</span>
-                      </div>
-                    }
-                    <div class="project-card-header">
-                      <h4 (click)="viewProjectDetailsByPhase(project.id, core.id); $event.stopPropagation()" style="cursor: pointer;">{{ project.name }}</h4>
-                      <div class="right-badges">
-                        <span class="member-badge" [class.vip]="project.memberType === 'vip'">{{ project.memberType === 'vip' ? 'VIP' : '普通' }}</span>
-                        <span class="project-urgency" [class]="'urgency-' + project.urgency">{{ getUrgencyLabel(project.urgency) }}</span>
-                      </div>
-                    </div>
-                    <div class="project-card-content">
-                      <p>负责人: {{ project.designerName || '未分配' }}</p>
-                      <p class="deadline">{{ project.isOverdue ? '超期' + project.overdueDays + '天' : (project.dueSoon ? '临期: ' + (project.deadline | date:'MM-dd') : '截止: ' + (project.deadline | date:'MM-dd')) }}</p>
-                    </div>
-                    <div class="project-card-footer">
-                      <button (click)="viewProjectDetailsByPhase(project.id, core.id); $event.stopPropagation()" class="btn-view">查看详情</button>
-                      @if (project.currentStage === 'pendingAssignment') {
-                        <button (click)="openSmartMatch(project); $event.stopPropagation()" class="btn-smart">🤖 智能推荐</button>
-                        <button (click)="quickAssignProject(project.id); $event.stopPropagation()" class="btn-assign">手动分配</button>
-                      }
-                      <!-- 新增:质量评审快捷操作(保留,不影响四大板块分类) -->
-                      @if (project.currentStage === 'review' || project.currentStage === 'delivery') {
-                        <div class="inline-actions">
-                          <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'excellent')">评为优秀</button>
-                          <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'qualified')">评为合格</button>
-                          <button class="btn-secondary" (click)="$event.stopPropagation(); reviewProjectQuality(project.id, 'unqualified')">评为不合格</button>
-                        </div>
-                      }
-                    </div>
-                  </div>
-                }
-                @if (getProjectsByCorePhase(core.id).length === 0) {
-                  <div class="empty-column">
-                    <span class="empty-icon">📦</span>
-                    <p>暂无项目</p>
-                  </div>
-                }
-              </div>
-            }
-          </div>
-        </div>
-      </div>
+      <!-- 项目看板组件 -->
+      <app-project-kanban
+        [corePhases]="corePhases"
+        [projects]="filteredProjects"
+        (viewProject)="viewProjectDetailsByPhase($event.projectId, $event.phaseId)"
+        (openSmartMatch)="openSmartMatch($event)"
+        (assignProject)="quickAssignProject($event)"
+        (reviewProject)="reviewProjectQuality($event.projectId, $event.rating)">
+      </app-project-kanban>
     }
   </section>
 
-  <!-- 🆕 待办任务双栏布局(待办问题 + 紧急事件) -->
-  <section class="todo-section todo-section-dual">
-    <div class="section-header">
-      <h2>待办事项</h2>
-      <button 
-        class="btn-refresh" 
-        (click)="refreshTodoTasks()"
-        [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">
-        <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 (!loadingTodoTasks && todoTaskError) {
-      <div class="error-state">
-        <svg viewBox="0 0 24 24" width="48" height="48" fill="#ef4444">
-          <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>
-        <p>{{ todoTaskError }}</p>
-        <button class="btn-retry" (click)="refreshTodoTasks()">重试</button>
-      </div>
-    }
-    
-    <!-- 空状态 -->
-    @if (!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length === 0) {
-      <div class="empty-state">
-        <svg viewBox="0 0 24 24" width="64" height="64" fill="#d1d5db">
-          <path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
-        </svg>
-        <p>暂无待办任务</p>
-        <p class="hint">所有项目问题都已处理完毕 🎉</p>
-      </div>
-    }
-    
-    <!-- 待办任务列表 -->
-    @if (!loadingTodoTasks && !todoTaskError && todoTasksFromIssues.length > 0) {
-      <div class="todo-list-compact">
-        @for (task of todoTasksFromIssues; track task.id) {
-          <div class="todo-item-compact" [attr.data-priority]="task.priority">
-            <!-- 左侧优先级色条 -->
-            <div class="priority-indicator" [attr.data-priority]="task.priority"></div>
-            
-            <!-- 任务内容 -->
-            <div class="task-content">
-              <!-- 标题行 -->
-              <div class="task-header">
-                <span class="task-title">{{ task.title }}</span>
-                <div class="task-badges">
-                  <span class="badge badge-priority" [attr.data-priority]="task.priority">
-                    {{ getPriorityConfig(task.priority).label }}
-                  </span>
-                  <span class="badge badge-type">{{ getIssueTypeLabel(task.type) }}</span>
-                </div>
-              </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>
-                  项目: {{ task.projectName }}
-                  @if (task.relatedSpace) {
-                    | {{ task.relatedSpace }}
-                  }
-                  @if (task.relatedStage) {
-                    | {{ task.relatedStage }}
-                  }
-                </span>
-              </div>
-              
-              <!-- 底部信息行 -->
-              <div class="task-footer">
-                <span class="time-info" [title]="formatExactTime(task.createdAt)">
-                  <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>
-                  创建于 {{ formatRelativeTime(task.createdAt) }}
-                </span>
-                
-                <span class="assignee-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>
-                  指派给: {{ task.assigneeName }}
-                </span>
-              </div>
-            </div>
-            
-            <!-- 右侧操作按钮 -->
-            <div class="task-actions">
-              <button 
-                class="btn-action btn-view" 
-                (click)="navigateToIssue(task)"
-                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>
-              <button 
-                class="btn-action btn-mark-read" 
-                (click)="markAsRead(task)"
-                title="标记已读">
-                <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
-                  <path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
-                </svg>
-                标记已读
-              </button>
-            </div>
-          </div>
-        }
-      </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 && urgentEvents.length > 0) {
-          <div class="tag-filter-bar">
-            <button 
-              class="tag-button"
-              [class.active]="urgentEventTagFilter === 'all'"
-              (click)="filterUrgentEventsByTag('all')"
-              title="显示所有紧急事件"
-            >
-              <span class="tag-icon">📋</span>
-              <span class="tag-label">全部</span>
-              <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'"
-              (click)="filterUrgentEventsByTag('phase')"
-              title="工作阶段"
-            >
-              <span class="tag-icon">🔧</span>
-              <span class="tag-label">工作阶段</span>
-              <span class="tag-count">{{ getTagCount('phase') }}</span>
-            </button>
-            
-            <button 
-              class="tag-button"
-              [class.active]="urgentEventTagFilter === 'review'"
-              (click)="filterUrgentEventsByTag('review')"
-              title="小图截止"
-            >
-              <span class="tag-icon">📐</span>
-              <span class="tag-label">小图截止</span>
-              <span class="tag-count">{{ getTagCount('review') }}</span>
-            </button>
-            
-            <button 
-              class="tag-button"
-              [class.active]="urgentEventTagFilter === 'delivery'"
-              (click)="filterUrgentEventsByTag('delivery')"
-              title="交付延期"
-            >
-              <span class="tag-icon">📦</span>
-              <span class="tag-label">交付延期</span>
-              <span class="tag-count">{{ getTagCount('delivery') }}</span>
-            </button>
-          </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 && 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"/>
-            </svg>
-            <p>该筛选条件下暂无事件</p>
-            <p class="hint">尝试切换其他标签</p>
-          </div>
-        }
-        
-        <!-- 紧急事件列表 -->
-        @if (!loadingUrgentEvents && filteredUrgentEventsList.length > 0) {
-          <div class="todo-list-compact urgent-list">
-            @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>
-                
-                <!-- 事件内容 -->
-                <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 }} }
-                      @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>
-                  
-                  <!-- 描述 -->
-                  <div class="task-description">
-                    {{ event.description }}
-                  </div>
-                @if (event.followUpNeeded) {
-                  <div class="followup-tip">
-                    客户反馈待跟进 · 请及时追踪
-                  </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-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)"
-                    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>
+  <!-- 待办任务双栏布局(待办问题 + 紧急事件) -->
+  <app-todo-section
+    [todoTasksFromIssues]="todoTasksFromIssues"
+    [loadingTodoTasks]="loadingTodoTasks"
+    [todoTaskError]="todoTaskError"
+    [urgentEvents]="urgentEvents"
+    [loadingUrgentEvents]="loadingUrgentEvents"
+    (refresh)="refreshTodoTasks()"
+    (navigateToIssue)="navigateToIssue($event)"
+    (markAsRead)="markAsRead($event)"
+    (projectClick)="onProjectClick($event)"
+    (confirmEventOnTime)="confirmEventOnTime($event)"
+    (markEventAsStagnant)="markEventAsStagnant($event)"
+    (resolveUrgentEvent)="resolveUrgentEvent($event)"
+    (createTodoFromEvent)="createTodoFromEvent($event)">
+  </app-todo-section>
 
-  <!-- 超期项目提醒 -->
-  @if (showAlert && overdueProjects.length > 0) {
-    <div class="overdue-alert">
-      <div class="alert-content">
-        <h3>⚠️ 超期项目提醒</h3>
-        <ul>
-          @for (project of overdueProjects.slice(0, 3); track $index) {
-            <li>
-              {{ project.name }} ({{ project.designerName }} 负责) - 超期{{ project.overdueDays }}天
-            </li>
-          }
-        </ul>
-        <div class="alert-actions">
-          <button (click)="viewAllOverdueProjects()" class="btn-view-all">查看全部</button>
-          <button (click)="closeAlert()" class="btn-close">关闭</button>
-        </div>
-      </div>
-    </div>
-  }
-  @if (urgentPinnedProjects && urgentPinnedProjects.length > 0) {
-    <div class="urgent-pinned">
-      <div class="pinned-title">紧急任务固定区(超期 + 高紧急)</div>
-      <div class="pinned-list">
-        @for (p of urgentPinnedProjects.slice(0, 3); track $index) {
-          <div class="pinned-item" (click)="filterByStatus('overdue')">
-            <span class="dot dot-high"></span>
-            <span class="name">{{ p.name }}</span>
-            <span class="meta">{{ p.designerName || '未分配' }} · 超期{{ p.overdueDays }}天</span>
-          </div>
-        }
-        @if (urgentPinnedProjects.length > 3) {
-          <button class="btn-view-all" (click)="viewAllOverdueProjects()">更多…</button>
-        }
-      </div>
-    </div>
-  }
+  <!-- 超期/紧急项目提醒组件 -->
+  <app-dashboard-alerts
+    [showAlert]="showAlert"
+    [overdueProjects]="overdueProjects"
+    [urgentPinnedProjects]="urgentPinnedProjects"
+    (viewAllOverdue)="viewAllOverdueProjects()"
+    (closeAlert)="closeAlert()"
+    (filterStatus)="filterByStatus($event)">
+  </app-dashboard-alerts>
 </main>
 
 <!-- 员工详情面板组件 -->
 <app-employee-detail-panel
   [visible]="showEmployeeDetailPanel"
-  [employeeDetail]="selectedEmployeeDetail"
+  [employeeName]="selectedEmployeeName"
+  [projects]="selectedEmployeeProjects"
   (close)="closeEmployeeDetailPanel()"
-  (calendarMonthChange)="changeEmployeeCalendarMonth($event)"
-  (calendarDayClick)="onCalendarDayClick($event)"
-  (projectClick)="navigateToProjectFromPanel($event)"
-  (refreshSurvey)="refreshEmployeeSurvey()">
+  (projectClick)="navigateToProjectFromPanel($event)">
 </app-employee-detail-panel>
 
-<!-- 以下代码已由 EmployeeDetailPanelComponent 组件替代,日历项目列表弹窗也已集成到组件内部 -->
-<!--
-<!-- 员工详情面板(旧代码已废弃) -->
-@if (showEmployeeDetailPanel && selectedEmployeeDetail) {
-  <div class="employee-detail-overlay" (click)="closeEmployeeDetailPanel()">
-    <div class="employee-detail-panel" (click)="$event.stopPropagation()">
-      <!-- 面板头部 -->
-      <div class="panel-header">
-        <h3 class="panel-title">
-          <svg class="icon-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
-            <circle cx="12" cy="7" r="4"></circle>
-          </svg>
-          {{ selectedEmployeeDetail.name }} 详情
-        </h3>
-        <button class="btn-close" (click)="closeEmployeeDetailPanel()">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <line x1="18" y1="6" x2="6" y2="18"></line>
-            <line x1="6" y1="6" x2="18" y2="18"></line>
-          </svg>
-        </button>
-      </div>
-
-      <!-- 面板内容 -->
-      <div class="panel-content">
-        <!-- 负载概况栏 -->
-        <div class="section workload-section">
-          <div class="section-header">
-            <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
-              <line x1="9" y1="9" x2="15" y2="9"></line>
-              <line x1="9" y1="15" x2="15" y2="15"></line>
-            </svg>
-            <h4>负载概况</h4>
-          </div>
-          <div class="workload-info">
-            <div class="workload-stat">
-              <span class="stat-label">当前负责项目数:</span>
-              <span class="stat-value" [class]="selectedEmployeeDetail.currentProjects >= 3 ? 'high-workload' : 'normal-workload'">
-                {{ selectedEmployeeDetail.currentProjects }} 个
-              </span>
-            </div>
-            @if (selectedEmployeeDetail.projectData.length > 0) {
-              <div class="project-list">
-                <span class="project-label">核心项目:</span>
-                <div class="project-tags">
-                  @for (project of selectedEmployeeDetail.projectData; track project.id) {
-                    <span class="project-tag clickable" 
-                          (click)="navigateToProjectFromPanel(project.id)"
-                          title="点击查看项目详情">
-                      {{ project.name }}
-                      <svg class="icon-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                        <path d="M7 17L17 7M17 7H7M17 7V17"/>
-                      </svg>
-                    </span>
-                  }
-                  @if (selectedEmployeeDetail.currentProjects > selectedEmployeeDetail.projectData.length) {
-                    <span class="project-tag more">+{{ selectedEmployeeDetail.currentProjects - selectedEmployeeDetail.projectData.length }}</span>
-                  }
-                </div>
-              </div>
-            }
-          </div>
-        </div>
-
-        <!-- 负载详细日历 -->
-        <div class="section calendar-section">
-          <div class="section-header">
-            <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
-              <line x1="16" y1="2" x2="16" y2="6"></line>
-              <line x1="8" y1="2" x2="8" y2="6"></line>
-              <line x1="3" y1="10" x2="21" y2="10"></line>
-            </svg>
-            <h4>负载详细日历</h4>
-          </div>
-          
-          @if (selectedEmployeeDetail.calendarData) {
-            <div class="employee-calendar">
-              <!-- 月份标题 -->
-              <div class="calendar-month-header">
-                <button class="btn-prev-month" 
-                        (click)="changeEmployeeCalendarMonth(-1)"
-                        title="上月">
-                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                    <polyline points="15 18 9 12 15 6"></polyline>
-                  </svg>
-                </button>
-                <span class="month-title">
-                  {{ selectedEmployeeDetail.calendarData.currentMonth | date:'yyyy年M月' }}
-                </span>
-                <button class="btn-next-month" 
-                        (click)="changeEmployeeCalendarMonth(1)"
-                        title="下月">
-                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                    <polyline points="9 18 15 12 9 6"></polyline>
-                  </svg>
-                </button>
-              </div>
-              
-              <!-- 星期标题 -->
-              <div class="calendar-weekdays">
-                <div class="weekday">日</div>
-                <div class="weekday">一</div>
-                <div class="weekday">二</div>
-                <div class="weekday">三</div>
-                <div class="weekday">四</div>
-                <div class="weekday">五</div>
-                <div class="weekday">六</div>
-              </div>
-              
-              <!-- 日历网格 -->
-              <div class="calendar-grid">
-                @for (day of selectedEmployeeDetail.calendarData.days; track day.date.getTime()) {
-                  <div class="calendar-day"
-                       [class.today]="day.isToday"
-                       [class.other-month]="!day.isCurrentMonth"
-                       [class.has-projects]="day.projectCount > 0"
-                       [class.clickable]="day.projectCount > 0 && day.isCurrentMonth"
-                       (click)="onCalendarDayClick(day)">
-                    <div class="day-number">{{ day.date.getDate() }}</div>
-                    @if (day.projectCount > 0) {
-                      <div class="day-badge" [class.high-load]="day.projectCount >= 2">
-                        {{ day.projectCount }}个项目
-                      </div>
-                    }
-                  </div>
-                }
-              </div>
-              
-              <!-- 图例 -->
-              <div class="calendar-legend">
-                <div class="legend-item">
-                  <span class="legend-dot today-dot"></span>
-                  <span class="legend-text">今天</span>
-                </div>
-                <div class="legend-item">
-                  <span class="legend-dot project-dot"></span>
-                  <span class="legend-text">有项目</span>
-                </div>
-                <div class="legend-item">
-                  <span class="legend-dot high-dot"></span>
-                  <span class="legend-text">高负载</span>
-                </div>
-              </div>
-            </div>
-          }
-        </div>
-
-        <!-- 请假明细栏 -->
-        <div class="section leave-section">
-          <div class="section-header">
-            <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
-              <line x1="16" y1="2" x2="16" y2="6"></line>
-              <line x1="8" y1="2" x2="8" y2="6"></line>
-              <line x1="3" y1="10" x2="21" y2="10"></line>
-            </svg>
-            <h4>请假明细(未来7天)</h4>
-          </div>
-          <div class="leave-table">
-            @if (selectedEmployeeDetail.leaveRecords.length > 0) {
-              <table>
-                <thead>
-                  <tr>
-                    <th>日期</th>
-                    <th>状态</th>
-                    <th>备注</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  @for (record of selectedEmployeeDetail.leaveRecords; track record.id) {
-                    <tr [class]="record.isLeave ? 'leave-day' : 'work-day'">
-                      <td>{{ record.date | date:'M月d日' }}</td>
-                      <td>
-                        <span class="status-badge" [class]="record.isLeave ? 'leave' : 'work'">
-                          {{ record.isLeave ? '请假' : '正常' }}
-                        </span>
-                      </td>
-                      <td>{{ record.isLeave ? getLeaveTypeText(record.leaveType) : '-' }}</td>
-                    </tr>
-                  }
-                </tbody>
-              </table>
-            } @else {
-              <div class="no-leave">
-                <svg class="no-data-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                  <circle cx="12" cy="12" r="10"></circle>
-                  <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
-                  <line x1="9" y1="9" x2="9.01" y2="9"></line>
-                  <line x1="15" y1="9" x2="15.01" y2="9"></line>
-                </svg>
-                <p>未来7天无请假安排</p>
-              </div>
-            }
-          </div>
-        </div>
-
-        <!-- 红色标记说明 -->
-        <div class="section explanation-section">
-          <div class="section-header">
-            <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <circle cx="12" cy="12" r="10"></circle>
-              <line x1="12" y1="8" x2="12" y2="12"></line>
-              <line x1="12" y1="16" x2="12.01" y2="16"></line>
-            </svg>
-            <h4>红色标记说明</h4>
-          </div>
-          <div class="explanation-content">
-            <p class="explanation-text">{{ selectedEmployeeDetail.redMarkExplanation }}</p>
-          </div>
-        </div>
-        
-        <!-- 新增:能力问卷 -->
-        <div class="section survey-section">
-          <div class="section-header">
-            <svg class="section-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <path d="M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3"/>
-            </svg>
-            <h4>能力问卷</h4>
-            <button 
-              class="btn-refresh-survey" 
-              (click)="refreshEmployeeSurvey()"
-              [disabled]="refreshingSurvey"
-              title="刷新问卷状态">
-              <svg viewBox="0 0 24 24" width="16" height="16" [class.rotating]="refreshingSurvey">
-                <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>
-          
-          @if (selectedEmployeeDetail.surveyCompleted && selectedEmployeeDetail.surveyData) {
-            <div class="survey-content">
-              <div class="survey-status completed">
-                <svg viewBox="0 0 24 24" width="20" height="20" fill="#34c759">
-                  <path d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z"/>
-                </svg>
-                <span>已完成问卷</span>
-                <span class="survey-time">
-                  {{ selectedEmployeeDetail.surveyData.createdAt | date:'yyyy-MM-dd HH:mm' }}
-                </span>
-              </div>
-              
-              <!-- 能力画像摘要 -->
-              @if (!showFullSurvey) {
-                <div class="capability-summary">
-                  <h5>您的能力画像</h5>
-                  @if (getCapabilitySummary(selectedEmployeeDetail.surveyData.answers); as summary) {
-                    <div class="summary-grid">
-                      <div class="summary-item">
-                        <span class="label">擅长风格:</span>
-                        <span class="value">{{ summary.styles }}</span>
-                      </div>
-                      <div class="summary-item">
-                        <span class="label">擅长空间:</span>
-                        <span class="value">{{ summary.spaces }}</span>
-                      </div>
-                      <div class="summary-item">
-                        <span class="label">技术优势:</span>
-                        <span class="value">{{ summary.advantages }}</span>
-                      </div>
-                      <div class="summary-item">
-                        <span class="label">项目难度:</span>
-                        <span class="value">{{ summary.difficulty }}</span>
-                      </div>
-                      <div class="summary-item">
-                        <span class="label">周承接量:</span>
-                        <span class="value">{{ summary.capacity }}</span>
-                      </div>
-                      <div class="summary-item">
-                        <span class="label">紧急订单:</span>
-                        <span class="value">
-                          {{ summary.urgent }}
-                          @if (summary.urgentLimit) {
-                            <span class="limit-hint">(每月不超过{{summary.urgentLimit}}次)</span>
-                          }
-                        </span>
-                      </div>
-                      <div class="summary-item">
-                        <span class="label">进度同步:</span>
-                        <span class="value">{{ summary.feedback }}</span>
-                      </div>
-                      <div class="summary-item">
-                        <span class="label">沟通方式:</span>
-                        <span class="value">{{ summary.communication }}</span>
-                      </div>
-                    </div>
-                  }
-                  
-                  <button class="btn-view-full" (click)="toggleSurveyDisplay()">
-                    <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
-                      <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
-                    </svg>
-                    查看完整问卷(共 {{ selectedEmployeeDetail.surveyData.answers.length }} 道题)
-                  </button>
-                </div>
-              }
-              
-              <!-- 完整问卷答案 -->
-              @if (showFullSurvey) {
-                <div class="survey-answers">
-                  <h5>完整问卷答案(共 {{ selectedEmployeeDetail.surveyData.answers.length }} 道题):</h5>
-                  @for (answer of selectedEmployeeDetail.surveyData.answers; track $index) {
-                    <div class="answer-item">
-                      <div class="question-text">
-                        <strong>Q{{$index + 1}}:</strong> {{ answer.question }}
-                      </div>
-                      <div class="answer-text">
-                        @if (!answer.answer) {
-                          <span class="answer-tag empty">未填写(选填)</span>
-                        } @else if (answer.type === 'single' || answer.type === 'text' || answer.type === 'textarea' || answer.type === 'number') {
-                          <span class="answer-tag single">{{ answer.answer }}</span>
-                        } @else if (answer.type === 'multiple') {
-                          @if (Array.isArray(answer.answer)) {
-                            @for (opt of answer.answer; track opt) {
-                              <span class="answer-tag multiple">{{ opt }}</span>
-                            }
-                          } @else {
-                            <span class="answer-tag single">{{ answer.answer }}</span>
-                          }
-                        } @else if (answer.type === 'scale') {
-                          <div class="answer-scale">
-                            <div class="scale-bar">
-                              <div class="scale-fill" [style.width.%]="(answer.answer / 10) * 100">
-                                <span>{{ answer.answer }} / 10</span>
-                              </div>
-                            </div>
-                          </div>
-                        } @else {
-                          <span class="answer-tag single">{{ answer.answer }}</span>
-                        }
-                      </div>
-                    </div>
-                  }
-                  
-                  <button class="btn-collapse" (click)="toggleSurveyDisplay()">
-                    <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
-                      <path d="M19 13H5v-2h14v2z"/>
-                    </svg>
-                    收起详情
-                  </button>
-                </div>
-              }
-            </div>
-          } @else {
-            <div class="survey-empty">
-              <svg class="no-data-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                <circle cx="12" cy="12" r="10"></circle>
-                <path d="M8 12h8M12 8v8"/>
-              </svg>
-              <p>该员工尚未完成能力问卷</p>
-            </div>
-          }
-        </div>
-      </div>
-    </div>
-  </div>
-}
-
-<!-- 日历项目列表弹窗 -->
-@if (showCalendarProjectList) {
-  <div class="calendar-project-modal-overlay" (click)="closeCalendarProjectList()">
-    <div class="calendar-project-modal" (click)="$event.stopPropagation()">
-      <div class="modal-header">
-        <h3>
-          <svg class="header-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M9 11l3 3L22 4"></path>
-            <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
-          </svg>
-          {{ selectedDate | date:'M月d日' }} 的项目
-        </h3>
-        <button class="btn-close" (click)="closeCalendarProjectList()">
-          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <line x1="18" y1="6" x2="6" y2="18"></line>
-            <line x1="6" y1="6" x2="18" y2="18"></line>
-          </svg>
-        </button>
-      </div>
-      
-      <div class="modal-body">
-        <div class="project-count-info">
-          共 <strong>{{ selectedDayProjects.length }}</strong> 个项目
-        </div>
-        
-        <div class="project-list">
-          @for (project of selectedDayProjects; track project.id) {
-            <div class="project-item" (click)="navigateToProjectFromPanel(project.id); closeCalendarProjectList()">
-              <div class="project-info">
-                <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>
-                </svg>
-                <div class="project-details">
-                  <h4 class="project-name">{{ project.name }}</h4>
-                  @if (project.deadline) {
-                    <p class="project-deadline">
-                      截止日期: {{ project.deadline | date:'yyyy-MM-dd' }}
-                    </p>
-                  }
-                </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>
-          }
-        </div>
-      </div>
-    </div>
-  </div>
-}
--->
-
 <!-- 智能推荐弹窗 -->
-@if (showSmartMatch) {
-  <div class="smart-match-modal">
-    <div class="modal-backdrop" (click)="closeSmartMatch()"></div>
-    <div class="modal-content">
-      <div class="modal-header">
-        <h3>🤖 智能推荐设计师</h3>
-        <button class="btn-close" (click)="closeSmartMatch()">×</button>
-      </div>
-      
-      @if (selectedProject) {
-        <div class="project-info">
-          <h4>{{ selectedProject.name }}</h4>
-          <div class="tags">
-            <span class="tag">{{ selectedProject.type === 'hard' ? '硬装' : '软装' }}</span>
-            <span class="tag">{{ selectedProject.memberType === 'vip' ? 'VIP' : '普通' }}</span>
-            <span class="tag urgency u-{{ selectedProject.urgency }}">
-              {{ getUrgencyLabel(selectedProject.urgency) }}
-            </span>
-          </div>
-        </div>
-      }
-      
-      <div class="recommendations-list">
-        @for (rec of recommendations; track rec.designer.id; let i = $index) {
-          <div class="rec-card">
-            <div class="rank" [class.gold]="i===0" [class.silver]="i===1" [class.bronze]="i===2">
-              {{ i + 1 }}
-            </div>
-            <div class="designer-info">
-              <h4>{{ rec.designer.name }}</h4>
-              <div class="match-score-bar">
-                <div class="score-fill" [style.width.%]="rec.matchScore">
-                  <span>{{ rec.matchScore }}分</span>
-                </div>
-              </div>
-            </div>
-            <div class="details">
-              <p><strong>擅长:</strong>{{ rec.designer.tags.expertise.styles.join('、') || '暂无标签' }}</p>
-              <p><strong>负载:</strong>{{ rec.loadRate.toFixed(0) }}% ({{ rec.currentProjects }}个项目)</p>
-              <p><strong>评分:</strong>⭐ {{ rec.designer.tags.history.avgRating || '暂无' }}</p>
-              <p class="reason"><strong>推荐理由:</strong>{{ rec.reason }}</p>
-            </div>
-            <button class="btn-assign" (click)="assignToDesigner(rec.designer.id)">
-              分配给TA
-            </button>
-          </div>
-        }
-        
-        @if (recommendations.length === 0) {
-          <div class="empty">
-            <p>未找到合适的设计师</p>
-            <p>您可以手动分配或调整项目参数</p>
-          </div>
-        }
-      </div>
-    </div>
-  </div>
-}
+<app-smart-match-modal
+  [visible]="showSmartMatch"
+  [selectedProject]="selectedProject"
+  [recommendations]="recommendations"
+  (close)="closeSmartMatch()"
+  (assign)="assignToDesigner($event)">
+</app-smart-match-modal>

+ 134 - 0
src/app/pages/team-leader/dashboard/dashboard.model.ts

@@ -0,0 +1,134 @@
+import { IssueStatus, IssuePriority, IssueType } from '../../../../modules/project/services/project-issue.service';
+
+export interface ProjectStage {
+  id: string;
+  name: string;
+  order: number;
+}
+
+export interface ProjectPhase {
+  name: string;
+  percentage: number;
+  startPercentage: number;
+  isCompleted: boolean;
+  isCurrent: boolean;
+}
+
+export interface Project {
+  id: string;
+  name: string;
+  type: 'soft' | 'hard';
+  memberType: 'vip' | 'normal';
+  designerName: string;
+  status: string;
+  expectedEndDate: Date; // TODO: 兼容旧字段,后续可移除
+  deadline: Date; // 真实截止时间字段
+  createdAt?: Date; // 真实开始时间字段(可选)
+  isOverdue: boolean;
+  overdueDays: number;
+  dueSoon: boolean;
+  urgency: 'high' | 'medium' | 'low';
+  phases: ProjectPhase[];
+  currentStage: string; // 新增:当前项目阶段
+  // 新增:质量评级
+  qualityRating?: 'excellent' | 'qualified' | 'unqualified' | 'pending';
+  lastCustomerFeedback?: string;
+  // 预构建的搜索索引,减少重复 toLowerCase 与拼接
+  searchIndex?: string;
+  // Optional additional fields that might be used
+  designerId?: string;
+  data?: any;
+  contact?: any;
+  space?: string;
+  customer?: string;
+  demoday?: Date;
+}
+
+// 新增:从问题板块映射的待办任务
+export interface TodoTaskFromIssue {
+  id: string;
+  title: string;
+  description?: string;
+  priority: IssuePriority;
+  type: IssueType;
+  status: IssueStatus;
+  projectId: string;
+  projectName: string;
+  relatedSpace?: string;
+  relatedStage?: string;
+  assigneeName?: string;
+  creatorName?: string;
+  createdAt: Date;
+  updatedAt: Date;
+  dueDate?: Date;
+  tags?: string[];
+}
+
+/**
+ * 🆕 紧急事件接口
+ * 从项目时间轴自动计算,表示截止时间到了但未完成的事件
+ */
+export interface UrgentEvent {
+  id: string;
+  title: string;
+  description: string;
+  eventType: 'review' | 'delivery' | 'phase_deadline' | 'customer_alert'; // 事件类型
+  phaseName?: string; // 阶段名称(如果是阶段截止)
+  deadline: Date; // 截止时间
+  projectId: string;
+  projectName: string;
+  designerName?: string;
+  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;
+}
+
+// 员工请假记录接口
+export interface LeaveRecord {
+  id: string;
+  employeeName: string;
+  date: string; // YYYY-MM-DD 格式
+  isLeave: boolean;
+  leaveType?: 'sick' | 'personal' | 'annual' | 'other'; // 请假类型
+  reason?: string; // 请假原因
+}
+
+// 员工日历数据接口
+export interface EmployeeCalendarData {
+  currentMonth: Date;
+  days: EmployeeCalendarDay[];
+}
+
+// 日历日期数据
+export interface EmployeeCalendarDay {
+  date: Date;
+  projectCount: number; // 当天项目数量
+  projects: Array<{ id: string; name: string; deadline?: Date }>; // 项目列表
+  isToday: boolean;
+  isCurrentMonth: boolean;
+}
+
+// 员工详情面板数据接口
+export interface EmployeeDetail {
+  name: string;
+  currentProjects: number; // 当前负责项目数
+  projectNames: string[]; // 项目名称列表(用于显示)
+  projectData: Array<{ id: string; name: string }>; // 项目数据(包含ID和名称,用于跳转)
+  leaveRecords: LeaveRecord[]; // 未来7天请假记录
+  redMarkExplanation: string; // 红色标记说明
+  calendarData?: EmployeeCalendarData; // 负载日历数据
+  // 新增:问卷相关
+  surveyCompleted?: boolean; // 是否完成问卷
+  surveyData?: any; // 问卷答案数据
+  profileId?: string; // Profile ID
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 110 - 1447
src/app/pages/team-leader/dashboard/dashboard.ts


+ 134 - 0
src/app/pages/team-leader/dashboard/interfaces.ts

@@ -0,0 +1,134 @@
+
+import { IssueStatus, IssuePriority, IssueType } from '../../../../modules/project/services/project-issue.service';
+
+export interface ProjectStage {
+  id: string;
+  name: string;
+  order: number;
+}
+
+export interface ProjectPhase {
+  name: string;
+  percentage: number;
+  startPercentage: number;
+  isCompleted: boolean;
+  isCurrent: boolean;
+}
+
+export interface Project {
+  id: string;
+  name: string;
+  type: 'soft' | 'hard';
+  memberType: 'vip' | 'normal';
+  designerName: string;
+  status: string;
+  expectedEndDate: Date;
+  deadline: Date;
+  createdAt?: Date;
+  isOverdue: boolean;
+  overdueDays: number;
+  dueSoon: boolean;
+  urgency: 'high' | 'medium' | 'low';
+  phases: ProjectPhase[];
+  currentStage: string;
+  qualityRating?: 'excellent' | 'qualified' | 'unqualified' | 'pending';
+  lastCustomerFeedback?: string;
+  searchIndex?: string;
+  // 可选扩展字段
+  contact?: any;
+  customer?: string;
+  designerId?: string;
+  space?: string;
+  demoday?: Date;
+  updatedAt?: Date | string;
+  data?: any;
+}
+
+export interface TodoTask {
+  id: string;
+  title: string;
+  description: string;
+  deadline: Date;
+  priority: 'high' | 'medium' | 'low';
+  type: 'review' | 'assign' | 'performance';
+  targetId: string;
+}
+
+export interface TodoTaskFromIssue {
+  id: string;
+  title: string;
+  description?: string;
+  priority: IssuePriority;
+  type: IssueType;
+  status: IssueStatus;
+  projectId: string;
+  projectName: string;
+  relatedSpace?: string;
+  relatedStage?: string;
+  assigneeName?: string;
+  creatorName?: string;
+  createdAt: Date;
+  updatedAt: Date;
+  dueDate?: Date;
+  tags?: string[];
+}
+
+export interface UrgentEvent {
+  id: string;
+  title: string;
+  description: string;
+  eventType: 'review' | 'delivery' | 'phase_deadline' | 'customer_alert';
+  phaseName?: string;
+  deadline: Date;
+  projectId: string;
+  projectName: string;
+  designerName?: string;
+  urgencyLevel: 'critical' | 'high' | 'medium';
+  overdueDays?: number;
+  completionRate?: number;
+  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;
+}
+
+export interface LeaveRecord {
+  id: string;
+  employeeName: string;
+  date: string;
+  isLeave: boolean;
+  leaveType?: 'sick' | 'personal' | 'annual' | 'other';
+  reason?: string;
+}
+
+export interface EmployeeDetail {
+  name: string;
+  currentProjects: number;
+  projectNames: string[];
+  projectData: Array<{ id: string; name: string }>;
+  leaveRecords: LeaveRecord[];
+  redMarkExplanation: string;
+  calendarData?: EmployeeCalendarData;
+  surveyCompleted?: boolean;
+  surveyData?: any;
+  profileId?: string;
+}
+
+export interface EmployeeCalendarData {
+  currentMonth: Date;
+  days: EmployeeCalendarDay[];
+}
+
+export interface EmployeeCalendarDay {
+  date: Date;
+  projectCount: number;
+  projects: Array<{ id: string; name: string; deadline?: Date }>;
+  isToday: boolean;
+  isCurrentMonth: boolean;
+}

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

@@ -1,5 +1,5 @@
 <!-- 员工详情面板 -->
-@if (visible && employeeDetail) {
+@if (visible && currentEmployeeDetail) {
   <div class="employee-detail-overlay" (click)="onClose()">
     <div class="employee-detail-panel" (click)="stopPropagation($event)">
       <!-- 面板头部 -->
@@ -9,7 +9,7 @@
             <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
             <circle cx="12" cy="7" r="4"></circle>
           </svg>
-          {{ employeeDetail.name }} 详情
+          {{ currentEmployeeDetail.name }} 详情
         </h3>
         <button class="btn-close" (click)="onClose()">
           <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -34,15 +34,15 @@
           <div class="workload-info">
             <div class="workload-stat">
               <span class="stat-label">当前负责项目数:</span>
-              <span class="stat-value" [class]="employeeDetail.currentProjects >= 3 ? 'high-workload' : 'normal-workload'">
-                {{ employeeDetail.currentProjects }} 个
+              <span class="stat-value" [class]="currentEmployeeDetail.currentProjects >= 3 ? 'high-workload' : 'normal-workload'">
+                {{ currentEmployeeDetail.currentProjects }} 个
               </span>
             </div>
-            @if (employeeDetail.projectData.length > 0) {
+            @if (currentEmployeeDetail.projectData.length > 0) {
               <div class="project-list">
                 <span class="project-label">核心项目:</span>
                 <div class="project-tags">
-                  @for (project of employeeDetail.projectData; track project.id) {
+                  @for (project of currentEmployeeDetail.projectData; track project.id) {
                     <span class="project-tag clickable" 
                           (click)="onProjectClick(project.id)"
                           title="点击查看项目详情">
@@ -52,8 +52,8 @@
                       </svg>
                     </span>
                   }
-                  @if (employeeDetail.currentProjects > employeeDetail.projectData.length) {
-                    <span class="project-tag more">+{{ employeeDetail.currentProjects - employeeDetail.projectData.length }}</span>
+                  @if (currentEmployeeDetail.currentProjects > currentEmployeeDetail.projectData.length) {
+                    <span class="project-tag more">+{{ currentEmployeeDetail.currentProjects - currentEmployeeDetail.projectData.length }}</span>
                   }
                 </div>
               </div>
@@ -76,7 +76,7 @@
             </button>
           </div>
           
-          @if (employeeDetail.calendarData) {
+          @if (currentEmployeeDetail.calendarData) {
             <div class="employee-calendar">
               <!-- 月份标题 -->
               <div class="calendar-month-header">
@@ -88,7 +88,7 @@
                   </svg>
                 </button>
                 <span class="month-title">
-                  {{ employeeDetail.calendarData.currentMonth | date:'yyyy年M月' }}
+                  {{ currentEmployeeDetail.calendarData.currentMonth | date:'yyyy年M月' }}
                 </span>
                 <button class="btn-next-month" 
                         (click)="onChangeMonth(1)"
@@ -112,7 +112,7 @@
               
               <!-- 日历网格 -->
               <div class="calendar-grid">
-                @for (day of employeeDetail.calendarData.days; track day.date.getTime()) {
+                @for (day of currentEmployeeDetail.calendarData.days; track day.date.getTime()) {
                   <div class="calendar-day"
                        [class.today]="day.isToday"
                        [class.other-month]="!day.isCurrentMonth"
@@ -161,7 +161,7 @@
             <h4>请假明细(未来7天)</h4>
           </div>
           <div class="leave-table">
-            @if (employeeDetail.leaveRecords.length > 0) {
+            @if (currentEmployeeDetail.leaveRecords.length > 0) {
               <table>
                 <thead>
                   <tr>
@@ -171,7 +171,7 @@
                   </tr>
                 </thead>
                 <tbody>
-                  @for (record of employeeDetail.leaveRecords; track record.id) {
+                  @for (record of currentEmployeeDetail.leaveRecords; track record.id) {
                     <tr [class]="record.isLeave ? 'leave-day' : 'work-day'">
                       <td>{{ record.date | date:'M月d日' }}</td>
                       <td>
@@ -209,7 +209,7 @@
             <h4>红色标记说明</h4>
           </div>
           <div class="explanation-content">
-            <p class="explanation-text">{{ employeeDetail.redMarkExplanation }}</p>
+            <p class="explanation-text">{{ currentEmployeeDetail.redMarkExplanation }}</p>
           </div>
         </div>
         
@@ -231,7 +231,7 @@
             </button>
           </div>
           
-          @if (employeeDetail.surveyCompleted && employeeDetail.surveyData) {
+          @if (currentEmployeeDetail.surveyCompleted && currentEmployeeDetail.surveyData) {
             <div class="survey-content">
               <div class="survey-status completed">
                 <svg viewBox="0 0 24 24" width="20" height="20" fill="#34c759">
@@ -239,7 +239,7 @@
                 </svg>
                 <span>已完成问卷</span>
                 <span class="survey-time">
-                  {{ employeeDetail.surveyData.createdAt | date:'yyyy-MM-dd HH:mm' }}
+                  {{ currentEmployeeDetail.surveyData.createdAt | date:'yyyy-MM-dd HH:mm' }}
                 </span>
               </div>
               
@@ -247,7 +247,7 @@
               @if (!showFullSurvey) {
                 <div class="capability-summary">
                   <h5>您的能力画像</h5>
-                  @if (getCapabilitySummary(employeeDetail.surveyData.answers); as summary) {
+                  @if (getCapabilitySummary(currentEmployeeDetail.surveyData.answers); as summary) {
                     <div class="summary-grid">
                       <div class="summary-item">
                         <span class="label">擅长风格:</span>
@@ -293,7 +293,7 @@
                     <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
                       <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
                     </svg>
-                    查看完整问卷(共 {{ employeeDetail.surveyData.answers.length }} 道题)
+                    查看完整问卷(共 {{ currentEmployeeDetail.surveyData.answers.length }} 道题)
                   </button>
                 </div>
               }
@@ -301,8 +301,8 @@
               <!-- 完整问卷答案 -->
               @if (showFullSurvey) {
                 <div class="survey-answers">
-                  <h5>完整问卷答案(共 {{ employeeDetail.surveyData.answers.length }} 道题):</h5>
-                  @for (answer of employeeDetail.surveyData.answers; track $index) {
+                  <h5>完整问卷答案(共 {{ currentEmployeeDetail.surveyData.answers.length }} 道题):</h5>
+                  @for (answer of currentEmployeeDetail.surveyData.answers; track $index) {
                     <div class="answer-item">
                       <div class="question-text">
                         <strong>Q{{$index + 1}}:</strong> {{ answer.question }}

+ 360 - 21
src/app/pages/team-leader/employee-detail-panel/employee-detail-panel.ts

@@ -1,4 +1,4 @@
-import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, ChangeDetectorRef } from '@angular/core';
 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';
@@ -50,41 +50,340 @@ export interface EmployeeCalendarDay {
   templateUrl: './employee-detail-panel.html',
   styleUrls: ['./employee-detail-panel.scss']
 })
-export class EmployeeDetailPanelComponent implements OnInit {
+export class EmployeeDetailPanelComponent implements OnInit, OnChanges {
   // 暴露 Array 给模板使用
   Array = Array;
   
   // 输入属性
   @Input() visible: boolean = false;
+  // 兼容旧模式:直接传入详情数据
   @Input() employeeDetail: EmployeeDetail | null = null;
-  @Input() embedMode: boolean = false; // 🆕 嵌入模式:true = 只渲染内容,false = 完整侧边栏(默认)
   
+  // 🆕 替换直接传入 employeeDetail,改为传入基础数据自行计算
+  @Input() employeeName: string = '';
+  @Input() projects: any[] = []; // 该员工的项目列表
+  @Input() allLeaveRecords: LeaveRecord[] = []; // 所有请假记录(组件内过滤)
+  @Input() embedMode: boolean = false; // 嵌入模式
+
   // 输出事件
   @Output() close = new EventEmitter<void>();
+  @Output() projectClick = new EventEmitter<string>();
+  // 兼容旧模式的输出事件
   @Output() calendarMonthChange = new EventEmitter<number>();
   @Output() calendarDayClick = new EventEmitter<EmployeeCalendarDay>();
-  @Output() projectClick = new EventEmitter<string>();
   @Output() refreshSurvey = new EventEmitter<void>();
   
   // 组件内部状态
+  internalEmployeeDetail: EmployeeDetail | null = null; // 内部生成的详情
   showFullSurvey: boolean = false;
   refreshingSurvey: boolean = false;
+
+  // 获取当前显示的 employeeDetail(优先使用 Input,否则使用内部生成的)
+  get currentEmployeeDetail(): EmployeeDetail | null {
+    return this.employeeDetail || this.internalEmployeeDetail;
+  }
   
   // 日历项目列表弹窗状态
   showCalendarProjectList: boolean = false;
   selectedDate: Date | null = null;
   selectedDayProjects: Array<{ id: string; name: string; deadline?: Date }> = [];
 
-  // 设计师详细日历(与订单分配页复用)
+  // 设计师详细日历
   showDesignerCalendar: boolean = false;
   calendarDesigners: CalendarDesigner[] = [];
   calendarViewMode: 'week' | 'month' | 'quarter' = 'month';
   
-  constructor(private router: Router) {}
+  constructor(
+    private router: Router,
+    private cdr: ChangeDetectorRef
+  ) {}
   
   ngOnInit(): void {
     console.log('📋 EmployeeDetailPanelComponent 初始化');
   }
+
+  ngOnChanges(changes: SimpleChanges): void {
+    // 优先处理直接传入的 employeeDetail
+    if (changes['employeeDetail']) {
+      // 如果外部传入了数据,不需要做额外操作,直接使用
+      return;
+    }
+
+    // 当 visible 变为 true,或员工姓名/项目数据改变时,重新生成详情(仅在未传入 employeeDetail 时)
+    if (!this.employeeDetail && (
+        (changes['visible']?.currentValue === true) || 
+        (this.visible && (changes['employeeName'] || changes['projects']))
+       )) {
+      if (this.employeeName) {
+        this.generateEmployeeDetail();
+      }
+    }
+  }
+  
+  /**
+   * 生成员工详情数据
+   */
+  async generateEmployeeDetail(): Promise<void> {
+    if (!this.employeeName) return;
+
+    const employeeName = this.employeeName;
+    const currentProjects = this.projects.length;
+    
+    // 保存完整的项目数据(最多显示3个)
+    const projectData = this.projects.slice(0, 3).map(p => ({
+      id: p.id,
+      name: p.name
+    }));
+    
+    const projectNames = projectData.map(p => p.name); // 项目名称列表
+    
+    // 获取该员工的请假记录(未来7天)
+    const today = new Date();
+    const next7Days = Array.from({ length: 7 }, (_, i) => {
+      const date = new Date(today);
+      date.setDate(today.getDate() + i);
+      return date.toISOString().split('T')[0]; // YYYY-MM-DD 格式
+    });
+    
+    const employeeLeaveRecords = this.allLeaveRecords.filter(record => 
+      record.employeeName === employeeName && next7Days.includes(record.date)
+    );
+    
+    // 生成红色标记说明
+    const redMarkExplanation = this.generateRedMarkExplanation(employeeName, employeeLeaveRecords, currentProjects);
+    
+    // 生成日历数据
+    const calendarData = this.generateEmployeeCalendar(employeeName, this.projects);
+    
+    // 构建基础对象
+    this.internalEmployeeDetail = {
+      name: employeeName,
+      currentProjects,
+      projectNames,
+      projectData,
+      leaveRecords: employeeLeaveRecords,
+      redMarkExplanation,
+      calendarData
+    };
+
+    // 加载问卷数据
+    await this.loadSurveyData(employeeName);
+    
+    // 触发变更检测
+    this.cdr.markForCheck();
+  }
+
+  /**
+   * 加载问卷数据
+   */
+  async loadSurveyData(employeeName: string): Promise<void> {
+    if (!this.internalEmployeeDetail) return;
+
+    try {
+      const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
+      
+      // 通过员工名字查找Profile(同时查询 realname 和 name 字段)
+      const realnameQuery = new Parse.Query('Profile');
+      realnameQuery.equalTo('realname', employeeName);
+      
+      const nameQuery = new Parse.Query('Profile');
+      nameQuery.equalTo('name', employeeName);
+      
+      // 使用 or 查询
+      const profileQuery = Parse.Query.or(realnameQuery, nameQuery);
+      profileQuery.limit(1);
+      
+      const profileResults = await profileQuery.find();
+      
+      if (profileResults.length > 0) {
+        const profile = profileResults[0];
+        this.internalEmployeeDetail.profileId = profile.id;
+        this.internalEmployeeDetail.surveyCompleted = profile.get('surveyCompleted') || false;
+        
+        // 如果已完成问卷,加载问卷答案
+        if (this.internalEmployeeDetail.surveyCompleted) {
+          const surveyQuery = new Parse.Query('SurveyLog');
+          surveyQuery.equalTo('profile', profile.toPointer());
+          surveyQuery.equalTo('type', 'survey-profile');
+          surveyQuery.descending('createdAt');
+          surveyQuery.limit(1);
+          
+          const surveyResults = await surveyQuery.find();
+          
+          if (surveyResults.length > 0) {
+            const survey = surveyResults[0];
+            this.internalEmployeeDetail.surveyData = {
+              answers: survey.get('answers') || [],
+              createdAt: survey.get('createdAt'),
+              updatedAt: survey.get('updatedAt')
+            };
+          }
+        }
+      }
+      this.cdr.markForCheck();
+    } catch (error) {
+      console.error(`❌ 加载员工 ${employeeName} 问卷数据失败:`, error);
+    }
+  }
+
+  /**
+   * 生成员工日历数据(支持指定月份)
+   */
+  private generateEmployeeCalendar(employeeName: string, employeeProjects: any[], targetMonth?: Date): EmployeeCalendarData {
+    const currentMonth = targetMonth || new Date();
+    const year = currentMonth.getFullYear();
+    const month = currentMonth.getMonth();
+    
+    // 获取当月天数
+    const daysInMonth = new Date(year, month + 1, 0).getDate();
+    const days: EmployeeCalendarDay[] = [];
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    
+    // 生成当月每一天的数据
+    for (let day = 1; day <= daysInMonth; day++) {
+      const date = new Date(year, month, day);
+      const dateStr = date.toISOString().split('T')[0];
+      
+      // 找出该日期相关的项目(项目进行中且在当天范围内)
+      const dayProjects = employeeProjects.filter(p => {
+        // 处理 Parse Date 对象:检查是否有 toDate 方法
+        const getDate = (dateValue: any) => {
+          if (!dateValue) return null;
+          if (dateValue.toDate && typeof dateValue.toDate === 'function') {
+            return dateValue.toDate(); // Parse Date对象
+          }
+          if (dateValue instanceof Date) {
+            return dateValue;
+          }
+          return new Date(dateValue); // 字符串或时间戳
+        };
+        
+        const deadlineDate = getDate(p.deadline);
+        const createdDate = p.createdAt ? getDate(p.createdAt) : null;
+        
+        // 如果项目既没有 deadline 也没有 createdAt,则跳过
+        if (!deadlineDate && !createdDate) {
+          return false;
+        }
+        
+        // 智能处理日期范围
+        let startDate: Date;
+        let endDate: Date;
+        
+        if (deadlineDate && createdDate) {
+          // 情况1:两个日期都有
+          startDate = createdDate;
+          endDate = deadlineDate;
+        } else if (deadlineDate) {
+          // 情况2:只有deadline,往前推30天
+          startDate = new Date(deadlineDate.getTime() - 30 * 24 * 60 * 60 * 1000);
+          endDate = deadlineDate;
+        } else {
+          // 情况3:只有createdAt,往后推30天
+          startDate = createdDate!;
+          endDate = new Date(createdDate!.getTime() + 30 * 24 * 60 * 60 * 1000);
+        }
+        
+        startDate.setHours(0, 0, 0, 0);
+        endDate.setHours(0, 0, 0, 0);
+        
+        const inRange = date >= startDate && date <= endDate;
+        
+        return inRange;
+      }).map(p => {
+        const getDate = (dateValue: any) => {
+          if (!dateValue) return undefined;
+          if (dateValue.toDate && typeof dateValue.toDate === 'function') {
+            return dateValue.toDate();
+          }
+          if (dateValue instanceof Date) {
+            return dateValue;
+          }
+          return new Date(dateValue);
+        };
+        
+        return {
+          id: p.id,
+          name: p.name,
+          deadline: getDate(p.deadline)
+        };
+      });
+      
+      days.push({
+        date,
+        projectCount: dayProjects.length,
+        projects: dayProjects,
+        isToday: date.getTime() === today.getTime(),
+        isCurrentMonth: true
+      });
+    }
+    
+    // 补齐前后的日期(保证从周日开始)
+    const firstDay = new Date(year, month, 1);
+    const firstDayOfWeek = firstDay.getDay(); // 0=周日
+    
+    // 前置补齐(上个月的日期)
+    for (let i = firstDayOfWeek - 1; i >= 0; i--) {
+      const date = new Date(year, month, -i);
+      days.unshift({
+        date,
+        projectCount: 0,
+        projects: [],
+        isToday: false,
+        isCurrentMonth: false
+      });
+    }
+    
+    // 后置补齐(下个月的日期,保证总数是7的倍数)
+    const remainder = days.length % 7;
+    if (remainder !== 0) {
+      const needed = 7 - remainder;
+      for (let i = 1; i <= needed; i++) {
+        const date = new Date(year, month + 1, i);
+        days.push({
+          date,
+          projectCount: 0,
+          projects: [],
+          isToday: false,
+          isCurrentMonth: false
+        });
+      }
+    }
+    
+    return {
+      currentMonth: new Date(year, month, 1),
+      days
+    };
+  }
+
+  // 生成红色标记说明
+  private generateRedMarkExplanation(employeeName: string, leaveRecords: LeaveRecord[], projectCount: number): string {
+    const explanations: string[] = [];
+    
+    // 检查请假情况
+    const leaveDays = leaveRecords.filter(record => record.isLeave);
+    if (leaveDays.length > 0) {
+      leaveDays.forEach(leave => {
+        const date = new Date(leave.date);
+        const dateStr = `${date.getMonth() + 1}月${date.getDate()}日`;
+        explanations.push(`${dateStr}(${leave.reason || '请假'})`);
+      });
+    }
+    
+    // 检查项目繁忙情况
+    if (projectCount >= 3) {
+      const today = new Date();
+      const dateStr = `${today.getMonth() + 1}月${today.getDate()}日`;
+      explanations.push(`${dateStr}(${projectCount}个项目繁忙)`);
+    }
+    
+    if (explanations.length === 0) {
+      return '当前无红色标记时段';
+    }
+    
+    return `甘特图中红色时段说明:${explanations.map((exp, index) => `${index + 1}${exp}`).join(';')}`;
+  }
   
   /**
    * 关闭面板
@@ -99,13 +398,43 @@ export class EmployeeDetailPanelComponent implements OnInit {
    * 切换月份
    */
   onChangeMonth(direction: number): void {
-    this.calendarMonthChange.emit(direction);
+    // 如果是外部数据模式,发出事件让父组件处理
+    if (this.employeeDetail) {
+      this.calendarMonthChange.emit(direction);
+      return;
+    }
+    
+    // 内部模式处理
+    if (!this.internalEmployeeDetail?.calendarData) {
+      return;
+    }
+    
+    const currentMonth = this.internalEmployeeDetail.calendarData.currentMonth;
+    const newMonth = new Date(currentMonth);
+    newMonth.setMonth(newMonth.getMonth() + direction);
+    
+    // 重新生成日历数据
+    const newCalendarData = this.generateEmployeeCalendar(
+      this.employeeName, 
+      this.projects, 
+      newMonth
+    );
+    
+    // 更新员工详情中的日历数据
+    this.internalEmployeeDetail = {
+      ...this.internalEmployeeDetail,
+      calendarData: newCalendarData
+    };
   }
   
   /**
    * 日历日期点击
    */
   onCalendarDayClick(day: EmployeeCalendarDay): void {
+    // 发出事件
+    this.calendarDayClick.emit(day);
+
+    // 内部处理
     if (!day.isCurrentMonth || day.projectCount === 0) {
       return;
     }
@@ -137,14 +466,15 @@ export class EmployeeDetailPanelComponent implements OnInit {
    * 将当前员工信息适配为 DesignerCalendar 组件的数据结构
    */
   openDesignerCalendar(): void {
-    if (!this.employeeDetail) return;
+    const detail = this.currentEmployeeDetail;
+    if (!detail) return;
 
-    const name = this.employeeDetail.name || '设计师';
-    const currentProjects = this.employeeDetail.currentProjects || 0;
+    const name = detail.name || '设计师';
+    const currentProjects = detail.currentProjects || 0;
 
     // 将已有的 employeeDetail.calendarData 映射为日历事件(粗粒度:有项目视为当日有工作)
     const upcomingEvents: CalendarDesigner['upcomingEvents'] = [];
-    const days = this.employeeDetail.calendarData?.days || [];
+    const days = detail.calendarData?.days || [];
     for (const day of days) {
       if (day.projectCount > 0) {
         upcomingEvents.push({
@@ -159,7 +489,7 @@ export class EmployeeDetailPanelComponent implements OnInit {
 
     // 适配为日历组件的设计师数据(单人视图)
     this.calendarDesigners = [{
-      id: this.employeeDetail.profileId || name,
+      id: detail.profileId || name,
       name,
       groupId: '',
       groupName: '',
@@ -181,17 +511,27 @@ export class EmployeeDetailPanelComponent implements OnInit {
   /**
    * 刷新问卷
    */
-  onRefreshSurvey(): void {
-    if (this.refreshingSurvey) {
+  async onRefreshSurvey(): Promise<void> {
+    // 如果是外部数据模式,发出事件让父组件处理
+    if (this.employeeDetail) {
+      this.refreshSurvey.emit();
       return;
     }
-    this.refreshingSurvey = true;
-    this.refreshSurvey.emit();
-    
-    // 模拟加载完成(实际由父组件控制)
-    setTimeout(() => {
+
+    if (this.refreshingSurvey || !this.internalEmployeeDetail) {
+      return;
+    }
+
+    try {
+      this.refreshingSurvey = true;
+      console.log('🔄 刷新问卷状态...');
+      await this.loadSurveyData(this.employeeName);
+      console.log('✅ 问卷状态刷新成功');
+    } catch (error) {
+      console.error('❌ 刷新问卷状态失败:', error);
+    } finally {
       this.refreshingSurvey = false;
-    }, 2000);
+    }
   }
   
   /**
@@ -250,4 +590,3 @@ export class EmployeeDetailPanelComponent implements OnInit {
     event.stopPropagation();
   }
 }
-

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác