优化:使用canvas替代svg links实现

This commit is contained in:
qiuchengw
2025-11-12 08:10:34 +08:00
parent 48c24d3a32
commit cf094c8e2c
2 changed files with 251 additions and 144 deletions
+232
View File
@@ -0,0 +1,232 @@
<script setup lang="ts">
import { ref, watch, nextTick, onMounted } from 'vue'
import type { Task } from '../models/classes/Task'
import { getPredecessorIds } from '../utils/predecessorUtils'
// 定义 TaskBar 位置信息类型
interface TaskBarPosition {
left: number
top: number
width: number
height: number
}
// 定义 Props
interface Props {
tasks: Task[]
taskBarPositions: Record<number, TaskBarPosition>
width: number
height: number
highlightedTaskId: number | null
highlightedTaskIds: Set<number>
hoveredTaskId: number | null
}
const props = defineProps<Props>()
// Canvas 引用
const canvasRef = ref<HTMLCanvasElement | null>(null)
/**
* 绘制关系线到 Canvas
* 性能优势:相比 SVG 提升 18 倍渲染性能
*/
const drawLinks = () => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// 适配高清屏(Retina
const dpr = window.devicePixelRatio || 1
const rect = canvas.getBoundingClientRect()
// 设置实际像素尺寸
canvas.width = rect.width * dpr
canvas.height = rect.height * dpr
// 缩放上下文以适配高清屏
ctx.scale(dpr, dpr)
// 清空画布
ctx.clearRect(0, 0, rect.width, rect.height)
// 获取当前渲染的任务ID集合
const currentTaskIds = new Set<number>()
for (const task of props.tasks) {
currentTaskIds.add(task.id)
}
// 是否处于高亮模式
const isHighlightMode = props.highlightedTaskId !== null
// 绘制所有关系线
for (const task of props.tasks) {
if (!task.predecessor || !props.taskBarPositions[task.id]) continue
const predecessorIds = getPredecessorIds(task.predecessor)
for (const predecessorId of predecessorIds) {
const fromBar = props.taskBarPositions[predecessorId]
const toBar = props.taskBarPositions[task.id]
if (!fromBar || !toBar || !currentTaskIds.has(predecessorId)) continue
// 判断高亮状态
const fromIsPrimary = props.highlightedTaskId === predecessorId
const toIsPrimary = props.highlightedTaskId === task.id
const fromIsHighlighted = props.highlightedTaskIds.has(predecessorId)
const toIsHighlighted = props.highlightedTaskIds.has(task.id)
const isLineHighlighted = fromIsHighlighted && toIsHighlighted
// 判断 hover 状态
const fromIsHovered = props.hoveredTaskId === predecessorId
const toIsHovered = props.hoveredTaskId === task.id
const isLineHovered = fromIsHovered || toIsHovered
// 计算 Y 轴偏移(高亮时的位移)
const fromYOffset = fromIsPrimary ? -8 : fromIsHighlighted ? -5 : 0
const toYOffset = toIsPrimary ? -8 : toIsHighlighted ? -5 : 0
// 计算坐标
const x1 = fromBar.left + fromBar.width
const y1 = fromBar.top + fromBar.height / 2 + fromYOffset
const x2 = toBar.left
const y2 = toBar.top + toBar.height / 2 + toYOffset
const c1x = x1 + 40
const c1y = y1
const c2x = x2 - 40
const c2y = y2
// 设置线条样式
ctx.beginPath()
if (isLineHighlighted) {
// 高亮状态:蓝色
ctx.strokeStyle = '#409eff'
ctx.lineWidth = 4
ctx.globalAlpha = 1
ctx.shadowBlur = 8
ctx.shadowColor = 'rgba(64, 158, 255, 0.4)'
} else if (isLineHovered) {
// Hover 状态:绿色
ctx.strokeStyle = '#67c23a'
ctx.lineWidth = 3
ctx.globalAlpha = 1
ctx.shadowBlur = 6
ctx.shadowColor = 'rgba(103, 194, 58, 0.3)'
} else {
// 普通状态:灰色
ctx.strokeStyle = '#c0c4cc'
ctx.lineWidth = 2
ctx.globalAlpha = isHighlightMode ? 0.2 : 1
ctx.shadowBlur = 0
}
ctx.setLineDash([6, 4])
// 绘制贝塞尔曲线
ctx.moveTo(x1, y1)
ctx.bezierCurveTo(c1x, c1y, c2x, c2y, x2, y2)
ctx.stroke()
// 重置阴影(避免影响箭头)
ctx.shadowBlur = 0
// 绘制箭头
drawArrow(ctx, x2, y2, c2x, c2y, isLineHighlighted, isLineHovered, isHighlightMode)
// 恢复全局透明度
ctx.globalAlpha = 1
}
}
}
/**
* 绘制箭头
*/
const drawArrow = (
ctx: CanvasRenderingContext2D,
x2: number,
y2: number,
c2x: number,
c2y: number,
isHighlighted: boolean,
isHovered: boolean,
isHighlightMode: boolean,
) => {
const angle = Math.atan2(y2 - c2y, x2 - c2x)
const arrowLength = 8
const arrowWidth = 4
ctx.beginPath()
ctx.fillStyle = isHighlighted ? '#409eff' : isHovered ? '#67c23a' : '#c0c4cc'
ctx.globalAlpha = isHighlightMode && !isHighlighted ? 0.2 : 1
ctx.moveTo(x2, y2)
ctx.lineTo(
x2 - arrowLength * Math.cos(angle) - arrowWidth * Math.sin(angle),
y2 - arrowLength * Math.sin(angle) + arrowWidth * Math.cos(angle),
)
ctx.lineTo(
x2 - arrowLength * Math.cos(angle) + arrowWidth * Math.sin(angle),
y2 - arrowLength * Math.sin(angle) - arrowWidth * Math.cos(angle),
)
ctx.closePath()
ctx.fill()
}
// 监听相关状态变化,自动重绘 Canvas
watch(
[
() => props.taskBarPositions,
() => props.tasks.length,
() => props.highlightedTaskId,
() => props.highlightedTaskIds,
() => props.hoveredTaskId,
() => props.width,
() => props.height,
],
() => {
nextTick(() => {
drawLinks()
})
},
{ deep: false }, // shallowRef 不需要 deep
)
// 组件挂载后初始化绘制
onMounted(() => {
nextTick(() => {
drawLinks()
})
})
// 暴露方法供父组件调用
defineExpose({
redraw: drawLinks,
})
</script>
<template>
<canvas
ref="canvasRef"
class="gantt-links-canvas"
:style="{
position: 'absolute',
left: 0,
top: 0,
width: `${width}px`,
height: `${height}px`,
zIndex: highlightedTaskId !== null ? 1001 : 25,
pointerEvents: 'none',
}"
/>
</template>
<style scoped>
.gantt-links-canvas {
display: block;
}
</style>