实施日期: 2025-10-22
文档状态: 最新版(基于原版dashboard重新规划)
| 模块 | 功能 | 数据来源 | 问题 |
|---|---|---|---|
| KPI卡片 | 4个指标(延期、临期、待确认、待分配) | 模拟数据 | ❌ 未对接真实数据 |
| 工作量概览 | ECharts横向堆叠柱状图 | 模拟数据 | ❌ 仅按项目数量统计,未考虑复杂度 |
| 甘特图 | 两种模式(项目/设计师) | 模拟数据 | ❌ 切换过多,信息密度过高 |
| 项目看板 | 4列看板(订单→需求→交付→售后) | 模拟数据 | ✅ 结构合理,需对接真实数据 |
| 筛选器 | 7个维度筛选 | - | ⚠️ 功能完整但UI可简化 |
| 搜索 | 关键词搜索+建议 | 模拟数据 | ✅ 交互良好,需对接真实数据 |
| 设计师画像 | 硬编码8个设计师 | 模拟数据 | ❌ 需改为Profile.data.tags动态读取 |
| 目标 | 当前耗时 | 优化后 | 提升 |
|---|---|---|---|
| 发现问题项目 | 5-10秒(需扫描多个视图) | 1-2秒(统一预警面板) | 80%↓ |
| 分配项目 | 5-10分钟(手动查看负载+擅长) | 1-2分钟(智能推荐) | 70%↓ |
| 负载评估 | 仅凭项目数量 | 加权工作量科学评估 | 准确度↑ |
// 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);
}
}
| 模块 | 优化前 | 优化后 | 改进 |
|---|---|---|---|
| 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>
// 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); // 按负载率降序
}
src/app/pages/team-leader/services/designer.service.tsdesignerService.getProjects()designerProfilescalculateWorkloadWeight()、designerWorkloadCardsinterface 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[];
};
}
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%↑ |
对于现有员工,需要批量初始化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();
}
}
}
确认现有Project记录的data字段是否包含:
projectType(软装/硬装)urgency(紧急度)memberType(VIP/普通)如缺失,需补充或使用默认值。
文档版本: v1.0
最后更新: 2025-10-22
维护者: YSS Project Team