From 9d5cdef8358310600499d42fc36878b7019a1c6c Mon Sep 17 00:00:00 2001 From: "LINING-PC\\lining" Date: Tue, 3 Feb 2026 11:23:12 +0800 Subject: [PATCH] v1.9.0-rc.5 - SIT --- demo/App.vue | 8 +- demo/data-resources-large.json | 200 +++++++++++------------ demo/data-resources.json | 26 +-- scripts/generate-large-data.js | 2 +- src/components/GanttChart.vue | 61 ++----- src/components/TaskBar.vue | 8 +- src/components/Timeline.vue | 9 +- src/components/Timeline/TaskBarTab.vue | 33 ++-- src/models/classes/Resource.ts | 45 ++--- src/models/configs/ResourceListConfig.ts | 8 +- 10 files changed, 182 insertions(+), 218 deletions(-) diff --git a/demo/App.vue b/demo/App.vue index 215794e..3ffc4e9 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -101,7 +101,7 @@ const applyDataSource = (source: RawDataSource) => { description: resData.description, department: resData.department, skills: resData.skills, - utilization: resData.utilization, + capacity: resData.capacity, color: resData.color, tasks: resData.tasks || [] }) @@ -293,13 +293,13 @@ const resourceListConfig = computed(() => ({ formatter: (resource: Resource) => resource.name || '-', }, { - key: 'utilization', + key: 'capacity', label: '利用率', visible: true, width: 100, formatter: (resource: Resource) => { - if (resource.utilization != null) { - return `${resource.utilization}%` + if (resource.capacity != null) { + return `${resource.capacity}%` } // 简单计算:基于任务数量的利用率 const taskCount = resource.tasks?.length || 0 diff --git a/demo/data-resources-large.json b/demo/data-resources-large.json index 1595734..bd0a9d2 100644 --- a/demo/data-resources-large.json +++ b/demo/data-resources-large.json @@ -11,7 +11,7 @@ "技能B", "技能C" ], - "utilization": 66, + "capacity": 66, "color": "#4caf50", "tasks": [ { @@ -477,7 +477,7 @@ "技能B", "技能C" ], - "utilization": 73, + "capacity": 73, "color": "#ff9800", "tasks": [ { @@ -928,7 +928,7 @@ "技能B", "技能C" ], - "utilization": 79, + "capacity": 79, "color": "#f44336", "tasks": [ { @@ -1394,7 +1394,7 @@ "技能B", "技能C" ], - "utilization": 64, + "capacity": 64, "color": "#9c27b0", "tasks": [ { @@ -1815,7 +1815,7 @@ "技能B", "技能C" ], - "utilization": 82, + "capacity": 82, "color": "#00bcd4", "tasks": [ { @@ -2266,7 +2266,7 @@ "技能B", "技能C" ], - "utilization": 75, + "capacity": 75, "color": "#8bc34a", "tasks": [ { @@ -2657,7 +2657,7 @@ "技能B", "技能C" ], - "utilization": 93, + "capacity": 93, "color": "#a8a8a8", "tasks": [ { @@ -3108,7 +3108,7 @@ "技能B", "技能C" ], - "utilization": 73, + "capacity": 73, "color": "#e91e63", "tasks": [ { @@ -3454,7 +3454,7 @@ "技能B", "技能C" ], - "utilization": 80, + "capacity": 80, "color": "#3f51b5", "tasks": [ { @@ -3920,7 +3920,7 @@ "技能B", "技能C" ], - "utilization": 63, + "capacity": 63, "color": "#2196f3", "tasks": [ { @@ -4251,7 +4251,7 @@ "技能B", "技能C" ], - "utilization": 86, + "capacity": 86, "color": "#4caf50", "tasks": [ { @@ -4672,7 +4672,7 @@ "技能B", "技能C" ], - "utilization": 62, + "capacity": 62, "color": "#ff9800", "tasks": [ { @@ -5018,7 +5018,7 @@ "技能B", "技能C" ], - "utilization": 95, + "capacity": 95, "color": "#f44336", "tasks": [ { @@ -5394,7 +5394,7 @@ "技能B", "技能C" ], - "utilization": 87, + "capacity": 87, "color": "#9c27b0", "tasks": [ { @@ -5785,7 +5785,7 @@ "技能B", "技能C" ], - "utilization": 73, + "capacity": 73, "color": "#00bcd4", "tasks": [ { @@ -6221,7 +6221,7 @@ "技能B", "技能C" ], - "utilization": 67, + "capacity": 67, "color": "#8bc34a", "tasks": [ { @@ -6537,7 +6537,7 @@ "技能B", "技能C" ], - "utilization": 95, + "capacity": 95, "color": "#ffc107", "tasks": [ { @@ -6988,7 +6988,7 @@ "技能B", "技能C" ], - "utilization": 77, + "capacity": 77, "color": "#e91e63", "tasks": [ { @@ -7364,7 +7364,7 @@ "技能B", "技能C" ], - "utilization": 63, + "capacity": 63, "color": "#3f51b5", "tasks": [ { @@ -7725,7 +7725,7 @@ "技能B", "技能C" ], - "utilization": 61, + "capacity": 61, "color": "#2196f3", "tasks": [ { @@ -8161,7 +8161,7 @@ "技能B", "技能C" ], - "utilization": 69, + "capacity": 69, "color": "#4caf50", "tasks": [ { @@ -8522,7 +8522,7 @@ "技能B", "技能C" ], - "utilization": 83, + "capacity": 83, "color": "#ff9800", "tasks": [ { @@ -8928,7 +8928,7 @@ "技能B", "技能C" ], - "utilization": 99, + "capacity": 99, "color": "#f44336", "tasks": [ { @@ -9304,7 +9304,7 @@ "技能B", "技能C" ], - "utilization": 66, + "capacity": 66, "color": "#9c27b0", "tasks": [ { @@ -9725,7 +9725,7 @@ "技能B", "技能C" ], - "utilization": 97, + "capacity": 97, "color": "#00bcd4", "tasks": [ { @@ -10071,7 +10071,7 @@ "技能B", "技能C" ], - "utilization": 95, + "capacity": 95, "color": "#8bc34a", "tasks": [ { @@ -10462,7 +10462,7 @@ "技能B", "技能C" ], - "utilization": 80, + "capacity": 80, "color": "#ffc107", "tasks": [ { @@ -10778,7 +10778,7 @@ "技能B", "技能C" ], - "utilization": 82, + "capacity": 82, "color": "#e91e63", "tasks": [ { @@ -11199,7 +11199,7 @@ "技能B", "技能C" ], - "utilization": 86, + "capacity": 86, "color": "#3f51b5", "tasks": [ { @@ -11605,7 +11605,7 @@ "技能B", "技能C" ], - "utilization": 74, + "capacity": 74, "color": "#2196f3", "tasks": [ { @@ -11951,7 +11951,7 @@ "技能B", "技能C" ], - "utilization": 73, + "capacity": 73, "color": "#4caf50", "tasks": [ { @@ -12417,7 +12417,7 @@ "技能B", "技能C" ], - "utilization": 64, + "capacity": 64, "color": "#ff9800", "tasks": [ { @@ -12823,7 +12823,7 @@ "技能B", "技能C" ], - "utilization": 93, + "capacity": 93, "color": "#f44336", "tasks": [ { @@ -13229,7 +13229,7 @@ "技能B", "技能C" ], - "utilization": 62, + "capacity": 62, "color": "#9c27b0", "tasks": [ { @@ -13695,7 +13695,7 @@ "技能B", "技能C" ], - "utilization": 93, + "capacity": 93, "color": "#00bcd4", "tasks": [ { @@ -14146,7 +14146,7 @@ "技能B", "技能C" ], - "utilization": 69, + "capacity": 69, "color": "#8bc34a", "tasks": [ { @@ -14597,7 +14597,7 @@ "技能B", "技能C" ], - "utilization": 88, + "capacity": 88, "color": "#ffc107", "tasks": [ { @@ -14958,7 +14958,7 @@ "技能B", "技能C" ], - "utilization": 84, + "capacity": 84, "color": "#e91e63", "tasks": [ { @@ -15364,7 +15364,7 @@ "技能B", "技能C" ], - "utilization": 60, + "capacity": 60, "color": "#3f51b5", "tasks": [ { @@ -15680,7 +15680,7 @@ "技能B", "技能C" ], - "utilization": 81, + "capacity": 81, "color": "#2196f3", "tasks": [ { @@ -16116,7 +16116,7 @@ "技能B", "技能C" ], - "utilization": 97, + "capacity": 97, "color": "#4caf50", "tasks": [ { @@ -16492,7 +16492,7 @@ "技能B", "技能C" ], - "utilization": 88, + "capacity": 88, "color": "#ff9800", "tasks": [ { @@ -16913,7 +16913,7 @@ "技能B", "技能C" ], - "utilization": 90, + "capacity": 90, "color": "#f44336", "tasks": [ { @@ -17259,7 +17259,7 @@ "技能B", "技能C" ], - "utilization": 63, + "capacity": 63, "color": "#9c27b0", "tasks": [ { @@ -17590,7 +17590,7 @@ "技能B", "技能C" ], - "utilization": 69, + "capacity": 69, "color": "#00bcd4", "tasks": [ { @@ -17951,7 +17951,7 @@ "技能B", "技能C" ], - "utilization": 85, + "capacity": 85, "color": "#8bc34a", "tasks": [ { @@ -18387,7 +18387,7 @@ "技能B", "技能C" ], - "utilization": 67, + "capacity": 67, "color": "#ffc107", "tasks": [ { @@ -18733,7 +18733,7 @@ "技能B", "技能C" ], - "utilization": 63, + "capacity": 63, "color": "#e91e63", "tasks": [ { @@ -19154,7 +19154,7 @@ "技能B", "技能C" ], - "utilization": 78, + "capacity": 78, "color": "#3f51b5", "tasks": [ { @@ -19500,7 +19500,7 @@ "技能B", "技能C" ], - "utilization": 87, + "capacity": 87, "color": "#2196f3", "tasks": [ { @@ -19921,7 +19921,7 @@ "技能B", "技能C" ], - "utilization": 91, + "capacity": 91, "color": "#4caf50", "tasks": [ { @@ -20327,7 +20327,7 @@ "技能B", "技能C" ], - "utilization": 65, + "capacity": 65, "color": "#ff9800", "tasks": [ { @@ -20658,7 +20658,7 @@ "技能B", "技能C" ], - "utilization": 68, + "capacity": 68, "color": "#f44336", "tasks": [ { @@ -21034,7 +21034,7 @@ "技能B", "技能C" ], - "utilization": 67, + "capacity": 67, "color": "#9c27b0", "tasks": [ { @@ -21350,7 +21350,7 @@ "技能B", "技能C" ], - "utilization": 72, + "capacity": 72, "color": "#00bcd4", "tasks": [ { @@ -21786,7 +21786,7 @@ "技能B", "技能C" ], - "utilization": 97, + "capacity": 97, "color": "#8bc34a", "tasks": [ { @@ -22252,7 +22252,7 @@ "技能B", "技能C" ], - "utilization": 60, + "capacity": 60, "color": "#ffc107", "tasks": [ { @@ -22568,7 +22568,7 @@ "技能B", "技能C" ], - "utilization": 96, + "capacity": 96, "color": "#e91e63", "tasks": [ { @@ -22944,7 +22944,7 @@ "技能B", "技能C" ], - "utilization": 87, + "capacity": 87, "color": "#3f51b5", "tasks": [ { @@ -23380,7 +23380,7 @@ "技能B", "技能C" ], - "utilization": 82, + "capacity": 82, "color": "#2196f3", "tasks": [ { @@ -23801,7 +23801,7 @@ "技能B", "技能C" ], - "utilization": 74, + "capacity": 74, "color": "#4caf50", "tasks": [ { @@ -24117,7 +24117,7 @@ "技能B", "技能C" ], - "utilization": 99, + "capacity": 99, "color": "#ff9800", "tasks": [ { @@ -24538,7 +24538,7 @@ "技能B", "技能C" ], - "utilization": 70, + "capacity": 70, "color": "#f44336", "tasks": [ { @@ -24989,7 +24989,7 @@ "技能B", "技能C" ], - "utilization": 91, + "capacity": 91, "color": "#9c27b0", "tasks": [ { @@ -25320,7 +25320,7 @@ "技能B", "技能C" ], - "utilization": 93, + "capacity": 93, "color": "#00bcd4", "tasks": [ { @@ -25726,7 +25726,7 @@ "技能B", "技能C" ], - "utilization": 99, + "capacity": 99, "color": "#8bc34a", "tasks": [ { @@ -26132,7 +26132,7 @@ "技能B", "技能C" ], - "utilization": 68, + "capacity": 68, "color": "#ffc107", "tasks": [ { @@ -26538,7 +26538,7 @@ "技能B", "技能C" ], - "utilization": 99, + "capacity": 99, "color": "#e91e63", "tasks": [ { @@ -26944,7 +26944,7 @@ "技能B", "技能C" ], - "utilization": 81, + "capacity": 81, "color": "#3f51b5", "tasks": [ { @@ -27410,7 +27410,7 @@ "技能B", "技能C" ], - "utilization": 95, + "capacity": 95, "color": "#2196f3", "tasks": [ { @@ -27801,7 +27801,7 @@ "技能B", "技能C" ], - "utilization": 98, + "capacity": 98, "color": "#4caf50", "tasks": [ { @@ -28147,7 +28147,7 @@ "技能B", "技能C" ], - "utilization": 86, + "capacity": 86, "color": "#ff9800", "tasks": [ { @@ -28568,7 +28568,7 @@ "技能B", "技能C" ], - "utilization": 61, + "capacity": 61, "color": "#f44336", "tasks": [ { @@ -28989,7 +28989,7 @@ "技能B", "技能C" ], - "utilization": 86, + "capacity": 86, "color": "#9c27b0", "tasks": [ { @@ -29305,7 +29305,7 @@ "技能B", "技能C" ], - "utilization": 97, + "capacity": 97, "color": "#00bcd4", "tasks": [ { @@ -29696,7 +29696,7 @@ "技能B", "技能C" ], - "utilization": 97, + "capacity": 97, "color": "#8bc34a", "tasks": [ { @@ -30117,7 +30117,7 @@ "技能B", "技能C" ], - "utilization": 78, + "capacity": 78, "color": "#ffc107", "tasks": [ { @@ -30553,7 +30553,7 @@ "技能B", "技能C" ], - "utilization": 66, + "capacity": 66, "color": "#e91e63", "tasks": [ { @@ -30974,7 +30974,7 @@ "技能B", "技能C" ], - "utilization": 63, + "capacity": 63, "color": "#3f51b5", "tasks": [ { @@ -31335,7 +31335,7 @@ "技能B", "技能C" ], - "utilization": 79, + "capacity": 79, "color": "#2196f3", "tasks": [ { @@ -31711,7 +31711,7 @@ "技能B", "技能C" ], - "utilization": 72, + "capacity": 72, "color": "#4caf50", "tasks": [ { @@ -32027,7 +32027,7 @@ "技能B", "技能C" ], - "utilization": 82, + "capacity": 82, "color": "#ff9800", "tasks": [ { @@ -32373,7 +32373,7 @@ "技能B", "技能C" ], - "utilization": 61, + "capacity": 61, "color": "#f44336", "tasks": [ { @@ -32719,7 +32719,7 @@ "技能B", "技能C" ], - "utilization": 89, + "capacity": 89, "color": "#9c27b0", "tasks": [ { @@ -33065,7 +33065,7 @@ "技能B", "技能C" ], - "utilization": 78, + "capacity": 78, "color": "#00bcd4", "tasks": [ { @@ -33381,7 +33381,7 @@ "技能B", "技能C" ], - "utilization": 64, + "capacity": 64, "color": "#8bc34a", "tasks": [ { @@ -33832,7 +33832,7 @@ "技能B", "技能C" ], - "utilization": 68, + "capacity": 68, "color": "#ffc107", "tasks": [ { @@ -34193,7 +34193,7 @@ "技能B", "技能C" ], - "utilization": 73, + "capacity": 73, "color": "#e91e63", "tasks": [ { @@ -34524,7 +34524,7 @@ "技能B", "技能C" ], - "utilization": 91, + "capacity": 91, "color": "#3f51b5", "tasks": [ { @@ -34960,7 +34960,7 @@ "技能B", "技能C" ], - "utilization": 96, + "capacity": 96, "color": "#2196f3", "tasks": [ { @@ -35411,7 +35411,7 @@ "技能B", "技能C" ], - "utilization": 96, + "capacity": 96, "color": "#4caf50", "tasks": [ { @@ -35802,7 +35802,7 @@ "技能B", "技能C" ], - "utilization": 77, + "capacity": 77, "color": "#ff9800", "tasks": [ { @@ -36193,7 +36193,7 @@ "技能B", "技能C" ], - "utilization": 85, + "capacity": 85, "color": "#f44336", "tasks": [ { @@ -36524,7 +36524,7 @@ "技能B", "技能C" ], - "utilization": 89, + "capacity": 89, "color": "#9c27b0", "tasks": [ { @@ -36870,7 +36870,7 @@ "技能B", "技能C" ], - "utilization": 79, + "capacity": 79, "color": "#00bcd4", "tasks": [ { @@ -37201,7 +37201,7 @@ "技能B", "技能C" ], - "utilization": 91, + "capacity": 91, "color": "#8bc34a", "tasks": [ { @@ -37622,7 +37622,7 @@ "技能B", "技能C" ], - "utilization": 91, + "capacity": 91, "color": "#ffc107", "tasks": [ { @@ -38073,7 +38073,7 @@ "技能B", "技能C" ], - "utilization": 67, + "capacity": 67, "color": "#e91e63", "tasks": [ { @@ -38524,7 +38524,7 @@ "技能B", "技能C" ], - "utilization": 77, + "capacity": 77, "color": "#3f51b5", "tasks": [ { @@ -38870,7 +38870,7 @@ "技能B", "技能C" ], - "utilization": 65, + "capacity": 65, "color": "#2196f3", "tasks": [ { diff --git a/demo/data-resources.json b/demo/data-resources.json index bbe8bf7..e247480 100644 --- a/demo/data-resources.json +++ b/demo/data-resources.json @@ -7,7 +7,7 @@ "avatar": "/50-11.jpg", "department": "临床研究部", "skills": ["临床试验管理", "项目规划", "团队领导"], - "utilization": 85, + "capacity": 85, "color": "#2196f3", "tasks": [ { @@ -52,7 +52,7 @@ "avatar": "/50-12.jpg", "department": "I期临床研究组", "skills": ["I期试验设计", "药代动力学", "安全性评估"], - "utilization": 92, + "capacity": 92, "color": "#4caf50", "tasks": [ { @@ -97,7 +97,7 @@ "avatar": "/50-13.jpg", "department": "临床研究部", "skills": ["方案设计", "伦理审查", "监管申报"], - "utilization": 75, + "capacity": 75, "tasks": [ { "id": 1101, @@ -131,7 +131,7 @@ "avatar": "/50-14.jpg", "department": "数据管理与统计部", "skills": ["统计分析", "数据可视化", "报告撰写"], - "utilization": 68, + "capacity": 68, "color": "#ff9800", "tasks": [ { @@ -176,7 +176,7 @@ "avatar": "/50-15.jpg", "department": "质量保证部", "skills": ["GCP审计", "质量控制", "SOP管理"], - "utilization": 88, + "capacity": 88, "tasks": [ { "id": 1104, @@ -211,7 +211,7 @@ "avatar": "/50-16.jpg", "department": "II期临床研究组", "skills": ["试验协调", "中心管理", "受试者跟踪"], - "utilization": 55, + "capacity": 55, "tasks": [ { "id": 1200, @@ -238,7 +238,7 @@ "avatar": "/50-17.jpg", "department": "设备与后勤部", "skills": ["设备维护", "库存管理", "供应链协调"], - "utilization": 42, + "capacity": 42, "tasks": [ { "id": 5011, @@ -265,7 +265,7 @@ "avatar": "/50-18.jpg", "department": "III期临床研究组", "skills": ["大型试验管理", "多中心协调", "里程碑管理"], - "utilization": 35, + "capacity": 35, "color": "#9c27b0", "tasks": [ { @@ -285,7 +285,7 @@ "avatar": "/50-19.jpg", "department": "数据管理与统计部", "skills": ["统计建模", "样本量计算", "数据分析"], - "utilization": 95, + "capacity": 95, "color": "#ff5733", "tasks": [ { @@ -341,7 +341,7 @@ "avatar": "/50-20.jpg", "department": "临床监查部", "skills": ["现场监查", "数据核查", "问题解决"], - "utilization": 110, + "capacity": 110, "color": "#3498db", "tasks": [ { @@ -397,7 +397,7 @@ "avatar": "/50-21.jpg", "department": "医学事务部", "skills": ["研究报告撰写", "文献检索", "医学翻译"], - "utilization": 72, + "capacity": 72, "color": "#e74c3c", "tasks": [ { @@ -442,7 +442,7 @@ "avatar": "/50-22.jpg", "department": "药物警戒部", "skills": ["不良事件管理", "安全性报告", "信号检测"], - "utilization": 88, + "capacity": 88, "color": "#f39c12", "tasks": [ { @@ -478,7 +478,7 @@ "avatar": "/50-23.jpg", "department": "注册事务部", "skills": ["IND申报", "监管沟通", "文件管理"], - "utilization": 65, + "capacity": 65, "tasks": [ { "id": 6015, diff --git a/scripts/generate-large-data.js b/scripts/generate-large-data.js index d10453f..0ab98c9 100644 --- a/scripts/generate-large-data.js +++ b/scripts/generate-large-data.js @@ -56,7 +56,7 @@ function generateLargeResourceData() { avatar: `https://i.pravatar.cc/150?img=${i}`, department: departments[i % departments.length], skills: ['技能A', '技能B', '技能C'], - utilization: 60 + Math.floor(Math.random() * 40), + capacity: 60 + Math.floor(Math.random() * 40), color: colors[i % colors.length], tasks, }) diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue index c842b42..9e13f4f 100644 --- a/src/components/GanttChart.vue +++ b/src/components/GanttChart.vue @@ -28,6 +28,7 @@ import { } from '../models/configs/TaskListConfig' import type { TaskBarConfig } from '../models/configs/TaskBarConfig' import { TimelineScale, SCALE_CONFIGS } from '../models/types/TimelineScale' +import { detectConflicts } from '../utils/conflictUtils' import { useMessage } from '../composables/useMessage' const props = withDefaults(defineProps(), { @@ -189,6 +190,7 @@ const resourceRowPositions = computed(() => { }) // 计算资源冲突状态(依赖updateTaskTrigger以便实时更新) +// v1.9.9 修复:使用 detectConflicts 函数来正确检测多任务叠加的超载情况 const resourceConflicts = computed(() => { if (currentViewMode.value !== 'resource') return new Map() @@ -198,38 +200,22 @@ const resourceConflicts = computed(() => { // 依赖 updateTaskTrigger 以便在任务更新时重新计算冲突 if (updateTaskTrigger.value >= 0) { resources.forEach(resource => { - // v1.9.2 使用 Resource.isOverloaded() 方法检测真正的资源超载 - // 超载定义:同一时间段内,资源总占用比例 > 100% - if (typeof resource.isOverloaded === 'function' && resource.isOverloaded()) { + const tasks = resource.tasks || [] + if (tasks.length < 2) return + + // 使用 conflictUtils 的 detectConflicts 函数来检测冲突 + // 这个函数能正确处理多任务叠加的超载情况(如 A:40% + B:40% + C:30% = 110%) + const conflictZones = detectConflicts(tasks, resource.id) + + if (conflictZones.length > 0) { const conflicts = new Set() - const tasks = resource.tasks || [] - const validTasks = tasks.filter(task => task.startDate && task.endDate) - // 找出所有导致超载的任务对 - for (let i = 0; i < validTasks.length; i++) { - for (let j = i + 1; j < validTasks.length; j++) { - const task1 = validTasks[i] - const task2 = validTasks[j] - - // 检查时间重叠 - const start1 = new Date(task1.startDate!).getTime() - const end1 = new Date(task1.endDate!).getTime() - const start2 = new Date(task2.startDate!).getTime() - const end2 = new Date(task2.endDate!).getTime() - - if (start1 < end2 && start2 < end1) { - // 有时间重叠,检查总占比是否超过100% - const percent1 = getTaskAllocationPercent(resource, task1) - const percent2 = getTaskAllocationPercent(resource, task2) - - if (percent1 + percent2 > 100) { - // 超载,标记这两个任务 - conflicts.add(task1.id) - conflicts.add(task2.id) - } - } - } - } + // 收集所有冲突区域中涉及的任务ID + conflictZones.forEach(zone => { + zone.tasks.forEach(taskInfo => { + conflicts.add(taskInfo.id) + }) + }) if (conflicts.size > 0) { conflictsMap.set(String(resource.id), conflicts) @@ -241,21 +227,6 @@ const resourceConflicts = computed(() => { return conflictsMap }) -// 辅助函数:获取任务中当前资源的投入占比 -function getTaskAllocationPercent(resource: Resource, task: Task): number { - if (!task.resources || !Array.isArray(task.resources)) { - return 100 // 未配置resources时,默认100% - } - - const allocation = task.resources.find((r: any) => String(r.id) === String(resource.id)) - if (!allocation) { - return 100 // 未找到当前资源的分配信息,默认100% - } - - const percent = allocation.percent ?? 100 - return Math.max(20, Math.min(100, percent)) // 限制范围 20-100 -} - // 提供资源布局信息给子组件 provide('resourceTaskLayouts', resourceTaskLayouts) provide('resourceRowPositions', resourceRowPositions) diff --git a/src/components/TaskBar.vue b/src/components/TaskBar.vue index 3fbd009..7f09f23 100644 --- a/src/components/TaskBar.vue +++ b/src/components/TaskBar.vue @@ -47,7 +47,7 @@ defineSlots<{ // 从 GanttChart 注入 enableLinkAnchor 配置 const enableLinkAnchor = inject>('enable-link-anchor', computed(() => true)) -// v1.9.0 计算当前资源的投入占比 +// v1.9.0 计算当前资源的利用率 const resourcePercent = computed(() => { // 如果直接传递了占比,使用传递的值 if (props.resourceAllocationPercent !== undefined) { @@ -156,7 +156,7 @@ interface Props { hasResourceConflict?: boolean // v1.9.2 资源视图:冲突任务列表(用于显示详细冲突信息) conflictTasks?: Task[] - // v1.9.0 资源视图:当前资源在任务中的投入占比 (20-100) + // v1.9.0 资源视图:当前资源在任务中的利用率 (20-100) resourceAllocationPercent?: number // v1.9.0 资源视图:当前资源ID(用于查找占比信息) currentResourceId?: string | number @@ -2694,7 +2694,7 @@ const handleTaskBarMouseEnter = (event: MouseEvent) => { const rowHeight = 24 // 每行内容高度 let contentRows = 4 // 默认4行(开始日期、结束日期、预估工时、实际工时、进度) - // 资源视图且投入占比<100%时,多1行 + // 资源视图且利用率<100%时,多1行 if (viewMode.value === 'resource' && resourcePercent.value < 100) { contentRows += 1 } @@ -3636,7 +3636,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
{{ task.name }}
- +
{{ t('investment') || '投入' }}: {{ resourcePercent }}% diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index ac111c3..6d59b33 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -130,6 +130,9 @@ const getConflictTasksForTask = (resourceId: string | number, taskId: string | n // v1.9.7 修复:返回所有与当前任务时间重叠的冲突任务 // 不需要再次验证占比相加是否超过100%,因为resourceConflicts已经包含了所有冲突的任务ID // 当多个任务同时重叠时(如3个任务各75%),应该全部返回,而不是只返回第一个两两超载的任务 + // v1.9.9 修复:endDate 包含当天,需要 +1 天来判断交集(与 conflictUtils 保持一致) + const currentEndPlus = currentEnd + 24 * 60 * 60 * 1000 + const conflictTasks = resource.tasks.filter(task => { if (task.id === taskId) return false if (!task.startDate || !task.endDate) return false @@ -138,9 +141,11 @@ const getConflictTasksForTask = (resourceId: string | number, taskId: string | n const taskStart = new Date(task.startDate).getTime() const taskEnd = new Date(task.endDate).getTime() + const taskEndPlus = taskEnd + 24 * 60 * 60 * 1000 - // 检查时间重叠:只要与当前任务有时间交集,就是冲突任务 - return currentStart < taskEnd && taskStart < currentEnd + // 检查时间重叠:endDate 包含当天,所以需要 +1 天来判断 + // 例如:任务A endDate=12-24, 任务B startDate=12-24,应判断为重叠(都占用12-24这一天) + return currentStart < taskEndPlus && taskStart < currentEndPlus }) return conflictTasks diff --git a/src/components/Timeline/TaskBarTab.vue b/src/components/Timeline/TaskBarTab.vue index bdc4b10..904090d 100644 --- a/src/components/Timeline/TaskBarTab.vue +++ b/src/components/Timeline/TaskBarTab.vue @@ -197,7 +197,7 @@ const conflictInfoList = computed(() => { // 计算当前任务的资源占比 const currentPercent = props.resourcePercent || 100 - // v1.9.8 修改:只显示冲突任务自己的投入占比 + // v1.9.8 修改:只显示冲突任务自己的利用率 return props.conflictTasks.map(conflictTask => { if (!conflictTask.startDate || !conflictTask.endDate) return null @@ -228,7 +228,7 @@ const conflictInfoList = computed(() => { taskName: conflictTask.name, overlapStart: formatDate(overlapStart), overlapEnd: formatDate(overlapEnd), - conflictPercent, // 该冲突任务自己的投入占比 + conflictPercent, // 该冲突任务自己的利用率 } }).filter(Boolean) }) @@ -246,6 +246,9 @@ const totalOverloadPercent = computed(() => { const currentEnd = new Date(currentTask.endDate).getTime() const currentPercent = props.resourcePercent || 100 + // v1.9.9 修复:endDate 包含当天,需要 +1 天来判断交集 + const DAY_MS = 24 * 60 * 60 * 1000 + // 计算所有冲突任务在重叠时间段内的最大总占比 let maxTotalPercent = currentPercent @@ -257,26 +260,31 @@ const totalOverloadPercent = computed(() => { if (!task1.startDate || !task1.endDate) return const start1 = new Date(task1.startDate).getTime() const end1 = new Date(task1.endDate).getTime() + const end1Plus = end1 + DAY_MS // endDate 包含当天,需要 +1 天 allTasks.forEach((task2, j) => { if (i >= j || !task2.startDate || !task2.endDate) return const start2 = new Date(task2.startDate).getTime() const end2 = new Date(task2.endDate).getTime() + const end2Plus = end2 + DAY_MS // endDate 包含当天,需要 +1 天 - // 检查是否有时间重叠 - if (start1 < end2 && start2 < end1) { + // 检查是否有时间重叠(使用 +1 天后的 endDate) + // 例如:任务A endDate=12-24, 任务B startDate=12-24,应判断为重叠 + if (start1 < end2Plus && start2 < end1Plus) { // 计算该重叠区间的所有任务总占比 const overlapStart = Math.max(start1, start2) const overlapEnd = Math.min(end1, end2) + const overlapEndPlus = overlapEnd + DAY_MS let intervalTotal = 0 allTasks.forEach(task => { if (!task.startDate || !task.endDate) return const tStart = new Date(task.startDate).getTime() const tEnd = new Date(task.endDate).getTime() + const tEndPlus = tEnd + DAY_MS - // 检查任务是否在该重叠区间内 - if (tStart < overlapEnd && tEnd > overlapStart) { + // 检查任务是否在该重叠区间内(使用 +1 天后的 endDate) + if (tStart < overlapEndPlus && tEndPlus > overlapStart) { let taskPercent = 100 if (task.resources && Array.isArray(task.resources)) { const allocation = task.resources.find( @@ -422,9 +430,9 @@ onUnmounted(() => {
- +
- 投入占比 + 利用率 {{ percentText }}
@@ -440,7 +448,7 @@ onUnmounted(() => { 资源超载警告 - +{{ totalOverloadPercent }}% + ▲ {{ totalOverloadPercent }}%
@@ -451,7 +459,7 @@ onUnmounted(() => { {{ info.overlapStart }} ~ {{ info.overlapEnd }}
- 任务投入占比: + 任务利用率: {{ info.conflictPercent }}%
@@ -598,10 +606,13 @@ onUnmounted(() => { } .total-overload { - font-size: 13px; + font-size: 12px; font-weight: 700; color: #ff5252; margin-left: auto; + background: #FFC107; + padding: 2px 4px; + border-radius: 4px; } .conflict-item { diff --git a/src/models/classes/Resource.ts b/src/models/classes/Resource.ts index 2de0854..64cc54c 100644 --- a/src/models/classes/Resource.ts +++ b/src/models/classes/Resource.ts @@ -1,4 +1,5 @@ import type { Task } from './Task' +import { detectConflicts } from '../../utils/conflictUtils' /** * 资源类 (Resource Class) @@ -15,7 +16,7 @@ export class Resource { description?: string department?: string skills?: string[] - utilization?: number + capacity?: number color?: string // 自定义资源行左边框颜色,如 '#ff5733',若不设置则使用默认颜色方案 tasks: Task[] [key: string]: unknown @@ -28,7 +29,7 @@ export class Resource { description?: string department?: string skills?: string[] - utilization?: number + capacity?: number color?: string tasks?: Task[] [key: string]: unknown @@ -40,7 +41,7 @@ export class Resource { this.description = data.description this.department = data.department this.skills = data.skills - this.utilization = data.utilization + this.capacity = data.capacity this.color = data.color this.tasks = data.tasks || [] @@ -89,7 +90,7 @@ export class Resource { * 更新利用率 */ updateUtilization(): void { - this.utilization = this.calculateUtilization() + this.capacity = this.calculateUtilization() } /** @@ -130,45 +131,21 @@ export class Resource { /** * v1.9.0 检测资源是否超载(基于占用比例) * 超载定义:同一时间段内,资源总占用比例 > 100% + * v1.9.9 修复:使用 detectConflicts 函数来正确检测多任务叠加的超载 * @returns 是否资源超载 */ isOverloaded(): boolean { if (this.tasks.length < 2) return false - // 过滤掉没有开始日期和结束日期的任务 - const validTasks = this.tasks.filter(task => task.startDate && task.endDate) - if (validTasks.length < 2) return false + // 使用 conflictUtils 的 detectConflicts 函数来检测冲突 + // 这个函数能正确处理多任务叠加的超载情况(如 A:40% + B:40% + C:30% = 110%) + const conflictZones = detectConflicts(this.tasks, this.id) - // 检测任意时间点的总占比是否超过100% - for (let i = 0; i < validTasks.length; i++) { - for (let j = i + 1; j < validTasks.length; j++) { - const task1 = validTasks[i] - const task2 = validTasks[j] - - // 检查两个任务是否有时间交集 - const start1 = new Date(task1.startDate!).getTime() - const end1 = new Date(task1.endDate!).getTime() - const start2 = new Date(task2.startDate!).getTime() - const end2 = new Date(task2.endDate!).getTime() - - // 判断时间是否重叠 - if (start1 < end2 && start2 < end1) { - // 有重叠,计算总占比 - const percent1 = this.getTaskAllocationPercent(task1) - const percent2 = this.getTaskAllocationPercent(task2) - - if (percent1 + percent2 > 100) { - return true // 超负荷 - } - } - } - } - - return false + return conflictZones.length > 0 } /** - * v1.9.0 获取任务中当前资源的投入占比 + * v1.9.0 获取任务中当前资源的利用率 * @param task 任务对象 * @returns 占比百分比 (20-100),默认100 */ diff --git a/src/models/configs/ResourceListConfig.ts b/src/models/configs/ResourceListConfig.ts index 36b75e8..3a2ca36 100644 --- a/src/models/configs/ResourceListConfig.ts +++ b/src/models/configs/ResourceListConfig.ts @@ -7,7 +7,7 @@ export type ResourceListColumnType = | 'name' | 'type' | 'department' - | 'utilization' + | 'capacity' | 'taskCount' /** @@ -71,10 +71,10 @@ export const DEFAULT_RESOURCE_LIST_COLUMNS: ResourceListColumnConfig[] = [ visible: true, }, { - type: 'utilization', - key: 'utilization', + type: 'capacity', + key: 'capacity', label: '利用率', - cssClass: 'col-utilization', + cssClass: 'col-capacity', visible: true, }, ]