|
@@ -153,16 +153,46 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
workloadByDesigner[name] = [];
|
|
workloadByDesigner[name] = [];
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // Calculate total load for sorting
|
|
|
|
|
- const designerTotalLoad: Record<string, number> = {};
|
|
|
|
|
|
|
+ // Calculate active load for today (for sorting and display)
|
|
|
|
|
+ const designerTodayLoad: Record<string, number> = {};
|
|
|
|
|
+
|
|
|
designers.forEach(name => {
|
|
designers.forEach(name => {
|
|
|
const projects = this.designerWorkloadMap.get(name) || [];
|
|
const projects = this.designerWorkloadMap.get(name) || [];
|
|
|
- designerTotalLoad[name] = projects.length;
|
|
|
|
|
|
|
+ const dayStart = todayTs;
|
|
|
|
|
+ const dayEnd = todayTs + DAY - 1;
|
|
|
|
|
+
|
|
|
|
|
+ const activeProjects = projects.filter(p => {
|
|
|
|
|
+ const isCompleted = p.status === '已完成' || p.status === '已交付';
|
|
|
|
|
+
|
|
|
|
|
+ if (isCompleted) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!p.deadline) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pEnd = new Date(p.deadline).getTime();
|
|
|
|
|
+
|
|
|
|
|
+ if (isNaN(pEnd)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (dayStart > pEnd) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pStart = p.createdAt ? new Date(p.createdAt).getTime() : todayTs;
|
|
|
|
|
+
|
|
|
|
|
+ return !(pEnd < dayStart || pStart > dayEnd);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ designerTodayLoad[name] = activeProjects.length;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // Sort designers by total load descending
|
|
|
|
|
|
|
+ // Sort designers by today's load descending
|
|
|
const sortedDesigners = designers.sort((a, b) => {
|
|
const sortedDesigners = designers.sort((a, b) => {
|
|
|
- return designerTotalLoad[b] - designerTotalLoad[a];
|
|
|
|
|
|
|
+ return designerTodayLoad[b] - designerTodayLoad[a];
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Generate time slot data for each designer
|
|
// Generate time slot data for each designer
|
|
@@ -209,25 +239,30 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
let status: 'idle' | 'busy' | 'overload' | 'leave' = 'idle';
|
|
let status: 'idle' | 'busy' | 'overload' | 'leave' = 'idle';
|
|
|
- let color = '#d1fae5'; // Idle - Light Green
|
|
|
|
|
|
|
+ let color = 'rgba(243, 244, 246, 0.4)'; // Idle - Very faint gray
|
|
|
|
|
+ let borderColor = 'transparent';
|
|
|
|
|
|
|
|
const projectCount = dayProjects.length;
|
|
const projectCount = dayProjects.length;
|
|
|
|
|
|
|
|
if (projectCount === 0) {
|
|
if (projectCount === 0) {
|
|
|
status = 'idle';
|
|
status = 'idle';
|
|
|
- color = '#d1fae5';
|
|
|
|
|
|
|
+ color = 'rgba(243, 244, 246, 0.4)';
|
|
|
} else if (projectCount >= 3) {
|
|
} else if (projectCount >= 3) {
|
|
|
status = 'overload';
|
|
status = 'overload';
|
|
|
- color = '#fecaca'; // Overload - Light Red
|
|
|
|
|
|
|
+ color = '#ef4444'; // Overload - Red
|
|
|
} else {
|
|
} else {
|
|
|
status = 'busy';
|
|
status = 'busy';
|
|
|
- color = '#bfdbfe'; // Busy - Light Blue
|
|
|
|
|
|
|
+ color = '#3b82f6'; // Busy - Blue
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
workloadByDesigner[designerName].push({
|
|
workloadByDesigner[designerName].push({
|
|
|
name: `${designerName}-${i}`,
|
|
name: `${designerName}-${i}`,
|
|
|
- value: [yIndex, dayStart, dayEnd, designerName, status, projectCount, dayProjects.map(p => p.name)],
|
|
|
|
|
- itemStyle: { color }
|
|
|
|
|
|
|
+ // value: [index, start, end, name, status, count, projects]
|
|
|
|
|
+ value: [yIndex, dayStart, dayEnd, designerName, status, projectCount, dayProjects],
|
|
|
|
|
+ itemStyle: {
|
|
|
|
|
+ color: color,
|
|
|
|
|
+ borderRadius: 4
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -237,99 +272,112 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
|
|
|
|
|
const option = {
|
|
const option = {
|
|
|
backgroundColor: '#fff',
|
|
backgroundColor: '#fff',
|
|
|
- title: {
|
|
|
|
|
- text: this.workloadGanttScale === 'week' ? '未来7天工作状态' : '未来30天工作状态',
|
|
|
|
|
- subtext: '🟢空闲 🔵忙碌 🔴超负荷',
|
|
|
|
|
- left: 'center',
|
|
|
|
|
- textStyle: { fontSize: 14, color: '#374151', fontWeight: 600 },
|
|
|
|
|
- subtextStyle: { fontSize: 12, color: '#6b7280' }
|
|
|
|
|
- },
|
|
|
|
|
tooltip: {
|
|
tooltip: {
|
|
|
|
|
+ trigger: 'item',
|
|
|
|
|
+ padding: 0,
|
|
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
|
|
+ borderColor: '#e5e7eb',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ textStyle: { color: '#374151' },
|
|
|
|
|
+ extraCssText: 'box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); border-radius: 8px;',
|
|
|
formatter: (params: any) => {
|
|
formatter: (params: any) => {
|
|
|
- const [yIndex, start, end, name, status, projectCount, projectNames = []] = params.value;
|
|
|
|
|
|
|
+ const [yIndex, start, end, name, status, projectCount, projects = []] = params.value;
|
|
|
const startDate = new Date(start);
|
|
const startDate = new Date(start);
|
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
|
|
|
|
|
|
- let statusText = '';
|
|
|
|
|
- let statusColor = '';
|
|
|
|
|
- let statusBadge = '';
|
|
|
|
|
|
|
+ // Status Header
|
|
|
|
|
+ let headerColor = '#f3f4f6';
|
|
|
|
|
+ let headerTextColor = '#374151';
|
|
|
|
|
+ let statusLabel = '空闲';
|
|
|
|
|
|
|
|
- if (status === 'leave') {
|
|
|
|
|
- statusText = '请假';
|
|
|
|
|
- statusColor = '#6b7280';
|
|
|
|
|
- statusBadge = '<span style="background:#e5e7eb;color:#374151;padding:2px 8px;border-radius:4px;font-size:11px;">请假</span>';
|
|
|
|
|
- } else if (projectCount === 0) {
|
|
|
|
|
- statusText = '空闲';
|
|
|
|
|
- statusColor = '#10b981';
|
|
|
|
|
- statusBadge = '<span style="background:#d1fae5;color:#059669;padding:2px 8px;border-radius:4px;font-size:11px;">🟢 空闲</span>';
|
|
|
|
|
- } else if (projectCount >= 3) {
|
|
|
|
|
- statusText = '超负荷';
|
|
|
|
|
- statusColor = '#dc2626';
|
|
|
|
|
- statusBadge = '<span style="background:#fecaca;color:#dc2626;padding:2px 8px;border-radius:4px;font-size:11px;">🔴 超负荷</span>';
|
|
|
|
|
- } else {
|
|
|
|
|
- statusText = '忙碌';
|
|
|
|
|
- statusColor = '#3b82f6';
|
|
|
|
|
- statusBadge = '<span style="background:#bfdbfe;color:#1d4ed8;padding:2px 8px;border-radius:4px;font-size:11px;">🔵 忙碌</span>';
|
|
|
|
|
|
|
+ if (status === 'overload') {
|
|
|
|
|
+ headerColor = '#fee2e2';
|
|
|
|
|
+ headerTextColor = '#991b1b';
|
|
|
|
|
+ statusLabel = '超负荷';
|
|
|
|
|
+ } else if (status === 'busy') {
|
|
|
|
|
+ headerColor = '#dbeafe';
|
|
|
|
|
+ headerTextColor = '#1e40af';
|
|
|
|
|
+ statusLabel = '忙碌';
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Project List
|
|
|
let projectListHtml = '';
|
|
let projectListHtml = '';
|
|
|
- if (projectNames && projectNames.length > 0) {
|
|
|
|
|
|
|
+ if (projects && projects.length > 0) {
|
|
|
|
|
+ const listItems = projects.slice(0, 6).map((p: any, idx: number) => {
|
|
|
|
|
+ const isUrgent = p.status === 'urgent' || p.status === 'overdue';
|
|
|
|
|
+ const stage = p.currentStage || '进行中';
|
|
|
|
|
+ return `
|
|
|
|
|
+ <div style="display: flex; align-items: center; justify-content: space-between; padding: 4px 0; font-size: 12px; border-bottom: 1px dashed #f3f4f6;">
|
|
|
|
|
+ <div style="display: flex; align-items: center; gap: 6px; max-width: 70%;">
|
|
|
|
|
+ <span style="color: #9ca3af; font-size: 10px; width: 15px;">${idx + 1}.</span>
|
|
|
|
|
+ <span style="color: #374151; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">${p.name}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="display: flex; align-items: center; gap: 4px;">
|
|
|
|
|
+ <span style="background: #f3f4f6; color: #6b7280; padding: 1px 4px; border-radius: 3px; font-size: 10px;">${stage}</span>
|
|
|
|
|
+ ${isUrgent ? '<span style="background: #fee2e2; color: #ef4444; width: 6px; height: 6px; border-radius: 50%; display: inline-block;"></span>' : ''}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+
|
|
|
projectListHtml = `
|
|
projectListHtml = `
|
|
|
- <div style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed #e5e7eb;">
|
|
|
|
|
- <div style="font-size: 12px; color: #6b7280; margin-bottom: 4px;">项目列表:</div>
|
|
|
|
|
- ${projectNames.slice(0, 5).map((pName: string, idx: number) =>
|
|
|
|
|
- `<div style="font-size: 12px; color: #374151; margin-left: 8px;">
|
|
|
|
|
- ${idx + 1}. ${pName.length > 20 ? pName.substring(0, 20) + '...' : pName}
|
|
|
|
|
- </div>`
|
|
|
|
|
- ).join('')}
|
|
|
|
|
- ${projectNames.length > 5 ? `<div style="font-size: 12px; color: #6b7280; margin-left: 8px;">...及${projectNames.length - 5}个其他项目</div>` : ''}
|
|
|
|
|
|
|
+ <div style="padding: 8px 12px;">
|
|
|
|
|
+ <div style="font-size: 11px; color: #9ca3af; margin-bottom: 4px; display: flex; justify-content: space-between;">
|
|
|
|
|
+ <span>项目列表</span>
|
|
|
|
|
+ <span>阶段</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ${listItems}
|
|
|
|
|
+ ${projects.length > 6 ? `<div style="text-align: center; font-size: 11px; color: #9ca3af; margin-top: 4px;">...共 ${projects.length} 个项目</div>` : ''}
|
|
|
</div>
|
|
</div>
|
|
|
`;
|
|
`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ projectListHtml = `<div style="padding: 12px; text-align: center; color: #9ca3af; font-size: 12px;">无项目安排</div>`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return `<div style="padding: 12px; min-width: 220px;">
|
|
|
|
|
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
|
|
|
- <strong style="font-size: 15px; color: #1f2937;">${name}</strong>
|
|
|
|
|
- ${statusBadge}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div style="color: #6b7280; font-size: 13px;">
|
|
|
|
|
- 📅 ${startDate.getMonth() + 1}月${startDate.getDate()}日 ${weekDays[startDate.getDay()]}<br/>
|
|
|
|
|
- 📊 项目数量: <span style="font-weight: 600; color: ${statusColor};">${projectCount}个</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- ${projectListHtml}
|
|
|
|
|
- <div style="margin-top: 10px; padding-top: 8px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 11px; text-align: center;">
|
|
|
|
|
- 💡 点击查看设计师详细信息
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>`;
|
|
|
|
|
|
|
+ return `
|
|
|
|
|
+ <div style="width: 260px; overflow: hidden; border-radius: 8px;">
|
|
|
|
|
+ <div style="background: ${headerColor}; padding: 8px 12px; border-bottom: 1px solid rgba(0,0,0,0.05);">
|
|
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
+ <span style="font-weight: 600; color: ${headerTextColor}; font-size: 14px;">${name}</span>
|
|
|
|
|
+ <span style="font-size: 12px; color: ${headerTextColor}; opacity: 0.8;">${statusLabel} (${projectCount})</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div style="font-size: 11px; color: ${headerTextColor}; opacity: 0.7; margin-top: 2px;">
|
|
|
|
|
+ ${startDate.getMonth() + 1}月${startDate.getDate()}日 ${weekDays[startDate.getDay()]}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ${projectListHtml}
|
|
|
|
|
+ <div style="padding: 6px 12px; background: #f9fafb; border-top: 1px solid #f3f4f6; text-align: center;">
|
|
|
|
|
+ <span style="font-size: 10px; color: #9ca3af;">点击查看详情</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `;
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
grid: {
|
|
grid: {
|
|
|
left: 100,
|
|
left: 100,
|
|
|
- right: 50,
|
|
|
|
|
- top: 60,
|
|
|
|
|
- bottom: 60
|
|
|
|
|
|
|
+ right: 30,
|
|
|
|
|
+ top: 70,
|
|
|
|
|
+ bottom: 30,
|
|
|
|
|
+ containLabel: true
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'time',
|
|
type: 'time',
|
|
|
min: xMin,
|
|
min: xMin,
|
|
|
max: xMax,
|
|
max: xMax,
|
|
|
|
|
+ position: 'top',
|
|
|
boundaryGap: false,
|
|
boundaryGap: false,
|
|
|
- axisLine: { lineStyle: { color: '#e5e7eb' } },
|
|
|
|
|
|
|
+ axisLine: { show: false },
|
|
|
|
|
+ axisTick: { show: false },
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
color: '#6b7280',
|
|
color: '#6b7280',
|
|
|
formatter: xLabelFormatter,
|
|
formatter: xLabelFormatter,
|
|
|
interval: 0,
|
|
interval: 0,
|
|
|
- rotate: this.workloadGanttScale === 'week' ? 0 : 45,
|
|
|
|
|
- showMinLabel: true,
|
|
|
|
|
- showMaxLabel: true
|
|
|
|
|
- },
|
|
|
|
|
- axisTick: {
|
|
|
|
|
- alignWithLabel: true,
|
|
|
|
|
- interval: 0
|
|
|
|
|
|
|
+ margin: 12,
|
|
|
|
|
+ fontSize: 11
|
|
|
},
|
|
},
|
|
|
splitLine: {
|
|
splitLine: {
|
|
|
show: true,
|
|
show: true,
|
|
|
- lineStyle: { color: '#f1f5f9' }
|
|
|
|
|
|
|
+ lineStyle: { color: '#f3f4f6', type: 'dashed' }
|
|
|
},
|
|
},
|
|
|
splitNumber: xSplitNumber,
|
|
splitNumber: xSplitNumber,
|
|
|
minInterval: DAY
|
|
minInterval: DAY
|
|
@@ -345,13 +393,13 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
fontSize: 13,
|
|
fontSize: 13,
|
|
|
fontWeight: 500,
|
|
fontWeight: 500,
|
|
|
formatter: (value: string) => {
|
|
formatter: (value: string) => {
|
|
|
- const totalProjects = designerTotalLoad[value] || 0;
|
|
|
|
|
|
|
+ const totalProjects = designerTodayLoad[value] || 0;
|
|
|
const icon = totalProjects >= 3 ? '🔥' : totalProjects >= 2 ? '⚡' : totalProjects >= 1 ? '✓' : '○';
|
|
const icon = totalProjects >= 3 ? '🔥' : totalProjects >= 2 ? '⚡' : totalProjects >= 1 ? '✓' : '○';
|
|
|
return `${icon} ${value} (${totalProjects})`;
|
|
return `${icon} ${value} (${totalProjects})`;
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
axisTick: { show: false },
|
|
axisTick: { show: false },
|
|
|
- axisLine: { lineStyle: { color: '#e5e7eb' } }
|
|
|
|
|
|
|
+ axisLine: { show: false }
|
|
|
},
|
|
},
|
|
|
series: [
|
|
series: [
|
|
|
{
|
|
{
|
|
@@ -359,13 +407,17 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
name: '工作负载',
|
|
name: '工作负载',
|
|
|
renderItem: (params: any, api: any) => {
|
|
renderItem: (params: any, api: any) => {
|
|
|
const categoryIndex = api.value(0);
|
|
const categoryIndex = api.value(0);
|
|
|
|
|
+ // Calculate coordinates
|
|
|
const start = api.coord([api.value(1), categoryIndex]);
|
|
const start = api.coord([api.value(1), categoryIndex]);
|
|
|
const end = api.coord([api.value(2), categoryIndex]);
|
|
const end = api.coord([api.value(2), categoryIndex]);
|
|
|
- const height = api.size([0, 1])[1] * 0.6;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Adjust height and position for a "pill" look
|
|
|
|
|
+ const height = api.size([0, 1])[1] * 0.7; // 70% of row height
|
|
|
|
|
+
|
|
|
const rectShape = echarts.graphic.clipRectByRect({
|
|
const rectShape = echarts.graphic.clipRectByRect({
|
|
|
x: start[0],
|
|
x: start[0],
|
|
|
y: start[1] - height / 2,
|
|
y: start[1] - height / 2,
|
|
|
- width: Math.max(end[0] - start[0], 2),
|
|
|
|
|
|
|
+ width: Math.max(end[0] - start[0], 2), // Minimum width 2px
|
|
|
height
|
|
height
|
|
|
}, {
|
|
}, {
|
|
|
x: params.coordSys.x,
|
|
x: params.coordSys.x,
|
|
@@ -373,9 +425,13 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
width: params.coordSys.width,
|
|
width: params.coordSys.width,
|
|
|
height: params.coordSys.height
|
|
height: params.coordSys.height
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
return rectShape ? {
|
|
return rectShape ? {
|
|
|
type: 'rect',
|
|
type: 'rect',
|
|
|
- shape: rectShape,
|
|
|
|
|
|
|
+ shape: {
|
|
|
|
|
+ ...rectShape,
|
|
|
|
|
+ r: [4, 4, 4, 4] // Rounded corners
|
|
|
|
|
+ },
|
|
|
style: api.style()
|
|
style: api.style()
|
|
|
} : undefined;
|
|
} : undefined;
|
|
|
},
|
|
},
|