清理调试代码,抽取canvas功能封装

This commit is contained in:
qiuchengw
2025-12-03 10:47:45 +08:00
parent 9db16b4e16
commit f13a1ff128
4 changed files with 229 additions and 121 deletions
+12 -40
View File
@@ -2,15 +2,7 @@
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue' import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import type { Task } from '../models/classes/Task' import type { Task } from '../models/classes/Task'
import { getPredecessorIds } from '../utils/predecessorUtils' import { getPredecessorIds } from '../utils/predecessorUtils'
import { CanvasContextManager } from '../utils/canvasUtils'
// uni-app 类型声明
declare global {
interface Window {
uni?: {
createCanvasContext: (canvasId: string) => CanvasRenderingContext2D & { draw?: () => void }
}
}
}
// 定义 TaskBar 位置信息类型 // 定义 TaskBar 位置信息类型
interface TaskBarPosition { interface TaskBarPosition {
@@ -52,6 +44,9 @@ const props = withDefaults(defineProps<Props>(), {
// Canvas 引用 // Canvas 引用
const canvasRef = ref<HTMLCanvasElement | null>(null) const canvasRef = ref<HTMLCanvasElement | null>(null)
// Canvas 上下文管理器
const canvasManager = new CanvasContextManager()
// requestAnimationFrame 防抖控制 // requestAnimationFrame 防抖控制
let rafId: number | null = null let rafId: number | null = null
let pendingRedraw = false let pendingRedraw = false
@@ -71,22 +66,13 @@ const updateTheme = () => {
* 性能优势:相比 SVG 提升 18 倍渲染性能 * 性能优势:相比 SVG 提升 18 倍渲染性能
*/ */
const drawLinks = () => { const drawLinks = () => {
const canvas = canvasRef.value const ctx = canvasManager.getContext({
if (!canvas) return canvas: canvasRef.value,
canvasId: 'gantt-link-canvas',
// 判断是否在 uni-app 环境中 width: props.width,
const isUniApp = typeof window !== 'undefined' && window.uni height: props.height,
let ctx: (CanvasRenderingContext2D & { draw?: () => void }) | null = null contextOptions: { alpha: true },
})
if (isUniApp && window.uni) {
// uni-app 环境:使用 uni.createCanvasContext
// https://uniapp.dcloud.net.cn/component/canvas.html
// 这儿存在一个问题:如果一个应用多个gantt视图会存在id冲突问题,所以后期可以加上一个配置,外部传入canvas-id
ctx = window.uni.createCanvasContext('gantt-link-canvas')
} else {
// Web 环境:使用标准 Canvas API
ctx = canvas.getContext('2d', { alpha: true })
}
if (!ctx) { if (!ctx) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@@ -97,18 +83,6 @@ const drawLinks = () => {
const displayWidth = props.width const displayWidth = props.width
const displayHeight = props.height const displayHeight = props.height
// Web 环境才需要手动处理 DPRuni-app 通过 hidpi 属性自动处理)
if (!isUniApp) {
const dpr = window.devicePixelRatio || 1
const pixelWidth = displayWidth * dpr
const pixelHeight = displayHeight * dpr
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth
canvas.height = pixelHeight
ctx.scale(dpr, dpr)
}
}
// 清空画布(透明) // 清空画布(透明)
ctx.clearRect(0, 0, displayWidth, displayHeight) ctx.clearRect(0, 0, displayWidth, displayHeight)
@@ -336,9 +310,7 @@ const drawLinks = () => {
} }
// uni-app 环境需要调用 draw() 方法将绘制内容渲染到画布 // uni-app 环境需要调用 draw() 方法将绘制内容渲染到画布
if (isUniApp && ctx?.draw) { canvasManager.draw()
ctx.draw()
}
} }
/** /**
+22 -43
View File
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onUnmounted } from 'vue' import { ref, watch, onUnmounted } from 'vue'
import { CanvasContextManager } from '../utils/canvasUtils'
interface Props { interface Props {
active: boolean active: boolean
@@ -17,47 +18,20 @@ const props = withDefaults(defineProps<Props>(), {
const canvasRef = ref<HTMLCanvasElement | null>(null) const canvasRef = ref<HTMLCanvasElement | null>(null)
// 缓存 canvas 上下文和尺寸信息,避免重复初始化 // Canvas 上下文管理器
let cachedCtx: CanvasRenderingContext2D | null = null const canvasManager = new CanvasContextManager()
let cachedWidth = 0
let cachedHeight = 0
let cachedDpr = 0
/** /**
* 初始化或更新 canvas 尺寸 * 初始化或更新 canvas 尺寸
*/ */
const initCanvas = () => { const initCanvas = () => {
const canvas = canvasRef.value return canvasManager.getContext({
if (!canvas) return null canvas: canvasRef.value,
canvasId: 'link-drag-guide-canvas',
const displayWidth = props.width width: props.width,
const displayHeight = props.height height: props.height,
const dpr = window.devicePixelRatio || 1 contextOptions: { alpha: true, willReadFrequently: false },
})
// 只在尺寸或 DPR 变化时重新初始化
if (
!cachedCtx ||
cachedWidth !== displayWidth ||
cachedHeight !== displayHeight ||
cachedDpr !== dpr
) {
const ctx = canvas.getContext('2d', { alpha: true, willReadFrequently: false })
if (!ctx) return null
const pixelWidth = displayWidth * dpr
const pixelHeight = displayHeight * dpr
canvas.width = pixelWidth
canvas.height = pixelHeight
ctx.scale(dpr, dpr)
cachedCtx = ctx
cachedWidth = displayWidth
cachedHeight = displayHeight
cachedDpr = dpr
}
return cachedCtx
} }
/** /**
@@ -86,8 +60,8 @@ const draw = (
return return
} }
const displayWidth = cachedWidth const displayWidth = props.width
const displayHeight = cachedHeight const displayHeight = props.height
// 清空画布 // 清空画布
ctx.clearRect(0, 0, displayWidth, displayHeight) ctx.clearRect(0, 0, displayWidth, displayHeight)
@@ -180,21 +154,23 @@ const draw = (
} }
ctx.restore() ctx.restore()
// uni-app 环境需要调用 draw() 方法将绘制内容渲染到画布
canvasManager.draw()
} }
/** /**
* 清空画布 * 清空画布
*/ */
const clear = () => { const clear = () => {
if (!cachedCtx) return canvasManager.clear(props.width, props.height)
cachedCtx.clearRect(0, 0, cachedWidth, cachedHeight)
} }
// 监听尺寸变化,需要重新初始化 canvas // 监听尺寸变化,需要重新初始化 canvas
watch( watch(
[() => props.width, () => props.height], [() => props.width, () => props.height],
() => { () => {
cachedCtx = null // 清除缓存,下次绘制时重新初始化 canvasManager.reset() // 清除缓存,下次绘制时重新初始化
}, },
) )
@@ -204,14 +180,14 @@ watch(
(newActive) => { (newActive) => {
if (!newActive) { if (!newActive) {
// canvas 即将被移除,清除缓存 // canvas 即将被移除,清除缓存
cachedCtx = null canvasManager.reset()
} }
}, },
) )
// 组件卸载时清除缓存 // 组件卸载时清除缓存
onUnmounted(() => { onUnmounted(() => {
cachedCtx = null canvasManager.reset()
}) })
// 🚀 暴露命令式 API 给父组件 // 🚀 暴露命令式 API 给父组件
@@ -225,7 +201,10 @@ defineExpose({
<canvas <canvas
v-if="active" v-if="active"
ref="canvasRef" ref="canvasRef"
canvas-id="link-drag-guide-canvas"
class="link-drag-guide-canvas" class="link-drag-guide-canvas"
type="2d"
:hidpi="true"
:style="{ :style="{
position: 'absolute', position: 'absolute',
left: `${offsetLeft}px`, left: `${offsetLeft}px`,
+7 -15
View File
@@ -666,7 +666,7 @@ let nonReactiveErrorMessage = ''
const taskIdMap = new Map<number, Task>() const taskIdMap = new Map<number, Task>()
// 性能监控开关(开发调试用) // 性能监控开关(开发调试用)
const ENABLE_PERF_MONITOR = true const ENABLE_PERF_MONITOR = false
// 开始连接线拖拽 // 开始连接线拖拽
const handleLinkDragStart = (event: { task: Task; type: 'predecessor' | 'successor'; mouseEvent: MouseEvent }) => { const handleLinkDragStart = (event: { task: Task; type: 'predecessor' | 'successor'; mouseEvent: MouseEvent }) => {
@@ -674,7 +674,9 @@ const handleLinkDragStart = (event: { task: Task; type: 'predecessor' | 'success
linkDragSourceTask.value = event.task linkDragSourceTask.value = event.task
// 启动帧监控 // 启动帧监控
if (ENABLE_PERF_MONITOR) {
perfMonitor.startFrameMonitor() perfMonitor.startFrameMonitor()
}
// 初始化鼠标坐标(使用非响应式版本) // 初始化鼠标坐标(使用非响应式版本)
updateLinkDragCoordinatesNonReactive(event.mouseEvent.clientX, event.mouseEvent.clientY) updateLinkDragCoordinatesNonReactive(event.mouseEvent.clientX, event.mouseEvent.clientY)
@@ -704,17 +706,10 @@ let pendingMouseY = 0
let currentDragX = 0 let currentDragX = 0
let currentDragY = 0 let currentDragY = 0
// 🔧 调试开关:逐步启用各操作以定位性能瓶颈
const DEBUG_ENABLE_COORD_UPDATE = true // 坐标更新 ✅ 不是瓶颈
const DEBUG_ENABLE_TARGET_DETECT = true // 目标检测 ✅ 不是瓶颈
const DEBUG_ENABLE_CANVAS_DRAW = true // Canvas 绘制 - 测试中
// 🚀 优化后的 RAF 回调:在一帧内批量处理坐标更新、目标检测和绘制 // 🚀 优化后的 RAF 回调:在一帧内批量处理坐标更新、目标检测和绘制
const processLinkDragFrame = () => { const processLinkDragFrame = () => {
linkDragRafId = null linkDragRafId = null
// 🔧 调试:逐步启用各操作
if (DEBUG_ENABLE_COORD_UPDATE) {
if (ENABLE_PERF_MONITOR) { if (ENABLE_PERF_MONITOR) {
const startTime = performance.now() const startTime = performance.now()
updateLinkDragCoordinatesNonReactive(pendingMouseX, pendingMouseY) updateLinkDragCoordinatesNonReactive(pendingMouseX, pendingMouseY)
@@ -722,9 +717,7 @@ const processLinkDragFrame = () => {
} else { } else {
updateLinkDragCoordinatesNonReactive(pendingMouseX, pendingMouseY) updateLinkDragCoordinatesNonReactive(pendingMouseX, pendingMouseY)
} }
}
if (DEBUG_ENABLE_TARGET_DETECT) {
if (ENABLE_PERF_MONITOR) { if (ENABLE_PERF_MONITOR) {
const startTime = performance.now() const startTime = performance.now()
detectLinkTargetNonReactive(pendingMouseX, pendingMouseY) detectLinkTargetNonReactive(pendingMouseX, pendingMouseY)
@@ -732,9 +725,7 @@ const processLinkDragFrame = () => {
} else { } else {
detectLinkTargetNonReactive(pendingMouseX, pendingMouseY) detectLinkTargetNonReactive(pendingMouseX, pendingMouseY)
} }
}
if (DEBUG_ENABLE_CANVAS_DRAW) {
if (linkDragGuideRef.value && linkDragSourceTask.value) { if (linkDragGuideRef.value && linkDragSourceTask.value) {
linkDragGuideRef.value.draw( linkDragGuideRef.value.draw(
getLinkDragStartX(), getLinkDragStartX(),
@@ -746,7 +737,6 @@ const processLinkDragFrame = () => {
) )
} }
} }
}
// 全局鼠标移动处理(🚀 优化:使用 RAF 统一调度,避免每次 mousemove 都触发响应式更新) // 全局鼠标移动处理(🚀 优化:使用 RAF 统一调度,避免每次 mousemove 都触发响应式更新)
const handleGlobalMouseMove = (e: MouseEvent) => { const handleGlobalMouseMove = (e: MouseEvent) => {
@@ -770,7 +760,9 @@ const handleGlobalMouseUp = () => {
if (!dragLinkMode.value) return if (!dragLinkMode.value) return
// 停止帧监控 // 停止帧监控
if (ENABLE_PERF_MONITOR) {
perfMonitor.stopFrameMonitor() perfMonitor.stopFrameMonitor()
}
// 🚀 取消待处理的 RAF // 🚀 取消待处理的 RAF
if (linkDragRafId !== null) { if (linkDragRafId !== null) {
@@ -809,8 +801,8 @@ const updateCoordinates = (mouseX: number, mouseY: number): void => {
bodyRectCacheTime = now bodyRectCacheTime = now
bodyRectInvalidated = false bodyRectInvalidated = false
} }
currentDragX = mouseX - cachedBodyRect.left currentDragX = mouseX - cachedBodyRect!.left
currentDragY = mouseY - cachedBodyRect.top currentDragY = mouseY - cachedBodyRect!.top
} }
// 🚀 非响应式坐标更新(完全绕过 Vue 响应式系统) // 🚀 非响应式坐标更新(完全绕过 Vue 响应式系统)
+165
View File
@@ -0,0 +1,165 @@
/**
* Canvas - uni-app Web
*/
// uni-app 类型声明
declare global {
interface Window {
uni?: {
createCanvasContext: (canvasId: string) => CanvasRenderingContext2D & { draw?: () => void }
}
}
}
/**
* Canvas uni-app Web
*/
export type CanvasContext = CanvasRenderingContext2D & { draw?: () => void }
/**
* Canvas
*/
export interface CanvasInitOptions {
/** Canvas 元素引用(Web 环境) */
canvas: HTMLCanvasElement | null
/** Canvas IDuni-app 环境) */
canvasId: string
/** 显示宽度 */
width: number
/** 显示高度 */
height: number
/** Canvas 上下文配置(仅 Web 环境) */
contextOptions?: CanvasRenderingContext2DSettings
}
/**
* Canvas
*/
export class CanvasContextManager {
private cachedCtx: CanvasContext | null = null
private cachedWidth = 0
private cachedHeight = 0
private isUniApp = false
constructor() {
// 判断是否在 uni-app 环境中
this.isUniApp = typeof window !== 'undefined' && !!window.uni
}
/**
* uni-app
*/
get isUniAppEnvironment(): boolean {
return this.isUniApp
}
/**
* Canvas
* @param options
* @returns Canvas null
*/
getContext(options: CanvasInitOptions): CanvasContext | null {
const { canvas, canvasId, width, height, contextOptions } = options
// 只在尺寸变化时重新初始化
if (!this.cachedCtx || this.cachedWidth !== width || this.cachedHeight !== height) {
let ctx: CanvasContext | null = null
if (this.isUniApp && window.uni) {
// uni-app 环境:使用 uni.createCanvasContext
ctx = window.uni.createCanvasContext(canvasId)
} else {
// Web 环境:使用标准 Canvas API
if (!canvas) return null
const dpr = window.devicePixelRatio || 1
ctx = canvas.getContext('2d', contextOptions || { alpha: true }) as CanvasContext
if (!ctx) return null
const pixelWidth = width * dpr
const pixelHeight = height * dpr
canvas.width = pixelWidth
canvas.height = pixelHeight
ctx.scale(dpr, dpr)
}
if (!ctx) return null
this.cachedCtx = ctx
this.cachedWidth = width
this.cachedHeight = height
}
return this.cachedCtx
}
/**
*
*/
clear(width: number, height: number): void {
if (!this.cachedCtx) return
this.cachedCtx.clearRect(0, 0, width, height)
}
/**
* uni-app uni-app
*/
draw(): void {
if (this.isUniApp && this.cachedCtx?.draw) {
this.cachedCtx.draw()
}
}
/**
* Canvas
*/
reset(): void {
this.cachedCtx = null
this.cachedWidth = 0
this.cachedHeight = 0
}
}
/**
* uni-app
*/
export function isUniAppEnvironment(): boolean {
return typeof window !== 'undefined' && !!window.uni
}
/**
* Canvas API
*/
export function createCanvasContext(
canvas: HTMLCanvasElement | null,
canvasId: string,
width: number,
height: number,
contextOptions?: CanvasRenderingContext2DSettings,
): CanvasContext | null {
const isUniApp = isUniAppEnvironment()
if (isUniApp && window.uni) {
// uni-app 环境
return window.uni.createCanvasContext(canvasId)
} else {
// Web 环境
if (!canvas) return null
const dpr = window.devicePixelRatio || 1
const ctx = canvas.getContext('2d', contextOptions || { alpha: true }) as CanvasContext
if (!ctx) return null
const pixelWidth = width * dpr
const pixelHeight = height * dpr
canvas.width = pixelWidth
canvas.height = pixelHeight
ctx.scale(dpr, dpr)
return ctx
}
}