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