v1.9.0-rc.4 - TaskDrawer upgrades
This commit is contained in:
+144
-74
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
// 检查是否可以使用增量更新(单个TaskBar变化且有ID记录)
|
||||
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`)
|
||||
}
|
||||
|
||||
// 计算冲突区域在Canvas上的位置(与TaskBar使用相同逻辑)
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user