v1.4.3 - 垂直虚拟滚动优化
This commit is contained in:
@@ -24,6 +24,7 @@ interface Props {
|
|||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
offsetLeft?: number // Canvas 在全局坐标系中的偏移量(用于虚拟渲染)
|
offsetLeft?: number // Canvas 在全局坐标系中的偏移量(用于虚拟渲染)
|
||||||
|
offsetTop?: number // Canvas 在垂直方向的偏移量(用于虚拟渲染)
|
||||||
highlightedTaskId: number | null
|
highlightedTaskId: number | null
|
||||||
highlightedTaskIds: Set<number>
|
highlightedTaskIds: Set<number>
|
||||||
hoveredTaskId: number | null
|
hoveredTaskId: number | null
|
||||||
@@ -36,6 +37,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
verticalLines: () => [],
|
verticalLines: () => [],
|
||||||
showVerticalLines: true,
|
showVerticalLines: true,
|
||||||
offsetLeft: 0,
|
offsetLeft: 0,
|
||||||
|
offsetTop: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Canvas 引用
|
// Canvas 引用
|
||||||
@@ -124,6 +126,8 @@ const drawLinks = () => {
|
|||||||
// 虚拟渲染:计算 Canvas 覆盖的范围
|
// 虚拟渲染:计算 Canvas 覆盖的范围
|
||||||
const canvasStartX = props.offsetLeft
|
const canvasStartX = props.offsetLeft
|
||||||
const canvasEndX = props.offsetLeft + displayWidth
|
const canvasEndX = props.offsetLeft + displayWidth
|
||||||
|
const canvasStartY = props.offsetTop
|
||||||
|
const canvasEndY = props.offsetTop + displayHeight
|
||||||
|
|
||||||
// 定义线条数据类型
|
// 定义线条数据类型
|
||||||
interface LineData {
|
interface LineData {
|
||||||
@@ -157,16 +161,6 @@ const drawLinks = () => {
|
|||||||
continue
|
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 fromIsPrimary = props.highlightedTaskId === predecessorId
|
||||||
const toIsPrimary = props.highlightedTaskId === task.id
|
const toIsPrimary = props.highlightedTaskId === task.id
|
||||||
@@ -189,11 +183,26 @@ const drawLinks = () => {
|
|||||||
const globalX2 = toBar.left
|
const globalX2 = toBar.left
|
||||||
const globalY2 = toBar.top + toBar.height / 2 + toYOffset
|
const globalY2 = toBar.top + toBar.height / 2 + toYOffset
|
||||||
|
|
||||||
|
// 虚拟渲染:跳过不在 Canvas 覆盖范围内的关系线
|
||||||
|
// 如果起点和终点都在 Canvas 外,跳过
|
||||||
|
const lineMinX = Math.min(globalX1, globalX2)
|
||||||
|
const lineMaxX = Math.max(globalX1, globalX2)
|
||||||
|
const lineMinY = Math.min(globalY1, globalY2)
|
||||||
|
const lineMaxY = Math.max(globalY1, globalY2)
|
||||||
|
if (
|
||||||
|
lineMaxX < canvasStartX ||
|
||||||
|
lineMinX > canvasEndX ||
|
||||||
|
lineMaxY < canvasStartY ||
|
||||||
|
lineMinY > canvasEndY
|
||||||
|
) {
|
||||||
|
continue // 完全在 Canvas 外,跳过
|
||||||
|
}
|
||||||
|
|
||||||
// 转换为 Canvas 局部坐标
|
// 转换为 Canvas 局部坐标
|
||||||
const x1 = globalX1 - props.offsetLeft
|
const x1 = globalX1 - props.offsetLeft
|
||||||
const y1 = globalY1
|
const y1 = globalY1 - props.offsetTop
|
||||||
const x2 = globalX2 - props.offsetLeft
|
const x2 = globalX2 - props.offsetLeft
|
||||||
const y2 = globalY2
|
const y2 = globalY2 - props.offsetTop
|
||||||
|
|
||||||
const c1x = x1 + 40
|
const c1x = x1 + 40
|
||||||
const c1y = y1
|
const c1y = y1
|
||||||
@@ -369,6 +378,7 @@ watch(
|
|||||||
() => props.verticalLines,
|
() => props.verticalLines,
|
||||||
() => props.showVerticalLines,
|
() => props.showVerticalLines,
|
||||||
() => props.offsetLeft, // 监听虚拟渲染的偏移量变化
|
() => props.offsetLeft, // 监听虚拟渲染的偏移量变化
|
||||||
|
() => props.offsetTop,
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
// 使用 RAF 调度重绘,合并连续的多次变化为单次绘制
|
// 使用 RAF 调度重绘,合并连续的多次变化为单次绘制
|
||||||
@@ -430,7 +440,7 @@ defineExpose({
|
|||||||
top: 0,
|
top: 0,
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
transform: `translateX(${offsetLeft}px)`,
|
transform: `translate(${offsetLeft}px, ${offsetTop}px)`,
|
||||||
zIndex: highlightedTaskId !== null ? 1001 : 25,
|
zIndex: highlightedTaskId !== null ? 1001 : 25,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -746,12 +746,12 @@ const debounce = <T extends (...args: unknown[]) => void>(func: T, wait: number)
|
|||||||
// 优化的滚动处理器(增加防抖时间到 50ms)
|
// 优化的滚动处理器(增加防抖时间到 50ms)
|
||||||
const debouncedUpdatePositions = debounce(() => {
|
const debouncedUpdatePositions = debounce(() => {
|
||||||
computeAllMilestonesPositions()
|
computeAllMilestonesPositions()
|
||||||
}, 50)
|
}, 200)
|
||||||
|
|
||||||
// 虚拟渲染:防抖更新 Canvas 位置(滚动时触发)
|
// 虚拟渲染:防抖更新 Canvas 位置(滚动时触发)
|
||||||
const debouncedUpdateCanvasPosition = debounce(() => {
|
const debouncedUpdateCanvasPosition = debounce(() => {
|
||||||
updateSvgSize() // 重新计算 Canvas 位置和尺寸
|
updateSvgSize() // 重新计算 Canvas 位置和尺寸
|
||||||
}, 50)
|
}, 200)
|
||||||
|
|
||||||
// 防抖更新纵向滚动位置
|
// 防抖更新纵向滚动位置
|
||||||
const debouncedUpdateVerticalScroll = debounce((scrollTop: number) => {
|
const debouncedUpdateVerticalScroll = debounce((scrollTop: number) => {
|
||||||
@@ -1972,17 +1972,20 @@ const svgHeight = ref(0)
|
|||||||
const canvasWidth = ref(0)
|
const canvasWidth = ref(0)
|
||||||
const canvasHeight = ref(0)
|
const canvasHeight = ref(0)
|
||||||
const canvasOffsetLeft = ref(0) // Canvas 在全局坐标系中的偏移量
|
const canvasOffsetLeft = ref(0) // Canvas 在全局坐标系中的偏移量
|
||||||
|
const canvasOffsetTop = ref(0)
|
||||||
|
|
||||||
// 虚拟渲染 Canvas 的安全宽度(防止超过浏览器限制)
|
// 虚拟渲染 Canvas 的安全宽度(防止超过浏览器限制)
|
||||||
// 可根据实际需求调整:
|
// 可根据实际需求调整:
|
||||||
// - 5000: 最小内存 (~30MB),适合低端设备,但滚动时更频繁更新
|
// - 5000: 最小内存 (~30MB),适合低端设备,但滚动时更频繁更新
|
||||||
// - 10000: 平衡选择 (~60MB),覆盖小时视图 10 天,周视图 2 年
|
// - 10000: 平衡选择 (~60MB),覆盖小时视图 10 天,周视图 2 年
|
||||||
const SAFE_CANVAS_WIDTH = 5000 // 平衡性能和覆盖范围
|
const SAFE_CANVAS_WIDTH = 5000 // 平衡性能和覆盖范围
|
||||||
|
const SAFE_CANVAS_HEIGHT = 5000
|
||||||
|
|
||||||
function updateSvgSize() {
|
function updateSvgSize() {
|
||||||
if (bodyContentRef.value) {
|
if (bodyContentRef.value) {
|
||||||
// 获取 bodyContent 的总宽度和可视区域宽度
|
// 获取 bodyContent 的总宽度和可视区域宽度
|
||||||
const totalWidth = bodyContentRef.value.offsetWidth
|
const totalWidth = bodyContentRef.value.offsetWidth
|
||||||
|
const totalHeight = contentHeight.value
|
||||||
|
|
||||||
// 使用已经维护的 timelineScrollLeft,而不是从 DOM 重新读取
|
// 使用已经维护的 timelineScrollLeft,而不是从 DOM 重新读取
|
||||||
// 因为 handleTimelineScroll 已经实时更新了这个值
|
// 因为 handleTimelineScroll 已经实时更新了这个值
|
||||||
@@ -2009,9 +2012,23 @@ function updateSvgSize() {
|
|||||||
|
|
||||||
canvasOffsetLeft.value = idealOffsetLeft
|
canvasOffsetLeft.value = idealOffsetLeft
|
||||||
|
|
||||||
|
const clampedHeight = Math.min(totalHeight, SAFE_CANVAS_HEIGHT)
|
||||||
|
canvasHeight.value = clampedHeight
|
||||||
svgWidth.value = canvasWidth.value
|
svgWidth.value = canvasWidth.value
|
||||||
svgHeight.value = contentHeight.value
|
svgHeight.value = clampedHeight
|
||||||
canvasHeight.value = contentHeight.value
|
|
||||||
|
const scrollTop = timelineBodyScrollTop.value
|
||||||
|
const bufferTop = clampedHeight / 3
|
||||||
|
let idealOffsetTop = Math.max(0, scrollTop - bufferTop)
|
||||||
|
|
||||||
|
if (totalHeight <= clampedHeight) {
|
||||||
|
idealOffsetTop = 0
|
||||||
|
} else {
|
||||||
|
const maxOffsetTop = totalHeight - clampedHeight
|
||||||
|
idealOffsetTop = Math.min(idealOffsetTop, maxOffsetTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasOffsetTop.value = idealOffsetTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2035,7 +2052,9 @@ function handleBarMounted(payload: {
|
|||||||
height: payload.height,
|
height: payload.height,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
updateSvgSize()
|
setTimeout(() => {
|
||||||
|
updateSvgSize()
|
||||||
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向上传递 TaskBar 拖拽/拉伸事件
|
// 向上传递 TaskBar 拖拽/拉伸事件
|
||||||
@@ -2200,6 +2219,8 @@ const handleTaskListVerticalScroll = (event: CustomEvent) => {
|
|||||||
// 立即更新纵向滚动位置(用于虚拟滚动计算)
|
// 立即更新纵向滚动位置(用于虚拟滚动计算)
|
||||||
timelineBodyScrollTop.value = scrollTop
|
timelineBodyScrollTop.value = scrollTop
|
||||||
|
|
||||||
|
debouncedUpdateCanvasPosition()
|
||||||
|
|
||||||
if (timelineBodyElement.value && Math.abs(timelineBodyElement.value.scrollTop - scrollTop) > 1) {
|
if (timelineBodyElement.value && Math.abs(timelineBodyElement.value.scrollTop - scrollTop) > 1) {
|
||||||
// 使用更精确的比较,避免1px以内的细微差异导致的循环触发
|
// 使用更精确的比较,避免1px以内的细微差异导致的循环触发
|
||||||
timelineBodyElement.value.scrollTop = scrollTop
|
timelineBodyElement.value.scrollTop = scrollTop
|
||||||
@@ -2216,6 +2237,8 @@ const handleTimelineBodyScroll = (event: Event) => {
|
|||||||
// 立即更新纵向滚动位置(用于虚拟滚动计算)
|
// 立即更新纵向滚动位置(用于虚拟滚动计算)
|
||||||
timelineBodyScrollTop.value = scrollTop
|
timelineBodyScrollTop.value = scrollTop
|
||||||
|
|
||||||
|
debouncedUpdateCanvasPosition()
|
||||||
|
|
||||||
// 拖拽时不同步滚动事件,避免性能问题
|
// 拖拽时不同步滚动事件,避免性能问题
|
||||||
if (isDragging.value) return
|
if (isDragging.value) return
|
||||||
|
|
||||||
@@ -2830,7 +2853,7 @@ watch([timelineData, timelineContainerWidth], () => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateSvgSize()
|
updateSvgSize()
|
||||||
}, 50)
|
}, 200)
|
||||||
})
|
})
|
||||||
taskBarRenderTimer = null
|
taskBarRenderTimer = null
|
||||||
}, 100)
|
}, 100)
|
||||||
@@ -3368,6 +3391,7 @@ const handleAddSuccessor = (task: Task) => {
|
|||||||
:width="canvasWidth"
|
:width="canvasWidth"
|
||||||
:height="canvasHeight"
|
:height="canvasHeight"
|
||||||
:offset-left="canvasOffsetLeft"
|
:offset-left="canvasOffsetLeft"
|
||||||
|
:offset-top="canvasOffsetTop"
|
||||||
:highlighted-task-id="highlightedTaskId"
|
:highlighted-task-id="highlightedTaskId"
|
||||||
:highlighted-task-ids="highlightedTaskIds"
|
:highlighted-task-ids="highlightedTaskIds"
|
||||||
:hovered-task-id="hoveredTaskId"
|
:hovered-task-id="hoveredTaskId"
|
||||||
|
|||||||
@@ -240,6 +240,29 @@ const messages = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
disableTaskbarFocusMode: '关闭聚焦功能',
|
disableTaskbarFocusMode: '关闭聚焦功能',
|
||||||
|
dataSourceAlreadyLoaded: '{name} 已是当前数据源',
|
||||||
|
dataSourceLoadSuccess: '已加载 {name}',
|
||||||
|
dataSourceLoadFailed: '{name} 加载失败',
|
||||||
|
dataSourceSwitch: {
|
||||||
|
title: '数据源切换',
|
||||||
|
subtitle: '对比常规与超大数据集的初始化体验',
|
||||||
|
loading: '数据加载中,请稍候...',
|
||||||
|
alreadyLoaded: '{name} 已是当前数据源',
|
||||||
|
loadSuccess: '已加载 {name}',
|
||||||
|
loadFailed: '{name} 加载失败',
|
||||||
|
sources: {
|
||||||
|
normal: {
|
||||||
|
label: '常规数据源',
|
||||||
|
description: 'data.json · 含完整前/后置依赖,适合功能演示',
|
||||||
|
badge: 'data.json',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
label: '超大数据源',
|
||||||
|
description: 'data-large-1m.json · 百万级任务,验证虚拟渲染性能',
|
||||||
|
badge: 'data-large-1m.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'en-US': {
|
'en-US': {
|
||||||
dateNotSet: 'Not set',
|
dateNotSet: 'Not set',
|
||||||
@@ -478,6 +501,29 @@ const messages = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
disableTaskbarFocusMode: 'Disable Focus Mode',
|
disableTaskbarFocusMode: 'Disable Focus Mode',
|
||||||
|
dataSourceAlreadyLoaded: '{name} is already active',
|
||||||
|
dataSourceLoadSuccess: '{name} loaded successfully',
|
||||||
|
dataSourceLoadFailed: '{name} failed to load',
|
||||||
|
dataSourceSwitch: {
|
||||||
|
title: 'Data Sources',
|
||||||
|
subtitle: 'Compare default vs. mega dataset initialization',
|
||||||
|
loading: 'Loading data, please wait…',
|
||||||
|
alreadyLoaded: '{name} is already active',
|
||||||
|
loadSuccess: '{name} loaded successfully',
|
||||||
|
loadFailed: '{name} failed to load',
|
||||||
|
sources: {
|
||||||
|
normal: {
|
||||||
|
label: 'Standard Dataset',
|
||||||
|
description: 'data.json · Full predecessor graph for feature demos',
|
||||||
|
badge: 'data.json',
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
label: 'Massive Dataset',
|
||||||
|
description: 'data-large-1m.json · Million-level tasks to stress virtual rendering',
|
||||||
|
badge: 'data-large-1m.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user