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

View File

@@ -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

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 新增任务时同步到资源视图

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,
},
}),
)
}
}
},
)

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%

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"

View File

@@ -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 布局计算逻辑

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"

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()))
// 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,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 }>
>({})
// 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

View File

@@ -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) {

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 供外部使用

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
}
}

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

View File

@@ -1,4 +1,4 @@
import type { Resource } from '../classes/Resource'
import type { Resource } from '../types/Resource'
/**
* 资源列表列类型枚举

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
}

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
}