可视化连接线性能问题调试
This commit is contained in:
@@ -1,36 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
active: boolean
|
||||
startX: number
|
||||
startY: number
|
||||
endX: number
|
||||
endY: number
|
||||
// 尺寸和位置(变化频率低)
|
||||
width: number
|
||||
height: number
|
||||
offsetLeft?: number
|
||||
offsetTop?: number
|
||||
isValidTarget?: boolean // 是否是合法的连接目标
|
||||
errorMessage?: string // 错误提示消息
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
offsetLeft: 0,
|
||||
offsetTop: 0,
|
||||
isValidTarget: true,
|
||||
errorMessage: '',
|
||||
})
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
// 性能监控
|
||||
const ENABLE_PERF_MONITOR = true
|
||||
const SKIP_ACTUAL_DRAW = false // 调试开关:跳过实际绘制,只打印日志
|
||||
const ENABLE_PERF_MONITOR = false // 生产环境关闭
|
||||
let drawCount = 0
|
||||
let drawTotalTime = 0
|
||||
let lastReportTime = 0
|
||||
let lastCallTime = 0 // 上次调用时间,用于计算间隔
|
||||
|
||||
// 缓存 canvas 上下文和尺寸信息,避免重复初始化
|
||||
let cachedCtx: CanvasRenderingContext2D | null = null
|
||||
@@ -76,40 +67,50 @@ const initCanvas = () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制拖拽引导线
|
||||
* 使用贝塞尔曲线,与 GanttLinks 保持一致的视觉风格
|
||||
* 立即绘制,不使用RAF节流,确保跟随鼠标
|
||||
* 🚀 命令式绘制方法 - 由父组件直接调用,避免 Vue 响应式开销
|
||||
* @param startX 起始点X坐标
|
||||
* @param startY 起始点Y坐标
|
||||
* @param endX 结束点X坐标
|
||||
* @param endY 结束点Y坐标
|
||||
* @param isValidTarget 是否是合法的连接目标
|
||||
* @param errorMessage 错误提示消息
|
||||
*/
|
||||
const drawGuideLine = () => {
|
||||
performDraw()
|
||||
}
|
||||
// 🔧 调试开关:跳过实际绘制
|
||||
const DEBUG_SKIP_ACTUAL_DRAW = false
|
||||
const USE_SIMPLE_LINE = true // true = 使用简单直线替代贝塞尔曲线(性能更好)
|
||||
|
||||
const performDraw = () => {
|
||||
if (!props.active) return
|
||||
|
||||
// 调试模式:跳过实际绘制
|
||||
if (SKIP_ACTUAL_DRAW) {
|
||||
return
|
||||
}
|
||||
const draw = (
|
||||
startX: number,
|
||||
startY: number,
|
||||
endX: number,
|
||||
endY: number,
|
||||
isValidTarget = true,
|
||||
errorMessage = '',
|
||||
) => {
|
||||
// 🔧 调试:跳过实际绘制
|
||||
if (DEBUG_SKIP_ACTUAL_DRAW) return
|
||||
|
||||
const startTime = ENABLE_PERF_MONITOR ? performance.now() : 0
|
||||
|
||||
const ctx = initCanvas()
|
||||
if (!ctx) return
|
||||
if (!ctx) {
|
||||
console.warn('[LinkDragGuide] initCanvas returned null')
|
||||
return
|
||||
}
|
||||
|
||||
const displayWidth = cachedWidth
|
||||
const displayHeight = cachedHeight
|
||||
|
||||
// 清空画布(使用缓存的尺寸)
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, displayWidth, displayHeight)
|
||||
|
||||
// 转换为 Canvas 局部坐标
|
||||
const localX1 = props.startX - props.offsetLeft
|
||||
const localY1 = props.startY - props.offsetTop
|
||||
const localX2 = props.endX - props.offsetLeft
|
||||
const localY2 = props.endY - props.offsetTop
|
||||
const localX1 = startX - props.offsetLeft
|
||||
const localY1 = startY - props.offsetTop
|
||||
const localX2 = endX - props.offsetLeft
|
||||
const localY2 = endY - props.offsetTop
|
||||
|
||||
// 检查坐标是否在 Canvas 范围内(增加缓冲区以允许部分可见)
|
||||
// 检查坐标是否在 Canvas 范围内
|
||||
const buffer = 100
|
||||
const isInBounds =
|
||||
(localX1 >= -buffer || localX2 >= -buffer) &&
|
||||
@@ -119,7 +120,7 @@ const performDraw = () => {
|
||||
|
||||
if (!isInBounds) return
|
||||
|
||||
// 贝塞尔曲线控制点(与 GanttLinks 一致)
|
||||
// 贝塞尔曲线控制点
|
||||
const c1x = localX1 + 40
|
||||
const c1y = localY1
|
||||
const c2x = localX2 - 40
|
||||
@@ -128,16 +129,25 @@ const performDraw = () => {
|
||||
ctx.save()
|
||||
|
||||
// 根据是否是合法目标设置颜色
|
||||
const color = props.isValidTarget ? '#67c23a' : '#f56c6c'
|
||||
const color = isValidTarget ? '#67c23a' : '#f56c6c'
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = 3
|
||||
ctx.setLineDash([8, 4])
|
||||
// 🔧 测试:移除虚线,看是否是虚线+贝塞尔曲线导致的性能问题
|
||||
// ctx.setLineDash([8, 4])
|
||||
ctx.globalAlpha = 0.8
|
||||
|
||||
// 绘制贝塞尔曲线
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(localX1, localY1)
|
||||
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, localX2, localY2)
|
||||
|
||||
if (USE_SIMPLE_LINE) {
|
||||
// 🚀 性能优化:使用简单直线(性能最佳)
|
||||
ctx.lineTo(localX2, localY2)
|
||||
} else {
|
||||
// 使用贝塞尔曲线(视觉效果好但性能较差)
|
||||
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, localX2, localY2)
|
||||
}
|
||||
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制箭头
|
||||
@@ -160,25 +170,23 @@ const performDraw = () => {
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
// 绘制错误提示文字(当连接无效且有错误消息时)
|
||||
if (!props.isValidTarget && props.errorMessage) {
|
||||
// 绘制错误提示文字
|
||||
if (!isValidTarget && errorMessage) {
|
||||
const textX = (localX1 + localX2) / 2
|
||||
const textY = (localY1 + localY2) / 2 - 10
|
||||
|
||||
// 文字背景
|
||||
ctx.font = '12px Arial, sans-serif'
|
||||
const textMetrics = ctx.measureText(props.errorMessage)
|
||||
const textMetrics = ctx.measureText(errorMessage)
|
||||
const textWidth = textMetrics.width
|
||||
const padding = 8
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'
|
||||
ctx.fillRect(textX - textWidth / 2 - padding, textY - 12, textWidth + padding * 2, 24)
|
||||
|
||||
// 文字内容
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(props.errorMessage, textX, textY)
|
||||
ctx.fillText(errorMessage, textX, textY)
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
@@ -203,54 +211,39 @@ const performDraw = () => {
|
||||
/**
|
||||
* 清空画布
|
||||
*/
|
||||
const clearCanvas = () => {
|
||||
const ctx = cachedCtx
|
||||
if (!ctx) return
|
||||
|
||||
ctx.clearRect(0, 0, cachedWidth, cachedHeight)
|
||||
const clear = () => {
|
||||
if (!cachedCtx) return
|
||||
cachedCtx.clearRect(0, 0, cachedWidth, cachedHeight)
|
||||
}
|
||||
|
||||
// 监听尺寸变化,需要重新初始化 canvas
|
||||
watch(
|
||||
[() => props.width, () => props.height],
|
||||
() => {
|
||||
cachedCtx = null // 清除缓存,下次绘制时重新初始化
|
||||
},
|
||||
)
|
||||
|
||||
// 监听 active 变化,清除缓存(因为 v-if 会移除/重建 canvas 元素)
|
||||
watch(
|
||||
() => props.active,
|
||||
(newActive) => {
|
||||
if (!newActive) {
|
||||
// canvas 即将被移除,清除缓存
|
||||
cachedCtx = null
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// 组件卸载时清除缓存
|
||||
onUnmounted(() => {
|
||||
cachedCtx = null
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.active,
|
||||
() => props.startX,
|
||||
() => props.startY,
|
||||
() => props.endX,
|
||||
() => props.endY,
|
||||
() => props.isValidTarget,
|
||||
() => props.errorMessage,
|
||||
],
|
||||
() => {
|
||||
if (props.active) {
|
||||
drawGuideLine()
|
||||
} else {
|
||||
clearCanvas()
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' }, // 同步执行,立即响应坐标变化
|
||||
)
|
||||
|
||||
// 监听尺寸变化,需要重新初始化 canvas(尺寸变化频率低)
|
||||
watch(
|
||||
[() => props.width, () => props.height],
|
||||
() => {
|
||||
// 清除缓存,强制重新初始化
|
||||
cachedCtx = null
|
||||
if (props.active) {
|
||||
drawGuideLine()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.active) {
|
||||
drawGuideLine()
|
||||
}
|
||||
// 🚀 暴露命令式 API 给父组件
|
||||
defineExpose({
|
||||
draw,
|
||||
clear,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -653,13 +653,17 @@ const DISABLE_REACTIVE_PROPS_TO_TASKBAR = true // 设为 true 测试是否是 Vu
|
||||
|
||||
const dragLinkMode = ref<'predecessor' | 'successor' | null>(null) // 当前拖拽模式
|
||||
const linkDragSourceTask = shallowRef<Task | null>(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<Task | null>(null) // 当前悬停的目标任务(使用 shallowRef 优化性能)
|
||||
const isValidLinkTarget = ref(false) // 是否是有效的连接目标
|
||||
const linkValidationError = ref<string>('') // 连接验证失败的原因
|
||||
const isValidLinkTarget = ref(false) // 是否是有效的连接目标(保留用于 handleLinkDragEnd)
|
||||
const linkValidationError = ref<string>('') // 连接验证失败的原因(保留用于兼容)
|
||||
const linkAutoScrollInterval = ref<number | null>(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<HTMLElement | null>(null)
|
||||
// 🚀 LinkDragGuide 命令式 API 引用
|
||||
const linkDragGuideRef = ref<InstanceType<typeof LinkDragGuide> | null>(null)
|
||||
const svgWidth = ref(0)
|
||||
const svgHeight = ref(0)
|
||||
|
||||
@@ -4078,20 +4220,14 @@ const handleAddSuccessor = (task: Task) => {
|
||||
:show-vertical-lines="currentTimeScale === TimelineScale.WEEK"
|
||||
/>
|
||||
|
||||
<!-- 连接线拖拽引导线 -->
|
||||
<!-- 连接线拖拽引导线 - 🚀 优化:使用命令式 API,由 RAF 直接调用 draw() -->
|
||||
<LinkDragGuide
|
||||
v-if="dragLinkMode && linkDragSourceTask"
|
||||
:active="true"
|
||||
:start-x="getLinkDragStartX()"
|
||||
:start-y="getLinkDragStartY()"
|
||||
:end-x="linkDragCurrentX"
|
||||
:end-y="linkDragCurrentY"
|
||||
ref="linkDragGuideRef"
|
||||
:active="!!dragLinkMode && !!linkDragSourceTask"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
:offset-left="canvasOffsetLeft"
|
||||
:offset-top="canvasOffsetTop"
|
||||
:is-valid-target="isValidLinkTarget"
|
||||
:error-message="linkValidationError"
|
||||
/>
|
||||
|
||||
<!-- 年度视图今日标记线 -->
|
||||
|
||||
Reference in New Issue
Block a user