12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793 |
- 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;
- }
- // 员工请假记录接口
- interface LeaveRecord {
- id: string;
- employeeName: string;
- date: string; // YYYY-MM-DD 格式
- isLeave: boolean;
- leaveType?: 'sick' | 'personal' | 'annual' | 'other'; // 请假类型
- reason?: string; // 请假原因
- }
- // 员工详情面板数据接口
- interface EmployeeDetail {
- name: string;
- currentProjects: number; // 当前负责项目数
- projectNames: string[]; // 项目名称列表(用于显示)
- leaveRecords: LeaveRecord[]; // 未来7天请假记录
- redMarkExplanation: 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: 95, avgRating: 4.5, experience: 3 },
- { id: 'li', name: '李四', skills: ['新中式', '宋式风格'], workload: 25, avgRating: 4.8, experience: 5 },
- { id: 'wang', name: '王五', skills: ['北欧风格', '日式风格'], workload: 75, avgRating: 4.2, experience: 2 },
- { id: 'zhao', name: '赵六', skills: ['现代风格', '轻奢风格'], workload: 15, avgRating: 4.6, experience: 4 },
- { id: 'sun', name: '孙七', skills: ['简约风格', '工业风格'], workload: 35, avgRating: 4.3, experience: 3 },
- { id: 'zhou', name: '周八', skills: ['欧式风格', '美式风格'], workload: 5, avgRating: 4.7, experience: 6 },
- { id: 'wu', name: '吴九', skills: ['地中海风格', '田园风格'], workload: 60, avgRating: 4.4, experience: 4 },
- { id: 'chen', name: '陈十', skills: ['现代简约', '新古典'], workload: 0, avgRating: 4.9, experience: 7 }
- ];
- // 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: 'day' | 'week' | 'month' = 'week';
- // 新增:甘特模式(项目 / 设计师排班)
- ganttMode: 'project' | 'designer' = 'project';
- // 个人详情面板相关属性
- showEmployeeDetailPanel: boolean = false;
- selectedEmployeeDetail: EmployeeDetail | null = null;
-
- // 员工请假数据(模拟数据)
- private leaveRecords: LeaveRecord[] = [
- { id: '1', employeeName: '张三', date: '2024-01-20', isLeave: true, leaveType: 'personal', reason: '事假' },
- { id: '2', employeeName: '张三', date: '2024-01-21', isLeave: false },
- { id: '3', employeeName: '张三', date: '2024-01-22', isLeave: false },
- { id: '4', employeeName: '张三', date: '2024-01-23', isLeave: false },
- { id: '5', employeeName: '张三', date: '2024-01-24', isLeave: false },
- { id: '6', employeeName: '张三', date: '2024-01-25', isLeave: false },
- { id: '7', employeeName: '张三', date: '2024-01-26', isLeave: false },
-
- { id: '8', employeeName: '李四', date: '2024-01-20', isLeave: false },
- { id: '9', employeeName: '李四', date: '2024-01-21', isLeave: true, leaveType: 'sick', reason: '病假' },
- { id: '10', employeeName: '李四', date: '2024-01-22', isLeave: true, leaveType: 'sick', reason: '病假' },
- { id: '11', employeeName: '李四', date: '2024-01-23', isLeave: false },
- { id: '12', employeeName: '李四', date: '2024-01-24', isLeave: false },
- { id: '13', employeeName: '李四', date: '2024-01-25', isLeave: false },
- { id: '14', employeeName: '李四', date: '2024-01-26', isLeave: false },
-
- { id: '15', employeeName: '王五', date: '2024-01-20', isLeave: false },
- { id: '16', employeeName: '王五', date: '2024-01-21', isLeave: false },
- { id: '17', employeeName: '王五', date: '2024-01-22', isLeave: false },
- { id: '18', employeeName: '王五', date: '2024-01-23', isLeave: true, leaveType: 'annual', reason: '年假' },
- { id: '19', employeeName: '王五', date: '2024-01-24', isLeave: false },
- { id: '20', employeeName: '王五', date: '2024-01-25', isLeave: false },
- { id: '21', employeeName: '王五', date: '2024-01-26', isLeave: false },
-
- { id: '22', employeeName: '赵六', date: '2024-01-20', isLeave: false },
- { id: '23', employeeName: '赵六', date: '2024-01-21', isLeave: false },
- { id: '24', employeeName: '赵六', date: '2024-01-22', isLeave: false },
- { id: '25', employeeName: '赵六', date: '2024-01-23', isLeave: false },
- { id: '26', employeeName: '赵六', date: '2024-01-24', isLeave: false },
- { id: '27', employeeName: '赵六', date: '2024-01-25', isLeave: false },
- { id: '28', employeeName: '赵六', date: '2024-01-26', isLeave: false }
- ];
- 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: '已完成'
- };
- // 为有项目的设计师分配项目
- const busyDesigners = ['张三', '王五', '吴九']; // 高负荷设计师
- const moderateDesigners = ['孙七']; // 中等负荷设计师
- const idleDesigners = ['李四', '赵六', '周八', '陈十']; // 空闲设计师
- // 为忙碌的设计师分配更多项目
- for (let i = 8; i <= 30; i++) {
- const designerIndex = (i - 8) % busyDesigners.length;
- const designerName = busyDesigners[designerIndex];
- const stageIndex = (i - 1) % 7 + 3; // 主要在进行中的阶段
- const currentStage = stageIds[stageIndex];
- const type: 'soft' | 'hard' = i % 2 === 0 ? 'soft' : 'hard';
- const urgency: 'high' | 'medium' | 'low' = i % 4 === 0 ? 'high' : (i % 3 === 0 ? 'medium' : 'low');
- const isOverdue = i % 8 === 0;
- const overdueDays = isOverdue ? (i % 5) + 1 : 0;
- const status = statusMap[currentStage] || '进行中';
- const expectedEndDate = new Date();
- const daysOffset = isOverdue ? -(overdueDays + (i % 3)) : ((i % 15) + 5);
- 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 % 5 === 0 ? 'vip' : 'normal';
- this.projects.push({
- id: `proj-${String(i).padStart(3, '0')}`,
- name: `${designerName}负责的${type === 'soft' ? '软装' : '硬装'}项目 ${i}`,
- type,
- memberType,
- designerName,
- status,
- expectedEndDate,
- deadline: expectedEndDate,
- isOverdue,
- overdueDays,
- dueSoon,
- urgency,
- currentStage,
- phases: []
- });
- }
- // 为中等负荷设计师分配少量项目
- for (let i = 31; i <= 35; i++) {
- const designerName = moderateDesigners[0];
- const stageIndex = (i - 1) % 5 + 4; // 中间阶段
- const currentStage = stageIds[stageIndex];
- const type: 'soft' | 'hard' = i % 2 === 0 ? 'soft' : 'hard';
- const urgency: 'high' | 'medium' | 'low' = 'medium';
- const status = statusMap[currentStage] || '进行中';
- const expectedEndDate = new Date();
- expectedEndDate.setDate(expectedEndDate.getDate() + (i % 10) + 7);
- const memberType: 'vip' | 'normal' = 'normal';
- this.projects.push({
- id: `proj-${String(i).padStart(3, '0')}`,
- name: `${designerName}负责的${type === 'soft' ? '软装' : '硬装'}项目 ${i}`,
- type,
- memberType,
- designerName,
- status,
- expectedEndDate,
- deadline: expectedEndDate,
- isOverdue: false,
- overdueDays: 0,
- dueSoon: false,
- urgency,
- currentStage,
- phases: []
- });
- }
- // 空闲设计师不分配项目,或只分配很少的已完成项目
- for (let i = 36; i <= 40; i++) {
- const designerIndex = (i - 36) % idleDesigners.length;
- const designerName = idleDesigners[designerIndex];
- const currentStage = 'delivery'; // 已完成的项目
- const type: 'soft' | 'hard' = i % 2 === 0 ? 'soft' : 'hard';
- const urgency: 'high' | 'medium' | 'low' = 'low';
- const status = '已完成';
- const expectedEndDate = new Date();
- expectedEndDate.setDate(expectedEndDate.getDate() - (i % 10) - 5); // 过去的日期
- const memberType: 'vip' | 'normal' = 'normal';
- this.projects.push({
- id: `proj-${String(i).padStart(3, '0')}`,
- name: `${designerName}已完成的${type === 'soft' ? '软装' : '硬装'}项目 ${i}`,
- type,
- memberType,
- designerName,
- status,
- expectedEndDate,
- deadline: expectedEndDate,
- isOverdue: false,
- overdueDays: 0,
- dueSoon: false,
- 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: 'day' | '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);
-
- // 添加点击事件监听器
- this.ganttChart.on('click', (params: any) => {
- if (params.componentType === 'series' && params.seriesType === 'custom') {
- // 获取点击的员工名称(从y轴类目数据中获取)
- const yAxisData = this.ganttChart.getOption().yAxis[0].data;
- if (yAxisData && params.dataIndex !== undefined) {
- const employeeName = yAxisData[params.value[0]];
- if (employeeName && employeeName !== '未分配') {
- this.onEmployeeClick(employeeName);
- }
- }
- }
- });
-
- 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 leaveOverlayData = this.generateLeaveOverlayData(categories, xMin, xMax);
- 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',
- name: '项目进度',
- 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 } ]
- }
- },
- // 请假覆盖层系列
- {
- type: 'custom',
- name: '请假/繁忙标记',
- 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.8, 12); // 稍微高一点,覆盖项目条
- 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: leaveOverlayData,
- itemStyle: { borderRadius: 4 },
- emphasis: { focus: 'self' },
- z: 10 // 确保覆盖层在项目条之上
- }
- ]
- };
- // 强制刷新,避免缓存导致坐标轴不更新
- 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 === 'day') {
- // 日视图:显示今日24小时
- const startOfDay = new Date(today.getTime());
- const endOfDay = new Date(today.getTime() + DAY - 1);
- xMin = startOfDay.getTime();
- xMax = endOfDay.getTime();
- xSplitNumber = 24;
- xLabelFormatter = (val) => `${new Date(val).getHours()}:00`;
- } else if (this.ganttScale === 'week') {
- // 周视图:从今天开始显示未来7天的具体日期
- const startOfWeek = new Date(today.getTime());
- const endOfWeek = new Date(today.getTime() + 7 * DAY - 1);
- xMin = startOfWeek.getTime();
- xMax = endOfWeek.getTime();
- xSplitNumber = 7;
- xLabelFormatter = (val) => {
- const date = new Date(val);
- const month = date.getMonth() + 1;
- const day = date.getDate();
- return `${month}月${day}日`;
- };
- } else {
- // 月视图:从当前月份开始显示未来几个月
- const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
- const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 3, 0, 23, 59, 59, 999); // 显示未来3个月
- xMin = startOfMonth.getTime();
- xMax = endOfMonth.getTime();
- xSplitNumber = 3;
- xLabelFormatter = (val) => {
- const date = new Date(val);
- const year = date.getFullYear();
- const month = date.getMonth() + 1;
- return `${year}年${month}月`;
- };
- }
- // 仅统计已分配项目
- 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;
- const workloadStatusMap: Record<string, 'overloaded'|'busy'|'available'> = {} as any;
- categories.forEach(name => {
- const cnt = busyCountMap[name] || 0;
- if (cnt >= 5) {
- workloadLevelMap[name] = 'high';
- workloadStatusMap[name] = 'overloaded'; // 不宜派单
- } else if (cnt >= 3) {
- workloadLevelMap[name] = 'medium';
- workloadStatusMap[name] = 'busy'; // 适度忙碌
- } else {
- workloadLevelMap[name] = 'low';
- workloadStatusMap[name] = 'available'; // 可接单
- }
- });
- // 条形颜色按项目紧急度,增强高负荷时段的视觉效果
- const colorByUrgency: Record<'high'|'medium'|'low', string> = {
- high: '#dc2626', // 更深的红色,突出高紧急度
- medium: '#ea580c', // 更深的橙色
- low: '#16a34a' // 更深的绿色
- } 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 workloadStatus = workloadStatusMap[p.designerName];
- let color = colorByUrgency[p.urgency] || '#60a5fa';
- let borderWidth = 1;
- let borderColor = 'transparent';
-
- // 高负荷时段增强视觉效果
- if (workloadStatus === 'overloaded') {
- borderWidth = 3;
- borderColor = '#991b1b'; // 深红色边框
- // 对于超负荷状态,使用更深的红色调
- if (p.urgency === 'high') {
- color = '#7f1d1d'; // 深红色
- } else if (p.urgency === 'medium') {
- color = '#c2410c'; // 深橙色
- } else {
- color = '#dc2626'; // 红色(即使是低紧急度也用红色表示超负荷)
- }
- }
-
- return [{
- name: p.name,
- value: [yIndex, start, end, p.designerName, p.urgency, p.memberType, p.currentStage, workloadStatus],
- itemStyle: {
- color,
- borderWidth,
- borderColor,
- opacity: workloadStatus === 'overloaded' ? 0.9 : 1.0
- }
- }];
- });
- // 生成空闲时段背景数据 - 只在真正空闲的时间段显示
- const idleBackgroundData: any[] = [];
-
- categories.forEach((designerName, yIndex) => {
- const designerProjects = byDesigner[designerName] || [];
- const workloadStatus = workloadStatusMap[designerName];
-
- // 获取该设计师的所有项目时间段
- const projectTimeRanges = designerProjects.map(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;
- return { start, end };
- }).sort((a, b) => a.start - b.start);
-
- // 找出空闲时间段
- const idleTimeRanges: { start: number; end: number }[] = [];
-
- if (projectTimeRanges.length === 0) {
- // 完全没有项目,整个时间轴都是空闲
- idleTimeRanges.push({ start: xMin, end: xMax });
- } else {
- // 检查项目之间的空隙
- let currentTime = xMin;
-
- for (const range of projectTimeRanges) {
- if (currentTime < range.start) {
- // 在项目开始前有空闲时间
- idleTimeRanges.push({ start: currentTime, end: range.start });
- }
- currentTime = Math.max(currentTime, range.end);
- }
-
- // 检查最后一个项目后是否还有空闲时间
- if (currentTime < xMax) {
- idleTimeRanges.push({ start: currentTime, end: xMax });
- }
- }
-
- // 为每个空闲时间段创建背景数据
- idleTimeRanges.forEach((idleRange, index) => {
- // 只有当空闲时间段足够长时才显示(至少1天)
- if (idleRange.end - idleRange.start >= DAY) {
- let backgroundColor = 'transparent';
-
- if (workloadStatus === 'available') {
- backgroundColor = 'rgba(34, 197, 94, 0.15)'; // 淡绿色背景表示空闲可接单
- } else if (workloadStatus === 'overloaded') {
- backgroundColor = 'rgba(239, 68, 68, 0.1)'; // 淡红色背景表示超负荷
- }
-
- if (backgroundColor !== 'transparent') {
- idleBackgroundData.push({
- name: `${designerName}-空闲${index + 1}`,
- value: [yIndex, idleRange.start, idleRange.end, designerName, 'background', workloadStatus],
- itemStyle: {
- color: backgroundColor,
- borderWidth: 0
- }
- });
- }
- }
- });
- });
- 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;
- if (v[4] === 'background') {
- const workloadStatus = v[5];
- const statusText = workloadStatus === 'available' ? '空闲可接单' :
- workloadStatus === 'overloaded' ? '超负荷不宜派单' : '适度忙碌';
- return `设计师:${v[3]}<br/>状态:${statusText}`;
- }
- const start = new Date(v[1]);
- const end = new Date(v[2]);
- const workloadStatus = v[7];
- const statusText = workloadStatus === 'available' ? '可接单' :
- workloadStatus === 'overloaded' ? '超负荷' : '适度忙碌';
- return `项目:${params.name}<br/>设计师:${v[3]}(${statusText})<br/>阶段:${v[6]}<br/>起止:${start.toLocaleDateString()} ~ ${end.toLocaleDateString()}`;
- }
- },
- legend: {
- data: ['空闲可接单', '适度忙碌', '超负荷不宜派单', '高紧急', '中紧急', '低紧急'],
- bottom: 0,
- itemGap: 20,
- textStyle: { fontSize: 12 }
- },
- grid: { left: 110, right: 64, top: 30, bottom: 60 },
- 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 status = workloadStatusMap[val] || 'available';
- const text = val.length > 8 ? val.slice(0, 8) + '…' : val;
- const statusIcon = status === 'available' ? '🟢' :
- status === 'overloaded' ? '🔴' : '🟡';
- return `{${lvl}Dot|●} ${statusIcon} ${text}(${count}项)`;
- },
- rich: {
- highDot: { color: '#dc2626' },
- mediumDot: { color: '#ea580c' },
- lowDot: { color: '#16a34a' }
- }
- },
- 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',
- name: '工作负荷背景',
- 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 = api.size([0, 1])[1] * 0.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: idleBackgroundData,
- z: 1
- },
- // 项目条层
- {
- type: 'custom',
- name: '项目进度',
- 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' },
- z: 2,
- 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 {
- // 检测是否在iframe中运行(即从客服端访问)
- const isInIframe = window.self !== window.top;
-
- if (isInIframe) {
- // 如果在iframe中,跳转到设计师端项目详情页面,并传递客服角色标识
- // 使用parent.window来在父窗口中进行导航
- const targetUrl = `/designer/project-detail/${projectId}?role=customer-service`;
- if (window.parent) {
- window.parent.postMessage({
- type: 'navigate',
- url: targetUrl
- }, '*');
- }
- } else {
- // 正常情况下跳转到组长端项目详情页面
- 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();
- }
- // 处理甘特图员工点击事件
- onEmployeeClick(employeeName: string): void {
- if (!employeeName || employeeName === '未分配') {
- return;
- }
-
- // 生成员工详情数据
- this.selectedEmployeeDetail = this.generateEmployeeDetail(employeeName);
- this.showEmployeeDetailPanel = true;
- }
- // 生成员工详情数据
- private generateEmployeeDetail(employeeName: string): EmployeeDetail {
- // 获取该员工负责的项目
- const employeeProjects = this.filteredProjects.filter(p => p.designerName === employeeName);
- const currentProjects = employeeProjects.length;
- const projectNames = employeeProjects.slice(0, 3).map(p => p.name); // 最多显示3个项目名称
-
- // 获取该员工的请假记录(未来7天)
- const today = new Date();
- const next7Days = Array.from({ length: 7 }, (_, i) => {
- const date = new Date(today);
- date.setDate(today.getDate() + i);
- return date.toISOString().split('T')[0]; // YYYY-MM-DD 格式
- });
-
- const employeeLeaveRecords = this.leaveRecords.filter(record =>
- record.employeeName === employeeName && next7Days.includes(record.date)
- );
-
- // 生成红色标记说明
- const redMarkExplanation = this.generateRedMarkExplanation(employeeName, employeeLeaveRecords, currentProjects);
-
- return {
- name: employeeName,
- currentProjects,
- projectNames,
- leaveRecords: employeeLeaveRecords,
- redMarkExplanation
- };
- }
- // 生成红色标记说明
- private generateRedMarkExplanation(employeeName: string, leaveRecords: LeaveRecord[], projectCount: number): string {
- const explanations: string[] = [];
-
- // 检查请假情况
- const leaveDays = leaveRecords.filter(record => record.isLeave);
- if (leaveDays.length > 0) {
- leaveDays.forEach(leave => {
- const date = new Date(leave.date);
- const dateStr = `${date.getMonth() + 1}月${date.getDate()}日`;
- explanations.push(`${dateStr}(${leave.reason || '请假'})`);
- });
- }
-
- // 检查项目繁忙情况
- if (projectCount >= 3) {
- const today = new Date();
- const dateStr = `${today.getMonth() + 1}月${today.getDate()}日`;
- explanations.push(`${dateStr}(${projectCount}个项目繁忙)`);
- }
-
- if (explanations.length === 0) {
- return '当前无红色标记时段';
- }
-
- return `甘特图中红色时段说明:${explanations.map((exp, index) => `${index + 1}${exp}`).join(';')}`;
- }
- // 关闭员工详情面板
- closeEmployeeDetailPanel(): void {
- this.showEmployeeDetailPanel = false;
- this.selectedEmployeeDetail = null;
- }
- // 获取请假类型显示文本
- getLeaveTypeText(leaveType?: string): string {
- const typeMap: Record<string, string> = {
- 'sick': '病假',
- 'personal': '事假',
- 'annual': '年假',
- 'other': '其他'
- };
- return typeMap[leaveType || ''] || '请假';
- }
- // 生成请假覆盖层数据
- private generateLeaveOverlayData(categories: string[], xMin: number, xMax: number): any[] {
- const DAY = 24 * 60 * 60 * 1000;
- const overlayData: any[] = [];
- categories.forEach((employeeName, yIndex) => {
- // 获取该员工在时间范围内的请假记录
- const employeeLeaves = this.leaveRecords.filter(record => {
- if (record.employeeName !== employeeName || !record.isLeave) {
- return false;
- }
-
- const recordDate = new Date(record.date).getTime();
- return recordDate >= xMin && recordDate <= xMax;
- });
- // 为每个请假日期创建覆盖层
- employeeLeaves.forEach(leave => {
- const leaveDate = new Date(leave.date);
- const startOfDay = new Date(leaveDate.getFullYear(), leaveDate.getMonth(), leaveDate.getDate()).getTime();
- const endOfDay = startOfDay + DAY - 1;
- overlayData.push({
- name: `${employeeName} - ${this.getLeaveTypeText(leave.leaveType)}`,
- value: [yIndex, startOfDay, endOfDay, employeeName, leave.leaveType, leave.reason],
- itemStyle: {
- color: 'rgba(239, 68, 68, 0.6)', // 半透明红色
- borderColor: '#ef4444',
- borderWidth: 1
- }
- });
- });
- // 检查项目繁忙情况,如果项目数>=3,也添加红色标记
- const employeeProjects = this.filteredProjects.filter(p => p.designerName === employeeName);
- if (employeeProjects.length >= 3) {
- // 在当前日期添加繁忙标记
- const today = new Date();
- const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
- const endOfToday = startOfToday + DAY - 1;
- if (startOfToday >= xMin && startOfToday <= xMax) {
- overlayData.push({
- name: `${employeeName} - 项目繁忙(${employeeProjects.length}个项目)`,
- value: [yIndex, startOfToday, endOfToday, employeeName, 'busy', `负责${employeeProjects.length}个项目`],
- itemStyle: {
- color: 'rgba(239, 68, 68, 0.4)', // 稍微透明的红色
- borderColor: '#ef4444',
- borderWidth: 1,
- borderType: 'dashed' // 虚线边框区分请假和繁忙
- }
- });
- }
- }
- });
- return overlayData;
- }
- }
|