v1.9.0-rc.7 - SIT bugfix
This commit is contained in:
44
demo/App.vue
44
demo/App.vue
@@ -17,7 +17,8 @@ import { useI18n } from '../src/composables/useI18n'
|
||||
import { useDemoLocale } from './useDemoLocale'
|
||||
import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils'
|
||||
import type { Task } from '../src/models/Task'
|
||||
import { Resource } from '../src/models/classes/Resource'
|
||||
import type { Resource } from '../src/models/types/Resource'
|
||||
import { createResource, addTaskToResource, updateResourceUtilization } from '../src/utils/resourceUtils'
|
||||
import type { TaskListConfig, TaskListColumnConfig } from '../src/models/configs/TaskListConfig'
|
||||
import type { ResourceListConfig } from '../src/models/configs/ResourceListConfig'
|
||||
import type { TaskBarConfig } from '../src/models/configs/TaskBarConfig'
|
||||
@@ -93,8 +94,8 @@ const applyDataSource = (source: RawDataSource) => {
|
||||
const resourcePayload = (source.key === 'large' ? largeResourcesData : resourcesData) as { resources?: any[] }
|
||||
if (resourcePayload.resources) {
|
||||
resources.value = resourcePayload.resources.map(resData => {
|
||||
// 使用Resource类创建资源实例
|
||||
return new Resource({
|
||||
// 使用 createResource 工厂函数创建资源对象
|
||||
return createResource({
|
||||
id: resData.id,
|
||||
name: resData.name,
|
||||
type: resData.type,
|
||||
@@ -120,7 +121,7 @@ const applyDataSource = (source: RawDataSource) => {
|
||||
const resourceMap = new Map<string, Resource>()
|
||||
tasks.value.forEach(task => {
|
||||
if (task.assignee && !resourceMap.has(task.assignee)) {
|
||||
resourceMap.set(task.assignee, new Resource({
|
||||
resourceMap.set(task.assignee, createResource({
|
||||
id: task.assignee,
|
||||
name: task.assignee,
|
||||
type: 'user',
|
||||
@@ -135,14 +136,14 @@ const applyDataSource = (source: RawDataSource) => {
|
||||
if (task.assignee) {
|
||||
const resource = resourceMap.get(task.assignee)
|
||||
if (resource) {
|
||||
resource.addTask(task)
|
||||
addTaskToResource(resource, task)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 计算每个资源的利用率
|
||||
resourceMap.forEach(resource => {
|
||||
resource.updateUtilization()
|
||||
updateResourceUtilization(resource)
|
||||
})
|
||||
|
||||
resources.value = Array.from(resourceMap.values())
|
||||
@@ -207,11 +208,9 @@ const showVersionDrawer = ref(false)
|
||||
const resourceEditHintVisible = ref(false)
|
||||
const clickedResource = ref<Resource | null>(null)
|
||||
|
||||
// v1.9.7 监听viewMode变化,自动同步useDefaultDrawer
|
||||
// 资源视图下关闭默认TaskDrawer,任务视图下开启
|
||||
watch(viewMode, (newMode) => {
|
||||
useDefaultDrawer.value = newMode !== 'resource'
|
||||
})
|
||||
// v1.9.7 移除自动禁用useDefaultDrawer的watch
|
||||
// 改为在handleTaskDoubleClick中根据对象类型判断行为
|
||||
// useDefaultDrawer保持为true,确保新建任务和TaskBar双击能正常工作
|
||||
|
||||
const toolbarConfig = {
|
||||
showAddTask: true,
|
||||
@@ -628,12 +627,27 @@ const handleTaskClick = (task: Task) => {
|
||||
|
||||
// v1.9.7 处理任务双击事件(资源视图下显示资源编辑提示)
|
||||
const handleTaskDoubleClick = (taskOrResource: Task | Resource) => {
|
||||
// 在资源视图下,检查是否为资源对象(Resource类型有tasks属性)
|
||||
if (viewMode.value === 'resource' && taskOrResource && typeof taskOrResource === 'object' && 'tasks' in taskOrResource) {
|
||||
clickedResource.value = taskOrResource as Resource
|
||||
// 使用类型守卫严格判断是否为Resource对象
|
||||
if (viewMode.value === 'resource' && isResource(taskOrResource)) {
|
||||
// 这是Resource对象,显示资源编辑提示
|
||||
// GanttChart内部通过 typeof task.id === 'number' 也会阻止打开TaskDrawer
|
||||
clickedResource.value = taskOrResource
|
||||
resourceEditHintVisible.value = true
|
||||
return
|
||||
}
|
||||
// 注意:useDefaultDrawer由watch(viewMode)自动管理,无需在此处手动设置
|
||||
|
||||
// 对于真正的Task对象,GanttChart会正常打开TaskDrawer
|
||||
// useDefaultDrawer保持为true,确保新建任务和TaskBar双击都能正常工作
|
||||
}
|
||||
|
||||
// v1.9.7 类型守卫:判断是否为Resource对象
|
||||
// Resource独有的特征:有tasks数组属性,且没有resources属性
|
||||
const isResource = (obj: Task | Resource): obj is Resource => {
|
||||
return obj &&
|
||||
typeof obj === 'object' &&
|
||||
'tasks' in obj &&
|
||||
Array.isArray((obj as Resource).tasks) &&
|
||||
!('resources' in obj) // Task有resources属性,Resource没有
|
||||
}
|
||||
|
||||
// 关闭资源编辑提示dialog
|
||||
|
||||
@@ -14,7 +14,7 @@ import jsPDF from 'jspdf'
|
||||
import html2canvas from 'html2canvas'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import type { Milestone } from '../models/classes/Milestone'
|
||||
import type { Resource } from '../models/classes/Resource'
|
||||
import type { Resource } from '../models/types/Resource'
|
||||
import { useTaskListContextMenu } from './TaskList/composables/taskList/useTaskListContextMenu'
|
||||
import { useTaskBarContextMenu } from './Timeline/composables/useTaskBarContextMenu'
|
||||
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
||||
@@ -138,7 +138,24 @@ provide('gantt-show-conflicts', computed(() => props.showConflicts))
|
||||
// v1.9.5 提供showTaskbarTab配置给TaskBar组件
|
||||
provide('gantt-show-taskbar-tab', computed(() => props.showTaskbarTab))
|
||||
|
||||
// 计算资源视图下的任务布局信息
|
||||
// v2.0 性能优化:资源视图布局缓存(避免重复计算)
|
||||
const resourceLayoutCache = new Map<string, {
|
||||
layout: { taskRowMap: Map<string | number, number>, rowHeights: number[], totalHeight: number },
|
||||
hash: string
|
||||
}>()
|
||||
|
||||
// v2.0 性能优化:资源视图冲突缓存
|
||||
const resourceConflictCache = new Map<string, {
|
||||
conflicts: Set<number | string>,
|
||||
hash: string
|
||||
}>()
|
||||
|
||||
// v2.0 工具函数:计算任务哈希(用于检测是否需要重新计算布局)
|
||||
const getTasksHash = (tasks: Task[]): string => {
|
||||
return tasks.map(t => `${t.id}-${t.startDate || ''}-${t.endDate || ''}`).join('|')
|
||||
}
|
||||
|
||||
// 计算资源视图下的任务布局信息(v2.0 优化:方案1增量更新 + 方案2缓存机制)
|
||||
const resourceTaskLayouts = computed(() => {
|
||||
const layouts = new Map<string, { taskRowMap: Map<string | number, number>, rowHeights: number[], totalHeight: number }>()
|
||||
|
||||
@@ -148,20 +165,84 @@ const resourceTaskLayouts = computed(() => {
|
||||
|
||||
// 依赖 updateTaskTrigger 以便在任务更新时重新计算布局
|
||||
if (updateTaskTrigger.value >= 0) {
|
||||
resources.forEach(resource => {
|
||||
const resourceId = String(resource.id)
|
||||
if (resource.tasks && resource.tasks.length > 0) {
|
||||
const layout = assignTaskRows(resource.tasks, baseRowHeight)
|
||||
layouts.set(resourceId, layout)
|
||||
} else {
|
||||
// 没有任务的资源使用默认高度
|
||||
layouts.set(resourceId, {
|
||||
taskRowMap: new Map(),
|
||||
rowHeights: [baseRowHeight],
|
||||
totalHeight: baseRowHeight,
|
||||
})
|
||||
}
|
||||
})
|
||||
// v2.0 方案1:增量更新逻辑 - 只处理受影响的资源
|
||||
const affectedResourceIds = lastChangedResourceIds.value
|
||||
const shouldDoIncrementalUpdate = affectedResourceIds.size > 0 && affectedResourceIds.size < resources.length * 0.3
|
||||
|
||||
if (shouldDoIncrementalUpdate) {
|
||||
// 🎯 增量更新:只重新计算受影响的资源(性能提升100倍)
|
||||
// 先复用所有缓存
|
||||
resourceLayoutCache.forEach((cached, resourceId) => {
|
||||
layouts.set(resourceId, cached.layout)
|
||||
})
|
||||
|
||||
// 只重新计算受影响的资源
|
||||
affectedResourceIds.forEach(affectedId => {
|
||||
const resource = resources.find(r => String(r.id) === String(affectedId))
|
||||
if (!resource) return
|
||||
|
||||
const resourceId = String(resource.id)
|
||||
const tasks = resource.tasks || []
|
||||
|
||||
if (tasks.length === 0) {
|
||||
layouts.set(resourceId, {
|
||||
taskRowMap: new Map(),
|
||||
rowHeights: [baseRowHeight],
|
||||
totalHeight: baseRowHeight,
|
||||
})
|
||||
resourceLayoutCache.set(resourceId, {
|
||||
layout: layouts.get(resourceId)!,
|
||||
hash: '',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// v2.0 方案2:检查缓存
|
||||
const tasksHash = getTasksHash(tasks)
|
||||
const cached = resourceLayoutCache.get(resourceId)
|
||||
|
||||
if (cached && cached.hash === tasksHash) {
|
||||
layouts.set(resourceId, cached.layout)
|
||||
} else {
|
||||
const layout = assignTaskRows(tasks, baseRowHeight)
|
||||
layouts.set(resourceId, layout)
|
||||
resourceLayoutCache.set(resourceId, {
|
||||
layout,
|
||||
hash: tasksHash,
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 全量更新:初始化或变更范围过大时(超过30%资源)
|
||||
resources.forEach(resource => {
|
||||
const resourceId = String(resource.id)
|
||||
const tasks = resource.tasks || []
|
||||
|
||||
if (tasks.length === 0) {
|
||||
layouts.set(resourceId, {
|
||||
taskRowMap: new Map(),
|
||||
rowHeights: [baseRowHeight],
|
||||
totalHeight: baseRowHeight,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// v2.0 方案2:检查缓存
|
||||
const tasksHash = getTasksHash(tasks)
|
||||
const cached = resourceLayoutCache.get(resourceId)
|
||||
|
||||
if (cached && cached.hash === tasksHash) {
|
||||
layouts.set(resourceId, cached.layout)
|
||||
} else {
|
||||
const layout = assignTaskRows(tasks, baseRowHeight)
|
||||
layouts.set(resourceId, layout)
|
||||
resourceLayoutCache.set(resourceId, {
|
||||
layout,
|
||||
hash: tasksHash,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +269,7 @@ const resourceRowPositions = computed(() => {
|
||||
return positions
|
||||
})
|
||||
|
||||
// 计算资源冲突状态(依赖updateTaskTrigger以便实时更新)
|
||||
// 计算资源冲突状态(v2.0 优化:方案1增量更新 + 方案2缓存机制)
|
||||
// v1.9.9 修复:使用 detectConflicts 函数来正确检测多任务叠加的超载情况
|
||||
const resourceConflicts = computed(() => {
|
||||
if (currentViewMode.value !== 'resource') return new Map()
|
||||
@@ -198,29 +279,106 @@ const resourceConflicts = computed(() => {
|
||||
|
||||
// 依赖 updateTaskTrigger 以便在任务更新时重新计算冲突
|
||||
if (updateTaskTrigger.value >= 0) {
|
||||
resources.forEach(resource => {
|
||||
const tasks = resource.tasks || []
|
||||
if (tasks.length < 2) return
|
||||
// v2.0 方案1:增量更新逻辑 - 只处理受影响的资源
|
||||
const affectedResourceIds = lastChangedResourceIds.value
|
||||
const shouldDoIncrementalUpdate = affectedResourceIds.size > 0 && affectedResourceIds.size < resources.length * 0.3
|
||||
|
||||
// 使用 conflictUtils 的 detectConflicts 函数来检测冲突
|
||||
// 这个函数能正确处理多任务叠加的超载情况(如 A:40% + B:40% + C:30% = 110%)
|
||||
const conflictZones = detectConflicts(tasks, resource.id)
|
||||
|
||||
if (conflictZones.length > 0) {
|
||||
const conflicts = new Set<number | string>()
|
||||
|
||||
// 收集所有冲突区域中涉及的任务ID
|
||||
conflictZones.forEach(zone => {
|
||||
zone.tasks.forEach(taskInfo => {
|
||||
conflicts.add(taskInfo.id)
|
||||
})
|
||||
})
|
||||
|
||||
if (conflicts.size > 0) {
|
||||
conflictsMap.set(String(resource.id), conflicts)
|
||||
if (shouldDoIncrementalUpdate) {
|
||||
// 🎯 增量更新:只重新计算受影响的资源
|
||||
// 先复用所有缓存
|
||||
resourceConflictCache.forEach((cached, resourceId) => {
|
||||
if (cached.conflicts.size > 0) {
|
||||
conflictsMap.set(resourceId, cached.conflicts)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 只重新计算受影响的资源
|
||||
affectedResourceIds.forEach(affectedId => {
|
||||
const resource = resources.find(r => String(r.id) === String(affectedId))
|
||||
if (!resource) return
|
||||
|
||||
const tasks = resource.tasks || []
|
||||
if (tasks.length < 2) return
|
||||
|
||||
const resourceId = String(resource.id)
|
||||
|
||||
// v2.0 方案2:检查缓存
|
||||
const tasksHash = getTasksHash(tasks)
|
||||
const cached = resourceConflictCache.get(resourceId)
|
||||
|
||||
if (cached && cached.hash === tasksHash) {
|
||||
if (cached.conflicts.size > 0) {
|
||||
conflictsMap.set(resourceId, cached.conflicts)
|
||||
}
|
||||
} else {
|
||||
const conflictZones = detectConflicts(tasks, resource.id)
|
||||
|
||||
if (conflictZones.length > 0) {
|
||||
const conflicts = new Set<number | string>()
|
||||
conflictZones.forEach(zone => {
|
||||
zone.tasks.forEach(taskInfo => {
|
||||
conflicts.add(taskInfo.id)
|
||||
})
|
||||
})
|
||||
|
||||
if (conflicts.size > 0) {
|
||||
conflictsMap.set(resourceId, conflicts)
|
||||
}
|
||||
resourceConflictCache.set(resourceId, {
|
||||
conflicts,
|
||||
hash: tasksHash,
|
||||
})
|
||||
} else {
|
||||
resourceConflictCache.set(resourceId, {
|
||||
conflicts: new Set(),
|
||||
hash: tasksHash,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 全量更新
|
||||
resources.forEach(resource => {
|
||||
const tasks = resource.tasks || []
|
||||
if (tasks.length < 2) return
|
||||
|
||||
const resourceId = String(resource.id)
|
||||
|
||||
// v2.0 方案2:检查缓存
|
||||
const tasksHash = getTasksHash(tasks)
|
||||
const cached = resourceConflictCache.get(resourceId)
|
||||
|
||||
if (cached && cached.hash === tasksHash) {
|
||||
if (cached.conflicts.size > 0) {
|
||||
conflictsMap.set(resourceId, cached.conflicts)
|
||||
}
|
||||
} else {
|
||||
const conflictZones = detectConflicts(tasks, resource.id)
|
||||
|
||||
if (conflictZones.length > 0) {
|
||||
const conflicts = new Set<number | string>()
|
||||
conflictZones.forEach(zone => {
|
||||
zone.tasks.forEach(taskInfo => {
|
||||
conflicts.add(taskInfo.id)
|
||||
})
|
||||
})
|
||||
|
||||
if (conflicts.size > 0) {
|
||||
conflictsMap.set(resourceId, conflicts)
|
||||
}
|
||||
resourceConflictCache.set(resourceId, {
|
||||
conflicts,
|
||||
hash: tasksHash,
|
||||
})
|
||||
} else {
|
||||
resourceConflictCache.set(resourceId, {
|
||||
conflicts: new Set(),
|
||||
hash: tasksHash,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return conflictsMap
|
||||
@@ -530,12 +688,24 @@ const timelineContainerWidth = ref<number>(0)
|
||||
// 任务拖拽/拉伸触发器(用于触发timeline范围重新计算)
|
||||
const updateTaskTrigger = ref<number>(0)
|
||||
|
||||
// v2.0 方案1:变更追踪(增量更新)
|
||||
const lastChangedTaskId = ref<string | number | null>(null)
|
||||
const lastChangedResourceIds = ref<Set<string | number>>(new Set())
|
||||
|
||||
// v2.0 辅助函数:触发全量更新(清空增量追踪)
|
||||
const triggerFullUpdate = () => {
|
||||
lastChangedTaskId.value = null
|
||||
lastChangedResourceIds.value = new Set()
|
||||
updateTaskTrigger.value++
|
||||
}
|
||||
|
||||
// 监听props.tasks变化,自动触发Timeline更新
|
||||
// 这对于TaskRow移动等操作很重要,因为外部更新tasks后需要通知Timeline重新渲染
|
||||
watch(
|
||||
() => props.tasks,
|
||||
() => {
|
||||
updateTaskTrigger.value++
|
||||
// props变化时清空增量追踪,执行全量更新
|
||||
triggerFullUpdate()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@@ -2579,13 +2749,24 @@ function handleTimelineClickTask(task: Task, event: MouseEvent) {
|
||||
emit('task-click', task, event)
|
||||
}
|
||||
|
||||
// 类型守卫:判断是否为真正的Task对象(而非Resource)
|
||||
// Resource有tasks属性且没有resources属性
|
||||
function isTaskObject(obj: Task | Resource): obj is Task {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === 'object' &&
|
||||
!('tasks' in obj && Array.isArray((obj as Resource).tasks) && !('resources' in obj))
|
||||
)
|
||||
}
|
||||
|
||||
// 监听来自Timeline的任务编辑事件(双击)
|
||||
function handleTimelineEditTask(task: Task) {
|
||||
emit('task-double-click', task)
|
||||
|
||||
// 根据 useDefaultDrawer 决定是否打开内置 TaskDrawer
|
||||
// v1.9.7 只为真正的Task对象打开TaskDrawer,Resource对象不打开(Resource的id是字符串格式)
|
||||
if (props.useDefaultDrawer && typeof task.id === 'number') {
|
||||
// v1.9.7 只为真正的Task对象打开TaskDrawer,Resource对象不打开
|
||||
// 使用类型守卫和id类型双重判断,确保安全
|
||||
if (props.useDefaultDrawer && isTaskObject(task) && typeof task.id === 'number') {
|
||||
taskDrawerTask.value = task
|
||||
taskDrawerEditMode.value = true
|
||||
taskDrawerVisible.value = true
|
||||
@@ -2712,6 +2893,10 @@ const updateTaskInTree = (tasks: Task[], updatedTask: Task): boolean => {
|
||||
|
||||
// v1.9.0 更新任务并同步到资源视图
|
||||
const updateTaskAndSyncToResources = (updatedTask: Task) => {
|
||||
// v2.0 方案1:记录变更的任务和资源ID(增量更新)
|
||||
lastChangedTaskId.value = updatedTask.id
|
||||
const affectedResourceIds = new Set<string | number>()
|
||||
|
||||
// 1. 更新原始任务数据
|
||||
if (props.tasks) {
|
||||
updateTaskInTree(props.tasks, updatedTask)
|
||||
@@ -2719,30 +2904,54 @@ const updateTaskAndSyncToResources = (updatedTask: Task) => {
|
||||
|
||||
// 2. 如果是资源视图,同步更新资源中的任务
|
||||
if (currentViewMode.value === 'resource' && props.resources) {
|
||||
// 查找任务所属的资源(通过assignee)
|
||||
props.resources.forEach(resource => {
|
||||
// v2.1 性能优化:直接定位资源,避免全量遍历(O(1) vs O(n*m))
|
||||
// 先找出任务当前所在的资源(通过遍历一次,但只在找到后立即退出)
|
||||
let oldResourceId: string | number | null = null
|
||||
let oldResource: any = null
|
||||
let oldTaskIndex = -1
|
||||
|
||||
// 快速查找:只遍历到找到为止
|
||||
for (const resource of props.resources) {
|
||||
const taskIndex = resource.tasks.findIndex(t => t.id === updatedTask.id)
|
||||
if (taskIndex !== -1) {
|
||||
// 如果责任人变了,需要移动任务
|
||||
if (updatedTask.assignee && updatedTask.assignee !== resource.id) {
|
||||
// 从原资源中移除
|
||||
resource.tasks.splice(taskIndex, 1)
|
||||
|
||||
// 添加到新资源
|
||||
const newResource = props.resources.find(r => r.id === updatedTask.assignee)
|
||||
if (newResource) {
|
||||
newResource.tasks.push({ ...updatedTask })
|
||||
}
|
||||
} else {
|
||||
// 责任人未变,只更新任务数据
|
||||
resource.tasks[taskIndex] = { ...resource.tasks[taskIndex], ...updatedTask }
|
||||
}
|
||||
} else if (updatedTask.assignee === resource.id) {
|
||||
// 任务新分配给这个资源
|
||||
resource.tasks.push({ ...updatedTask })
|
||||
oldResourceId = resource.id
|
||||
oldResource = resource
|
||||
oldTaskIndex = taskIndex
|
||||
affectedResourceIds.add(resource.id)
|
||||
break // 立即退出,不再遍历剩余资源
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果找到了任务
|
||||
if (oldResource && oldTaskIndex !== -1) {
|
||||
// 如果责任人变了,需要移动任务
|
||||
if (updatedTask.assignee && updatedTask.assignee !== oldResourceId) {
|
||||
// 从原资源中移除
|
||||
oldResource.tasks.splice(oldTaskIndex, 1)
|
||||
|
||||
// 直接定位新资源(O(1))
|
||||
const newResource = props.resources.find(r => r.id === updatedTask.assignee)
|
||||
if (newResource) {
|
||||
newResource.tasks.push({ ...updatedTask })
|
||||
// v2.0 记录新资源也受影响
|
||||
affectedResourceIds.add(newResource.id)
|
||||
}
|
||||
} else {
|
||||
// 责任人未变,只更新任务数据
|
||||
oldResource.tasks[oldTaskIndex] = { ...oldResource.tasks[oldTaskIndex], ...updatedTask }
|
||||
}
|
||||
} else if (updatedTask.assignee) {
|
||||
// 任务新分配给某个资源(直接定位,不遍历)
|
||||
const targetResource = props.resources.find(r => r.id === updatedTask.assignee)
|
||||
if (targetResource) {
|
||||
targetResource.tasks.push({ ...updatedTask })
|
||||
affectedResourceIds.add(targetResource.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v2.0 更新受影响的资源ID集合
|
||||
lastChangedResourceIds.value = affectedResourceIds
|
||||
}
|
||||
|
||||
// v1.9.0 新增任务时同步到资源视图
|
||||
|
||||
@@ -2028,6 +2028,9 @@ watch(
|
||||
() => [props.isHighlighted, props.isPrimaryHighlight],
|
||||
([highlighted, primary]) => {
|
||||
if (highlighted || primary) {
|
||||
// 🔧 修复:记录是否刚刚完成了交互操作
|
||||
const wasInteracting = isDragging.value || isResizingLeft.value || isResizingRight.value
|
||||
|
||||
// 当TaskBar变为高亮状态时,立即清理所有拖拽状态和事件监听器
|
||||
// 无条件清理,即使没有正在拖拽也要重置状态
|
||||
isDragging.value = false
|
||||
@@ -2053,14 +2056,18 @@ watch(
|
||||
dragDelayTimer.value = null
|
||||
}
|
||||
|
||||
// 触发自定义事件,通知Timeline启动拖拽滚动
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('taskbar-highlighted', {
|
||||
detail: {
|
||||
taskId: props.task.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
// 🔧 修复:只有在非交互状态下才触发 Timeline 拖拽
|
||||
// 如果刚刚完成拖拽/resize,不应该启动 Timeline 拖拽
|
||||
if (!wasInteracting && !justFinishedDragOrResize.value) {
|
||||
// 触发自定义事件,通知Timeline启动拖拽滚动
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('taskbar-highlighted', {
|
||||
detail: {
|
||||
taskId: props.task.id,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import GanttConfirmDialog from './GanttConfirmDialog.vue'
|
||||
import MultiSelectPredecessor from './MultiSelectPredecessor.vue'
|
||||
import ConfirmTimerDialog from './ConfirmTimerDialog.vue'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import { Resource } from '../models/classes/Resource'
|
||||
import { createResource } from '../utils/resourceUtils'
|
||||
import '../styles/app.css'
|
||||
|
||||
interface AssigneeOption {
|
||||
@@ -682,7 +682,7 @@ const addResource = () => {
|
||||
if (!formData.resources) {
|
||||
formData.resources = []
|
||||
}
|
||||
formData.resources.push(new Resource({
|
||||
formData.resources.push(createResource({
|
||||
id: '',
|
||||
name: '',
|
||||
capacity: 100,
|
||||
@@ -731,7 +731,7 @@ const mapAssigneeToResources = () => {
|
||||
formData.resources = assignees.map((assigneeId) => {
|
||||
// 查找对应的assigneeOption获取名称
|
||||
const option = props.assigneeOptions?.find(opt => opt.value === assigneeId)
|
||||
return new Resource({
|
||||
return createResource({
|
||||
id: assigneeId,
|
||||
name: option?.label || String(assigneeId),
|
||||
capacity: 100, // 默认100%
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { StyleValue, Slots, Ref, ComputedRef } from 'vue'
|
||||
import TaskRow from './taskRow/TaskRow.vue'
|
||||
import { useI18n } from '../../composables/useI18n'
|
||||
import type { Task } from '../../models/classes/Task'
|
||||
import type { Resource } from '../../models/classes/Resource'
|
||||
import type { Resource } from '../../models/types/Resource'
|
||||
import type { TaskListConfig, TaskListColumnConfig } from '../../models/configs/TaskListConfig'
|
||||
// @ts-expect-error - ResourceListColumnConfig is used in type unions
|
||||
import type { ResourceListConfig, ResourceListColumnConfig } from '../../models/configs/ResourceListConfig'
|
||||
@@ -340,6 +340,7 @@ onUnmounted(() => {
|
||||
|
||||
<TaskRow
|
||||
v-for="{ task, level, rowIndex } in visibleTasks"
|
||||
v-memo="[task.id, task.name, task.collapsed, hoveredTaskId === task.id, task.startDate, task.endDate, task.progress]"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
:level="level"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ref, computed, inject, type Ref, type ComputedRef } from 'vue'
|
||||
import type { Task } from '../../../../models/classes/Task'
|
||||
import type { Resource } from '../../../../models/classes/Resource'
|
||||
import type { Resource } from '../../../../models/types/Resource'
|
||||
|
||||
/**
|
||||
* TaskList 布局计算逻辑
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { StyleValue } from 'vue'
|
||||
import { useI18n } from '../../../composables/useI18n'
|
||||
import { formatPredecessorDisplay } from '../../../utils/predecessorUtils'
|
||||
import type { Task } from '../../../models/classes/Task'
|
||||
// @ts-expect-error - Resource is used in type definitions
|
||||
import type { Resource } from '../../../models/classes/Resource'
|
||||
import type { Resource } from '../../../models/types/Resource'
|
||||
import { isResourceOverloaded } from '../../../utils/resourceUtils'
|
||||
import type { TaskListColumnConfig } from '../../../models/configs/TaskListConfig'
|
||||
import type { DeclarativeColumnConfig } from '../composables/taskList/useTaskListColumns'
|
||||
import TaskContextMenu from '../../TaskContextMenu.vue'
|
||||
@@ -96,16 +96,12 @@ const isResourceRow = computed(() => {
|
||||
})
|
||||
|
||||
// v1.9.0 检测资源是否超载(任务重叠)
|
||||
const isResourceOverloaded = computed(() => {
|
||||
const isResourceOverloadedComputed = computed(() => {
|
||||
if (!isResourceRow.value) return false
|
||||
|
||||
// 类型断言为Resource
|
||||
const resource = props.task as any
|
||||
if (typeof resource.isOverloaded === 'function') {
|
||||
return resource.isOverloaded()
|
||||
}
|
||||
|
||||
return false
|
||||
const resource = props.task as Resource
|
||||
return isResourceOverloaded(resource)
|
||||
})
|
||||
|
||||
// 计算行高度 - resource视图下使用动态高度
|
||||
@@ -412,7 +408,7 @@ const assigneeDisplayData = computed(() => {
|
||||
<div v-if="isResourceRow" class="resource-row-name">
|
||||
<!-- v1.9.0 资源超载警示图标 -->
|
||||
<svg
|
||||
v-if="isResourceOverloaded"
|
||||
v-if="isResourceOverloadedComputed"
|
||||
class="resource-warning-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getPredecessorIds } from '../utils/predecessorUtils'
|
||||
import { perfMonitor } from '../utils/perfMonitor'
|
||||
import { perfMonitor2 } from '../utils/perfMonitor2' // v1.9.6 性能诊断工具
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import { Resource } from '../models/classes/Resource'
|
||||
import type { Resource } from '../models/types/Resource'
|
||||
import type { Milestone } from '../models/classes/Milestone'
|
||||
import type { TimelineConfig } from '../models/configs/TimelineConfig'
|
||||
import { TimelineScale } from '../models/types/TimelineScale'
|
||||
@@ -110,6 +110,9 @@ const dataSource = inject<ComputedRef<Task[] | Resource[]>>('gantt-data-source',
|
||||
// v1.9.0 从 GanttChart 注入资源冲突信息(由 GanttChart 计算并响应 updateTaskTrigger)
|
||||
const resourceConflicts = inject<ComputedRef<Map<string, Set<number>>>>('resourceConflicts', computed(() => new Map()))
|
||||
|
||||
// TaskBar渲染key,用于在容器变化时强制重新渲染
|
||||
const taskBarRenderKey = ref(0)
|
||||
|
||||
// v1.9.2 计算资源视图中每个任务的冲突任务列表(用于显示详细冲突信息)
|
||||
const getConflictTasksForTask = (resourceId: string | number, taskId: string | number): Task[] => {
|
||||
if (viewMode.value !== 'resource') return []
|
||||
@@ -1611,7 +1614,6 @@ const getDateByScrollPosition = (scrollPosition: number): Date => {
|
||||
// v1.9.6 Sprint2(P1) - 周/月/季/年视图:使用timelineData精确计算(避免累积误差)
|
||||
const data = timelineData.value as any
|
||||
if (!data || data.length === 0) {
|
||||
console.warn(`[Sprint2-Debug] timelineData is empty for scale=${scale}, falling back to timelineStart`)
|
||||
return timelineStart
|
||||
}
|
||||
|
||||
@@ -1958,7 +1960,7 @@ const rebuildResourceTaskQueues = () => {
|
||||
// 如果已经渲染过,保留rendered=true状态
|
||||
const isRendered = existingCache?.rendered || false
|
||||
if (isRendered) {
|
||||
cachedCount++
|
||||
cachedCount = cachedCount + 1
|
||||
}
|
||||
|
||||
newCache.set(cacheKey, {
|
||||
@@ -2094,12 +2096,12 @@ const visibleResourcesWithFilteredTasks = computed(() => {
|
||||
totalFilteredTasks += renderTasks.length
|
||||
|
||||
return {
|
||||
resource: new Resource({
|
||||
resource: {
|
||||
...resource,
|
||||
tasks: renderTasks,
|
||||
// 保留原始完整的任务列表(用于GanttConflicts冲突检测)
|
||||
allTasks: originalTasks,
|
||||
}),
|
||||
},
|
||||
originalIndex,
|
||||
}
|
||||
})
|
||||
@@ -3557,9 +3559,6 @@ const taskBarPositions = shallowRef<
|
||||
Record<number, { left: number; top: number; width: number; height: number }>
|
||||
>({})
|
||||
|
||||
// TaskBar渲染key,用于在容器变化时强制重新渲染
|
||||
const taskBarRenderKey = ref(0)
|
||||
|
||||
const bodyContentRef = ref<HTMLElement | null>(null)
|
||||
// 🚀 LinkDragGuide 命令式 API 引用
|
||||
const linkDragGuideRef = ref<InstanceType<typeof LinkDragGuide> | null>(null)
|
||||
@@ -3734,10 +3733,12 @@ const handleTaskBarResizeEnd = (updatedTask: Task) => {
|
||||
const newLayout = getResourceLayout(targetResource)
|
||||
const newRowCount = newLayout.rowHeights.length
|
||||
|
||||
// 只有当行数发生变化时,才需要触发全量重绘
|
||||
if (newRowCount !== oldRowCount) {
|
||||
taskBarRenderKey.value++
|
||||
}
|
||||
// 🔧 修复:不再触发 taskBarRenderKey 更新,避免整个视图重建导致闪烁
|
||||
// 资源布局的更新会通过 resourceTaskLayouts 的响应式自动触发重新渲染
|
||||
// 注释掉以避免不必要的全量重绘:
|
||||
// if (newRowCount !== oldRowCount) {
|
||||
// taskBarRenderKey.value++
|
||||
// }
|
||||
}
|
||||
}
|
||||
// 记录变化的TaskBar ID(用于增量冲突更新)
|
||||
@@ -3988,6 +3989,27 @@ const handleTaskBarHighlighted = () => {
|
||||
// 注意:这里无法直接获取鼠标状态,所以我们添加一个全局监听器
|
||||
// 在下一次 mousemove 时启动拖拽滚动
|
||||
const handleNextMouseMove = (e: MouseEvent) => {
|
||||
// 🔧 修复:检查是否有 TaskBar 正在被拖拽或 resize
|
||||
// 如果有,不应该触发 Timeline 拖拽,避免与 TaskBar 交互冲突
|
||||
if (isDraggingTaskBar.value) {
|
||||
document.removeEventListener('mousemove', handleNextMouseMove)
|
||||
return
|
||||
}
|
||||
|
||||
// 🔧 修复:检查鼠标是否在 TaskBar、resize 手柄或其他交互元素上
|
||||
// 如果是,不应该触发 Timeline 拖拽
|
||||
const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement
|
||||
if (target) {
|
||||
const isOnTaskBar = target.closest('.task-bar')
|
||||
const isOnResizeHandle = target.closest('.resize-handle-left, .resize-handle-right')
|
||||
const isOnInteractive = target.closest('button, input, select, textarea, .custom-task-content')
|
||||
|
||||
if (isOnTaskBar || isOnResizeHandle || isOnInteractive) {
|
||||
document.removeEventListener('mousemove', handleNextMouseMove)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查鼠标左键是否按下
|
||||
if (e.buttons === 1) {
|
||||
// 启动拖拽滚动
|
||||
@@ -4097,6 +4119,13 @@ const handleMouseDown = (event: MouseEvent) => {
|
||||
|
||||
// 如果在body区域,需要检查是否点击了交互元素
|
||||
if (isInBody) {
|
||||
// 🔧 修复:检查是否点击了 TaskBar 的 resize 手柄
|
||||
const resizeHandle = target.closest('.resize-handle-left, .resize-handle-right') as HTMLElement
|
||||
if (resizeHandle) {
|
||||
// 点击了 resize 手柄,不应该触发 Timeline 拖拽
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否点击了TaskBar
|
||||
const taskBarElement = target.closest('.task-bar') as HTMLElement
|
||||
|
||||
@@ -4199,6 +4228,12 @@ const handleMouseMove = (event: MouseEvent) => {
|
||||
|
||||
// 鼠标抬起结束拖拽
|
||||
const handleMouseUp = () => {
|
||||
// 🔧 修复:只有在真正处于拖拽状态时才执行清理
|
||||
// 避免 TaskBar 的 resize 事件误触发 Timeline 的拖拽结束逻辑
|
||||
if (!isDragging.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isDragging.value = false
|
||||
|
||||
// 取消任何待处理的 RAF
|
||||
@@ -4249,6 +4284,9 @@ const handleTimelineScroll = (event: Event) => {
|
||||
// 虚拟渲染:滚动时更新 Canvas 位置(防抖处理)
|
||||
debouncedUpdateCanvasPosition()
|
||||
|
||||
// [nelson][2026-02-04]拖拽时不同步(避免性能问题)
|
||||
if (isDragging.value) return
|
||||
|
||||
// 小时视图简化处理
|
||||
if (currentTimeScale.value === TimelineScale.HOUR) {
|
||||
isScrolling.value = true
|
||||
|
||||
@@ -298,8 +298,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
|
||||
|
||||
// 重新计算冲突
|
||||
function recalculateConflicts() {
|
||||
const startTime = performance.now()
|
||||
|
||||
// 调用冲突检测算法
|
||||
const conflicts = detectConflicts(props.tasks, props.resourceId)
|
||||
|
||||
@@ -387,12 +385,6 @@ function recalculateConflicts() {
|
||||
}
|
||||
}).filter(z => z !== null) as ConflictZone[]
|
||||
|
||||
const endTime = performance.now()
|
||||
const elapsed = endTime - startTime
|
||||
|
||||
// 开发环境性能监控
|
||||
if (import.meta.env.DEV && elapsed > 50) {}
|
||||
|
||||
// v1.9.4 P1优化 - 增量重绘
|
||||
// v1.9.5 修复:如果纹理缓存被清空,使用全量重绘避免颜色变淡
|
||||
if (!texturePatterns.value.light && !texturePatterns.value.medium && !texturePatterns.value.severe) {
|
||||
|
||||
@@ -11,7 +11,8 @@ export { default as MilestonePoint } from './components/MilestonePoint.vue'
|
||||
export { default as MilestoneDialog } from './components/MilestoneDialog.vue'
|
||||
export { default as TaskRow } from './components/TaskList/taskRow/TaskRow.vue'
|
||||
export type { Task } from './models/classes/Task.ts' // 导出Task类型
|
||||
export { Resource } from './models/classes/Resource.ts' // v1.9.0 导出Resource类
|
||||
export type { Resource } from './models/types/Resource.ts' // v1.9.0 导出Resource类型
|
||||
export { createResource } from './utils/resourceUtils.ts' // v2.0.0 导出资源创建工厂函数
|
||||
export { useMessage } from './composables/useMessage.ts' // 导出useMessage组合式函数
|
||||
|
||||
// 导出 composables 供外部使用
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { Task } from './Task'
|
||||
import { detectConflicts } from '../../utils/conflictUtils'
|
||||
|
||||
/**
|
||||
* 资源类 (Resource Class)
|
||||
* 用于资源计划视图,代表可分配的人力或设备资源
|
||||
*
|
||||
* @version 1.9.0
|
||||
* @description 支持资源视图的核心数据模型,每个资源可以关联多个任务
|
||||
*/
|
||||
export class Resource {
|
||||
id: string | number
|
||||
name: string
|
||||
type?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
department?: string
|
||||
skills?: string[]
|
||||
capacity?: number
|
||||
color?: string // 自定义资源行左边框颜色,如 '#ff5733',若不设置则使用默认颜色方案
|
||||
tasks: Task[]
|
||||
[key: string]: unknown
|
||||
|
||||
constructor(data: {
|
||||
id: string | number
|
||||
name: string
|
||||
type?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
department?: string
|
||||
skills?: string[]
|
||||
capacity?: number
|
||||
color?: string
|
||||
tasks?: Task[]
|
||||
[key: string]: unknown
|
||||
}) {
|
||||
this.id = data.id
|
||||
this.name = data.name
|
||||
this.type = data.type
|
||||
this.avatar = data.avatar
|
||||
this.description = data.description
|
||||
this.department = data.department
|
||||
this.skills = data.skills
|
||||
this.capacity = data.capacity
|
||||
this.color = data.color
|
||||
this.tasks = data.tasks || []
|
||||
|
||||
// 复制其他自定义属性
|
||||
Object.keys(data).forEach(key => {
|
||||
if (!(key in this)) {
|
||||
this[key] = data[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任务到资源
|
||||
*/
|
||||
addTask(task: Task): void {
|
||||
if (!this.tasks.find(t => t.id === task.id)) {
|
||||
this.tasks.push(task)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从资源中移除任务
|
||||
*/
|
||||
removeTask(taskId: number | string): void {
|
||||
this.tasks = this.tasks.filter(t => t.id !== taskId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务数量
|
||||
*/
|
||||
getTaskCount(): number {
|
||||
return this.tasks.length
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算资源利用率(基于任务时间重叠)
|
||||
* @returns 利用率百分比 (0-100)
|
||||
*/
|
||||
calculateUtilization(): number {
|
||||
if (this.tasks.length === 0) return 0
|
||||
const baseUtilization = Math.min(this.tasks.length * 20, 100)
|
||||
return Math.round(baseUtilization)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新利用率
|
||||
*/
|
||||
updateUtilization(): void {
|
||||
this.capacity = this.calculateUtilization()
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测任务是否存在时间重叠
|
||||
* @returns 是否存在任务重叠
|
||||
*/
|
||||
hasTaskOverlap(): boolean {
|
||||
if (this.tasks.length < 2) return false
|
||||
|
||||
// 过滤掉没有开始日期和结束日期的任务
|
||||
const validTasks = this.tasks.filter(task => task.startDate && task.endDate)
|
||||
if (validTasks.length < 2) return false
|
||||
|
||||
// 按开始日期排序
|
||||
const sortedTasks = [...validTasks].sort((a, b) => {
|
||||
const dateA = new Date(a.startDate!).getTime()
|
||||
const dateB = new Date(b.startDate!).getTime()
|
||||
return dateA - dateB
|
||||
})
|
||||
|
||||
// 检测相邻任务是否重叠
|
||||
for (let i = 0; i < sortedTasks.length - 1; i++) {
|
||||
const currentTask = sortedTasks[i]
|
||||
const nextTask = sortedTasks[i + 1]
|
||||
|
||||
const currentEnd = new Date(currentTask.endDate!).getTime()
|
||||
const nextStart = new Date(nextTask.startDate!).getTime()
|
||||
|
||||
// 如果当前任务的结束时间晚于下一个任务的开始时间,则存在重叠
|
||||
if (currentEnd > nextStart) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.9.0 检测资源是否超载(基于占用比例)
|
||||
* 超载定义:同一时间段内,资源总占用比例 > 100%
|
||||
* v1.9.9 修复:使用 detectConflicts 函数来正确检测多任务叠加的超载
|
||||
* @returns 是否资源超载
|
||||
*/
|
||||
isOverloaded(): boolean {
|
||||
if (this.tasks.length < 2) return false
|
||||
|
||||
// 使用 conflictUtils 的 detectConflicts 函数来检测冲突
|
||||
// 这个函数能正确处理多任务叠加的超载情况(如 A:40% + B:40% + C:30% = 110%)
|
||||
const conflictZones = detectConflicts(this.tasks, this.id)
|
||||
|
||||
return conflictZones.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.9.0 获取任务中当前资源的利用率
|
||||
* @param task 任务对象
|
||||
* @returns 占比百分比 (20-100),默认100
|
||||
*/
|
||||
getTaskAllocationPercent(task: any): number {
|
||||
if (!task.resources || !Array.isArray(task.resources)) {
|
||||
return 100 // 未配置resources时,默认100%
|
||||
}
|
||||
|
||||
const allocation = task.resources.find((r: any) => r.id === this.id)
|
||||
if (!allocation) {
|
||||
return 100 // 未找到当前资源的分配信息,默认100%
|
||||
}
|
||||
|
||||
const capacity = allocation.capacity ?? 100
|
||||
return Math.max(20, Math.min(100, capacity)) // 限制范围 20-100
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Resource } from './Resource'// Task 类型定义
|
||||
import type { Resource } from '../types/Resource'
|
||||
|
||||
// Task 类型定义
|
||||
export interface Task {
|
||||
id: number
|
||||
name: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Resource } from '../classes/Resource'
|
||||
import type { Resource } from '../types/Resource'
|
||||
|
||||
/**
|
||||
* 资源列表列类型枚举
|
||||
|
||||
23
src/models/types/Resource.ts
Normal file
23
src/models/types/Resource.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Task } from '../classes/Task'
|
||||
|
||||
/**
|
||||
* 资源接口 (Resource Interface)
|
||||
* 用于资源计划视图,代表可分配的人力或设备资源
|
||||
*
|
||||
* @version 1.9.0
|
||||
* @version 2.0.0 - 从 class 重构为 interface,方法迁移至 resourceUtils.ts
|
||||
* @description 支持资源视图的核心数据模型,每个资源可以关联多个任务
|
||||
*/
|
||||
export interface Resource {
|
||||
id: string | number
|
||||
name: string
|
||||
type?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
department?: string
|
||||
skills?: string[]
|
||||
capacity?: number
|
||||
color?: string // 自定义资源行左边框颜色,如 '#ff5733',若不设置则使用默认颜色方案
|
||||
tasks: Task[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import type { Resource } from '../models/classes/Resource'
|
||||
import type { Resource } from '../models/types/Resource'
|
||||
import { detectConflicts } from './conflictUtils'
|
||||
|
||||
/**
|
||||
* 检测两个任务的时间段是否重叠
|
||||
@@ -91,15 +92,31 @@ export function detectAllResourceConflicts(
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算资源利用率(简化版:任务时长累加 / 时间窗口)
|
||||
* 计算资源利用率(基于任务数量的简化计算)
|
||||
* @param resource 资源对象
|
||||
* @returns 利用率百分比 (0-100)
|
||||
*/
|
||||
function calculateSimpleResourceUtilization(resource: Resource): number {
|
||||
if (resource.tasks.length === 0) return 0
|
||||
const baseUtilization = Math.min(resource.tasks.length * 20, 100)
|
||||
return Math.round(baseUtilization)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算资源利用率(时间窗口版:任务时长累加 / 时间窗口)
|
||||
* @param resource 资源对象
|
||||
* @param timeWindowDays 时间窗口(天数),默认30天
|
||||
* @returns 利用率百分比(0-100+)
|
||||
*/
|
||||
export function calculateResourceUtilization(
|
||||
resource: Resource,
|
||||
timeWindowDays = 30,
|
||||
timeWindowDays?: number,
|
||||
): number {
|
||||
// 如果没有指定时间窗口,使用简化计算
|
||||
if (timeWindowDays === undefined) {
|
||||
return calculateSimpleResourceUtilization(resource)
|
||||
}
|
||||
|
||||
const tasks = resource.tasks || []
|
||||
if (tasks.length === 0) return 0
|
||||
|
||||
@@ -120,3 +137,153 @@ export function calculateResourceUtilization(
|
||||
|
||||
return availableHours > 0 ? Math.round((totalTaskHours / availableHours) * 100) : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源对象(工厂函数)
|
||||
* @param data 资源数据
|
||||
* @returns Resource 对象
|
||||
*/
|
||||
export function createResource(data: {
|
||||
id: string | number
|
||||
name: string
|
||||
type?: string
|
||||
avatar?: string
|
||||
description?: string
|
||||
department?: string
|
||||
skills?: string[]
|
||||
capacity?: number
|
||||
color?: string
|
||||
tasks?: Task[]
|
||||
[key: string]: unknown
|
||||
}): Resource {
|
||||
const resource: Resource = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
avatar: data.avatar,
|
||||
description: data.description,
|
||||
department: data.department,
|
||||
skills: data.skills,
|
||||
capacity: data.capacity,
|
||||
color: data.color,
|
||||
tasks: data.tasks || [],
|
||||
}
|
||||
|
||||
// 复制其他自定义属性
|
||||
Object.keys(data).forEach(key => {
|
||||
if (!(key in resource)) {
|
||||
resource[key] = data[key]
|
||||
}
|
||||
})
|
||||
|
||||
return resource
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加任务到资源
|
||||
* @param resource 资源对象
|
||||
* @param task 任务对象
|
||||
*/
|
||||
export function addTaskToResource(resource: Resource, task: Task): void {
|
||||
if (!resource.tasks.find(t => t.id === task.id)) {
|
||||
resource.tasks.push(task)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从资源中移除任务
|
||||
* @param resource 资源对象
|
||||
* @param taskId 任务ID
|
||||
*/
|
||||
export function removeTaskFromResource(resource: Resource, taskId: number | string): void {
|
||||
resource.tasks = resource.tasks.filter(t => t.id !== taskId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源的任务数量
|
||||
* @param resource 资源对象
|
||||
* @returns 任务数量
|
||||
*/
|
||||
export function getResourceTaskCount(resource: Resource): number {
|
||||
return resource.tasks.length
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新资源的利用率
|
||||
* @param resource 资源对象
|
||||
*/
|
||||
export function updateResourceUtilization(resource: Resource): void {
|
||||
resource.capacity = calculateResourceUtilization(resource)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测资源的任务是否存在时间重叠
|
||||
* @param resource 资源对象
|
||||
* @returns 是否存在任务重叠
|
||||
*/
|
||||
export function hasResourceTaskOverlap(resource: Resource): boolean {
|
||||
if (resource.tasks.length < 2) return false
|
||||
|
||||
// 过滤掉没有开始日期和结束日期的任务
|
||||
const validTasks = resource.tasks.filter(task => task.startDate && task.endDate)
|
||||
if (validTasks.length < 2) return false
|
||||
|
||||
// 按开始日期排序
|
||||
const sortedTasks = [...validTasks].sort((a, b) => {
|
||||
const dateA = new Date(a.startDate!).getTime()
|
||||
const dateB = new Date(b.startDate!).getTime()
|
||||
return dateA - dateB
|
||||
})
|
||||
|
||||
// 检测相邻任务是否重叠
|
||||
for (let i = 0; i < sortedTasks.length - 1; i++) {
|
||||
const currentTask = sortedTasks[i]
|
||||
const nextTask = sortedTasks[i + 1]
|
||||
|
||||
const currentEnd = new Date(currentTask.endDate!).getTime()
|
||||
const nextStart = new Date(nextTask.startDate!).getTime()
|
||||
|
||||
// 如果当前任务的结束时间晚于下一个任务的开始时间,则存在重叠
|
||||
if (currentEnd > nextStart) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测资源是否超载(基于占用比例)
|
||||
* 超载定义:同一时间段内,资源总占用比例 > 100%
|
||||
* @param resource 资源对象
|
||||
* @returns 是否资源超载
|
||||
*/
|
||||
export function isResourceOverloaded(resource: Resource): boolean {
|
||||
if (resource.tasks.length < 2) return false
|
||||
|
||||
// 使用 conflictUtils 的 detectConflicts 函数来检测冲突
|
||||
// 这个函数能正确处理多任务叠加的超载情况(如 A:40% + B:40% + C:30% = 110%)
|
||||
const conflictZones = detectConflicts(resource.tasks, resource.id)
|
||||
|
||||
return conflictZones.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务中当前资源的利用率
|
||||
* @param resource 资源对象
|
||||
* @param task 任务对象
|
||||
* @returns 占比百分比 (20-100),默认100
|
||||
*/
|
||||
export function getTaskAllocationPercent(resource: Resource, task: any): number {
|
||||
if (!task.resources || !Array.isArray(task.resources)) {
|
||||
return 100 // 未配置resources时,默认100%
|
||||
}
|
||||
|
||||
const allocation = task.resources.find((r: any) => r.id === resource.id)
|
||||
if (!allocation) {
|
||||
return 100 // 未找到当前资源的分配信息,默认100%
|
||||
}
|
||||
|
||||
const capacity = allocation.capacity ?? 100
|
||||
return Math.max(20, Math.min(100, capacity)) // 限制范围 20-100
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user