v1.4.2-patch.3 - 性能优化

This commit is contained in:
LINING-PC\lining
2025-11-11 23:41:22 +08:00
parent 25c54a1982
commit 1278c41e2b
5 changed files with 377 additions and 155 deletions
+6 -6
View File
@@ -31,7 +31,7 @@
"id": 1101, "id": 1101,
"name": "试验方案设计与<span style='font-weight: bold;color:red;'>伦理审查</span>", "name": "试验方案设计与<span style='font-weight: bold;color:red;'>伦理审查</span>",
"assignee": "方案设计师 李明", "assignee": "方案设计师 李明",
"avatar": "https://i.pravatar.cc/150?img=1", "avatar": "https://i.pravatar.cc/50?img=1",
"startDate": "2025-01-01", "startDate": "2025-01-01",
"endDate": "2025-02-28", "endDate": "2025-02-28",
"progress": 100, "progress": 100,
@@ -46,7 +46,7 @@
"id": 1102, "id": 1102,
"name": "受试者招募与筛选", "name": "受试者招募与筛选",
"assignee": "招募专员 张丽", "assignee": "招募专员 张丽",
"avatar": "https://i.pravatar.cc/150?img=5", "avatar": "https://i.pravatar.cc/50?img=5",
"startDate": "2025-03-01", "startDate": "2025-03-01",
"endDate": "2025-04-30", "endDate": "2025-04-30",
"progress": 90, "progress": 90,
@@ -62,7 +62,7 @@
"id": 1103, "id": 1103,
"name": "药物给药与安全性监测", "name": "药物给药与安全性监测",
"assignee": "临床医生 Dr. Liu", "assignee": "临床医生 Dr. Liu",
"avatar": "https://i.pravatar.cc/150?img=8", "avatar": "https://i.pravatar.cc/50?img=8",
"startDate": "2025-05-01", "startDate": "2025-05-01",
"endDate": "2025-08-31", "endDate": "2025-08-31",
"progress": 40, "progress": 40,
@@ -95,7 +95,7 @@
"id": 1201, "id": 1201,
"name": "多中心试验<span style='font-weight: bold; color: blue;'>启动</span>", "name": "多中心试验<span style='font-weight: bold; color: blue;'>启动</span>",
"assignee": "项目经理 王芳", "assignee": "项目经理 王芳",
"avatar": "https://i.pravatar.cc/150?img=10", "avatar": "https://i.pravatar.cc/50?img=10",
"startDate": "2025-09-01", "startDate": "2025-09-01",
"endDate": "2025-11-30", "endDate": "2025-11-30",
"progress": 25, "progress": 25,
@@ -111,7 +111,7 @@
"id": 1202, "id": 1202,
"name": "患者入组与随机化", "name": "患者入组与随机化",
"assignee": "数据管理员 陈静", "assignee": "数据管理员 陈静",
"avatar": "https://i.pravatar.cc/150?img=20", "avatar": "https://i.pravatar.cc/50?img=20",
"startDate": "2025-12-01", "startDate": "2025-12-01",
"endDate": "2026-03-31", "endDate": "2026-03-31",
"progress": 0, "progress": 0,
@@ -127,7 +127,7 @@
"id": 1203, "id": 1203,
"name": "疗效评估与数据收集", "name": "疗效评估与数据收集",
"assignee": "统计师 赵磊", "assignee": "统计师 赵磊",
"avatar": "https://i.pravatar.cc/150?img=15", "avatar": "https://i.pravatar.cc/50?img=15",
"startDate": "2026-01-01", "startDate": "2026-01-01",
"endDate": "2026-08-31", "endDate": "2026-08-31",
"progress": 0, "progress": 0,
+59 -28
View File
@@ -136,35 +136,45 @@ interface Props {
const ganttRootRef = ref<HTMLElement | null>(null) const ganttRootRef = ref<HTMLElement | null>(null)
const ganttContainerWidth = ref(1920) // 默认使用常见的屏幕宽度作为初始值 const ganttContainerWidth = ref(1920) // 默认使用常见的屏幕宽度作为初始值
// 监听容器宽度变化 // ResizeObserver 用于监听容器宽度变化
const updateContainerWidth = () => { let ganttRootResizeObserver: ResizeObserver | null = null
if (ganttRootRef.value) {
const newWidth = ganttRootRef.value.clientWidth
if (newWidth !== ganttContainerWidth.value) {
ganttContainerWidth.value = newWidth
// 容器宽度变化时,重新计算 TaskList 的宽度限制
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
taskListBodyWidth.value = getTaskListMaxWidth()
taskListBodyProposedWidth.value = getTaskListMaxWidth()
taskListBodyWidthLimit.value = getTaskListMaxWidth()
// 确保当前宽度在新的限制范围内 // 监听容器宽度变化
const adjustedWidth = checkWidthLimits(leftPanelWidth.value) const updateContainerWidth = (newWidth: number) => {
if (adjustedWidth !== leftPanelWidth.value) { if (newWidth !== ganttContainerWidth.value) {
leftPanelWidth.value = adjustedWidth ganttContainerWidth.value = newWidth
} // 容器宽度变化时,重新计算 TaskList 的宽度限制
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
taskListBodyWidth.value = getTaskListMaxWidth()
taskListBodyProposedWidth.value = getTaskListMaxWidth()
taskListBodyWidthLimit.value = getTaskListMaxWidth()
// 确保当前宽度在新的限制范围内
const adjustedWidth = checkWidthLimits(leftPanelWidth.value)
if (adjustedWidth !== leftPanelWidth.value) {
leftPanelWidth.value = adjustedWidth
} }
} }
} }
onMounted(() => { onMounted(() => {
updateContainerWidth() if (ganttRootRef.value) {
// 监听窗口大小变化 // 使用 ResizeObserver 监听容器宽度变化,避免频繁读取 clientWidth
window.addEventListener('resize', updateContainerWidth) ganttRootResizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// 使用 contentRect.width,避免强制重排
updateContainerWidth(entry.contentRect.width)
}
})
ganttRootResizeObserver.observe(ganttRootRef.value)
}
}) })
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', updateContainerWidth) if (ganttRootResizeObserver) {
ganttRootResizeObserver.disconnect()
ganttRootResizeObserver = null
}
}) })
// TaskList最小宽度,支持通过taskListConfig配置(支持像素和百分比) // TaskList最小宽度,支持通过taskListConfig配置(支持像素和百分比)
@@ -331,9 +341,11 @@ function onMouseDown(e: MouseEvent) {
document.addEventListener('wheel', blockAllEvents, { capture: true, passive: false }) document.addEventListener('wheel', blockAllEvents, { capture: true, passive: false })
document.addEventListener('contextmenu', blockAllEvents, { capture: true }) document.addEventListener('contextmenu', blockAllEvents, { capture: true })
// ⚠️ 使用requestAnimationFrame节流,但移除阈值检测,确保每帧都更新
let rafId: number | null = null
function onMouseMove(ev: MouseEvent) { function onMouseMove(ev: MouseEvent) {
if (!dragging.value) return if (!dragging.value) return
// 强制阻止所有默认行为和事件传播 // 强制阻止所有默认行为和事件传播
ev.preventDefault() ev.preventDefault()
ev.stopPropagation() ev.stopPropagation()
@@ -341,15 +353,29 @@ function onMouseDown(e: MouseEvent) {
const delta = ev.clientX - startX const delta = ev.clientX - startX
const proposedWidth = startWidth + delta const proposedWidth = startWidth + delta
// 直接使用面板宽度限制检查,无需复杂的坐标计算
const finalWidth = checkWidthLimits(proposedWidth) const finalWidth = checkWidthLimits(proposedWidth)
leftPanelWidth.value = finalWidth
// 取消之前的帧请求
if (rafId !== null) {
cancelAnimationFrame(rafId)
}
// 在下一帧更新(节流到60fps,避免过度触发响应式系统)
rafId = requestAnimationFrame(() => {
leftPanelWidth.value = finalWidth
rafId = null
})
} }
function onMouseUp() { function onMouseUp() {
dragging.value = false dragging.value = false
// 取消未完成的帧请求
if (rafId !== null) {
cancelAnimationFrame(rafId)
rafId = null
}
// 移除全局事件拦截器 // 移除全局事件拦截器
document.removeEventListener('mousedown', blockAllEvents, { capture: true }) document.removeEventListener('mousedown', blockAllEvents, { capture: true })
document.removeEventListener('click', blockAllEvents, { capture: true }) document.removeEventListener('click', blockAllEvents, { capture: true })
@@ -480,10 +506,8 @@ onMounted(() => {
// 监听右侧面板(timeline 的可视容器)的宽度 // 监听右侧面板(timeline 的可视容器)的宽度
const rightPanel = document.querySelector('.gantt-panel-right') const rightPanel = document.querySelector('.gantt-panel-right')
if (rightPanel) { if (rightPanel) {
// 初始化宽度 // 使用 ResizeObserver 自动更新宽度,避免直接读取clientWidth造成强制重排
timelineContainerWidth.value = rightPanel.clientWidth // ResizeObserver 会在开始观察时立即触发一次回调,提供初始宽度
// 使用 ResizeObserver 监听宽度变化
resizeObserver = new ResizeObserver(entries => { resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) { for (const entry of entries) {
timelineContainerWidth.value = entry.contentRect.width timelineContainerWidth.value = entry.contentRect.width
@@ -2162,6 +2186,7 @@ function handleMilestoneDialogDelete(milestoneId: number) {
<div <div
v-if="isTaskListVisible" v-if="isTaskListVisible"
class="gantt-panel gantt-panel-left" class="gantt-panel gantt-panel-left"
:class="{ dragging: dragging }"
:style="{ width: leftPanelWidth + 'px' }" :style="{ width: leftPanelWidth + 'px' }"
> >
<TaskList <TaskList
@@ -2317,6 +2342,12 @@ function handleMilestoneDialogDelete(milestoneId: number) {
.gantt-panel-left { .gantt-panel-left {
/* width 由js控制 */ /* width 由js控制 */
min-width: 320px; min-width: 320px;
/* ⚠️ 拖拽时禁用transition,提升响应速度 */
transition: none;
}
.gantt-panel-left:not(.dragging) {
/* 非拖拽时保留平滑过渡效果(如toggle时) */
transition: width 0.1s; transition: width 0.1s;
} }
+102 -10
View File
@@ -119,6 +119,25 @@ const createLocalToday = (): Date => {
return new Date(now.getFullYear(), now.getMonth(), now.getDate()) return new Date(now.getFullYear(), now.getMonth(), now.getDate())
} }
//
//
const cachedToday = ref(createLocalToday())
let todayCacheTimer: number | null = null
onMounted(() => {
// 60
todayCacheTimer = window.setInterval(() => {
cachedToday.value = createLocalToday()
}, 60000)
})
onUnmounted(() => {
if (todayCacheTimer !== null) {
clearInterval(todayCacheTimer)
todayCacheTimer = null
}
})
const formatDateToLocalString = (date: Date): string => { const formatDateToLocalString = (date: Date): string => {
const year = date.getFullYear() const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0')
@@ -223,7 +242,7 @@ const taskBarStyle = computed(() => {
const startDate = createLocalDate(currentStartDate) const startDate = createLocalDate(currentStartDate)
const endDate = createLocalDate(currentEndDate) const endDate = createLocalDate(currentEndDate)
const baseStart = createLocalDate(props.startDate) const baseStart = parsedBaseStartDate.value
if (!startDate || !endDate || !baseStart) { if (!startDate || !endDate || !baseStart) {
return { return {
left: '0px', left: '0px',
@@ -392,6 +411,24 @@ const taskBarStyle = computed(() => {
} }
}) })
// TaskBar DOM
const cachedPosition = ref({
left: 0,
top: 0,
width: 0,
height: 0,
timestamp: 0,
})
//
const POSITION_CACHE_TTL = 100 // 100ms 使
// taskStatus
const parsedEndDate = computed(() => createLocalDate(props.task.endDate || ''))
//
const parsedBaseStartDate = computed(() => createLocalDate(props.startDate))
// //
const taskStatus = computed(() => { const taskStatus = computed(() => {
// (Story)使 // (Story)使
@@ -404,8 +441,9 @@ const taskStatus = computed(() => {
} }
} }
const today = createLocalToday() // 使
const endDate = createLocalDate(props.task.endDate || '') const today = cachedToday.value
const endDate = parsedEndDate.value
const progress = props.task.progress || 0 const progress = props.task.progress || 0
// //
@@ -556,6 +594,8 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r
const timelineContainer = document.querySelector('.timeline') as HTMLElement const timelineContainer = document.querySelector('.timeline') as HTMLElement
if (!timelineContainer || !barRef.value) return if (!timelineContainer || !barRef.value) return
// mousedown
//
const barRect = barRef.value.getBoundingClientRect() const barRect = barRef.value.getBoundingClientRect()
// TaskBar // TaskBar
@@ -595,17 +635,57 @@ const handleAutoScroll = (event: CustomEvent) => {
} }
} }
// 使 DOM
let reportPositionScheduled = false
function reportBarPosition() { function reportBarPosition() {
if (barRef.value) { //
if (reportPositionScheduled) return
reportPositionScheduled = true
requestAnimationFrame(() => {
reportPositionScheduled = false
if (!barRef.value) return
const now = Date.now()
// 使
if (now - cachedPosition.value.timestamp < POSITION_CACHE_TTL) {
emit('bar-mounted', {
id: props.task.id,
left: cachedPosition.value.left,
top: cachedPosition.value.top,
width: cachedPosition.value.width,
height: cachedPosition.value.height,
})
return
}
// DOM
// TaskBar Timeline
const rect = barRef.value.getBoundingClientRect() const rect = barRef.value.getBoundingClientRect()
emit('bar-mounted', {
id: props.task.id, //
const position = {
left: rect.left, left: rect.left,
top: rect.top, top: rect.top,
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
}
//
cachedPosition.value = {
...position,
timestamp: now,
}
emit('bar-mounted', {
id: props.task.id,
...position,
}) })
} })
} }
// //
@@ -1123,8 +1203,20 @@ const handleMouseUp = () => {
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
reportBarPosition() reportBarPosition()
// 使 ResizeObserver
if (taskBarNameRef.value) { if (taskBarNameRef.value) {
nameTextWidth.value = taskBarNameRef.value.getBoundingClientRect().width const nameResizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
nameTextWidth.value = entry.contentRect.width
}
})
nameResizeObserver.observe(taskBarNameRef.value)
//
onUnmounted(() => {
nameResizeObserver.disconnect()
})
} }
}) })
@@ -1395,7 +1487,7 @@ const stickyStyles = computed(() => {
} else if (nameNeedsRightSticky) { } else if (nameNeedsRightSticky) {
const offset = rightBoundary - taskLeft - nameWidth const offset = rightBoundary - taskLeft - nameWidth
// name // name
nameLeft = `${offset - handleWidth - 3}px` // + nameLeft = `${offset - handleWidth - 10}px` // +
namePosition = 'absolute' namePosition = 'absolute'
nameTop = '2px' nameTop = '2px'
} }
@@ -1431,7 +1523,7 @@ const stickyStyles = computed(() => {
progressTop = '18px' progressTop = '18px'
} else if (progressNeedsRightSticky) { } else if (progressNeedsRightSticky) {
const offset = rightBoundary - taskLeft - progressWidth const offset = rightBoundary - taskLeft - progressWidth
// progress //
progressLeft = `${offset - handleWidth - 3}px` // + progressLeft = `${offset - handleWidth - 3}px` // +
progressPosition = 'absolute' progressPosition = 'absolute'
progressTop = '18px' progressTop = '18px'
+38 -1
View File
@@ -36,6 +36,12 @@ const { t } = useI18n()
// TaskList // TaskList
const taskListRef = ref<HTMLElement | null>(null) const taskListRef = ref<HTMLElement | null>(null)
// offsetWidth
const cachedContainerWidth = ref(0)
// 使 ResizeObserver
let containerResizeObserver: ResizeObserver | null = null
// //
const getColumnWidthStyle = (column: { width?: number | string }) => { const getColumnWidthStyle = (column: { width?: number | string }) => {
if (!column.width) return {} if (!column.width) return {}
@@ -44,7 +50,7 @@ const getColumnWidthStyle = (column: { width?: number | string }) => {
// //
if (typeof column.width === 'string' && column.width.includes('%')) { if (typeof column.width === 'string' && column.width.includes('%')) {
const containerWidth = taskListRef.value?.offsetWidth || 0 const containerWidth = cachedContainerWidth.value || 0
if (containerWidth > 0) { if (containerWidth > 0) {
const percentage = parseFloat(column.width) / 100 const percentage = parseFloat(column.width) / 100
const pixels = Math.floor(containerWidth * percentage) const pixels = Math.floor(containerWidth * percentage)
@@ -89,6 +95,14 @@ const handleSplitterDragStart = () => {
// //
const handleSplitterDragEnd = () => { const handleSplitterDragEnd = () => {
isSplitterDragging.value = false isSplitterDragging.value = false
//
if (taskListRef.value) {
const newWidth = taskListRef.value.offsetWidth
if (Math.abs(newWidth - cachedContainerWidth.value) > 1) {
cachedContainerWidth.value = newWidth
}
}
} }
// //
@@ -386,6 +400,23 @@ const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
} }
onMounted(async () => { 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)
}
window.addEventListener('task-updated', handleTaskUpdated as EventListener) window.addEventListener('task-updated', handleTaskUpdated as EventListener)
window.addEventListener('task-added', handleTaskAdded as EventListener) window.addEventListener('task-added', handleTaskAdded as EventListener)
window.addEventListener('request-task-list', handleRequestTaskList as EventListener) window.addEventListener('request-task-list', handleRequestTaskList as EventListener)
@@ -401,6 +432,12 @@ onMounted(async () => {
}) })
onUnmounted(() => { onUnmounted(() => {
// ResizeObserver
if (containerResizeObserver) {
containerResizeObserver.disconnect()
containerResizeObserver = null
}
window.removeEventListener('task-updated', handleTaskUpdated as EventListener) window.removeEventListener('task-updated', handleTaskUpdated as EventListener)
window.removeEventListener('task-added', handleTaskAdded as EventListener) window.removeEventListener('task-added', handleTaskAdded as EventListener)
window.removeEventListener('request-task-list', handleRequestTaskList as EventListener) window.removeEventListener('request-task-list', handleRequestTaskList as EventListener)
+172 -110
View File
@@ -627,6 +627,7 @@ let scrollTimeout: number | null = null
// //
const timelineScrollLeft = ref(0) const timelineScrollLeft = ref(0)
const timelineContainerWidth = ref(0) const timelineContainerWidth = ref(0)
const timelineScrollWidth = ref(0) // scrollWidth
// //
const hideBubbles = ref(true) // const hideBubbles = ref(true) //
@@ -643,6 +644,9 @@ const timelineDataCache = new Map<string, unknown>()
// //
const isInitialLoad = ref(true) const isInitialLoad = ref(true)
//
const throttledContainerWidth = ref(0)
// //
const visibleHourRange = computed(() => { const visibleHourRange = computed(() => {
if (currentTimeScale.value !== TimelineScale.HOUR) { if (currentTimeScale.value !== TimelineScale.HOUR) {
@@ -650,7 +654,7 @@ const visibleHourRange = computed(() => {
} }
const scrollLeft = timelineScrollLeft.value const scrollLeft = timelineScrollLeft.value
const containerWidth = timelineContainerWidth.value const containerWidth = throttledContainerWidth.value || timelineContainerWidth.value
// 使 // 使
if (isInitialLoad.value && scrollLeft === 0) { if (isInitialLoad.value && scrollLeft === 0) {
@@ -734,17 +738,25 @@ const optimizedTimelineData = computed(() => {
if (currentTimeScale.value === TimelineScale.HOUR && Array.isArray(cachedData)) { if (currentTimeScale.value === TimelineScale.HOUR && Array.isArray(cachedData)) {
const { startHour, endHour } = visibleHourRange.value const { startHour, endHour } = visibleHourRange.value
return (cachedData as any[]) // 🚀 365
.map((day: any) => { const dayStart = new Date(timelineConfig.value.startDate)
// 线 dayStart.setHours(0, 0, 0, 0)
const dayStart = new Date(timelineConfig.value.startDate)
dayStart.setHours(0, 0, 0, 0) //
const currentDay = new Date(day.year, day.month - 1, day.day) const startDay = Math.floor(startHour / 24)
currentDay.setHours(0, 0, 0, 0) const endDay = Math.ceil(endHour / 24)
const daysDiff = Math.floor(
(currentDay.getTime() - dayStart.getTime()) / (1000 * 60 * 60 * 24), // +
) const visibleDays = (cachedData as any[]).slice(
const totalHourOffset = daysDiff * 24 Math.max(0, startDay - 1),
Math.min(cachedData.length, endDay + 1),
)
return visibleDays
.map((day: any, index: number) => {
// 使
const actualDayIndex = Math.max(0, startDay - 1) + index
const totalHourOffset = actualDayIndex * 24
// //
const dayStartHour = Math.max(0, startHour - totalHourOffset) const dayStartHour = Math.max(0, startHour - totalHourOffset)
@@ -763,6 +775,7 @@ const optimizedTimelineData = computed(() => {
dayStartHour, dayStartHour,
dayEndHour, dayEndHour,
visibleRange: { startHour, endHour }, visibleRange: { startHour, endHour },
actualDayIndex,
}, },
} }
}) })
@@ -813,9 +826,18 @@ const totalTimelineWidth = computed(() => {
return 0 return 0
}) })
// 使 watch scrollWidth
// DOM scrollWidth
// 使 immediate: true
// onMounted
watch(totalTimelineWidth, (newWidth) => {
timelineScrollWidth.value = newWidth
})
// //
const timelineBodyHeight = ref(0) const timelineBodyHeight = ref(0)
let resizeObserver: ResizeObserver | null = null let resizeObserver: ResizeObserver | null = null
let containerResizeObserver: ResizeObserver | null = null
// //
const milestonePositions = ref< const milestonePositions = ref<
@@ -940,21 +962,15 @@ const getOtherMilestonesInfo = (currentId: number) => {
// //
const handleSplitterDragStart = () => { const handleSplitterDragStart = () => {
isSplitterDragging.value = true isSplitterDragging.value = true
// ResizeObserver
// ResizeObserverisSplitterDragging
} }
// //
const handleSplitterDragEnd = () => { const handleSplitterDragEnd = () => {
isSplitterDragging.value = false isSplitterDragging.value = false
//
const timelineContainer = document.querySelector('.timeline') as HTMLElement
if (timelineContainer) {
const newWidth = timelineContainer.clientWidth
if (Math.abs(newWidth - timelineContainerWidth.value) > 1) {
timelineContainerWidth.value = newWidth
}
}
// Splitter // Splitter
// Timeline // Timeline
hideBubbles.value = true hideBubbles.value = true
@@ -966,7 +982,6 @@ const handleSplitterDragEnd = () => {
// TimelineresizeTaskList // TimelineresizeTaskList
const handleTimelineContainerResized = () => { const handleTimelineContainerResized = () => {
// Timeline // Timeline
// TaskBar // TaskBar
hideBubbles.value = true hideBubbles.value = true
@@ -1061,7 +1076,7 @@ const handleMilestoneUpdate = (updatedMilestone: Milestone) => {
} }
// //
const generateTimelineData = (): any => { function generateTimelineData(): any {
// 使 // 使
return getCachedTimelineData() return getCachedTimelineData()
} }
@@ -1072,7 +1087,7 @@ const clearTimelineCache = () => {
} }
// () // ()
const generateDayTimelineData = () => { function generateDayTimelineData() {
const months: unknown[] = [] const months: unknown[] = []
const currentDate = new Date(timelineConfig.value.startDate) const currentDate = new Date(timelineConfig.value.startDate)
@@ -1113,7 +1128,7 @@ const generateDayTimelineData = () => {
} }
// //
const isWorkingHour = (hour: number, dayOfWeek: number) => { function isWorkingHour(hour: number, dayOfWeek: number) {
// =6=0false // =6=0false
if (dayOfWeek === 0 || dayOfWeek === 6) { if (dayOfWeek === 0 || dayOfWeek === 6) {
return false return false
@@ -1142,7 +1157,7 @@ const isWorkingHour = (hour: number, dayOfWeek: number) => {
} }
// //
const generateHourTimelineData = () => { function generateHourTimelineData() {
const days: unknown[] = [] const days: unknown[] = []
const currentDate = new Date(timelineConfig.value.startDate) const currentDate = new Date(timelineConfig.value.startDate)
@@ -1184,7 +1199,7 @@ const generateHourTimelineData = () => {
} }
// //
const generateWeekTimelineData = () => { function generateWeekTimelineData() {
const allWeeks: unknown[] = [] const allWeeks: unknown[] = []
// //
const startDate = new Date(timelineConfig.value.startDate) const startDate = new Date(timelineConfig.value.startDate)
@@ -1251,7 +1266,7 @@ const generateWeekTimelineData = () => {
} }
// 7 // 7
const generateSubDaysForWeek = (weekStart: Date) => { function generateSubDaysForWeek(weekStart: Date) {
const subDays = [] const subDays = []
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
const date = new Date(weekStart) const date = new Date(weekStart)
@@ -1265,7 +1280,7 @@ const generateSubDaysForWeek = (weekStart: Date) => {
} }
// //
const isWeekContainsToday = (weekStart: Date, weekEnd: Date) => { function isWeekContainsToday(weekStart: Date, weekEnd: Date) {
const today = new Date() const today = new Date()
today.setHours(0, 0, 0, 0) today.setHours(0, 0, 0, 0)
return today >= weekStart && today <= weekEnd return today >= weekStart && today <= weekEnd
@@ -1400,7 +1415,7 @@ const updateTimeScale = (scale: TimelineScale) => {
} }
// //
const isToday = (date: Date) => { function isToday(date: Date) {
const today = new Date() const today = new Date()
return ( return (
date.getDate() === today.getDate() && date.getDate() === today.getDate() &&
@@ -1632,11 +1647,17 @@ const scrollToTodayCenter = (retry = 0) => {
todayPosition = daysDiff * dayWidth.value todayPosition = daysDiff * dayWidth.value
} }
// .timeline-body 退 .timeline // 使 timelineContainer containerWidth querySelector clientWidth
const scrollContainer = document.querySelector('.timeline') as HTMLElement if (!timelineContainer.value) {
//
if (retry < 10) {
setTimeout(() => scrollToTodayCenter(retry + 1), 60)
}
return
}
const containerWidth = scrollContainer.clientWidth const containerWidth = timelineContainerWidth.value
// 010 // 0 ResizeObserver 10
if (containerWidth === 0 && retry < 10) { if (containerWidth === 0 && retry < 10) {
setTimeout(() => scrollToTodayCenter(retry + 1), 60) setTimeout(() => scrollToTodayCenter(retry + 1), 60)
return return
@@ -1644,10 +1665,10 @@ const scrollToTodayCenter = (retry = 0) => {
// //
const centeredScrollPosition = todayPosition - containerWidth / 2 + 15 const centeredScrollPosition = todayPosition - containerWidth / 2 + 15
if (typeof scrollContainer.scrollTo === 'function') { if (typeof timelineContainer.value.scrollTo === 'function') {
scrollContainer.scrollTo({ left: Math.max(0, centeredScrollPosition), behavior: 'smooth' }) timelineContainer.value.scrollTo({ left: Math.max(0, centeredScrollPosition), behavior: 'smooth' })
} else { } else {
scrollContainer.scrollLeft = Math.max(0, centeredScrollPosition) timelineContainer.value.scrollLeft = Math.max(0, centeredScrollPosition)
} }
// //
@@ -1772,15 +1793,13 @@ const scrollToTasks = () => {
} }
totalDays += day totalDays += day
// 30px // 使
const timelinePanel = document.querySelector('.gantt-panel-right') const containerWidth = timelineContainerWidth.value || 400 //
const timelinePanelW = timelinePanel?.clientWidth const scrollPosition = (totalDays - 1) * 30 - containerWidth / 2
const scrollPosition = (totalDays - 1) * 30 - (timelinePanelW ? timelinePanelW / 2 : 200)
// // 使 timelineContainer querySelector
const timeline = document.querySelector('.timeline') if (timelineContainer.value) {
if (timeline) { timelineContainer.value.scrollLeft = Math.max(0, scrollPosition)
timeline.scrollLeft = Math.max(0, scrollPosition)
} }
} }
@@ -1809,17 +1828,16 @@ const scrollToToday = () => {
// 线30px // 线30px
const todayPosition = daysDiff * 30 const todayPosition = daysDiff * 30
// 线 // 使 timelineContainer containerWidth
const timeline = document.querySelector('.timeline') as HTMLElement if (!timelineContainer.value) return
if (!timeline) return
const containerWidth = timeline.clientWidth const containerWidth = timelineContainerWidth.value
// //
const centeredScrollPosition = todayPosition - containerWidth / 2 + 15 const centeredScrollPosition = todayPosition - containerWidth / 2 + 15
// //
timeline.scrollTo({ timelineContainer.value.scrollTo({
left: Math.max(0, centeredScrollPosition), left: Math.max(0, centeredScrollPosition),
behavior: 'smooth', behavior: 'smooth',
}) })
@@ -1878,12 +1896,17 @@ const bodyContentRef = ref<HTMLElement | null>(null)
const svgWidth = ref(0) const svgWidth = ref(0)
const svgHeight = ref(0) const svgHeight = ref(0)
// ResizeObserver bodyContent
let bodyContentResizeObserver: ResizeObserver | null = null
// bodyContent getBoundingClientRect
const bodyContentPosition = ref({ left: 0, top: 0, timestamp: 0 })
const BODY_POSITION_CACHE_TTL = 100 // 100ms
function updateSvgSize() { function updateSvgSize() {
if (bodyContentRef.value) { // ResizeObserver
svgWidth.value = bodyContentRef.value.offsetWidth // 使SVG
// 使SVG svgHeight.value = contentHeight.value
svgHeight.value = contentHeight.value
}
} }
function handleBarMounted(payload: { function handleBarMounted(payload: {
@@ -1894,11 +1917,33 @@ function handleBarMounted(payload: {
height: number height: number
}) { }) {
if (!bodyContentRef.value) return if (!bodyContentRef.value) return
const baseRect = bodyContentRef.value.getBoundingClientRect()
const now = Date.now()
let baseLeft = 0
let baseTop = 0
//
if (now - bodyContentPosition.value.timestamp < BODY_POSITION_CACHE_TTL) {
// 使
baseLeft = bodyContentPosition.value.left
baseTop = bodyContentPosition.value.top
} else {
// DOM
const baseRect = bodyContentRef.value.getBoundingClientRect()
baseLeft = baseRect.left
baseTop = baseRect.top
bodyContentPosition.value = {
left: baseLeft,
top: baseTop,
timestamp: now,
}
}
// bodyContent // bodyContent
taskBarPositions.value[payload.id] = { taskBarPositions.value[payload.id] = {
left: payload.left - baseRect.left, left: payload.left - baseLeft,
top: payload.top - baseRect.top, top: payload.top - baseTop,
width: payload.width, width: payload.width,
height: payload.height, height: payload.height,
} }
@@ -1930,8 +1975,8 @@ const handleScrollToPosition = (targetScrollLeft: number) => {
// //
hideBubbles.value = true hideBubbles.value = true
// // 使
const maxScrollLeft = timelineContainer.value.scrollWidth - timelineContainer.value.clientWidth const maxScrollLeft = timelineScrollWidth.value - timelineContainerWidth.value
const clampedScrollLeft = Math.max(0, Math.min(targetScrollLeft, maxScrollLeft)) const clampedScrollLeft = Math.max(0, Math.min(targetScrollLeft, maxScrollLeft))
// //
@@ -2001,6 +2046,9 @@ const links = computed(() => {
}) })
onMounted(() => { onMounted(() => {
// timelineScrollWidth
timelineScrollWidth.value = totalTimelineWidth.value
// DOM // DOM
nextTick(() => { nextTick(() => {
setTimeout(() => { setTimeout(() => {
@@ -2041,8 +2089,7 @@ onMounted(() => {
const timelineBody = document.querySelector('.timeline-body') as HTMLElement const timelineBody = document.querySelector('.timeline-body') as HTMLElement
const timelineContainer = document.querySelector('.timeline') as HTMLElement const timelineContainer = document.querySelector('.timeline') as HTMLElement
if (timelineBody) { if (timelineBody) {
timelineBodyHeight.value = timelineBody.clientHeight // 使ResizeObserverclientHeight
resizeObserver = new ResizeObserver(entries => { resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) { for (const entry of entries) {
timelineBodyHeight.value = entry.contentRect.height timelineBodyHeight.value = entry.contentRect.height
@@ -2054,31 +2101,39 @@ onMounted(() => {
// 使 // 使
if (timelineContainer) { if (timelineContainer) {
timelineScrollLeft.value = timelineContainer.scrollLeft // 使ResizeObserverclientWidth
timelineContainerWidth.value = timelineContainer.clientWidth // scrollLeftscroll
// ResizeObserver // ResizeObserver
const containerResizeObserver = new ResizeObserver(entries => { containerResizeObserver = new ResizeObserver(entries => {
for (const entry of entries) { for (const entry of entries) {
const newWidth = entry.contentRect.width const newWidth = entry.contentRect.width
//
//
// 使TaskBarcontainerWidth
if (Math.abs(newWidth - timelineContainerWidth.value) > 1) { if (Math.abs(newWidth - timelineContainerWidth.value) > 1) {
timelineContainerWidth.value = newWidth timelineContainerWidth.value = newWidth
// // scrollWidth
// TaskBar //
hideBubbles.value = true
// // /
if (hideBubblesTimeout) { if (!isSplitterDragging.value) {
clearTimeout(hideBubblesTimeout) //
// TaskBar
hideBubbles.value = true
//
if (hideBubblesTimeout) {
clearTimeout(hideBubblesTimeout)
}
//
hideBubblesTimeout = setTimeout(() => {
hideBubbles.value = false
hideBubblesTimeout = null
}, 300) // 300msresize
} }
//
hideBubblesTimeout = setTimeout(() => {
hideBubbles.value = false
hideBubblesTimeout = null
}, 300) // 300msresize
} }
} }
}) })
@@ -2088,6 +2143,16 @@ onMounted(() => {
if (!resizeObserver) { if (!resizeObserver) {
resizeObserver = containerResizeObserver resizeObserver = containerResizeObserver
} }
// bodyContentupdateSvgSizeoffsetWidth
if (bodyContentRef.value) {
bodyContentResizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
svgWidth.value = entry.contentRect.width
}
})
bodyContentResizeObserver.observe(bodyContentRef.value)
}
} }
}) })
@@ -2284,14 +2349,15 @@ const handleTimelineScroll = (event: Event) => {
const target = event.target as HTMLElement const target = event.target as HTMLElement
if (!target) return if (!target) return
// 使scrollLeft
const scrollLeft = target.scrollLeft const scrollLeft = target.scrollLeft
const scrollWidth = target.scrollWidth const scrollWidth = timelineScrollWidth.value
const clientWidth = target.clientWidth const clientWidth = timelineContainerWidth.value
const maxScroll = scrollWidth - clientWidth const maxScroll = scrollWidth - clientWidth
// //
timelineScrollLeft.value = scrollLeft timelineScrollLeft.value = scrollLeft
timelineContainerWidth.value = clientWidth // timelineContainerWidth ResizeObserver
// //
if (isInitialLoad.value && scrollLeft > 0) { if (isInitialLoad.value && scrollLeft > 0) {
@@ -2349,7 +2415,8 @@ const startAutoScroll = (direction: 'left' | 'right') => {
if (!timelineContainer.value || !isAutoScrolling.value) return if (!timelineContainer.value || !isAutoScrolling.value) return
const currentScrollLeft = timelineContainer.value.scrollLeft const currentScrollLeft = timelineContainer.value.scrollLeft
const maxScrollLeft = timelineContainer.value.scrollWidth - timelineContainer.value.clientWidth // 使
const maxScrollLeft = timelineScrollWidth.value - timelineContainerWidth.value
let newScrollLeft let newScrollLeft
if (direction === 'left') { if (direction === 'left') {
@@ -2404,8 +2471,7 @@ const handleDragBoundaryCheck = (event: CustomEvent) => {
startAutoScroll('left') startAutoScroll('left')
} else if ( } else if (
relativeX >= containerRect.width - EDGE_SCROLL_ZONE && relativeX >= containerRect.width - EDGE_SCROLL_ZONE &&
timelineContainer.value.scrollLeft < timelineContainer.value.scrollLeft < timelineScrollWidth.value - timelineContainerWidth.value
timelineContainer.value.scrollWidth - timelineContainer.value.clientWidth
) { ) {
// //
startAutoScroll('right') startAutoScroll('right')
@@ -2442,6 +2508,12 @@ onUnmounted(() => {
resizeObserver = null resizeObserver = null
} }
// bodyContentResizeObserver
if (bodyContentResizeObserver) {
bodyContentResizeObserver.disconnect()
bodyContentResizeObserver = null
}
// window // window
window.removeEventListener('resize', updateSvgSize) window.removeEventListener('resize', updateSvgSize)
@@ -2580,7 +2652,6 @@ watch(
(newWidth, oldWidth) => { (newWidth, oldWidth) => {
// splitter timelineData // splitter timelineData
if (isSplitterDragging.value) return if (isSplitterDragging.value) return
// 0 // 0
if (!oldWidth || oldWidth === 0 || Math.abs(newWidth - oldWidth) > 50) { if (!oldWidth || oldWidth === 0 || Math.abs(newWidth - oldWidth) > 50) {
if (newWidth > 0) { if (newWidth > 0) {
@@ -2620,9 +2691,11 @@ watch(
{ immediate: true }, { immediate: true },
) )
// timelineDataTaskBar线 // timelineDataTaskBar线
watch([timelineData, timelineContainerWidth], () => { // timelineDatatimelineContainerWidth
// splitter TaskBar // TaskBarcomputedcontainerWidth
watch(timelineData, () => {
// splitter TaskBar
if (isSplitterDragging.value) return if (isSplitterDragging.value) return
// //
@@ -2658,38 +2731,27 @@ watch(
const handleMilestoneClickLocate = (event: CustomEvent) => { const handleMilestoneClickLocate = (event: CustomEvent) => {
const { scrollLeft, smooth } = event.detail const { scrollLeft, smooth } = event.detail
// Timeline - // 使timelineContainerquerySelector
const timelineMain = document.querySelector('.timeline') as HTMLElement if (timelineContainer.value) {
const timelineBody = document.querySelector('.timeline-body') as HTMLElement // 使
const maxScrollLeft = timelineScrollWidth.value - timelineContainerWidth.value
//
let scrollContainer: HTMLElement | null = null
if (timelineMain && timelineMain.scrollWidth > timelineMain.clientWidth) {
scrollContainer = timelineMain
} else if (timelineBody && timelineBody.scrollWidth > timelineBody.clientWidth) {
scrollContainer = timelineBody
}
if (scrollContainer) {
//
const maxScrollLeft = scrollContainer.scrollWidth - scrollContainer.clientWidth
const targetScrollLeft = Math.min(Math.max(0, scrollLeft), maxScrollLeft) const targetScrollLeft = Math.min(Math.max(0, scrollLeft), maxScrollLeft)
if (smooth) { if (smooth) {
// //
scrollContainer.scrollTo({ timelineContainer.value.scrollTo({
left: targetScrollLeft, left: targetScrollLeft,
behavior: 'smooth', behavior: 'smooth',
}) })
} else { } else {
// //
scrollContainer.scrollLeft = targetScrollLeft timelineContainer.value.scrollLeft = targetScrollLeft
} }
} }
} }
// //
const generateMonthTimelineData = () => { function generateMonthTimelineData() {
// //
let startDate: Date, endDate: Date let startDate: Date, endDate: Date
@@ -2820,7 +2882,7 @@ const generateMonthTimelineData = () => {
} }
// //
const generateQuarterTimelineData = () => { function generateQuarterTimelineData() {
// 使 GanttChart buffer // 使 GanttChart buffer
const startDate = timelineConfig.value.startDate const startDate = timelineConfig.value.startDate
const endDate = timelineConfig.value.endDate const endDate = timelineConfig.value.endDate
@@ -2865,14 +2927,14 @@ const generateQuarterTimelineData = () => {
} }
// //
const isQuarterContainsToday = (startDate: Date, endDate: Date) => { function isQuarterContainsToday(startDate: Date, endDate: Date) {
const today = new Date() const today = new Date()
today.setHours(0, 0, 0, 0) today.setHours(0, 0, 0, 0)
return today >= startDate && today <= endDate return today >= startDate && today <= endDate
} }
// //
const generateYearTimelineData = () => { function generateYearTimelineData() {
// 使 GanttChart buffer // 使 GanttChart buffer
const startDate = timelineConfig.value.startDate const startDate = timelineConfig.value.startDate
const endDate = timelineConfig.value.endDate const endDate = timelineConfig.value.endDate
@@ -3557,7 +3619,7 @@ const handleAddSuccessor = (task: Task) => {
:container-width="timelineContainerWidth" :container-width="timelineContainerWidth"
:hide-bubbles="hideBubbles" :hide-bubbles="hideBubbles"
:timeline-data=" :timeline-data="
currentTimeScale === TimelineScale.HOUR ? optimizedTimelineData : timelineData currentTimeScale === TimelineScale.HOUR ? [] : timelineData
" "
:current-time-scale="currentTimeScale" :current-time-scale="currentTimeScale"
:task-bar-config="props.taskBarConfig" :task-bar-config="props.taskBarConfig"