فهرست منبع

```
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 4 روز پیش
والد
کامیت
576b9f1a00
29فایلهای تغییر یافته به همراه4261 افزوده شده و 2611 حذف شده
  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
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 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();
   }
 }
-

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است