Explorar o código

designer(project-detail): 移除分阶段结算记录卡片,遵循@for/@if 规范;不再展示结算记录模块

0235711 hai 3 días
pai
achega
1f12ec3ded

+ 3 - 7
.trae/rules/project_rules.md

@@ -1,8 +1,4 @@
-<<<<<<< HEAD
+项目规范
+========
 
-模板页面显示的条件和循环if和for统一用控制流的@if和@for
-=======
-# 项目规范
-
-//模板页面显示的条件和循环采用控制流的@if和@for
->>>>>>> c955fbffd90c3e0d8285002bb4c199a356069c1a
+- 模板页面显示的条件和循环应统一使用控制流指令:@if 与 @for。

+ 3 - 1
src/app/app.routes.ts

@@ -27,6 +27,7 @@ import { TeamManagementComponent } from './pages/team-leader/team-management/tea
 import { ProjectReviewComponent } from './pages/team-leader/project-review/project-review';
 import { QualityManagementComponent } from './pages/team-leader/quality-management/quality-management';
 import { KnowledgeBaseComponent } from './pages/team-leader/knowledge-base/knowledge-base';
+import { WorkloadCalendarComponent } from './pages/team-leader/workload-calendar/workload-calendar';
 
 // 财务页面
 import { Dashboard as FinanceDashboard } from './pages/finance/dashboard/dashboard';
@@ -96,7 +97,8 @@ export const routes: Routes = [
       { path: 'team-management', component: TeamManagementComponent, title: '团队管理' },
       { path: 'project-review', component: ProjectReviewComponent, title: '项目审核' },
       { path: 'quality-management', component: QualityManagementComponent, title: '质量管理' },
-      { path: 'knowledge-base', component: KnowledgeBaseComponent, title: '知识库与能力复制' }
+      { path: 'knowledge-base', component: KnowledgeBaseComponent, title: '知识库与能力复制' },
+      { path: 'workload-calendar', component: WorkloadCalendarComponent, title: '负载日历' }
     ]
   },
 

+ 2 - 2
src/app/pages/customer-service/project-list/project-list.html

@@ -47,7 +47,7 @@
               {{ option.label }}
             </option>
           </select>
-        </div>
+        </div>  
         
         <div class="filter-group">
           <label>排序方式</label>
@@ -162,7 +162,7 @@
               </svg>
               <span>联系</span>
             </button>
-            <a [routerLink]="['/customer-service/project-detail', project.id]" class="primary-btn card-action">
+            <a [routerLink]="['/designer/project-detail', project.id]" class="primary-btn card-action">
               <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                 <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
               </svg>

+ 1 - 30
src/app/pages/designer/project-detail/project-detail.html

@@ -658,33 +658,4 @@
     </div>
   </div>
 
-  <!-- 分阶段结算记录卡片 -->
-  <div class="settlement-card card">
-    <h2>分阶段结算记录</h2>
-    <div *ngIf="settlements.length === 0" class="empty-state">
-      <div class="empty-icon">💰</div>
-      <span>暂无结算记录</span>
-    </div>
-    <div *ngIf="settlements.length > 0" class="settlement-table">
-      <table>
-        <thead>
-          <tr>
-            <th>阶段</th>
-            <th>比例</th>
-            <th>金额(元)</th>
-            <th>状态</th>
-            <th>完成时间</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr *ngFor="let settlement of settlements">
-            <td>{{ settlement.stage }}</td>
-            <td>{{ settlement.percentage }}%</td>
-            <td>{{ settlement.amount }}</td>
-            <td><span class="status-badge" [class.status-pending]="settlement.status === '待结算'" [class.status-settled]="settlement.status === '已结算'">{{ settlement.status }}</span></td>
-            <td>{{ settlement.completionTime | date:'yyyy-MM-dd' }}</td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-  </div>
+  <!-- 分阶段结算记录卡片:已按需求移除 -->

+ 2 - 1
src/app/pages/team-leader/dashboard/dashboard.ts

@@ -562,7 +562,8 @@ export class Dashboard implements OnInit {
 
   // 打开负载日历(占位:跳转到团队管理)
   navigateToWorkloadCalendar(): void {
-    this.router.navigate(['/team-leader/team-management']);
+-    this.router.navigate(['/team-leader/team-management']);
++    this.router.navigate(['/team-leader/workload-calendar']);
   }
 
   // 查看项目详情

+ 1 - 2
src/app/pages/team-leader/performance/performance.scss

@@ -212,8 +212,7 @@
           color: ios.$ios-primary;
         }
 
