# 组长端Dashboard综合优化方案 **实施日期**: 2025-10-22 **文档状态**: 最新版(基于原版dashboard重新规划) ## 一、现状分析 ### 1.1 当前功能清单 | 模块 | 功能 | 数据来源 | 问题 | |-----|------|---------|-----| | **KPI卡片** | 4个指标(延期、临期、待确认、待分配) | 模拟数据 | ❌ 未对接真实数据 | | **工作量概览** | ECharts横向堆叠柱状图 | 模拟数据 | ❌ 仅按项目数量统计,未考虑复杂度 | | **甘特图** | 两种模式(项目/设计师) | 模拟数据 | ❌ 切换过多,信息密度过高 | | **项目看板** | 4列看板(订单→需求→交付→售后) | 模拟数据 | ✅ 结构合理,需对接真实数据 | | **筛选器** | 7个维度筛选 | - | ⚠️ 功能完整但UI可简化 | | **搜索** | 关键词搜索+建议 | 模拟数据 | ✅ 交互良好,需对接真实数据 | | **设计师画像** | 硬编码8个设计师 | 模拟数据 | ❌ 需改为Profile.data.tags动态读取 | ### 1.2 核心问题 1. **数据层**:100%模拟数据,未使用fmode-ng查询Parse Server 2. **算法层**:工作量计算过于简单(项目数量≠真实工作量) 3. **UI层**:切换过多(工作量按设计师/会员切换,甘特按项目/设计师切换) 4. **功能层**:缺少智能推荐、设计师标签体系 --- ## 二、优化目标 ### 2.1 业务目标 | 目标 | 当前耗时 | 优化后 | 提升 | |-----|---------|-------|-----| | **发现问题项目** | 5-10秒(需扫描多个视图) | 1-2秒(统一预警面板) | 80%↓ | | **分配项目** | 5-10分钟(手动查看负载+擅长) | 1-2分钟(智能推荐) | 70%↓ | | **负载评估** | 仅凭项目数量 | 加权工作量科学评估 | 准确度↑ | ### 2.2 技术目标 - ✅ 100% fmode-ng真实数据对接(移除所有模拟数据) - ✅ 实现加权工作量计算(项目类型×剩余工期×紧急度) - ✅ 建立设计师标签体系(Profile.data.tags) - ✅ 实现智能匹配算法 - ✅ 简化UI切换(减少50%模式切换) --- ## 三、详细优化方案 ### 3.1 数据层优化:fmode-ng真实对接 #### 创建DesignerService ```typescript // src/app/pages/team-leader/services/designer.service.ts import { Injectable } from '@angular/core'; import { FmodeParse as Parse } from 'fmode-ng/parse'; interface DesignerTags { expertise: { styles: string[]; // 擅长风格 skills: string[]; // 专业技能 spaceTypes: string[]; // 擅长空间 }; capacity: { weeklyProjects: number; // 单周可处理项目量 maxConcurrent: number; // 最大并发数 avgDaysPerProject: number; // 平均完成天数 }; emergency: { willing: boolean; // 是否接受紧急单 premium: number; // 加急溢价(%) maxPerWeek: number; // 每周最多紧急单数 }; history: { totalProjects: number; // 历史总项目数 completionRate: number; // 完成率(%) avgRating: number; // 平均评分 onTimeRate: number; // 按时交付率(%) excellentCount: number; // 优秀作品数 }; portfolio: string[]; // 代表作(ProjectFile objectId数组) } @Injectable({ providedIn: 'root' }) export class DesignerService { private cid: string = ''; constructor() { this.cid = localStorage.getItem('company') || ''; } /** * 获取所有设计师(组员角色) */ async getDesigners(): Promise { const query = new Parse.Query('Profile'); query.equalTo('company', this.cid); query.equalTo('roleName', '组员'); // 设计师角色 query.notEqualTo('isDeleted', true); query.select('name', 'department', 'data'); query.limit(1000); const profiles = await query.find(); return profiles.map(p => { const data = p.get('data') || {}; const tags = data.tags || this.getDefaultTags(); return { id: p.id, name: p.get('name'), department: p.get('department')?.get?.('name') || '未分组', tags, profile: p }; }); } /** * 查询设计师当前负载 */ async getDesignerWorkload(designerId: string): Promise<{ projects: any[]; weightedTotal: number; overdueCount: number; loadRate: number; }> { // 查询设计师负责的所有进行中项目 const query = new Parse.Query('Project'); query.equalTo('assignee', Parse.Object.extend('Profile').createWithoutData(designerId)); query.equalTo('company', this.cid); query.containedIn('status', ['进行中', '待分配']); query.notEqualTo('isDeleted', true); query.select('title', 'deadline', 'status', 'data'); query.limit(1000); const projects = await query.find(); // 计算加权总量 let weightedTotal = 0; let overdueCount = 0; const now = new Date(); projects.forEach(proj => { const weight = this.calculateProjectWeight(proj); weightedTotal += weight; const deadline = proj.get('deadline'); if (deadline && deadline < now) { overdueCount++; } }); // 获取设计师单周处理量 const designerProfiles = await this.getDesigners(); const designer = designerProfiles.find(d => d.id === designerId); const weeklyCapacity = designer?.tags?.capacity?.weeklyProjects || 3; // 计算负载率 const loadRate = weeklyCapacity > 0 ? (weightedTotal / weeklyCapacity * 100) : 0; return { projects: projects.map(p => p.toJSON()), weightedTotal, overdueCount, loadRate }; } /** * 计算项目加权值 */ private calculateProjectWeight(project: any): number { const data = project.get('data') || {}; // 1. 项目类型系数 const projectType = data.projectType || 'soft'; const typeWeight = projectType === 'hard' ? 2.0 : 1.0; // 2. 剩余工期系数 const deadline = project.get('deadline'); const now = new Date(); const daysLeft = deadline ? Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) : 30; let timeWeight = 0.8; if (daysLeft < 0) timeWeight = 1.5; // 超期 else if (daysLeft <= 3) timeWeight = 1.3; // 临期 else if (daysLeft <= 7) timeWeight = 1.0; else timeWeight = 0.8; // 3. 紧急度系数 const urgency = data.urgency || 'low'; const urgencyWeight = urgency === 'high' ? 1.2 : urgency === 'medium' ? 1.0 : 0.8; return typeWeight * timeWeight * urgencyWeight; } /** * 获取所有项目 */ async getProjects(): Promise { const query = new Parse.Query('Project'); query.equalTo('company', this.cid); query.notEqualTo('isDeleted', true); query.include('assignee', 'contact'); query.descending('updatedAt'); query.limit(1000); const projects = await query.find(); return projects.map(p => this.transformProject(p)); } /** * 转换Project为前端格式 */ private transformProject(project: any): any { const data = project.get('data') || {}; const deadline = project.get('deadline') || new Date(); const now = new Date(); const daysLeft = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); const assignee = project.get('assignee'); const contact = project.get('contact'); return { id: project.id, name: project.get('title') || '未命名项目', type: data.projectType || 'soft', memberType: data.memberType || 'normal', designerName: assignee?.get('name') || '未分配', designerId: assignee?.id || '', customerName: contact?.get('name') || '未知客户', status: project.get('status') || '待分配', currentStage: project.get('currentStage') || 'pendingAssignment', deadline, createdAt: project.get('createdAt'), isOverdue: daysLeft < 0, overdueDays: daysLeft < 0 ? Math.abs(daysLeft) : 0, dueSoon: daysLeft >= 0 && daysLeft <= 3, urgency: data.urgency || 'low', weight: this.calculateProjectWeight(project), data, project }; } /** * 默认标签(新员工) */ private getDefaultTags(): DesignerTags { return { expertise: { styles: [], skills: [], spaceTypes: [] }, capacity: { weeklyProjects: 2, maxConcurrent: 3, avgDaysPerProject: 10 }, emergency: { willing: false, premium: 0, maxPerWeek: 0 }, history: { totalProjects: 0, completionRate: 0, avgRating: 0, onTimeRate: 0, excellentCount: 0 }, portfolio: [] }; } /** * 智能匹配算法 */ async getRecommendedDesigners( project: any, allDesigners?: any[] ): Promise> { if (!allDesigners) { allDesigners = await this.getDesigners(); } const projectStyle = project.data?.style || project.data?.requirement?.style || ''; const isEmergency = project.urgency === 'high'; const recommendations = []; for (const designer of allDesigners) { // 1. 风格匹配分(30分) let styleScore = 0; const styles = designer.tags.expertise.styles || []; if (projectStyle && styles.includes(projectStyle)) { styleScore = 30; } else if (styles.length > 0) { styleScore = 10; // 有标签但不完全匹配 } // 2. 负载适配分(30分) const workload = await this.getDesignerWorkload(designer.id); let loadScore = 0; if (workload.loadRate < 60) loadScore = 30; else if (workload.loadRate < 80) loadScore = 20; else if (workload.loadRate < 100) loadScore = 10; else loadScore = 0; // 3. 历史表现分(25分) const history = designer.tags.history; const performanceScore = ( (history.completionRate || 0) * 0.4 + (history.onTimeRate || 0) * 0.3 + ((history.avgRating || 0) / 5 * 100) * 0.3 ) * 0.25; // 4. 紧急适配分(15分,仅紧急项目) let emergencyScore = 0; if (isEmergency) { if (designer.tags.emergency.willing) { emergencyScore = 15; } } const matchScore = styleScore + loadScore + performanceScore + emergencyScore; // 生成推荐理由 let reason = []; if (styleScore >= 20) reason.push('风格匹配度高'); if (loadScore >= 25) reason.push('负载空闲'); else if (loadScore >= 15) reason.push('负载适中'); if (performanceScore >= 20) reason.push('历史表现优秀'); if (emergencyScore > 0) reason.push('愿意接急单'); if (matchScore >= 40) { recommendations.push({ designer, matchScore: Math.round(matchScore), reason: reason.join('、') || '基础匹配', loadRate: workload.loadRate, currentProjects: workload.projects.length }); } } return recommendations .sort((a, b) => b.matchScore - a.matchScore) .slice(0, 5); } } ``` ### 3.2 UI层优化:简化视图 #### 优化前 vs 优化后 | 模块 | 优化前 | 优化后 | 改进 | |-----|-------|-------|-----| | **KPI卡片** | 4个 | 6个(新增超负荷设计师、平均负载率) | +2个关键指标 | | **工作量概览** | ECharts切换按设计师/会员 | 卡片式,仅按设计师,直接显示负载率 | 移除不必要切换 | | **甘特图** | 两种模式切换 | 仅保留设计师排班模式 | 移除按项目模式 | | **预警面板** | 无 | 新增(超期+超负荷+即将到期) | 集中展示异常 | #### 新增组件:智能推荐弹窗 ```html
``` ### 3.3 算法层:加权工作量计算 ```typescript // dashboard.ts 中添加 /** * 计算项目加权值(复用DesignerService逻辑) */ calculateWorkloadWeight(project: any): number { // 1. 项目类型系数 const typeWeight = project.type === 'hard' ? 2.0 : 1.0; // 2. 剩余工期系数 const now = new Date(); const daysLeft = Math.ceil((project.deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); let timeWeight = 0.8; if (daysLeft < 0) timeWeight = 1.5; // 超期 else if (daysLeft <= 3) timeWeight = 1.3; // 临期 else if (daysLeft <= 7) timeWeight = 1.0; // 3. 紧急度系数 const urgencyWeight = project.urgency === 'high' ? 1.2 : project.urgency === 'medium' ? 1.0 : 0.8; return typeWeight * timeWeight * urgencyWeight; } /** * 获取设计师加权工作量 */ 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; // 假设单周处理量为3(后续从Profile.data.tags读取) const weeklyCapacity = 3; const loadRate = (weightedTotal / weeklyCapacity) * 100; 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))); 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); // 按负载率降序 } ``` --- ## 四、实施步骤 ### Phase 1: 创建DesignerService(2小时) 1. ✅ 创建 `src/app/pages/team-leader/services/designer.service.ts` 2. ✅ 实现Profile查询、Project查询、加权计算、智能匹配 ### Phase 2: 修改dashboard.ts数据源(3小时) 1. 🔨 注入DesignerService 2. 🔨 loadProjects() 改为调用 `designerService.getProjects()` 3. 🔨 移除硬编码的 `designerProfiles` 4. 🔨 实现 `calculateWorkloadWeight()`、`designerWorkloadCards` ### Phase 3: 简化UI(2小时) 1. 🔨 移除工作量概览的按会员类型切换 2. 🔨 移除甘特图的按项目模式 3. 🔨 新增预警面板HTML 4. 🔨 新增智能推荐弹窗HTML ### Phase 4: 集成智能推荐(2小时) 1. 🔨 添加"智能推荐"按钮到待分配项目卡片 2. 🔨 实现推荐弹窗逻辑 3. 🔨 实现一键分配功能 ### Phase 5: 样式优化(1小时) 1. 🔨 优化卡片式工作量展示 2. 🔨 添加预警面板样式 3. 🔨 添加智能推荐弹窗样式 ### Phase 6: 测试与修复(2小时) 1. 🔨 测试fmode-ng数据查询 2. 🔨 测试智能匹配准确性 3. 🔨 测试页面滚动(确保之前的CSS修复生效) 4. 🔨 测试所有筛选功能 --- ## 五、数据库Schema要求 ### Profile.data.tags结构(与之前方案一致) ```typescript interface ProfileData { tags: { expertise: { styles: string[]; // ['现代简约', '北欧', ...] skills: string[]; // ['建模', '渲染', ...] spaceTypes: string[]; // ['卧室', '客厅', ...] }; capacity: { weeklyProjects: number; maxConcurrent: number; avgDaysPerProject: number; }; emergency: { willing: boolean; premium: number; maxPerWeek: number; }; history: { totalProjects: number; completionRate: number; avgRating: number; onTimeRate: number; excellentCount: number; }; portfolio: string[]; }; } ``` ### Project.data扩展字段 ```typescript interface ProjectData { projectType?: 'soft' | 'hard'; // 软装/硬装 memberType?: 'vip' | 'normal'; // 会员类型 urgency?: 'high' | 'medium' | 'low'; // 紧急度 style?: string; // 风格(用于智能匹配) requirement?: { style?: string; spaces?: string[]; // ... }; } ``` --- ## 六、效率对比 | 维度 | 优化前 | 优化后 | 提升 | |-----|-------|-------|-----| | **问题发现** | 5-10秒(扫描多视图) | 1-2秒(预警面板) | 80%↓ | | **项目分配** | 5-10分钟 | 1-2分钟(智能推荐) | 70%↓ | | **负载评估** | 不准确(仅项目数) | 准确(加权计算) | 准确度↑ | | **数据真实性** | 0%(模拟数据) | 100%(fmode-ng) | 100%↑ | --- ## 七、兼容性保障 - ✅ 保留所有现有筛选功能 - ✅ 保留项目看板4列结构 - ✅ 保留搜索建议功能 - ✅ 仅移除不必要的切换,不破坏核心功能 - ✅ 新增功能(智能推荐)为独立模块,不影响手动分配 --- ## 八、待确认事项 ### 1. Profile.data.tags数据初始化 对于现有员工,需要批量初始化tags字段: ```typescript // 管理员后台运行 async initializeAllDesignerTags() { const query = new Parse.Query('Profile'); query.equalTo('company', this.cid); query.equalTo('roleName', '组员'); query.notEqualTo('isDeleted', true); const profiles = await query.find(); for (const profile of profiles) { const data = profile.get('data') || {}; if (!data.tags) { data.tags = { expertise: { styles: [], skills: [], spaceTypes: [] }, capacity: { weeklyProjects: 2, maxConcurrent: 3, avgDaysPerProject: 10 }, emergency: { willing: false, premium: 0, maxPerWeek: 0 }, history: { totalProjects: 0, completionRate: 0, avgRating: 0, onTimeRate: 0, excellentCount: 0 }, portfolio: [] }; profile.set('data', data); await profile.save(); } } } ``` ### 2. Project.data字段补充 确认现有Project记录的data字段是否包含: - `projectType`(软装/硬装) - `urgency`(紧急度) - `memberType`(VIP/普通) 如缺失,需补充或使用默认值。 --- **文档版本**: v1.0 **最后更新**: 2025-10-22 **维护者**: YSS Project Team