731 lines
22 KiB
Vue
731 lines
22 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, useSlots, computed, nextTick } from 'vue'
|
||
import TaskRow from './TaskRow.vue'
|
||
import { useI18n } from '../composables/useI18n'
|
||
import type { Task } from '../models/classes/Task'
|
||
import type { TaskListConfig } from '../models/configs/TaskListConfig'
|
||
import { DEFAULT_TASK_LIST_COLUMNS } from '../models/configs/TaskListConfig'
|
||
import { useTaskRowDrag } from '../composables/useTaskRowDrag'
|
||
import { moveTask } from '../utils/taskTreeUtils'
|
||
|
||
interface Props {
|
||
tasks?: Task[]
|
||
useDefaultDrawer?: boolean
|
||
taskListConfig?: TaskListConfig
|
||
enableTaskRowMove?: boolean
|
||
}
|
||
|
||
const props = defineProps<Props>()
|
||
|
||
// 定义emit事件
|
||
// const emit = defineEmits(['task-collapse-change', 'start-timer', 'stop-timer'])
|
||
// +'add-predecessor': [task: Task] // 新增:添加前置任务事件
|
||
// 'add-successor': [task: Task] // 新增:添加后置任务事件
|
||
|
||
const emit = defineEmits<{
|
||
'task-collapse-change': [task: Task]
|
||
'start-timer': [task: Task]
|
||
'stop-timer': [task: Task]
|
||
'add-predecessor': [task: Task] // 新增:添加前置任务事件
|
||
'add-successor': [task: Task] // 新增:添加后置任务事件
|
||
delete: [task: Task, deleteChildren?: boolean]
|
||
'task-row-moved': [
|
||
payload: {
|
||
draggedTask: Task
|
||
targetTask: Task
|
||
position: 'after' | 'child' // 'after': 放在目标任务之后(同级), 'child': 作为目标任务的子任务
|
||
oldParent: Task | null
|
||
newParent: Task | null
|
||
},
|
||
]
|
||
}>()
|
||
const slots = useSlots()
|
||
const hasRowSlot = computed(() => Boolean(slots['custom-task-content']))
|
||
|
||
// 多语言支持
|
||
const { t } = useI18n()
|
||
|
||
// TaskList 容器引用
|
||
const taskListRef = ref<HTMLElement | null>(null)
|
||
const taskListBodyRef = ref<HTMLElement | null>(null)
|
||
|
||
// 缓存容器宽度,避免频繁读取 offsetWidth 导致强制重排
|
||
const cachedContainerWidth = ref(0)
|
||
|
||
// 使用 ResizeObserver 监听容器宽度变化
|
||
let containerResizeObserver: ResizeObserver | null = null
|
||
let bodyResizeObserver: ResizeObserver | null = null
|
||
|
||
// 纵向虚拟滚动相关状态
|
||
const ROW_HEIGHT = 51 // 每行高度(与TaskList Row一致)
|
||
const VERTICAL_BUFFER = 5 // 上下额外渲染的缓冲行数
|
||
const taskListScrollTop = ref(0)
|
||
const taskListBodyHeight = ref(0)
|
||
|
||
// 获取列宽度样式(百分比转像素)
|
||
const getColumnWidthStyle = (column: { width?: number | string }) => {
|
||
if (!column.width) return {}
|
||
|
||
let widthPx: string
|
||
|
||
// 如果是百分比,转换为像素
|
||
if (typeof column.width === 'string' && column.width.includes('%')) {
|
||
const containerWidth = cachedContainerWidth.value || 0
|
||
if (containerWidth > 0) {
|
||
const percentage = parseFloat(column.width) / 100
|
||
const pixels = Math.floor(containerWidth * percentage)
|
||
widthPx = `${pixels}px`
|
||
} else {
|
||
return {} // 容器宽度未知时返回空
|
||
}
|
||
} else {
|
||
// 像素值
|
||
widthPx = `${column.width}px`
|
||
}
|
||
|
||
return {
|
||
flex: `0 0 ${widthPx}`,
|
||
minWidth: widthPx,
|
||
maxWidth: widthPx,
|
||
}
|
||
}
|
||
|
||
// 计算可见的列配置
|
||
const visibleColumns = computed(() => {
|
||
const columns = props.taskListConfig?.columns || DEFAULT_TASK_LIST_COLUMNS
|
||
|
||
// 过滤出可见的列(visible !== false)
|
||
return columns.filter(col => col.visible !== false)
|
||
})
|
||
|
||
// 使用 props.tasks 的引用,不创建副本
|
||
// 这样可以直接修改对象,触发响应式更新,不需要外部监听事件
|
||
const localTasks = computed(() => props.tasks || [])
|
||
|
||
// 悬停状态管理
|
||
const hoveredTaskId = ref<number | null>(null)
|
||
|
||
// 拖拽状态管理
|
||
const isSplitterDragging = ref(false)
|
||
|
||
// 处理拖拽开始事件
|
||
const handleSplitterDragStart = () => {
|
||
isSplitterDragging.value = true
|
||
}
|
||
|
||
// 处理拖拽结束事件
|
||
const handleSplitterDragEnd = () => {
|
||
isSplitterDragging.value = false
|
||
|
||
// ⚠️ 拖拽结束后,手动更新一次容器宽度,触发列宽重新计算
|
||
if (taskListRef.value) {
|
||
const newWidth = taskListRef.value.offsetWidth
|
||
if (Math.abs(newWidth - cachedContainerWidth.value) > 1) {
|
||
cachedContainerWidth.value = newWidth
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理任务行悬停事件
|
||
const handleTaskRowHover = (taskId: number | null) => {
|
||
// 如果正在拖拽Splitter,则不响应悬停事件
|
||
if (isSplitterDragging.value) {
|
||
return
|
||
}
|
||
|
||
hoveredTaskId.value = taskId
|
||
// 发送事件通知Timeline组件
|
||
window.dispatchEvent(
|
||
new CustomEvent('task-list-hover', {
|
||
detail: taskId,
|
||
}),
|
||
)
|
||
}
|
||
|
||
// 监听Timeline的悬停事件
|
||
const handleTimelineHover = (event: CustomEvent) => {
|
||
hoveredTaskId.value = event.detail
|
||
}
|
||
|
||
// 处理TaskRow双击事件 (与Timeline的逻辑保持一致)
|
||
const handleTaskRowDoubleClick = (task: Task) => {
|
||
// 如果是里程碑类型,则不响应双击事件
|
||
if (task.type === 'milestone' || task.type === 'milestone-group') {
|
||
return
|
||
}
|
||
|
||
// 始终发送到Timeline处理(通过全局事件),让Timeline决定是否打开编辑器
|
||
// 这样外部监听的 @task-double-click 事件也会被触发
|
||
window.dispatchEvent(
|
||
new CustomEvent('task-row-double-click', {
|
||
detail: task,
|
||
}),
|
||
)
|
||
}
|
||
|
||
// 计算父级任务的进度和日期范围
|
||
const calculateParentTaskData = (
|
||
task: Task,
|
||
): { progress: number; startDate: string; endDate: string } => {
|
||
if (!task.children || task.children.length === 0) {
|
||
return {
|
||
progress: task.progress || 0,
|
||
startDate: task.startDate || '',
|
||
endDate: task.endDate || '',
|
||
}
|
||
}
|
||
|
||
// 递归计算所有子任务(包括折叠的子任务)
|
||
const allChildTasks: Task[] = []
|
||
const collectChildTasks = (tasks: Task[]) => {
|
||
tasks.forEach(childTask => {
|
||
allChildTasks.push(childTask)
|
||
if (childTask.children && childTask.children.length > 0) {
|
||
collectChildTasks(childTask.children)
|
||
}
|
||
})
|
||
}
|
||
collectChildTasks(task.children)
|
||
|
||
// 计算进度:所有子任务进度的平均值
|
||
const totalProgress = allChildTasks.reduce((sum, childTask) => {
|
||
return sum + (childTask.progress || 0)
|
||
}, 0)
|
||
const averageProgress =
|
||
allChildTasks.length > 0 ? Math.round(totalProgress / allChildTasks.length) : 0
|
||
|
||
// 计算日期范围:最早开始日期和最晚结束日期
|
||
const validTasks = allChildTasks.filter(childTask => childTask.startDate && childTask.endDate)
|
||
if (validTasks.length === 0) {
|
||
return {
|
||
progress: averageProgress,
|
||
startDate: task.startDate || '',
|
||
endDate: task.endDate || '',
|
||
}
|
||
}
|
||
|
||
const startDates = validTasks.map(childTask => new Date(childTask.startDate!))
|
||
const endDates = validTasks.map(childTask => new Date(childTask.endDate!))
|
||
|
||
const earliestStart = new Date(Math.min(...startDates.map(date => date.getTime())))
|
||
const latestEnd = new Date(Math.max(...endDates.map(date => date.getTime())))
|
||
|
||
return {
|
||
progress: averageProgress,
|
||
startDate: earliestStart.toISOString().split('T')[0],
|
||
endDate: latestEnd.toISOString().split('T')[0],
|
||
}
|
||
}
|
||
|
||
// 更新所有父级任务的进度和日期范围
|
||
const updateParentTasksData = () => {
|
||
if (!props.tasks) return
|
||
|
||
const updateParentTask = (taskList: Task[]): void => {
|
||
taskList.forEach(task => {
|
||
if (task.children && task.children.length > 0) {
|
||
// 先更新子任务
|
||
updateParentTask(task.children)
|
||
|
||
// 计算父级任务的进度和日期范围
|
||
const parentData = calculateParentTaskData(task)
|
||
|
||
// 直接修改对象属性
|
||
task.progress = parentData.progress
|
||
task.startDate = parentData.startDate
|
||
task.endDate = parentData.endDate
|
||
}
|
||
})
|
||
}
|
||
|
||
updateParentTask(props.tasks)
|
||
}
|
||
|
||
// 获取所有任务的扁平化列表(包括子任务)
|
||
const getAllTasks = (taskList: Task[]): Task[] => {
|
||
const allTasks: Task[] = []
|
||
|
||
const collectTasks = (tasks: Task[]) => {
|
||
tasks.forEach(task => {
|
||
allTasks.push(task)
|
||
if (task.children && task.children.length > 0) {
|
||
collectTasks(task.children)
|
||
}
|
||
})
|
||
}
|
||
|
||
collectTasks(taskList)
|
||
return allTasks
|
||
}
|
||
|
||
// 获取当前折叠状态下的可见任务列表
|
||
const getFlattenedVisibleTasks = (
|
||
taskList: Task[],
|
||
level = 0,
|
||
): Array<{ task: Task; level: number }> => {
|
||
const result: Array<{ task: Task; level: number }> = []
|
||
|
||
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(localTasks.value))
|
||
|
||
// 计算可视区域任务范围
|
||
const visibleTaskRange = computed(() => {
|
||
const scrollTop = taskListScrollTop.value
|
||
const containerHeight = taskListBodyHeight.value || 600
|
||
|
||
const startIndex = Math.floor(scrollTop / ROW_HEIGHT) - VERTICAL_BUFFER
|
||
const endIndex = Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + VERTICAL_BUFFER
|
||
|
||
const total = flattenedTasks.value.length
|
||
const clampedStart = Math.min(Math.max(0, startIndex), total)
|
||
const clampedEnd = Math.min(total, Math.max(clampedStart + 1, endIndex))
|
||
|
||
return {
|
||
startIndex: clampedStart,
|
||
endIndex: clampedEnd,
|
||
}
|
||
})
|
||
|
||
// 虚拟列表中需要渲染的任务
|
||
const visibleTasks = computed(() => {
|
||
const { startIndex, endIndex } = visibleTaskRange.value
|
||
return flattenedTasks.value.slice(startIndex, endIndex)
|
||
})
|
||
|
||
// Spacer 高度用于撑起滚动区域
|
||
const totalContentHeight = computed(() => flattenedTasks.value.length * ROW_HEIGHT)
|
||
const startSpacerHeight = computed(() => visibleTaskRange.value.startIndex * ROW_HEIGHT)
|
||
const endSpacerHeight = computed(() => {
|
||
const visibleHeight = visibleTasks.value.length * ROW_HEIGHT
|
||
return Math.max(0, totalContentHeight.value - startSpacerHeight.value - visibleHeight)
|
||
})
|
||
|
||
// 不再需要 watch,直接使用 props.tasks 的引用
|
||
|
||
function toggleCollapse(task: Task) {
|
||
task.collapsed = !task.collapsed
|
||
|
||
// 触发自定义事件,通知父组件任务折叠状态变化
|
||
emit('task-collapse-change', task)
|
||
}
|
||
|
||
// 更新任务数据
|
||
const updateTaskData = (updatedTask: Task) => {
|
||
if (!props.tasks) return
|
||
|
||
const updateTaskInTree = (taskList: Task[]): boolean => {
|
||
for (const task of taskList) {
|
||
if (task.id === updatedTask.id) {
|
||
// 直接修改对象属性
|
||
Object.assign(task, updatedTask)
|
||
// 保留子任务结构
|
||
if (updatedTask.children === undefined) {
|
||
// 如果 updatedTask 没有 children,保持原来的
|
||
// task.children 保持不变
|
||
}
|
||
return true
|
||
}
|
||
if (task.children && updateTaskInTree(task.children)) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
updateTaskInTree(props.tasks)
|
||
|
||
// 更新父级任务的进度和日期范围
|
||
updateParentTasksData()
|
||
}
|
||
|
||
// 监听TaskBar更新事件
|
||
const handleTaskUpdated = (event: CustomEvent) => {
|
||
const updatedTask = event.detail
|
||
updateTaskData(updatedTask)
|
||
}
|
||
|
||
// 监听Timeline的任务新增事件
|
||
const handleTaskAdded = (event: CustomEvent) => {
|
||
const newTask = event.detail
|
||
if (props.tasks) {
|
||
props.tasks.push(newTask)
|
||
}
|
||
|
||
// 更新父级任务数据
|
||
updateParentTasksData()
|
||
}
|
||
|
||
// 监听 TaskDrawer 请求任务列表事件
|
||
const handleRequestTaskList = () => {
|
||
// 获取所有任务的扁平化列表
|
||
const allTasks = getAllTasks(localTasks.value)
|
||
// 发送任务列表给 TaskDrawer
|
||
window.dispatchEvent(
|
||
new CustomEvent('task-list-updated', {
|
||
detail: allTasks,
|
||
}),
|
||
)
|
||
}
|
||
|
||
// 处理里程碑图标变更事件
|
||
const handleMilestoneIconChange = (event: CustomEvent) => {
|
||
const { milestoneId, icon } = event.detail
|
||
|
||
// 递归更新里程碑图标
|
||
const updateMilestoneIcon = (taskList: Task[]) => {
|
||
for (const task of taskList) {
|
||
if (task.type === 'milestone-group' && task.children) {
|
||
const milestone = task.children.find(m => m.id === milestoneId)
|
||
if (milestone) {
|
||
milestone.icon = icon
|
||
return true
|
||
}
|
||
}
|
||
if (task.children && updateMilestoneIcon(task.children)) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
updateMilestoneIcon(localTasks.value)
|
||
}
|
||
|
||
// 垂直滚动同步处理
|
||
const handleTaskListScroll = (event: Event) => {
|
||
const target = event.target as HTMLElement
|
||
if (!target) return
|
||
|
||
const scrollTop = target.scrollTop
|
||
|
||
taskListScrollTop.value = scrollTop
|
||
|
||
// 同步垂直滚动到Timeline
|
||
window.dispatchEvent(
|
||
new CustomEvent('task-list-vertical-scroll', {
|
||
detail: { scrollTop },
|
||
}),
|
||
)
|
||
}
|
||
|
||
// 处理Timeline垂直滚动同步
|
||
const handleTimelineVerticalScroll = (event: CustomEvent) => {
|
||
const { scrollTop } = event.detail
|
||
const taskListBodyElement = taskListBodyRef.value
|
||
|
||
taskListScrollTop.value = scrollTop
|
||
|
||
if (taskListBodyElement && Math.abs(taskListBodyElement.scrollTop - scrollTop) > 1) {
|
||
// 使用更精确的比较,避免1px以内的细微差异导致的循环触发
|
||
taskListBodyElement.scrollTop = scrollTop
|
||
}
|
||
}
|
||
|
||
// 处理任务行右键菜单事件
|
||
const handleTaskRowContextMenu = (event: { task: Task; position: { x: number; y: number } }) => {
|
||
// 直接从TaskRow触发全局事件,跳过TaskList中转
|
||
// 将事件转发为全局事件,让GanttChart组件处理
|
||
try {
|
||
window.dispatchEvent(
|
||
new CustomEvent('context-menu', {
|
||
detail: event,
|
||
}),
|
||
)
|
||
} catch (error) {
|
||
// eslint-disable-next-line no-console
|
||
console.error('TaskList - Failed to dispatch context-menu event', error)
|
||
}
|
||
}
|
||
|
||
// 处理TaskRow计时事件向上传递
|
||
const handleStartTimer = (task: Task) => {
|
||
emit('start-timer', task)
|
||
}
|
||
const handleStopTimer = (task: Task) => {
|
||
emit('stop-timer', task)
|
||
}
|
||
|
||
// 处理添加前置任务事件
|
||
const handleAddPredecessor = (task: Task) => {
|
||
emit('add-predecessor', task)
|
||
}
|
||
|
||
// 处理添加后置任务事件
|
||
const handleAddSuccessor = (task: Task) => {
|
||
emit('add-successor', task)
|
||
}
|
||
|
||
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||
// 触发删除事件
|
||
emit('delete', task, deleteChildren)
|
||
}
|
||
|
||
// 处理TaskRow拖拽移动事件(核心逻辑在组件内部)
|
||
const handleTaskRowMoved = (payload: {
|
||
draggedTask: Task
|
||
targetTask: Task
|
||
position: 'after' | 'child'
|
||
}) => {
|
||
const { draggedTask, targetTask, position } = payload
|
||
|
||
if (!props.tasks) return
|
||
|
||
// 执行移动操作(直接修改 props.tasks 对象,保持引用不变)
|
||
const result = moveTask(props.tasks, draggedTask.id, targetTask.id, position)
|
||
|
||
if (!result) {
|
||
return
|
||
}
|
||
|
||
// moveTask 已直接修改了 props.tasks,无需手动 splice
|
||
// 强制触发下一次更新,确保视图刷新
|
||
nextTick(() => {
|
||
// 更新父级任务数据(重新计算进度和日期)
|
||
updateParentTasksData()
|
||
|
||
// 触发事件,通知外部组件(可选)
|
||
// 外部可以监听此事件进行额外处理,如调用API保存、显示提示等
|
||
// 但不需要更新 tasks,组件已自动完成
|
||
emit('task-row-moved', {
|
||
draggedTask: result.movedTask,
|
||
targetTask,
|
||
position,
|
||
oldParent: result.oldParent,
|
||
newParent: result.newParent,
|
||
})
|
||
})
|
||
}
|
||
|
||
// 全局拖拽管理器(在TaskList层面,所有TaskRow共享)
|
||
const { startDrag, handleDragOver } = useTaskRowDrag({
|
||
enabled: props.enableTaskRowMove ?? false,
|
||
onDrop: (draggedTask, targetTask, position) => {
|
||
handleTaskRowMoved({ draggedTask, targetTask, position })
|
||
},
|
||
})
|
||
|
||
onMounted(async () => {
|
||
// 初始化 ResizeObserver 监听容器宽度变化
|
||
if (taskListRef.value) {
|
||
containerResizeObserver = new ResizeObserver((entries) => {
|
||
// ⚠️ 拖拽期间跳过更新,避免高频重新计算列宽
|
||
// TaskList的列宽不需要在拖拽期间实时更新,只需在拖拽结束后更新一次
|
||
if (isSplitterDragging.value) {
|
||
return
|
||
}
|
||
|
||
for (const entry of entries) {
|
||
// 使用 contentRect.width 避免强制重排
|
||
cachedContainerWidth.value = entry.contentRect.width
|
||
}
|
||
})
|
||
containerResizeObserver.observe(taskListRef.value)
|
||
}
|
||
|
||
// 监听TaskList body高度变化,提供虚拟滚动所需尺寸
|
||
if (taskListBodyRef.value) {
|
||
taskListBodyHeight.value = taskListBodyRef.value.clientHeight
|
||
taskListScrollTop.value = taskListBodyRef.value.scrollTop
|
||
|
||
bodyResizeObserver = new ResizeObserver(entries => {
|
||
for (const entry of entries) {
|
||
taskListBodyHeight.value = entry.contentRect.height
|
||
}
|
||
})
|
||
|
||
bodyResizeObserver.observe(taskListBodyRef.value)
|
||
}
|
||
|
||
window.addEventListener('task-updated', handleTaskUpdated as EventListener)
|
||
window.addEventListener('task-added', handleTaskAdded as EventListener)
|
||
window.addEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||
window.addEventListener('timeline-task-hover', handleTimelineHover as EventListener)
|
||
window.addEventListener('timeline-vertical-scroll', handleTimelineVerticalScroll as EventListener)
|
||
window.addEventListener('milestone-icon-changed', handleMilestoneIconChange as EventListener)
|
||
// 监听Splitter拖拽事件
|
||
window.addEventListener('splitter-drag-start', handleSplitterDragStart as EventListener)
|
||
window.addEventListener('splitter-drag-end', handleSplitterDragEnd as EventListener)
|
||
|
||
// 初始化时计算父级任务的进度和日期范围
|
||
updateParentTasksData()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
// 清理 ResizeObserver
|
||
if (containerResizeObserver) {
|
||
containerResizeObserver.disconnect()
|
||
containerResizeObserver = null
|
||
}
|
||
|
||
if (bodyResizeObserver) {
|
||
bodyResizeObserver.disconnect()
|
||
bodyResizeObserver = null
|
||
}
|
||
|
||
window.removeEventListener('task-updated', handleTaskUpdated as EventListener)
|
||
window.removeEventListener('task-added', handleTaskAdded as EventListener)
|
||
window.removeEventListener('request-task-list', handleRequestTaskList as EventListener)
|
||
window.removeEventListener('timeline-task-hover', handleTimelineHover as EventListener)
|
||
window.removeEventListener(
|
||
'timeline-vertical-scroll',
|
||
handleTimelineVerticalScroll as EventListener,
|
||
)
|
||
window.removeEventListener('milestone-icon-changed', handleMilestoneIconChange as EventListener)
|
||
window.removeEventListener('splitter-drag-start', handleSplitterDragStart as EventListener)
|
||
window.removeEventListener('splitter-drag-end', handleSplitterDragEnd as EventListener)
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div ref="taskListRef" class="task-list">
|
||
<div class="task-list-header">
|
||
<!-- 任务名称列,始终显示 -->
|
||
<div class="col col-name">
|
||
{{ (t as any).taskName || '任务名称' }}
|
||
</div>
|
||
<!-- 可配置的其他列 -->
|
||
<div
|
||
v-for="column in visibleColumns"
|
||
:key="column.key"
|
||
class="col"
|
||
:class="column.cssClass || `col-${column.key}`"
|
||
:style="getColumnWidthStyle(column)"
|
||
>
|
||
{{ (t as any)[column.key] || column.label }}
|
||
</div>
|
||
</div>
|
||
<div ref="taskListBodyRef" class="task-list-body" @scroll="handleTaskListScroll">
|
||
<div class="task-list-body-spacer" :style="{ height: `${startSpacerHeight}px` }"></div>
|
||
|
||
<TaskRow
|
||
v-for="{ task, level } in visibleTasks"
|
||
:key="task.id"
|
||
:task="task"
|
||
:level="level"
|
||
:is-hovered="hoveredTaskId === task.id"
|
||
:hovered-task-id="hoveredTaskId"
|
||
:on-hover="handleTaskRowHover"
|
||
:columns="visibleColumns"
|
||
:get-column-width-style="getColumnWidthStyle"
|
||
:disable-children-render="true"
|
||
:show-task-icon="props.taskListConfig?.showTaskIcon"
|
||
:enable-drag="props.enableTaskRowMove"
|
||
:drag-start="startDrag"
|
||
:drag-over="handleDragOver"
|
||
@toggle="toggleCollapse"
|
||
@dblclick="handleTaskRowDoubleClick"
|
||
@contextmenu="handleTaskRowContextMenu"
|
||
@start-timer="handleStartTimer"
|
||
@stop-timer="handleStopTimer"
|
||
@add-predecessor="handleAddPredecessor"
|
||
@add-successor="handleAddSuccessor"
|
||
@delete="handleTaskDelete"
|
||
>
|
||
<template v-if="hasRowSlot" #custom-task-content="rowScope">
|
||
<slot name="custom-task-content" v-bind="rowScope" />
|
||
</template>
|
||
</TaskRow>
|
||
|
||
<div class="task-list-body-spacer" :style="{ height: `${endSpacerHeight}px` }"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
@import '../styles/theme-variables.css';
|
||
@import '../styles/list.css';
|
||
|
||
.task-list {
|
||
width: 100%;
|
||
height: 100%;
|
||
font-size: 15px;
|
||
color: var(--gantt-text-primary);
|
||
background: var(--gantt-bg-primary);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-x: auto; /* 防止内容溢出 */
|
||
|
||
/* Webkit浏览器滚动条样式 */
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--gantt-scrollbar-thumb) transparent;
|
||
}
|
||
|
||
.task-list-header {
|
||
display: flex;
|
||
background: var(--gantt-bg-secondary);
|
||
border-bottom: 1px solid var(--gantt-border-medium);
|
||
border-left: 3px solid transparent; /* 添加3px透明左边框保持对齐 */
|
||
font-weight: 700;
|
||
padding: 0;
|
||
height: 80px;
|
||
align-items: center;
|
||
width: max-content;
|
||
flex-shrink: 0; /* 防止header被压缩 */
|
||
position: sticky; /* 使header固定 */
|
||
top: 0;
|
||
z-index: 10; /* 确保在滚动时保持在最上层 */
|
||
}
|
||
|
||
.task-list-header .col {
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
background: var(--gantt-bg-secondary);
|
||
color: var(--gantt-text-header);
|
||
border-right-color: var(--gantt-border-medium);
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.task-list-body {
|
||
width: max-content;
|
||
background: var(--gantt-bg-primary);
|
||
flex: 1;
|
||
overflow-x: hidden; /* 允许横向滚动,确保列完整展示 */
|
||
overflow-y: auto;
|
||
|
||
/* Webkit浏览器滚动条样式 */
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--gantt-scrollbar-thumb) transparent;
|
||
}
|
||
|
||
.task-list-body-spacer {
|
||
width: 100%;
|
||
}
|
||
|
||
.task-list-body::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
.task-list-body::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.task-list-body::-webkit-scrollbar-thumb {
|
||
background-color: var(--gantt-scrollbar-thumb);
|
||
border-radius: 4px;
|
||
border: 2px solid transparent;
|
||
background-clip: content-box;
|
||
}
|
||
|
||
.task-list-body::-webkit-scrollbar-thumb:hover {
|
||
background-color: var(--gantt-scrollbar-thumb-hover);
|
||
}
|
||
|
||
.task-list-body::-webkit-scrollbar-corner {
|
||
background: transparent;
|
||
}
|
||
</style>
|