diff --git a/src/components/LinkDragGuide.vue b/src/components/LinkDragGuide.vue index d843061..2d4ec15 100644 --- a/src/components/LinkDragGuide.vue +++ b/src/components/LinkDragGuide.vue @@ -1,36 +1,27 @@ diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 1116b92..a51f935 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -653,13 +653,17 @@ const DISABLE_REACTIVE_PROPS_TO_TASKBAR = true // 设为 true 测试是否是 Vu const dragLinkMode = ref<'predecessor' | 'successor' | null>(null) // 当前拖拽模式 const linkDragSourceTask = shallowRef(null) // 拖拽起始任务(使用 shallowRef 优化性能) -const linkDragCurrentX = ref(0) // 当前鼠标X坐标 -const linkDragCurrentY = ref(0) // 当前鼠标Y坐标 +const linkDragCurrentX = ref(0) // 当前鼠标X坐标(保留用于兼容) +const linkDragCurrentY = ref(0) // 当前鼠标Y坐标(保留用于兼容) const linkDragTargetTask = shallowRef(null) // 当前悬停的目标任务(使用 shallowRef 优化性能) -const isValidLinkTarget = ref(false) // 是否是有效的连接目标 -const linkValidationError = ref('') // 连接验证失败的原因 +const isValidLinkTarget = ref(false) // 是否是有效的连接目标(保留用于 handleLinkDragEnd) +const linkValidationError = ref('') // 连接验证失败的原因(保留用于兼容) const linkAutoScrollInterval = ref(null) // 自动滚动定时器 +// 🚀 非响应式拖拽状态(用于高频更新,避免 Vue 响应式开销) +let nonReactiveIsValidTarget = false +let nonReactiveErrorMessage = '' + // 🔧 调试用:用于传递给 TaskBar 的静态替代值(避免触发 Vue 更新) const staticDragLinkMode = computed(() => DISABLE_REACTIVE_PROPS_TO_TASKBAR ? null : dragLinkMode.value, @@ -734,12 +738,16 @@ const handleLinkDragStart = (event: { task: Task; type: 'predecessor' | 'success // 启动帧监控 startFrameMonitor() - // 初始化鼠标坐标 - updateLinkDragCoordinates(event.mouseEvent.clientX, event.mouseEvent.clientY) + // 初始化鼠标坐标(使用非响应式版本) + updateLinkDragCoordinatesNonReactive(event.mouseEvent.clientX, event.mouseEvent.clientY) linkDragTargetTask.value = null isValidLinkTarget.value = false linkValidationError.value = '' + // 🚀 重置非响应式状态 + nonReactiveTargetTask = null + nonReactiveIsValidTarget = false + nonReactiveErrorMessage = '' // 启动自动滚动检测 startLinkAutoScroll() @@ -760,13 +768,40 @@ let linkDragRafId: number | null = null let pendingMouseX = 0 let pendingMouseY = 0 -// 🚀 优化后的 RAF 回调:在一帧内批量处理坐标更新和目标检测 +// 🚀 非响应式拖拽坐标(避免 Vue 响应式系统开销) +let currentDragX = 0 +let currentDragY = 0 + +// 🔧 调试开关:逐步启用各操作以定位性能瓶颈 +const DEBUG_ENABLE_COORD_UPDATE = true // 坐标更新 ✅ 不是瓶颈 +const DEBUG_ENABLE_TARGET_DETECT = true // 目标检测 ✅ 不是瓶颈 +const DEBUG_ENABLE_CANVAS_DRAW = true // Canvas 绘制 - 测试中 + +// 🚀 优化后的 RAF 回调:在一帧内批量处理坐标更新、目标检测和绘制 const processLinkDragFrame = () => { linkDragRafId = null - // 批量更新:坐标 + 目标检测 - updateLinkDragCoordinates(pendingMouseX, pendingMouseY) - detectLinkTarget(pendingMouseX, pendingMouseY) + // 🔧 调试:逐步启用各操作 + if (DEBUG_ENABLE_COORD_UPDATE) { + updateLinkDragCoordinatesNonReactive(pendingMouseX, pendingMouseY) + } + + if (DEBUG_ENABLE_TARGET_DETECT) { + detectLinkTargetNonReactive(pendingMouseX, pendingMouseY) + } + + if (DEBUG_ENABLE_CANVAS_DRAW) { + if (linkDragGuideRef.value && linkDragSourceTask.value) { + linkDragGuideRef.value.draw( + getLinkDragStartX(), + getLinkDragStartY(), + currentDragX, + currentDragY, + nonReactiveIsValidTarget, + nonReactiveErrorMessage, + ) + } + } } // 全局鼠标移动处理(🚀 优化:使用 RAF 统一调度,避免每次 mousemove 都触发响应式更新) @@ -826,13 +861,13 @@ let bodyRectCacheTime = 0 const BODY_RECT_CACHE_DURATION = 200 // 200ms 缓存(优化:增加缓存时间) let bodyRectInvalidated = false // 缓存失效标记(滚动时失效) -// 快速更新鼠标坐标(无节流,确保引导线跟随) -const updateLinkDragCoordinates = (mouseX: number, mouseY: number) => { +// 🚀 非响应式坐标更新(完全绕过 Vue 响应式系统) +const updateLinkDragCoordinatesNonReactive = (mouseX: number, mouseY: number) => { const startTime = ENABLE_PERF_MONITOR ? performance.now() : 0 if (!bodyContentRef.value) { - linkDragCurrentX.value = mouseX - linkDragCurrentY.value = mouseY + currentDragX = mouseX + currentDragY = mouseY if (ENABLE_PERF_MONITOR) { perfStats.coordUpdateCount++ @@ -850,8 +885,8 @@ const updateLinkDragCoordinates = (mouseX: number, mouseY: number) => { bodyRectCacheTime = now bodyRectInvalidated = false } - linkDragCurrentX.value = mouseX - cachedBodyRect.left - linkDragCurrentY.value = mouseY - cachedBodyRect.top + currentDragX = mouseX - cachedBodyRect.left + currentDragY = mouseY - cachedBodyRect.top if (ENABLE_PERF_MONITOR) { perfStats.coordUpdateCount++ @@ -868,6 +903,7 @@ const updateLinkDragCoordinates = (mouseX: number, mouseY: number) => { ? (perfStats.targetDetectTotalTime / perfStats.targetDetectCount).toFixed(3) : 0 + // eslint-disable-next-line no-console console.log( `[LinkDrag Perf] 坐标更新: ${perfStats.coordUpdateCount}次, 平均${avgCoordTime}ms | ` + `目标检测: ${perfStats.targetDetectCount}次, 平均${avgTargetTime}ms`, @@ -909,7 +945,12 @@ const handleLinkDragEnd = (event: { task: Task; type: 'predecessor' | 'successor document.removeEventListener('mouseup', handleGlobalMouseUp) // 停止自动滚动 - stopLinkAutoScroll() // 如果有有效目标,创建连接 + stopLinkAutoScroll() + + // 🚀 清除 LinkDragGuide 画布 + linkDragGuideRef.value?.clear() + + // 如果有有效目标,创建连接 if (linkDragTargetTask.value && isValidLinkTarget.value) { createLink(event.task, linkDragTargetTask.value, event.type) } @@ -1048,6 +1089,102 @@ const detectLinkTarget = (mouseX: number, mouseY: number) => { } } } + +// 🚀 非响应式目标检测(完全绕过 Vue 响应式系统) +// 缓存当前检测到的目标任务(用于 handleLinkDragEnd) +let nonReactiveTargetTask: Task | null = null + +const detectLinkTargetNonReactive = (mouseX: number, mouseY: number) => { + const startTime = ENABLE_PERF_MONITOR ? performance.now() : 0 + + if (!linkDragSourceTask.value || !bodyContentRef.value) return + + // 使用缓存的 rect + if (!cachedBodyRect) { + cachedBodyRect = bodyContentRef.value.getBoundingClientRect() + bodyRectCacheTime = Date.now() + } + + const relativeX = mouseX - cachedBodyRect.left + const relativeY = mouseY - cachedBodyRect.top + + let foundTaskId: number | null = null + const isPredecessorMode = dragLinkMode.value === 'predecessor' + const halfSize = (ANCHOR_SIZE + ANCHOR_TOLERANCE) / 2 + const expandedHalfSize = halfSize + 10 + + for (const taskIdStr in taskBarPositions.value) { + const pos = taskBarPositions.value[taskIdStr] + const taskId = Number(taskIdStr) + + if ( + relativeY < pos.top - expandedHalfSize || + relativeY > pos.top + pos.height + expandedHalfSize + ) { + continue + } + + if (isPredecessorMode) { + const anchorX = pos.left + pos.width + if (relativeX < anchorX - expandedHalfSize || relativeX > anchorX + expandedHalfSize) { + continue + } + const anchorY = pos.top + pos.height / 2 + if ( + relativeX >= anchorX - halfSize && + relativeX <= anchorX + halfSize && + relativeY >= anchorY - halfSize && + relativeY <= anchorY + halfSize + ) { + foundTaskId = taskId + break + } + } else { + const anchorX = pos.left + if (relativeX < anchorX - expandedHalfSize || relativeX > anchorX + expandedHalfSize) { + continue + } + const anchorY = pos.top + pos.height / 2 + if ( + relativeX >= anchorX - halfSize && + relativeX <= anchorX + halfSize && + relativeY >= anchorY - halfSize && + relativeY <= anchorY + halfSize + ) { + foundTaskId = taskId + break + } + } + } + + const foundTarget = foundTaskId !== null ? taskIdMap.get(foundTaskId) || null : null + const currentTargetId = nonReactiveTargetTask?.id ?? null + const newTargetId = foundTarget?.id ?? null + + if (currentTargetId !== newTargetId) { + nonReactiveTargetTask = foundTarget + + if (foundTarget && linkDragSourceTask.value) { + const validation = validateLink(linkDragSourceTask.value, foundTarget, dragLinkMode.value!) + nonReactiveIsValidTarget = validation.valid + nonReactiveErrorMessage = validation.error || '' + // 🚀 同步更新响应式变量(仅在目标变化时,用于 handleLinkDragEnd) + isValidLinkTarget.value = validation.valid + linkDragTargetTask.value = foundTarget + } else { + nonReactiveIsValidTarget = false + nonReactiveErrorMessage = '' + isValidLinkTarget.value = false + linkDragTargetTask.value = null + } + } + + if (ENABLE_PERF_MONITOR) { + perfStats.targetDetectCount++ + perfStats.targetDetectTotalTime += performance.now() - startTime + } +} + // 验证连接是否有效(返回 { valid: boolean, error?: string }) const validateLink = ( sourceTask: Task, @@ -1249,6 +1386,9 @@ const cleanupLinkDrag = () => { mouseMoveRafId = null } + // 🚀 清除 LinkDragGuide 画布 + linkDragGuideRef.value?.clear() + // 移除全局监听器 document.removeEventListener('keydown', handleLinkDragEscape) document.removeEventListener('mousemove', handleGlobalMouseMove) @@ -2614,6 +2754,8 @@ const taskBarPositions = shallowRef< const taskBarRenderKey = ref(0) const bodyContentRef = ref(null) +// 🚀 LinkDragGuide 命令式 API 引用 +const linkDragGuideRef = ref | null>(null) const svgWidth = ref(0) const svgHeight = ref(0) @@ -4078,20 +4220,14 @@ const handleAddSuccessor = (task: Task) => { :show-vertical-lines="currentTimeScale === TimelineScale.WEEK" /> - +