-        .col-name {
-        }
+        // removed empty rule .col-name {}
 
         .col-score {
           font-weight: 600;

+ 140 - 0
src/app/pages/team-leader/workload-calendar/workload-calendar.html

@@ -0,0 +1,140 @@
+<div class="workload-calendar-container">
+  <header class="page-header" role="navigation" aria-label="工作量日历控制">
+    <h1>负载日历</h1>
+    <div class="controls">
+      <div class="view-switch" role="tablist" aria-label="视图切换">
+        <button [class.active]="view==='day'" (click)="switchView('day')" role="tab" aria-selected="{{view==='day'}}" title="日视图">日</button>
+        <button [class.active]="view==='week'" (click)="switchView('week')" role="tab" aria-selected="{{view==='week'}}" title="周视图">周</button>
+        <button [class.active]="view==='month'" (click)="switchView('month')" role="tab" aria-selected="{{view==='month'}}" title="月视图">月</button>
+      </div>
+      <div class="date-nav">
+        <button (click)="navigateDate('prev')" aria-label="上一周期">‹</button>
+        <span class="month-label" aria-live="polite">{{ monthLabel }}</span>
+        <button (click)="navigateDate('next')" aria-label="下一周期">›</button>
+        <button class="today" (click)="setToday()" title="返回今天">今天</button>
+      </div>
+      <div class="filters">
+        <label for="designerSelect">设计师:</label>
+        <select id="designerSelect" [(ngModel)]="selectedDesigner" (change)="onDesignerChange()">
+          <option value="all">全部</option>
+          @for (d of designers; track d) {
+            <option [value]="d">{{ d }}</option>
+          }
+        </select>
+        <label class="overdue-toggle" for="overdueOnly">
+          <input id="overdueOnly" type="checkbox" [(ngModel)]="showOverdueOnly" (change)="onOverdueOnlyChange()" />
+          仅看超期
+        </label>
+      </div>
+    </div>
+  </header>
+
+  @if (view==='month') {
+    <section class="calendar-section" aria-label="月视图">
+      <div class="weekdays">
+        <div class="weekday">一</div>
+        <div class="weekday">二</div>
+        <div class="weekday">三</div>
+        <div class="weekday">四</div>
+        <div class="weekday">五</div>
+        <div class="weekday weekend">六</div>
+        <div class="weekday weekend">日</div>
+      </div>
+      <div class="calendar-grid">
+        @for (d of monthDays; track d.date) {
+          <div class="day"
+               [class.other-month]="!d.currentMonth"
+               [class.today]="isSameDay(d.date, today)"
+               [class.selected]="isSameDay(d.date, selectedDate)"
+               [class.weekend]="isWeekend(d.date)"
+               (click)="selectDate(d.date)"
+               title="查看 {{ d.date | date:'yyyy-MM-dd' }} 的任务">
+            <div class="date-label" [title]="d.date | date:'fullDate'" (click)="selectDate(d.date)">{{ d.date | date:'MM/dd' }}</div>
+            <div class="tasks">
+              @if (!isExpanded(d.date)) {
+                @for (t of d.tasks.slice(0,3); track t.id) {
+                  <button class="task-chip" type="button" (click)="navigateToProject(t, $event)" [class.overdue]="t.isOverdue" [class.high]="t.priority==='high'" title="进入项目:{{t.projectName}}">
+                    <span class="task-title">{{ t.title }}</span>
+                    <span class="assignee" title="按设计师筛选" (click)="filterByDesigner(t.assignee, $event)">{{ t.assignee }}</span>
+                  </button>
+                }
+                @if (d.tasks.length > 3) {
+                  <button class="more-btn" (click)="toggleExpand(d.date, $event)" aria-label="展开更多">+{{ d.tasks.length - 3 }}</button>
+                }
+              } @else {
+                @for (t of d.tasks; track t.id) {
+                  <button class="task-chip" type="button" (click)="navigateToProject(t, $event)" [class.overdue]="t.isOverdue" [class.high]="t.priority==='high'" title="进入项目:{{t.projectName}}">
+                    <span class="task-title">{{ t.title }}</span>
+                    <span class="assignee" title="按设计师筛选" (click)="filterByDesigner(t.assignee, $event)">{{ t.assignee }}</span>
+                  </button>
+                }
+                <button class="more-btn" (click)="toggleExpand(d.date, $event)" aria-label="收起">收起</button>
+              }
+              @if (d.tasks.length === 0) {
+                <div class="no-task" aria-hidden="true">—</div>
+              }
+            </div>
+          </div>
+        }
+      </div>
+    </section>
+  }
+
+  @if (view==='week') {
+    <section class="calendar-section" aria-label="周视图">
+      <div class="week-grid">
+        @for (d of weekDays; track d.date) {
+          <div class="week-day" [class.today]="isSameDay(d.date, today)" [class.selected]="isSameDay(d.date, selectedDate)" [class.weekend]="isWeekend(d.date)" (click)="selectDate(d.date)">
+            <div class="date-label" (click)="selectDate(d.date)">{{ d.date | date:'EEE MM/dd' }}</div>
+            <div class="tasks">
+              @for (t of d.tasks; track t.id) {
+                <button class="task-chip" type="button" (click)="navigateToProject(t, $event)" [class.overdue]="t.isOverdue" [class.high]="t.priority==='high'" title="进入项目:{{t.projectName}}">
+                  <span class="task-title">{{ t.title }}</span>
+                  <span class="assignee" title="按设计师筛选" (click)="filterByDesigner(t.assignee, $event)">{{ t.assignee }}</span>
+                </button>
+              }
+              @if (d.tasks.length === 0) {
+                <div class="no-task" aria-hidden="true">—</div>
+              }
+            </div>
+          </div>
+        }
+      </div>
+    </section>
+  }
+
+  @if (view==='day') {
+    <section class="calendar-section" aria-label="日视图">
+      <div class="day-panel">
+        <div class="date-label large">{{ selectedDate | date:'yyyy-MM-dd EEEE' }}</div>
+        <div class="tasks">
+          @let dayTasksLocal = dayTasks;
+          @for (t of dayTasksLocal; track t.id) {
+            <div class="task-row" [class.overdue]="t.isOverdue" title="{{t.title}} - {{t.projectName}} / {{t.assignee}}" (click)="navigateToProject(t, $event)">
+              <div class="title">{{ t.title }}</div>
+              <div class="project">{{ t.projectName }}</div>
+              <div class="assignee"><button type="button" class="linklike" (click)="filterByDesigner(t.assignee, $event)" title="按设计师筛选">{{ t.assignee }}</button></div>
+              <div class="priority" [class.high]="t.priority==='high'">{{ t.priority }}</div>
+            </div>
+          }
+          @if (dayTasksLocal.length === 0) {
+            <div class="no-task">今日暂无任务</div>
+          }
+        </div>
+      </div>
+    </section>
+  }
+
+  <section class="status-board" aria-label="设计师工作状态">
+    <h2>设计师工作状态</h2>
+    <div class="status-list">
+      @for (s of designerStatuses; track s.name) {
+        <div class="status-item" [class]="s.cls">
+          <div class="name">{{ s.name }}</div>
+          <div class="meta">任务数:{{ s.tasksCount }};超期:{{ s.overdue }}</div>
+          <div class="label">{{ s.label }}</div>
+        </div>
+      }
+    </div>
+  </section>
+</div>

+ 155 - 0
src/app/pages/team-leader/workload-calendar/workload-calendar.scss

@@ -0,0 +1,155 @@
+.workload-calendar-container {
+  padding: 16px;
+}
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 16px;
+}
+.controls { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
+.view-switch button, .date-nav button, .filters select, .date-nav .today {
+  padding: 6px 10px; border: 1px solid #e5e7eb; border-radius: 6px; background: #fff; cursor: pointer;
+}
+.view-switch button.active { background: #111827; color: #fff; }
+.month-label { margin: 0 8px; font-weight: 600; }
+
+.calendar-section { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; }
+.weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 6px; color: #6b7280; font-size: 12px; }
+.weekdays .weekday { text-align: center; }
+.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
+.day { min-height: 110px; border: 1px solid #f3f4f6; border-radius: 8px; padding: 6px; background: #fafafa; display: flex; flex-direction: column; cursor: pointer; }
+.day.today { outline: 2px solid #2563eb; }
+.day.selected { outline: 2px solid #16a34a; background: #f0fdf4; }
+.day.other-month { opacity: .45; }
+.date-label { font-size: 12px; color: #6b7280; margin-bottom: 4px; }
+.tasks { display: flex; flex-direction: column; gap: 4px; }
+.task-chip { display: flex; justify-content: space-between; gap: 6px; padding: 4px 6px; border-radius: 6px; background: #eef2ff; color: #1f2937; font-size: 12px; border: 1px solid #e0e7ff; }
+.task-chip.overdue { background: #fee2e2; border-color: #fecaca; }
+.task-chip.high { background: #fef3c7; border-color: #fde68a; }
+.task-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 140px; }
+.assignee { color: #6b7280; min-width: 56px; text-align: right; }
+.no-task { color: #d1d5db; text-align: center; }
+
+.week-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; }
+.week-day { border: 1px solid #f3f4f6; border-radius: 8px; padding: 8px; background: #fff; cursor: pointer; }
+.week-day.today { outline: 2px solid #2563eb; }
+.week-day.selected { outline: 2px solid #16a34a; background: #f0fdf4; }
+
+.day-panel { border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; background: #fff; }
+.day-panel .date-label.large { font-size: 18px; font-weight: 600; margin-bottom: 10px; }
+.task-row { display: grid; grid-template-columns: 2fr 2fr 1fr auto; gap: 8px; align-items: center; padding: 8px; border: 1px solid #f3f4f6; border-radius: 8px; }
+.task-row + .task-row { margin-top: 6px; }
+.task-row.overdue { background: #fff1f2; }
+.priority.high { color: #b45309; font-weight: 600; }
+
+.status-board { margin-top: 16px; }
+.status-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
+.status-item { border: 1px solid #e5e7eb; border-radius: 8px; padding: 10px; background: #fff; }
+.status-item.idle { background: #ecfeff; }
+.status-item.busy { background: #fff7ed; }
+.status-item.abnormal { background: #fef2f2; }
+.status-item .name { font-weight: 600; }
+.status-item .meta { color: #6b7280; font-size: 12px; margin-top: 4px; }
+.status-item .label { margin-top: 4px; font-size: 12px; }
+
+.filters .overdue-toggle { display: inline-flex; align-items: center; gap: 6px; margin-left: 8px; font-size: 13px; color: #374151; }
+
+@media (max-width: 768px) {
+  .calendar-grid { grid-template-columns: repeat(7, 1fr); }
+  .task-title { max-width: 100px; }
+}
+.tasks .more-btn {
+  margin-top: 2px;
+  padding: 2px 6px;
+  border: 1px dashed #cbd5e1;
+  border-radius: 6px;
+  background: #f8fafc;
+  color: #334155;
+  font-size: 12px;
+  cursor: pointer;
+}
+.tasks .more-btn:hover { background: #eef2ff; }
+
+/* 周末与日期态 */
+.weekdays {
+  .weekday.weekend { color: #b26; opacity: .9; }
+}
+.day,
+.week-day {
+  &.weekend { background: linear-gradient(180deg, rgba(255, 240, 245, .6), rgba(255,255,255,0)); }
+  &.today { outline: 2px solid #1677ff; outline-offset: -2px; }
+  &.selected { box-shadow: inset 0 0 0 2px #52c41a; }
+}
+
+/* 日期角标 */
+.date-label {
+  font-weight: 600;
+  &.large { font-size: 18px; }
+}
+
+/* 任务 chip 样式 */
+.tasks {
+  .task-chip {
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    padding: 2px 6px;
+    margin: 2px 0;
+    border-radius: 6px;
+    background: #f5f7fa;
+    color: #333;
+    border-left: 3px solid transparent;
+    transition: transform .12s ease, background .12s ease, box-shadow .12s ease;
+    .task-title { max-width: 10em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+    .assignee { color: #666; font-size: 12px; }
+    &.overdue { border-left-color: #ff4d4f; background: #fff2f0; }
+    &.high { box-shadow: inset 0 0 0 1px #faad14; background: #fff7e6; }
+    &:hover { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,.06); }
+  }
+  .task-row {
+    display: grid; grid-template-columns: 1fr auto auto auto; gap: 8px; align-items: center;
+    padding: 8px 10px; border-radius: 8px; background: #fafafa; margin-bottom: 6px;
+    border-left: 3px solid transparent;
+    &.overdue { border-left-color: #ff4d4f; background: #fff2f0; }
+    .title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+    .project { color: #666; }
+    .assignee { color: #666; }
+    .priority { text-transform: uppercase; font-size: 12px; padding: 2px 6px; border-radius: 12px; background: #eef5ff; color: #1677ff;
+      &.high { background: #fff7e6; color: #fa8c16; font-weight: 600; }
+    }
+  }
+  .no-task { color: #bbb; text-align: center; padding: 8px 0; }
+}
+
+/* 展开/收起按钮 */
+.more-btn {
+  margin-top: 4px;
+  padding: 2px 8px;
+  border-radius: 12px;
+  border: 1px solid #d9d9d9;
+  background: #fff;
+  color: #595959;
+  cursor: pointer;
+  transition: background .12s ease, color .12s ease, border-color .12s ease;
+  &:hover { background: #f5f5f5; color: #262626; border-color: #bfbfbf; }
+}
+
+/* 状态面板 */
+.status-board {
+  margin-top: 16px;
+  .status-list { display: grid; grid-template-columns: repeat(auto-fill,minmax(180px,1fr)); gap: 10px; }
+  .status-item {
+    padding: 10px; border-radius: 8px; background: #f7fbff; border: 1px solid #e6f4ff;
+    .name { font-weight: 700; }
+    .meta { color: #666; font-size: 12px; margin-top: 2px; }
+    .label { margin-top: 6px; font-size: 12px; color: #1677ff; }
+  }
+}
+
+/* 响应式微调 */
+@media (max-width: 768px) {
+  .date-label.large { font-size: 16px; }
+  .tasks .task-row { grid-template-columns: 1fr auto; .project, .assignee { display: none; } }
+}

+ 428 - 0
src/app/pages/team-leader/workload-calendar/workload-calendar.ts

@@ -0,0 +1,428 @@
+import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, isDevMode } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Observable, Subject, Subscription, debounceTime } from 'rxjs';
+import { ProjectService } from '../../../services/project.service';
+import { Task } from '../../../models/project.model';
+import { Router } from '@angular/router';
+
+interface CalendarDay {
+  date: Date;
+  currentMonth: boolean;
+  tasks: Task[];
+}
+
+@Component({
+  selector: 'app-workload-calendar',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  templateUrl: './workload-calendar.html',
+  styleUrls: ['./workload-calendar.scss'],
+  changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class WorkloadCalendarComponent implements OnInit, OnDestroy {
+  view: 'day' | 'week' | 'month' = 'month';
+  selectedDate: Date = new Date();
+  today: Date = new Date();
+  selectedDesigner: string = 'all';
+  designers: string[] = [];
+  tasks: Task[] = [];
+  monthDays: CalendarDay[] = [];
+  showOverdueOnly: boolean = false;
+  expandedDays = new Set<string>();
+  designerStatuses: { name: string; label: string; cls: string; tasksCount: number; overdue: number }[] = [];
+  weekDays: CalendarDay[] = [];
+  // 缓存:模板直接使用,避免频繁函数调用
+  dayTasks: Task[] = [];
+  monthLabel: string = '';
+  // 记忆化:周期筛选缓存
+  private tasksVersion = 0;
+  private memoSignature = '';
+  private memoPeriodTasks: Task[] = [];
+  private monthTaskMap: Map<string, Task[]> = new Map();
+  private monthTaskMapKey: string | null = null; // 记录缓存对应的年月(YYYY-MM)
+  private recompute$ = new Subject<void>();
+  private recomputeSub?: Subscription;
+  private tasksSub?: Subscription;
+
+  constructor(private projectService: ProjectService, private router: Router) {}
+
+  ngOnInit(): void {
+    // 恢复上次的视图状态
+    try {
+      const savedRaw = localStorage.getItem('workloadCalendarState');
+      if (savedRaw) {
+        const saved = JSON.parse(savedRaw);
+        if (saved.view) this.view = saved.view;
+        if (saved.selectedDesigner) this.selectedDesigner = saved.selectedDesigner;
+        if (typeof saved.showOverdueOnly === 'boolean') this.showOverdueOnly = saved.showOverdueOnly;
+        if (saved.selectedDate) this.selectedDate = new Date(saved.selectedDate);
+      }
+    } catch {}
+
+    // 初始化变更去抖订阅:合并快速连续变更并统一重算
+    this.recomputeSub = this.recompute$.pipe(debounceTime(50)).subscribe(() => this.recomputeAll());
+
+    // 开发模式:允许通过 localStorage.mockTasks 注入大数据量以进行性能压测
+    const mockSizeRaw = isDevMode() ? localStorage.getItem('mockTasks') : null;
+    const mockSize = mockSizeRaw ? Number(mockSizeRaw) : NaN;
+    const tasks$ : Observable<Task[]> = isDevMode() && Number.isFinite(mockSize) && mockSize > 0
+      ? (console.info('[WorkloadCalendar] Using mock tasks for benchmarking:', mockSize), this.projectService.getTasks(mockSize))
+      : this.projectService.getTasks();
+
+    this.tasksSub = tasks$.subscribe((tasks: Task[]) => {
+      // 仅将有截止日期的任务纳入排期,并规范化 deadline 类型
+      this.tasks = (tasks || [])
+        .filter((t: Task) => !!t.deadline)
+        .map((t: Task) => ({ ...t, deadline: new Date(t.deadline) } as Task));
+      this.tasksVersion++;
+      this.designers = Array.from(new Set(this.tasks.map(t => t.assignee))).filter(Boolean).sort();
+      // 懒构建:仅为当前视图构建相应数据
+      if (this.view === 'month') this.buildMonthDays();
+      else if (this.view === 'week') this.buildWeekDays();
+      this.computeDesignerStatuses();
+      // 初始化缓存
+      this.monthLabel = this.formatMonthYear(this.selectedDate);
+      this.dayTasks = this.getTasksForDate(this.selectedDate);
+    });
+  }
+
+  ngOnDestroy(): void {
+    this.recomputeSub?.unsubscribe();
+    this.tasksSub?.unsubscribe();
+  }
+
+  private computeDesignerStatuses(): void {
+    if (isDevMode()) console.time('computeDesignerStatuses');
+    const periodTasks = this.getPeriodFilteredTasks();
+    const agg = new Map<string, { count: number; overdue: number }>();
+    for (const t of periodTasks) {
+      const name = t.assignee || '未分配';
+      const cur = agg.get(name) || { count: 0, overdue: 0 };
+      cur.count += 1;
+      if (t.isOverdue) cur.overdue += 1;
+      agg.set(name, cur);
+    }
+    this.designerStatuses = this.designers.map(name => {
+      const a = agg.get(name) || { count: 0, overdue: 0 };
+      let label = '正常', cls = 'normal';
+      if (a.overdue > 0) { label = '异常'; cls = 'abnormal'; }
+      else if (a.count === 0) { label = '空闲'; cls = 'idle'; }
+      else if ((this.view === 'week' && a.count >= 6) || (this.view === 'month' && a.count >= 15) || (this.view === 'day' && a.count >= 3)) { label = '繁忙'; cls = 'busy'; }
+      return { name, label, cls, tasksCount: a.count, overdue: a.overdue };
+    });
+    if (isDevMode()) console.timeEnd('computeDesignerStatuses');
+  }
+
+  private getPeriodFilteredTasks(): Task[] {
+    const tasks = this.tasks.filter(t => (!this.showOverdueOnly || t.isOverdue));
+    const d = this.selectedDate;
+    if (this.view === 'day') {
+      return tasks.filter(t => this.isSameDay(t.deadline, d));
+    }
+    if (this.view === 'week') {
+      const day = d.getDay() || 7;
+      const start = new Date(d); start.setDate(d.getDate() - day + 1);
+      const end = new Date(start); end.setDate(start.getDate() + 6);
+      return tasks.filter(t => t.deadline >= start && t.deadline <= end);
+    }
+    const start = new Date(d.getFullYear(), d.getMonth(), 1);
+    const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
+    return tasks.filter(t => t.deadline >= start && t.deadline <= end);
+  }
+
+  // 计算设计师在当前视图周期下的工作状态
+  getDesignerStatus(name: string): { label: string; cls: string; tasksCount: number; overdue: number } {
+    // 兼容旧方法:直接从一次聚合的结果中取数
+    const all = this.getPeriodFilteredTasks().filter(t => !name || t.assignee === name);
+    const overdue = all.filter(t => t.isOverdue).length;
+    const count = all.length;
+    let label = '正常', cls = 'normal';
+    if (overdue > 0) { label = '异常'; cls = 'abnormal'; }
+    else if (count === 0) { label = '空闲'; cls = 'idle'; }
+    else if ((this.view === 'week' && count >= 6) || (this.view === 'month' && count >= 15) || (this.view === 'day' && count >= 3)) { label = '繁忙'; cls = 'busy'; }
+    return { label, cls, tasksCount: count, overdue };
+  }
+
+  private getPeriodDateRangeTasks(name?: string): Task[] {
+    // 保留方法签名用于兼容,但内部委托至一次性筛选结果
+    const all = this.getPeriodFilteredTasks();
+    return all.filter(t => !name || t.assignee === name);
+  }
+  private saveState(): void {
+    try {
+      localStorage.setItem('workloadCalendarState', JSON.stringify({
+        view: this.view,
+        selectedDesigner: this.selectedDesigner,
+        showOverdueOnly: this.showOverdueOnly,
+        selectedDate: this.selectedDate
+      }));
+    } catch {}
+  }
+
+  private priorityRank(p: any): number {
+    const r: Record<string, number> = { high: 3, medium: 2, low: 1 };
+    const key = typeof p === 'string' ? p.toLowerCase() : String(p);
+    return r[key] ?? 0;
+  }
+
+  switchView(v: 'day' | 'week' | 'month'): void {
+    this.view = v;
+    this.scheduleRecompute();
+  }
+
+  navigateDate(direction: 'prev' | 'next'): void {
+    const d = new Date(this.selectedDate);
+    if (this.view === 'day') {
+      d.setDate(d.getDate() + (direction === 'prev' ? -1 : 1));
+    } else if (this.view === 'week') {
+      d.setDate(d.getDate() + (direction === 'prev' ? -7 : 7));
+    } else {
+      d.setMonth(d.getMonth() + (direction === 'prev' ? -1 : 1));
+    }
+    this.selectedDate = d;
+    this.scheduleRecompute();
+  }
+
+  setToday(): void {
+    this.selectedDate = new Date();
+    this.today = new Date();
+    this.scheduleRecompute();
+  }
+
+  isSameDay(a: Date, b: Date): boolean {
+    return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
+  }
+
+  public isWeekend(d: Date): boolean {
+    const day = d.getDay();
+    return day === 0 || day === 6;
+  }
+
+  private matchesDesigner(t: Task): boolean {
+    return this.selectedDesigner === 'all' || t.assignee === this.selectedDesigner;
+  }
+
+  // 新增:跳转到项目详情
+  navigateToProject(t: Task, ev?: Event): void {
+    if (ev) { ev.stopPropagation(); ev.preventDefault?.(); }
+    if (!t || !t.projectId) return;
+    // 复用设计师端项目详情页面
+    this.router.navigate(['/designer/project-detail', t.projectId]);
+  }
+
+  // 新增:按设计师快速筛选(保持当前日期与视图)
+  filterByDesigner(name: string, ev?: Event): void {
+    if (ev) { ev.stopPropagation(); ev.preventDefault?.(); }
+    if (!name) return;
+    this.selectedDesigner = name;
+    this.scheduleRecompute();
+  }
+
+  onDesignerChange(): void {
+    this.scheduleRecompute();
+  }
+
+  onOverdueOnlyChange(): void {
+    this.scheduleRecompute();
+  }
+
+  private scheduleRecompute(): void {
+    this.recompute$.next();
+  }
+
+  private recomputeAll(): void {
+    if (this.view === 'month') {
+      this.buildMonthDays();
+    } else if (this.view === 'week') {
+      this.buildWeekDays();
+    }
+    this.dayTasks = this.getTasksForDate(this.selectedDate);
+    this.monthLabel = this.formatMonthYear(this.selectedDate);
+    this.computeDesignerStatuses();
+    this.saveState();
+  }
+
+  getTasksForDate(date: Date): Task[] {
+    // 若已有当月的任务分组缓存且日期同当前所选月份,走快捷路径
+    const selKeyYM = `${this.selectedDate.getFullYear()}-${(this.selectedDate.getMonth() + 1).toString().padStart(2, '0')}`;
+    const dateKeyYM = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
+    if (
+      this.monthTaskMap.size > 0 &&
+      this.monthTaskMapKey === selKeyYM &&
+      dateKeyYM === selKeyYM
+    ) {
+      const key = this.toKey(date);
+      const cached = this.monthTaskMap.get(key);
+      if (cached) {
+        return cached.slice();
+      }
+    }
+    return this.tasks
+      .filter(t => this.matchesDesigner(t) && this.isSameDay(t.deadline, date))
+      .filter(t => !this.showOverdueOnly || t.isOverdue)
+      .slice()
+      .sort((a, b) =>
+        Number(b.isOverdue) - Number(a.isOverdue) ||
+        this.priorityRank(b.priority) - this.priorityRank(a.priority) ||
+        (a.deadline as any) - (b.deadline as any)
+      );
+  }
+
+  getWeekDays(): CalendarDay[] {
+    const base = new Date(this.selectedDate);
+    const day = base.getDay() || 7; // 周日归为7
+    const start = new Date(base);
+    start.setDate(base.getDate() - day + 1); // 周一作为一周的第一天
+    const days: CalendarDay[] = [];
+    for (let i = 0; i < 7; i++) {
+      const d = new Date(start);
+      d.setDate(start.getDate() + i);
+      days.push({ date: d, currentMonth: true, tasks: this.getTasksForDate(d) });
+    }
+    return days;
+  }
+
+  buildWeekDays(): void {
+    if (isDevMode()) console.time('buildWeekDays');
+    const base = new Date(this.selectedDate);
+    const day = base.getDay() || 7; // 周日归为7
+    const start = new Date(base);
+    start.setDate(base.getDate() - day + 1); // 周一作为一周的第一天
+    const days: CalendarDay[] = [];
+    for (let i = 0; i < 7; i++) {
+      const d = new Date(start);
+      d.setDate(start.getDate() + i);
+      days.push({ date: d, currentMonth: true, tasks: this.getTasksForDate(d) });
+    }
+    this.weekDays = days;
+    if (isDevMode()) console.timeEnd('buildWeekDays');
+  }
+
+  getDayTasks(): Task[] {
+    // 移除重复排序:getTasksForDate 已经排好序
+    return this.getTasksForDate(this.selectedDate);
+  }
+
+  getPeriodTasks(): Task[] {
+    if (this.view === 'day') return this.getDayTasks();
+    if (this.view === 'week') {
+      return this.weekDays.flatMap(d => d.tasks);
+    }
+    return this.monthDays.flatMap(d => d.tasks);
+  }
+
+  formatMonthYear(d: Date = this.selectedDate): string {
+    return `${d.getFullYear()}年${(d.getMonth() + 1).toString().padStart(2, '0')}月`;
+  }
+
+  formatDateLabel(d: Date): string {
+    return `${d.getMonth() + 1}/${d.getDate()}`;
+  }
+
+  private toKey(d: Date): string {
+    return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
+  }
+  isExpanded(d: Date): boolean { return this.expandedDays.has(this.toKey(d)); }
+  toggleExpand(d: Date, ev?: MouseEvent): void {
+    if (ev) ev.stopPropagation();
+    const k = this.toKey(d);
+    if (this.expandedDays.has(k)) this.expandedDays.delete(k); else this.expandedDays.add(k);
+  }
+
+  selectDate(d: Date): void {
+    this.selectedDate = new Date(d);
+    this.view = 'day';
+    this.scheduleRecompute();
+  }
+
+  buildMonthDays(): void {
+    if (isDevMode()) console.time('buildMonthDays');
+    const year = this.selectedDate.getFullYear();
+    const month = this.selectedDate.getMonth();
+    const firstDay = new Date(year, month, 1);
+    const lastDay = new Date(year, month + 1, 0);
+    // 以周一为一周第一天的偏移(周一=0,周日=6)
+    const firstWeekday = (firstDay.getDay() || 7) - 1;
+    const days: CalendarDay[] = [];
+
+    // 预过滤当月范围+筛选条件(设计师/仅看超期),并一次性按日期分组
+    const monthStart = new Date(year, month, 1);
+    const monthEnd = new Date(year, month + 1, 0);
+    const grouped = new Map<string, Task[]>();
+    const base = this.tasks.filter(t =>
+      this.matchesDesigner(t) && (!this.showOverdueOnly || t.isOverdue) &&
+      t.deadline >= monthStart && t.deadline <= monthEnd
+    );
+    for (const t of base) {
+      const k = this.toKey(t.deadline);
+      const arr = grouped.get(k) || [];
+      arr.push(t);
+      grouped.set(k, arr);
+    }
+    // 统一在分组阶段做排序,避免每格重复排序
+    for (const [k, arr] of grouped) {
+      arr.sort((a, b) =>
+        Number(b.isOverdue) - Number(a.isOverdue) ||
+        this.priorityRank(b.priority) - this.priorityRank(a.priority) ||
+        (a.deadline as any) - (b.deadline as any)
+      );
+      grouped.set(k, arr);
+    }
+
+    // 上月填充
+    for (let i = firstWeekday; i > 0; i--) {
+      const d = new Date(year, month, 1 - i);
+      days.push({ date: d, currentMonth: false, tasks: [] });
+    }
+    // 当月
+    for (let i = 1; i <= lastDay.getDate(); i++) {
+      const d = new Date(year, month, i);
+      const key = this.toKey(d);
+      const tasks = grouped.get(key) || [];
+      days.push({ date: d, currentMonth: true, tasks });
+    }
+    // 下月填充至42格
+    while (days.length < 42) {
+      const last = days[days.length - 1].date;
+      const next = new Date(last);
+      next.setDate(last.getDate() + 1);
+      days.push({ date: next, currentMonth: false, tasks: [] });
+    }
+
+    this.monthDays = days;
+    // 同步月度任务映射缓存
+    this.monthTaskMap = grouped;
+    this.monthTaskMapKey = `${year}-${(month + 1).toString().padStart(2, '0')}`;
+    if (isDevMode()) console.timeEnd('buildMonthDays');
+  }
+
+  // 计算设计师在当前视图周期下的工作状态
+  getDesignerStatusLegacy(name: string): { label: string; cls: string; tasksCount: number; overdue: number } {
+    const periodTasks = this.getPeriodDateRangeTasksLegacy(name);
+    const overdue = periodTasks.filter(t => t.isOverdue).length;
+    const count = periodTasks.length;
+    let label = '正常', cls = 'normal';
+    if (overdue > 0) { label = '异常'; cls = 'abnormal'; }
+    else if (count === 0) { label = '空闲'; cls = 'idle'; }
+    else if ((this.view === 'week' && count >= 6) || (this.view === 'month' && count >= 15) || (this.view === 'day' && count >= 3)) { label = '繁忙'; cls = 'busy'; }
+    return { label, cls, tasksCount: count, overdue };
+  }
+
+  private getPeriodDateRangeTasksLegacy(name?: string): Task[] {
+    const tasks = this.tasks.filter(t => (!name || t.assignee === name) && (!this.showOverdueOnly || t.isOverdue));
+    const d = new Date(this.selectedDate);
+    if (this.view === 'day') {
+      return tasks.filter(t => this.isSameDay(t.deadline, d));
+    }
+    if (this.view === 'week') {
+      const day = d.getDay() || 7;
+      const start = new Date(d); start.setDate(d.getDate() - day + 1);
+      const end = new Date(start); end.setDate(start.getDate() + 6);
+      return tasks.filter(t => t.deadline >= start && t.deadline <= end);
+    }
+    const start = new Date(d.getFullYear(), d.getMonth(), 1);
+    const end = new Date(d.getFullYear(), d.getMonth() + 1, 0);
+    return tasks.filter(t => t.deadline >= start && t.deadline <= end);
+  }
+}

+ 28 - 1
src/app/services/project.service.ts

@@ -264,7 +264,34 @@ export class ProjectService {
   }
 
   // 获取待办任务
-  getTasks(): Observable<Task[]> {
+  getTasks(mockSize?: number): Observable<Task[]> {
+    if (typeof mockSize === 'number' && mockSize > 0) {
+      const now = new Date(this.projects[0]?.deadline || new Date());
+      const year = now.getFullYear();
+      const month = now.getMonth();
+      const pool = ['设计师A','设计师B','设计师C','设计师D','设计师E'];
+      const prios: Array<'high'|'medium'|'low'> = ['high','medium','low'];
+      const gen: Task[] = [];
+      const daysInMonth = new Date(year, month + 1, 0).getDate();
+      for (let i = 0; i < mockSize; i++) {
+        const day = (i % daysInMonth) + 1;
+        const deadline = new Date(year, month, day);
+        gen.push({
+          id: `m${i}`,
+          projectId: String((i % this.projects.length) + 1),
+          projectName: this.projects[i % this.projects.length].name,
+          title: `Mock任务${i}`,
+          stage: '建模',
+          deadline,
+          isOverdue: deadline < new Date(),
+          isCompleted: false,
+          priority: prios[i % prios.length],
+          assignee: pool[i % pool.length],
+          description: '性能压测生成'
+        });
+      }
+      return of(gen);
+    }
     return of(this.tasks);
   }
 

+ 1 - 1
tsconfig.app.json

@@ -4,7 +4,7 @@
   "extends": "./tsconfig.json",
   "compilerOptions": {
     "outDir": "./out-tsc/app",
-    "types": ["echarts", "qrcode"]
+    "types": ["echarts"]
   },
   "include": [
     "src/**/*.ts"