Browse Source

refactor: extract todo section into reusable component

- Replaced 370+ lines of duplicated todo section HTML with shared TodoSectionComponent
- Consolidated urgent events and todo tasks logic to use team leader interfaces
- Removed redundant type definitions in favor of shared interfaces from team-leader module
徐福静0235668 6 hours ago
parent
commit
1c5155a83c

+ 0 - 0
docs/modal-and-ai-timeout-fix-report.md


+ 0 - 0
docs/stalled-modification-sync-fix.md


+ 0 - 0
scripts/check-stalled-projects.ts


+ 20 - 391
src/app/pages/customer-service/dashboard/dashboard.html

@@ -249,397 +249,26 @@
   </div>
 </section>
 
-<!-- 🆕 待办任务双栏布局(待办问题 + 紧急事件) -->
-<section class="urgent-tasks-section">
-  <div class="section-header">
-    <h2>待办事项</h2>
-  </div>
-  
-  <!-- 🆕 双栏容器 -->
-  <div class="todo-dual-columns">
-    <!-- ========== 左栏:紧急事件 ========== -->
-    <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 (urgentEventsList().length > 0) {
-            <span class="task-count urgent">({{ urgentEventsList().length }})</span>
-          }
-        </h3>
-        <span class="column-subtitle">自动计算的截止事件</span>
-      </div>
-      
-      <!-- 🆕 标签筛选栏 -->
-      <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">{{ urgentEventsList().length }}</span>
-        </button>
-        
-        <button 
-          class="tag-button"
-          [class.active]="urgentEventTagFilter() === 'customer'"
-          (click)="filterUrgentEventsByTag('customer')"
-          title="客户服务预警"
-        >
-          <span class="tag-icon">👥</span>
-          <span class="tag-label">客户服务</span>
-          <span class="tag-count">{{ getTagCount('customer') }}</span>
-        </button>
-        
-        <!-- 工作阶段标签 -->
-        <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() && urgentEventsList().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() && filteredUrgentEvents().length > 0) {
-        <div class="todo-list-compact urgent-list">
-          @for (event of filteredUrgentEvents(); track event.id) {
-            <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 (event.category === 'customer') { 客户 }
-                    </span>
-                    <span class="badge badge-status overdue" *ngIf="event.statusType === 'overdue'">逾期</span>
-                    <span class="badge badge-status upcoming" *ngIf="event.statusType === 'dueSoon'">临近</span>
-                    <span class="badge badge-status stagnant" *ngIf="event.statusType === 'stagnant'">
-                      停滞{{ event.stagnationDays || 7 }}天
-                    </span>
-                    <span class="badge badge-status customer" *ngIf="getEventCategory(event) === 'customer'">客户预警</span>
-                  </div>
-                </div>
-                
-                <!-- 描述 -->
-                <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)"
-                  title="确认可按时交付后隐藏该事件"
-                >
-                  可按时交付
-                </button>
-                <button 
-                  class="btn-action btn-stagnant"
-                  *ngIf="event.statusType !== 'stagnant'"
-                  (click)="markEventAsStagnant(event)"
-                  title="标记为客户停滞期"
-                >
-                  标记停滞
-                </button>
-                <button 
-                  class="btn-action btn-resolve" 
-                  *ngIf="event.allowMarkHandled"
-                  (click)="resolveUrgentEvent(event)"
-                  title="事件已处理,不再提醒"
-                >
-                  事件已处理
-                </button>
-                <button 
-                  class="btn-action btn-todo"
-                  *ngIf="event.allowCreateTodo"
-                  (click)="createTodoFromEvent(event)"
-                  title="将该事件生成代办任务"
-                >
-                  创建代办
-                </button>
-                <button 
-                  class="btn-action btn-view" 
-                  (click)="onUrgentEventViewProject(event.projectId)"
-                  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>
-      }
-      
-      <!-- 过滤后没有结果的空状态 -->
-      @if (!loadingUrgentEvents() && urgentEventsList().length > 0 && filteredUrgentEvents().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>
-      }
-    </div>
-    <!-- ========== 左栏结束 ========== -->
-    
-    <!-- ========== 右栏:待办任务 ========== -->
-    <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)="onTodoTaskMarkAsRead(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>
-  <!-- ========== 双栏容器结束 ========== -->
-</section>
+<!-- ⭐ 复用组长端待办事项组件 -->
+<app-todo-section
+  [todoTasksFromIssues]="todoTasksFromIssues()"
+  [loadingTodoTasks]="loadingTodoTasks()"
+  [todoTaskError]="todoTaskError()"
+  [urgentEvents]="urgentEventsList()"
+  [loadingUrgentEvents]="loadingUrgentEvents()"
+  (refresh)="refreshTodoTasks()"
+  (navigateToIssue)="navigateToIssue($event)"
+  (markAsRead)="onTodoTaskMarkAsRead($event)"
+  (projectClick)="viewProjectDetails($event)"
+  (confirmEventOnTime)="confirmEventOnTime($event)"
+  (markEventAsStagnant)="markEventAsStagnantWithReason($event)"
+  (markEventAsModification)="markEventAsModificationWithReason($event)"
+  (resolveUrgentEvent)="resolveUrgentEvent($event)"
+  (createTodoFromEvent)="createTodoFromEvent($event)">
+</app-todo-section>
+
+<!-- iOS风格的添加紧急事项面板 -->
+   
 
   <!-- iOS风格的添加紧急事项面板 -->
   @if (isTaskFormVisible()) {

+ 102 - 52
src/app/pages/customer-service/dashboard/dashboard.ts

@@ -9,32 +9,9 @@ import { ActivityLogService } from '../../../services/activity-log.service';
 import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
 // 问题板块服务与类型(复用组长端逻辑)
 import { ProjectIssueService, IssuePriority, IssueStatus, IssueType } from '../../../../modules/project/services/project-issue.service';
-// ⭐ 导入紧急事件类型定义(复用组长端)
-// 注意:UrgentEvent 类型与组长端保持一致,便于后续组件化
-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;
-}
+// ⭐ 导入组长端的类型定义
+import { TodoTaskFromIssue, UrgentEvent } from '../../team-leader/dashboard/interfaces';
+import { StagnationReasonData } from '../../team-leader/dashboard/components/stagnation-reason-modal/stagnation-reason-modal.component';
 
 const Parse = FmodeParse.with('nova');
 
