From f4555c96c33f81c6f65566275534712d61e5cc3f Mon Sep 17 00:00:00 2001 From: qiuchengw Date: Wed, 3 Dec 2025 07:11:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=BF=9E=E6=8E=A5=E7=BA=BF?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LinkDragGuide.vue | 90 +++--- src/components/Timeline.vue | 453 ++++++++++++++++++++++++------- 2 files changed, 418 insertions(+), 125 deletions(-) diff --git a/src/components/LinkDragGuide.vue b/src/components/LinkDragGuide.vue index 18eb0f8..d843061 100644 --- a/src/components/LinkDragGuide.vue +++ b/src/components/LinkDragGuide.vue @@ -12,19 +12,25 @@ interface Props { offsetLeft?: number offsetTop?: number isValidTarget?: boolean // 是否是合法的连接目标 + errorMessage?: string // 错误提示消息 } const props = withDefaults(defineProps(), { offsetLeft: 0, offsetTop: 0, isValidTarget: true, + errorMessage: '', }) const canvasRef = ref(null) -// 使用 requestAnimationFrame 节流重绘 -let rafId: number | null = null -let pendingDraw = false +// 性能监控 +const ENABLE_PERF_MONITOR = true +const SKIP_ACTUAL_DRAW = false // 调试开关:跳过实际绘制,只打印日志 +let drawCount = 0 +let drawTotalTime = 0 +let lastReportTime = 0 +let lastCallTime = 0 // 上次调用时间,用于计算间隔 // 缓存 canvas 上下文和尺寸信息,避免重复初始化 let cachedCtx: CanvasRenderingContext2D | null = null @@ -72,30 +78,22 @@ const initCanvas = () => { /** * 绘制拖拽引导线 * 使用贝塞尔曲线,与 GanttLinks 保持一致的视觉风格 + * 立即绘制,不使用RAF节流,确保跟随鼠标 */ const drawGuideLine = () => { - // 如果已经有待处理的绘制请求,取消标记 - if (rafId !== null) { - pendingDraw = true - return - } - - // 使用 requestAnimationFrame 确保在下一帧绘制 - rafId = requestAnimationFrame(() => { - rafId = null - performDraw() - - // 如果在绘制期间又有新的请求,再次绘制 - if (pendingDraw) { - pendingDraw = false - drawGuideLine() - } - }) + performDraw() } const performDraw = () => { if (!props.active) return + // 调试模式:跳过实际绘制 + if (SKIP_ACTUAL_DRAW) { + return + } + + const startTime = ENABLE_PERF_MONITOR ? performance.now() : 0 + const ctx = initCanvas() if (!ctx) return @@ -162,7 +160,44 @@ const performDraw = () => { ctx.closePath() ctx.fill() + // 绘制错误提示文字(当连接无效且有错误消息时) + if (!props.isValidTarget && props.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 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.restore() + + if (ENABLE_PERF_MONITOR) { + drawCount++ + drawTotalTime += performance.now() - startTime + + const now = Date.now() + if (now - lastReportTime > 1000) { + const avgTime = drawCount > 0 ? (drawTotalTime / drawCount).toFixed(3) : 0 + // eslint-disable-next-line no-console + console.log(`[LinkDragGuide Perf] 绘制次数: ${drawCount}/秒, 平均耗时: ${avgTime}ms`) + + drawCount = 0 + drawTotalTime = 0 + lastReportTime = now + } + } } /** @@ -177,12 +212,9 @@ const clearCanvas = () => { // 组件卸载时清除缓存 onUnmounted(() => { - if (rafId !== null) { - cancelAnimationFrame(rafId) - rafId = null - } cachedCtx = null }) + watch( [ () => props.active, @@ -191,6 +223,7 @@ watch( () => props.endX, () => props.endY, () => props.isValidTarget, + () => props.errorMessage, ], () => { if (props.active) { @@ -199,6 +232,7 @@ watch( clearCanvas() } }, + { flush: 'sync' }, // 同步执行,立即响应坐标变化 ) // 监听尺寸变化,需要重新初始化 canvas(尺寸变化频率低) @@ -218,14 +252,6 @@ onMounted(() => { drawGuideLine() } }) - -// 组件卸载时取消待处理的动画帧 -onUnmounted(() => { - if (rafId !== null) { - cancelAnimationFrame(rafId) - rafId = null - } -})