v1.9.0-rc.6 - SIT bugfix

This commit is contained in:
LINING-PC\lining
2026-02-03 21:51:10 +08:00
parent 9d5cdef835
commit 6e727fea3f
30 changed files with 3377 additions and 2984 deletions
+50 -8
View File
@@ -93,6 +93,7 @@ const emit = defineEmits([
'task-updated',
'task-collapse-change', // 任务折叠状态变化事件
'link-deleted', // 链接删除事件
'view-mode-changed', // 视图模式变化事件
// 工具栏事件
'add-task',
'add-milestone',
@@ -106,8 +107,6 @@ const emit = defineEmits([
// TaskRow拖拽事件
'task-row-moved',
// v1.9.0 资源视图事件
'view-mode-change', // 视图模式切换事件
'resource-click', // 资源行点击事件
'taskbar-resource-change', // 任务跨资源移动事件
'resource-drag-end', // v1.9.0 资源视图垂直拖拽结束事件
])
@@ -152,7 +151,7 @@ const resourceTaskLayouts = computed(() => {
resources.forEach(resource => {
const resourceId = String(resource.id)
if (resource.tasks && resource.tasks.length > 0) {
const layout = assignTaskRows(resource.tasks, resourceId, baseRowHeight)
const layout = assignTaskRows(resource.tasks, baseRowHeight)
layouts.set(resourceId, layout)
} else {
// 没有任务的资源使用默认高度
@@ -195,7 +194,7 @@ const resourceConflicts = computed(() => {
if (currentViewMode.value !== 'resource') return new Map()
const resources = currentDataSource.value as Resource[]
const conflictsMap = new Map<string, Set<number>>()
const conflictsMap = new Map<string, Set<number | string>>()
// 依赖 updateTaskTrigger 以便在任务更新时重新计算冲突
if (updateTaskTrigger.value >= 0) {
@@ -208,7 +207,7 @@ const resourceConflicts = computed(() => {
const conflictZones = detectConflicts(tasks, resource.id)
if (conflictZones.length > 0) {
const conflicts = new Set<number>()
const conflicts = new Set<number | string>()
// 收集所有冲突区域中涉及的任务ID
conflictZones.forEach(zone => {
@@ -548,7 +547,8 @@ const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
const handleViewModeChange = (newMode: 'task' | 'resource') => {
if (currentViewMode.value !== newMode) {
currentViewMode.value = newMode
emit('view-mode-change', newMode)
// v1.9.7 emit视图模式变化事件,让应用层能够同步状态
emit('view-mode-changed', newMode)
}
}
@@ -558,6 +558,8 @@ watch(
newMode => {
if (newMode && currentViewMode.value !== newMode) {
currentViewMode.value = newMode
// v1.9.7 emit视图模式变化事件
emit('view-mode-changed', newMode)
}
},
)
@@ -2525,6 +2527,7 @@ function handleToolbarAddTask() {
description: '',
parentId: undefined,
children: [],
resources: [],
}
taskDrawerTask.value = newTask
taskDrawerEditMode.value = false
@@ -2581,7 +2584,8 @@ function handleTimelineEditTask(task: Task) {
emit('task-double-click', task)
// 根据 useDefaultDrawer 决定是否打开内置 TaskDrawer
if (props.useDefaultDrawer) {
// v1.9.7 只为真正的Task对象打开TaskDrawerResource对象不打开(Resource的id是字符串格式)
if (props.useDefaultDrawer && typeof task.id === 'number') {
taskDrawerTask.value = task
taskDrawerEditMode.value = true
taskDrawerVisible.value = true
@@ -2614,6 +2618,7 @@ function handleAddPredecessor(targetTask: Task) {
description: '',
parentId: targetTask.parentId,
children: [],
resources: [],
isTimerRunning: false,
timerStartTime: undefined,
timerEndTime: undefined,
@@ -2648,6 +2653,7 @@ function handleAddSuccessor(targetTask: Task) {
description: '',
parentId: targetTask.parentId,
children: [],
resources: [],
isTimerRunning: false,
timerStartTime: undefined,
timerEndTime: undefined,
@@ -2739,6 +2745,40 @@ const updateTaskAndSyncToResources = (updatedTask: Task) => {
}
}
// v1.9.0 新增任务时同步到资源视图
const addTaskToResource = (newTask: Task) => {
// 只在资源视图模式下处理
if (currentViewMode.value !== 'resource' || !props.resources) {
return
}
// 1. 优先处理 resources 字段(支持多资源分配)
if (newTask.resources && newTask.resources.length > 0) {
newTask.resources.forEach(resourceAlloc => {
const resource = props.resources.find(r => r.id === resourceAlloc.id)
if (resource) {
// 避免重复添加
const exists = resource.tasks.find(t => t.id === newTask.id)
if (!exists) {
resource.tasks.push({ ...newTask })
}
}
})
}
// 2. 兼容 assignee 字段(单资源分配)
else if (newTask.assignee) {
const assigneeId = Array.isArray(newTask.assignee) ? newTask.assignee[0] : newTask.assignee
const resource = props.resources.find(r => r.id === assigneeId)
if (resource) {
// 避免重复添加
const exists = resource.tasks.find(t => t.id === newTask.id)
if (!exists) {
resource.tasks.push({ ...newTask })
}
}
}
}
// v1.9.0 在任务树中更新任务状态(用于timer等部分更新)
const updateTaskStateInTree = (taskId: number, updateFn: (task: Task) => void): boolean => {
const updateInList = (tasks: Task[]): boolean => {
@@ -2779,6 +2819,9 @@ function handleTaskDrawerSubmit(task: Task) {
if (props.tasks) {
insertTask(props.tasks, task)
}
// v1.9.0 新增任务时同步到资源视图
addTaskToResource(task)
// emit 新增任务事件
emit('task-added', { task })
if (taskToAddPredecessorTo.value) {
@@ -2984,7 +3027,6 @@ defineExpose({
@add-milestone="milestoneAddHandler"
@expand-all="handleExpandAll"
@collapse-all="handleCollapseAll"
@view-mode-change="handleViewModeChange"
/>
<!-- 甘特图主体 -->
+3 -4
View File
@@ -227,14 +227,13 @@ const handleCollapseAll = () => {
}
}
// v1.9.0 视图模式切换处理
// v1.9.0 视图模式切换处理 - 通过回调直接更新父组件状态
const handleViewModeChange = (mode: 'task' | 'resource') => {
if (currentViewMode.value !== mode) {
currentViewMode.value = mode
// 通过回调通知 GanttChart 更新内部状态
if (props.onViewModeChange && typeof props.onViewModeChange === 'function') {
props.onViewModeChange(mode)
} else {
emit('view-mode-change', mode)
}
}
}
@@ -608,7 +607,7 @@ onUnmounted(() => {
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
{{ t('resourceView') || '资源视图' }}
{{ t('resourceView.desc') || '资源视图' }}
</button>
</div>
+15 -22
View File
@@ -64,8 +64,8 @@ const resourcePercent = computed(() => {
return String(r.id) === String(props.currentResourceId)
})
if (allocation && allocation.percent !== undefined) {
const val = Number(allocation.percent)
if (allocation && allocation.capacity !== undefined) {
const val = Number(allocation.capacity)
if (Number.isFinite(val) && val >= 0) {
return Math.max(0, Math.min(100, val))
}
@@ -87,13 +87,6 @@ const currentResourceColor = computed(() => {
return '#85ce61'
})
// v1.9.2 当前资源总负载(用于超载警告)
const currentResourceTotalLoad = computed(() => {
// 这个值应该从外部传入,这里暂时返回undefined
// 实际应该在Timeline层计算好并通过props传递
return undefined
})
// v1.9.2 当前资源名称
const currentResourceName = computed(() => {
if (!props.currentResourceId) return ''
@@ -3357,7 +3350,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
'--row-height': `${rowHeight}px` /* 传递行高给CSS变量 */,
'--handle-width': `${actualHandleWidth}px` /* 传递手柄宽度给CSS变量 */,
'--parent-color': taskStatus.color, /* 传递父级TaskBar颜色给伪元素箭头使用 */
'--allocation-percent': (Number.isFinite(resourcePercent) ? resourcePercent / 100 : 1), /* v1.9.1 传递占比给CSS变量 */
'--allocation-capacity': (Number.isFinite(resourcePercent) ? resourcePercent / 100 : 1), /* v1.9.1 传递占比给CSS变量 */
'--task-bar-bg-color': taskStatus.bgColor, /* v1.9.1 传递背景色给伪元素 */
'--task-bar-border-color': dynamicBorderColor, /* v1.9.2 使用动态边框颜色 */
boxShadow: isParent
@@ -3410,7 +3403,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
:task="task"
:current-resource-id="currentResourceId"
:resource-color="currentResourceColor"
:resource-percent="resourcePercent"
:resource-capacity="resourcePercent"
:resource-name="currentResourceName"
:task-bar-width="taskBarWidth"
:task-bar-left="taskBarLeft"
@@ -3505,7 +3498,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
<div v-else class="task-name">
{{ task.name }}
<!-- v1.9.0 资源视图:显示占比文字 -->
<span v-if="shouldShowPercentText" class="resource-percent-text">
<span v-if="shouldShowPercentText" class="resource-capacity-text">
{{ resourcePercent }}%
</span>
</div>
@@ -3638,7 +3631,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
<div class="tooltip-content">
<!-- v1.9.0 资源视图:显示利用率 -->
<div v-if="viewMode === 'resource' && resourcePercent < 100" class="tooltip-row">
<span class="tooltip-label">{{ t('investment') || '投入' }}:</span>
<span class="tooltip-label">{{ t('resourceView.capacity') || '利用率' }}:</span>
<span class="tooltip-value">{{ resourcePercent }}%</span>
</div>
<div class="tooltip-row">
@@ -3663,7 +3656,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
</div>
<!-- v1.9.0 资源冲突警告 -->
<div v-if="props.hasResourceConflict" class="tooltip-row tooltip-warning">
<span class="tooltip-label">⚠️ {{ t('resourceOverloaded') || '资源超负荷' }}</span>
<span class="tooltip-label">⚠️ {{ t('resourceView.overloaded') || '资源超负荷' }}</span>
</div>
</div>
</div>
@@ -3849,7 +3842,7 @@ class="hover-tooltip-arrow" :style="{
top: 0;
left: 0;
right: 0;
height: calc((1 - var(--allocation-percent, 1)) * 100%);
height: calc((1 - var(--allocation-capacity, 1)) * 100%);
border-top: 1.5px dashed currentColor;
border-left: 1.5px dashed currentColor;
border-right: 1.5px dashed currentColor;
@@ -3863,7 +3856,7 @@ class="hover-tooltip-arrow" :style="{
}
/* 占比100%时,隐藏上半部分镂空区域 */
.task-bar.resource-view[style*="--allocation-percent: 1"]::before {
.task-bar.resource-view[style*="--allocation-capacity: 1"]::before {
display: none;
}
@@ -3874,7 +3867,7 @@ class="hover-tooltip-arrow" :style="{
left: 0;
right: 0;
bottom: 0;
height: calc(var(--allocation-percent, 1) * 100%);
height: calc(var(--allocation-capacity, 1) * 100%);
background: var(--task-bar-bg-color, #e3f2fd);
/*border: 1px solid var(--task-bar-border-color, #90caf9);*/
border-radius: 0 0 4px 4px;
@@ -3885,7 +3878,7 @@ class="hover-tooltip-arrow" :style="{
}
/* 占比100%时,整个TaskBar都是实心,四个角圆角 */
.task-bar.resource-view[style*="--allocation-percent: 1"]::after {
.task-bar.resource-view[style*="--allocation-capacity: 1"]::after {
border-radius: 4px;
}
@@ -3895,7 +3888,7 @@ class="hover-tooltip-arrow" :style="{
bottom: 0;
left: 0;
top: auto;
height: calc(var(--allocation-percent, 1) * 100%);
height: calc(var(--allocation-capacity, 1) * 100%);
z-index: 1;
border-radius: 0 0 4px 4px;
pointer-events: none;
@@ -3903,7 +3896,7 @@ class="hover-tooltip-arrow" :style="{
}
/* 占比100%时,进度条四个角圆角 */
.task-bar.resource-view[style*="--allocation-percent: 1"] .progress-bar {
.task-bar.resource-view[style*="--allocation-capacity: 1"] .progress-bar {
border-radius: 4px;
}
@@ -4380,7 +4373,7 @@ class="hover-tooltip-arrow" :style="{
}
/* v1.9.0 资源占比文字样式 */
.resource-percent-text {
.resource-capacity-text {
display: inline-block;
margin-left: 6px;
font-size: 12px;
@@ -4399,7 +4392,7 @@ class="hover-tooltip-arrow" :style="{
.task-bar[style*="width: 28px"],
.task-bar[style*="width: 32px"],
.task-bar[style*="width: 36px"] {
.resource-percent-text {
.resource-capacity-text {
display: none;
}
}
+26 -22
View File
@@ -7,6 +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 '../styles/app.css'
interface AssigneeOption {
@@ -197,7 +198,7 @@ const getTaskTypeDisplay = (type: string): string => {
const progressSliderStyle = computed(() => {
const progressPercent = formData.progress || 0
return {
'--progress-percent': `${progressPercent}%`,
'--progress-capacity': `${progressPercent}%`,
}
})
@@ -500,6 +501,7 @@ const handleSubmit = async () => {
const taskData: Task = {
...formData,
id: props.isEdit && props.task ? props.task.id : Date.now(),
resources: formData.resources, // resources
}
emit('submit', taskData)
handleClose()
@@ -680,11 +682,12 @@ const addResource = () => {
if (!formData.resources) {
formData.resources = []
}
formData.resources.push({
formData.resources.push(new Resource({
id: '',
name: '',
percent: 100,
})
capacity: 100,
tasks: [],
}))
}
const removeResource = (index: number) => {
@@ -693,7 +696,7 @@ const removeResource = (index: number) => {
}
}
const handleResourceChange = (index: number, field: 'id' | 'percent', value: string | number) => {
const handleResourceChange = (index: number, field: 'id' | 'capacity', value: string | number) => {
if (!formData.resources || !formData.resources[index]) return
if (field === 'id') {
@@ -702,12 +705,12 @@ const handleResourceChange = (index: number, field: 'id' | 'percent', value: str
formData.resources[index].id = selected.value
formData.resources[index].name = selected.label
}
} else if (field === 'percent') {
let percent = typeof value === 'string' ? parseInt(value) : value
} else if (field === 'capacity') {
let capacity = typeof value === 'string' ? parseInt(value) : value
// 20-100
if (percent < 20) percent = 20
if (percent > 100) percent = 100
formData.resources[index].percent = percent
if (capacity < 20) capacity = 20
if (capacity > 100) capacity = 100
formData.resources[index].capacity = capacity
}
}
@@ -728,11 +731,12 @@ const mapAssigneeToResources = () => {
formData.resources = assignees.map((assigneeId) => {
// assigneeOption
const option = props.assigneeOptions?.find(opt => opt.value === assigneeId)
return {
return new Resource({
id: assigneeId,
name: option?.label || String(assigneeId),
percent: 100, // 100%
}
capacity: 100, // 100%
tasks: [],
})
})
}
@@ -1026,9 +1030,9 @@ const taskStatus = computed(() => {
</select>
<select
v-model.number="resource.percent"
class="form-select percent-select"
@change="handleResourceChange(index, 'percent', ($event.target as HTMLSelectElement).value)"
v-model.number="resource.capacity"
class="form-select capacity-select"
@change="handleResourceChange(index, 'capacity', ($event.target as HTMLSelectElement).value)"
>
<option :value="25">25%</option>
<option :value="50">50%</option>
@@ -1474,8 +1478,8 @@ const taskStatus = computed(() => {
background: linear-gradient(
to right,
var(--gantt-primary, #409eff) 0%,
var(--gantt-primary, #409eff) var(--progress-percent, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-percent, 0%),
var(--gantt-primary, #409eff) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) 100%
);
}
@@ -1487,8 +1491,8 @@ const taskStatus = computed(() => {
background: linear-gradient(
to right,
var(--gantt-primary, #409eff) 0%,
var(--gantt-primary, #409eff) var(--progress-percent, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-percent, 0%),
var(--gantt-primary, #409eff) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) var(--progress-capacity, 0%),
var(--gantt-border-light, #e4e7ed) 100%
);
border: none;
@@ -1508,7 +1512,7 @@ const taskStatus = computed(() => {
top: 0;
left: 0;
height: 6px;
width: var(--progress-percent, 0%);
width: var(--progress-capacity, 0%);
background: var(--gantt-primary, #409eff);
border-radius: 3px;
pointer-events: none;
@@ -1769,7 +1773,7 @@ const taskStatus = computed(() => {
padding: 8px 12px;
}
.percent-select {
.capacity-select {
width: 110px;
height: 36px;
flex-shrink: 0;
+50 -103
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 type { Resource } from '../models/classes/Resource'
import { Resource } from '../models/classes/Resource'
import type { Milestone } from '../models/classes/Milestone'
import type { TimelineConfig } from '../models/configs/TimelineConfig'
import { TimelineScale } from '../models/types/TimelineScale'
@@ -115,7 +115,7 @@ const getConflictTasksForTask = (resourceId: string | number, taskId: string | n
if (viewMode.value !== 'resource') return []
const conflictTaskIds = resourceConflicts.value.get(String(resourceId))
if (!conflictTaskIds || !conflictTaskIds.has(taskId)) return []
if (!conflictTaskIds || !conflictTaskIds.has(Number(taskId))) return []
const resources = dataSource.value as Resource[]
const resource = resources.find(r => String(r.id) === String(resourceId))
@@ -180,7 +180,6 @@ const layoutCache = new Map<string, {
let resourceTaskLayoutsCallCount = 0
const resourceTaskLayouts = computed(() => {
resourceTaskLayoutsCallCount++
const startTime = performance.now()
const layoutMap = new Map<string | number, {
taskRowMap: Map<string | number, number>,
@@ -220,14 +219,6 @@ const resourceTaskLayouts = computed(() => {
}
})
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
//
if (resources.length > 0) {
const hitRate = ((cacheHits / resources.length) * 100).toFixed(1)
}
return layoutMap
})
@@ -258,7 +249,7 @@ const getResourceLayout = (resource: Resource) => {
//
let result
if (resourceTasks.length > 0) {
result = assignTaskRows(resourceTasks, resource.id, 51)
result = assignTaskRows(resourceTasks, 51)
} else {
result = {
taskRowMap: new Map(),
@@ -275,7 +266,6 @@ const getResourceLayout = (resource: Resource) => {
let resourceRowPositionsCallCount = 0
const resourceRowPositions = computed(() => {
resourceRowPositionsCallCount++
const startTime = performance.now()
const positions = new Map<string | number, number>()
if (viewMode.value !== 'resource') {
@@ -305,7 +295,6 @@ const resourceRowPositions = computed(() => {
//
if (cumulativeTop > scrollBottom + ROW_HEIGHT * 20) {
// 51px
const remainingResources = resources.length - processedCount
for (let i = processedCount; i < resources.length; i++) {
positions.set(resources[i].id, cumulativeTop) //
cumulativeTop += 51 //
@@ -314,9 +303,6 @@ const resourceRowPositions = computed(() => {
}
}
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return positions
})
@@ -1599,12 +1585,6 @@ const visibleTimeRange = computed(() => {
//
const endDate = getDateByScrollPosition(scrollLeft + containerWidth + bufferWidth)
// v1.9.6 Sprint2(P1) -
if (visibleTimeRangeCallCount % 10 === 0) {
const scale = currentTimeScale.value
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
}
return { startDate, endDate }
})
@@ -1724,7 +1704,6 @@ const getDateByScrollPosition = (scrollPosition: number): Date => {
let visibleTaskRangeCallCount = 0
const visibleTaskRange = computed(() => {
visibleTaskRangeCallCount++
const startTime = performance.now()
const scrollTop = timelineBodyScrollTop.value
const containerHeight = timelineBodyHeight.value || 600
@@ -1762,9 +1741,6 @@ const visibleTaskRange = computed(() => {
}
}
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return {
startIndex: Math.max(0, startIndex),
endIndex: Math.min(resources.length, endIndex),
@@ -1856,8 +1832,6 @@ const scheduleResourceBatchRender = () => {
const frameStartTime = performance.now()
const limits = new Map(resourceTaskRenderLimits.value)
let hasMore = false
let hasBackground = false
let processedInFrame = 0
resourceTaskQueues.value.forEach((queue, resourceId) => {
//
@@ -1868,19 +1842,12 @@ const scheduleResourceBatchRender = () => {
const current = limits.get(resourceId) || 0
// v1.9.6 Sprint4 - 使 visibleCount background
// queue.totalCount queue.visibleCount rebuildResourceTaskQueues backgroundTasks
const phaseTarget = queue.visibleCount
// v1.9.6 Sprint4 - hasBackground background
// if (queue.totalCount > queue.visibleCount) {
// hasBackground = true
// }
if (current < phaseTarget) {
const next = Math.min(phaseTarget, current + RESOURCE_BATCH_SIZE)
limits.set(resourceId, next)
hasMore = true
processedInFrame++
}
})
@@ -1915,51 +1882,54 @@ const rebuildResourceTaskQueues = () => {
const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value
const skipHorizontalFilter = currentTimeScale.value === TimelineScale.HOUR
// 1: -
const scrollLeft = timelineScrollLeft.value
const containerWidth = timelineContainerWidth.value
// v1.9.7 Bug//使visibleTimeRange
// 28-31使30
// 22830TaskBar
const scale = currentTimeScale.value
const timelineStart = timelineConfig.value.startDate.getTime()
const usePixelLevelFilter = scale === TimelineScale.HOUR || scale === TimelineScale.DAY || scale === TimelineScale.WEEK
//
let pixelPerMs = 0
if (scale === TimelineScale.HOUR) {
pixelPerMs = 40 / (60 * 60 * 1000) // 40px per hour
} else if (scale === TimelineScale.DAY) {
pixelPerMs = 30 / (24 * 60 * 60 * 1000) // 30px per day (5030)
} else if (scale === TimelineScale.WEEK) {
pixelPerMs = 60 / (7 * 24 * 60 * 60 * 1000) // 60px per week
} else if (scale === TimelineScale.MONTH) {
pixelPerMs = 60 / (30 * 24 * 60 * 60 * 1000) // 60px per month (approximate)
} else if (scale === TimelineScale.QUARTER) {
pixelPerMs = 90 / (90 * 24 * 60 * 60 * 1000) // 90px per quarter
} else if (scale === TimelineScale.YEAR) {
pixelPerMs = 120 / (365 * 24 * 60 * 60 * 1000) // 120px per year
let viewportStartTime: number
let viewportEndTime: number
if (usePixelLevelFilter) {
// 1: - - //
const scrollLeft = timelineScrollLeft.value
const containerWidth = timelineContainerWidth.value
const timelineStart = timelineConfig.value.startDate.getTime()
//
let pixelPerMs = 0
if (scale === TimelineScale.HOUR) {
pixelPerMs = 40 / (60 * 60 * 1000) // 40px per hour
} else if (scale === TimelineScale.DAY) {
pixelPerMs = 30 / (24 * 60 * 60 * 1000) // 30px per day
} else if (scale === TimelineScale.WEEK) {
pixelPerMs = 60 / (7 * 24 * 60 * 60 * 1000) // 60px per week
}
// 10%1
const ULTRA_TIGHT_BUFFER = 0.1 // 10%
const bufferPixels = containerWidth * ULTRA_TIGHT_BUFFER
const viewportStartPixel = Math.max(0, scrollLeft - bufferPixels)
const viewportEndPixel = scrollLeft + containerWidth + bufferPixels
//
viewportStartTime = timelineStart + viewportStartPixel / pixelPerMs
viewportEndTime = timelineStart + viewportEndPixel / pixelPerMs
} else {
// v1.9.7 //使visibleTimeRangetimelineData
viewportStartTime = visibleStartDate.getTime()
viewportEndTime = visibleEndDate.getTime()
}
// 10%1
const ULTRA_TIGHT_BUFFER = 0.1 // 10%
const bufferPixels = containerWidth * ULTRA_TIGHT_BUFFER
const viewportStartPixel = Math.max(0, scrollLeft - bufferPixels)
const viewportEndPixel = scrollLeft + containerWidth + bufferPixels
//
const viewportStartTime = timelineStart + viewportStartPixel / pixelPerMs
const viewportEndTime = timelineStart + viewportEndPixel / pixelPerMs
const queues = new Map<string | number, ResourceTaskQueue>()
const limits = new Map<string | number, number>()
// v1.9.6 Sprint4 -
const totalResources = (dataSource.value as Resource[]).length
const visibleResourcesCount = visibleResources.value.length
// v1.9.6 Sprint2(P5) -
const currentCache = new Map(taskBarRenderCache.value)
const newCache = new Map<string, TaskBarRenderCache>()
const currentTimestamp = Date.now()
let cachedCount = 0 // TaskBar
let totalVisibleTaskBars = 0 // v1.9.6 Sprint4 -
visibleResources.value.forEach(({ resource }) => {
const resourceId = resource.id as string | number
@@ -1995,7 +1965,11 @@ const rebuildResourceTaskQueues = () => {
taskId,
resourceId,
rendered: isRendered,
timestamp: isRendered ? existingCache.timestamp : currentTimestamp,
timestamp: isRendered ?
(existingCache ?
existingCache.timestamp
: currentTimestamp)
: currentTimestamp,
})
})
@@ -2008,8 +1982,6 @@ const rebuildResourceTaskQueues = () => {
originalTasks,
})
totalVisibleTaskBars += visibleTasks.length // v1.9.6 Sprint4 -
// v1.9.6 Sprint2(P5) - TaskBar
const previousLimit = resourceTaskRenderLimits.value.get(resourceId)
let initialLimit: number
@@ -2030,10 +2002,6 @@ const rebuildResourceTaskQueues = () => {
resourceRenderPhase.value = 'visible'
taskBarRenderCache.value = newCache //
// v1.9.6 Sprint2(P5) -
const totalTaskBars = newCache.size
const cacheHitRate = totalTaskBars > 0 ? ((cachedCount / totalTaskBars) * 100).toFixed(1) : '0.0'
scheduleResourceBatchRender()
}
@@ -2090,11 +2058,6 @@ const visibleResourcesWithFilteredTasks = computed(() => {
const { startDate: visibleStartDate, endDate: visibleEndDate } = visibleTimeRange.value
const skipHorizontalFilter = currentTimeScale.value === TimelineScale.HOUR
// v1.9.6 Sprint2(P1) -
if (filteredTasksCallCount <= 5 || filteredTasksCallCount % 10 === 0) {
const daysDiff = Math.ceil((visibleEndDate.getTime() - visibleStartDate.getTime()) / (1000 * 60 * 60 * 24))
}
//
let totalOriginalTasks = 0
let totalFilteredTasks = 0
@@ -2131,21 +2094,16 @@ const visibleResourcesWithFilteredTasks = computed(() => {
totalFilteredTasks += renderTasks.length
return {
resource: {
resource: new Resource({
...resource,
tasks: renderTasks,
// GanttConflicts
allTasks: originalTasks,
} as Resource,
}),
originalIndex,
}
})
// v1.9.6 Sprint2(P1) - 10
if (filteredTasksCallCount % 5 === 0 && totalOriginalTasks > 0) {
const filterRate = ((1 - totalFilteredTasks / totalOriginalTasks) * 100).toFixed(1)
}
return result
})
@@ -2524,7 +2482,6 @@ const handleTaskRowHover = (taskId: number | string | null) => {
// Timeline
const contentHeight = computed(() => {
const startTime = performance.now()
const minHeight = 400 //
// v1.9.0 使
@@ -2538,9 +2495,6 @@ const contentHeight = computed(() => {
totalHeight += layout?.totalHeight || 51
})
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
return Math.max(totalHeight, minHeight, timelineBodyHeight.value)
}
@@ -2603,10 +2557,8 @@ const handleMilestoneUpdate = (updatedMilestone: Milestone) => {
//
const generateTimelineData = (): any => {
const startTime = performance.now()
// 使
const result = getCachedTimelineData()
const duration = (performance.now() - startTime).toFixed(2)
return result
}
@@ -2984,18 +2936,12 @@ watch(
[timelineData, currentTimeScale],
([newData, newScale]) => {
positionCacheWatchCount++
const watchStartTime = performance.now()
if (newData && newScale) {
//
const cacheStartTime = performance.now()
positionCache.buildCache(newData as any[], newScale)
const cacheDuration = (performance.now() - cacheStartTime).toFixed(2)
}
const totalDuration = (performance.now() - watchStartTime).toFixed(2)
},
{ immediate: true } //
{ immediate: true }, //
)
// props/
@@ -5673,7 +5619,7 @@ const handleAddSuccessor = (task: Task) => {
<!-- 为资源下的每个任务渲染 TaskBar -->
<template v-if="(resource as any).tasks && (resource as any).tasks.length > 0">
<TaskBar
v-for="(task, taskIndex) in (resource as any).tasks"
v-for="(task) in (resource as any).tasks"
:key="`taskbar-${task.id}-${taskBarRenderKey}`"
:task="task"
:row-index="originalIndex"
@@ -5754,9 +5700,10 @@ const handleAddSuccessor = (task: Task) => {
<!-- v1.9.5 修复传递任务行号信息正确计算冲突区域高度 -->
<!-- v1.9.5 可通过 show-conflicts prop 控制是否显示 -->
<!-- v1.9.6 修复width使用totalTimelineWidth用于坐标计算containerWidth用于Canvas宽度 -->
<!-- v1.9.7 Bug修复使用渲染的tasks而不是allTasks避免滚动后显示已消失TaskBar的冲突 -->
<GanttConflicts
v-if="showConflicts"
:tasks="(resource as any).allTasks || (resource as any).tasks"
:tasks="(resource as any).tasks"
:resource-id="resource.id"
:day-width="dayWidth"
:start-date="
+16 -35
View File
@@ -50,9 +50,6 @@ const canvasRef = ref<HTMLCanvasElement | null>(null)
// containerWidth1900px0使1920使props.width30px
const canvasWidth = computed(() => {
const width = props.containerWidth || 1920
if (import.meta.env.DEV) {
console.log(`[GanttConflicts] canvasWidth: ${width}px (containerWidth: ${props.containerWidth}, totalWidth: ${props.width})`)
}
return width
})
const canvasHeight = computed(() => props.height)
@@ -161,8 +158,11 @@ watch([() => props.timelineData, () => props.currentTimeScale], () => {
if (import.meta.env.DEV) {}
// v1.9.4 BUG -
coordsCache.clear()
recalculateConflicts()
// v1.9.7 Bug - 使nextTick
texturePatterns.value = { light: null, medium: null, severe: null }
nextTick(() => {
recalculateConflicts()
})
}, { deep: true })
// v1.9.6 scrollLeft
@@ -186,8 +186,6 @@ watch(() => props.scrollLeft, () => {
// TaskBar
function recalculateConflictsIncremental(changedTaskId: string | number) {
const startTime = performance.now()
//
const changedTask = props.tasks.find(t => t.id === changedTaskId)
if (!changedTask) {
@@ -198,10 +196,14 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
//
const affectedTasks = props.tasks.filter(task => {
//
const taskStart = new Date(task.startDate)
const taskEnd = new Date(task.endDate)
const changedStart = new Date(changedTask.startDate)
const changedEnd = new Date(changedTask.endDate)
const taskStart = task.startDate ? new Date(task.startDate) : null
if (!taskStart) return false
const taskEnd = task.endDate ? new Date(task.endDate) : null
if (!taskEnd) return false
const changedStart = changedTask.startDate ? new Date(changedTask.startDate) : null
if (!changedStart) return false
const changedEnd = changedTask.endDate ? new Date(changedTask.endDate) : null
if (!changedEnd) return false
return !(taskEnd < changedStart || taskStart > changedEnd)
})
@@ -213,7 +215,7 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
const affectedTaskIds = new Set(affectedTasks.map(t => t.id))
const unchangedConflicts = previousConflictZones.value.filter(zone => {
//
return !zone.tasks.some(task => affectedTaskIds.has(task.id))
return !zone.tasks.some(task => affectedTaskIds.has(Number(task.id)))
})
//
@@ -288,10 +290,7 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
top,
height,
}
}).filter(Boolean)
const endTime = performance.now()
const elapsed = endTime - startTime
}).filter(z => z !== null) as ConflictZone[]
// 使
renderConflictsIncremental()
@@ -304,13 +303,6 @@ function recalculateConflicts() {
//
const conflicts = detectConflicts(props.tasks, props.resourceId)
//
const tasksInfo = props.tasks.map(t => {
const resource = (t as any).resources?.find((r: any) => String(r.id) === String(props.resourceId))
const percent = resource?.percent || 0
return `${t.name}(${percent}%)`
}).join(', ')
// v1.9.4 P1 - 使
conflictZones.value = conflicts.map((zone) => {
// key
@@ -386,9 +378,6 @@ function recalculateConflicts() {
}
}
//
if (import.meta.env.DEV) {}
return {
...zone,
left: canvasLeft,
@@ -396,19 +385,11 @@ function recalculateConflicts() {
top,
height,
}
}).filter(Boolean) //
}).filter(z => z !== null) as ConflictZone[]
const endTime = performance.now()
const elapsed = endTime - startTime
// v1.9.6
if (import.meta.env.DEV) {
console.log(`[GanttConflicts] ${conflictZones.value.length} conflict zones after viewport clipping:`)
conflictZones.value.forEach((zone, index) => {
console.log(` Zone ${index}: left=${zone.left}, width=${zone.width}, top=${zone.top}, height=${zone.height}, level=${zone.level}`)
})
}
//
if (import.meta.env.DEV && elapsed > 50) {}
+23 -26
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import type { Task } from '../../models/classes/Task'
import { useI18n } from '../../composables/useI18n'
interface Props {
task: Task
currentResourceId: string | number
resourceColor: string
resourcePercent: number
resourceCapacity: number
resourceName: string
taskBarWidth?: number
taskBarLeft?: number
@@ -33,6 +34,8 @@ const emit = defineEmits<{
'hover-change': [isHovered: boolean]
}>()
const { t } = useI18n()
//
const isExpanded = ref(false)
const tabElement = ref<HTMLElement | null>(null)
@@ -43,7 +46,7 @@ let debounceTimer: number | null = null
const DEBOUNCE_DELAY = 50 // 50ms
//
const percentText = computed(() => `${Math.round(props.resourcePercent)}%`)
const percentText = computed(() => `${Math.round(props.resourceCapacity)}%`)
// Tab taskBarWidthtaskbar
// taskbartab
@@ -64,7 +67,6 @@ const tabLeftOffset = computed(() => {
}
const taskBarLeft = props.taskBarLeft
const taskBarRight = taskBarLeft + (props.taskBarWidth || 0)
const viewportLeft = props.scrollLeft
// TaskBarTab
@@ -146,7 +148,7 @@ const expandedStyle = computed(() => {
if (shouldExpandUpward) {
//
return {
position: 'fixed',
position: 'fixed' as const,
bottom: `${viewportHeight - rect.top + 2}px`,
left: `${rect.left}px`,
maxHeight: `${Math.min(spaceAbove - 10, 400)}px`, // 10px
@@ -156,7 +158,7 @@ const expandedStyle = computed(() => {
} else {
//
return {
position: 'fixed',
position: 'fixed' as const,
top: `${rect.bottom + 2}px`,
left: `${rect.left}px`,
maxHeight: `${Math.min(spaceBelow - 10, 400)}px`, // 10px
@@ -174,9 +176,10 @@ const formattedDateRange = computed(() => {
const end = new Date(props.task.endDate)
const formatDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}`
return `${year}/${month}/${day}`
}
return `${formatDate(start)} ~ ${formatDate(end)}`
@@ -194,9 +197,6 @@ const conflictInfoList = computed(() => {
const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime()
//
const currentPercent = props.resourcePercent || 100
// v1.9.8
return props.conflictTasks.map(conflictTask => {
if (!conflictTask.startDate || !conflictTask.endDate) return null
@@ -210,8 +210,8 @@ const conflictInfoList = computed(() => {
const allocation = conflictTask.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId),
)
if (allocation && allocation.percent !== undefined) {
conflictPercent = Math.max(20, Math.min(100, allocation.percent))
if (allocation && allocation.capacity !== undefined) {
conflictPercent = Math.max(20, Math.min(100, allocation.capacity))
}
}
@@ -221,7 +221,7 @@ const conflictInfoList = computed(() => {
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
return `${date.getMonth() + 1}/${date.getDate()}`
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
}
return {
@@ -241,10 +241,7 @@ const totalOverloadPercent = computed(() => {
const currentTask = props.task
if (!currentTask.startDate || !currentTask.endDate) return 0
const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime()
const currentPercent = props.resourcePercent || 100
const currentPercent = props.resourceCapacity || 100
// v1.9.9 endDate +1
const DAY_MS = 24 * 60 * 60 * 1000
@@ -290,8 +287,8 @@ const totalOverloadPercent = computed(() => {
const allocation = task.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId),
)
if (allocation && allocation.percent !== undefined) {
taskPercent = allocation.percent
if (allocation && allocation.capacity !== undefined) {
taskPercent = allocation.capacity
}
}
intervalTotal += taskPercent
@@ -432,12 +429,12 @@ onUnmounted(() => {
<div class="expanded-body">
<!-- 利用率 -->
<div class="expanded-row">
<span class="info-label">利用率</span>
<span class="info-label">{{ t.resourceView.capacity }}</span>
<span class="info-value">{{ percentText }}</span>
</div>
<!-- 日期范围 -->
<div class="expanded-row">
<span class="info-label">时间范围</span>
<span class="info-label">{{ t.resourceView.duration }}</span>
<span class="info-value">{{ formattedDateRange }}</span>
</div>
<!-- 冲突预警有冲突时才显示 -->
@@ -447,20 +444,20 @@ onUnmounted(() => {
<svg class="warning-icon" viewBox="0 0 24 24" width="14" height="14">
<path fill="currentColor" d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>
<span class="conflict-title">资源超载警告</span>
<span class="conflict-title">{{ t.resourceView.overloadWarning }}</span>
<span class="total-overload"> {{ totalOverloadPercent }}%</span>
</div>
<!-- 可滚动的冲突列表 -->
<div class="conflict-list-container">
<div v-for="(info, index) in conflictInfoList" :key="index" class="conflict-item">
<div class="conflict-task-name">{{ info.taskName }}冲突</div>
<div class="conflict-task-name">{{ t.resourceView.conflictWith }}{{ info ? info.taskName : '' }}{{ t.resourceView.conflictSuffix }}</div>
<div class="conflict-detail">
<span class="conflict-label">冲突时段</span>
<span class="conflict-value">{{ info.overlapStart }} ~ {{ info.overlapEnd }}</span>
<span class="conflict-label">{{ t.resourceView.conflictDuration }}</span>
<span class="conflict-value">{{ info ? info.overlapStart : '' }} ~ {{ info ? info.overlapEnd : '' }}</span>
</div>
<div class="conflict-detail">
<span class="conflict-label">任务利用率</span>
<span class="conflict-value">{{ info.conflictPercent }}%</span>
<span class="conflict-label">{{ t.resourceView.capacity }}: </span>
<span class="conflict-value">{{ info ? info.conflictPercent : '' }}%</span>
</div>
</div>
</div>
+20 -2
View File
@@ -97,7 +97,6 @@ const messages = {
collapseAll: '全部折叠',
// v1.9.0 视图模式
taskView: '任务视图',
resourceView: '资源视图',
language: '中文',
languageTooltip: '选择语言',
lightMode: '明亮模式',
@@ -297,6 +296,16 @@ const messages = {
},
},
},
resourceView: {
desc: '资源视图',
capacity: '利用率',
overloaded: '超负荷',
duration: '时间周期',
overloadWarning: '资源超负荷警告',
conflictDuration: '冲突时段',
conflictWith: '与',
conflictSuffix: '冲突',
},
},
'en-US': {
dateNotSet: 'Not set',
@@ -389,7 +398,6 @@ const messages = {
collapseAll: 'Collapse All',
// v1.9.0 视图模式
taskView: 'Task View',
resourceView: 'Resource View',
language: 'English',
languageTooltip: 'Select language',
lightMode: 'Light Mode',
@@ -592,6 +600,16 @@ const messages = {
},
},
},
resourceView: {
desc: 'Resource View',
capacity: 'capacity',
overloaded: 'overLoaded',
duration: 'duration',
overloadWarning: 'Overload Warning',
conflictDuration: 'conflict duration',
conflictWith: 'conflict with',
conflictSuffix: '',
},
},
}
+3 -3
View File
@@ -149,7 +149,7 @@ export class Resource {
* @param task
* @returns (20-100)100
*/
private getTaskAllocationPercent(task: any): number {
getTaskAllocationPercent(task: any): number {
if (!task.resources || !Array.isArray(task.resources)) {
return 100 // 未配置resources时,默认100%
}
@@ -159,7 +159,7 @@ export class Resource {
return 100 // 未找到当前资源的分配信息,默认100%
}
const percent = allocation.percent ?? 100
return Math.max(20, Math.min(100, percent)) // 限制范围 20-100
const capacity = allocation.capacity ?? 100
return Math.max(20, Math.min(100, capacity)) // 限制范围 20-100
}
}
+2 -12
View File
@@ -1,14 +1,4 @@
/**
* (Resource Allocation)
* @version 1.9.0
*/
export interface ResourceAllocation {
id: string | number // 资源ID
name: string // 资源名称
percent?: number // 投入精力占比 (20-100),默认100
}
// Task 类型定义
import { Resource } from './Resource'// Task 类型定义
export interface Task {
id: number
name: string
@@ -42,7 +32,7 @@ export interface Task {
// 自定义样式
barColor?: string // 自定义TaskBar颜色,如 '#ff5733',若不设置则使用默认颜色方案
// v1.9.0 资源占用比例
resources?: ResourceAllocation[] // 资源分配列表,包含占比信息
resources?: Resource[] // 资源分配列表,包含占比信息
// 支持自定义属性 - 使用 unknown 允许任意类型
[key: string]: unknown
}
+16 -51
View File
@@ -8,7 +8,6 @@
*/
import type { Task } from '../models/classes/Task'
import { perfMonitor } from './perfMonitor'
/**
*
@@ -26,7 +25,7 @@ export interface ConflictZone {
tasks: Array<{
id: number | string
name: string
percent: number
capacity: number
}>
/** Canvas渲染坐标(由GanttConflicts组件计算填充) */
left?: number
@@ -57,8 +56,6 @@ export function detectConflicts(
tasks: Task[],
resourceId: string | number,
): ConflictZone[] {
const startTime = performance.now()
// 过滤出包含指定资源的任务(没有resources字段时视为100%分配给该资源)
const resourceTasks = tasks.filter((task) => {
// 如果没有resources字段或为空,视为100%分配
@@ -82,9 +79,6 @@ export function detectConflicts(
result = detectConflictsBruteForce(resourceTasks, resourceId)
}
const duration = performance.now() - startTime
perfMonitor.recordConflictDetection(resourceTasks.length, duration)
return result
}
@@ -108,18 +102,6 @@ function detectConflictsBruteForce(
const intersection = getTimeIntersection(task1, task2)
if (!intersection) continue
// 🔍 调试日志:输出检测到的时间交集(使用本地日期格式)
if (import.meta.env.DEV) {
const formatLocalDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
console.log(`[ConflictDetect] Tasks ${task1.name} & ${task2.name} overlap:`,
formatLocalDate(intersection.start), '~', formatLocalDate(intersection.end))
}
// 收集该时间段内的所有任务
// 🔧 修复:使用本地日期格式,避免toISOString的UTC时区问题
const formatLocalDate = (date: Date): string => {
@@ -140,49 +122,32 @@ function detectConflictsBruteForce(
return taskIntersection !== null
})
// 🔍 调试日志:输出收集到的重叠任务
if (import.meta.env.DEV) {
console.log(`[ConflictDetect] Found ${overlappingTasks.length} overlapping tasks:`,
overlappingTasks.map(t => `${t.name}[${t.startDate}~${t.endDate}]`).join(', '))
}
// 计算总投入比例
let totalPercent = 0
const taskDetails = overlappingTasks.map((task) => {
// 如果没有resources字段,默认100%;否则查找对应资源的percent
const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
const percent =
!task.resources || task.resources.length === 0 ? 100 : (resource?.percent || 0)
totalPercent += percent
const capacity =
!task.resources || task.resources.length === 0 ? 100 : (resource?.capacity || 0)
totalPercent += capacity
return {
id: task.id!,
name: task.name || '未命名任务',
percent,
capacity,
}
})
// 只有超载(>100%)才算冲突
if (totalPercent <= 100) {
// 🔍 调试:输出未超载的情况
if (import.meta.env.DEV && totalPercent > 0) {
console.log(`[ConflictDetect] No conflict: totalPercent=${totalPercent}% <= 100%`,
`Tasks: ${taskDetails.map(t => `${t.name}(${t.percent}%)`).join(' + ')}`)
}
continue
}
// 🔍 调试:输出检测到的冲突
if (import.meta.env.DEV) {
console.log(`[ConflictDetect] ✓ Conflict detected: totalPercent=${totalPercent}% > 100%`,
`Tasks: ${taskDetails.map(t => `${t.name}(${t.percent}%)`).join(' + ')}`)
}
// 计算冲突范围:所有参与冲突的任务在intersection范围内的并集
// 过滤出有资源分配的任务(没有resources字段视为100%分配)
const tasksWithResource = overlappingTasks.filter((task) => {
if (!task.resources || task.resources.length === 0) return true
const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
return resource && resource.percent > 0
return resource && resource.capacity && resource.capacity > 0
})
// v1.9.6 修复:精确计算真正超载的时间段
@@ -221,9 +186,9 @@ function detectConflictsBruteForce(
const taskEndInclusive = taskEnd.getTime() + 24 * 60 * 60 * 1000
if (taskStart.getTime() <= segmentStart && taskEndInclusive > segmentStart) {
const resource = task.resources?.find((r) => String(r.id) === String(resourceId))
const percent =
!task.resources || task.resources.length === 0 ? 100 : (resource?.percent || 0)
segmentPercent += percent
const capacity =
!task.resources || task.resources.length === 0 ? 100 : (resource?.capacity || 0)
segmentPercent += capacity
}
}
@@ -327,7 +292,7 @@ function mergeTasks(
} else {
// 已存在,更新为更高的投入比例
const existing = taskMap.get(task.id)!
if (task.percent > existing.percent) {
if (task.capacity > existing.capacity) {
taskMap.set(task.id, task)
}
}
@@ -594,12 +559,12 @@ function detectConflictsWithIntervalTree(
const taskDetails = overlappingTasks.map((t) => {
// 如果没有resources字段,默认100%;否则查找对应资源的percent
const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
const percent = !t.resources || t.resources.length === 0 ? 100 : (resource?.percent || 0)
totalPercent += percent
const capacity = !t.resources || t.resources.length === 0 ? 100 : (resource?.capacity || 0)
totalPercent += capacity
return {
id: t.id!,
name: t.name || '未命名任务',
percent,
capacity,
}
})
@@ -611,7 +576,7 @@ function detectConflictsWithIntervalTree(
const tasksWithResource = overlappingTasks.filter((t) => {
if (!t.resources || t.resources.length === 0) return true
const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
return resource && resource.percent > 0
return resource && resource.capacity && resource.capacity > 0
})
// 收集所有任务的时间边界点
@@ -648,8 +613,8 @@ function detectConflictsWithIntervalTree(
const tEndInclusive = tEnd.getTime() + 24 * 60 * 60 * 1000
if (tStart.getTime() <= segmentStart && tEndInclusive > segmentStart) {
const resource = t.resources?.find((r) => String(r.id) === String(resourceId))
const percent = !t.resources || t.resources.length === 0 ? 100 : (resource?.percent || 0)
segmentPercent += percent
const capacity = !t.resources || t.resources.length === 0 ? 100 : (resource?.capacity || 0)
segmentPercent += capacity
}
}
+20 -84
View File
@@ -69,31 +69,24 @@ export const perfMonitor = {
/**
*
*/
log(tag: string, data: unknown) {
log() {
const now = Date.now()
if (now - lastLogTime < LOG_THROTTLE) {
return
}
lastLogTime = now
// eslint-disable-next-line no-console
console.log(`[Perf] ${tag}:`, data)
},
/**
*
*/
measure<T>(tag: string, fn: () => T): T {
measure<T>(fn: () => T): T {
const start = performance.now()
const result = fn()
const end = performance.now()
const duration = end - start
if (duration > 5) {
// 只记录耗时超过 5ms 的操作
// eslint-disable-next-line no-console
console.log(`[Perf] ${tag}: ${duration.toFixed(2)}ms`)
}
if (duration > 5) { }
return result
},
@@ -115,7 +108,7 @@ export const perfMonitor = {
fpsValues = []
// eslint-disable-next-line no-console
console.log('[Perf] FPS 监控已启动')
// console.log('[Perf] FPS 监控已启动')
const updateFps = () => {
if (!fpsMonitorEnabled) return
@@ -133,19 +126,6 @@ export const perfMonitor = {
fpsValues.shift()
}
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
const minFps = Math.min(...fpsValues)
const maxFps = Math.max(...fpsValues)
// 根据 FPS 设置不同颜色
const color = fps >= 55 ? '#67c23a' : fps >= 30 ? '#e6a23c' : '#f56c6c'
// eslint-disable-next-line no-console
console.log(
`%c[Perf] FPS: ${fps} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
`color: ${color}; font-weight: bold;`,
)
fpsFrameCount = 0
fpsLastTime = currentTime
}
@@ -172,19 +152,6 @@ export const perfMonitor = {
fpsRafId = null
}
// 输出最终统计
if (fpsValues.length > 0) {
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
const minFps = Math.min(...fpsValues)
const maxFps = Math.max(...fpsValues)
// eslint-disable-next-line no-console
console.log(
`%c[Perf] FPS 监控已停止 | 总采样: ${fpsValues.length} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
'color: #909399; font-weight: bold;',
)
}
fpsValues = []
},
@@ -229,7 +196,7 @@ export const perfMonitor = {
printDrawStats() {
if (drawStats.totalDraws === 0) {
// eslint-disable-next-line no-console
console.log('[Perf] 暂无绘制数据')
// console.log('[Perf] 暂无绘制数据')
return
}
@@ -238,10 +205,10 @@ export const perfMonitor = {
recent10.reduce((sum, item) => sum + item.duration, 0) / recent10.length
// eslint-disable-next-line no-console
console.log(
'%c[Perf] Canvas 绘制统计',
'color: #409eff; font-weight: bold; font-size: 14px;',
)
// console.log(
// '%c[Perf] Canvas 绘制统计',
// 'color: #409eff; font-weight: bold; font-size: 14px;',
// )
// eslint-disable-next-line no-console
console.table({
总绘制次数: drawStats.totalDraws,
@@ -264,7 +231,7 @@ export const perfMonitor = {
drawStats.recentDraws = []
// eslint-disable-next-line no-console
console.log('[Perf] 绘制统计已重置')
// console.log('[Perf] 绘制统计已重置')
},
/**
@@ -273,10 +240,10 @@ export const perfMonitor = {
*/
startPerformanceTest(durationMs = 10000) {
// eslint-disable-next-line no-console
console.log(
`%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
'color: #409eff; font-weight: bold; font-size: 16px;',
)
// console.log(
// `%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
// 'color: #409eff; font-weight: bold; font-size: 16px;',
// )
this.resetDrawStats()
this.startFpsMonitor()
@@ -286,10 +253,10 @@ export const perfMonitor = {
this.printDrawStats()
// eslint-disable-next-line no-console
console.log(
'%c[Perf] 性能测试完成',
'color: #67c23a; font-weight: bold; font-size: 16px;',
)
// console.log(
// '%c[Perf] 性能测试完成',
// 'color: #67c23a; font-weight: bold; font-size: 16px;',
// )
}, durationMs)
},
@@ -306,21 +273,6 @@ export const perfMonitor = {
const now = Date.now()
if (now - linkDragStats.lastReportTime > 1000) {
const avgCoordTime =
linkDragStats.coordUpdateCount > 0
? (linkDragStats.coordUpdateTotalTime / linkDragStats.coordUpdateCount).toFixed(3)
: 0
const avgTargetTime =
linkDragStats.targetDetectCount > 0
? (linkDragStats.targetDetectTotalTime / linkDragStats.targetDetectCount).toFixed(3)
: 0
// eslint-disable-next-line no-console
console.log(
`[LinkDrag Perf] 坐标更新: ${linkDragStats.coordUpdateCount}次, 平均${avgCoordTime}ms | ` +
`目标检测: ${linkDragStats.targetDetectCount}次, 平均${avgTargetTime}ms`,
)
this.resetLinkDragStats()
linkDragStats.lastReportTime = now
return true
@@ -380,7 +332,7 @@ export const perfMonitor = {
if (frameTime > longFrameThreshold) {
frameMonitorStats.longFrameCount++
// eslint-disable-next-line no-console
console.warn(`[Frame Monitor] 长帧检测: ${frameTime.toFixed(1)}ms`)
// console.warn(`[Frame Monitor] 长帧检测: ${frameTime.toFixed(1)}ms`)
}
frameMonitorStats.lastFrameTime = now
@@ -397,17 +349,6 @@ export const perfMonitor = {
if (frameMonitorStats.frameMonitorId !== null) {
cancelAnimationFrame(frameMonitorStats.frameMonitorId)
frameMonitorStats.frameMonitorId = null
const percentage =
frameMonitorStats.frameCount > 0
? (frameMonitorStats.longFrameCount / frameMonitorStats.frameCount * 100).toFixed(1)
: 0
// eslint-disable-next-line no-console
console.log(
`[Frame Monitor] 统计: ${frameMonitorStats.frameCount}帧, ` +
`长帧数: ${frameMonitorStats.longFrameCount} (${percentage}%)`,
)
}
},
@@ -431,15 +372,10 @@ export const perfMonitor = {
* @param taskCount
* @param duration
*/
recordConflictDetection(taskCount: number, duration: number) {
recordConflictDetection() {
const now = performance.now()
// 每秒最多输出一次日志
if (now - lastLogTime > 1000) {
// eslint-disable-next-line no-console
console.log(
`[Conflict Detection] 任务数: ${taskCount}, 耗时: ${duration.toFixed(2)}ms, ` +
`算法: ${taskCount > 100 ? '区间树(O(n log n))' : '暴力遍历(O(n²))'}`,
)
lastLogTime = now
}
},
+4 -16
View File
@@ -14,7 +14,7 @@ class PerformanceMonitor {
*/
start(name: string): void {
this.marks.set(name, performance.now())
console.log(`[⏱️ START] ${name}`)
// console.log(`[⏱️ START] ${name}`)
}
/**
@@ -23,7 +23,7 @@ class PerformanceMonitor {
end(name: string): number {
const startTime = this.marks.get(name)
if (!startTime) {
console.warn(`[⏱️ WARN] No start mark found for: ${name}`)
// console.warn(`[⏱️ WARN] No start mark found for: ${name}`)
return 0
}
@@ -33,14 +33,6 @@ class PerformanceMonitor {
duration,
timestamp: Date.now(),
})
// 根据耗时使用不同颜色
let icon = '✅'
if (duration > 1000) icon = '🔴'
else if (duration > 500) icon = '🟠'
else if (duration > 100) icon = '🟡'
console.log(`[⏱️ END] ${icon} ${name}: ${duration.toFixed(2)}ms`)
this.marks.delete(name)
return duration
}
@@ -48,15 +40,11 @@ class PerformanceMonitor {
/**
*
*/
checkpoint(name: string, message: string): void {
checkpoint(name: string): void {
const startTime = this.marks.get(name)
if (!startTime) {
console.warn(`[⏱️ WARN] No start mark found for checkpoint: ${name}`)
return
}
const elapsed = performance.now() - startTime
console.log(`[⏱️ CHECKPOINT] ${name} - ${message}: ${elapsed.toFixed(2)}ms`)
}
/**
@@ -111,7 +99,7 @@ class PerformanceMonitor {
'Avg (ms)': stat.avg.toFixed(2),
'Max (ms)': stat.max.toFixed(2),
'Total (ms)': stat.total.toFixed(2),
}))
})),
)
console.groupEnd()
+3 -12
View File
@@ -92,7 +92,6 @@ export class PositionCache {
this.timelineDataHash = newHash
let cumulativePosition = 0
const startTime = performance.now()
// ⚠️ 关键优化:一次性遍历timelineData,构建完整的日期→位置映射表
for (const periodData of timelineData) {
@@ -102,7 +101,7 @@ export class PositionCache {
for (let i = 0; i < hours.length; i++) {
const hourData = hours[i]
const date = new Date(hourData.date)
const date = hourData.date ? new Date(hourData.date) : new Date()
const key = this.getCacheKey(date, timeScale)
const position = cumulativePosition + i * 30 // 小时视图每小时30px
this.cache.set(key, position)
@@ -143,8 +142,8 @@ export class PositionCache {
}
} else if (timeScale === TimelineScale.MONTH) {
// 月视图:为每个月的每一天建立映射
const startDate = new Date(periodData.startDate)
const endDate = new Date(periodData.endDate)
const startDate = new Date((periodData as TimelineMonth).startDate)
const endDate = new Date((periodData as TimelineMonth).endDate)
const daysInMonth = (periodData as TimelineMonth).monthData?.dayCount || 30
const monthWidth = 60
const dayWidth = monthWidth / daysInMonth
@@ -222,14 +221,6 @@ export class PositionCache {
}
}
}
const endTime = performance.now()
// Performance log for debugging
if (this.cache.size > 0) {
const duration = (endTime - startTime).toFixed(2)
// eslint-disable-next-line no-console
console.log(`[PositionCache] Built cache: ${this.cache.size} entries, took ${duration}ms`)
}
}
/**
+1 -1
View File
@@ -29,7 +29,7 @@ function getTaskResourcePercent(task: Task, resourceId: string | number): number
return 100 // 默认100%
}
const allocation = task.resources.find((r: any) => String(r.id) === String(resourceId))
return allocation?.percent ?? 100
return allocation?.capacity ?? 100
}
/**
+3 -18
View File
@@ -33,20 +33,6 @@ export function hasTimeOverlap(task1: Task, task2: Task): boolean {
return end1Plus > start2 && end2Plus > start1
}
/**
*
*/
function getTaskResourcePercent(task: Task, resourceId?: string | number): number {
if (!resourceId || !task.resources || task.resources.length === 0) {
return 100
}
const allocation = task.resources.find((r: any) => r.id === resourceId)
if (allocation && allocation.percent !== undefined) {
return Math.max(20, Math.min(100, allocation.percent))
}
return 100
}
/**
*
* 使
@@ -57,7 +43,6 @@ function getTaskResourcePercent(task: Task, resourceId?: string | number): numbe
*/
export function assignTaskRows(
tasks: Task[],
resourceId?: string | number,
baseRowHeight = 51,
): {
taskRowMap: Map<string | number, number>
@@ -139,8 +124,8 @@ export function assignTaskRows(
/**
*
*/
export function calculateMaxRows(tasks: Task[], resourceId?: string | number): number {
const result = assignTaskRows(tasks, resourceId)
export function calculateMaxRows(tasks: Task[]): number {
const result = assignTaskRows(tasks)
if (result.taskRowMap.size === 0) {
return 1
}
@@ -171,7 +156,7 @@ export function calculateResourceTaskLayout(
})
// 为当前资源的任务分配行
const result = assignTaskRows(resourceTasks, currentResourceId, baseRowHeight)
const result = assignTaskRows(resourceTasks, baseRowHeight)
resourceLayoutMap.set(currentResourceId, result)
return resourceLayoutMap