| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935 |
- import { CommonModule } from '@angular/common';
- import { FormsModule } from '@angular/forms';
- import { Router, RouterModule } from '@angular/router';
- import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
- import { ProjectService } from '../../../services/project.service';
- import { DesignerService } from '../services/designer.service';
- import { WxworkAuth } from 'fmode-ng/core';
- import { ProjectIssueService, ProjectIssue, IssueStatus, IssuePriority, IssueType } from '../../../../modules/project/services/project-issue.service';
- import { FmodeParse } from 'fmode-ng/parse';
- import { ProjectTimelineComponent } from '../project-timeline';
- import { EmployeeDetailPanelComponent } from '../employee-detail-panel';
- import { DashboardMetricsComponent } from './components/dashboard-metrics/dashboard-metrics.component';
- import { WorkloadGanttComponent } from './components/workload-gantt/workload-gantt.component';
- import { TodoSectionComponent } from './components/todo-section/todo-section.component';
- import { SmartMatchModalComponent } from './components/smart-match-modal/smart-match-modal.component';
- import { DashboardFilterBarComponent } from './components/dashboard-filter-bar/dashboard-filter-bar.component';
- import { ProjectKanbanComponent } from './components/project-kanban/project-kanban.component';
- import { DashboardAlertsComponent } from './components/dashboard-alerts/dashboard-alerts.component';
- import type { ProjectTimeline } from '../project-timeline/project-timeline';
- import { normalizeDateInput, addDays } from '../../../utils/date-utils';
- import { generatePhaseDeadlines } from '../../../utils/phase-deadline.utils';
- import { PhaseDeadlines, PhaseName } from '../../../models/project-phase.model';
- import {
- ProjectStage,
- ProjectPhase,
- Project,
- TodoTaskFromIssue,
- UrgentEvent,
- LeaveRecord,
- EmployeeDetail,
- EmployeeCalendarData,
- EmployeeCalendarDay
- } from './dashboard.model';
- @Component({
- selector: 'app-dashboard',
- standalone: true,
- imports: [
- CommonModule,
- FormsModule,
- RouterModule,
- ProjectTimelineComponent,
- EmployeeDetailPanelComponent,
- DashboardMetricsComponent,
- WorkloadGanttComponent,
- TodoSectionComponent,
- SmartMatchModalComponent,
- DashboardFilterBarComponent,
- ProjectKanbanComponent,
- DashboardAlertsComponent
- ],
- templateUrl: './dashboard.html',
- styleUrl: './dashboard.scss',
- changeDetection: ChangeDetectionStrategy.OnPush
- })
- export class Dashboard implements OnInit, OnDestroy {
- // 暴露 Array 给模板使用
- Array = Array;
-
- projects: Project[] = [];
- filteredProjects: Project[] = [];
- urgentPinnedProjects: Project[] = [];
- showAlert: boolean = false;
- selectedProjectId: string = '';
-
- // 待办任务数据(交给子组件处理显示)
- todoTasksFromIssues: TodoTaskFromIssue[] = [];
- loadingTodoTasks: boolean = false;
- todoTaskError: string = '';
- private todoTaskRefreshTimer: any;
-
- // 紧急事件数据(交给子组件处理显示)
- urgentEvents: UrgentEvent[] = [];
- loadingUrgentEvents: boolean = false;
- handledUrgentEventIds: Set<string> = new Set();
- mutedUrgentEventIds: Set<string> = new Set();
-
- // 新增:当前用户信息
- currentUser = {
- name: '组长',
- avatar: "data:image/svg+xml,%3Csvg width='40' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23CCFFCC'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3E组长%3C/text%3E%3C/svg%3E",
- roleName: '组长'
- };
- currentDate = new Date();
-
- // 真实设计师数据(从fmode-ng获取)
- realDesigners: any[] = [];
-
- // 设计师工作量映射(从 ProjectTeam 表)
- designerWorkloadMap: Map<string, any[]> = new Map(); // designerId/name -> projects[]
-
- // 智能推荐相关
- showSmartMatch: boolean = false;
- selectedProject: any = null;
- recommendations: any[] = [];
- // 新增:关键词搜索
- searchTerm: string = '';
-
- // 临期项目与筛选状态
- selectedType: 'all' | 'soft' | 'hard' = 'all';
- selectedUrgency: 'all' | 'high' | 'medium' | 'low' = 'all';
- selectedStatus: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon' = 'all';
- selectedDesigner: string = 'all';
- selectedMemberType: 'all' | 'vip' | 'normal' = 'all';
- // 新增:时间窗筛选
- selectedTimeWindow: 'all' | 'today' | 'threeDays' | 'sevenDays' = 'all';
- designers: string[] = [];
- // 新增:四大板块筛选
- selectedCorePhase: 'all' | 'order' | 'requirements' | 'delivery' | 'aftercare' = 'all';
-
- // 设计师画像(从fmode-ng动态获取,保留此字段作为兼容)
- designerProfiles: any[] = [];
- // 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 = true;
- // 个人详情面板相关属性
- showEmployeeDetailPanel: boolean = false;
- selectedEmployeeName: string = '';
- selectedEmployeeDetail: any | null = null;
- selectedEmployeeProjects: any[] = [];
-
- // 项目时间轴数据
- projectTimelineData: ProjectTimeline[] = [];
- private timelineDataCache: ProjectTimeline[] = [];
- private lastDesignerWorkloadMapSize: number = 0;
-
- // 员工请假数据
- // private leaveRecords: LeaveRecord[] = [];
-
- constructor(
- private projectService: ProjectService,
- private router: Router,
- private designerService: DesignerService,
- private issueService: ProjectIssueService,
- private cdr: ChangeDetectorRef
- ) {}
- async ngOnInit(): Promise<void> {
- // 新增:加载用户Profile信息
- await this.loadUserProfile();
-
- await this.loadProjects();
- await this.loadDesigners();
-
- // 加载待办任务(从问题板块)
- await this.loadTodoTasksFromIssues();
- // 🆕 计算紧急事件
- this.calculateUrgentEvents();
- // 启动自动刷新
- this.startAutoRefresh();
- }
- /**
- * 从fmode-ng加载真实设计师数据
- */
- async loadDesigners(): Promise<void> {
- try {
- this.realDesigners = await this.designerService.getDesigners();
- // 更新设计师列表(用于筛选下拉框)
- this.designers = this.realDesigners.map(d => d.name);
- // 同时更新designerProfiles以保持兼容性
- this.designerProfiles = this.realDesigners.map(d => ({
- id: d.id,
- name: d.name,
- skills: d.tags.expertise.styles || [],
- workload: 0, // 后续动态计算
- avgRating: d.tags.history.avgRating || 0,
- experience: 0 // 暂无此字段
- }));
-
- // 加载设计师的实际工作量
- await this.loadDesignerWorkload();
- } catch (error) {
- console.error('加载设计师数据失败:', error);
- }
- }
-
- /**
- * 🔧 从 ProjectTeam 表加载每个设计师的实际工作量
- */
- async loadDesignerWorkload(): Promise<void> {
- try {
- const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
-
- // 查询所有 ProjectTeam 记录
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
-
- // 先查询当前公司的所有项目
- const projectQuery = new Parse.Query('Project');
- projectQuery.equalTo('company', cid);
- projectQuery.notEqualTo('isDeleted', true);
-
- // 查询当前公司项目的 ProjectTeam
- const teamQuery = new Parse.Query('ProjectTeam');
- teamQuery.matchesQuery('project', projectQuery);
- teamQuery.notEqualTo('isDeleted', true);
- teamQuery.include('project');
- teamQuery.include('profile');
- teamQuery.limit(1000);
-
- const teamRecords = await teamQuery.find();
-
- // 如果 ProjectTeam 表为空,使用降级方案
- if (teamRecords.length === 0) {
- await this.loadDesignerWorkloadFromProjects();
- return;
- }
-
- // 构建设计师工作量映射
- this.designerWorkloadMap.clear();
-
- teamRecords.forEach((record: any) => {
- const profile = record.get('profile');
- const project = record.get('project');
-
- if (!profile || !project) {
- return;
- }
-
- const profileId = profile.id;
- const profileName = profile.get('name') || profile.get('user')?.get?.('name') || `设计师-${profileId.slice(-4)}`;
-
- // 提取项目信息
- // 优先获取各个日期字段
- const createdAtValue = project.get('createdAt');
- const updatedAtValue = project.get('updatedAt');
- const deadlineValue = project.get('deadline');
- const deliveryDateValue = project.get('deliveryDate');
- const expectedDeliveryDateValue = project.get('expectedDeliveryDate');
- const demodayValue = project.get('demoday'); // 🆕 小图对图日期
-
- // 🔧 如果 get() 方法都返回假值,尝试从 createdAt/updatedAt 属性直接获取
- // Parse 对象的 createdAt/updatedAt 是内置属性
- let finalCreatedAt = createdAtValue || updatedAtValue;
- if (!finalCreatedAt && project.createdAt) {
- finalCreatedAt = project.createdAt; // Parse 内置属性
- }
- if (!finalCreatedAt && project.updatedAt) {
- finalCreatedAt = project.updatedAt; // Parse 内置属性
- }
-
- // ✅ 应用方案:获取项目的 data 字段(包含 phaseDeadlines, deliveryStageStatus 等)
- const projectDataField = project.get('data') || {};
-
- const projectData = {
- id: project.id,
- name: project.get('title') || '未命名项目',
- status: project.get('status') || '进行中',
- currentStage: project.get('currentStage') || '未知阶段',
- deadline: deadlineValue || deliveryDateValue || expectedDeliveryDateValue,
- demoday: demodayValue, // 🆕 小图对图日期
- createdAt: finalCreatedAt,
- updatedAt: updatedAtValue || project.updatedAt, // ✅ 添加 updatedAt
- designerName: profileName,
- designerId: profileId, // ✅ 添加 designerId
- data: projectDataField, // ✅ 添加 data 字段
- contact: project.get('contact'), // ✅ 添加客户信息
- space: projectDataField.quotation?.spaces?.[0]?.name || '' // ✅ 添加空间信息
- };
-
- // 添加到映射 (by ID)
- if (!this.designerWorkloadMap.has(profileId)) {
- this.designerWorkloadMap.set(profileId, []);
- }
- this.designerWorkloadMap.get(profileId)!.push(projectData);
-
- // 同时建立 name -> projects 的映射(用于甘特图)
- if (!this.designerWorkloadMap.has(profileName)) {
- this.designerWorkloadMap.set(profileName, []);
- }
- this.designerWorkloadMap.get(profileName)!.push(projectData);
- });
-
- // 更新项目时间轴数据
- this.convertToProjectTimeline();
-
- } catch (error) {
- console.error('加载设计师工作量失败:', error);
- }
- }
-
- /**
- * 🔧 降级方案:从 Project.assignee 统计工作量
- * 当 ProjectTeam 表为空时使用
- */
- async loadDesignerWorkloadFromProjects(): Promise<void> {
- try {
- const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
-
- // 查询所有项目
- const projectQuery = new Parse.Query('Project');
- projectQuery.equalTo('company', cid);
- projectQuery.equalTo('isDeleted', false);
- projectQuery.include('assignee');
- projectQuery.include('department');
- projectQuery.limit(1000);
-
- const projects = await projectQuery.find();
-
- // 构建设计师工作量映射
- this.designerWorkloadMap.clear();
-
- projects.forEach((project: any) => {
- const assignee = project.get('assignee');
- if (!assignee) return;
-
- // 只统计组员角色的项目
- const assigneeRole = assignee.get('roleName');
- if (assigneeRole !== '组员') {
- return;
- }
-
- const assigneeName = assignee.get('name') || assignee.get('user')?.get?.('name') || `设计师-${assignee.id.slice(-4)}`;
-
- // 提取项目信息
- // 优先获取各个日期字段
- const createdAtValue = project.get('createdAt');
- const updatedAtValue = project.get('updatedAt');
- const deadlineValue = project.get('deadline');
- const deliveryDateValue = project.get('deliveryDate');
- const expectedDeliveryDateValue = project.get('expectedDeliveryDate');
- const demodayValue = project.get('demoday'); // 🆕 小图对图日期
-
- // 🔧 如果 get() 方法都返回假值,尝试从 createdAt/updatedAt 属性直接获取
- let finalCreatedAt = createdAtValue || updatedAtValue;
- if (!finalCreatedAt && project.createdAt) {
- finalCreatedAt = project.createdAt;
- }
- if (!finalCreatedAt && project.updatedAt) {
- finalCreatedAt = project.updatedAt;
- }
-
- // ✅ 应用方案:获取项目的 data 字段(包含 phaseDeadlines, deliveryStageStatus 等)
- const projectDataField = project.get('data') || {};
-
- const projectData = {
- id: project.id,
- name: project.get('title') || '未命名项目',
- status: project.get('status') || '进行中',
- currentStage: project.get('currentStage') || '未知阶段',
- deadline: deadlineValue || deliveryDateValue || expectedDeliveryDateValue,
- demoday: demodayValue, // 🆕 小图对图日期
- createdAt: finalCreatedAt,
- updatedAt: updatedAtValue || project.updatedAt, // ✅ 添加 updatedAt
- designerName: assigneeName,
- designerId: assignee.id, // ✅ 添加 designerId
- data: projectDataField, // ✅ 添加 data 字段
- contact: project.get('contact'), // ✅ 添加客户信息
- space: projectDataField.quotation?.spaces?.[0]?.name || '' // ✅ 添加空间信息
- };
-
- // 添加到映射
- if (!this.designerWorkloadMap.has(assigneeName)) {
- this.designerWorkloadMap.set(assigneeName, []);
- }
- this.designerWorkloadMap.get(assigneeName)!.push(projectData);
- });
-
- // ✅ 修复:加载完数据后,转换为时间轴格式
- console.log(`📊 [降级方案] 加载了 ${projects.length} 个项目,填充到 ${this.designerWorkloadMap.size} 个设计师的工作量映射`);
- this.convertToProjectTimeline();
-
- } catch (error) {
- console.error('[降级方案] 加载工作量失败:', error);
- }
- }
- /**
- * 从fmode-ng加载真实项目数据
- */
- async loadProjects(): Promise<void> {
- try {
- const realProjects = await this.designerService.getProjects();
-
- // 如果有真实数据,使用真实数据
- if (realProjects && realProjects.length > 0) {
- this.projects = realProjects;
- } else {
- this.projects = [];
- }
- } catch (error) {
- console.error('加载项目数据失败:', error);
- this.projects = [];
- }
-
- // 应用筛选
- this.applyFilters();
- }
-
- /**
- * 将项目数据转换为ProjectTimeline格式(带缓存优化 + 去重)
- */
- private convertToProjectTimeline(): void {
- // 🔧 不去重,保留所有项目-设计师关联关系(一个项目可能有多个设计师)
- const allDesignerProjects: any[] = [];
-
- // 统计项目数量
- let totalProjectsInMap = 0;
- this.designerWorkloadMap.forEach((projects, key) => {
- totalProjectsInMap += projects.length;
- console.log(`📊 设计师 "${key}": ${projects.length} 个项目`);
- });
-
- console.log(`📊 总计 ${totalProjectsInMap} 个项目分布在 ${this.designerWorkloadMap.size} 个设计师中`);
-
- this.designerWorkloadMap.forEach((projects, designerKey) => {
- // 🔧 改进判断逻辑:跳过明显的 ID 格式(Parse objectId 是10位字母数字组合)
- // 只要包含中文字符,就认为是设计师名称
- const isParseId = typeof designerKey === 'string'
- && designerKey.length === 10
- && /^[a-zA-Z0-9]{10}$/.test(designerKey); // Parse ID 格式:10位字母数字
-
- const isDesignerName = !isParseId && typeof designerKey === 'string' && /[\u4e00-\u9fa5]/.test(designerKey);
-
- if (isDesignerName) {
- projects.forEach(proj => {
- // ✅ 不去重,保留每个设计师-项目的关联
- const projectWithDesigner = {
- ...proj,
- designerName: designerKey // 使用当前的设计师名称
- };
- allDesignerProjects.push(projectWithDesigner);
- });
- }
- });
-
- console.log(`📊 开始转换 ${allDesignerProjects.length} 个项目为时间轴格式`);
-
- this.projectTimelineData = allDesignerProjects.map((project, index) => {
- const now = new Date();
-
- // ✅ 应用方案:使用真实字段数据
- const projectData = project.data || {};
-
- // 1. 获取真实的项目开始时间
- const realStartDate = normalizeDateInput(
- projectData.phaseDeadlines?.modeling?.startDate ||
- projectData.requirementsConfirmedAt ||
- project.createdAt,
- new Date()
- );
-
- // 2. 获取真实的交付日期
- // ✅ 修复:确保 deadline 是未来的日期(不使用过去的初始值或未初始化的值)
- let proposedEndDate = project.deadline || projectData.phaseDeadlines?.postProcessing?.deadline;
- let realEndDate: Date;
-
- // 如果提议的结束日期在过去,或者日期无效,使用默认值
- if (proposedEndDate) {
- const proposed = normalizeDateInput(proposedEndDate, realStartDate);
- // 只有当提议的日期在未来时才使用它
- if (proposed.getTime() > now.getTime()) {
- realEndDate = proposed;
- } else {
- // 日期在过去,使用默认值(从开始日期起30天)
- realEndDate = addDays(realStartDate, 30);
- }
- } else {
- // 没有提议的日期,使用默认值
- realEndDate = addDays(realStartDate, 30);
- }
-
- // 3. 获取真实的对图时间(小图对图)
- // ✅ 逻辑:优先使用 project.demoday,否则在软装截止时间后半天
- let realReviewDate: Date;
- let reviewDateSource = 'default';
-
- if (project.demoday) {
- // 如果有显式设置的小图对图日期,使用它
- realReviewDate = normalizeDateInput(project.demoday, new Date());
- reviewDateSource = 'demoday';
- } else if (projectData.phaseDeadlines?.softDecor?.deadline) {
- // 软装截止时间后半天作为小图对图时间
- const softDecorDeadline = normalizeDateInput(projectData.phaseDeadlines.softDecor.deadline, new Date());
- realReviewDate = new Date(softDecorDeadline.getTime() + 12 * 60 * 60 * 1000); // 加12小时
- reviewDateSource = 'softDecor + 12h';
- } else {
- // 默认值:项目进度的60%位置,下午2点
- const defaultReviewPoint = new Date(
- realStartDate.getTime() + (realEndDate.getTime() - realStartDate.getTime()) * 0.6
- );
- defaultReviewPoint.setHours(14, 0, 0, 0);
- realReviewDate = defaultReviewPoint;
- reviewDateSource = 'default 60%';
- }
-
- // 调试日志
- if (project.name?.includes('紫云') || project.name?.includes('自建')) {
- console.log(`📸 [${project.name}] 小图对图时间计算:`, {
- source: reviewDateSource,
- reviewDate: realReviewDate.toLocaleString('zh-CN'),
- demoday: project.demoday,
- softDecorDeadline: projectData.phaseDeadlines?.softDecor?.deadline,
- hasPhaseDeadlines: !!projectData.phaseDeadlines
- });
- }
-
- // 4. 计算距离交付还有几天(使用真实日期)
- const daysUntilDeadline = Math.ceil((realEndDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
-
- // 5. 计算项目状态
- let status: 'normal' | 'warning' | 'urgent' | 'overdue' = 'normal';
- if (daysUntilDeadline < 0) {
- status = 'overdue';
- } else if (daysUntilDeadline <= 1) {
- status = 'urgent';
- } else if (daysUntilDeadline <= 3) {
- status = 'warning';
- }
-
- // 6. 映射阶段
- const stageMap: Record<string, 'plan' | 'model' | 'decoration' | 'render' | 'delivery'> = {
- '方案设计': 'plan',
- '方案规划': 'plan',
- '建模': 'model',
- '建模阶段': 'model',
- '软装': 'decoration',
- '软装设计': 'decoration',
- '渲染': 'render',
- '渲染阶段': 'render',
- '后期': 'render',
- '交付': 'delivery',
- '已完成': 'delivery'
- };
- const currentStage = stageMap[project.currentStage || '建模阶段'] || 'model';
- const stageName = project.currentStage || '建模阶段';
-
- // 7. 🆕 阶段任务完成度(由时间轴组件的 getProjectCompletionRate 计算)
- // ✅ 重要变更:进度条现在表示"任务完成度"而不是"时间百分比"
- // - 时间轴组件会优先使用交付物完成率(overallCompletionRate)
- // - 若无交付物数据,则根据 phaseDeadlines.status 推断任务完成度
- // - stageProgress 保留作为兼容字段,但已弃用
- let stageProgress = 50; // 默认兼容值(实际进度由时间轴组件计算)
-
- // 8. 检查是否停滞(基于 updatedAt)
- let isStalled = false;
- let stalledDays = 0;
- if (project.updatedAt) {
- const updatedAt = project.updatedAt instanceof Date ? project.updatedAt : new Date(project.updatedAt);
- const daysSinceUpdate = Math.floor((now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24));
- // 如果超过7天未更新,认为停滞
- isStalled = daysSinceUpdate > 7;
- stalledDays = daysSinceUpdate;
- }
-
- // 9. 催办次数(基于状态和历史记录)
- const urgentCount = status === 'overdue' ? 2 : status === 'urgent' ? 1 : 0;
-
- // 10. 优先级
- let priority: 'low' | 'medium' | 'high' | 'critical' = 'medium';
- if (status === 'overdue') {
- priority = 'critical';
- } else if (status === 'urgent') {
- priority = 'high';
- } else if (status === 'warning') {
- priority = 'medium';
- } else {
- priority = 'low';
- }
-
- // 11. 获取或生成阶段截止时间数据
- let phaseDeadlines: PhaseDeadlines | undefined = projectData.phaseDeadlines;
- if (!phaseDeadlines) {
- phaseDeadlines = generatePhaseDeadlines(realStartDate, realEndDate);
- }
- if (phaseDeadlines) {
- (Object.keys(phaseDeadlines) as PhaseName[]).forEach((phaseKey) => {
- const info = phaseDeadlines![phaseKey];
- if (!info) return;
- const phaseStart = normalizeDateInput(info.startDate, realStartDate);
- const phaseEnd = normalizeDateInput(info.deadline, realEndDate);
- if (now >= phaseEnd) {
- info.status = 'completed';
- } else if (now >= phaseStart) {
- info.status = 'in_progress';
- } else {
- info.status = info.status || 'not_started';
- }
- });
- }
-
- // 12. 获取空间和客户信息
- const spaceName = project.space || projectData.quotation?.spaces?.[0]?.name || '';
- const customerName = project.customer || project.contact?.name || '';
-
- return {
- projectId: project.id || `proj-${Math.random().toString(36).slice(2, 9)}`,
- projectName: project.name || '未命名项目',
- designerId: project.designerId || project.designerName || '未分配',
- designerName: project.designerName || '未分配',
- startDate: realStartDate, // ✅ 使用真实开始时间
- endDate: realEndDate, // ✅ 使用真实结束时间
- deliveryDate: realEndDate, // ✅ 使用真实交付日期
- reviewDate: realReviewDate, // ✅ 使用真实对图时间
- currentStage,
- stageName,
- stageProgress: Math.round(stageProgress), // ✅ 使用计算的真实进度
- status, // ✅ 基于真实日期计算的状态
- isStalled, // ✅ 基于 updatedAt 计算的停滞状态
- stalledDays, // ✅ 真实的停滞天数
- urgentCount,
- priority,
- spaceName, // ✅ 从项目数据获取
- customerName, // ✅ 从项目数据获取
- phaseDeadlines: phaseDeadlines, // ✅ 使用真实或计算的阶段截止时间
- data: projectData // ✅ 保留原始数据,供后续使用
- };
- });
-
- // 更新缓存
- this.timelineDataCache = this.projectTimelineData;
- this.lastDesignerWorkloadMapSize = totalProjectsInMap;
-
- console.log(`📊 convertToProjectTimeline 完成: 转换了 ${this.projectTimelineData.length} 个项目`);
- if (this.projectTimelineData.length > 0) {
- const now = new Date();
- const sampleProject = this.projectTimelineData[0];
- const daysFromStart = Math.floor((now.getTime() - sampleProject.startDate.getTime()) / (1000 * 60 * 60 * 24));
- const daysToEnd = Math.floor((sampleProject.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
-
- console.log(`📊 第一个项目示例:`, {
- projectId: sampleProject.projectId,
- projectName: sampleProject.projectName,
- designerName: sampleProject.designerName,
- startDate: sampleProject.startDate.toLocaleString('zh-CN'),
- endDate: sampleProject.endDate.toLocaleString('zh-CN'),
- startDateFromNow: `${daysFromStart} 天前`,
- endDateFromNow: daysToEnd >= 0 ? `${daysToEnd} 天后` : `${Math.abs(daysToEnd)} 天前`,
- isEndDateInPast: daysToEnd < 0,
- hasPhaseDeadlines: !!sampleProject.phaseDeadlines
- });
-
- // ✅ 检查日期问题:统计有多少项目的结束日期在过去
- const projectsWithPastEndDate = this.projectTimelineData.filter(p => {
- const daysToEnd = Math.floor((p.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
- return daysToEnd < 0;
- });
-
- if (projectsWithPastEndDate.length > 0) {
- console.warn(`⚠️ 发现 ${projectsWithPastEndDate.length} 个项目的结束日期在过去,这些项目在时间轴上可能显示为一个点`);
- console.log(`⚠️ 示例项目:`, projectsWithPastEndDate.slice(0, 3).map(p => ({
- name: p.projectName,
- endDate: p.endDate.toLocaleString('zh-CN'),
- daysAgo: Math.abs(Math.floor((p.endDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)))
- })));
- }
- }
- }
-
- /**
- * 处理项目点击事件
- */
- onProjectTimelineClick(projectId: string): void {
- if (!projectId) {
- return;
- }
-
- // 🔥 标记从组长看板进入(用于跳过激活检查和显示审批按钮)
- try {
- localStorage.setItem('enterAsTeamLeader', '1');
- localStorage.setItem('teamLeaderMode', 'true');
- // 🔥 关键:清除客服端标记,避免冲突
- localStorage.removeItem('enterFromCustomerService');
- localStorage.removeItem('customerServiceMode');
- console.log('✅ 已标记从组长看板进入,启用组长模式');
- } catch (e) {
- console.warn('无法设置 localStorage 标记:', e);
- }
-
- // 🔥 根据项目当前阶段决定跳转到哪个阶段页面
- const project = this.projects.find(p => p.id === projectId);
- const currentStage = project?.currentStage || '订单分配';
-
- // 阶段映射:项目阶段 → 路由路径
- const stageRouteMap: Record<string, string> = {
- '订单分配': 'order',
- '确认需求': 'requirements',
- '方案深化': 'requirements',
- '建模': 'requirements',
- '软装': 'requirements',
- '渲染': 'requirements',
- '后期': 'requirements',
- '交付执行': 'delivery',
- '交付': 'delivery',
- '售后归档': 'aftercare',
- '已完成': 'aftercare'
- };
-
- const stagePath = stageRouteMap[currentStage] || 'order';
-
- console.log(`🎯 项目当前阶段: ${currentStage},跳转到: ${stagePath}`);
-
- // 获取公司ID(与 viewProjectDetails 保持一致)
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
-
- // 跳转到对应阶段,带上组长标识
- this.router.navigate(['/wxwork', cid, 'project', projectId, stagePath], {
- queryParams: { roleName: 'team-leader' }
- });
- }
-
- /**
- * 构建搜索索引(如果需要)
- */
- private buildSearchIndexes(): void {
- this.projects.forEach(p => {
- if (!p.searchIndex) {
- p.searchIndex = `${p.name}|${p.designerName}`.toLowerCase();
- }
- });
- }
- // 筛选状态改变(由子组件触发)
- // 重置状态筛选
- resetStatusFilter(): void {
- this.selectedStatus = 'all';
- this.applyFilters();
- }
- onFilterChange(filterState: any): void {
- // 更新本地状态
- this.searchTerm = filterState.searchTerm;
- this.selectedType = filterState.type;
- this.selectedUrgency = filterState.urgency;
- this.selectedStatus = filterState.status;
- this.selectedDesigner = filterState.designer;
- this.selectedMemberType = filterState.memberType;
- this.selectedCorePhase = filterState.corePhase;
- this.selectedProjectId = filterState.projectId;
- this.selectedTimeWindow = filterState.timeWindow;
-
- // 应用筛选
- this.applyFilters();
- }
-
- // 状态筛选(由指标卡片点击触发)
- filterByStatus(status: string): void {
- this.selectedStatus = status as any;
- this.applyFilters();
- }
- // 阶段映射(简化版,用于筛选)
- private mapStageToCorePhase(stageId: string): string {
- const orderStages = ['pendingApproval', 'pendingAssignment'];
- const requirementStages = ['requirement', 'planning'];
- const deliveryStages = ['modeling', 'rendering', 'postProduction', 'review', 'revision'];
- const aftercareStages = ['delivery'];
- if (orderStages.includes(stageId)) return 'order';
- if (requirementStages.includes(stageId)) return 'requirements';
- if (deliveryStages.includes(stageId)) return 'delivery';
- if (aftercareStages.includes(stageId)) return 'aftercare';
-
- return '';
- }
- // 统一筛选
- 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.convertToProjectTimeline();
- }
- }
- /**
- * 计算项目加权值
- */
- calculateWorkloadWeight(project: any): number {
- return this.designerService.calculateProjectWeight(project);
- }
- /**
- * 获取设计师加权工作量
- */
- getDesignerWeightedWorkload(designerName: string): {
- weightedTotal: number;
- projectCount: number;
- overdueCount: number;
- loadRate: number;
- } {
- const designerProjects = this.filteredProjects.filter(p => p.designerName === designerName);
- const weightedTotal = designerProjects.reduce((sum, p) => sum + this.calculateWorkloadWeight(p), 0);
- const overdueCount = designerProjects.filter(p => p.isOverdue).length;
-
- // 从realDesigners获取设计师的单周处理量
- const designer = this.realDesigners.find(d => d.name === designerName);
- const weeklyCapacity = designer?.tags?.capacity?.weeklyProjects || 3;
- const loadRate = weeklyCapacity > 0 ? (weightedTotal / weeklyCapacity) * 100 : 0;
-
- return {
- weightedTotal,
- projectCount: designerProjects.length,
- overdueCount,
- loadRate
- };
- }
- /**
- * 工作量卡片数据(替代ECharts)
- */
- get designerWorkloadCards(): Array<{
- name: string;
- loadRate: number;
- weightedValue: number;
- projectCount: number;
- overdueCount: number;
- status: 'overload' | 'busy' | 'idle';
- }> {
- const designers = Array.from(new Set(this.filteredProjects.map(p => p.designerName).filter(n => n && n !== '未分配')));
-
- return designers.map(name => {
- const workload = this.getDesignerWeightedWorkload(name);
- let status: 'overload' | 'busy' | 'idle' = 'idle';
- if (workload.loadRate > 80) status = 'overload';
- else if (workload.loadRate > 50) status = 'busy';
-
- return {
- name,
- loadRate: workload.loadRate,
- weightedValue: workload.weightedTotal,
- projectCount: workload.projectCount,
- overdueCount: workload.overdueCount,
- status
- };
- }).sort((a, b) => b.loadRate - a.loadRate); // 按负载率降序
- }
- /**
- * 获取超负荷设计师数量
- */
- get overloadedDesignersCount(): number {
- return this.designerWorkloadCards.filter(d => d.status === 'overload').length;
- }
- /**
- * 获取平均负载率
- */
- get averageWorkloadRate(): number {
- const cards = this.designerWorkloadCards;
- if (cards.length === 0) return 0;
- const sum = cards.reduce((acc, card) => acc + card.loadRate, 0);
- return sum / cards.length;
- }
- /**
- * 获取预警汇总数据
- */
- getAlertSummary(): {
- totalAlerts: number;
- overdueHighRisk: Project[];
- overloadedDesigners: any[];
- dueSoonProjects: Project[];
- } {
- // 1. 超期高危项目(超期>=5天 或 高紧急度超期)
- const overdueHighRisk = this.filteredProjects
- .filter(p => p.isOverdue && (p.overdueDays >= 5 || p.urgency === 'high'))
- .sort((a, b) => b.overdueDays - a.overdueDays)
- .slice(0, 5);
-
- // 2. 超负荷设计师
- const overloadedDesigners = this.designerWorkloadCards
- .filter(d => d.loadRate > 80)
- .sort((a, b) => b.loadRate - a.loadRate)
- .slice(0, 5);
-
- // 3. 即将到期项目(1-2天内)
- const now = new Date();
- const dueSoonProjects = this.filteredProjects
- .filter(p => {
- if (p.isOverdue) return false;
- const daysLeft = Math.ceil((p.deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
- return daysLeft >= 1 && daysLeft <= 2;
- })
- .sort((a, b) => a.deadline.getTime() - b.deadline.getTime())
- .slice(0, 5);
-
- const totalAlerts = overdueHighRisk.length + overloadedDesigners.length + dueSoonProjects.length;
-
- return {
- totalAlerts,
- overdueHighRisk,
- overloadedDesigners,
- dueSoonProjects
- };
- }
- /**
- * 打开智能推荐弹窗
- */
- async openSmartMatch(project: any): Promise<void> {
- this.selectedProject = project;
- this.showSmartMatch = true;
-
- try {
- this.recommendations = await this.designerService.getRecommendedDesigners(project, this.realDesigners);
- } catch (error) {
- console.error('智能推荐失败:', error);
- this.recommendations = [];
- }
- }
- /**
- * 关闭智能推荐弹窗
- */
- closeSmartMatch(): void {
- this.showSmartMatch = false;
- this.selectedProject = null;
- this.recommendations = [];
- }
- /**
- * 分配项目给设计师
- */
- async assignToDesigner(designerId: string): Promise<void> {
- if (!this.selectedProject) return;
-
- try {
- const success = await this.designerService.assignProject(this.selectedProject.id, designerId);
- if (success) {
- this.closeSmartMatch();
- await this.loadProjects(); // 重新加载项目数据
- }
- } catch (error) {
- console.error('❌ 分配项目失败:', error);
- window?.fmode?.alert('分配失败,请重试');
- }
- }
- // 切换视图
- toggleView(): void {
- this.showGanttView = !this.showGanttView;
- if (this.showGanttView) {
- this.convertToProjectTimeline();
- }
- }
- ngOnDestroy(): void {
- // 清理待办任务自动刷新定时器
- if (this.todoTaskRefreshTimer) {
- clearInterval(this.todoTaskRefreshTimer);
- }
- }
- // 🔥 已延期项目
- get overdueProjects(): Project[] {
- return this.projects.filter(p => p.isOverdue);
- }
- // ⏳ 临期项目(3天内)
- get dueSoonProjects(): Project[] {
- return this.projects.filter(p => p.dueSoon && !p.isOverdue);
- }
- // 📋 待审批项目(支持中文和英文阶段名称)
- get pendingApprovalProjects(): Project[] {
- const pending = this.projects.filter(p => {
- const stage = (p.currentStage || '').trim();
- const data = (p as any).data || {};
- const approvalStatus = data.approvalStatus;
-
- // 1. 阶段为"订单分配"且审批状态为 pending
- // 2. 或者阶段为"待确认"/"待审批"(兼容旧数据)
- return (stage === '订单分配' && approvalStatus === 'pending') ||
- stage === '待审批' ||
- stage === '待确认';
- });
-
- return pending;
- }
- // 检查项目是否待审批
- isPendingApproval(project: Project): boolean {
- const stage = (project.currentStage || '').trim();
- const stageEn = stage.toLowerCase();
- const data: any = (project as any).data || {};
-
- // 🔥 新增:检查顶层的 pendingApproval 字段(备用方案)
- const topLevelPending = (project as any).pendingApproval === true && (project as any).approvalStage === '订单分配';
-
- return (stage === '订单分配' && (data.approvalStatus === 'pending' || topLevelPending)) ||
- ((stage === '交付执行' || stageEn === 'delivery') &&
- (data.deliveryApproval?.status === 'pending' ||
- (Array.isArray(data.pendingDeliveryApprovals) && data.pendingDeliveryApprovals.some((x: any) => x?.status === 'pending'))));
- }
- // 🎯 待分配项目(支持中文和英文阶段名称)
- get pendingAssignmentProjects(): Project[] {
- return this.projects.filter(p => {
- const stage = (p.currentStage || '').trim().toLowerCase();
- return stage === 'pendingassignment' ||
- stage === '待分配' ||
- stage === '订单分配';
- });
- }
- // 智能推荐设计师
- private getRecommendedDesigner(projectType: 'soft' | 'hard') {
- if (!this.designerProfiles || !this.designerProfiles.length) return null;
- const scoreOf = (p: any) => {
- const workloadScore = 100 - (p.workload ?? 0); // 负载越低越好
- const ratingScore = (p.avgRating ?? 0) * 10; // 评分越高越好
- const expScore = (p.experience ?? 0) * 5; // 经验越高越好
- return workloadScore * 0.5 + ratingScore * 0.3 + expScore * 0.2;
- };
- const sorted = [...this.designerProfiles].sort((a, b) => scoreOf(b) - scoreOf(a));
- return sorted[0] || null;
- }
- // 项目质量评审(由子组件触发)
- reviewProjectQuality(data: {projectId: string, rating: 'excellent' | 'qualified' | 'unqualified'}): void {
- const project = this.projects.find(p => p.id === data.projectId);
- if (!project) return;
- project.qualityRating = data.rating;
- if (data.rating === 'unqualified') {
- project.currentStage = 'revision';
- }
- this.applyFilters();
- window?.fmode?.alert('质量评审已提交');
- }
- /**
- * 根据看板列跳转到项目详情(参考客服板块实现)
- * @param projectId 项目ID
- * @param corePhaseId 核心阶段ID (order/requirements/delivery/aftercare)
- */
- viewProjectDetailsByPhase(projectId: string, corePhaseId: string): void {
- if (!projectId) {
- return;
- }
-
- // 🔥 标记从组长看板进入(用于跳过激活检查和显示审批按钮)
- try {
- localStorage.setItem('enterAsTeamLeader', '1');
- localStorage.setItem('teamLeaderMode', 'true');
- // 🔥 关键:清除客服端标记,避免冲突
- localStorage.removeItem('enterFromCustomerService');
- localStorage.removeItem('customerServiceMode');
- console.log('✅ 已标记从组长看板进入,启用组长模式');
- } catch (e) {
- console.warn('无法设置 localStorage 标记:', e);
- }
-
- // 获取公司ID
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
-
- // 🔥 根据看板列ID直接映射到路由路径(与客服板块保持一致)
- // corePhaseId已经是路由路径格式:order, requirements, delivery, aftercare
- const stagePath = corePhaseId;
-
- console.log(`🎯 从看板列「${this.corePhases.find(c => c.id === corePhaseId)?.name}」进入项目,跳转到: ${stagePath}`);
-
- // ✅ 跳转到对应阶段,并通过查询参数标识为组长视角
- this.router.navigate(['/wxwork', cid, 'project', projectId, stagePath], {
- queryParams: { roleName: 'team-leader' }
- });
- }
- // 查看项目详情 - 跳转到纯净的项目详情页(无管理端UI)
- viewProjectDetails(projectId: string): void {
- if (!projectId) {
- return;
- }
-
- // 🔥 标记从组长看板进入(用于跳过激活检查和显示审批按钮)
- try {
- localStorage.setItem('enterAsTeamLeader', '1');
- localStorage.setItem('teamLeaderMode', 'true');
- // 🔥 关键:清除客服端标记,避免冲突
- localStorage.removeItem('enterFromCustomerService');
- localStorage.removeItem('customerServiceMode');
- console.log('✅ 已标记从组长看板进入,启用组长模式');
- } catch (e) {
- console.warn('无法设置 localStorage 标记:', e);
- }
-
- // 获取公司ID
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
-
- // 🔥 根据项目当前阶段决定跳转到哪个阶段页面
- const project = this.projects.find(p => p.id === projectId);
- const currentStage = project?.currentStage || '订单分配';
-
- // 阶段映射:项目阶段 → 路由路径
- const stageRouteMap: Record<string, string> = {
- '订单分配': 'order',
- '确认需求': 'requirements',
- '方案深化': 'requirements',
- '建模': 'requirements',
- '软装': 'requirements',
- '渲染': 'requirements',
- '后期': 'requirements',
- '交付执行': 'delivery',
- '交付': 'delivery',
- '售后归档': 'aftercare',
- '已完成': 'aftercare'
- };
-
- const stagePath = stageRouteMap[currentStage] || 'order';
-
- console.log(`🎯 项目当前阶段: ${currentStage},跳转到: ${stagePath}`);
-
- // ✅ 跳转到对应阶段,并通过查询参数标识为组长视角
- this.router.navigate(['/wxwork', cid, 'project', projectId, stagePath], {
- queryParams: { roleName: 'team-leader' }
- });
- }
- // 快速分配项目(增强:加入智能推荐)
- async quickAssignProject(projectId: string): Promise<void> {
- const project = this.projects.find(p => p.id === projectId);
- if (!project) {
- window?.fmode?.alert('未找到对应项目');
- return;
- }
- const recommended = this.getRecommendedDesigner(project.type);
- if (recommended) {
- const reassigning = !!project.designerName;
- const message = `推荐设计师:${recommended.name}(工作负载:${recommended.workload}%,评分:${recommended.avgRating}分)` +
- (reassigning ? `\n\n该项目当前已由「${project.designerName}」负责,是否改为分配给「${recommended.name}」?` : '\n\n是否确认分配?');
- const confirmAssign = await window?.fmode?.confirm(message);
- if (confirmAssign) {
- project.designerName = recommended.name;
- if (project.currentStage === 'pendingAssignment' || project.currentStage === 'pendingApproval') {
- project.currentStage = 'requirement';
- }
- project.status = '进行中';
- // 更新设计师筛选列表
- this.designers = Array.from(new Set(this.projects.map(p => p.designerName).filter(n => !!n)));
- this.applyFilters();
- window?.fmode?.alert(`项目已${reassigning ? '重新' : ''}分配给 ${recommended.name}`);
- return;
- }
- }
- // 无推荐或用户取消,跳转到详细分配页面
- // 跳转到项目详情页
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
- // 动态跳转到项目当前阶段,并标记组长身份
- const current = this.projects.find(p => p.id === projectId)?.currentStage || '订单分配';
- const stageRouteMap: Record<string, string> = {
- '订单分配': 'order',
- '确认需求': 'requirements',
- '方案深化': 'requirements',
- '建模': 'requirements',
- '软装': 'requirements',
- '渲染': 'requirements',
- '后期': 'requirements',
- '交付执行': 'delivery',
- '交付': 'delivery',
- '售后归档': 'aftercare',
- '已完成': 'aftercare'
- };
- const stagePath = stageRouteMap[current] || 'order';
- this.router.navigate(['/wxwork', cid, 'project', projectId, stagePath], {
- queryParams: { roleName: 'team-leader' }
- });
- }
- // 预警相关操作(由子组件触发)
- viewAllOverdueProjects(): void {
- this.filterByStatus('overdue');
- this.showAlert = false;
- }
- closeAlert(): void {
- this.showAlert = false;
- }
- // 处理甘特图员工点击事件
- async onEmployeeClick(employeeName: string): Promise<void> {
- if (!employeeName || employeeName === '未分配') {
- return;
- }
-
- this.selectedEmployeeName = employeeName;
- this.selectedEmployeeProjects = this.designerWorkloadMap.get(employeeName) || [];
- this.showEmployeeDetailPanel = true;
- // 🔥 修复:手动触发变更检测,确保弹窗立即显示
- this.cdr.markForCheck();
- }
- // 关闭员工详情面板
- closeEmployeeDetailPanel(): void {
- this.showEmployeeDetailPanel = false;
- this.selectedEmployeeName = '';
- this.selectedEmployeeProjects = [];
- this.selectedEmployeeDetail = null;
- }
-
- // 员工详情面板:日历月份切换
- changeEmployeeCalendarMonth(direction: number): void {
- // 由 EmployeeDetailPanelComponent 内部处理
- console.log('日历月份切换:', direction);
- }
-
- // 员工详情面板:日历日期点击
- onCalendarDayClick(day: any): void {
- // 由 EmployeeDetailPanelComponent 内部处理
- console.log('日历日期点击:', day);
- }
-
- // 员工详情面板:刷新问卷
- refreshEmployeeSurvey(): void {
- // 由 EmployeeDetailPanelComponent 内部处理
- console.log('刷新员工问卷');
- }
- // 从员工详情面板跳转到项目详情
- navigateToProjectFromPanel(projectId: string): void {
- if (!projectId) {
- return;
- }
-
- // 关闭员工详情面板
- this.closeEmployeeDetailPanel();
-
- // 🔥 标记从组长看板进入(用于跳过激活检查和显示审批按钮)
- try {
- localStorage.setItem('enterAsTeamLeader', '1');
- localStorage.setItem('teamLeaderMode', 'true');
- // 🔥 关键:清除客服端标记,避免冲突
- localStorage.removeItem('enterFromCustomerService');
- localStorage.removeItem('customerServiceMode');
- console.log('✅ 已标记从组长看板进入,启用组长模式');
- } catch (e) {
- console.warn('无法设置 localStorage 标记:', e);
- }
-
- // 🔥 根据项目当前阶段决定跳转到哪个阶段页面
- const project = this.projects.find(p => p.id === projectId);
- const currentStage = project?.currentStage || '订单分配';
-
- // 阶段映射:项目阶段 → 路由路径
- const stageRouteMap: Record<string, string> = {
- '订单分配': 'order',
- '确认需求': 'requirements',
- '方案深化': 'requirements',
- '建模': 'requirements',
- '软装': 'requirements',
- '渲染': 'requirements',
- '后期': 'requirements',
- '交付执行': 'delivery',
- '交付': 'delivery',
- '售后归档': 'aftercare',
- '已完成': 'aftercare'
- };
-
- const stagePath = stageRouteMap[currentStage] || 'order';
-
- console.log(`🎯 项目当前阶段: ${currentStage},跳转到: ${stagePath}`);
-
- // 跳转到对应阶段,通过查询参数标识为组长视角
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
- this.router.navigate(['/wxwork', cid, 'project', projectId, stagePath], {
- queryParams: { roleName: 'team-leader' }
- });
- }
- /**
- * 加载用户Profile信息
- */
- async loadUserProfile(): Promise<void> {
- try {
- const cid = localStorage.getItem("company");
- if (!cid) {
- console.warn('未找到公司ID,使用默认用户信息');
- return;
- }
- const wwAuth = new WxworkAuth({ cid });
- const profile = await wwAuth.currentProfile();
- if (profile) {
- const name = profile.get("name") || profile.get("mobile") || '组长';
- const avatar = profile.get("avatar");
- const roleName = profile.get("roleName") || '组长';
- this.currentUser = {
- name,
- avatar: avatar || this.generateDefaultAvatar(name),
- roleName
- };
- console.log('用户Profile加载成功:', this.currentUser);
- }
- } catch (error) {
- console.error('加载用户Profile失败:', error);
- // 保持默认值
- }
- }
- /**
- * 生成默认头像(SVG格式)
- * @param name 用户名
- * @returns Base64编码的SVG数据URL
- */
- generateDefaultAvatar(name: string): string {
- const initial = name ? name.substring(0, 2).toUpperCase() : '组长';
- const bgColor = '#CCFFCC';
- const textColor = '#555555';
-
- const svg = `<svg width='40' height='40' xmlns='http://www.w3.org/2000/svg'>
- <rect width='100%' height='100%' fill='${bgColor}'/>
- <text x='50%' y='50%' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='${textColor}' dy='0.3em'>${initial}</text>
- </svg>`;
-
- return `data:image/svg+xml,${encodeURIComponent(svg)}`;
- }
- // ==================== 新增:待办任务相关方法 ====================
-
- /**
- * 从问题板块加载待办任务
- */
- async loadTodoTasksFromIssues(): Promise<void> {
- this.loadingTodoTasks = true;
- this.todoTaskError = '';
-
- try {
- const Parse: any = FmodeParse.with('nova');
- const query = new Parse.Query('ProjectIssue');
-
- // 筛选条件:待处理 + 处理中
- query.containedIn('status', ['待处理', '处理中']);
- query.notEqualTo('isDeleted', true);
-
- // 关联数据
- query.include(['project', 'creator', 'assignee']);
-
- // 排序:更新时间倒序
- query.descending('updatedAt');
-
- // 限制数量
- query.limit(50);
-
- const results = await query.find();
-
- console.log(`📥 查询到 ${results.length} 条问题记录`);
-
- // 数据转换(异步处理以支持 fetch)
- const tasks = await Promise.all(results.map(async (obj: any) => {
- let project = obj.get('project');
- const assignee = obj.get('assignee');
- const creator = obj.get('creator');
- const data = obj.get('data') || {};
-
- let projectName = '未知项目';
- let projectId = '';
-
- // 如果 project 存在,尝试获取完整数据
- if (project) {
- projectId = project.id;
-
- // 尝试从已加载的对象获取 name
- projectName = project.get('name');
-
- // 如果 name 为空,使用 Parse.Query 查询项目
- if (!projectName && projectId) {
- try {
- console.log(`🔄 查询项目数据: ${projectId}`);
- const projectQuery = new Parse.Query('Project');
- const fetchedProject = await projectQuery.get(projectId);
- projectName = fetchedProject.get('name') || fetchedProject.get('title') || '未知项目';
- console.log(`✅ 项目名称: ${projectName}`);
- } catch (error) {
- console.warn(`⚠️ 无法加载项目 ${projectId}:`, error);
- projectName = `项目-${projectId.slice(0, 6)}`;
- }
- }
- } else {
- console.warn('⚠️ 问题缺少关联项目:', {
- issueId: obj.id,
- title: obj.get('title')
- });
- }
-
- return {
- id: obj.id,
- title: obj.get('title') || obj.get('description')?.slice(0, 40) || '未命名问题',
- description: obj.get('description'),
- priority: obj.get('priority') as IssuePriority || 'medium',
- type: obj.get('issueType') as IssueType || 'task',
- status: this.zh2enStatus(obj.get('status')) as IssueStatus,
- projectId,
- projectName,
- relatedSpace: obj.get('relatedSpace') || data.relatedSpace,
- relatedStage: obj.get('relatedStage') || data.relatedStage,
- assigneeName: assignee?.get('name') || assignee?.get('realname') || '未指派',
- creatorName: creator?.get('name') || creator?.get('realname') || '未知',
- createdAt: obj.createdAt || new Date(),
- updatedAt: obj.updatedAt || new Date(),
- dueDate: obj.get('dueDate'),
- tags: (data.tags || []) as string[]
- };
- }));
-
- this.todoTasksFromIssues = tasks;
-
- // 排序:优先级 -> 时间
- this.todoTasksFromIssues.sort((a, b) => {
- const priorityA = this.getPriorityOrder(a.priority);
- const priorityB = this.getPriorityOrder(b.priority);
-
- if (priorityA !== priorityB) {
- return priorityA - priorityB;
- }
-
- return +new Date(b.updatedAt) - +new Date(a.updatedAt);
- });
-
- console.log(`✅ 加载待办任务成功,共 ${this.todoTasksFromIssues.length} 条`);
-
- } catch (error) {
- console.error('❌ 加载待办任务失败:', error);
- this.todoTaskError = '加载失败,请稍后重试';
- } finally {
- this.loadingTodoTasks = false;
- }
- }
-
- /**
- * 启动自动刷新(每5分钟)
- */
- startAutoRefresh(): void {
- this.todoTaskRefreshTimer = setInterval(() => {
- console.log('🔄 自动刷新待办任务...');
- this.loadTodoTasksFromIssues();
- }, 5 * 60 * 1000); // 5分钟
- }
-
- /**
- * 手动刷新待办任务
- */
- refreshTodoTasks(): void {
- console.log('🔄 手动刷新待办任务...');
- this.loadTodoTasksFromIssues();
- this.calculateUrgentEvents(); // 🆕 同时刷新紧急事件
- }
-
- /**
- * 🆕 从项目时间轴数据计算紧急事件
- * 识别截止时间已到或即将到达但未完成的关键节点
- */
- calculateUrgentEvents(): void {
- this.loadingUrgentEvents = true;
- const events: UrgentEvent[] = [];
- const now = new Date();
- const oneDayMs = 24 * 60 * 60 * 1000;
- const handledSet = this.handledUrgentEventIds;
- const mutedSet = this.mutedUrgentEventIds;
- const projectEventCount = new Map<string, number>(); // 追踪每个项目生成的事件数
- const MAX_EVENTS_PER_PROJECT = 2; // 每个项目最多生成2个最紧急的事件
- const resolveCategory = (
- eventType: UrgentEvent['eventType'],
- category?: 'customer' | 'phase' | 'review' | 'delivery'
- ): 'customer' | 'phase' | 'review' | 'delivery' => {
- if (category) return category;
- switch (eventType) {
- case 'phase_deadline':
- return 'phase';
- case 'delivery':
- return 'delivery';
- case 'customer_alert':
- return 'customer';
- default:
- return 'review';
- }
- };
- // 🆕 辅助方法:获取逾期原因
- const getOverdueReason = (daysDiff: number, stalledDays: number = 0) => {
- if (daysDiff >= 0) return ''; // 未逾期
-
- // 如果项目超过3天未更新/无反馈,推测为客户原因
- if (stalledDays >= 3) {
- return '原因:客户未跟进反馈导致逾期';
- }
-
- // 否则推测为设计师进度原因
- return '原因:设计师进度原因导致逾期';
- };
- const addEvent = (
- partial: Omit<UrgentEvent, 'category' | 'statusType' | 'labels' | 'allowConfirmOnTime' | 'allowMarkHandled' | 'allowCreateTodo' | 'followUpNeeded'> &
- Partial<UrgentEvent>
- ) => {
- // 检查该项目是否已达到事件数量上限
- const currentCount = projectEventCount.get(partial.projectId) || 0;
- if (currentCount >= MAX_EVENTS_PER_PROJECT) {
- return; // 跳过,避免单个项目产生过多事件
- }
-
- const category = resolveCategory(partial.eventType, partial.category);
- const statusType: UrgentEvent['statusType'] =
- partial.statusType || (partial.overdueDays && partial.overdueDays > 0 ? 'overdue' : 'dueSoon');
-
- // 简化描述,避免过长字符串
- const description = partial.description?.substring(0, 100) || '';
-
- const event: UrgentEvent = {
- ...partial,
- description,
- category,
- statusType,
- labels: partial.labels ?? [],
- followUpNeeded: partial.followUpNeeded ?? false,
- allowConfirmOnTime:
- partial.allowConfirmOnTime ?? (category !== 'customer' && statusType === 'dueSoon'),
- allowMarkHandled: partial.allowMarkHandled ?? true,
- allowCreateTodo: partial.allowCreateTodo ?? category === 'customer'
- };
- events.push(event);
- projectEventCount.set(partial.projectId, currentCount + 1);
- };
-
- try {
- // 从 projectTimelineData 中提取数据
- this.projectTimelineData.forEach(project => {
- // 1. 检查小图对图事件
- if (project.reviewDate) {
- const reviewTime = project.reviewDate.getTime();
- const timeDiff = reviewTime - now.getTime();
- const daysDiff = Math.ceil(timeDiff / oneDayMs);
-
- // 如果小图对图已经到期或即将到期(1天内),且不在已完成状态
- if (daysDiff <= 1 && project.currentStage !== 'delivery') {
- const reason = getOverdueReason(daysDiff, project.stalledDays);
- const descSuffix = reason ? `,${reason}` : '';
-
- addEvent({
- id: `${project.projectId}-review`,
- title: `小图对图截止`,
- description: `项目「${project.projectName}」的小图对图时间已${daysDiff < 0 ? '逾期' : '临近'}${descSuffix}`,
- eventType: 'review',
- deadline: project.reviewDate,
- projectId: project.projectId,
- projectName: project.projectName,
- designerName: project.designerName,
- urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
- overdueDays: -daysDiff,
- labels: daysDiff < 0 ? ['逾期'] : ['临近'],
- followUpNeeded: (project.stageName || '').includes('图') || project.status === 'warning'
- });
- }
- }
-
- // 2. 检查交付事件
- if (project.deliveryDate) {
- const deliveryTime = project.deliveryDate.getTime();
- const timeDiff = deliveryTime - now.getTime();
- const daysDiff = Math.ceil(timeDiff / oneDayMs);
-
- // 如果交付已经到期或即将到期(1天内),且不在已完成状态
- if (daysDiff <= 1 && project.currentStage !== 'delivery') {
- const summary = project.spaceDeliverableSummary;
- const completionRate = summary?.overallCompletionRate || 0;
- const reason = getOverdueReason(daysDiff, project.stalledDays);
- const descSuffix = reason ? `,${reason}` : '';
-
- addEvent({
- id: `${project.projectId}-delivery`,
- title: `项目交付截止`,
- description: `项目「${project.projectName}」需要在 ${project.deliveryDate.toLocaleDateString()} 交付(当前完成率 ${completionRate}%)${descSuffix}`,
- eventType: 'delivery',
- deadline: project.deliveryDate,
- projectId: project.projectId,
- projectName: project.projectName,
- designerName: project.designerName,
- urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
- overdueDays: -daysDiff,
- completionRate,
- labels: daysDiff < 0 ? ['逾期'] : ['临近']
- });
- }
- }
-
- // 3. 检查各阶段截止时间
- if (project.phaseDeadlines) {
- const phaseMap = {
- modeling: '建模',
- softDecor: '软装',
- rendering: '渲染',
- postProcessing: '后期'
- };
-
- Object.entries(project.phaseDeadlines).forEach(([key, phaseInfo]: [string, any]) => {
- if (phaseInfo && phaseInfo.deadline) {
- const deadline = new Date(phaseInfo.deadline);
- const phaseTime = deadline.getTime();
- const timeDiff = phaseTime - now.getTime();
- const daysDiff = Math.ceil(timeDiff / oneDayMs);
-
- // 如果阶段已经到期或即将到期(1天内),且状态不是已完成
- if (daysDiff <= 1 && phaseInfo.status !== 'completed') {
- const phaseName = phaseMap[key as keyof typeof phaseMap] || key;
-
- // 获取该阶段的完成率
- const summary = project.spaceDeliverableSummary;
- let completionRate = 0;
- if (summary && summary.phaseProgress) {
- const phaseProgress = summary.phaseProgress[key as keyof typeof summary.phaseProgress];
- completionRate = phaseProgress?.completionRate || 0;
- }
-
- const reason = getOverdueReason(daysDiff, project.stalledDays);
- const descSuffix = reason ? `,${reason}` : '';
- addEvent({
- id: `${project.projectId}-phase-${key}`,
- title: `${phaseName}阶段截止`,
- description: `项目「${project.projectName}」的${phaseName}阶段截止时间已${daysDiff < 0 ? '逾期' : '临近'}(完成率 ${completionRate}%)${descSuffix}`,
- eventType: 'phase_deadline',
- phaseName,
- deadline,
- projectId: project.projectId,
- projectName: project.projectName,
- designerName: project.designerName,
- urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
- overdueDays: -daysDiff,
- completionRate,
- labels: daysDiff < 0 ? ['逾期'] : ['临近']
- });
- }
- }
- });
- }
-
- if (project.stalledDays && project.stalledDays >= 7) {
- addEvent({
- id: `${project.projectId}-stagnant`,
- title: project.stalledDays >= 14 ? '客户停滞预警' : '停滞期提醒',
- description: `项目「${project.projectName}」已有 ${project.stalledDays} 天未收到客户反馈,请主动跟进。`,
- eventType: 'customer_alert',
- deadline: new Date(),
- projectId: project.projectId,
- projectName: project.projectName,
- designerName: project.designerName,
- urgencyLevel: project.stalledDays >= 14 ? 'critical' : 'high',
- statusType: 'stagnant',
- stagnationDays: project.stalledDays,
- labels: ['停滞期'],
- followUpNeeded: true,
- allowCreateTodo: true,
- allowConfirmOnTime: false,
- category: 'customer'
- });
- }
- const inReviewStage = (project.stageName || '').includes('图') || (project.currentStage || '').includes('图');
- if (inReviewStage && project.status === 'warning') {
- addEvent({
- id: `${project.projectId}-review-followup`,
- title: '对图反馈待跟进',
- description: `项目「${project.projectName}」客户反馈尚未处理,请尽快跟进。`,
- eventType: 'customer_alert',
- deadline: project.reviewDate || new Date(),
- projectId: project.projectId,
- projectName: project.projectName,
- designerName: project.designerName,
- urgencyLevel: 'high',
- statusType: project.reviewDate && project.reviewDate < now ? 'overdue' : 'dueSoon',
- labels: ['对图期'],
- followUpNeeded: true,
- allowCreateTodo: true,
- customerIssueType: 'feedback_pending',
- category: 'customer'
- });
- }
- if (project.priority === 'critical') {
- addEvent({
- id: `${project.projectId}-customer-alert`,
- title: '客户服务预警',
- description: `项目「${project.projectName}」存在客户不满或抱怨,需要立即处理并记录。`,
- eventType: 'customer_alert',
- deadline: new Date(),
- projectId: project.projectId,
- projectName: project.projectName,
- designerName: project.designerName,
- urgencyLevel: 'critical',
- statusType: 'dueSoon',
- labels: ['客户预警'],
- followUpNeeded: true,
- allowCreateTodo: true,
- customerIssueType: 'complaint',
- category: 'customer'
- });
- }
- });
-
- // 按紧急程度和时间排序
- events.sort((a, b) => {
- // 首先按紧急程度排序
- const urgencyOrder = { critical: 0, high: 1, medium: 2 };
- const urgencyDiff = urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel];
- if (urgencyDiff !== 0) return urgencyDiff;
-
- // 相同紧急程度,按截止时间排序(越早越靠前)
- return a.deadline.getTime() - b.deadline.getTime();
- });
-
- // 过滤已处理和静音的事件
- const filteredEvents = events.filter(event => !handledSet.has(event.id) && !mutedSet.has(event.id));
-
- // 🔥 限制最大显示数量,避免渲染过多 DOM 导致卡顿和 RangeError
- const MAX_URGENT_EVENTS = 50;
- this.urgentEvents = filteredEvents.slice(0, MAX_URGENT_EVENTS);
-
- if (filteredEvents.length > MAX_URGENT_EVENTS) {
- console.warn(`⚠️ 紧急事件过多(${filteredEvents.length}个),已限制为前 ${MAX_URGENT_EVENTS} 个最紧急的事件`);
- }
-
- console.log(`✅ 计算紧急事件完成,共 ${this.urgentEvents.length} 个紧急事件(原始${events.length}个)`);
-
- } catch (error) {
- console.error('❌ 计算紧急事件失败:', error);
- // 发生错误时清空列表,避免渲染异常数据
- this.urgentEvents = [];
- } finally {
- this.loadingUrgentEvents = false;
- // 确保触发变更检测
- this.cdr.markForCheck();
- }
- }
- // 紧急事件处理方法(由子组件触发)
- confirmEventOnTime(event: UrgentEvent): void {
- this.mutedUrgentEventIds.add(event.id);
- this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id);
- this.cdr.markForCheck();
- }
- resolveUrgentEvent(event: UrgentEvent): void {
- this.handledUrgentEventIds.add(event.id);
- this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id);
- this.cdr.markForCheck();
- }
- markEventAsStagnant(event: UrgentEvent): void {
- this.urgentEvents = this.urgentEvents.map(item => {
- if (item.id !== event.id) return item;
- const labels = new Set(item.labels || []);
- labels.add('停滞期');
- return {
- ...item,
- category: 'customer' as const,
- statusType: 'stagnant' as const,
- stagnationDays: item.stagnationDays || 7,
- labels: Array.from(labels),
- followUpNeeded: true
- };
- });
- this.cdr.markForCheck();
- }
- createTodoFromEvent(event: UrgentEvent): void {
- const now = new Date();
- const newTask: TodoTaskFromIssue = {
- id: `urgent-todo-${event.id}-${now.getTime()}`,
- title: `【紧急】${event.title}`,
- description: event.description,
- priority: event.urgencyLevel === 'critical' ? 'urgent' : event.urgencyLevel === 'high' ? 'high' : 'medium',
- type: 'feedback',
- status: 'open',
- projectId: event.projectId,
- projectName: event.projectName,
- relatedStage: event.phaseName,
- assigneeName: event.designerName || '待分配',
- creatorName: this.currentUser.name,
- createdAt: now,
- updatedAt: now,
- dueDate: event.deadline,
- tags: [...(event.labels || []), '来自紧急事件']
- };
- this.todoTasksFromIssues = [newTask, ...this.todoTasksFromIssues];
- this.resolveUrgentEvent(event);
- }
-
- // 待办任务操作(由子组件触发)
- navigateToIssue(task: TodoTaskFromIssue): void {
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
- this.router.navigate(
- ['/wxwork', cid, 'project', task.projectId, 'order'],
- {
- queryParams: {
- openIssues: 'true',
- highlightIssue: task.id,
- roleName: 'team-leader'
- }
- }
- );
- }
-
- markAsRead(task: TodoTaskFromIssue): void {
- this.todoTasksFromIssues = this.todoTasksFromIssues.filter(t => t.id !== task.id);
- console.log(`✅ 标记问题为已读: ${task.title}`);
- }
-
- // 状态映射辅助方法
- private zh2enStatus(status: string): IssueStatus {
- const map: Record<string, IssueStatus> = {
- '待处理': 'open',
- '处理中': 'in_progress',
- '已解决': 'resolved',
- '已关闭': 'closed'
- };
- return map[status] || 'open';
- }
-
- private getPriorityOrder(priority: IssuePriority): number {
- const config: Record<IssuePriority, number> = {
- urgent: 0,
- critical: 0,
- high: 1,
- medium: 2,
- low: 3
- };
- return config[priority] || 2;
- }
- }
|