Skip to content

Commit fc1799b

Browse files
committed
v1.4.3 - 垂直虚拟滚动优化
1 parent e4f3cf5 commit fc1799b

File tree

3 files changed

+99
-19
lines changed

3 files changed

+99
-19
lines changed

src/components/GanttLinks.vue

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface Props {
2424
width: number
2525
height: number
2626
offsetLeft?: number // Canvas 在全局坐标系中的偏移量(用于虚拟渲染)
27+
offsetTop?: number // Canvas 在垂直方向的偏移量(用于虚拟渲染)
2728
highlightedTaskId: number | null
2829
highlightedTaskIds: Set<number>
2930
hoveredTaskId: number | null
@@ -36,6 +37,7 @@ const props = withDefaults(defineProps<Props>(), {
3637
verticalLines: () => [],
3738
showVerticalLines: true,
3839
offsetLeft: 0,
40+
offsetTop: 0,
3941
})
4042
4143
// Canvas 引用
@@ -124,6 +126,8 @@ const drawLinks = () => {
124126
// 虚拟渲染:计算 Canvas 覆盖的范围
125127
const canvasStartX = props.offsetLeft
126128
const canvasEndX = props.offsetLeft + displayWidth
129+
const canvasStartY = props.offsetTop
130+
const canvasEndY = props.offsetTop + displayHeight
127131
128132
// 定义线条数据类型
129133
interface LineData {
@@ -157,16 +161,6 @@ const drawLinks = () => {
157161
continue
158162
}
159163
160-
// 虚拟渲染:跳过不在 Canvas 覆盖范围内的关系线
161-
// 如果起点和终点都在 Canvas 外,跳过
162-
const fromX = fromBar.left + fromBar.width
163-
const toX = toBar.left
164-
const lineMinX = Math.min(fromX, toX)
165-
const lineMaxX = Math.max(fromX, toX)
166-
if (lineMaxX < canvasStartX || lineMinX > canvasEndX) {
167-
continue // 完全在 Canvas 外,跳过
168-
}
169-
170164
// 判断高亮状态
171165
const fromIsPrimary = props.highlightedTaskId === predecessorId
172166
const toIsPrimary = props.highlightedTaskId === task.id
@@ -189,11 +183,26 @@ const drawLinks = () => {
189183
const globalX2 = toBar.left
190184
const globalY2 = toBar.top + toBar.height / 2 + toYOffset
191185
186+
// 虚拟渲染:跳过不在 Canvas 覆盖范围内的关系线
187+
// 如果起点和终点都在 Canvas 外,跳过
188+
const lineMinX = Math.min(globalX1, globalX2)
189+
const lineMaxX = Math.max(globalX1, globalX2)
190+
const lineMinY = Math.min(globalY1, globalY2)
191+
const lineMaxY = Math.max(globalY1, globalY2)
192+
if (
193+
lineMaxX < canvasStartX ||
194+
lineMinX > canvasEndX ||
195+
lineMaxY < canvasStartY ||
196+
lineMinY > canvasEndY
197+
) {
198+
continue // 完全在 Canvas 外,跳过
199+
}
200+
192201
// 转换为 Canvas 局部坐标
193202
const x1 = globalX1 - props.offsetLeft
194-
const y1 = globalY1
203+
const y1 = globalY1 - props.offsetTop
195204
const x2 = globalX2 - props.offsetLeft
196-
const y2 = globalY2
205+
const y2 = globalY2 - props.offsetTop
197206
198207
const c1x = x1 + 40
199208
const c1y = y1
@@ -369,6 +378,7 @@ watch(
369378
() => props.verticalLines,
370379
() => props.showVerticalLines,
371380
() => props.offsetLeft, // 监听虚拟渲染的偏移量变化
381+
() => props.offsetTop,
372382
],
373383
() => {
374384
// 使用 RAF 调度重绘,合并连续的多次变化为单次绘制
@@ -430,7 +440,7 @@ defineExpose({
430440
top: 0,
431441
width: `${width}px`,
432442
height: `${height}px`,
433-
transform: `translateX(${offsetLeft}px)`,
443+
transform: `translate(${offsetLeft}px, ${offsetTop}px)`,
434444
zIndex: highlightedTaskId !== null ? 1001 : 25,
435445
pointerEvents: 'none',
436446
}"

src/components/Timeline.vue

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -746,12 +746,12 @@ const debounce = <T extends (...args: unknown[]) => void>(func: T, wait: number)
746746
// 优化的滚动处理器(增加防抖时间到 50ms)
747747
const debouncedUpdatePositions = debounce(() => {
748748
computeAllMilestonesPositions()
749-
}, 50)
749+
}, 200)
750750
751751
// 虚拟渲染:防抖更新 Canvas 位置(滚动时触发)
752752
const debouncedUpdateCanvasPosition = debounce(() => {
753753
updateSvgSize() // 重新计算 Canvas 位置和尺寸
754-
}, 50)
754+
}, 200)
755755
756756
// 防抖更新纵向滚动位置
757757
const debouncedUpdateVerticalScroll = debounce((scrollTop: number) => {
@@ -1972,17 +1972,20 @@ const svgHeight = ref(0)
19721972
const canvasWidth = ref(0)
19731973
const canvasHeight = ref(0)
19741974
const canvasOffsetLeft = ref(0) // Canvas 在全局坐标系中的偏移量
1975+
const canvasOffsetTop = ref(0)
19751976
19761977
// 虚拟渲染 Canvas 的安全宽度(防止超过浏览器限制)
19771978
// 可根据实际需求调整:
19781979
// - 5000: 最小内存 (~30MB),适合低端设备,但滚动时更频繁更新
19791980
// - 10000: 平衡选择 (~60MB),覆盖小时视图 10 天,周视图 2 年
19801981
const SAFE_CANVAS_WIDTH = 5000 // 平衡性能和覆盖范围
1982+
const SAFE_CANVAS_HEIGHT = 5000
19811983
19821984
function updateSvgSize() {
19831985
if (bodyContentRef.value) {
19841986
// 获取 bodyContent 的总宽度和可视区域宽度
19851987
const totalWidth = bodyContentRef.value.offsetWidth
1988+
const totalHeight = contentHeight.value
19861989
19871990
// 使用已经维护的 timelineScrollLeft,而不是从 DOM 重新读取
19881991
// 因为 handleTimelineScroll 已经实时更新了这个值
@@ -2009,9 +2012,23 @@ function updateSvgSize() {
20092012
20102013
canvasOffsetLeft.value = idealOffsetLeft
20112014
2015+
const clampedHeight = Math.min(totalHeight, SAFE_CANVAS_HEIGHT)
2016+
canvasHeight.value = clampedHeight
20122017
svgWidth.value = canvasWidth.value
2013-
svgHeight.value = contentHeight.value
2014-
canvasHeight.value = contentHeight.value
2018+
svgHeight.value = clampedHeight
2019+
2020+
const scrollTop = timelineBodyScrollTop.value
2021+
const bufferTop = clampedHeight / 3
2022+
let idealOffsetTop = Math.max(0, scrollTop - bufferTop)
2023+
2024+
if (totalHeight <= clampedHeight) {
2025+
idealOffsetTop = 0
2026+
} else {
2027+
const maxOffsetTop = totalHeight - clampedHeight
2028+
idealOffsetTop = Math.min(idealOffsetTop, maxOffsetTop)
2029+
}
2030+
2031+
canvasOffsetTop.value = idealOffsetTop
20152032
}
20162033
}
20172034
@@ -2035,7 +2052,9 @@ function handleBarMounted(payload: {
20352052
height: payload.height,
20362053
},
20372054
}
2038-
updateSvgSize()
2055+
setTimeout(() => {
2056+
updateSvgSize()
2057+
}, 200)
20392058
}
20402059
20412060
// 向上传递 TaskBar 拖拽/拉伸事件
@@ -2200,6 +2219,8 @@ const handleTaskListVerticalScroll = (event: CustomEvent) => {
22002219
// 立即更新纵向滚动位置(用于虚拟滚动计算)
22012220
timelineBodyScrollTop.value = scrollTop
22022221
2222+
debouncedUpdateCanvasPosition()
2223+
22032224
if (timelineBodyElement.value && Math.abs(timelineBodyElement.value.scrollTop - scrollTop) > 1) {
22042225
// 使用更精确的比较,避免1px以内的细微差异导致的循环触发
22052226
timelineBodyElement.value.scrollTop = scrollTop
@@ -2216,6 +2237,8 @@ const handleTimelineBodyScroll = (event: Event) => {
22162237
// 立即更新纵向滚动位置(用于虚拟滚动计算)
22172238
timelineBodyScrollTop.value = scrollTop
22182239
2240+
debouncedUpdateCanvasPosition()
2241+
22192242
// 拖拽时不同步滚动事件,避免性能问题
22202243
if (isDragging.value) return
22212244
@@ -2830,7 +2853,7 @@ watch([timelineData, timelineContainerWidth], () => {
28302853
nextTick(() => {
28312854
setTimeout(() => {
28322855
updateSvgSize()
2833-
}, 50)
2856+
}, 200)
28342857
})
28352858
taskBarRenderTimer = null
28362859
}, 100)
@@ -3368,6 +3391,7 @@ const handleAddSuccessor = (task: Task) => {
33683391
:width="canvasWidth"
33693392
:height="canvasHeight"
33703393
:offset-left="canvasOffsetLeft"
3394+
:offset-top="canvasOffsetTop"
33713395
:highlighted-task-id="highlightedTaskId"
33723396
:highlighted-task-ids="highlightedTaskIds"
33733397
:hovered-task-id="hoveredTaskId"

src/composables/useI18n.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,29 @@ const messages = {
240240
},
241241
},
242242
disableTaskbarFocusMode: '关闭聚焦功能',
243+
dataSourceAlreadyLoaded: '{name} 已是当前数据源',
244+
dataSourceLoadSuccess: '已加载 {name}',
245+
dataSourceLoadFailed: '{name} 加载失败',
246+
dataSourceSwitch: {
247+
title: '数据源切换',
248+
subtitle: '对比常规与超大数据集的初始化体验',
249+
loading: '数据加载中,请稍候...',
250+
alreadyLoaded: '{name} 已是当前数据源',
251+
loadSuccess: '已加载 {name}',
252+
loadFailed: '{name} 加载失败',
253+
sources: {
254+
normal: {
255+
label: '常规数据源',
256+
description: 'data.json · 含完整前/后置依赖,适合功能演示',
257+
badge: 'data.json',
258+
},
259+
large: {
260+
label: '超大数据源',
261+
description: 'data-large-1m.json · 百万级任务,验证虚拟渲染性能',
262+
badge: 'data-large-1m.json',
263+
},
264+
},
265+
},
243266
},
244267
'en-US': {
245268
dateNotSet: 'Not set',
@@ -478,6 +501,29 @@ const messages = {
478501
},
479502
},
480503
disableTaskbarFocusMode: 'Disable Focus Mode',
504+
dataSourceAlreadyLoaded: '{name} is already active',
505+
dataSourceLoadSuccess: '{name} loaded successfully',
506+
dataSourceLoadFailed: '{name} failed to load',
507+
dataSourceSwitch: {
508+
title: 'Data Sources',
509+
subtitle: 'Compare default vs. mega dataset initialization',
510+
loading: 'Loading data, please wait…',
511+
alreadyLoaded: '{name} is already active',
512+
loadSuccess: '{name} loaded successfully',
513+
loadFailed: '{name} failed to load',
514+
sources: {
515+
normal: {
516+
label: 'Standard Dataset',
517+
description: 'data.json · Full predecessor graph for feature demos',
518+
badge: 'data.json',
519+
},
520+
large: {
521+
label: 'Massive Dataset',
522+
description: 'data-large-1m.json · Million-level tasks to stress virtual rendering',
523+
badge: 'data-large-1m.json',
524+
},
525+
},
526+
},
481527
},
482528
}
483529

0 commit comments

Comments
 (0)