import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Router, RouterModule } from '@angular/router'; import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core'; import { ProjectService } from '../../../services/project.service'; // 项目阶段定义 interface ProjectStage { id: string; name: string; order: number; } interface ProjectPhase { name: string; percentage: number; startPercentage: number; isCompleted: boolean; isCurrent: boolean; } interface Project { id: string; name: string; type: 'soft' | 'hard'; memberType: 'vip' | 'normal'; designerName: string; status: string; expectedEndDate: Date; // TODO: 兼容旧字段,后续可移除 deadline: Date; // 真实截止时间字段 createdAt?: Date; // 真实开始时间字段(可选) isOverdue: boolean; overdueDays: number; dueSoon: boolean; urgency: 'high' | 'medium' | 'low'; phases: ProjectPhase[]; currentStage: string; // 新增:当前项目阶段 // 新增:质量评级 qualityRating?: 'excellent' | 'qualified' | 'unqualified' | 'pending'; lastCustomerFeedback?: string; // 预构建的搜索索引,减少重复 toLowerCase 与拼接 searchIndex?: string; } interface TodoTask { id: string; title: string; description: string; deadline: Date; priority: 'high' | 'medium' | 'low'; type: 'review' | 'assign' | 'performance'; targetId: string; } declare const echarts: any; @Component({ selector: 'app-dashboard', imports: [CommonModule, FormsModule, RouterModule], templateUrl: './dashboard.html', styleUrl: './dashboard.scss' }) export class Dashboard implements OnInit, OnDestroy { projects: Project[] = []; filteredProjects: Project[] = []; todoTasks: TodoTask[] = []; overdueProjects: Project[] = []; urgentPinnedProjects: Project[] = []; showAlert: boolean = false; selectedProjectId: string = ''; // 新增:关键词搜索 searchTerm: string = ''; searchSuggestions: Project[] = []; showSuggestions: boolean = false; private hideSuggestionsTimer: any; // 搜索性能与交互控制 private searchDebounceTimer: any; private readonly SEARCH_DEBOUNCE_MS = 200; // 防抖时长(毫秒) private readonly MIN_SEARCH_LEN = 2; // 最小触发建议长度 private readonly MAX_SUGGESTIONS = 8; // 建议最大条数 private isSearchFocused: boolean = false; // 是否处于输入聚焦态 // 新增:临期项目与筛选状态 dueSoonProjects: Project[] = []; 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: string[] = []; // 新增:四大板块筛选 selectedCorePhase: 'all' | 'order' | 'requirements' | 'delivery' | 'aftercare' = 'all'; // 设计师画像(用于智能推荐) designerProfiles: any[] = [ { id: 'zhang', name: '张三', skills: ['现代风格', '北欧风格'], workload: 70, avgRating: 4.5, experience: 3 }, { id: 'li', name: '李四', skills: ['新中式', '宋式风格'], workload: 45, avgRating: 4.8, experience: 5 }, { id: 'wang', name: '王五', skills: ['北欧风格', '日式风格'], workload: 85, avgRating: 4.2, experience: 2 }, { id: 'zhao', name: '赵六', skills: ['现代风格', '轻奢风格'], workload: 30, avgRating: 4.6, experience: 4 } ]; // 10个项目阶段 projectStages: ProjectStage[] = [ { id: 'pendingApproval', name: '待确认', order: 1 }, { id: 'pendingAssignment', name: '待分配', order: 2 }, { id: 'requirement', name: '需求沟通', order: 3 }, { id: 'planning', name: '方案规划', order: 4 }, { id: 'modeling', name: '建模阶段', order: 5 }, { id: 'rendering', name: '渲染阶段', order: 6 }, { id: 'postProduction', name: '后期处理', order: 7 }, { id: 'review', name: '方案评审', order: 8 }, { id: 'revision', name: '方案修改', order: 9 }, { id: 'delivery', name: '交付完成', order: 10 } ]; // 5大核心阶段(聚合展示) corePhases: ProjectStage[] = [ { id: 'order', name: '订单创建', order: 1 }, // 待确认、待分配 { id: 'requirements', name: '确认需求', order: 2 }, // 需求沟通、方案规划 { id: 'delivery', name: '交付执行', order: 3 }, // 建模、渲染、后期/评审/修改 { id: 'aftercare', name: '售后', order: 4 } // 交付完成 → 售后 ]; // 甘特视图开关与实例引用 showGanttView: boolean = false; private ganttChart: any | null = null; @ViewChild('ganttChartRef', { static: false }) ganttChartRef!: ElementRef; // 新增:工作量概览图表引用与实例 @ViewChild('workloadChartRef', { static: false }) workloadChartRef!: ElementRef; private workloadChart: any | null = null; workloadDimension: 'designer' | 'member' = 'designer'; // 甘特时间尺度:仅周/月 ganttScale: 'week' | 'month' = 'week'; // 新增:甘特模式(项目 / 设计师排班) ganttMode: 'project' | 'designer' = 'project'; constructor(private projectService: ProjectService, private router: Router) {} ngOnInit(): void { this.loadProjects(); this.loadTodoTasks(); // 首次微任务后尝试初始化一次,确保容器已渲染 setTimeout(() => this.updateWorkloadChart(), 0); } loadProjects(): void { // 模拟数据加载 - 增强数据结构,添加currentStage this.projects = [ { id: 'proj-001', name: '现代风格客厅设计', type: 'soft', memberType: 'vip', designerName: '张三', status: '进行中', expectedEndDate: new Date(2023, 9, 15), deadline: new Date(2023, 9, 15), isOverdue: true, overdueDays: 2, dueSoon: false, urgency: 'high', currentStage: 'rendering', phases: [ { name: '建模', percentage: 15, startPercentage: 0, isCompleted: true, isCurrent: false }, { name: '渲染', percentage: 20, startPercentage: 15, isCompleted: false, isCurrent: true }, { name: '后期', percentage: 15, startPercentage: 35, isCompleted: false, isCurrent: false }, { name: '交付', percentage: 50, startPercentage: 50, isCompleted: false, isCurrent: false } ] }, { id: 'proj-002', name: '北欧风格卧室设计', type: 'soft', memberType: 'normal', designerName: '李四', status: '进行中', expectedEndDate: new Date(2023, 9, 20), deadline: new Date(2023, 9, 20), isOverdue: false, overdueDays: 0, dueSoon: false, urgency: 'medium', currentStage: 'postProduction', phases: [ { name: '建模', percentage: 15, startPercentage: 0, isCompleted: true, isCurrent: false }, { name: '渲染', percentage: 20, startPercentage: 15, isCompleted: true, isCurrent: false }, { name: '后期', percentage: 15, startPercentage: 35, isCompleted: false, isCurrent: true }, { name: '交付', percentage: 50, startPercentage: 50, isCompleted: false, isCurrent: false } ] }, { id: 'proj-003', name: '新中式餐厅设计', type: 'hard', memberType: 'normal', designerName: '王五', status: '进行中', expectedEndDate: new Date(2023, 9, 25), deadline: new Date(2023, 9, 25), isOverdue: false, overdueDays: 0, dueSoon: false, urgency: 'low', currentStage: 'modeling', phases: [ { name: '建模', percentage: 15, startPercentage: 0, isCompleted: false, isCurrent: true }, { name: '渲染', percentage: 20, startPercentage: 15, isCompleted: false, isCurrent: false }, { name: '后期', percentage: 15, startPercentage: 35, isCompleted: false, isCurrent: false }, { name: '交付', percentage: 50, startPercentage: 50, isCompleted: false, isCurrent: false } ] }, { id: 'proj-004', name: '工业风办公室设计', type: 'hard', memberType: 'normal', designerName: '赵六', status: '进行中', expectedEndDate: new Date(2023, 9, 10), deadline: new Date(2023, 9, 10), isOverdue: true, overdueDays: 7, dueSoon: false, urgency: 'high', currentStage: 'review', phases: [ { name: '建模', percentage: 15, startPercentage: 0, isCompleted: true, isCurrent: false }, { name: '渲染', percentage: 20, startPercentage: 15, isCompleted: true, isCurrent: false }, { name: '后期', percentage: 15, startPercentage: 35, isCompleted: false, isCurrent: false }, { name: '交付', percentage: 50, startPercentage: 50, isCompleted: false, isCurrent: false } ] }, // 添加更多不同阶段的项目 { id: 'proj-005', name: '现代简约厨房设计', type: 'soft', memberType: 'normal', designerName: '', status: '待分配', expectedEndDate: new Date(2023, 10, 5), deadline: new Date(2023, 10, 5), isOverdue: false, overdueDays: 0, dueSoon: false, urgency: 'medium', currentStage: 'pendingAssignment', phases: [] }, { id: 'proj-006', name: '日式风格书房设计', type: 'hard', memberType: 'normal', designerName: '', status: '待确认', expectedEndDate: new Date(2023, 10, 10), deadline: new Date(2023, 10, 10), isOverdue: false, overdueDays: 0, dueSoon: false, urgency: 'low', currentStage: 'pendingApproval', phases: [] }, { id: 'proj-007', name: '轻奢风格浴室设计', type: 'soft', memberType: 'normal', designerName: '钱七', status: '已完成', expectedEndDate: new Date(2023, 9, 5), deadline: new Date(2023, 9, 5), isOverdue: false, overdueDays: 0, dueSoon: false, urgency: 'medium', currentStage: 'delivery', phases: [] } ]; // ===== 追加生成示例数据:保证总量达到100条 ===== const stageIds = this.projectStages.map(s => s.id); const designers = ['张三','李四','王五','赵六','钱七','孙八','周九','吴十','郑一','冯二','陈三','褚四']; const statusMap: Record = { pendingApproval: '待确认', pendingAssignment: '待分配', requirement: '进行中', planning: '进行中', modeling: '进行中', rendering: '进行中', postProduction: '进行中', review: '进行中', revision: '进行中', delivery: '已完成' }; for (let i = 8; i <= 100; i++) { const stageIndex = (i - 1) % stageIds.length; const currentStage = stageIds[stageIndex]; const type: 'soft' | 'hard' = i % 2 === 0 ? 'soft' : 'hard'; const urgency: 'high' | 'medium' | 'low' = i % 5 === 0 ? 'high' : (i % 3 === 0 ? 'medium' : 'low'); const isOverdue = ['planning','modeling','rendering','postProduction','review','revision','delivery'].includes(currentStage) ? i % 7 === 0 : false; const overdueDays = isOverdue ? (i % 10) + 1 : 0; const hasDesigner = !['pendingApproval', 'pendingAssignment'].includes(currentStage); const designerName = hasDesigner ? designers[i % designers.length] : ''; const status = statusMap[currentStage] || '进行中'; const expectedEndDate = new Date(); const daysOffset = isOverdue ? - (overdueDays + (i % 5)) : ((i % 20) + 3); expectedEndDate.setDate(expectedEndDate.getDate() + daysOffset); const daysToDeadline = Math.ceil((expectedEndDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); const dueSoon = !isOverdue && daysToDeadline >= 0 && daysToDeadline <= 3; const memberType: 'vip' | 'normal' = i % 4 === 0 ? 'vip' : 'normal'; this.projects.push({ id: `proj-${String(i).padStart(3, '0')}`, name: `${type === 'soft' ? '软装' : '硬装'}示例项目 ${i}`, type, memberType, designerName, status, expectedEndDate, deadline: expectedEndDate, isOverdue, overdueDays, dueSoon, urgency, currentStage, phases: [] }); } // ===== 示例数据生成结束 ===== // 统一补齐真实时间字段(deadline/createdAt),以真实字段贯通筛选与甘特 const DAY = 24 * 60 * 60 * 1000; this.projects = this.projects.map(p => { const deadline = p.deadline || p.expectedEndDate; const baseDays = p.type === 'hard' ? 30 : 14; const createdAt = p.createdAt || new Date(new Date(deadline).getTime() - baseDays * DAY); return { ...p, deadline, createdAt } as Project; }); // 筛选超期与临期项目 this.overdueProjects = this.projects.filter(project => project.isOverdue); this.dueSoonProjects = this.projects.filter(project => project.dueSoon && !project.isOverdue); this.filteredProjects = [...this.projects]; // 供筛选用的设计师列表 this.designers = Array.from(new Set(this.projects.map(p => p.designerName).filter(n => !!n))); // 显示超期提醒 if (this.overdueProjects.length > 0) { this.showAlert = true; } } loadTodoTasks(): void { // 模拟待办任务数据 this.todoTasks = [ { id: 'todo-001', title: '待评审效果图', description: '现代风格客厅设计项目需要进行效果图评审', deadline: new Date(2023, 9, 18, 15, 0), priority: 'high', type: 'review', targetId: 'proj-001' }, { id: 'todo-002', title: '待分配项目', description: '新中式厨房设计项目需要分配给合适的设计师', deadline: new Date(2023, 9, 19, 10, 0), priority: 'high', type: 'assign', targetId: 'proj-new' }, { id: 'todo-003', title: '待确认绩效', description: '9月份团队绩效需要进行审核确认', deadline: new Date(2023, 9, 22, 18, 0), priority: 'medium', type: 'performance', targetId: 'sep-2023' }, { id: 'todo-004', title: '待处理客户反馈', description: '北欧风格卧室设计项目有客户反馈需要处理', deadline: new Date(2023, 9, 20, 14, 0), priority: 'medium', type: 'review', targetId: 'proj-002' }, { id: 'todo-005', title: '团队会议', description: '每周团队进度沟通会议', deadline: new Date(2023, 9, 18, 10, 0), priority: 'low', type: 'performance', targetId: 'weekly-meeting' } ]; // 按优先级排序:紧急且重要 > 重要不紧急 > 紧急不重要 this.todoTasks.sort((a, b) => { const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); } // 筛选项目类型 filterProjects(event: Event): void { const target = event.target as HTMLSelectElement; this.selectedType = (target && target.value ? target.value : 'all') as any; this.applyFilters(); } // 筛选紧急程度 filterByUrgency(event: Event): void { const target = event.target as HTMLSelectElement; this.selectedUrgency = (target && target.value ? target.value : 'all') as any; this.applyFilters(); } // 筛选项目状态 filterByStatus(status: string): void { // 点击同一状态时,切换回“全部”,以便恢复全量项目列表 const next = (this.selectedStatus === status) ? 'all' : (status && status.length ? status : 'all'); this.selectedStatus = next as any; this.applyFilters(); } // 处理状态筛选下拉框变化 onStatusChange(event: Event): void { const target = event.target as HTMLSelectElement; this.selectedStatus = (target && target.value ? target.value : 'all') as any; this.applyFilters(); } // 新增:设计师筛选下拉事件处理 onDesignerChange(event: Event): void { const target = event.target as HTMLSelectElement; this.selectedDesigner = (target && target.value ? target.value : 'all'); this.applyFilters(); } // 新增:会员类型筛选下拉事件处理 onMemberTypeChange(event: Event): void { const select = event.target as HTMLSelectElement; this.selectedMemberType = select.value as any; this.applyFilters(); } // 新增:四大板块改变 onCorePhaseChange(event: Event): void { const select = event.target as HTMLSelectElement; this.selectedCorePhase = select.value as any; this.applyFilters(); } // 时间窗快捷筛选(供UI按钮触发) filterByTimeWindow(timeWindow: 'all' | 'today' | 'threeDays' | 'sevenDays'): void { this.selectedTimeWindow = timeWindow; this.applyFilters(); } // 新增:搜索输入变化 onSearchChange(): void { if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer); this.searchDebounceTimer = setTimeout(() => { this.updateSearchSuggestions(); this.applyFilters(); }, this.SEARCH_DEBOUNCE_MS); } // 新增:搜索框聚焦/失焦控制建议显隐 onSearchFocus(): void { if (this.hideSuggestionsTimer) clearTimeout(this.hideSuggestionsTimer); this.isSearchFocused = true; this.updateSearchSuggestions(); } onSearchBlur(): void { // 延迟隐藏以允许选择项的 mousedown 触发 this.isSearchFocused = false; this.hideSuggestionsTimer = setTimeout(() => { this.showSuggestions = false; }, 150); } // 新增:更新搜索建议(不叠加其它筛选,仅基于关键字) private updateSearchSuggestions(): void { const q = (this.searchTerm || '').trim().toLowerCase(); if (q.length < this.MIN_SEARCH_LEN) { this.searchSuggestions = []; this.showSuggestions = false; return; } const scored = this.projects .filter(p => (p.searchIndex || `${(p.name || '')}|${(p.designerName || '')}`.toLowerCase()).includes(q)) .map(p => { const dl = p.deadline || p.expectedEndDate; const dlTime = dl ? new Date(dl).getTime() : NaN; const daysToDl = Math.ceil(((isNaN(dlTime) ? 0 : dlTime) - Date.now()) / (1000 * 60 * 60 * 24)); const urgencyScore = p.urgency === 'high' ? 3 : p.urgency === 'medium' ? 2 : 1; const overdueScore = p.isOverdue ? 10 : 0; const score = overdueScore + (4 - urgencyScore) * 2 - (isNaN(daysToDl) ? 0 : daysToDl); return { p, score }; }) .sort((a, b) => b.score - a.score) .slice(0, this.MAX_SUGGESTIONS) .map(x => x.p); this.searchSuggestions = scored; this.showSuggestions = this.isSearchFocused && this.searchSuggestions.length > 0; } // 新增:选择建议项 selectSuggestion(project: Project): void { this.searchTerm = project.name; this.showSuggestions = false; this.viewProjectDetails(project.id); } // 统一筛选 private applyFilters(): void { let result = [...this.projects]; // 新增:关键词搜索(项目名 / 设计师名 / 含风格关键词的项目名) const q = (this.searchTerm || '').trim().toLowerCase(); if (q) { result = result.filter(p => (p.searchIndex || `${(p.name || '')}|${(p.designerName || '')}`.toLowerCase()).includes(q)); } // 类型筛选 if (this.selectedType !== 'all') { result = result.filter(p => p.type === this.selectedType); } // 紧急程度筛选 if (this.selectedUrgency !== 'all') { result = result.filter(p => p.urgency === this.selectedUrgency); } // 项目状态筛选 if (this.selectedStatus !== 'all') { if (this.selectedStatus === 'overdue') { result = result.filter(p => p.isOverdue); } else if (this.selectedStatus === 'dueSoon') { result = result.filter(p => p.dueSoon && !p.isOverdue); } else if (this.selectedStatus === 'pendingApproval') { result = result.filter(p => p.currentStage === 'pendingApproval'); } else if (this.selectedStatus === 'pendingAssignment') { result = result.filter(p => p.currentStage === 'pendingAssignment'); } else if (this.selectedStatus === 'progress') { const progressStages = ['requirement','planning','modeling','rendering','postProduction','review','revision']; result = result.filter(p => progressStages.includes(p.currentStage)); } else if (this.selectedStatus === 'completed') { result = result.filter(p => p.currentStage === 'delivery'); } } // 新增:四大板块筛选 if (this.selectedCorePhase !== 'all') { result = result.filter(p => this.mapStageToCorePhase(p.currentStage) === this.selectedCorePhase); } // 设计师筛选 if (this.selectedDesigner !== 'all') { result = result.filter(p => p.designerName === this.selectedDesigner); } // 会员类型筛选 if (this.selectedMemberType !== 'all') { result = result.filter(p => p.memberType === this.selectedMemberType); } // 新增:时间窗筛选 if (this.selectedTimeWindow !== 'all') { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); result = result.filter(p => { const projectDeadline = new Date(p.deadline); const timeDiff = projectDeadline.getTime() - today.getTime(); const daysDiff = Math.ceil(timeDiff / (1000 * 60 * 60 * 24)); switch (this.selectedTimeWindow) { case 'today': return daysDiff <= 1 && daysDiff >= 0; case 'threeDays': return daysDiff <= 3 && daysDiff >= 0; case 'sevenDays': return daysDiff <= 7 && daysDiff >= 0; default: return true; } }); } this.filteredProjects = result; // 新增:计算紧急任务固定区(超期 + 高紧急),与筛选联动 this.urgentPinnedProjects = this.filteredProjects .filter(p => p.isOverdue && p.urgency === 'high') .sort((a, b) => (b.overdueDays - a.overdueDays) || a.name.localeCompare(b.name, 'zh-Hans-CN')); // 当显示甘特卡片时,同步刷新甘特图 if (this.showGanttView) { this.updateGantt(); } // 同步刷新工作量概览图 this.updateWorkloadChart(); } // 切换项目看板/负载日历(甘特)视图 toggleView(): void { this.showGanttView = !this.showGanttView; if (this.showGanttView) { setTimeout(() => this.initOrUpdateGantt(), 0); } else { if (this.ganttChart) { this.ganttChart.dispose(); this.ganttChart = null; } if (this.workloadChart) { this.workloadChart.dispose(); this.workloadChart = null; } } } // 设置甘特时间尺度 setGanttScale(scale: 'week' | 'month'): void { if (this.ganttScale !== scale) { this.ganttScale = scale; this.updateGantt(); } } // 新增:切换甘特模式 setGanttMode(mode: 'project' | 'designer'): void { if (this.ganttMode !== mode) { this.ganttMode = mode; this.updateGantt(); } } private initOrUpdateGantt(): void { if (!this.ganttChartRef) return; const el = this.ganttChartRef.nativeElement; if (!this.ganttChart) { this.ganttChart = echarts.init(el); window.addEventListener('resize', () => { this.ganttChart && this.ganttChart.resize(); }); } this.updateGantt(); } private updateGantt(): void { if (!this.ganttChart) return; if (this.ganttMode === 'designer') { this.updateGanttDesigner(); return; } // 按紧急程度从上到下排序(高->中->低),同级按到期时间升序 const urgencyRank: Record<'high'|'medium'|'low', number> = { high: 0, medium: 1, low: 2 }; const projects = [...this.filteredProjects] .sort((a, b) => { const u = urgencyRank[a.urgency] - urgencyRank[b.urgency]; if (u !== 0) return u; // 二级排序:临期优先(到期更近)> 已分配人员 > VIP客户 > 其他 const endDiff = new Date(a.deadline).getTime() - new Date(b.deadline).getTime(); if (endDiff !== 0) return endDiff; const assignedA = !!a.designerName; const assignedB = !!b.designerName; if (assignedA !== assignedB) return assignedA ? -1 : 1; // 已分配在前 const vipA = a.memberType === 'vip'; const vipB = b.memberType === 'vip'; if (vipA !== vipB) return vipA ? -1 : 1; // VIP在前 return a.name.localeCompare(b.name, 'zh-CN'); }); const categories = projects.map(p => p.name); const urgencyMap: Record = Object.fromEntries(projects.map(p => [p.name, p.urgency])) as any; const colorByUrgency: Record<'high'|'medium'|'low', string> = { high: '#ef4444', medium: '#f59e0b', low: '#22c55e' } as const; const DAY = 24 * 60 * 60 * 1000; const data = projects.map((p, idx) => { const end = new Date(p.deadline).getTime(); const baseDays = p.type === 'hard' ? 30 : 14; const start = p.createdAt ? new Date(p.createdAt).getTime() : end - baseDays * DAY; const color = colorByUrgency[p.urgency] || '#60a5fa'; return { name: p.name, value: [idx, start, end, p.designerName, p.urgency, p.memberType, p.currentStage], itemStyle: { color } }; }); // 计算时间范围(仅周/月) const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const todayTs = today.getTime(); let xMin: number; let xMax: number; let xSplitNumber: number; let xLabelFormatter: (value: number) => string; if (this.ganttScale === 'week') { const day = today.getDay(); // 0=周日 const diffToMonday = (day === 0 ? 6 : day - 1); const startOfWeek = new Date(today.getTime() - diffToMonday * DAY); const endOfWeek = new Date(startOfWeek.getTime() + 7 * DAY - 1); xMin = startOfWeek.getTime(); xMax = endOfWeek.getTime(); xSplitNumber = 7; const WEEK_LABELS = ['周日','周一','周二','周三','周四','周五','周六']; xLabelFormatter = (val) => { const d = new Date(val); return WEEK_LABELS[d.getDay()]; }; } else { // month const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0, 23, 59, 59, 999); xMin = startOfMonth.getTime(); xMax = endOfMonth.getTime(); xSplitNumber = 4; xLabelFormatter = (val) => { const d = new Date(val); const weekOfMonth = Math.ceil(d.getDate() / 7); return `第${weekOfMonth}周`; }; } // 计算默认可视区,并尝试保留上一次的滚动/缩放位置 const total = categories.length; const visible = Math.min(total, 15); // 默认首屏展开15条 const defaultEndPercent = total > 0 ? Math.min(100, (visible / total) * 100) : 100; const prevOpt: any = (this.ganttChart as any).getOption ? (this.ganttChart as any).getOption() : null; const preservedStart = typeof prevOpt?.dataZoom?.[0]?.start === 'number' ? prevOpt.dataZoom[0].start : 0; const preservedEnd = typeof prevOpt?.dataZoom?.[0]?.end === 'number' ? prevOpt.dataZoom[0].end : defaultEndPercent; const option = { backgroundColor: 'transparent', tooltip: { trigger: 'item', formatter: (params: any) => { const v = params.value; const start = new Date(v[1]); const end = new Date(v[2]); return `项目:${params.name}
负责人:${v[3] || '未分配'}
阶段:${v[6]}
起止:${start.toLocaleDateString()} ~ ${end.toLocaleDateString()}`; } }, grid: { left: 100, right: 64, top: 30, bottom: 30 }, xAxis: { type: 'time', min: xMin, max: xMax, splitNumber: xSplitNumber, axisLine: { lineStyle: { color: '#e5e7eb' } }, axisLabel: { color: '#6b7280', formatter: (value: number) => xLabelFormatter(value) }, splitLine: { lineStyle: { color: '#f1f5f9' } } }, yAxis: { type: 'category', data: categories, inverse: true, axisLabel: { color: '#374151', margin: 8, formatter: (val: string) => { const u = urgencyMap[val] || 'low'; const text = val.length > 16 ? val.slice(0, 16) + '…' : val; return `{${u}Dot|●} ${text}`; }, rich: { highDot: { color: '#ef4444' }, mediumDot: { color: '#f59e0b' }, lowDot: { color: '#22c55e' } } }, axisTick: { show: false }, axisLine: { lineStyle: { color: '#e5e7eb' } } }, dataZoom: [ { type: 'slider', yAxisIndex: 0, orient: 'vertical', right: 6, width: 14, start: preservedStart, end: preservedEnd, zoomLock: false }, { type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: true } ], series: [ { type: 'custom', renderItem: (params: any, api: any) => { const categoryIndex = api.value(0); const start = api.coord([api.value(1), categoryIndex]); const end = api.coord([api.value(2), categoryIndex]); const height = Math.max(api.size([0, 1])[1] * 0.5, 8); const rectShape = echarts.graphic.clipRectByRect({ x: start[0], y: start[1] - height / 2, width: Math.max(end[0] - start[0], 2), height }, { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height }); return rectShape ? { type: 'rect', shape: rectShape, style: api.style() } : undefined; }, encode: { x: [1, 2], y: 0 }, data, itemStyle: { borderRadius: 4 }, emphasis: { focus: 'self' }, markLine: { silent: true, symbol: 'none', lineStyle: { color: '#ef4444', type: 'dashed', width: 1 }, label: { formatter: '今日', color: '#ef4444', fontSize: 10, position: 'end' }, data: [ { xAxis: todayTs } ] } } ] }; // 强制刷新,避免缓存导致坐标轴不更新 this.ganttChart.clear(); this.ganttChart.setOption(option, true); this.ganttChart.resize(); } // 新增:设计师排班甘特 private updateGanttDesigner(): void { if (!this.ganttChart) return; const DAY = 24 * 60 * 60 * 1000; const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const todayTs = today.getTime(); // 时间轴按当前周/月 let xMin: number; let xMax: number; let xSplitNumber: number; let xLabelFormatter: (value: number) => string; if (this.ganttScale === 'week') { const day = today.getDay(); const diffToMonday = (day === 0 ? 6 : day - 1); const startOfWeek = new Date(today.getTime() - diffToMonday * DAY); const endOfWeek = new Date(startOfWeek.getTime() + 7 * DAY - 1); xMin = startOfWeek.getTime(); xMax = endOfWeek.getTime(); xSplitNumber = 7; const WEEK_LABELS = ['周日','周一','周二','周三','周四','周五','周六']; xLabelFormatter = (val) => WEEK_LABELS[new Date(val).getDay()]; } else { const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0, 23, 59, 59, 999); xMin = startOfMonth.getTime(); xMax = endOfMonth.getTime(); xSplitNumber = 4; xLabelFormatter = (val) => `第${Math.ceil(new Date(val).getDate() / 7)}周`; } // 仅统计已分配项目 const assigned = this.filteredProjects.filter(p => !!p.designerName); const designers = Array.from(new Set(assigned.map(p => p.designerName))); const byDesigner: Record = {} as any; designers.forEach(n => byDesigner[n] = [] as any); assigned.forEach(p => byDesigner[p.designerName].push(p)); const busyCountMap: Record = Object.fromEntries(designers.map(n => [n, byDesigner[n].length])); const sortedDesigners = designers.sort((a, b) => { const diff = (busyCountMap[b] || 0) - (busyCountMap[a] || 0); return diff !== 0 ? diff : a.localeCompare(b, 'zh-CN'); }); const categories = sortedDesigners; // 工作量等级(用于左侧小圆点颜色) const workloadLevelMap: Record = {} as any; categories.forEach(name => { const cnt = busyCountMap[name] || 0; workloadLevelMap[name] = cnt >= 5 ? 'high' : (cnt >= 3 ? 'medium' : 'low'); }); // 条形颜色仍按项目紧急度 const colorByUrgency: Record<'high'|'medium'|'low', string> = { high: '#ef4444', medium: '#f59e0b', low: '#22c55e' } as const; const data = assigned.flatMap(p => { const end = new Date(p.deadline).getTime(); const baseDays = p.type === 'hard' ? 30 : 14; const start = p.createdAt ? new Date(p.createdAt).getTime() : end - baseDays * DAY; const yIndex = categories.indexOf(p.designerName); if (yIndex === -1) return [] as any[]; const color = colorByUrgency[p.urgency] || '#60a5fa'; return [{ name: p.name, value: [yIndex, start, end, p.designerName, p.urgency, p.memberType, p.currentStage], itemStyle: { color } }]; }); const prevOpt: any = (this.ganttChart as any).getOption ? (this.ganttChart as any).getOption() : null; const total = categories.length || 1; const visible = Math.min(total, 30); const defaultEndPercent = Math.min(100, (visible / total) * 100); const preservedStart = typeof prevOpt?.dataZoom?.[0]?.start === 'number' ? prevOpt.dataZoom[0].start : 0; const preservedEnd = typeof prevOpt?.dataZoom?.[0]?.end === 'number' ? prevOpt.dataZoom[0].end : defaultEndPercent; const option = { backgroundColor: 'transparent', tooltip: { trigger: 'item', formatter: (params: any) => { const v = params.value; const start = new Date(v[1]); const end = new Date(v[2]); return `项目:${params.name}
设计师:${v[3]}
阶段:${v[6]}
起止:${start.toLocaleDateString()} ~ ${end.toLocaleDateString()}`; } }, grid: { left: 110, right: 64, top: 30, bottom: 30 }, xAxis: { type: 'time', min: xMin, max: xMax, splitNumber: xSplitNumber, axisLine: { lineStyle: { color: '#e5e7eb' } }, axisLabel: { color: '#6b7280', formatter: (value: number) => xLabelFormatter(value) }, splitLine: { lineStyle: { color: '#f1f5f9' } } }, yAxis: { type: 'category', data: categories, inverse: true, axisLabel: { color: '#374151', margin: 8, formatter: (val: string) => { const lvl = workloadLevelMap[val] || 'low'; const count = busyCountMap[val] || 0; const text = val.length > 8 ? val.slice(0, 8) + '…' : val; return `{${lvl}Dot|●} ${text}(${count}项)`; }, rich: { highDot: { color: '#ef4444' }, mediumDot: { color: '#f59e0b' }, lowDot: { color: '#22c55e' } } }, axisTick: { show: false }, axisLine: { lineStyle: { color: '#e5e7eb' } } }, dataZoom: [ { type: 'slider', yAxisIndex: 0, orient: 'vertical', right: 6, start: preservedStart, end: preservedEnd, zoomLock: false }, { type: 'inside', yAxisIndex: 0, zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: true } ], series: [ { type: 'custom', renderItem: (params: any, api: any) => { const categoryIndex = api.value(0); const start = api.coord([api.value(1), categoryIndex]); const end = api.coord([api.value(2), categoryIndex]); const height = Math.max(api.size([0, 1])[1] * 0.5, 8); const rectShape = echarts.graphic.clipRectByRect({ x: start[0], y: start[1] - height / 2, width: Math.max(end[0] - start[0], 2), height }, { x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height }); return rectShape ? { type: 'rect', shape: rectShape, style: api.style() } : undefined; }, encode: { x: [1, 2], y: 0 }, data, itemStyle: { borderRadius: 4 }, emphasis: { focus: 'self' }, markLine: { silent: true, symbol: 'none', lineStyle: { color: '#ef4444', type: 'dashed', width: 1 }, label: { formatter: '今日', color: '#ef4444', fontSize: 10, position: 'end' }, data: [ { xAxis: todayTs } ] } } ] } as any; this.ganttChart.clear(); this.ganttChart.setOption(option, true); this.ganttChart.resize(); } ngOnDestroy(): void { if (this.ganttChart) { this.ganttChart.dispose(); this.ganttChart = null; } if (this.workloadChart) { this.workloadChart.dispose(); this.workloadChart = null; } } // 选择单个项目 selectProject(): void { if (this.selectedProjectId) { this.router.navigate(['/team-leader/project-detail', this.selectedProjectId]); } } // 获取特定阶段的项目 getProjectsByStage(stageId: string): Project[] { return this.filteredProjects.filter(project => project.currentStage === stageId); } // 新增:阶段到核心阶段的映射 private mapStageToCorePhase(stageId: string): 'order' | 'requirements' | 'delivery' | 'aftercare' { // 订单创建:立项初期 if (stageId === 'pendingApproval' || stageId === 'pendingAssignment') return 'order'; // 确认需求:需求沟通 + 方案规划 if (stageId === 'requirement' || stageId === 'planning') return 'requirements'; // 交付执行:制作与评审修订过程 if (stageId === 'modeling' || stageId === 'rendering' || stageId === 'postProduction' || stageId === 'review' || stageId === 'revision') return 'delivery'; // 售后:交付完成后的跟进(当前数据以交付完成代表进入售后) return 'aftercare'; } // 新增:获取核心阶段的项目 getProjectsByCorePhase(coreId: string): Project[] { return this.filteredProjects.filter(p => this.mapStageToCorePhase(p.currentStage) === coreId); } // 新增:获取核心阶段的项目数量 getProjectCountByCorePhase(coreId: string): number { return this.getProjectsByCorePhase(coreId).length; } // 获取特定阶段的项目数量 getProjectCountByStage(stageId: string): number { return this.getProjectsByStage(stageId).length; } // 待审批项目:currentStage === 'pendingApproval' get pendingApprovalProjects(): Project[] { const src = (this.filteredProjects && this.filteredProjects.length) ? this.filteredProjects : this.projects; return src.filter(p => p.currentStage === 'pendingApproval'); } // 待指派项目:currentStage === 'pendingAssignment' get pendingAssignmentProjects(): Project[] { const src = (this.filteredProjects && this.filteredProjects.length) ? this.filteredProjects : this.projects; return src.filter(p => p.currentStage === 'pendingAssignment'); } // 获取紧急程度标签 getUrgencyLabel(urgency: string): string { const labels = { high: '紧急', medium: '一般', low: '普通' }; return labels[urgency as keyof typeof labels] || urgency; } // 智能推荐设计师 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(projectId: string, rating: 'excellent' | 'qualified' | 'unqualified'): void { const project = this.projects.find(p => p.id === projectId); if (!project) return; project.qualityRating = rating; if (rating === 'unqualified') { // 不合格:回退到修改阶段 project.currentStage = 'revision'; } this.applyFilters(); alert('质量评审已提交'); } // 查看绩效预警(占位:跳转到团队管理) viewPerformanceDetails(): void { this.router.navigate(['/team-leader/team-management']); } // 打开负载日历(占位:跳转到团队管理) navigateToWorkloadCalendar(): void { this.router.navigate(['/team-leader/workload-calendar']); } // 查看项目详情 viewProjectDetails(projectId: string): void { // 改为跳转到复用的项目详情(组长上下文,具备审核权限) this.router.navigate(['/team-leader/project-detail', projectId]); } // 快速分配项目(增强:加入智能推荐) quickAssignProject(projectId: string): void { const project = this.projects.find(p => p.id === projectId); if (!project) { 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 = confirm(message); if (confirmAssign) { project.designerName = recommended.name; if (project.currentStage === 'pendingAssignment' || project.currentStage === 'pendingApproval') { project.currentStage = 'requirement'; } project.status = '进行中'; // 更新设计师筛选列表 this.designers = Array.from(new Set(this.projects.map(p => p.designerName).filter(n => !!n))); this.applyFilters(); alert(`项目已${reassigning ? '重新' : ''}分配给 ${recommended.name}`); return; } } // 无推荐或用户取消,跳转到详细分配页面 // 改为跳转到复用详情(组长视图),通过 query 参数标记 assign 模式 this.router.navigate(['/team-leader/project-detail', projectId], { queryParams: { mode: 'assign' } }); } // 导航到待办任务 navigateToTask(task: TodoTask): void { switch (task.type) { case 'review': this.router.navigate(['team-leader/quality-management', task.targetId]); break; case 'assign': this.router.navigate(['/team-leader/dashboard']); break; case 'performance': this.router.navigate(['team-leader/team-management']); break; } } // 获取优先级标签 getPriorityLabel(priority: 'high' | 'medium' | 'low'): string { const labels: Record<'high' | 'medium' | 'low', string> = { 'high': '紧急且重要', 'medium': '重要不紧急', 'low': '紧急不重要' }; return labels[priority]; } // 导航到团队管理 navigateToTeamManagement(): void { this.router.navigate(['/team-leader/team-management']); } // 导航到项目评审 navigateToProjectReview(): void { // 统一入口:跳转到项目列表/看板,而非旧评审页 this.router.navigate(['/team-leader/dashboard']); } // 导航到质量管理 navigateToQualityManagement(): void { this.router.navigate(['/team-leader/quality-management']); } // 打开工作量预估工具(已迁移) openWorkloadEstimator(): void { // 工具迁移至详情页:引导前往当前选中项目详情 if (this.selectedProjectId) { this.router.navigate(['/team-leader/project-detail', this.selectedProjectId], { queryParams: { tool: 'estimator' } }); } else { this.router.navigate(['/team-leader/dashboard']); } alert('工作量预估工具已迁移至项目详情页,您可以在建模阶段之前使用该工具进行工作量计算。'); } // 查看所有超期项目 viewAllOverdueProjects(): void { this.filterByStatus('overdue'); this.closeAlert(); } // 关闭提醒 closeAlert(): void { this.showAlert = false; } // 维度切换(设计师/会员类型) setWorkloadDimension(dim: 'designer' | 'member'): void { if (this.workloadDimension !== dim) { this.workloadDimension = dim; this.updateWorkloadChart(); } } // 刷新“工作量概览”图表 private updateWorkloadChart(): void { if (!this.workloadChartRef) { return; } const el = this.workloadChartRef.nativeElement; if (!el) { return; } // 初始化实例(使用 SVG 渲染以获得更佳文本清晰度) if (!this.workloadChart) { this.workloadChart = echarts.init(el, null, { renderer: 'svg' }); } const data = (this.filteredProjects && this.filteredProjects.length) ? this.filteredProjects : this.projects; const byDesigner = this.workloadDimension === 'designer'; const groupKey = byDesigner ? 'designerName' : 'memberType'; const labelMap: Record = { vip: 'VIP', normal: '普通' }; const groupSet = new Set(); data.forEach(p => { const val = (p as any)[groupKey] || (byDesigner ? '未分配' : '未知'); groupSet.add(val); }); const categories = Array.from(groupSet).sort((a, b) => a.localeCompare(b, 'zh-Hans-CN')); const count = (urg: 'high'|'medium'|'low', group: string) => data.filter(p => (((p as any)[groupKey] || (byDesigner ? '未分配' : '未知')) === group) && p.urgency === urg).length; const high = categories.map(c => count('high', c)); const medium = categories.map(c => count('medium', c)); const low = categories.map(c => count('low', c)); const option = { tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { data: ['高', '中', '低'] }, grid: { left: 12, right: 16, top: 28, bottom: 8, containLabel: true }, xAxis: { type: 'value', boundaryGap: [0, 0.01] }, yAxis: { type: 'category', data: categories.map(c => byDesigner ? c : (labelMap[c] || c)) }, series: [ { name: '高', type: 'bar', stack: 'workload', data: high, itemStyle: { color: '#ef4444' } }, { name: '中', type: 'bar', stack: 'workload', data: medium, itemStyle: { color: '#f59e0b' } }, { name: '低', type: 'bar', stack: 'workload', data: low, itemStyle: { color: '#10b981' } } ] } as any; this.workloadChart.clear(); this.workloadChart.setOption(option, true); this.workloadChart.resize(); } resetStatusFilter(): void { this.selectedStatus = 'all'; this.applyFilters(); } }