From fc1799b4a3927e60d42a759d1d243558128eb4f9 Mon Sep 17 00:00:00 2001 From: "LINING-PC\\lining" Date: Fri, 28 Nov 2025 21:33:37 +0800 Subject: [PATCH] =?UTF-8?q?v1.4.3=20-=20=E5=9E=82=E7=9B=B4=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E6=BB=9A=E5=8A=A8=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/GanttLinks.vue | 36 +++++++++++++++++---------- src/components/Timeline.vue | 36 ++++++++++++++++++++++----- src/composables/useI18n.ts | 46 +++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/components/GanttLinks.vue b/src/components/GanttLinks.vue index c0e76b2..a5523cc 100644 --- a/src/components/GanttLinks.vue +++ b/src/components/GanttLinks.vue @@ -24,6 +24,7 @@ interface Props { width: number height: number offsetLeft?: number // Canvas 在全局坐标系中的偏移量(用于虚拟渲染) + offsetTop?: number // Canvas 在垂直方向的偏移量(用于虚拟渲染) highlightedTaskId: number | null highlightedTaskIds: Set hoveredTaskId: number | null @@ -36,6 +37,7 @@ const props = withDefaults(defineProps(), { verticalLines: () => [], showVerticalLines: true, offsetLeft: 0, + offsetTop: 0, }) // Canvas 引用 @@ -124,6 +126,8 @@ const drawLinks = () => { // 虚拟渲染:计算 Canvas 覆盖的范围 const canvasStartX = props.offsetLeft const canvasEndX = props.offsetLeft + displayWidth + const canvasStartY = props.offsetTop + const canvasEndY = props.offsetTop + displayHeight // 定义线条数据类型 interface LineData { @@ -157,16 +161,6 @@ const drawLinks = () => { 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 @@ -189,11 +183,26 @@ const drawLinks = () => { const globalX2 = toBar.left 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 局部坐标 const x1 = globalX1 - props.offsetLeft - const y1 = globalY1 + const y1 = globalY1 - props.offsetTop const x2 = globalX2 - props.offsetLeft - const y2 = globalY2 + const y2 = globalY2 - props.offsetTop const c1x = x1 + 40 const c1y = y1 @@ -369,6 +378,7 @@ watch( () => props.verticalLines, () => props.showVerticalLines, () => props.offsetLeft, // 监听虚拟渲染的偏移量变化 + () => props.offsetTop, ], () => { // 使用 RAF 调度重绘,合并连续的多次变化为单次绘制 @@ -430,7 +440,7 @@ defineExpose({ top: 0, width: `${width}px`, height: `${height}px`, - transform: `translateX(${offsetLeft}px)`, + transform: `translate(${offsetLeft}px, ${offsetTop}px)`, zIndex: highlightedTaskId !== null ? 1001 : 25, pointerEvents: 'none', }" diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index e9b4c6a..49f0654 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -746,12 +746,12 @@ const debounce = void>(func: T, wait: number) // 优化的滚动处理器(增加防抖时间到 50ms) const debouncedUpdatePositions = debounce(() => { computeAllMilestonesPositions() -}, 50) +}, 200) // 虚拟渲染:防抖更新 Canvas 位置(滚动时触发) const debouncedUpdateCanvasPosition = debounce(() => { updateSvgSize() // 重新计算 Canvas 位置和尺寸 -}, 50) +}, 200) // 防抖更新纵向滚动位置 const debouncedUpdateVerticalScroll = debounce((scrollTop: number) => { @@ -1972,17 +1972,20 @@ const svgHeight = ref(0) const canvasWidth = ref(0) const canvasHeight = ref(0) const canvasOffsetLeft = ref(0) // Canvas 在全局坐标系中的偏移量 +const canvasOffsetTop = ref(0) // 虚拟渲染 Canvas 的安全宽度(防止超过浏览器限制) // 可根据实际需求调整: // - 5000: 最小内存 (~30MB),适合低端设备,但滚动时更频繁更新 // - 10000: 平衡选择 (~60MB),覆盖小时视图 10 天,周视图 2 年 const SAFE_CANVAS_WIDTH = 5000 // 平衡性能和覆盖范围 +const SAFE_CANVAS_HEIGHT = 5000 function updateSvgSize() { if (bodyContentRef.value) { // 获取 bodyContent 的总宽度和可视区域宽度 const totalWidth = bodyContentRef.value.offsetWidth + const totalHeight = contentHeight.value // 使用已经维护的 timelineScrollLeft,而不是从 DOM 重新读取 // 因为 handleTimelineScroll 已经实时更新了这个值 @@ -2009,9 +2012,23 @@ function updateSvgSize() { canvasOffsetLeft.value = idealOffsetLeft + const clampedHeight = Math.min(totalHeight, SAFE_CANVAS_HEIGHT) + canvasHeight.value = clampedHeight svgWidth.value = canvasWidth.value - svgHeight.value = contentHeight.value - canvasHeight.value = contentHeight.value + svgHeight.value = clampedHeight + + 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, }, } - updateSvgSize() + setTimeout(() => { + updateSvgSize() + }, 200) } // 向上传递 TaskBar 拖拽/拉伸事件 @@ -2200,6 +2219,8 @@ const handleTaskListVerticalScroll = (event: CustomEvent) => { // 立即更新纵向滚动位置(用于虚拟滚动计算) timelineBodyScrollTop.value = scrollTop + debouncedUpdateCanvasPosition() + if (timelineBodyElement.value && Math.abs(timelineBodyElement.value.scrollTop - scrollTop) > 1) { // 使用更精确的比较,避免1px以内的细微差异导致的循环触发 timelineBodyElement.value.scrollTop = scrollTop @@ -2216,6 +2237,8 @@ const handleTimelineBodyScroll = (event: Event) => { // 立即更新纵向滚动位置(用于虚拟滚动计算) timelineBodyScrollTop.value = scrollTop + debouncedUpdateCanvasPosition() + // 拖拽时不同步滚动事件,避免性能问题 if (isDragging.value) return @@ -2830,7 +2853,7 @@ watch([timelineData, timelineContainerWidth], () => { nextTick(() => { setTimeout(() => { updateSvgSize() - }, 50) + }, 200) }) taskBarRenderTimer = null }, 100) @@ -3368,6 +3391,7 @@ const handleAddSuccessor = (task: Task) => { :width="canvasWidth" :height="canvasHeight" :offset-left="canvasOffsetLeft" + :offset-top="canvasOffsetTop" :highlighted-task-id="highlightedTaskId" :highlighted-task-ids="highlightedTaskIds" :hovered-task-id="hoveredTaskId" diff --git a/src/composables/useI18n.ts b/src/composables/useI18n.ts index 830c24b..b2b1e13 100644 --- a/src/composables/useI18n.ts +++ b/src/composables/useI18n.ts @@ -240,6 +240,29 @@ const messages = { }, }, 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': { dateNotSet: 'Not set', @@ -478,6 +501,29 @@ const messages = { }, }, 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', + }, + }, + }, }, }