2025102210-comprehensive-dashboard-optimization.md 20 KB

组长端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

// 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<any[]> {
    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<any[]> {
    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<Array<{
    designer: any;
    matchScore: number;
    reason: string;
  }>> {
    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切换按设计师/会员 卡片式,仅按设计师,直接显示负载率 移除不必要切换
甘特图 两种模式切换 仅保留设计师排班模式 移除按项目模式
预警面板 新增(超期+超负荷+即将到期) 集中展示异常

新增组件:智能推荐弹窗

<!-- 智能推荐弹窗 -->
<div class="smart-match-modal" *ngIf="showSmartMatch">
  <div class="modal-backdrop" (click)="closeSmartMatch()"></div>
  <div class="modal-content">
    <div class="modal-header">
      <h3>智能推荐设计师</h3>
      <button class="btn-close" (click)="closeSmartMatch()">×</button>
    </div>
    
    <div class="project-info">
      <h4>{{ selectedProject?.name }}</h4>
      <div class="tags">
        <span class="tag">{{ selectedProject?.type === 'hard' ? '硬装' : '软装' }}</span>
        <span class="tag">{{ selectedProject?.memberType === 'vip' ? 'VIP' : '普通' }}</span>
        <span class="tag urgency" [class]="'u-' + selectedProject?.urgency">
          {{ getUrgencyLabel(selectedProject?.urgency) }}
        </span>
      </div>
    </div>
    
    <div class="recommendations-list">
      <div class="rec-card" *ngFor="let rec of recommendations; let i = index">
        <div class="rank" [class.gold]="i===0" [class.silver]="i===1" [class.bronze]="i===2">
          {{ i + 1 }}
        </div>
        <div class="designer-info">
          <h4>{{ rec.designer.name }}</h4>
          <div class="match-score" [style.width.%]="rec.matchScore">
            <span>{{ rec.matchScore }}分</span>
          </div>
        </div>
        <div class="details">
          <p><strong>擅长:</strong>{{ rec.designer.tags.expertise.styles.join('、') || '暂无标签' }}</p>
          <p><strong>负载:</strong>{{ rec.loadRate.toFixed(0) }}% ({{ rec.currentProjects }}个项目)</p>
          <p><strong>评分:</strong>⭐ {{ rec.designer.tags.history.avgRating || '暂无' }}</p>
          <p class="reason"><strong>推荐理由:</strong>{{ rec.reason }}</p>
        </div>
        <button class="btn-assign" (click)="assignToDesigner(rec.designer.id)">
          分配给TA
        </button>
      </div>
      
      <div class="empty" *ngIf="!recommendations.length">
        <p>未找到合适的设计师</p>
        <p>您可以手动分配或调整项目参数</p>
      </div>
    </div>
  </div>
</div>

3.3 算法层:加权工作量计算

// 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结构(与之前方案一致)

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扩展字段

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字段:

// 管理员后台运行
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