|
|
@@ -94,38 +94,33 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
const todayTs = today.getTime();
|
|
|
|
|
|
- // Time range
|
|
|
- let xMin: number;
|
|
|
- let xMax: number;
|
|
|
- let xSplitNumber: number;
|
|
|
- let xLabelFormatter: (value: number) => string;
|
|
|
-
|
|
|
+ // Time range & Category Data
|
|
|
+ let xCategories: string[] = [];
|
|
|
+
|
|
|
if (this.workloadGanttScale === 'week') {
|
|
|
// Week view: next 7 days
|
|
|
- xMin = todayTs;
|
|
|
- xMax = todayTs + 7 * DAY;
|
|
|
- xSplitNumber = 7;
|
|
|
- xLabelFormatter = (val: any) => {
|
|
|
- const date = new Date(val);
|
|
|
+ const days = 7;
|
|
|
+ for (let i = 0; i < days; i++) {
|
|
|
+ const d = new Date(todayTs + i * DAY);
|
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
|
- return `${date.getMonth() + 1}/${date.getDate()}\n${weekDays[date.getDay()]}`;
|
|
|
- };
|
|
|
+ // 格式化为 "11/27\n周三" 或简单日期
|
|
|
+ xCategories.push(`${d.getMonth() + 1}/${d.getDate()}\n${weekDays[d.getDay()]}`);
|
|
|
+ }
|
|
|
} else {
|
|
|
// Month view: next 30 days
|
|
|
- xMin = todayTs;
|
|
|
- xMax = todayTs + 30 * DAY;
|
|
|
- xSplitNumber = 30;
|
|
|
- xLabelFormatter = (val: any) => {
|
|
|
- const date = new Date(val);
|
|
|
- return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
|
- };
|
|
|
+ const days = 30;
|
|
|
+ for (let i = 0; i < days; i++) {
|
|
|
+ const d = new Date(todayTs + i * DAY);
|
|
|
+ xCategories.push(`${d.getMonth() + 1}/${d.getDate()}`);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Get all real designers
|
|
|
let designers: string[] = [];
|
|
|
|
|
|
if (this.realDesigners && this.realDesigners.length > 0) {
|
|
|
- designers = this.realDesigners.map(d => d.name);
|
|
|
+ // 使用 Set 去重,防止同名设计师导致行重复
|
|
|
+ designers = Array.from(new Set(this.realDesigners.map(d => d.name)));
|
|
|
} else {
|
|
|
// Fallback: extract from filtered projects
|
|
|
const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
|
|
|
@@ -240,7 +235,6 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
|
|
|
let status: 'idle' | 'busy' | 'overload' | 'leave' = 'idle';
|
|
|
let color = 'rgba(243, 244, 246, 0.4)'; // Idle - Very faint gray
|
|
|
- let borderColor = 'transparent';
|
|
|
|
|
|
const projectCount = dayProjects.length;
|
|
|
|
|
|
@@ -257,8 +251,7 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
|
|
|
workloadByDesigner[designerName].push({
|
|
|
name: `${designerName}-${i}`,
|
|
|
- // value: [index, start, end, name, status, count, projects]
|
|
|
- value: [yIndex, dayStart, dayEnd, designerName, status, projectCount, dayProjects],
|
|
|
+ value: [yIndex, i, i + 1, designerName, status, projectCount, dayProjects, dayStart],
|
|
|
itemStyle: {
|
|
|
color: color,
|
|
|
borderRadius: 4
|
|
|
@@ -275,112 +268,80 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
tooltip: {
|
|
|
trigger: 'item',
|
|
|
padding: 0,
|
|
|
- backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
- borderColor: '#e5e7eb',
|
|
|
+ backgroundColor: '#fff',
|
|
|
+ borderColor: '#e2e8f0',
|
|
|
borderWidth: 1,
|
|
|
- textStyle: { color: '#374151' },
|
|
|
+ textStyle: { color: '#334155' },
|
|
|
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) => {
|
|
|
const [yIndex, start, end, name, status, projectCount, projects = []] = params.value;
|
|
|
- const startDate = new Date(start);
|
|
|
- const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
|
+ const date = new Date(start);
|
|
|
+ const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
|
|
|
|
|
|
- // Status Header
|
|
|
- let headerColor = '#f3f4f6';
|
|
|
- let headerTextColor = '#374151';
|
|
|
- let statusLabel = '空闲';
|
|
|
+ // 精简的 Tooltip
|
|
|
+ let listHtml = '';
|
|
|
|
|
|
- if (status === 'overload') {
|
|
|
- headerColor = '#fee2e2';
|
|
|
- headerTextColor = '#991b1b';
|
|
|
- statusLabel = '超负荷';
|
|
|
- } else if (status === 'busy') {
|
|
|
- headerColor = '#dbeafe';
|
|
|
- headerTextColor = '#1e40af';
|
|
|
- statusLabel = '忙碌';
|
|
|
- }
|
|
|
-
|
|
|
- // Project List
|
|
|
- let projectListHtml = '';
|
|
|
if (projects && projects.length > 0) {
|
|
|
- const listItems = projects.slice(0, 6).map((p: any, idx: number) => {
|
|
|
- const isUrgent = p.status === 'urgent' || p.status === 'overdue';
|
|
|
+ listHtml = projects.slice(0, 6).map((p: any) => {
|
|
|
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>
|
|
|
- `;
|
|
|
+ <div style="display:flex;justify-content:space-between;font-size:12px;margin-top:4px;color:#64748b;">
|
|
|
+ <span style="max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${p.name}</span>
|
|
|
+ <span style="font-size:10px;background:#f1f5f9;padding:1px 4px;border-radius:2px;flex-shrink:0;">${stage}</span>
|
|
|
+ </div>`;
|
|
|
}).join('');
|
|
|
|
|
|
- projectListHtml = `
|
|
|
- <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>
|
|
|
- `;
|
|
|
- } else {
|
|
|
- projectListHtml = `<div style="padding: 12px; text-align: center; color: #9ca3af; font-size: 12px;">无项目安排</div>`;
|
|
|
+ if (projects.length > 6) {
|
|
|
+ listHtml += `<div style="font-size:11px;color:#94a3b8;text-align:center;margin-top:4px;">...共 ${projects.length} 个项目</div>`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let statusColor = '#94a3b8'; // idle
|
|
|
+ let statusText = '空闲';
|
|
|
+
|
|
|
+ if (status === 'overload') {
|
|
|
+ statusColor = '#ef4444';
|
|
|
+ statusText = '超负荷';
|
|
|
+ } else if (status === 'busy') {
|
|
|
+ statusColor = '#3b82f6';
|
|
|
+ statusText = '忙碌';
|
|
|
}
|
|
|
|
|
|
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 style="width:200px; padding:10px;">
|
|
|
+ <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #f1f5f9; padding-bottom:6px; margin-bottom:6px;">
|
|
|
+ <span style="font-weight:600;">${name}</span>
|
|
|
+ <span style="font-size:12px; color:${statusColor}; font-weight:500;">${statusText} (${projectCount})</span>
|
|
|
</div>
|
|
|
+ ${projectCount > 0 ? listHtml : '<div style="font-size:12px;color:#cbd5e1;text-align:center;">无安排</div>'}
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
},
|
|
|
grid: {
|
|
|
left: 100,
|
|
|
- right: 30,
|
|
|
+ right: 40, // 增加右侧留白,防止截断
|
|
|
top: 70,
|
|
|
bottom: 30,
|
|
|
containLabel: true
|
|
|
},
|
|
|
xAxis: {
|
|
|
- type: 'time',
|
|
|
- min: xMin,
|
|
|
- max: xMax,
|
|
|
+ type: 'category',
|
|
|
+ data: xCategories,
|
|
|
position: 'top',
|
|
|
- boundaryGap: false,
|
|
|
+ boundaryGap: true,
|
|
|
axisLine: { show: false },
|
|
|
axisTick: { show: false },
|
|
|
axisLabel: {
|
|
|
- color: '#6b7280',
|
|
|
- formatter: xLabelFormatter,
|
|
|
+ color: '#9ca3af',
|
|
|
interval: 0,
|
|
|
margin: 12,
|
|
|
- fontSize: 11
|
|
|
+ fontSize: 11,
|
|
|
+ align: 'center'
|
|
|
},
|
|
|
splitLine: {
|
|
|
- show: true,
|
|
|
- lineStyle: { color: '#f3f4f6', type: 'dashed' }
|
|
|
- },
|
|
|
- splitNumber: xSplitNumber,
|
|
|
- minInterval: DAY
|
|
|
+ show: false
|
|
|
+ }
|
|
|
},
|
|
|
yAxis: {
|
|
|
type: 'category',
|
|
|
@@ -389,13 +350,11 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
triggerEvent: true,
|
|
|
axisLabel: {
|
|
|
color: '#374151',
|
|
|
- margin: 8,
|
|
|
+ margin: 12,
|
|
|
fontSize: 13,
|
|
|
fontWeight: 500,
|
|
|
formatter: (value: string) => {
|
|
|
- const totalProjects = designerTodayLoad[value] || 0;
|
|
|
- const icon = totalProjects >= 3 ? '🔥' : totalProjects >= 2 ? '⚡' : totalProjects >= 1 ? '✓' : '○';
|
|
|
- return `${icon} ${value} (${totalProjects})`;
|
|
|
+ return value;
|
|
|
}
|
|
|
},
|
|
|
axisTick: { show: false },
|
|
|
@@ -406,19 +365,43 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
type: 'custom',
|
|
|
name: '工作负载',
|
|
|
renderItem: (params: any, api: any) => {
|
|
|
- const categoryIndex = api.value(0);
|
|
|
- // Calculate coordinates
|
|
|
- const start = api.coord([api.value(1), categoryIndex]);
|
|
|
- const end = api.coord([api.value(2), categoryIndex]);
|
|
|
+ const categoryIndex = api.value(0); // Y轴索引
|
|
|
+ const xIndex = api.value(1); // X轴索引 (0, 1, 2...)
|
|
|
|
|
|
- // Adjust height and position for a "pill" look
|
|
|
- const height = api.size([0, 1])[1] * 0.7; // 70% of row height
|
|
|
+ // 1. 获取当前单元格中心点坐标
|
|
|
+ const point = api.coord([xIndex, categoryIndex]);
|
|
|
|
|
|
+ // 2. 计算列宽 (通过计算下一个点的距离,或者直接使用 size)
|
|
|
+ // api.size([1, 0]) 在 category 轴下可能不准,最稳妥是计算相邻点距离
|
|
|
+ // 模拟下一个点来计算步长
|
|
|
+ const nextPoint = api.coord([xIndex + 1, categoryIndex]);
|
|
|
+ const columnWidth = nextPoint[0] - point[0];
|
|
|
+
|
|
|
+ // 3. 计算行高
|
|
|
+ const rowHeight = api.size([0, 1])[1];
|
|
|
+
|
|
|
+ // 4. 确定矩形尺寸
|
|
|
+ const width = columnWidth * 0.85; // 宽度占 85%,留间隙
|
|
|
+ const height = rowHeight * 0.7; // 高度占 70%
|
|
|
+
|
|
|
+ // 5. 计算矩形左上角坐标 (基于中心点偏移)
|
|
|
+ const rectX = point[0] - width / 2;
|
|
|
+ const rectY = point[1] - height / 2;
|
|
|
+
|
|
|
+ // 获取项目数量和状态
|
|
|
+ const count = api.value(5);
|
|
|
+ const status = api.value(4); // idle, busy, overload
|
|
|
+
|
|
|
+ // 确定文字颜色
|
|
|
+ let textColor = '#9ca3af'; // 默认灰色 (idle)
|
|
|
+ if (status === 'busy') textColor = '#ffffff';
|
|
|
+ if (status === 'overload') textColor = '#ffffff';
|
|
|
+
|
|
|
const rectShape = echarts.graphic.clipRectByRect({
|
|
|
- x: start[0],
|
|
|
- y: start[1] - height / 2,
|
|
|
- width: Math.max(end[0] - start[0], 2), // Minimum width 2px
|
|
|
- height
|
|
|
+ x: rectX,
|
|
|
+ y: rectY,
|
|
|
+ width: width,
|
|
|
+ height: height
|
|
|
}, {
|
|
|
x: params.coordSys.x,
|
|
|
y: params.coordSys.y,
|
|
|
@@ -426,14 +409,34 @@ export class WorkloadGanttComponent implements OnDestroy, OnChanges, AfterViewIn
|
|
|
height: params.coordSys.height
|
|
|
});
|
|
|
|
|
|
- return rectShape ? {
|
|
|
- type: 'rect',
|
|
|
- shape: {
|
|
|
- ...rectShape,
|
|
|
- r: [4, 4, 4, 4] // Rounded corners
|
|
|
- },
|
|
|
- style: api.style()
|
|
|
- } : undefined;
|
|
|
+ if (!rectShape) return undefined;
|
|
|
+
|
|
|
+ return {
|
|
|
+ type: 'group',
|
|
|
+ children: [
|
|
|
+ {
|
|
|
+ type: 'rect',
|
|
|
+ shape: {
|
|
|
+ ...rectShape,
|
|
|
+ r: [4, 4, 4, 4] // 圆角
|
|
|
+ },
|
|
|
+ style: api.style()
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'text',
|
|
|
+ style: {
|
|
|
+ text: count > 0 ? count : '',
|
|
|
+ x: rectX + width / 2,
|
|
|
+ y: rectY + height / 2,
|
|
|
+ textAlign: 'center',
|
|
|
+ textVerticalAlign: 'middle',
|
|
|
+ fill: textColor,
|
|
|
+ fontSize: 12,
|
|
|
+ fontWeight: 'bold'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
},
|
|
|
encode: { x: [1, 2], y: 0 },
|
|
|
data,
|