# 组长端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<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切换按设计师/会员 | 卡片式，仅按设计师，直接显示负载率 | 移除不必要切换 |
| **甘特图** | 两种模式切换 | 仅保留设计师排班模式 | 移除按项目模式 |
| **预警面板** | 无 | 新增（超期+超负荷+即将到期） | 集中展示异常 |

#### 新增组件：智能推荐弹窗

```html
<!-- 智能推荐弹窗 -->
<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 算法层：加权工作量计算

```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


