Files
jordium-gantt-vue3/src/components/TaskList/composables/taskList/useTaskListLayout.ts
2026-01-30 16:53:59 +08:00

195 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ref, computed, inject, type Ref, type ComputedRef } from 'vue'
import type { Task } from '../../../../models/classes/Task'
import type { Resource } from '../../../../models/classes/Resource'
/**
* TaskList 布局计算逻辑
* 包含虚拟滚动、任务扁平化、可见区域计算等
*/
// 常量定义
export const ROW_HEIGHT = 51 // 每行高度与TaskList Row一致
export const VERTICAL_BUFFER = 5 // 上下额外渲染的缓冲行数
export interface TaskWithLevel {
task: Task
level: number
}
export interface TaskWithLevelAndIndex extends TaskWithLevel {
rowIndex: number
}
export interface VisibleTaskRange {
startIndex: number
endIndex: number
}
export function useTaskListLayout(tasks: Ref<Task[]>) {
// 纵向虚拟滚动相关状态
const taskListScrollTop = ref(0)
const taskListBodyHeight = ref(0)
// v1.9.0 注入视图模式和资源布局信息
const viewMode = inject<Ref<'task' | 'resource'>>('gantt-view-mode', ref('task'))
const dataSource = inject<ComputedRef<Task[] | Resource[]>>('gantt-data-source', computed(() => []))
const resourceTaskLayouts = inject<ComputedRef<Map<string | number, {
taskRowMap: Map<string | number, number>,
rowHeights: number[],
totalHeight: number
}>>>('resourceTaskLayouts', computed(() => new Map()))
/**
* 获取当前折叠状态下的可见任务列表(扁平化)
*/
const getFlattenedVisibleTasks = (
taskList: Task[],
level = 0,
): TaskWithLevel[] => {
const result: TaskWithLevel[] = []
for (const task of taskList) {
result.push({ task, level })
const isMilestoneGroup = task.type === 'milestone-group'
if (!isMilestoneGroup && task.children && task.children.length > 0 && !task.collapsed) {
result.push(...getFlattenedVisibleTasks(task.children, level + 1))
}
}
return result
}
// 扁平化后的可见任务列表
const flattenedTasks = computed(() => getFlattenedVisibleTasks(tasks.value))
/**
* v1.9.0 计算每个任务/资源的累计高度位置(支持动态行高)
*/
const cumulativeHeights = computed<number[]>(() => {
if (viewMode.value === 'resource') {
// 资源视图:使用每个资源的实际高度
const resources = dataSource.value as Resource[]
const heights: number[] = [0] // 第一个位置是0
let cumulative = 0
resources.forEach(resource => {
const layout = resourceTaskLayouts.value.get(resource.id)
const height = layout?.totalHeight || ROW_HEIGHT
cumulative += height
heights.push(cumulative)
})
return heights
} else {
// 任务视图:使用固定行高
const heights: number[] = [0]
for (let i = 1; i <= flattenedTasks.value.length; i++) {
heights.push(i * ROW_HEIGHT)
}
return heights
}
})
/**
* 根据滚动位置查找对应的任务/资源索引(二分查找)
*/
const findIndexByScrollTop = (scrollTop: number): number => {
const heights = cumulativeHeights.value
let left = 0
let right = heights.length - 1
while (left < right) {
const mid = Math.floor((left + right) / 2)
if (heights[mid] <= scrollTop) {
left = mid + 1
} else {
right = mid
}
}
return Math.max(0, left - 1)
}
/**
* 计算可视区域任务范围(支持动态行高)
*/
const visibleTaskRange = computed<VisibleTaskRange>(() => {
const scrollTop = taskListScrollTop.value
const containerHeight = taskListBodyHeight.value || 600
const heights = cumulativeHeights.value
if (heights.length <= 1) {
return { startIndex: 0, endIndex: 0 }
}
// 找到起始索引(考虑缓冲区)
let startIndex = findIndexByScrollTop(scrollTop)
startIndex = Math.max(0, startIndex - VERTICAL_BUFFER)
// 找到结束索引(考虑缓冲区)
const scrollBottom = scrollTop + containerHeight
let endIndex = findIndexByScrollTop(scrollBottom)
endIndex = Math.min(heights.length - 1, endIndex + VERTICAL_BUFFER + 1)
const total = viewMode.value === 'resource'
? (dataSource.value as Resource[]).length
: flattenedTasks.value.length
return {
startIndex: Math.min(startIndex, total),
endIndex: Math.min(endIndex, total),
}
})
/**
* 虚拟列表中需要渲染的任务
*/
const visibleTasks = computed<TaskWithLevelAndIndex[]>(() => {
const { startIndex, endIndex } = visibleTaskRange.value
const slicedTasks = flattenedTasks.value.slice(startIndex, endIndex)
// 添加 rowIndex基于在扁平化列表中的实际索引
return slicedTasks.map((item, index) => ({
...item,
rowIndex: startIndex + index,
}))
})
/**
* Spacer 高度用于撑起滚动区域(支持动态行高)
*/
const totalContentHeight = computed(() => {
const heights = cumulativeHeights.value
return heights.length > 0 ? heights[heights.length - 1] : 0
})
const startSpacerHeight = computed(() => {
const startIdx = visibleTaskRange.value.startIndex
return cumulativeHeights.value[startIdx] || 0
})
const endSpacerHeight = computed(() => {
const endIdx = visibleTaskRange.value.endIndex
const endHeight = cumulativeHeights.value[endIdx] || 0
return Math.max(0, totalContentHeight.value - endHeight)
})
return {
// 状态
taskListScrollTop,
taskListBodyHeight,
// 计算属性
flattenedTasks,
visibleTaskRange,
visibleTasks,
totalContentHeight,
startSpacerHeight,
endSpacerHeight,
// 方法
getFlattenedVisibleTasks,
}
}