v1.9.0-rc.4 - TaskDrawer upgrades

This commit is contained in:
LINING-PC\lining
2026-02-02 22:38:19 +08:00
parent 8148969e1e
commit aacd7457fe
78 changed files with 406 additions and 305 deletions
+144 -74
View File
@@ -13,6 +13,7 @@ interface AssigneeOption {
key?: string | number
value: string | number
label: string
avatar?: string // v1.9.0 资源头像
}
interface Props {
@@ -367,6 +368,9 @@ watch(
// 使用 nextTick 确保 resetForm 的响应式更新完成后再赋值
nextTick(() => {
Object.assign(formData, taskData)
// v1.9.0 从 assignee/avatar 映射到 resources
mapAssigneeToResources()
})
} else if (props.task && !props.isEdit) {
// 新建模式,重置表单
@@ -489,6 +493,10 @@ const handleSubmit = async () => {
}
try {
submitting.value = true
// v1.9.0 提交前从 resources 映射到 assignee/avatar
mapResourcesToAssignee()
const taskData: Task = {
...formData,
id: props.isEdit && props.task ? props.task.id : Date.now(),
@@ -667,18 +675,6 @@ function confirmTimer(desc: string) {
handleStartTimer(desc)
}
// 处理负责人变更
const handleAssigneeChanged = (event: Event) => {
const value = (event.target as HTMLSelectElement).value
// 通过value过滤props.assigneeOptions获取对应的label
const selected = props.assigneeOptions?.find(option => option.value === value)
if (selected) {
// 这里可以根据需要处理选中的负责人信息
// 例如,可以将负责人名称存储在formData中
formData.assigneeName = selected.label
}
}
// v1.9.0 资源占比管理
const addResource = () => {
if (!formData.resources) {
@@ -715,6 +711,54 @@ const handleResourceChange = (index: number, field: 'id' | 'percent', value: str
}
}
// v1.9.0 从 task.assignee/avatar 映射到 resources
const mapAssigneeToResources = () => {
if (!formData.resources) {
formData.resources = []
}
// 如果已有resources数据,保持不变(优先使用resources)
if (formData.resources && formData.resources.length > 0) {
return
}
// 从 assignee/avatar 映射到 resources
const assignees = Array.isArray(formData.assignee) ? formData.assignee : (formData.assignee ? [formData.assignee] : [])
formData.resources = assignees.map((assigneeId) => {
// 查找对应的assigneeOption获取名称
const option = props.assigneeOptions?.find(opt => opt.value === assigneeId)
return {
id: assigneeId,
name: option?.label || String(assigneeId),
percent: 100, // 默认100%
}
})
}
// v1.9.0 从 resources 映射回 task.assignee/avatar 数组
const mapResourcesToAssignee = () => {
if (!formData.resources || formData.resources.length === 0) {
formData.assignee = ''
formData.avatar = ''
return
}
// 映射 assignee 数组
const assignees = formData.resources.map(r => String(r.id)).filter(id => id)
formData.assignee = assignees.length > 1 ? assignees : (assignees[0] || '')
// 映射 avatar 数组(从assigneeOptions中查找对应的avatar
const avatars = formData.resources
.map(r => {
const option = props.assigneeOptions?.find(opt => opt.value === r.id)
return option?.avatar || ''
})
.filter(avatar => avatar)
formData.avatar = avatars.length > 1 ? avatars : (avatars[0] || '')
}
// 计算任务状态
const taskStatus = computed(() => {
if (!props.task) return { text: '', color: '', bgColor: '', borderColor: '' }
@@ -949,24 +993,17 @@ const taskStatus = computed(() => {
<span v-if="errors.type" class="error-text">{{ errors.type }}</span>
</div>
<div class="form-group">
<label class="form-label" for="task-assignee">{{ t.assignee }}</label>
<select id="task-assignee" v-model="formData.assignee" class="form-select" @change="handleAssigneeChanged">
<option value="">{{ t.selectAssignee }}</option>
<option
v-for="assignee in props.assigneeOptions"
:key="assignee.key ?? assignee.value"
:value="assignee.value"
>
{{ assignee.label }}
</option>
</select>
</div>
<!-- v1.9.0 资源分配含占比配置 -->
<div class="form-group">
<label class="form-label">{{ t.resourceAllocation || '资源分配' }}</label>
<div class="resource-list">
<!-- 资源分配标题行 -->
<div class="resource-header">
<span class="resource-header-label">资源名称</span>
<span class="resource-header-label">占用比例</span>
<span class="resource-header-action"></span>
</div>
<div
v-for="(resource, index) in formData.resources"
:key="index"
@@ -982,32 +1019,22 @@ const taskStatus = computed(() => {
v-for="assignee in props.assigneeOptions"
:key="assignee.key ?? assignee.value"
:value="assignee.value"
:data-avatar="assignee.avatar"
>
{{ assignee.label }}
</option>
</select>
<div class="percent-input-wrapper">
<select
v-model.number="resource.percent"
class="form-select percent-select"
@change="handleResourceChange(index, 'percent', ($event.target as HTMLSelectElement).value)"
>
<option :value="25">25%</option>
<option :value="50">50%</option>
<option :value="75">75%</option>
<option :value="100">100%</option>
</select>
<input
v-model.number="resource.percent"
type="number"
class="form-input percent-input"
min="20"
max="100"
@input="handleResourceChange(index, 'percent', ($event.target as HTMLInputElement).value)"
/>
<span class="percent-unit">%</span>
</div>
<select
v-model.number="resource.percent"
class="form-select percent-select"
@change="handleResourceChange(index, 'percent', ($event.target as HTMLSelectElement).value)"
>
<option :value="25">25%</option>
<option :value="50">50%</option>
<option :value="75">75%</option>
<option :value="100">100%</option>
</select>
<button
type="button"
@@ -1015,7 +1042,12 @@ const taskStatus = computed(() => {
title="删除资源"
@click="removeResource(index)"
>
×
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button>
</div>
<button
@@ -1375,7 +1407,7 @@ const taskStatus = computed(() => {
.form-input,
.form-select,
.form-textarea {
padding: 12px 16px;
padding: 8px;
border: 1px solid var(--gantt-border-medium, #dcdfe6);
border-radius: 4px;
font-size: 14px;
@@ -1681,54 +1713,81 @@ const taskStatus = computed(() => {
gap: 8px;
}
/* 资源分配标题行 */
.resource-header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--gantt-bg-toolbar, #fafafa);
border-radius: 4px;
border: 1px solid var(--gantt-border-light, #ebeef5);
}
.resource-header-label {
font-size: 12px;
font-weight: 600;
color: var(--gantt-text-secondary, #606266);
white-space: nowrap;
overflow: visible;
}
.resource-header-label:first-child {
flex: 1;
min-width: 0;
text-align: center;
}
.resource-header-label:nth-child(2) {
width: 100px;
text-align: center;
}
.resource-header-action {
width: 48px;
flex-shrink: 0;
}
.resource-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
padding: 8px 12px;
background: var(--gantt-bg-secondary, #f5f7fa);
border-radius: 4px;
min-height: 48px;
}
.resource-select {
flex: 1;
min-width: 140px;
min-width: 180px;
max-width: 280px;
height: 36px;
}
.percent-input-wrapper {
display: flex;
align-items: center;
gap: 4px;
.resource-select option {
padding: 8px 12px;
}
.percent-select {
width: 80px;
}
.percent-input {
width: 60px;
padding: 6px 8px;
}
.percent-unit {
font-size: 14px;
color: var(--gantt-text-secondary, #606266);
width: 110px;
height: 36px;
flex-shrink: 0;
}
.btn-remove-resource {
width: 28px;
height: 28px;
display: flex;
width: 32px;
height: 32px;
display: grid;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--gantt-border-base, #dcdfe6);
border: none;
border-radius: 4px;
color: var(--gantt-text-secondary, #606266);
color: var(--gantt-danger, #f56c6c);
cursor: pointer;
font-size: 20px;
line-height: 1;
transition: all 0.2s;
flex-shrink: 0;
}
.btn-remove-resource:hover {
@@ -1739,7 +1798,7 @@ const taskStatus = computed(() => {
.btn-add-resource {
width: 100%;
padding: 8px;
padding: 10px;
background: transparent;
border: 1px dashed var(--gantt-border-base, #dcdfe6);
border-radius: 4px;
@@ -1747,6 +1806,7 @@ const taskStatus = computed(() => {
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
min-height: 40px;
}
.btn-add-resource:hover {
@@ -1754,6 +1814,16 @@ const taskStatus = computed(() => {
background: var(--gantt-primary-light, #ecf5ff);
}
/* 暗黑模式 */
:global(html[data-theme='dark']) .resource-header {
background: var(--gantt-bg-toolbar, rgba(255, 255, 255, 0.03)) !important;
border-color: var(--gantt-border-light, rgba(255, 255, 255, 0.1)) !important;
}
:global(html[data-theme='dark']) .resource-header-label {
color: var(--gantt-text-secondary, #a8a8a8) !important;
}
:global(html[data-theme='dark']) .resource-item {
background: var(--gantt-bg-secondary, rgba(255, 255, 255, 0.05)) !important;
}
+6 -57
View File
@@ -127,30 +127,20 @@ const getConflictTasksForTask = (resourceId: string | number, taskId: string | n
const currentStart = new Date(currentTask.startDate).getTime()
const currentEnd = new Date(currentTask.endDate).getTime()
// 获取当前资源在当前任务中的占比
const getCurrentPercent = (task: Task): number => {
if (!task.resources || !Array.isArray(task.resources)) return 100
const allocation = task.resources.find((r: any) => String(r.id) === String(resourceId))
return allocation?.percent ?? 100
}
const currentPercent = getCurrentPercent(currentTask)
// 找出所有与当前任务时间重叠且导致超载的任务
// v1.9.7 修复:返回所有与当前任务时间重叠的冲突任务
// 不需要再次验证占比相加是否超过100%,因为resourceConflicts已经包含了所有冲突的任务ID
// 当多个任务同时重叠时(如3个任务各75%),应该全部返回,而不是只返回第一个两两超载的任务
const conflictTasks = resource.tasks.filter(task => {
if (task.id === taskId) return false
if (!task.startDate || !task.endDate) return false
// 任务必须在冲突任务集合中
if (!conflictTaskIds.has(task.id)) return false
const taskStart = new Date(task.startDate).getTime()
const taskEnd = new Date(task.endDate).getTime()
// 检查时间重叠
if (!(currentStart < taskEnd && taskStart < currentEnd)) return false
// 检查占比相加是否超过100%
const taskPercent = getCurrentPercent(task)
return currentPercent + taskPercent > 100
// 检查时间重叠:只要与当前任务有时间交集,就是冲突任务
return currentStart < taskEnd && taskStart < currentEnd
})
return conflictTasks
@@ -186,7 +176,6 @@ let resourceTaskLayoutsCallCount = 0
const resourceTaskLayouts = computed(() => {
resourceTaskLayoutsCallCount++
const startTime = performance.now()
console.log(`[🔍 Performance] resourceTaskLayouts computed #${resourceTaskLayoutsCallCount} triggered`)
const layoutMap = new Map<string | number, {
taskRowMap: Map<string | number, number>,
@@ -195,7 +184,6 @@ const resourceTaskLayouts = computed(() => {
}>()
if (viewMode.value !== 'resource') {
console.log(`[🔍 Performance] resourceTaskLayouts: skipped (not resource view)`)
return layoutMap
}
@@ -233,7 +221,6 @@ const resourceTaskLayouts = computed(() => {
// 输出性能日志
if (resources.length > 0) {
const hitRate = ((cacheHits / resources.length) * 100).toFixed(1)
console.log(`[Performance] resourceTaskLayouts: ${duration}ms | computed ${resources.length} resources, cache hit rate: ${hitRate}%`)
}
return layoutMap
@@ -244,7 +231,6 @@ watch(dataSource, () => {
if (layoutCache.size > 100) {
const keysToDelete = Array.from(layoutCache.keys()).slice(0, layoutCache.size - 100)
keysToDelete.forEach(key => layoutCache.delete(key))
console.log(`[Performance] Layout cache cleaned: removed ${keysToDelete.length} entries`)
}
})
@@ -285,11 +271,9 @@ let resourceRowPositionsCallCount = 0
const resourceRowPositions = computed(() => {
resourceRowPositionsCallCount++
const startTime = performance.now()
console.log(`[🔍 Performance] resourceRowPositions computed #${resourceRowPositionsCallCount} triggered`)
const positions = new Map<string | number, number>()
if (viewMode.value !== 'resource') {
console.log(`[🔍 Performance] resourceRowPositions: skipped (not resource view)`)
return positions
}
@@ -328,12 +312,6 @@ const resourceRowPositions = computed(() => {
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
if (processedCount < resources.length) {
console.log(`[Phase2] resourceRowPositions: ${duration}ms | lazy-computed ${processedCount}/${resources.length} resources`)
} else {
console.log(`[Performance] resourceRowPositions: ${duration}ms | processed ${resources.length} resources`)
}
return positions
})
@@ -1595,9 +1573,6 @@ watch([timelineScrollLeft, timelineContainerWidth], ([newScrollLeft, newWidth])
// v1.9.5 P2-1优化 - 计算水平方向可见的时间范围(用于TaskBar过滤)
const visibleTimeRange = computed(() => {
visibleTimeRangeCallCount++
if (visibleTimeRangeCallCount % 10 === 0) {
console.log(`[Performance] visibleTimeRange called ${visibleTimeRangeCallCount} times`)
}
const scrollLeft = debouncedScrollLeft.value
const containerWidth = debouncedContainerWidth.value || timelineContainerWidth.value
@@ -1623,8 +1598,6 @@ const visibleTimeRange = computed(() => {
if (visibleTimeRangeCallCount % 10 === 0) {
const scale = currentTimeScale.value
const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
console.log(`[Sprint2-Debug] visibleTimeRange: ${scale}, scrollLeft=${scrollLeft.toFixed(0)}, containerWidth=${containerWidth.toFixed(0)}, buffer=${bufferWidth.toFixed(0)}`)
console.log(`[Sprint2-Debug] Date range: ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]} (${daysDiff} days)`)
}
return { startDate, endDate }
@@ -1749,7 +1722,6 @@ const visibleTaskRange = computed(() => {
const startTime = performance.now()
const scrollTop = timelineBodyScrollTop.value
const containerHeight = timelineBodyHeight.value || 600
console.log(`[🔍 Performance] visibleTaskRange computed #${visibleTaskRangeCallCount} triggered | scrollTop: ${scrollTop}`)
if (viewMode.value === 'resource') {
// 资源视图:基于资源行的实际高度计算可见范围
@@ -1787,7 +1759,6 @@ const visibleTaskRange = computed(() => {
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
console.log(`[Performance] visibleTaskRange: ${duration}ms | range: ${startIndex}-${endIndex} / ${resources.length}`)
return {
startIndex: Math.max(0, startIndex),
@@ -1977,7 +1948,6 @@ const rebuildResourceTaskQueues = () => {
// v1.9.6 Sprint4 - 日志:记录实际处理的资源数量
const totalResources = (dataSource.value as Resource[]).length
const visibleResourcesCount = visibleResources.value.length
console.log(`[Sprint4] rebuildResourceTaskQueues: processing ${visibleResourcesCount}/${totalResources} visible resources`)
// v1.9.6 Sprint2(P5) - 渲染缓存增量更新:保留已有记录
const currentCache = new Map(taskBarRenderCache.value)
@@ -2055,13 +2025,9 @@ const rebuildResourceTaskQueues = () => {
resourceRenderPhase.value = 'visible'
taskBarRenderCache.value = newCache // 更新缓存
// v1.9.6 Sprint4 - 输出优化后的统计信息
console.log(`[Sprint4] Will render ${totalVisibleTaskBars} TaskBars (from ${visibleResourcesCount} visible resources)`)
// v1.9.6 Sprint2(P5) - 输出缓存统计
const totalTaskBars = newCache.size
const cacheHitRate = totalTaskBars > 0 ? ((cachedCount / totalTaskBars) * 100).toFixed(1) : '0.0'
console.log(`[Sprint2-P5] TaskBar cache: ${cachedCount}/${totalTaskBars} already rendered (${cacheHitRate}% hit rate)`)
scheduleResourceBatchRender()
}
@@ -2102,7 +2068,6 @@ watch(
if (updatedCount > 0) {
taskBarRenderCache.value = cache
console.log(`[Sprint2-P5] Marked ${updatedCount} TaskBars as rendered (cache size: ${cache.size})`)
}
},
{ deep: false },
@@ -2123,7 +2088,6 @@ const visibleResourcesWithFilteredTasks = computed(() => {
// v1.9.6 Sprint2(P1) - 临时调试:每次都输出时间范围
if (filteredTasksCallCount <= 5 || filteredTasksCallCount % 10 === 0) {
const daysDiff = Math.ceil((visibleEndDate.getTime() - visibleStartDate.getTime()) / (1000 * 60 * 60 * 24))
console.log(`[Sprint2-Debug] #${filteredTasksCallCount} visibleTimeRange: ${visibleStartDate.toISOString().split('T')[0]} to ${visibleEndDate.toISOString().split('T')[0]} (${daysDiff} days)`)
}
// 性能监控统计
@@ -2175,7 +2139,6 @@ const visibleResourcesWithFilteredTasks = computed(() => {
// v1.9.6 Sprint2(P1) - 性能监控日志(每10次输出一次)
if (filteredTasksCallCount % 5 === 0 && totalOriginalTasks > 0) {
const filterRate = ((1 - totalFilteredTasks / totalOriginalTasks) * 100).toFixed(1)
console.log(`[Sprint2-P1] visibleResourcesWithFilteredTasks #${filteredTasksCallCount}: ${totalFilteredTasks}/${totalOriginalTasks} taskbars, filtered: ${filterRate}%`)
}
return result
@@ -2572,7 +2535,6 @@ const contentHeight = computed(() => {
const endTime = performance.now()
const duration = (endTime - startTime).toFixed(2)
console.log(`[Performance] contentHeight: ${duration}ms | processed ${resources.length} resources`)
return Math.max(totalHeight, minHeight, timelineBodyHeight.value)
}
@@ -2640,7 +2602,6 @@ const generateTimelineData = (): any => {
// 使用缓存版本提升性能
const result = getCachedTimelineData()
const duration = (performance.now() - startTime).toFixed(2)
console.log(`[🔍 Performance] generateTimelineData: ${duration}ms | scale: ${currentTimeScale.value}`)
return result
}
@@ -2872,7 +2833,6 @@ const getGlobalWeekPosition = (monthIndex: number, weekIndex: number) => {
// 更新时间刻度方法 - 供外部调用
const updateTimeScale = (scale: TimelineScale) => {
perfMonitor2.start(`updateTimeScale-${scale}`)
console.log(`[🔧 Action] updateTimeScale: ${currentTimeScale.value}${scale}`)
currentTimeScale.value = scale
@@ -2958,7 +2918,6 @@ const updateTimeScale = (scale: TimelineScale) => {
}
// 重新生成时间线数据
console.log('[📊 Timeline Data] Regenerating timeline data for new scale...')
timelineData.value = generateTimelineData()
// 等待DOM更新后触发多个重新计算事件
@@ -3021,18 +2980,15 @@ watch(
([newData, newScale]) => {
positionCacheWatchCount++
const watchStartTime = performance.now()
console.log(`[🔍 Performance] positionCache watch triggered #${positionCacheWatchCount} | scale: ${newScale}`)
if (newData && newScale) {
// 调用缓存构建(内部会判断是否需要重建)
const cacheStartTime = performance.now()
positionCache.buildCache(newData as any[], newScale)
const cacheDuration = (performance.now() - cacheStartTime).toFixed(2)
console.log(`[🔍 Performance] positionCache.buildCache: ${cacheDuration}ms`)
}
const totalDuration = (performance.now() - watchStartTime).toFixed(2)
console.log(`[🔍 Performance] positionCache watch completed: ${totalDuration}ms`)
},
{ immediate: true } // 立即执行,确保初始化时也构建缓存
)
@@ -3615,7 +3571,6 @@ const scrollToDate = (date: Date | string) => {
// 更新任务
const updateTask = (updatedTask: Task) => {
perfMonitor2.start('updateTask')
console.log('[🔧 Task Update] Task updated:', updatedTask.id)
// 不直接修改props数据,而是通过事件通知父组件
// 触发全局事件,通知父组件更新数据
@@ -3785,11 +3740,8 @@ const handleTaskBarDragEnd = (updatedTask: Task) => {
// 只有当行数发生变化时,才需要触发全量重绘
if (newRowCount !== oldRowCount) {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout changed: ${oldRowCount} -> ${newRowCount} rows after drag`)
// 行数变化,需要触发重绘
taskBarRenderKey.value++
} else {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout unchanged (${newRowCount} rows), skip render key update`)
}
}
}
@@ -3833,10 +3785,7 @@ const handleTaskBarResizeEnd = (updatedTask: Task) => {
// 只有当行数发生变化时,才需要触发全量重绘
if (newRowCount !== oldRowCount) {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout changed: ${oldRowCount} -> ${newRowCount} rows after resize`)
taskBarRenderKey.value++
} else {
console.log(`[Auto-Layout] Resource ${targetResourceId} layout unchanged (${newRowCount} rows), skip render key update`)
}
}
}
@@ -104,7 +104,6 @@ watch(isDraggingTaskBar, (dragging) => {
nextTick(() => {
// 使TaskBarID
if (lastChangedTaskId.value !== null) {
console.log(`[GanttConflicts] Incremental update for task ${lastChangedTaskId.value}`)
recalculateConflictsIncremental(lastChangedTaskId.value)
lastChangedTaskId.value = null //
} else {
@@ -136,12 +135,10 @@ watch(isDraggingTimeline, (dragging) => {
if (dragging) {
// Canvas
clearCanvas()
console.log('[GanttConflicts] Timeline drag started, canvas cleared')
} else {
//
nextTick(() => {
recalculateConflicts()
console.log('[GanttConflicts] Timeline drag ended, conflicts recalculated')
})
}
})
@@ -194,7 +191,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
//
const changedTask = props.tasks.find(t => t.id === changedTaskId)
if (!changedTask) {
console.log(`[GanttConflicts] Changed task ${changedTaskId} not found, fallback to full recalculation`)
recalculateConflicts()
return
}
@@ -210,8 +206,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
return !(taskEnd < changedStart || taskStart > changedEnd)
})
console.log(`[GanttConflicts] Incremental: ${affectedTasks.length} affected tasks (out of ${props.tasks.length} total)`)
//
const newConflicts = detectConflicts(affectedTasks, props.resourceId)
@@ -222,8 +216,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
return !zone.tasks.some(task => affectedTaskIds.has(task.id))
})
console.log(`[GanttConflicts] Incremental: kept ${unchangedConflicts.length} unchanged conflicts, adding ${newConflicts.length} new conflicts`)
//
const allConflicts = [...unchangedConflicts.map(z => ({
startDate: z.startDate,
@@ -301,8 +293,6 @@ function recalculateConflictsIncremental(changedTaskId: string | number) {
const endTime = performance.now()
const elapsed = endTime - startTime
console.log(`[GanttConflicts] Incremental update completed in ${elapsed.toFixed(2)}ms, ${conflictZones.value.length} total conflict zones`)
// 使
renderConflictsIncremental()
}
@@ -321,9 +311,6 @@ function recalculateConflicts() {
return `${t.name}(${percent}%)`
}).join(', ')
console.log(`[GanttConflicts] Resource ${props.resourceId}: ${props.tasks.length} tasks [${tasksInfo}], detected ${conflicts.length} conflict zones`)
console.log(`[GanttConflicts] Props - scrollLeft: ${props.scrollLeft}, containerWidth: ${props.containerWidth}, width: ${props.width}`)
// v1.9.4 P1 - 使
conflictZones.value = conflicts.map((zone) => {
// key
@@ -432,8 +419,6 @@ function recalculateConflicts() {
} else {
renderConflictsIncremental()
}
console.log(`[GanttConflicts] Resource ${props.resourceId}: Rendered ${conflictZones.value.length} conflict zones on canvas`)
}
// CanvasTaskBar使
@@ -784,19 +769,15 @@ function detectChangedZones(
function drawConflictZone(ctx: CanvasRenderingContext2D, zone: ConflictZone) {
// v1.9.6 使 === undefined left=0
if (zone.left === undefined || zone.width === undefined || zone.width <= 0) {
console.log(`[GanttConflicts] drawConflictZone skipped: left=${zone.left}, width=${zone.width}`)
return
}
//
const canvas = canvasRef.value
if (!canvas || zone.left + zone.width < 0 || zone.left > canvas.width) {
console.log(`[GanttConflicts] drawConflictZone out of viewport: left=${zone.left}, width=${zone.width}, canvas.width=${canvas?.width}`)
return
}
console.log(`[GanttConflicts] Drawing zone: left=${zone.left}, width=${zone.width}, top=${zone.top}, height=${zone.height}, level=${zone.level}`)
//
drawTextureBackground(ctx, zone)
@@ -811,18 +792,14 @@ function drawConflictZone(ctx: CanvasRenderingContext2D, zone: ConflictZone) {
function renderConflicts() {
const canvas = canvasRef.value
if (!canvas) {
console.log('[GanttConflicts] renderConflicts: canvas not found')
return
}
const ctx = canvas.getContext('2d')
if (!ctx) {
console.log('[GanttConflicts] renderConflicts: context not found')
return
}
console.log(`[GanttConflicts] renderConflicts: canvas=${canvas.width}x${canvas.height}, zones=${conflictZones.value.length}`)
// Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height)
@@ -838,13 +815,11 @@ function renderConflicts() {
function clearCanvas() {
const canvas = canvasRef.value
if (!canvas) {
console.log('[GanttConflicts] clearCanvas: canvas not found')
return
}
const ctx = canvas.getContext('2d')
if (!ctx) {
console.log('[GanttConflicts] clearCanvas: context not found')
return
}
@@ -853,8 +828,6 @@ function clearCanvas() {
//
previousConflictZones.value = []
console.log('[GanttConflicts] Canvas cleared, ready for redraw')
}
//
@@ -987,7 +960,6 @@ function drawTopWarning(ctx: CanvasRenderingContext2D, zone: ConflictZone) {
onMounted(() => {
// v1.9.6 使
// IntersectionObserver
console.log(`[GanttConflicts] Resource ${props.resourceId}: mounted, will calculate conflicts immediately`)
nextTick(() => {
recalculateConflicts()
})
+104 -29
View File
@@ -197,6 +197,7 @@ const conflictInfoList = computed(() => {
//
const currentPercent = props.resourcePercent || 100
// v1.9.8
return props.conflictTasks.map(conflictTask => {
if (!conflictTask.startDate || !conflictTask.endDate) return null
@@ -214,14 +215,10 @@ const conflictInfoList = computed(() => {
}
}
//
//
const overlapStart = Math.max(currentStart, conflictStart)
const overlapEnd = Math.min(currentEnd, conflictEnd)
//
const totalPercent = currentPercent + conflictPercent
const overloadPercent = totalPercent - 100
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
return `${date.getMonth() + 1}/${date.getDate()}`
@@ -231,14 +228,76 @@ const conflictInfoList = computed(() => {
taskName: conflictTask.name,
overlapStart: formatDate(overlapStart),
overlapEnd: formatDate(overlapEnd),
currentPercent,
conflictPercent,
totalPercent,
overloadPercent,
conflictPercent, //
}
}).filter(Boolean)
})
// v1.9.8 - 100%
const totalOverloadPercent = computed(() => {
if (!props.hasConflict || !props.conflictTasks || props.conflictTasks.length === 0) {
return 0
}
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
//
let maxTotalPercent = currentPercent
// +
const allTasks = [currentTask, ...props.conflictTasks]
//
allTasks.forEach((task1, i) => {
if (!task1.startDate || !task1.endDate) return
const start1 = new Date(task1.startDate).getTime()
const end1 = new Date(task1.endDate).getTime()
allTasks.forEach((task2, j) => {
if (i >= j || !task2.startDate || !task2.endDate) return
const start2 = new Date(task2.startDate).getTime()
const end2 = new Date(task2.endDate).getTime()
//
if (start1 < end2 && start2 < end1) {
//
const overlapStart = Math.max(start1, start2)
const overlapEnd = Math.min(end1, end2)
let intervalTotal = 0
allTasks.forEach(task => {
if (!task.startDate || !task.endDate) return
const tStart = new Date(task.startDate).getTime()
const tEnd = new Date(task.endDate).getTime()
//
if (tStart < overlapEnd && tEnd > overlapStart) {
let taskPercent = 100
if (task.resources && Array.isArray(task.resources)) {
const allocation = task.resources.find(
(r: any) => String(r.id) === String(props.currentResourceId),
)
if (allocation && allocation.percent !== undefined) {
taskPercent = allocation.percent
}
}
intervalTotal += taskPercent
}
})
maxTotalPercent = Math.max(maxTotalPercent, intervalTotal)
}
})
})
return Math.max(0, maxTotalPercent - 100)
})
// v1.9.4 P1 -
const handleMouseEnter = () => {
//
@@ -375,25 +434,26 @@ onUnmounted(() => {
</div>
<!-- 冲突预警有冲突时才显示 -->
<div v-if="hasConflict && conflictInfoList.length > 0" class="conflict-section">
<!-- 固定标题 -->
<div class="conflict-header">
<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="total-overload">+{{ totalOverloadPercent }}%</span>
</div>
<div v-for="(info, index) in conflictInfoList" :key="index" class="conflict-item">
<div class="conflict-task-name">{{ info.taskName }}冲突</div>
<div class="conflict-detail">
<span class="conflict-label">冲突时段</span>
<span class="conflict-value">{{ info.overlapStart }} ~ {{ info.overlapEnd }}</span>
</div>
<div class="conflict-detail">
<span class="conflict-label">资源占用</span>
<span class="conflict-value">{{ info.currentPercent }}% + {{ info.conflictPercent }}% = {{ info.totalPercent }}%</span>
</div>
<div class="conflict-detail overload-highlight">
<span class="conflict-label">超载</span>
<span class="conflict-value">+{{ info.overloadPercent }}%</span>
<!-- 可滚动的冲突列表 -->
<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-detail">
<span class="conflict-label">冲突时段</span>
<span class="conflict-value">{{ info.overlapStart }} ~ {{ info.overlapEnd }}</span>
</div>
<div class="conflict-detail">
<span class="conflict-label">任务投入占比</span>
<span class="conflict-value">{{ info.conflictPercent }}%</span>
</div>
</div>
</div>
</div>
@@ -489,8 +549,14 @@ onUnmounted(() => {
.conflict-section {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.2); /* 添加滚动支持 */
max-height: 200px; /* 限制冲突区域最大高度 */
border-top: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
flex-direction: column;
}
/* 可滚动的冲突列表容器 - v1.9.8 */
.conflict-list-container {
max-height: 200px; /* 限制冲突列表最大高度 */
overflow-y: auto; /* 垂直滚动 */
overflow-x: hidden;
/* 细滚动条样式 */
@@ -499,21 +565,22 @@ onUnmounted(() => {
}
/* Webkit浏览器(Chrome、Safari、Edge)的细滚动条样式 */
.conflict-section::-webkit-scrollbar {
.conflict-list-container::-webkit-scrollbar {
width: 4px; /* 细滚动条 */
}
.conflict-section::-webkit-scrollbar-track {
.conflict-list-container::-webkit-scrollbar-track {
background: transparent; /* 透明轨道 */
}
.conflict-section::-webkit-scrollbar-thumb {
.conflict-list-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3); /* 半透明滑块 */
border-radius: 2px;
}
.conflict-section::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5); /* 悬停时更明显 */}
.conflict-list-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5); /* 悬停时更明显 */
}
.conflict-header {
display: flex;
@@ -527,6 +594,14 @@ onUnmounted(() => {
.conflict-title {
font-size: 12px;
flex: 1;
}
.total-overload {
font-size: 13px;
font-weight: 700;
color: #ff5252;
margin-left: auto;
}
.conflict-item {