import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; import { firstValueFrom } from 'rxjs'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { ProjectService } from '../../../services/project.service'; import { DesignerService } from '../services/designer.service'; import { WxworkAuth } from 'fmode-ng/core'; import { FmodeParse } from 'fmode-ng/parse'; import { ProjectTimelineComponent } from '../project-timeline'; import { EmployeeDetailPanelComponent } from '../employee-detail-panel'; import { DashboardMetricsComponent } from './components/dashboard-metrics/dashboard-metrics.component'; import { DashboardNavbarComponent } from './components/dashboard-navbar/dashboard-navbar.component'; import { WorkloadGanttComponent } from './components/workload-gantt/workload-gantt.component'; import { TodoSectionComponent } from './components/todo-section/todo-section.component'; import { SmartMatchModalComponent } from './components/smart-match-modal/smart-match-modal.component'; import { StagnationReasonModalComponent } from './components/stagnation-reason-modal/stagnation-reason-modal.component'; import { DashboardFilterBarComponent } from './components/dashboard-filter-bar/dashboard-filter-bar.component'; import { ProjectKanbanComponent } from './components/project-kanban/project-kanban.component'; import { DashboardAlertsComponent } from './components/dashboard-alerts/dashboard-alerts.component'; import type { ProjectTimeline } from '../project-timeline/project-timeline'; import { UrgentEventService } from '../services/urgent-event.service'; import { DesignerWorkloadService } from '../services/designer-workload.service'; import { DashboardNavigationHelper } from '../services/dashboard-navigation.helper'; import { TodoTaskService } from '../services/todo-task.service'; import { DashboardFilterService } from '../services/dashboard-filter.service'; import { DashboardDataService } from '../services/dashboard-data.service'; import { PhaseDeadlines, PhaseName } from '../../../models/project-phase.model'; import { PROJECT_STAGES, CORE_PHASES } from './dashboard.constants'; import { ProjectStage, ProjectPhase, Project, TodoTaskFromIssue, UrgentEvent, LeaveRecord, EmployeeDetail, EmployeeCalendarData, EmployeeCalendarDay } from './dashboard.model'; // ✅ 初始化 Parse SDK const Parse = FmodeParse.with('nova'); @Component({ selector: 'app-dashboard', standalone: true, imports: [ CommonModule, FormsModule, RouterModule, ProjectTimelineComponent, EmployeeDetailPanelComponent, DashboardMetricsComponent, DashboardNavbarComponent, WorkloadGanttComponent, TodoSectionComponent, SmartMatchModalComponent, StagnationReasonModalComponent, DashboardFilterBarComponent, ProjectKanbanComponent, DashboardAlertsComponent ], templateUrl: './dashboard.html', styleUrl: './dashboard.scss', changeDetection: ChangeDetectionStrategy.OnPush }) export class Dashboard implements OnInit, OnDestroy { // 暴露 Array 给模板使用 Array = Array; projects: Project[] = []; filteredProjects: Project[] = []; urgentPinnedProjects: Project[] = []; showAlert: boolean = false; selectedProjectId: string = ''; // 待办任务数据(交给子组件处理显示) todoTasksFromIssues: TodoTaskFromIssue[] = []; loadingTodoTasks: boolean = false; todoTaskError: string = ''; private todoTaskRefreshTimer: any; // 紧急事件数据(交给子组件处理显示) urgentEvents: UrgentEvent[] = []; loadingUrgentEvents: boolean = false; handledUrgentEventIds: Set = new Set(); mutedUrgentEventIds: Set = new Set(); // 新增:当前用户信息 currentUser = { name: '组长', avatar: "data:image/svg+xml,%3Csvg width='40' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23CCFFCC'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3E组长%3C/text%3E%3C/svg%3E", roleName: '组长' }; currentDate = new Date(); // 真实设计师数据(从fmode-ng获取) realDesigners: any[] = []; // 设计师工作量映射(从 ProjectTeam 表) designerWorkloadMap: Map = new Map(); // designerId/name -> projects[] // 智能推荐相关 showSmartMatch: boolean = false; selectedProject: any = null; recommendations: any[] = []; // 🆕 停滞/改图原因弹窗 showStagnationModal: boolean = false; stagnationModalType: 'stagnation' | 'modification' = 'stagnation'; stagnationModalProject: Project | null = null; // 新增:关键词搜索 searchTerm: string = ''; // 临期项目与筛选状态 selectedType: 'all' | 'soft' | 'hard' = 'all'; selectedUrgency: 'all' | 'high' | 'medium' | 'low' = 'all'; selectedStatus: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon' = 'all'; selectedDesigner: string = 'all'; selectedMemberType: 'all' | 'vip' | 'normal' = 'all'; // 新增:时间窗筛选 selectedTimeWindow: 'all' | 'today' | 'threeDays' | 'sevenDays' = 'all'; designers: any[] = []; // 新增:四大板块筛选 selectedCorePhase: 'all' | 'order' | 'requirements' | 'delivery' | 'aftercare' | 'stalled' | 'modification' = 'all'; // 设计师画像(从fmode-ng动态获取,保留此字段作为兼容) designerProfiles: any[] = []; // 10个项目阶段 projectStages = PROJECT_STAGES; // 5大核心阶段(聚合展示) allCorePhases = CORE_PHASES; // 是否显示前期阶段(订单/需求) showPreProductionPhases: boolean = false; get visibleCorePhases(): any[] { if (this.showPreProductionPhases) { return this.allCorePhases; } // 默认隐藏订单和需求阶段 return this.allCorePhases.filter(p => p.id !== 'order' && p.id !== 'requirements'); } // 视图开关 showGanttView: boolean = true; // 个人详情面板相关属性 showEmployeeDetailPanel: boolean = false; selectedEmployeeName: string = ''; selectedEmployeeDetail: any | null = null; selectedEmployeeProjects: any[] = []; // 项目时间轴数据 projectTimelineData: ProjectTimeline[] = []; private timelineDataCache: ProjectTimeline[] = []; private lastDesignerWorkloadMapSize: number = 0; // 员工请假数据 // private leaveRecords: LeaveRecord[] = []; constructor( private projectService: ProjectService, private router: Router, private designerService: DesignerService, private cdr: ChangeDetectorRef, private urgentEventService: UrgentEventService, private designerWorkloadService: DesignerWorkloadService, private navigationHelper: DashboardNavigationHelper, private todoTaskService: TodoTaskService, private dashboardFilterService: DashboardFilterService, private dashboardDataService: DashboardDataService ) {} async ngOnInit(): Promise { // 新增:加载用户Profile信息 await this.loadUserProfile(); // 🔥 优先尝试从云函数加载数据 const cloudData = await this.dashboardDataService.getTeamLeaderDataFromCloud(); if (cloudData) { // 如果云函数成功,使用云端聚合数据 this.processCloudData(cloudData); } else { // 降级方案:使用原有慢速加载 console.warn('⚠️ 云函数加载失败或无数据,回退到本地聚合模式'); await this.loadProjects(); await this.loadDesigners(); // 加载待办任务(从问题板块)- 只有在云函数失败时才单独加载 await this.loadTodoTasksFromIssues(); } // 🆕 计算紧急事件 this.calculateUrgentEvents(); // 启动自动刷新 this.startAutoRefresh(); } /** * 处理云函数返回的数据 */ private processCloudData(data: any): void { try { const { projects, workload, spaceStats, issues } = data; // 1. 转换项目数据 this.projects = projects.map((p: any) => this.transformCloudProject(p)); // 2. 更新设计师工作量数据 // 注意:为了兼容现有逻辑,我们同时更新 designerWorkloadMap 和 realDesigners this.updateDesignerDataFromCloud(workload, projects, spaceStats); // 3. 应用筛选和构建索引 this.buildSearchIndexes(); this.applyFilters(); // 4. 处理待办任务 (Issues) if (issues && Array.isArray(issues)) { this.todoTasksFromIssues = issues; this.loadingTodoTasks = false; console.log(`✅ 云端待办事项加载成功,共 ${issues.length} 条`); } else { // 如果云端没有返回 issues,尝试单独加载 this.loadTodoTasksFromIssues(); } console.log(`✅ 云端数据处理完成,共 ${this.projects.length} 个项目`); } catch (error) { console.error('❌ 处理云端数据失败:', error); // 发生错误时回退 this.loadProjects(); this.loadTodoTasksFromIssues(); } } /** * 将云函数返回的项目对象转换为本地 Project 模型 */ private transformCloudProject(p: any): Project { const deadline = p.deadline ? new Date(p.deadline.iso || p.deadline) : new Date(); const createdAt = p.createdAt ? new Date(p.createdAt.iso || p.createdAt) : undefined; const updatedAt = p.updatedAt ? new Date(p.updatedAt.iso || p.updatedAt) : undefined; // 状态判断 const now = new Date(); const daysLeft = p.daysLeft !== undefined ? p.daysLeft : Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); const isOverdue = p.isOverdue || daysLeft < 0; const dueSoon = !isOverdue && daysLeft <= 3; return { id: p.id, name: p.name, status: p.status, currentStage: p.currentStage || '订单分配', createdAt: createdAt, updatedAt: updatedAt, deadline: deadline, designerName: p.designerName || '未分配', designerId: p.designerIds?.[0] || '', // 云函数需要返回 designerIds 数组,或者暂时留空 designerIds: [], // 云函数暂未返回此字段,后续可优化 type: (p.type === 'hard') ? 'hard' : 'soft', memberType: 'normal', urgency: (p.urgency === 'high' || p.urgency === 'medium') ? p.urgency : 'medium', phases: [], expectedEndDate: deadline, // 🔥 修复:从 data 字段读取停滞期/改图期状态和原因信息(防止云函数覆盖) isStalled: p.data?.isStalled === true, isModification: p.data?.isModification === true, stagnationReasonType: p.data?.stagnationReasonType, stagnationCustomReason: p.data?.stagnationCustomReason, modificationReasonType: p.data?.modificationReasonType, modificationCustomReason: p.data?.modificationCustomReason, estimatedResumeDate: p.data?.estimatedResumeDate ? new Date(p.data.estimatedResumeDate) : undefined, reasonNotes: p.data?.reasonNotes, markedAt: p.data?.markedAt ? new Date(p.data.markedAt) : undefined, markedBy: p.data?.markedBy, isOverdue: isOverdue, overdueDays: isOverdue ? Math.abs(daysLeft) : 0, dueSoon: dueSoon, searchIndex: `${p.name}|${p.designerName}`.toLowerCase(), data: p.data || {}, contact: null, customer: '' } as Project; } /** * 从云端数据更新设计师信息 */ private updateDesignerDataFromCloud(workload: any[], projects: any[], spaceStats?: any): void { // 1. 关键修复:利用 workload 更新 realDesigners,确保甘特图能获取到完整设计师列表 this.realDesigners = workload.map((w: any) => ({ id: w.id, name: w.name, tags: { capacity: { weeklyProjects: w.weeklyCapacity || 3 } } })); // 2. 更新设计师列表(用于筛选下拉框) this.designers = this.realDesigners.map(d => ({ id: d.id, name: d.name })); // 3. 更新 designerProfiles(用于兼容性) this.designerProfiles = workload.map((w: any) => ({ id: w.id, name: w.name, skills: [], workload: w.loadRate, avgRating: 0, experience: 0 })); // 4. 重建 designerWorkloadMap(用于甘特图) this.designerWorkloadMap.clear(); // 遍历所有项目,按设计师分组 projects.forEach((p: any) => { // ✅ 过滤已完成的项目,防止虚高负载 if (p.status === '已完成' || p.status === '已交付') { return; } const designerNames = (p.designerName || '未分配').split(',').map((n: string) => n.trim()); designerNames.forEach((name: string) => { if (!name) return; if (!this.designerWorkloadMap.has(name)) { this.designerWorkloadMap.set(name, []); } // 转换为甘特图需要的格式,并传递 spaceStats const timelineItem = this.transformCloudProjectToTimelineItem(p, name, spaceStats); this.designerWorkloadMap.get(name)?.push(timelineItem); }); }); // 5. 直接生成 timeline 数据 const timelineData: ProjectTimeline[] = []; this.designerWorkloadMap.forEach((items) => { timelineData.push(...items); }); this.projectTimelineData = timelineData; this.timelineDataCache = timelineData; } /** * 将云端项目转换为时间轴项 */ private transformCloudProjectToTimelineItem(p: any, designerName: string, spaceStats?: any): any { // 这里复用 transformCloudProject 的逻辑,但增加时间轴特有字段 const project = this.transformCloudProject(p); const now = new Date(); // 复用 DesignerWorkloadService 中的状态逻辑 let status: 'normal' | 'warning' | 'urgent' | 'overdue' = 'normal'; if (project.isOverdue) status = 'overdue'; else if (project.dueSoon) status = 'urgent'; // 阶段映射:将中文阶段名映射为 Gantt 组件支持的英文 key const stageMap: Record = { '订单分配': 'plan', '方案深化': 'model', '硬装': 'model', '软装': 'decoration', '渲染': 'render', '交付执行': 'delivery', '售后归档': 'aftercare' }; // 如果没有匹配到,默认使用 'model' 或保留原值 const currentStageKey = stageMap[project.currentStage] || 'model'; // ✅ 获取该项目的空间统计数据 const summary = spaceStats ? spaceStats[project.id] : null; return { id: project.id, // ✅ 修复:添加 id 属性以兼容 EmployeeDetailPanel projectId: project.id, projectName: project.name, name: project.name, // ✅ 修复:Gantt Tooltip 需要 name 字段 designerName: designerName, designerId: project.designerId, startDate: project.createdAt || new Date(), endDate: project.deadline, deadline: project.deadline, // ✅ 修复:传递 deadline 给详情面板 deliveryDate: project.deadline, reviewDate: project.deadline, currentStage: currentStageKey, // ✅ 修复:映射为英文 key stageName: project.currentStage, // 保留中文名称用于显示 stageProgress: 50, status: status, isStalled: false, isModification: false, priority: project.urgency === 'high' ? 'high' : 'medium', spaceName: '', customerName: '', phaseDeadlines: undefined, data: project.data, spaceDeliverableSummary: summary // ✅ 传递空间统计数据 }; } /** * 加载项目列表 */ async loadProjects(): Promise { try { const sourceProjects = await firstValueFrom(this.projectService.getProjects()) as any[]; // 获取项目分配信息(用于修正设计师信息,确保跟甘特图一致) const companyId = localStorage.getItem('company') || 'cDL6R1hgSi'; const assignments = await this.designerWorkloadService.getProjectAssignments(companyId); // 数据转换与增强,确保符合Dashboard模型要求 this.projects = sourceProjects.map(p => { const deadline = p.deadline ? new Date(p.deadline) : new Date(); const now = new Date(); const overdueDays = Math.ceil((now.getTime() - deadline.getTime()) / (1000 * 60 * 60 * 24)); // 🔧 增强时间字段获取逻辑 // 尝试从多种路径获取 createdAt let rawCreatedAt = p.createdAt; if (!rawCreatedAt && typeof p.get === 'function') { rawCreatedAt = p.get('createdAt'); } // 如果还是没有,尝试从 data 字段 if (!rawCreatedAt && p.data && p.data.createdAt) { rawCreatedAt = p.data.createdAt; } const createdAtDate = rawCreatedAt ? new Date(rawCreatedAt) : undefined; // 尝试获取 updatedAt let rawUpdatedAt = p.updatedAt; if (!rawUpdatedAt && typeof p.get === 'function') { rawUpdatedAt = p.get('updatedAt'); } const updatedAtDate = rawUpdatedAt ? new Date(rawUpdatedAt) : undefined; // 简单的类型推断 let type: 'soft' | 'hard' = 'soft'; if (p.customerTags && Array.isArray(p.customerTags)) { if (p.customerTags.some((t: any) => t.needType === '硬装')) { type = 'hard'; } } // 获取设计师分配信息 const projectAssignments = assignments.get(p.id) || []; const designerIds = projectAssignments.length > 0 ? projectAssignments.map(d => d.id) : (p.assigneeId ? [p.assigneeId] : []); const displayDesignerName = projectAssignments.length > 0 ? projectAssignments.map(d => d.name).join(', ') : (p.assigneeName || p.designerName || '未分配').trim(); const primaryDesignerId = designerIds[0] || p.assigneeId || ''; return { // 基础字段映射 id: p.id, name: p.name, status: p.status, currentStage: p.currentStage || '订单分配', createdAt: createdAtDate, // ✅ 使用增强后的 createdAt updatedAt: updatedAtDate, // ✅ 传递 updatedAt deadline: deadline, // 字段名称转换 designerName: displayDesignerName, designerId: primaryDesignerId, designerIds: designerIds, // 补充 Dashboard 模型必需的缺省字段 type: type, memberType: 'normal', urgency: 'medium', phases: [], expectedEndDate: deadline, // 从 data 字段读取停滞期/改图期状态和原因信息 isStalled: p.data?.isStalled === true, isModification: p.data?.isModification === true, stagnationReasonType: p.data?.stagnationReasonType, stagnationCustomReason: p.data?.stagnationCustomReason, modificationReasonType: p.data?.modificationReasonType, modificationCustomReason: p.data?.modificationCustomReason, estimatedResumeDate: p.data?.estimatedResumeDate ? new Date(p.data.estimatedResumeDate) : undefined, reasonNotes: p.data?.reasonNotes, markedAt: p.data?.markedAt ? new Date(p.data.markedAt) : undefined, markedBy: p.data?.markedBy, // 计算字段 isOverdue: p.status !== '已完成' && overdueDays > 0, overdueDays: overdueDays > 0 ? overdueDays : 0, dueSoon: p.status !== '已完成' && overdueDays <= 0 && overdueDays >= -3, searchIndex: `${p.name}|${p.assigneeName || p.designerName || ''}`.toLowerCase(), // 保留原始数据供其他用途 data: p.data, contact: p.contact, customer: p.customerName } as Project; }); this.buildSearchIndexes(); this.applyFilters(); console.log(`加载项目成功,共 ${this.projects.length} 个`); console.log(`✅ 加载项目成功,共 ${this.projects.length} 个`); } catch (error) { console.error('加载项目失败:', error); this.projects = []; } } /** * 从fmode-ng加载真实设计师数据 */ async loadDesigners(): Promise { try { this.realDesigners = await this.designerService.getDesigners(); // 更新设计师列表(用于筛选下拉框) this.designers = this.realDesigners.map(d => ({ id: d.id, name: (d.name || '').trim() })).filter(d => !!d.name); // 同时更新designerProfiles以保持兼容性 this.designerProfiles = this.realDesigners.map(d => ({ id: d.id, name: d.name, skills: d.tags.expertise.styles || [], workload: 0, // 后续动态计算 avgRating: d.tags.history.avgRating || 0, experience: 0 // 暂无此字段 })); // 加载设计师的实际工作量 await this.loadDesignerWorkload(); } catch (error) { console.error('加载设计师数据失败:', error); } } /** * 🔧 从 ProjectTeam 表加载每个设计师的实际工作量 */ async loadDesignerWorkload(): Promise { try { const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; // 使用服务加载工作量 this.designerWorkloadMap = await this.designerWorkloadService.loadWorkload(cid); // 转换为时间轴数据 this.projectTimelineData = this.designerWorkloadService.transformToTimeline(this.designerWorkloadMap); // 更新缓存 this.timelineDataCache = this.projectTimelineData; // 统计信息 logging let totalProjectsInMap = 0; this.designerWorkloadMap.forEach(projects => totalProjectsInMap += projects.length); this.lastDesignerWorkloadMapSize = totalProjectsInMap; console.log(`� 加载完成: ${totalProjectsInMap} 个项目分布在 ${this.designerWorkloadMap.size} 个设计师中`); } catch (error) { console.error('加载设计师工作量失败:', error); } } /** * 将筛选后的项目转换为时间轴数据 * ✅ 修复:正确处理多设计师项目,避免下拉列表出现合并的设计师名字 */ convertToProjectTimeline(): void { if (!this.designerWorkloadService) return; // 将筛选后的项目按设计师分组 const groupedMap = new Map(); this.filteredProjects.forEach(p => { // ✅ 修复:如果 designerName 包含逗号,说明是多个设计师,需要拆分 const designerNames = (p.designerName || '未分配') .split(',') .map(name => name.trim()) .filter(name => name.length > 0); // 为每个设计师单独添加项目 designerNames.forEach((designerName, index) => { if (!groupedMap.has(designerName)) { groupedMap.set(designerName, []); } // 创建项目副本,确保每个设计师都有独立的项目条目 const projectCopy = { ...p, designerName: designerName, // 如果有多个设计师,使用对应的 designerId designerId: p.designerIds && p.designerIds[index] ? p.designerIds[index] : p.designerId }; groupedMap.get(designerName)?.push(projectCopy); }); }); // 使用服务转换 this.projectTimelineData = this.designerWorkloadService.transformToTimeline(groupedMap); this.cdr.markForCheck(); } /** * 处理项目点击事件 */ onProjectTimelineClick(projectId: string): void { if (!projectId) { return; } const project = this.projects.find(p => p.id === projectId); const currentStage = project?.currentStage || '订单分配'; this.navigationHelper.navigateToProject(projectId, currentStage); } /** * 构建搜索索引(如果需要) */ private buildSearchIndexes(): void { this.projects.forEach(p => { if (!p.searchIndex) { p.searchIndex = `${p.name}|${p.designerName}`.toLowerCase(); } }); } // 筛选状态改变(由子组件触发) // 重置状态筛选 resetStatusFilter(): void { this.selectedStatus = 'all'; this.applyFilters(); } onFilterChange(filterState: any): void { // 更新本地状态 this.searchTerm = filterState.searchTerm; this.selectedType = filterState.type; this.selectedUrgency = filterState.urgency; this.selectedStatus = filterState.status; this.selectedDesigner = filterState.designer; this.selectedMemberType = filterState.memberType; this.selectedCorePhase = filterState.corePhase; this.selectedProjectId = filterState.projectId; this.selectedTimeWindow = filterState.timeWindow; // 应用筛选 this.applyFilters(); } // 状态筛选(由指标卡片点击触发) filterByStatus(status: string): void { this.selectedStatus = status as any; this.applyFilters(); } // 统一筛选 applyFilters(): void { const criteria = { searchTerm: this.searchTerm, type: this.selectedType, urgency: this.selectedUrgency, status: this.selectedStatus, designer: this.selectedDesigner, memberType: this.selectedMemberType, corePhase: this.selectedCorePhase, timeWindow: this.selectedTimeWindow }; const result = this.dashboardFilterService.filterProjects(this.projects, criteria); this.filteredProjects = result.filteredProjects; this.urgentPinnedProjects = result.urgentPinnedProjects; // 当显示甘特卡片时,同步刷新时间轴 if (this.showGanttView) { this.convertToProjectTimeline(); } } /** * 计算项目加权值 */ calculateWorkloadWeight(project: any): number { return this.designerService.calculateProjectWeight(project); } /** * 获取设计师加权工作量 */ getDesignerWeightedWorkload(designerName: string): { weightedTotal: number; projectCount: number; overdueCount: number; loadRate: number; } { const designerProjects = this.filteredProjects.filter(p => p.designerName === designerName); const weightedTotal = designerProjects.reduce((sum, p) => sum + this.calculateWorkloadWeight(p), 0); const overdueCount = designerProjects.filter(p => p.isOverdue).length; // 从realDesigners获取设计师的单周处理量 const designer = this.realDesigners.find(d => d.name === designerName); const weeklyCapacity = designer?.tags?.capacity?.weeklyProjects || 3; const loadRate = weeklyCapacity > 0 ? (weightedTotal / weeklyCapacity) * 100 : 0; return { weightedTotal, projectCount: designerProjects.length, overdueCount, loadRate }; } /** * 工作量卡片数据(替代ECharts) */ get designerWorkloadCards(): Array<{ name: string; loadRate: number; weightedValue: number; projectCount: number; overdueCount: number; status: 'overload' | 'busy' | 'idle'; }> { const designers = Array.from(new Set(this.filteredProjects.map(p => p.designerName).filter(n => n && n !== '未分配'))); return designers.map(name => { const workload = this.getDesignerWeightedWorkload(name); let status: 'overload' | 'busy' | 'idle' = 'idle'; if (workload.loadRate > 80) status = 'overload'; else if (workload.loadRate > 50) status = 'busy'; return { name, loadRate: workload.loadRate, weightedValue: workload.weightedTotal, projectCount: workload.projectCount, overdueCount: workload.overdueCount, status }; }).sort((a, b) => b.loadRate - a.loadRate); // 按负载率降序 } /** * 获取超负荷设计师数量 */ get overloadedDesignersCount(): number { return this.designerWorkloadCards.filter(d => d.status === 'overload').length; } /** * 获取平均负载率 */ get averageWorkloadRate(): number { const cards = this.designerWorkloadCards; if (cards.length === 0) return 0; const sum = cards.reduce((acc, card) => acc + card.loadRate, 0); return sum / cards.length; } /** * 获取预警汇总数据 */ getAlertSummary(): { totalAlerts: number; overdueHighRisk: Project[]; overloadedDesigners: any[]; dueSoonProjects: Project[]; } { // 1. 超期高危项目(超期>=5天 或 高紧急度超期) const overdueHighRisk = this.filteredProjects .filter(p => p.isOverdue && (p.overdueDays >= 5 || p.urgency === 'high')) .sort((a, b) => b.overdueDays - a.overdueDays) .slice(0, 5); // 2. 超负荷设计师 const overloadedDesigners = this.designerWorkloadCards .filter(d => d.loadRate > 80) .sort((a, b) => b.loadRate - a.loadRate) .slice(0, 5); // 3. 即将到期项目(1-2天内) const now = new Date(); const dueSoonProjects = this.filteredProjects .filter(p => { if (p.isOverdue) return false; const daysLeft = Math.ceil((p.deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); return daysLeft >= 1 && daysLeft <= 2; }) .sort((a, b) => a.deadline.getTime() - b.deadline.getTime()) .slice(0, 5); const totalAlerts = overdueHighRisk.length + overloadedDesigners.length + dueSoonProjects.length; return { totalAlerts, overdueHighRisk, overloadedDesigners, dueSoonProjects }; } /** * 打开智能推荐弹窗 */ async openSmartMatch(project: any): Promise { this.selectedProject = project; this.showSmartMatch = true; try { this.recommendations = await this.designerService.getRecommendedDesigners(project, this.realDesigners); } catch (error) { console.error('智能推荐失败:', error); this.recommendations = []; } } /** * 关闭智能推荐弹窗 */ closeSmartMatch(): void { this.showSmartMatch = false; this.selectedProject = null; this.recommendations = []; } /** * 分配项目给设计师 */ async assignToDesigner(designerId: string): Promise { if (!this.selectedProject) return; try { const success = await this.designerService.assignProject(this.selectedProject.id, designerId); if (success) { this.closeSmartMatch(); await this.loadProjects(); // 重新加载项目数据 } } catch (error) { console.error('❌ 分配项目失败:', error); window?.fmode?.alert('分配失败,请重试'); } } // 切换视图 toggleView(): void { this.showGanttView = !this.showGanttView; if (this.showGanttView) { this.convertToProjectTimeline(); } } ngOnDestroy(): void { // 清理待办任务自动刷新定时器 if (this.todoTaskRefreshTimer) { clearInterval(this.todoTaskRefreshTimer); } } // 🔥 已延期项目 get overdueProjects(): Project[] { return this.projects.filter(p => p.isOverdue); } // ⏳ 临期项目(3天内) get dueSoonProjects(): Project[] { return this.projects.filter(p => p.dueSoon && !p.isOverdue); } // 📋 待审批项目(支持中文和英文阶段名称) get pendingApprovalProjects(): Project[] { const pending = this.projects.filter(p => { const stage = (p.currentStage || '').trim(); const data = (p as any).data || {}; const approvalStatus = data.approvalStatus; // 1. 阶段为"订单分配"且审批状态为 pending // 2. 或者阶段为"待确认"/"待审批"(兼容旧数据) return (stage === '订单分配' && approvalStatus === 'pending') || stage === '待审批' || stage === '待确认'; }); return pending; } // 检查项目是否待审批 isPendingApproval(project: Project): boolean { const stage = (project.currentStage || '').trim(); const stageEn = stage.toLowerCase(); const data: any = (project as any).data || {}; // 🔥 新增:检查顶层的 pendingApproval 字段(备用方案) const topLevelPending = (project as any).pendingApproval === true && (project as any).approvalStage === '订单分配'; return (stage === '订单分配' && (data.approvalStatus === 'pending' || topLevelPending)) || ((stage === '交付执行' || stageEn === 'delivery') && (data.deliveryApproval?.status === 'pending' || (Array.isArray(data.pendingDeliveryApprovals) && data.pendingDeliveryApprovals.some((x: any) => x?.status === 'pending')))); } // 🎯 待分配项目(支持中文和英文阶段名称) get pendingAssignmentProjects(): Project[] { return this.projects.filter(p => { const stage = (p.currentStage || '').trim().toLowerCase(); return stage === 'pendingassignment' || stage === '待分配' || stage === '订单分配'; }); } // 智能推荐设计师 private getRecommendedDesigner(projectType: 'soft' | 'hard') { if (!this.designerProfiles || !this.designerProfiles.length) return null; const scoreOf = (p: any) => { const workloadScore = 100 - (p.workload ?? 0); // 负载越低越好 const ratingScore = (p.avgRating ?? 0) * 10; // 评分越高越好 const expScore = (p.experience ?? 0) * 5; // 经验越高越好 return workloadScore * 0.5 + ratingScore * 0.3 + expScore * 0.2; }; const sorted = [...this.designerProfiles].sort((a, b) => scoreOf(b) - scoreOf(a)); return sorted[0] || null; } // 项目质量评审(由子组件触发) reviewProjectQuality(data: {projectId: string, rating: 'excellent' | 'qualified' | 'unqualified'}): void { const project = this.projects.find(p => p.id === data.projectId); if (!project) return; project.qualityRating = data.rating; if (data.rating === 'unqualified') { project.currentStage = 'revision'; } this.applyFilters(); window?.fmode?.alert('质量评审已提交'); } /** * 根据看板列跳转到项目详情(参考客服板块实现) * @param projectId 项目ID * @param corePhaseId 核心阶段ID (order/requirements/delivery/aftercare/stalled/modification) */ viewProjectDetailsByPhase(projectId: string, corePhaseId: string): void { // 🔧 修复:如果是停滞期或改图期,需要使用项目的实际 currentStage 来确定路由 if (corePhaseId === 'stalled' || corePhaseId === 'modification') { const project = this.projects.find(p => p.id === projectId); if (project) { console.log(`🔧 [路由修复] ${corePhaseId} 项目,使用实际阶段: ${project.currentStage}`); this.navigationHelper.navigateToProject(projectId, project.currentStage); return; } } this.navigationHelper.navigateToProjectByPhase(projectId, corePhaseId); } /** * 🆕 取消项目停滞期状态 */ async cancelProjectStalled(project: Project): Promise { if (!project || !project.id) return; try { console.log(`🔄 [取消停滞期] 开始取消项目停滞期状态: ${project.name}`); const query = new Parse.Query('Project'); const projectObj = await query.get(project.id); if (!projectObj) { console.error('❌ 未找到项目:', project.id); return; } const projectData = projectObj.get('data') || {}; // 清除停滞期相关字段 projectData.isStalled = false; delete projectData.stagnationReasonType; delete projectData.stagnationCustomReason; delete projectData.estimatedResumeDate; delete projectData.reasonNotes; delete projectData.markedAt; delete projectData.markedBy; projectObj.set('data', projectData); await projectObj.save(); console.log(`✅ [取消停滞期] 停滞期状态已取消: ${project.name}`); // 更新本地数据 const index = this.projects.findIndex(p => p.id === project.id); if (index !== -1) { this.projects[index] = { ...this.projects[index], isStalled: false, stagnationReasonType: undefined, stagnationCustomReason: undefined, estimatedResumeDate: undefined, reasonNotes: undefined }; } // 重新应用筛选 this.applyFilters(); this.cdr.markForCheck(); window?.fmode?.alert('停滞期状态已取消'); } catch (error) { console.error('❌ [取消停滞期] 失败:', error); window?.fmode?.alert('取消停滞期失败,请重试'); } } /** * 🆕 取消项目改图期状态 */ async cancelProjectModification(project: Project): Promise { if (!project || !project.id) return; try { console.log(`🔄 [取消改图期] 开始取消项目改图期状态: ${project.name}`); const query = new Parse.Query('Project'); const projectObj = await query.get(project.id); if (!projectObj) { console.error('❌ 未找到项目:', project.id); return; } const projectData = projectObj.get('data') || {}; // 清除改图期相关字段 projectData.isModification = false; delete projectData.modificationReasonType; delete projectData.modificationCustomReason; delete projectData.reasonNotes; delete projectData.markedAt; delete projectData.markedBy; projectObj.set('data', projectData); await projectObj.save(); console.log(`✅ [取消改图期] 改图期状态已取消: ${project.name}`); // 更新本地数据 const index = this.projects.findIndex(p => p.id === project.id); if (index !== -1) { this.projects[index] = { ...this.projects[index], isModification: false, modificationReasonType: undefined, modificationCustomReason: undefined, reasonNotes: undefined }; } // 重新应用筛选 this.applyFilters(); this.cdr.markForCheck(); window?.fmode?.alert('改图期状态已取消'); } catch (error) { console.error('❌ [取消改图期] 失败:', error); window?.fmode?.alert('取消改图期失败,请重试'); } } // 查看项目详情 - 跳转到纯净的项目详情页(无管理端UI) viewProjectDetails(projectId: string): void { if (!projectId) { return; } // 🔥 根据项目当前阶段决定跳转到哪个阶段页面 const project = this.projects.find(p => p.id === projectId); const currentStage = project?.currentStage || '订单分配'; this.navigationHelper.navigateToProject(projectId, currentStage); } // 快速分配项目(增强:加入智能推荐) async quickAssignProject(projectId: string): Promise { const project = this.projects.find(p => p.id === projectId); if (!project) { window?.fmode?.alert('未找到对应项目'); return; } const recommended = this.getRecommendedDesigner(project.type); if (recommended) { const reassigning = !!project.designerName; const message = `推荐设计师:${recommended.name}(工作负载:${recommended.workload}%,评分:${recommended.avgRating}分)` + (reassigning ? `\n\n该项目当前已由「${project.designerName}」负责,是否改为分配给「${recommended.name}」?` : '\n\n是否确认分配?'); const confirmAssign = await window?.fmode?.confirm(message); if (confirmAssign) { project.designerName = recommended.name; if (project.currentStage === 'pendingAssignment' || project.currentStage === 'pendingApproval') { project.currentStage = 'requirement'; } project.status = '进行中'; this.applyFilters(); window?.fmode?.alert(`项目已${reassigning ? '重新' : ''}分配给 ${recommended.name}`); return; } } // 无推荐或用户取消,跳转到详细分配页面 // 跳转到项目详情页 const current = this.projects.find(p => p.id === projectId)?.currentStage || '订单分配'; this.navigationHelper.navigateToProject(projectId, current); } // 预警相关操作(由子组件触发) viewAllOverdueProjects(): void { this.filterByStatus('overdue'); this.showAlert = false; } closeAlert(): void { this.showAlert = false; } // 处理甘特图员工点击事件 async onEmployeeClick(employeeName: string): Promise { if (!employeeName || employeeName === '未分配') { return; } this.selectedEmployeeName = employeeName; this.selectedEmployeeProjects = this.designerWorkloadMap.get(employeeName) || []; this.showEmployeeDetailPanel = true; // 🔥 修复:手动触发变更检测,确保弹窗立即显示 this.cdr.markForCheck(); } // 关闭员工详情面板 closeEmployeeDetailPanel(): void { this.showEmployeeDetailPanel = false; this.selectedEmployeeName = ''; this.selectedEmployeeProjects = []; this.selectedEmployeeDetail = null; } // 员工详情面板:日历月份切换 changeEmployeeCalendarMonth(direction: number): void { // 由 EmployeeDetailPanelComponent 内部处理 console.log('日历月份切换:', direction); } // 员工详情面板:日历日期点击 onCalendarDayClick(day: any): void { // 由 EmployeeDetailPanelComponent 内部处理 console.log('日历日期点击:', day); } // 员工详情面板:刷新问卷 refreshEmployeeSurvey(): void { // 由 EmployeeDetailPanelComponent 内部处理 console.log('刷新员工问卷'); } // 从员工详情面板跳转到项目详情 navigateToProjectFromPanel(projectId: string): void { if (!projectId) { return; } // 关闭员工详情面板 this.closeEmployeeDetailPanel(); // 🔥 根据项目当前阶段决定跳转到哪个阶段页面 const project = this.projects.find(p => p.id === projectId); const currentStage = project?.currentStage || '订单分配'; this.navigationHelper.navigateToProject(projectId, currentStage); } /** * 加载用户Profile信息 */ async loadUserProfile(): Promise { try { const cid = localStorage.getItem("company"); if (!cid) { console.warn('未找到公司ID,使用默认用户信息'); return; } const wwAuth = new WxworkAuth({ cid }); const profile = await wwAuth.currentProfile(); if (profile) { const name = profile.get("name") || profile.get("mobile") || '组长'; const avatar = profile.get("avatar"); const roleName = profile.get("roleName") || '组长'; this.currentUser = { name, avatar: avatar || this.generateDefaultAvatar(name), roleName }; console.log('用户Profile加载成功:', this.currentUser); } } catch (error) { console.error('加载用户Profile失败:', error); // 保持默认值 } } /** * 生成默认头像(SVG格式) * @param name 用户名 * @returns Base64编码的SVG数据URL */ generateDefaultAvatar(name: string): string { const initial = name ? name.substring(0, 2).toUpperCase() : '组长'; const bgColor = '#CCFFCC'; const textColor = '#555555'; const svg = ` ${initial} `; return `data:image/svg+xml,${encodeURIComponent(svg)}`; } // ==================== 新增:待办任务相关方法 ==================== /** * 从问题板块加载待办任务 */ async loadTodoTasksFromIssues(): Promise { this.loadingTodoTasks = true; this.todoTaskError = ''; try { this.todoTasksFromIssues = await this.todoTaskService.getTodoTasks(); console.log(`✅ 加载待办任务成功,共 ${this.todoTasksFromIssues.length} 条`); } catch (error) { console.error('❌ 加载待办任务失败:', error); this.todoTaskError = '加载失败,请稍后重试'; } finally { this.loadingTodoTasks = false; } } /** * 启动自动刷新(每5分钟) */ startAutoRefresh(): void { this.todoTaskRefreshTimer = setInterval(() => { console.log('🔄 自动刷新待办任务...'); this.loadTodoTasksFromIssues(); }, 5 * 60 * 1000); // 5分钟 } /** * 手动刷新待办任务 */ refreshTodoTasks(): void { console.log('🔄 手动刷新待办任务...'); this.loadTodoTasksFromIssues(); this.calculateUrgentEvents(); // 🆕 同时刷新紧急事件 } /** * 🆕 从项目时间轴数据计算紧急事件 * 识别截止时间已到或即将到达但未完成的关键节点 */ calculateUrgentEvents(): void { this.loadingUrgentEvents = true; try { // 使用服务计算紧急事件 this.urgentEvents = this.urgentEventService.calculateUrgentEvents( this.projectTimelineData, this.handledUrgentEventIds, this.mutedUrgentEventIds ); } catch (error) { console.error('❌ 计算紧急事件失败:', error); // 发生错误时清空列表,避免渲染异常数据 this.urgentEvents = []; } finally { this.loadingUrgentEvents = false; // 确保触发变更检测 this.cdr.markForCheck(); } } // 紧急事件处理方法(由子组件触发) confirmEventOnTime(event: UrgentEvent): void { this.mutedUrgentEventIds.add(event.id); this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id); this.cdr.markForCheck(); } resolveUrgentEvent(event: UrgentEvent): void { this.handledUrgentEventIds.add(event.id); this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id); this.cdr.markForCheck(); } async markEventAsStagnant(payload: {event: UrgentEvent, reason: any}): Promise { const { event, reason } = payload; // 更新紧急事件 this.urgentEvents = this.urgentEvents.map(item => { if (item.id !== event.id) return item; const labels = new Set(item.labels || []); labels.add('停滞期'); return { ...item, category: 'customer' as const, statusType: 'stagnant' as const, isMarkedAsStagnant: true, stagnationReasonType: reason.reasonType, stagnationCustomReason: reason.customReason, estimatedResumeDate: reason.estimatedResumeDate, reasonNotes: reason.notes, markedAt: new Date(), markedBy: this.currentUser.name, stagnationDays: item.stagnationDays || 7, labels: Array.from(labels), followUpNeeded: true }; }); // 🆕 同步更新对应的项目对象并保存到数据库 try { await this.updateProjectMarkStatus(event.projectId, 'stagnation', reason); } catch (error) { console.error('❌ 保存停滞期标记失败:', error); } this.cdr.markForCheck(); // TODO: 持久化到后端数据库 this.saveEventMarkToDatabase(event, 'stagnation', reason); } async markEventAsModification(payload: {event: UrgentEvent, reason: any}): Promise { const { event, reason } = payload; // 更新紧急事件 this.urgentEvents = this.urgentEvents.map(item => { if (item.id !== event.id) return item; const labels = new Set(item.labels || []); labels.add('改图期'); return { ...item, statusType: 'modification' as const, isMarkedAsModification: true, modificationReasonType: reason.reasonType, modificationCustomReason: reason.customReason, reasonNotes: reason.notes, markedAt: new Date(), markedBy: this.currentUser.name, labels: Array.from(labels) }; }); // 🆕 同步更新对应的项目对象并保存到数据库 try { await this.updateProjectMarkStatus(event.projectId, 'modification', reason); } catch (error) { console.error('❌ 保存改图期标记失败:', error); } this.cdr.markForCheck(); // TODO: 持久化到后端数据库 this.saveEventMarkToDatabase(event, 'modification', reason); } /** * 更新项目的停滞/改图标记及原因信息 */ private async updateProjectMarkStatus(projectId: string, type: 'stagnation' | 'modification', reason: any): Promise { console.log(`🏷️ [标记] 开始标记项目为${type === 'stagnation' ? '停滞期' : '改图期'}:`, projectId); // 🆕 保存到Parse数据库(先保存数据库,再更新内存) try { console.log(`🔧 [Parse] 使用 Parse SDK 查询项目...`); const query = new Parse.Query('Project'); const project = await query.get(projectId); if (!project) { console.error('❌ 未找到项目:', projectId); return; } const projectData = project.get('data') || {}; if (type === 'stagnation') { projectData.isStalled = true; projectData.isModification = false; projectData.stagnationReasonType = reason.reasonType; projectData.stagnationCustomReason = reason.customReason; projectData.estimatedResumeDate = reason.estimatedResumeDate; projectData.reasonNotes = reason.notes; projectData.markedAt = new Date(); projectData.markedBy = this.currentUser.name; } else { projectData.isModification = true; projectData.isStalled = false; projectData.modificationReasonType = reason.reasonType; projectData.modificationCustomReason = reason.customReason; projectData.reasonNotes = reason.notes; projectData.markedAt = new Date(); projectData.markedBy = this.currentUser.name; } project.set('data', projectData); await project.save(); console.log(`✅ [数据库] ${type === 'stagnation' ? '停滞期' : '改图期'}标记已保存到数据库`, projectId); } catch (error) { console.error('❌ [数据库] 保存标记失败:', error); throw error; } // ✅ 更新内存中的项目数据(与 loadProjects 保持一致,都使用顶层字段) this.projects = this.projects.map(project => { if (project.id !== projectId) return project; console.log(`📝 [内存] 更新项目 ${project.name} 的状态`); if (type === 'stagnation') { return { ...project, isStalled: true, isModification: false, stagnationReasonType: reason.reasonType, stagnationCustomReason: reason.customReason, estimatedResumeDate: reason.estimatedResumeDate, reasonNotes: reason.notes, markedAt: new Date(), markedBy: this.currentUser.name }; } else { return { ...project, isModification: true, isStalled: false, modificationReasonType: reason.reasonType, modificationCustomReason: reason.customReason, reasonNotes: reason.notes, markedAt: new Date(), markedBy: this.currentUser.name }; } }); console.log(`✅ [内存] 项目数组已更新,共 ${this.projects.length} 个项目`); // 统计停滞期和改图期项目数量 const stalledCount = this.projects.filter(p => p.isStalled).length; const modificationCount = this.projects.filter(p => p.isModification).length; console.log(`📊 [统计] 停滞期项目: ${stalledCount} 个,改图期项目: ${modificationCount} 个`); // 重新应用筛选 console.log(`🔄 [筛选] 重新应用筛选规则...`); this.applyFilters(); // ✅ 触发变更检测 this.cdr.markForCheck(); console.log(`✅ [完成] ${type === 'stagnation' ? '停滞期' : '改图期'}标记完成`); } private saveEventMarkToDatabase(event: UrgentEvent, type: 'stagnation' | 'modification', reason: any): void { // TODO: 实现数据持久化逻辑 // 可以保存到 Parse 数据库的 ProjectEvent 或 UrgentEventMark 表 console.log(`💾 保存事件标记到数据库:`, { eventId: event.id, projectId: event.projectId, type, reason, timestamp: new Date() }); } /** * 🔥 将紧急事件转为待办问题(持久化到数据库) */ async createTodoFromEvent(event: UrgentEvent): Promise { const now = new Date(); // 1. 创建本地任务对象(用于立即显示) const tempId = `urgent-todo-${event.id}-${now.getTime()}`; const newTask: TodoTaskFromIssue = { id: tempId, title: `【紧急】${event.title}`, description: event.description, priority: event.urgencyLevel === 'critical' ? 'urgent' : event.urgencyLevel === 'high' ? 'high' : 'medium', type: 'feedback', status: 'open', projectId: event.projectId, projectName: event.projectName, relatedStage: event.phaseName, assigneeName: event.designerName || '待分配', creatorName: this.currentUser.name, createdAt: now, updatedAt: now, dueDate: event.deadline, tags: [...(event.labels || []), '来自紧急事件'] }; // 2. 立即添加到内存(优先显示给用户) this.todoTasksFromIssues = [newTask, ...this.todoTasksFromIssues]; this.cdr.markForCheck(); // 3. 🔥 保存到数据库(关键修复) try { console.log('💾 [待办问题] 开始保存到数据库...', { projectId: event.projectId, title: newTask.title }); // 获取当前用户的 Profile ID const cid = localStorage.getItem("company"); if (!cid) { throw new Error('无法获取公司ID'); } const wwAuth = new WxworkAuth({ cid }); const profile = await wwAuth.currentProfile(); const creatorId = profile?.id; if (!creatorId) { throw new Error('无法获取当前用户ID'); } // 创建 ProjectIssue 对象 const ProjectIssue = Parse.Object.extend('ProjectIssue'); const issueObj = new ProjectIssue(); issueObj.set('project', { __type: 'Pointer', className: 'Project', objectId: event.projectId }); issueObj.set('title', newTask.title); issueObj.set('description', newTask.description || ''); issueObj.set('priority', newTask.priority); issueObj.set('issueType', newTask.type); issueObj.set('status', '待处理'); // 使用中文状态 issueObj.set('creator', { __type: 'Pointer', className: 'Profile', objectId: creatorId }); // 设置关联信息 if (newTask.relatedStage) { issueObj.set('relatedStage', newTask.relatedStage); } if (newTask.dueDate) { issueObj.set('dueDate', newTask.dueDate); } // 设置 data 字段 issueObj.set('data', { tags: newTask.tags || [], comments: [], relatedStage: newTask.relatedStage, sourceEvent: { eventId: event.id, eventType: 'urgent', convertedAt: new Date(), convertedBy: creatorId } }); issueObj.set('isDeleted', false); // 保存到数据库 const saved = await issueObj.save(); console.log('✅ [待办问题] 保存成功:', saved.id); // 4. 更新内存中的任务ID(从临时ID改为真实ID) this.todoTasksFromIssues = this.todoTasksFromIssues.map(task => { if (task.id === tempId) { return { ...task, id: saved.id // 使用数据库返回的真实ID }; } return task; }); // 5. 标记紧急事件为已处理 this.resolveUrgentEvent(event); console.log('✅ [待办问题] 紧急事件已转为待办问题'); window?.fmode?.alert('已成功转为待办问题'); } catch (error) { console.error('❌ [待办问题] 保存失败:', error); // 保存失败,从内存中移除(避免误导用户) this.todoTasksFromIssues = this.todoTasksFromIssues.filter( task => task.id !== tempId ); const errorMsg = error instanceof Error ? error.message : '未知错误'; window?.fmode?.alert(`保存失败:${errorMsg}\n请重试`); } finally { this.cdr.markForCheck(); } } // 待办任务操作(由子组件触发) navigateToIssue(task: TodoTaskFromIssue): void { this.navigationHelper.navigateToIssue(task.projectId, task.id); } markAsRead(task: TodoTaskFromIssue): void { this.todoTasksFromIssues = this.todoTasksFromIssues.filter(t => t.id !== task.id); console.log(`✅ 标记问题为已读: ${task.title}`); } // 标记项目为停滞(从看板触发) markProjectAsStalled(project: Project): void { // 🆕 弹出原因输入弹窗 this.stagnationModalType = 'stagnation'; this.stagnationModalProject = project; this.showStagnationModal = true; } // 标记项目为改图(从看板触发) markProjectAsModification(project: Project): void { // 🆕 弹出原因输入弹窗 this.stagnationModalType = 'modification'; this.stagnationModalProject = project; this.showStagnationModal = true; } // 🆕 确认标记停滞/改图原因 async onStagnationReasonConfirm(reason: any): Promise { if (!this.stagnationModalProject) return; try { // 调用 updateProjectMarkStatus 更新项目并保存到数据库 await this.updateProjectMarkStatus( this.stagnationModalProject.id, this.stagnationModalType, reason ); // 关闭弹窗 this.closeStagnationModal(); // 显示确认消息 const message = this.stagnationModalType === 'stagnation' ? '已标记为停滞项目' : '已标记为改图项目'; window?.fmode?.alert(message); } catch (error) { console.error('❌ 标记失败:', error); window?.fmode?.alert('标记失败,请重试'); } } // 🆕 关闭停滞/改图原因弹窗 closeStagnationModal(): void { this.showStagnationModal = false; this.stagnationModalProject = null; } // 切换前期阶段显示 togglePreProductionPhases(): void { this.showPreProductionPhases = !this.showPreProductionPhases; } }