@@ -124,30 +101,13 @@ interface IssueUpdate {
   updatedAt: Date;
 }
 
-// 从问题板块映射的待办任务(复用组长端结构)
-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[];
-}
+// ⭐ 导入组长端待办事项组件
+import { TodoSectionComponent } from '../../team-leader/dashboard/components/todo-section/todo-section.component';
 
 @Component({
   selector: 'app-dashboard',
   standalone: true,
-  imports: [CommonModule, FormsModule, RouterModule],
+  imports: [CommonModule, FormsModule, RouterModule, TodoSectionComponent],
   templateUrl: './dashboard.html',
   styleUrls: ['./dashboard.scss', './dashboard-urgent-tasks-enhanced.scss', '../customer-service-styles.scss']
 }) 
@@ -1541,8 +1501,8 @@ onSearchInput(event: Event): void {
       const Parse: any = FmodeParse.with('nova');
       const query = new Parse.Query('ProjectIssue');
       
-      // 筛选条件:待处理 + 处理中
-      query.containedIn('status', ['待处理', '处理中']);
+      // 筛选条件:待处理 + 处理中(同时支持中英文格式)
+      query.containedIn('status', ['待处理', '处理中', 'open', 'in_progress']);
       query.notEqualTo('isDeleted', true);
       
       // 关联数据
@@ -1554,13 +1514,31 @@ onSearchInput(event: Event): void {
       // 限制数量
       query.limit(50);
       
+      console.log('📤 [客服-待办任务] 开始查询 ProjectIssue 表...');
       const results = await query.find();
       console.log(`📊 [客服-待办任务] 找到 ${results.length} 个问题`);
       
+      // 🆕 输出详细的查询结果
+      if (results.length === 0) {
+        console.warn('⚠️ [客服-待办任务] ProjectIssue表中没有符合条件的数据(status=待处理/处理中 且 isDeleted!=true)');
+        console.warn('💡 请检查数据库中是否有待办问题记录');
+      } else {
+        console.log('📋 [客服-待办任务] 前3条问题记录:');
+        results.slice(0, 3).forEach((r: any, i: number) => {
+          const project = r.get('project');
+          const status = r.get('status');
+          const title = r.get('title');
+          console.log(`  ${i + 1}. ID=${r.id}`);
+          console.log(`     status=${status}, title=${title}`);
+          console.log(`     project=${project ? `存在(ID=${project.id})` : '❌ null'}`);
+        });
+      }
+      
       // 数据转换(异步处理以支持 fetch,与组长端一致)
       const tasks: TodoTaskFromIssue[] = await Promise.all(results.map(async (obj: any) => {
         let project = obj.get('project');
         const assignee = obj.get('assignee');
+// ... (rest of the code remains the same)
         const creator = obj.get('creator');
         const data = obj.get('data') || {};
         
@@ -1653,9 +1631,17 @@ onSearchInput(event: Event): void {
   }
   
   /**
-   * 状态映射(中文 -> 英文)
+   * 状态映射(中文 -> 英文,同时兼容英文格式
    */
   private zh2enStatus(status: string): IssueStatus {
+    if (!status) return 'open';
+    
+    // 如果已经是英文格式,直接返回
+    if (['open', 'in_progress', 'resolved', 'closed'].includes(status)) {
+      return status as IssueStatus;
+    }
+    
+    // 中文转英文
     const map: Record<string, IssueStatus> = {
       '待处理': 'open',
       '处理中': 'in_progress',
@@ -1831,8 +1817,8 @@ onSearchInput(event: Event): void {
     
     const resolveCategory = (
       eventType: UrgentEvent['eventType'],
-      category?: 'customer' | 'phase' | 'review' | 'delivery'
-    ): 'customer' | 'phase' | 'review' | 'delivery' => {
+      category?: 'customer' | 'phase' | 'review' | 'delivery' | 'decision'
+    ): 'customer' | 'phase' | 'review' | 'delivery' | 'decision' => {
       if (category) return category;
       switch (eventType) {
         case 'phase_deadline':
@@ -1841,6 +1827,8 @@ onSearchInput(event: Event): void {
           return 'delivery';
         case 'customer_alert':
           return 'customer';
+        case 'decision_needed':
+          return 'decision';
         default:
           return 'review';
       }
@@ -2121,6 +2109,66 @@ onSearchInput(event: Event): void {
     this.urgentEventsList.set(updated);
   }
 
+  /**
+   * ⭐ 标记为停滞期(带原因数据)- 兼容组长端组件输出
+   */
+  markEventAsStagnantWithReason(data: {event: UrgentEvent, reason: StagnationReasonData}): void {
+    const { event, reason } = data;
+    console.log('🔴 [紧急事件] 标记为停滞期:', event.title, reason);
+    
+    const updated = this.urgentEventsList().map(item => {
+      if (item.id !== event.id) {
+        return item;
+      }
+      const labels = new Set(item.labels || []);
+      labels.add('停滞期');
+      return {
+        ...item,
+        category: 'customer' as const,
+        statusType: 'stagnant' as const,
+        stagnationDays: item.stagnationDays || 7,
+        labels: Array.from(labels),
+        followUpNeeded: true
+      };
+    });
+    this.urgentEventsList.set(updated);
+  }
+
+  /**
+   * ⭐ 标记为改图期(带原因数据)- 兼容组长端组件输出
+   */
+  markEventAsModificationWithReason(data: {event: UrgentEvent, reason: StagnationReasonData}): void {
+    const { event, reason } = data;
+    console.log('🟠 [紧急事件] 标记为改图期:', event.title, reason);
+    
+    const updated = this.urgentEventsList().map(item => {
+      if (item.id !== event.id) {
+        return item;
+      }
+      const labels = new Set(item.labels || []);
+      labels.add('改图期');
+      return {
+        ...item,
+        category: 'review' as const,
+        statusType: 'modification' as const,
+        labels: Array.from(labels),
+        followUpNeeded: true
+      };
+    });
+    this.urgentEventsList.set(updated);
+  }
+
+  /**
+   * ⭐ 点击项目(从待办任务或紧急事件)
+   */
+  viewProjectDetails(projectId: string): void {
+    console.log('🔍 [客服] 查看项目:', projectId);
+    const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
+    this.router.navigate(['/wxwork', cid, 'project', projectId, 'order'], {
+      queryParams: { roleName: 'customer-service' }
+    });
+  }
+
   createTodoFromEvent(event: UrgentEvent): void {
     const now = new Date();
     const newTask: TodoTaskFromIssue = {
@@ -2153,7 +2201,7 @@ onSearchInput(event: Event): void {
     return tag?.count || 0;
   }
 
-  getEventCategory(event: UrgentEvent): 'customer' | 'phase' | 'review' | 'delivery' {
+  getEventCategory(event: UrgentEvent): 'customer' | 'phase' | 'review' | 'delivery' | 'decision' {
     if (event.category) return event.category;
     switch (event.eventType) {
       case 'phase_deadline':
@@ -2164,6 +2212,8 @@ onSearchInput(event: Event): void {
         return 'review';
       case 'customer_alert':
         return 'customer';
+      case 'decision_needed':
+        return 'decision';
       default:
         return 'review';
     }

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

@@ -277,19 +277,27 @@
 
 
 
-      <!-- 视图:监控大盘模式 -->
+      <!-- 视图:监控大盘模式(项目看板) -->
       @if (viewMode() === 'dashboard') {
         <div class="dashboard-container">
-          <!-- 使用iframe嵌入组长端监控大盘,通过CSS隐藏待办任务栏 -->
-          <iframe 
-            src="/team-leader/dashboard" 
-            frameborder="0" 
-            width="100%" 
-            height="100%"
-            style="min-height: 800px;"
-            title="项目监控大盘"
-            onload="const doc = this.contentDocument; doc.querySelector('.todo-section').style.display = 'none'; const header = doc.querySelector('.dashboard-header h1'); if (header && header.textContent.includes('设计组长工作台')) { header.style.display = 'none'; doc.querySelector('.dashboard-metrics').style.marginTop = '20px'; }">
-          </iframe>
+          <!-- 🆕 使用组长端的ProjectKanbanComponent -->
+          <div class="kanban-header">
+            <h2>项目监控大盘</h2>
+            <p class="kanban-subtitle">实时查看所有项目进度和状态</p>
+          </div>
+          
+          <app-project-kanban
+            [corePhases]="corePhases"
+            [projects]="kanbanProjects()"
+            (viewProject)="handleViewProject($event)"
+            (openSmartMatch)="handleSmartMatch($event)"
+            (assignProject)="handleAssignProject($event)"
+            (reviewProject)="handleReviewProject($event)"
+            (markStalled)="handleMarkStalled($event)"
+            (markModification)="handleMarkModification($event)"
+            (cancelStalled)="handleCancelStalled($event)"
+            (cancelModification)="handleCancelModification($event)">
+          </app-project-kanban>
         </div>
       }
     </div>

+ 27 - 8
src/app/pages/customer-service/project-list/project-list.scss

@@ -1361,17 +1361,36 @@ $transition: all 0.3s ease;
 // 监控大盘视图样式
 .dashboard-container {
   width: 100%;
-  height: 100%;
   min-height: 800px;
-  border: none;
+  padding: 20px;
+  background: #f5f7fa;
   
-  iframe {
+  // 🆕 看板头部样式
+  .kanban-header {
+    margin-bottom: 24px;
+    padding: 20px 24px;
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    border-radius: 12px;
+    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
+    
+    h2 {
+      margin: 0 0 8px 0;
+      font-size: 24px;
+      font-weight: 600;
+      color: white;
+    }
+    
+    .kanban-subtitle {
+      margin: 0;
+      font-size: 14px;
+      color: rgba(255, 255, 255, 0.9);
+    }
+  }
+  
+  // 看板组件样式
+  app-project-kanban {
+    display: block;
     width: 100%;
-    height: 100%;
-    min-height: 800px;
-    border: none;
-    border-radius: 8px;
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
   }
 }
 

+ 98 - 1
src/app/pages/customer-service/project-list/project-list.ts

@@ -9,6 +9,9 @@ import { Project, ProjectStatus, ProjectStage } from '../../../models/project.mo
 import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
 import { ProfileService } from '../../../services/profile.service';
 import { normalizeStage, getProjectStatusByStage } from '../../../utils/project-stage-mapper';
+import { ProjectKanbanComponent } from '../../team-leader/dashboard/components/project-kanban/project-kanban.component';
+import { CORE_PHASES } from '../../team-leader/dashboard/dashboard.constants';
+import type { Project as TeamLeaderProject } from '../../team-leader/dashboard/interfaces';
 
 const Parse = FmodeParse.with('nova');
 
@@ -23,7 +26,7 @@ interface ProjectListItem extends Project {
 @Component({
   selector: 'app-project-list',
   standalone: true,
-  imports: [CommonModule, FormsModule, RouterModule, MatDialogModule],
+  imports: [CommonModule, FormsModule, RouterModule, MatDialogModule, ProjectKanbanComponent],
   templateUrl: './project-list.html',
   styleUrls: ['./project-list.scss', '../customer-service-styles.scss']
 })
@@ -45,6 +48,41 @@ export class ProjectList implements OnInit, OnDestroy {
     { id: 'aftercare', name: '售后' }
   ] as const;
 
+  // 🆕 看板核心阶段配置(用于监控大盘)
+  corePhases = CORE_PHASES;
+
+  // 🆕 转换为看板项目格式
+  kanbanProjects = computed<TeamLeaderProject[]>(() => {
+    const now = new Date();
+    
+    return this.allProjects().map(p => {
+      const deadline = p.deadline || new Date();
+      const isOverdue = deadline < now;
+      const daysUntilDeadline = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
+      
+      return {
+        id: p.id,
+        name: p.name || '未命名项目',
+        type: 'soft' as const, // 默认为软装项目
+        memberType: 'normal' as const, // 默认为普通会员
+        currentStage: p.currentStage || p.stage || 'order',
+        designerName: p.assigneeName || '未分配',
+        status: p.status || 'normal',
+        expectedEndDate: deadline,
+        deadline: deadline,
+        createdAt: p.createdAt || now,
+        isOverdue: isOverdue,
+        overdueDays: isOverdue ? Math.abs(daysUntilDeadline) : 0,
+        dueSoon: daysUntilDeadline <= 3 && daysUntilDeadline > 0,
+        urgency: (isOverdue ? 'high' : daysUntilDeadline <= 3 ? 'medium' : 'low') as 'high' | 'medium' | 'low',
+        phases: [], // 客服端不需要详细阶段信息
+        isStalled: (p as any).isStalled || false,
+        isModification: (p as any).isModification || false,
+        qualityRating: 'pending' as const
+      } as TeamLeaderProject;
+    });
+  });
+
   // 基础项目集合(服务端返回 + 本地生成),用于二次处理
   private baseProjects: Project[] = [];
 
@@ -948,6 +986,65 @@ export class ProjectList implements OnInit, OnDestroy {
     this.router.navigate(['/customer-service/messages'], { queryParams: { projectId: project.id } });
   }
 
+  // ==================== 🆕 看板事件处理方法 ====================
+  
+  /**
+   * 查看项目详情
+   */
+  handleViewProject(event: {projectId: string, phaseId: string}): void {
+    const cid = localStorage.getItem('company') || '';
+    this.router.navigate(['/wxwork', cid, 'project', event.projectId]);
+  }
+
+  /**
+   * 智能匹配(客服端不需要此功能)
+   */
+  handleSmartMatch(project: TeamLeaderProject): void {
+    console.log('客服端不支持智能匹配功能');
+  }
+
+  /**
+   * 分配项目(客服端不需要此功能)
+   */
+  handleAssignProject(projectId: string): void {
+    console.log('客服端不支持分配项目功能');
+  }
+
+  /**
+   * 评审项目(客服端不需要此功能)
+   */
+  handleReviewProject(event: {projectId: string, rating: string}): void {
+    console.log('客服端不支持评审项目功能');
+  }
+
+  /**
+   * 标记停滞(客服端不需要此功能)
+   */
+  handleMarkStalled(project: TeamLeaderProject): void {
+    console.log('客服端不支持标记停滞功能');
+  }
+
+  /**
+   * 标记改图(客服端不需要此功能)
+   */
+  handleMarkModification(project: TeamLeaderProject): void {
+    console.log('客服端不支持标记改图功能');
+  }
+
+  /**
+   * 取消停滞(客服端不需要此功能)
+   */
+  handleCancelStalled(project: TeamLeaderProject): void {
+    console.log('客服端不支持取消停滞功能');
+  }
+
+  /**
+   * 取消改图(客服端不需要此功能)
+   */
+  handleCancelModification(project: TeamLeaderProject): void {
+    console.log('客服端不支持取消改图功能');
+  }
+
   // 导航到创建订单页面
   navigateToCreateOrder() {
     // 打开咨询订单弹窗

+ 25 - 4
src/app/pages/team-leader/services/todo-task.service.ts

@@ -15,11 +15,12 @@ export class TodoTaskService {
    */
   async getTodoTasks(): Promise<TodoTaskFromIssue[]> {
     try {
+      console.log('🔍 [TodoTaskService] 开始加载待办任务...');
       const Parse: any = FmodeParse.with('nova');
       const query = new Parse.Query('ProjectIssue');
       
-      // 筛选条件:待处理 + 处理中
-      query.containedIn('status', ['待处理', '处理中']);
+      // 筛选条件:待处理 + 处理中(同时支持中英文格式)
+      query.containedIn('status', ['待处理', '处理中', 'open', 'in_progress']);
       query.notEqualTo('isDeleted', true);
       
       // 关联数据
@@ -31,9 +32,21 @@ export class TodoTaskService {
       // 限制数量
       query.limit(50);
       
+      console.log('📤 [TodoTaskService] 开始查询 ProjectIssue 表...');
       const results = await query.find();
       
-      console.log(`📥 查询到 ${results.length} 条问题记录`);
+      console.log(`📥 [TodoTaskService] 查询到 ${results.length} 条问题记录`);
+      
+      // 🆕 输出详细的查询结果
+      if (results.length === 0) {
+        console.warn('⚠️ [TodoTaskService] ProjectIssue表中没有符合条件的数据(status=待处理/处理中 且 isDeleted!=true)');
+      } else {
+        console.log('📊 [TodoTaskService] 问题记录详情:');
+        results.slice(0, 3).forEach((r: any, i: number) => {
+          const project = r.get('project');
+          console.log(`  ${i + 1}. ID=${r.id}, status=${r.get('status')}, title=${r.get('title')}, project=${project ? project.id : 'null'}`);
+        });
+      }
       
       // 数据转换
       const tasks = await Promise.all(results.map(async (obj: any) => {
@@ -104,8 +117,16 @@ export class TodoTaskService {
     }
   }
 
-  // 状态映射辅助方法
+  // 状态映射辅助方法(兼容中英文格式)
   private zh2enStatus(status: string): IssueStatus {
+    if (!status) return 'open';
+    
+    // 如果已经是英文格式,直接返回
+    if (['open', 'in_progress', 'resolved', 'closed'].includes(status)) {
+      return status as IssueStatus;
+    }
+    
+    // 中文转英文
     const map: Record<string, IssueStatus> = {
       '待处理': 'open',
       '处理中': 'in_progress',