12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292 |
- 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<HTMLDivElement>;
- // 新增:工作量概览图表引用与实例
- @ViewChild('workloadChartRef', { static: false }) workloadChartRef!: ElementRef<HTMLDivElement>;
- 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<string, string> = {
- 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<string, 'high'|'medium'|'low'> = 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}<br/>负责人:${v[3] || '未分配'}<br/>阶段:${v[6]}<br/>起止:${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<string, typeof assigned> = {} as any;
- designers.forEach(n => byDesigner[n] = [] as any);
- assigned.forEach(p => byDesigner[p.designerName].push(p));
- const busyCountMap: Record<string, number> = 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<string, 'high'|'medium'|'low'> = {} 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}<br/>设计师:${v[3]}<br/>阶段:${v[6]}<br/>起止:${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<string, string> = { vip: 'VIP', normal: '普通' };
- const groupSet = new Set<string>();
- 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();
- }
- }
|