@@ -35,12 +35,20 @@ const { t } = useI18n()
3535
3636// TaskList 容器引用
3737const taskListRef = ref <HTMLElement | null >(null )
38+ const taskListBodyRef = ref <HTMLElement | null >(null )
3839
3940// 缓存容器宽度,避免频繁读取 offsetWidth 导致强制重排
4041const cachedContainerWidth = ref (0 )
4142
4243// 使用 ResizeObserver 监听容器宽度变化
4344let containerResizeObserver: ResizeObserver | null = null
45+ let bodyResizeObserver: ResizeObserver | null = null
46+
47+ // 纵向虚拟滚动相关状态
48+ const ROW_HEIGHT = 51 // 每行高度(与TaskList Row一致)
49+ const VERTICAL_BUFFER = 5 // 上下额外渲染的缓冲行数
50+ const taskListScrollTop = ref (0 )
51+ const taskListBodyHeight = ref (0 )
4452
4553// 获取列宽度样式(百分比转像素)
4654const getColumnWidthStyle = (column : { width? : number | string }) => {
@@ -242,6 +250,61 @@ const getAllTasks = (taskList: Task[]): Task[] => {
242250 return allTasks
243251}
244252
253+ // 获取当前折叠状态下的可见任务列表
254+ const getFlattenedVisibleTasks = (
255+ taskList : Task [],
256+ level = 0 ,
257+ ): Array <{ task: Task ; level: number }> => {
258+ const result: Array <{ task: Task ; level: number }> = []
259+
260+ for (const task of taskList ) {
261+ result .push ({ task , level })
262+
263+ const isMilestoneGroup = task .type === ' milestone-group'
264+
265+ if (! isMilestoneGroup && task .children && task .children .length > 0 && ! task .collapsed ) {
266+ result .push (... getFlattenedVisibleTasks (task .children , level + 1 ))
267+ }
268+ }
269+
270+ return result
271+ }
272+
273+ // 扁平化后的可见任务列表
274+ const flattenedTasks = computed (() => getFlattenedVisibleTasks (localTasks .value ))
275+
276+ // 计算可视区域任务范围
277+ const visibleTaskRange = computed (() => {
278+ const scrollTop = taskListScrollTop .value
279+ const containerHeight = taskListBodyHeight .value || 600
280+
281+ const startIndex = Math .floor (scrollTop / ROW_HEIGHT ) - VERTICAL_BUFFER
282+ const endIndex = Math .ceil ((scrollTop + containerHeight ) / ROW_HEIGHT ) + VERTICAL_BUFFER
283+
284+ const total = flattenedTasks .value .length
285+ const clampedStart = Math .min (Math .max (0 , startIndex ), total )
286+ const clampedEnd = Math .min (total , Math .max (clampedStart + 1 , endIndex ))
287+
288+ return {
289+ startIndex: clampedStart ,
290+ endIndex: clampedEnd ,
291+ }
292+ })
293+
294+ // 虚拟列表中需要渲染的任务
295+ const visibleTasks = computed (() => {
296+ const { startIndex, endIndex } = visibleTaskRange .value
297+ return flattenedTasks .value .slice (startIndex , endIndex )
298+ })
299+
300+ // Spacer 高度用于撑起滚动区域
301+ const totalContentHeight = computed (() => flattenedTasks .value .length * ROW_HEIGHT )
302+ const startSpacerHeight = computed (() => visibleTaskRange .value .startIndex * ROW_HEIGHT )
303+ const endSpacerHeight = computed (() => {
304+ const visibleHeight = visibleTasks .value .length * ROW_HEIGHT
305+ return Math .max (0 , totalContentHeight .value - startSpacerHeight .value - visibleHeight )
306+ })
307+
245308// 监听外部传入的 tasks 数据变化
246309watch (
247310 () => props .tasks ,
@@ -345,16 +408,23 @@ const handleTaskListScroll = (event: Event) => {
345408
346409 const scrollTop = target .scrollTop
347410
411+ taskListScrollTop .value = scrollTop
412+
348413 // 同步垂直滚动到Timeline
349414 window .dispatchEvent (
350415 new CustomEvent (' task-list-vertical-scroll' , {
351416 detail: { scrollTop },
352417 }),
353418 )
354- } // 处理Timeline垂直滚动同步
419+ }
420+
421+ // 处理Timeline垂直滚动同步
355422const handleTimelineVerticalScroll = (event : CustomEvent ) => {
356423 const { scrollTop } = event .detail
357- const taskListBodyElement = document .querySelector (' .task-list-body' ) as HTMLElement
424+ const taskListBodyElement = taskListBodyRef .value
425+
426+ taskListScrollTop .value = scrollTop
427+
358428 if (taskListBodyElement && Math .abs (taskListBodyElement .scrollTop - scrollTop ) > 1 ) {
359429 // 使用更精确的比较,避免1px以内的细微差异导致的循环触发
360430 taskListBodyElement .scrollTop = scrollTop
@@ -372,6 +442,7 @@ const handleTaskRowContextMenu = (event: { task: Task; position: { x: number; y:
372442 }),
373443 )
374444 } catch (error ) {
445+ // eslint-disable-next-line no-console
375446 console .error (' TaskList - Failed to dispatch context-menu event' , error )
376447 }
377448}
@@ -417,6 +488,20 @@ onMounted(async () => {
417488 containerResizeObserver .observe (taskListRef .value )
418489 }
419490
491+ // 监听TaskList body高度变化,提供虚拟滚动所需尺寸
492+ if (taskListBodyRef .value ) {
493+ taskListBodyHeight .value = taskListBodyRef .value .clientHeight
494+ taskListScrollTop .value = taskListBodyRef .value .scrollTop
495+
496+ bodyResizeObserver = new ResizeObserver (entries => {
497+ for (const entry of entries ) {
498+ taskListBodyHeight .value = entry .contentRect .height
499+ }
500+ })
501+
502+ bodyResizeObserver .observe (taskListBodyRef .value )
503+ }
504+
420505 window .addEventListener (' task-updated' , handleTaskUpdated as EventListener )
421506 window .addEventListener (' task-added' , handleTaskAdded as EventListener )
422507 window .addEventListener (' request-task-list' , handleRequestTaskList as EventListener )
@@ -438,6 +523,11 @@ onUnmounted(() => {
438523 containerResizeObserver = null
439524 }
440525
526+ if (bodyResizeObserver ) {
527+ bodyResizeObserver .disconnect ()
528+ bodyResizeObserver = null
529+ }
530+
441531 window .removeEventListener (' task-updated' , handleTaskUpdated as EventListener )
442532 window .removeEventListener (' task-added' , handleTaskAdded as EventListener )
443533 window .removeEventListener (' request-task-list' , handleRequestTaskList as EventListener )
@@ -470,17 +560,20 @@ onUnmounted(() => {
470560 {{ (t as any)[column.key] || column.label }}
471561 </div >
472562 </div >
473- <div class =" task-list-body" @scroll =" handleTaskListScroll" >
474- <TaskRow
475- v-for =" task in localTasks"
563+ <div ref =" taskListBodyRef" class =" task-list-body" @scroll =" handleTaskListScroll" >
564+ <div class =" task-list-body-spacer" :style =" { height: `${startSpacerHeight}px` }" ></div >
565+
566+ <TaskRow
567+ v-for =" { task, level } in visibleTasks"
476568 :key =" task.id"
477569 :task =" task"
478- :level =" 0 "
570+ :level =" level "
479571 :is-hovered =" hoveredTaskId === task.id"
480572 :hovered-task-id =" hoveredTaskId"
481573 :on-hover =" handleTaskRowHover"
482574 :columns =" visibleColumns"
483575 :get-column-width-style =" getColumnWidthStyle"
576+ :disable-children-render =" true"
484577 @toggle =" toggleCollapse"
485578 @dblclick =" handleTaskRowDoubleClick"
486579 @contextmenu =" handleTaskRowContextMenu"
@@ -494,6 +587,8 @@ onUnmounted(() => {
494587 <slot name =" custom-task-content" v-bind =" rowScope" />
495588 </template >
496589 </TaskRow >
590+
591+ <div class =" task-list-body-spacer" :style =" { height: `${endSpacerHeight}px` }" ></div >
497592 </div >
498593 </div >
499594</template >
@@ -546,14 +641,18 @@ onUnmounted(() => {
546641 width : max-content ;
547642 background : var (--gantt-bg-primary );
548643 flex : 1 ;
549- overflow-x : hidden ; /* 让body部分可以滚动 */
550- overflow-y : auto ; /* 允许垂直滚动 */
644+ overflow-x : hidden ; /* 允许横向滚动,确保列完整展示 */
645+ overflow-y : auto ;
551646
552647 /* Webkit浏览器滚动条样式 */
553648 scrollbar-width : thin ;
554649 scrollbar-color : var (--gantt-scrollbar-thumb ) transparent ;
555650}
556651
652+ .task-list-body-spacer {
653+ width : 100% ;
654+ }
655+
557656.task-list-body ::-webkit-scrollbar {
558657 width : 8px ;
559658 height : 8px ;
0 commit comments