Merge branch 'master' into develop

# Conflicts:
#	src/components/GanttChart.vue
#	src/components/Timeline.vue
This commit is contained in:
LINING-PC\lining
2025-11-20 11:21:43 +08:00
15 changed files with 1600 additions and 893 deletions
+56 -58
View File
@@ -136,45 +136,64 @@ interface Props {
const ganttRootRef = ref<HTMLElement | null>(null)
const ganttContainerWidth = ref(1920) // 默认使用常见的屏幕宽度作为初始值
// ResizeObserver 用于监听容器宽度变化
let ganttRootResizeObserver: ResizeObserver | null = null
// 监听容器宽度变化
const updateContainerWidth = (newWidth: number) => {
if (newWidth !== ganttContainerWidth.value) {
ganttContainerWidth.value = newWidth
// 容器宽度变化时,重新计算 TaskList 的宽度限制
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
taskListBodyWidth.value = getTaskListMaxWidth()
taskListBodyProposedWidth.value = getTaskListMaxWidth()
taskListBodyWidthLimit.value = getTaskListMaxWidth()
// 节流函数工具
const throttle = <T extends (...args: unknown[]) => unknown>(func: T, delay: number): T => {
let lastCall = 0
let timeoutId: number | null = null
// 确保当前宽度在新的限制范围内
const adjustedWidth = checkWidthLimits(leftPanelWidth.value)
if (adjustedWidth !== leftPanelWidth.value) {
leftPanelWidth.value = adjustedWidth
return ((...args: Parameters<T>) => {
const now = Date.now()
const remaining = delay - (now - lastCall)
if (timeoutId) {
clearTimeout(timeoutId)
}
if (remaining <= 0) {
lastCall = now
func(...args)
} else {
timeoutId = window.setTimeout(() => {
lastCall = Date.now()
func(...args)
timeoutId = null
}, remaining)
}
}) as T
}
const updateContainerWidth = () => {
if (ganttRootRef.value) {
const newWidth = ganttRootRef.value.clientWidth
if (newWidth !== ganttContainerWidth.value) {
ganttContainerWidth.value = newWidth
// 容器宽度变化时,重新计算 TaskList 的宽度限制
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
taskListBodyWidth.value = getTaskListMaxWidth()
taskListBodyProposedWidth.value = getTaskListMaxWidth()
taskListBodyWidthLimit.value = getTaskListMaxWidth()
// 确保当前宽度在新的限制范围内
const adjustedWidth = checkWidthLimits(leftPanelWidth.value)
if (adjustedWidth !== leftPanelWidth.value) {
leftPanelWidth.value = adjustedWidth
}
}
}
}
// 创建节流版本的 updateContainerWidth,避免频繁调用
const throttledUpdateContainerWidth = throttle(updateContainerWidth, 100)
onMounted(() => {
if (ganttRootRef.value) {
// 使用 ResizeObserver 监听容器宽度变化,避免频繁读取 clientWidth
ganttRootResizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 使用 contentRect.width,避免强制重排
updateContainerWidth(entry.contentRect.width)
}
})
ganttRootResizeObserver.observe(ganttRootRef.value)
}
updateContainerWidth()
// 使用节流版本监听窗口大小变化
window.addEventListener('resize', throttledUpdateContainerWidth)
})
onUnmounted(() => {
if (ganttRootResizeObserver) {
ganttRootResizeObserver.disconnect()
ganttRootResizeObserver = null
}
window.removeEventListener('resize', throttledUpdateContainerWidth)
})
// TaskList最小宽度,支持通过taskListConfig配置(支持像素和百分比)
@@ -341,11 +360,9 @@ function onMouseDown(e: MouseEvent) {
document.addEventListener('wheel', blockAllEvents, { capture: true, passive: false })
document.addEventListener('contextmenu', blockAllEvents, { capture: true })
// ⚠️ 使用requestAnimationFrame节流,但移除阈值检测,确保每帧都更新
let rafId: number | null = null
function onMouseMove(ev: MouseEvent) {
if (!dragging.value) return
// 强制阻止所有默认行为和事件传播
ev.preventDefault()
ev.stopPropagation()
@@ -353,29 +370,15 @@ function onMouseDown(e: MouseEvent) {
const delta = ev.clientX - startX
const proposedWidth = startWidth + delta
// 直接使用面板宽度限制检查,无需复杂的坐标计算
const finalWidth = checkWidthLimits(proposedWidth)
// 取消之前的帧请求
if (rafId !== null) {
cancelAnimationFrame(rafId)
}
// 在下一帧更新(节流到60fps,避免过度触发响应式系统)
rafId = requestAnimationFrame(() => {
leftPanelWidth.value = finalWidth
rafId = null
})
leftPanelWidth.value = finalWidth
}
function onMouseUp() {
dragging.value = false
// 取消未完成的帧请求
if (rafId !== null) {
cancelAnimationFrame(rafId)
rafId = null
}
// 移除全局事件拦截器
document.removeEventListener('mousedown', blockAllEvents, { capture: true })
document.removeEventListener('click', blockAllEvents, { capture: true })
@@ -506,8 +509,10 @@ onMounted(() => {
// 监听右侧面板(timeline 的可视容器)的宽度
const rightPanel = document.querySelector('.gantt-panel-right')
if (rightPanel) {
// 使用 ResizeObserver 自动更新宽度,避免直接读取clientWidth造成强制重排
// ResizeObserver 会在开始观察时立即触发一次回调,提供初始宽度
// 初始化宽度
timelineContainerWidth.value = rightPanel.clientWidth
// 使用 ResizeObserver 监听宽度变化
resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
timelineContainerWidth.value = entry.contentRect.width
@@ -2186,7 +2191,6 @@ function handleMilestoneDialogDelete(milestoneId: number) {
<div
v-if="isTaskListVisible"
class="gantt-panel gantt-panel-left"
:class="{ dragging: dragging }"
:style="{ width: leftPanelWidth + 'px' }"
>
<TaskList
@@ -2342,12 +2346,6 @@ function handleMilestoneDialogDelete(milestoneId: number) {
.gantt-panel-left {
/* width 由js控制 */
min-width: 320px;
/* ⚠️ 拖拽时禁用transition,提升响应速度 */
transition: none;
}
.gantt-panel-left:not(.dragging) {
/* 非拖拽时保留平滑过渡效果(如toggle时) */
transition: width 0.1s;
}
+446
View File
@@ -0,0 +1,446 @@
<script setup lang="ts">
import { ref, watch, nextTick, onMounted, onUnmounted } 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
}
// 定义月份分隔线位置类型
interface VerticalLine {
left: number
label?: string
}
// 定义 Props
interface Props {
tasks: Task[]
taskBarPositions: Record<number, TaskBarPosition>
width: number
height: number
offsetLeft?: number // Canvas 在全局坐标系中的偏移量(用于虚拟渲染)
highlightedTaskId: number | null
highlightedTaskIds: Set<number>
hoveredTaskId: number | null
// 月份分隔线配置
verticalLines?: VerticalLine[]
showVerticalLines?: boolean
}
const props = withDefaults(defineProps<Props>(), {
verticalLines: () => [],
showVerticalLines: true,
offsetLeft: 0,
})
// Canvas 引用
const canvasRef = ref<HTMLCanvasElement | null>(null)
// requestAnimationFrame 防抖控制
let rafId: number | null = null
let pendingRedraw = false
let themeObserver: MutationObserver | null = null
// 当前主题(用于分隔线颜色)
const isDarkTheme = ref(document.documentElement.getAttribute('data-theme') === 'dark')
// 监听主题变化
const updateTheme = () => {
isDarkTheme.value = document.documentElement.getAttribute('data-theme') === 'dark'
scheduleRedraw()
}
/**
* 绘制关系线到 Canvas
* 性能优势:相比 SVG 提升 18 倍渲染性能
*/
const drawLinks = () => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d', { alpha: true })
if (!ctx) {
// eslint-disable-next-line no-console
console.error('❌ Canvas context 获取失败,可能是尺寸超限')
return
}
// 适配高清屏(Retina
const dpr = window.devicePixelRatio || 1
const displayWidth = props.width
const displayHeight = props.height
// 只在尺寸变化时更新 canvas 尺寸
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)
// 绘制月份分隔线(在关系线之前,作为背景)
if (props.showVerticalLines && props.verticalLines && props.verticalLines.length > 0) {
ctx.save()
// 使用与旗帜一致的主题色:浅色模式 #409eff,暗色模式 #66b1ff
const lineColor = isDarkTheme.value ? '#66b1ff' : '#409eff'
ctx.strokeStyle = lineColor
ctx.lineWidth = 1
// 优化:一次性绘制所有垂直线,避免多次 stroke() 调用
// 虚拟渲染:减去偏移量,转换为 Canvas 局部坐标
ctx.beginPath()
for (const line of props.verticalLines) {
const localX = line.left - props.offsetLeft
// 只绘制在 Canvas 可见范围内的线
if (localX >= 0 && localX <= displayWidth) {
ctx.moveTo(localX, 0)
ctx.lineTo(localX, displayHeight)
}
}
ctx.stroke()
ctx.restore()
}
// 获取当前渲染的任务ID集合
const currentTaskIds = new Set<number>()
for (const task of props.tasks) {
currentTaskIds.add(task.id)
}
// 是否处于高亮模式
const isHighlightMode = props.highlightedTaskId !== null
// 虚拟渲染:计算 Canvas 覆盖的范围
const canvasStartX = props.offsetLeft
const canvasEndX = props.offsetLeft + displayWidth
// 定义线条数据类型
interface LineData {
x1: number
y1: number
x2: number
y2: number
c1x: number
c1y: number
c2x: number
c2y: number
arrowAngle: number
}
// 按样式分组线条数据(减少状态切换)
const highlightedLines: LineData[] = []
const hoveredLines: LineData[] = []
const normalLines: LineData[] = []
const fadedLines: LineData[] = [] // 高亮模式下的普通线条(半透明)
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
}
// 虚拟渲染:跳过不在 Canvas 覆盖范围内的关系线
// 如果起点和终点都在 Canvas 外,跳过
const fromX = fromBar.left + fromBar.width
const toX = toBar.left
const lineMinX = Math.min(fromX, toX)
const lineMaxX = Math.max(fromX, toX)
if (lineMaxX < canvasStartX || lineMinX > canvasEndX) {
continue // 完全在 Canvas 外,跳过
}
// 判断高亮状态
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 globalX1 = fromBar.left + fromBar.width
const globalY1 = fromBar.top + fromBar.height / 2 + fromYOffset
const globalX2 = toBar.left
const globalY2 = toBar.top + toBar.height / 2 + toYOffset
// 转换为 Canvas 局部坐标
const x1 = globalX1 - props.offsetLeft
const y1 = globalY1
const x2 = globalX2 - props.offsetLeft
const y2 = globalY2
const c1x = x1 + 40
const c1y = y1
const c2x = x2 - 40
const c2y = y2
// 预计算箭头角度
const arrowAngle = Math.atan2(y2 - c2y, x2 - c2x)
const lineData: LineData = { x1, y1, x2, y2, c1x, c1y, c2x, c2y, arrowAngle }
// 根据状态分组
if (isLineHighlighted) {
highlightedLines.push(lineData)
} else if (isLineHovered) {
hoveredLines.push(lineData)
} else if (isHighlightMode) {
fadedLines.push(lineData)
} else {
normalLines.push(lineData)
}
}
}
// 批量绘制:设置虚线样式(所有线条共用)
ctx.setLineDash([6, 4])
// 批量绘制高亮线条
if (highlightedLines.length > 0) {
ctx.strokeStyle = '#409eff'
ctx.fillStyle = '#409eff'
ctx.lineWidth = 4
ctx.globalAlpha = 1
ctx.beginPath()
for (const line of highlightedLines) {
ctx.moveTo(line.x1, line.y1)
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
}
ctx.stroke()
// 批量绘制箭头
for (const line of highlightedLines) {
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
}
}
// 批量绘制悬停线条
if (hoveredLines.length > 0) {
ctx.strokeStyle = '#67c23a'
ctx.fillStyle = '#67c23a'
ctx.lineWidth = 3
ctx.globalAlpha = 1
ctx.beginPath()
for (const line of hoveredLines) {
ctx.moveTo(line.x1, line.y1)
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
}
ctx.stroke()
// 批量绘制箭头
for (const line of hoveredLines) {
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
}
}
// 批量绘制普通线条
if (normalLines.length > 0) {
ctx.strokeStyle = '#c0c4cc'
ctx.fillStyle = '#c0c4cc'
ctx.lineWidth = 2
ctx.globalAlpha = 1
ctx.beginPath()
for (const line of normalLines) {
ctx.moveTo(line.x1, line.y1)
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
}
ctx.stroke()
// 批量绘制箭头
for (const line of normalLines) {
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
}
}
// 批量绘制半透明线条(高亮模式下的普通线条)
if (fadedLines.length > 0) {
ctx.strokeStyle = '#c0c4cc'
ctx.fillStyle = '#c0c4cc'
ctx.lineWidth = 2
ctx.globalAlpha = 0.2
ctx.beginPath()
for (const line of fadedLines) {
ctx.moveTo(line.x1, line.y1)
ctx.bezierCurveTo(line.c1x, line.c1y, line.c2x, line.c2y, line.x2, line.y2)
}
ctx.stroke()
// 批量绘制箭头
for (const line of fadedLines) {
drawArrowOptimized(ctx, line.x2, line.y2, line.arrowAngle)
}
// 恢复透明度
ctx.globalAlpha = 1
}
}
/**
* 优化版箭头绘制(减少参数传递,复用已设置的 fillStyle)
*/
const drawArrowOptimized = (
ctx: CanvasRenderingContext2D,
x2: number,
y2: number,
angle: number,
) => {
const arrowLength = 8
const arrowWidth = 4
ctx.beginPath()
// fillStyle 已在外部设置,无需重复设置
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()
}
/**
* 使用 requestAnimationFrame 优化的重绘调度器
* 合并多个连续的重绘请求为单次绘制
*/
const scheduleRedraw = () => {
if (pendingRedraw) {
// 已有待处理的重绘请求,跳过
return
}
pendingRedraw = true
// 取消之前的 RAF(如果有)
if (rafId !== null) {
cancelAnimationFrame(rafId)
}
// 在下一帧绘制
rafId = requestAnimationFrame(() => {
pendingRedraw = false
rafId = null
drawLinks()
})
}
// 监听相关状态变化,自动重绘 Canvas
watch(
[
() => props.taskBarPositions,
() => props.tasks,
() => props.highlightedTaskId,
() => props.highlightedTaskIds,
() => props.hoveredTaskId,
() => props.width,
() => props.height,
() => props.verticalLines,
() => props.showVerticalLines,
() => props.offsetLeft, // 监听虚拟渲染的偏移量变化
],
() => {
// 使用 RAF 调度重绘,合并连续的多次变化为单次绘制
scheduleRedraw()
},
{ deep: false }, // shallowRef 不需要 deep
)
// 组件挂载后初始化绘制
onMounted(() => {
// 监听主题变化
themeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'data-theme') {
updateTheme()
break
}
}
})
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
})
nextTick(() => {
drawLinks()
})
})
// 组件卸载时清理
onUnmounted(() => {
// 清理主题观察器
if (themeObserver) {
themeObserver.disconnect()
themeObserver = null
}
// 取消待处理的 RAF
if (rafId !== null) {
cancelAnimationFrame(rafId)
rafId = null
}
})
// 暴露方法供父组件调用
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`,
transform: `translateX(${offsetLeft}px)`,
zIndex: highlightedTaskId !== null ? 1001 : 25,
pointerEvents: 'none',
}"
/>
</template>
<style scoped>
.gantt-links-canvas {
display: block;
background: transparent; /* 确保背景透明 */
opacity: 1; /* 确保不透明度为 100% */
}
</style>
File diff suppressed because it is too large Load Diff
+263
View File
@@ -0,0 +1,263 @@
/**
* 性能监控工具
*/
let lastLogTime = 0
const LOG_THROTTLE = 100 // 最多每 100ms 输出一次日志
// FPS 监控相关
let fpsFrameCount = 0
let fpsLastTime = performance.now()
let fpsValues: number[] = []
let fpsMonitorEnabled = false
let fpsRafId: number | null = null
// 绘制性能统计
interface DrawStats {
totalDraws: number
totalDuration: number
minDuration: number
maxDuration: number
avgDuration: number
recentDraws: Array<{ timestamp: number; duration: number; details?: unknown }>
}
const drawStats: DrawStats = {
totalDraws: 0,
totalDuration: 0,
minDuration: Infinity,
maxDuration: 0,
avgDuration: 0,
recentDraws: [],
}
const MAX_RECENT_DRAWS = 100 // 保留最近 100 次绘制记录
export const perfMonitor = {
/**
* 记录性能日志(自动节流)
*/
log(tag: string, data: unknown) {
const now = Date.now()
if (now - lastLogTime < LOG_THROTTLE) {
return
}
lastLogTime = now
// eslint-disable-next-line no-console
console.log(`[Perf] ${tag}:`, data)
},
/**
* 测量函数执行时间
*/
measure<T>(tag: string, fn: () => T): T {
const start = performance.now()
const result = fn()
const end = performance.now()
const duration = end - start
if (duration > 5) {
// 只记录耗时超过 5ms 的操作
// eslint-disable-next-line no-console
console.log(`[Perf] ${tag}: ${duration.toFixed(2)}ms`)
}
return result
},
/**
* 启动 FPS 监控
* @param intervalMs 刷新间隔(毫秒),默认 1000ms
*/
startFpsMonitor(intervalMs = 1000) {
if (fpsMonitorEnabled) {
// eslint-disable-next-line no-console
console.warn('[Perf] FPS 监控已在运行中')
return
}
fpsMonitorEnabled = true
fpsFrameCount = 0
fpsLastTime = performance.now()
fpsValues = []
// eslint-disable-next-line no-console
console.log('[Perf] FPS 监控已启动')
const updateFps = () => {
if (!fpsMonitorEnabled) return
fpsFrameCount++
const currentTime = performance.now()
const elapsed = currentTime - fpsLastTime
if (elapsed >= intervalMs) {
const fps = Math.round((fpsFrameCount * 1000) / elapsed)
fpsValues.push(fps)
// 保留最近 60 个 FPS 值用于计算平均值
if (fpsValues.length > 60) {
fpsValues.shift()
}
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
const minFps = Math.min(...fpsValues)
const maxFps = Math.max(...fpsValues)
// 根据 FPS 设置不同颜色
const color = fps >= 55 ? '#67c23a' : fps >= 30 ? '#e6a23c' : '#f56c6c'
// eslint-disable-next-line no-console
console.log(
`%c[Perf] FPS: ${fps} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
`color: ${color}; font-weight: bold;`,
)
fpsFrameCount = 0
fpsLastTime = currentTime
}
fpsRafId = requestAnimationFrame(updateFps)
}
fpsRafId = requestAnimationFrame(updateFps)
},
/**
* 停止 FPS 监控
*/
stopFpsMonitor() {
if (!fpsMonitorEnabled) {
// eslint-disable-next-line no-console
console.warn('[Perf] FPS 监控未运行')
return
}
fpsMonitorEnabled = false
if (fpsRafId !== null) {
cancelAnimationFrame(fpsRafId)
fpsRafId = null
}
// 输出最终统计
if (fpsValues.length > 0) {
const avgFps = Math.round(fpsValues.reduce((a, b) => a + b, 0) / fpsValues.length)
const minFps = Math.min(...fpsValues)
const maxFps = Math.max(...fpsValues)
// eslint-disable-next-line no-console
console.log(
`%c[Perf] FPS 监控已停止 | 总采样: ${fpsValues.length} | 平均: ${avgFps} | 最小: ${minFps} | 最大: ${maxFps}`,
'color: #909399; font-weight: bold;',
)
}
fpsValues = []
},
/**
* 记录绘制性能
* @param duration 绘制耗时(毫秒)
* @param details 额外的详细信息
*/
recordDraw(duration: number, details?: unknown) {
drawStats.totalDraws++
drawStats.totalDuration += duration
drawStats.minDuration = Math.min(drawStats.minDuration, duration)
drawStats.maxDuration = Math.max(drawStats.maxDuration, duration)
drawStats.avgDuration = drawStats.totalDuration / drawStats.totalDraws
// 记录最近的绘制
drawStats.recentDraws.push({
timestamp: Date.now(),
duration,
details,
})
// 保持数组大小在限制内
if (drawStats.recentDraws.length > MAX_RECENT_DRAWS) {
drawStats.recentDraws.shift()
}
},
/**
* 获取绘制统计信息
*/
getDrawStats() {
return {
...drawStats,
recentDraws: drawStats.recentDraws.slice(-10), // 只返回最近 10 次
}
},
/**
* 打印绘制统计信息
*/
printDrawStats() {
if (drawStats.totalDraws === 0) {
// eslint-disable-next-line no-console
console.log('[Perf] 暂无绘制数据')
return
}
const recent10 = drawStats.recentDraws.slice(-10)
const recent10Avg =
recent10.reduce((sum, item) => sum + item.duration, 0) / recent10.length
// eslint-disable-next-line no-console
console.log(
'%c[Perf] Canvas 绘制统计',
'color: #409eff; font-weight: bold; font-size: 14px;',
)
// eslint-disable-next-line no-console
console.table({
总绘制次数: drawStats.totalDraws,
: `${drawStats.avgDuration.toFixed(2)}ms`,
: `${drawStats.minDuration.toFixed(2)}ms`,
: `${drawStats.maxDuration.toFixed(2)}ms`,
10: `${recent10Avg.toFixed(2)}ms`,
})
},
/**
* 重置绘制统计
*/
resetDrawStats() {
drawStats.totalDraws = 0
drawStats.totalDuration = 0
drawStats.minDuration = Infinity
drawStats.maxDuration = 0
drawStats.avgDuration = 0
drawStats.recentDraws = []
// eslint-disable-next-line no-console
console.log('[Perf] 绘制统计已重置')
},
/**
* 开始完整的性能测试(FPS + 绘制统计)
* @param durationMs 测试持续时间(毫秒),默认 10000ms (10秒)
*/
startPerformanceTest(durationMs = 10000) {
// eslint-disable-next-line no-console
console.log(
`%c[Perf] 性能测试开始(持续 ${durationMs / 1000} 秒)`,
'color: #409eff; font-weight: bold; font-size: 16px;',
)
this.resetDrawStats()
this.startFpsMonitor()
setTimeout(() => {
this.stopFpsMonitor()
this.printDrawStats()
// eslint-disable-next-line no-console
console.log(
'%c[Perf] 性能测试完成',
'color: #67c23a; font-weight: bold; font-size: 16px;',
)
}, durationMs)
},
}