v1.9.0-rc.7 - SIT bugfix

This commit is contained in:
LINING-PC\lining
2026-02-05 13:19:08 +08:00
parent 6e727fea3f
commit 2c215b1e36
15 changed files with 579 additions and 294 deletions
+270 -61
View File
@@ -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对象打开TaskDrawerResource对象不打开Resource的id是字符串格式)
if (props.useDefaultDrawer && typeof task.id === 'number') {
// v1.9.7 只为真正的Task对象打开TaskDrawerResource对象不打开
// 使用类型守卫和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 新增任务时同步到资源视图
+15 -8
View File
@@ -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,
},
}),
)
}
}
},
)
+3 -3
View File
@@ -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%
+2 -1
View File
@@ -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
+6 -10
View File
@@ -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"
+54 -16
View File
@@ -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()))
// TaskBarkey
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,17 +1960,17 @@ const rebuildResourceTaskQueues = () => {
// rendered=true
const isRendered = existingCache?.rendered || false
if (isRendered) {
cachedCount++
cachedCount = cachedCount + 1
}
newCache.set(cacheKey, {
taskId,
resourceId,
rendered: isRendered,
timestamp: isRendered ?
(existingCache ?
existingCache.timestamp
: currentTimestamp)
timestamp: isRendered ?
(existingCache ?
existingCache.timestamp
: currentTimestamp)
: currentTimestamp,
})
})
@@ -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 }>
>({})
// TaskBarkey
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
}
// 🔧 TaskBarresize
// 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) {
+2 -1
View File
@@ -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 供外部使用
-165
View File
@@ -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
}
}
+3 -1
View File
@@ -1,4 +1,6 @@
import { Resource } from './Resource'// Task 类型定义
import type { Resource } from '../types/Resource'
// Task 类型定义
export interface Task {
id: number
name: string
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Resource } from '../classes/Resource'
import type { Resource } from '../types/Resource'
/**
*
+23
View 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
}
+170 -3
View File
@@ -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
}