Skip to content

Commit 1278c41

Browse files
committed
v1.4.2-patch.3 - 性能优化
1 parent 25c54a1 commit 1278c41

File tree

5 files changed

+377
-155
lines changed

5 files changed

+377
-155
lines changed

demo/data.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"id": 1101,
3232
"name": "试验方案设计与<span style='font-weight: bold;color:red;'>伦理审查</span>",
3333
"assignee": "方案设计师 李明",
34-
"avatar": "https://i.pravatar.cc/150?img=1",
34+
"avatar": "https://i.pravatar.cc/50?img=1",
3535
"startDate": "2025-01-01",
3636
"endDate": "2025-02-28",
3737
"progress": 100,
@@ -46,7 +46,7 @@
4646
"id": 1102,
4747
"name": "受试者招募与筛选",
4848
"assignee": "招募专员 张丽",
49-
"avatar": "https://i.pravatar.cc/150?img=5",
49+
"avatar": "https://i.pravatar.cc/50?img=5",
5050
"startDate": "2025-03-01",
5151
"endDate": "2025-04-30",
5252
"progress": 90,
@@ -62,7 +62,7 @@
6262
"id": 1103,
6363
"name": "药物给药与安全性监测",
6464
"assignee": "临床医生 Dr. Liu",
65-
"avatar": "https://i.pravatar.cc/150?img=8",
65+
"avatar": "https://i.pravatar.cc/50?img=8",
6666
"startDate": "2025-05-01",
6767
"endDate": "2025-08-31",
6868
"progress": 40,
@@ -95,7 +95,7 @@
9595
"id": 1201,
9696
"name": "多中心试验<span style='font-weight: bold; color: blue;'>启动</span>",
9797
"assignee": "项目经理 王芳",
98-
"avatar": "https://i.pravatar.cc/150?img=10",
98+
"avatar": "https://i.pravatar.cc/50?img=10",
9999
"startDate": "2025-09-01",
100100
"endDate": "2025-11-30",
101101
"progress": 25,
@@ -111,7 +111,7 @@
111111
"id": 1202,
112112
"name": "患者入组与随机化",
113113
"assignee": "数据管理员 陈静",
114-
"avatar": "https://i.pravatar.cc/150?img=20",
114+
"avatar": "https://i.pravatar.cc/50?img=20",
115115
"startDate": "2025-12-01",
116116
"endDate": "2026-03-31",
117117
"progress": 0,
@@ -127,7 +127,7 @@
127127
"id": 1203,
128128
"name": "疗效评估与数据收集",
129129
"assignee": "统计师 赵磊",
130-
"avatar": "https://i.pravatar.cc/150?img=15",
130+
"avatar": "https://i.pravatar.cc/50?img=15",
131131
"startDate": "2026-01-01",
132132
"endDate": "2026-08-31",
133133
"progress": 0,

src/components/GanttChart.vue

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -136,35 +136,45 @@ interface Props {
136136
const ganttRootRef = ref<HTMLElement | null>(null)
137137
const ganttContainerWidth = ref(1920) // 默认使用常见的屏幕宽度作为初始值
138138
139+
// ResizeObserver 用于监听容器宽度变化
140+
let ganttRootResizeObserver: ResizeObserver | null = null
141+
139142
// 监听容器宽度变化
140-
const updateContainerWidth = () => {
141-
if (ganttRootRef.value) {
142-
const newWidth = ganttRootRef.value.clientWidth
143-
if (newWidth !== ganttContainerWidth.value) {
144-
ganttContainerWidth.value = newWidth
145-
// 容器宽度变化时,重新计算 TaskList 的宽度限制
146-
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
147-
taskListBodyWidth.value = getTaskListMaxWidth()
148-
taskListBodyProposedWidth.value = getTaskListMaxWidth()
149-
taskListBodyWidthLimit.value = getTaskListMaxWidth()
143+
const updateContainerWidth = (newWidth: number) => {
144+
if (newWidth !== ganttContainerWidth.value) {
145+
ganttContainerWidth.value = newWidth
146+
// 容器宽度变化时,重新计算 TaskList 的宽度限制
147+
ganttPanelLeftMinWidth.value = getTaskListMinWidth()
148+
taskListBodyWidth.value = getTaskListMaxWidth()
149+
taskListBodyProposedWidth.value = getTaskListMaxWidth()
150+
taskListBodyWidthLimit.value = getTaskListMaxWidth()
150151
151-
// 确保当前宽度在新的限制范围内
152-
const adjustedWidth = checkWidthLimits(leftPanelWidth.value)
153-
if (adjustedWidth !== leftPanelWidth.value) {
154-
leftPanelWidth.value = adjustedWidth
155-
}
152+
// 确保当前宽度在新的限制范围内
153+
const adjustedWidth = checkWidthLimits(leftPanelWidth.value)
154+
if (adjustedWidth !== leftPanelWidth.value) {
155+
leftPanelWidth.value = adjustedWidth
156156
}
157157
}
158158
}
159159
160160
onMounted(() => {
161-
updateContainerWidth()
162-
// 监听窗口大小变化
163-
window.addEventListener('resize', updateContainerWidth)
161+
if (ganttRootRef.value) {
162+
// 使用 ResizeObserver 监听容器宽度变化,避免频繁读取 clientWidth
163+
ganttRootResizeObserver = new ResizeObserver((entries) => {
164+
for (const entry of entries) {
165+
// 使用 contentRect.width,避免强制重排
166+
updateContainerWidth(entry.contentRect.width)
167+
}
168+
})
169+
ganttRootResizeObserver.observe(ganttRootRef.value)
170+
}
164171
})
165172
166173
onUnmounted(() => {
167-
window.removeEventListener('resize', updateContainerWidth)
174+
if (ganttRootResizeObserver) {
175+
ganttRootResizeObserver.disconnect()
176+
ganttRootResizeObserver = null
177+
}
168178
})
169179
170180
// TaskList最小宽度,支持通过taskListConfig配置(支持像素和百分比)
@@ -331,25 +341,41 @@ function onMouseDown(e: MouseEvent) {
331341
document.addEventListener('wheel', blockAllEvents, { capture: true, passive: false })
332342
document.addEventListener('contextmenu', blockAllEvents, { capture: true })
333343
344+
// ⚠️ 使用requestAnimationFrame节流,但移除阈值检测,确保每帧都更新
345+
let rafId: number | null = null
346+
334347
function onMouseMove(ev: MouseEvent) {
335348
if (!dragging.value) return
336-
337349
// 强制阻止所有默认行为和事件传播
338350
ev.preventDefault()
339351
ev.stopPropagation()
340352
ev.stopImmediatePropagation()
341353
342354
const delta = ev.clientX - startX
343355
const proposedWidth = startWidth + delta
344-
345-
// 直接使用面板宽度限制检查,无需复杂的坐标计算
346356
const finalWidth = checkWidthLimits(proposedWidth)
347-
leftPanelWidth.value = finalWidth
357+
358+
// 取消之前的帧请求
359+
if (rafId !== null) {
360+
cancelAnimationFrame(rafId)
361+
}
362+
363+
// 在下一帧更新(节流到60fps,避免过度触发响应式系统)
364+
rafId = requestAnimationFrame(() => {
365+
leftPanelWidth.value = finalWidth
366+
rafId = null
367+
})
348368
}
349369
350370
function onMouseUp() {
351371
dragging.value = false
352372
373+
// 取消未完成的帧请求
374+
if (rafId !== null) {
375+
cancelAnimationFrame(rafId)
376+
rafId = null
377+
}
378+
353379
// 移除全局事件拦截器
354380
document.removeEventListener('mousedown', blockAllEvents, { capture: true })
355381
document.removeEventListener('click', blockAllEvents, { capture: true })
@@ -480,10 +506,8 @@ onMounted(() => {
480506
// 监听右侧面板(timeline 的可视容器)的宽度
481507
const rightPanel = document.querySelector('.gantt-panel-right')
482508
if (rightPanel) {
483-
// 初始化宽度
484-
timelineContainerWidth.value = rightPanel.clientWidth
485-
486-
// 使用 ResizeObserver 监听宽度变化
509+
// 使用 ResizeObserver 自动更新宽度,避免直接读取clientWidth造成强制重排
510+
// ResizeObserver 会在开始观察时立即触发一次回调,提供初始宽度
487511
resizeObserver = new ResizeObserver(entries => {
488512
for (const entry of entries) {
489513
timelineContainerWidth.value = entry.contentRect.width
@@ -2162,6 +2186,7 @@ function handleMilestoneDialogDelete(milestoneId: number) {
21622186
<div
21632187
v-if="isTaskListVisible"
21642188
class="gantt-panel gantt-panel-left"
2189+
:class="{ dragging: dragging }"
21652190
:style="{ width: leftPanelWidth + 'px' }"
21662191
>
21672192
<TaskList
@@ -2317,6 +2342,12 @@ function handleMilestoneDialogDelete(milestoneId: number) {
23172342
.gantt-panel-left {
23182343
/* width 由js控制 */
23192344
min-width: 320px;
2345+
/* ⚠️ 拖拽时禁用transition,提升响应速度 */
2346+
transition: none;
2347+
}
2348+
2349+
.gantt-panel-left:not(.dragging) {
2350+
/* 非拖拽时保留平滑过渡效果(如toggle时) */
23202351
transition: width 0.1s;
23212352
}
23222353

src/components/TaskBar.vue

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ const createLocalToday = (): Date => {
119119
return new Date(now.getFullYear(), now.getMonth(), now.getDate())
120120
}
121121
122+
// 缓存今天的日期,避免频繁创建
123+
// 每分钟更新一次缓存(对于日期判断来说足够了)
124+
const cachedToday = ref(createLocalToday())
125+
let todayCacheTimer: number | null = null
126+
127+
onMounted(() => {
128+
// 每60秒更新一次今天的日期缓存
129+
todayCacheTimer = window.setInterval(() => {
130+
cachedToday.value = createLocalToday()
131+
}, 60000)
132+
})
133+
134+
onUnmounted(() => {
135+
if (todayCacheTimer !== null) {
136+
clearInterval(todayCacheTimer)
137+
todayCacheTimer = null
138+
}
139+
})
140+
122141
const formatDateToLocalString = (date: Date): string => {
123142
const year = date.getFullYear()
124143
const month = String(date.getMonth() + 1).padStart(2, '0')
@@ -223,7 +242,7 @@ const taskBarStyle = computed(() => {
223242
224243
const startDate = createLocalDate(currentStartDate)
225244
const endDate = createLocalDate(currentEndDate)
226-
const baseStart = createLocalDate(props.startDate)
245+
const baseStart = parsedBaseStartDate.value
227246
if (!startDate || !endDate || !baseStart) {
228247
return {
229248
left: '0px',
@@ -392,6 +411,24 @@ const taskBarStyle = computed(() => {
392411
}
393412
})
394413
414+
// 缓存 TaskBar 的位置信息,减少 DOM 读取频率
415+
const cachedPosition = ref({
416+
left: 0,
417+
top: 0,
418+
width: 0,
419+
height: 0,
420+
timestamp: 0,
421+
})
422+
423+
// 位置缓存有效期(毫秒)
424+
const POSITION_CACHE_TTL = 100 // 100ms 内使用缓存值
425+
426+
// 缓存解析后的结束日期,避免在 taskStatus 中重复解析
427+
const parsedEndDate = computed(() => createLocalDate(props.task.endDate || ''))
428+
429+
// 缓存解析后的基准开始日期
430+
const parsedBaseStartDate = computed(() => createLocalDate(props.startDate))
431+
395432
// 计算任务状态和颜色
396433
const taskStatus = computed(() => {
397434
// 父级任务(Story类型)使用与新建按钮一致的配色
@@ -404,8 +441,9 @@ const taskStatus = computed(() => {
404441
}
405442
}
406443
407-
const today = createLocalToday()
408-
const endDate = createLocalDate(props.task.endDate || '')
444+
// 使用缓存的今天日期,避免频繁创建日期对象
445+
const today = cachedToday.value
446+
const endDate = parsedEndDate.value
409447
const progress = props.task.progress || 0
410448
411449
// 已完成
@@ -556,6 +594,8 @@ const handleMouseDown = (e: MouseEvent, type: 'drag' | 'resize-left' | 'resize-r
556594
const timelineContainer = document.querySelector('.timeline') as HTMLElement
557595
if (!timelineContainer || !barRef.value) return
558596
597+
// 在 mousedown 事件中读取位置是合理的(不是高频操作)
598+
// 这个值用于计算拖拽偏移量,只在开始拖拽时读取一次
559599
const barRect = barRef.value.getBoundingClientRect()
560600
561601
// 计算鼠标相对于TaskBar的位置
@@ -595,17 +635,57 @@ const handleAutoScroll = (event: CustomEvent) => {
595635
}
596636
}
597637
638+
// 使用缓存机制减少 DOM 读取频率,但保证位置准确性
639+
let reportPositionScheduled = false
640+
598641
function reportBarPosition() {
599-
if (barRef.value) {
642+
// 如果已经安排了本帧的位置报告,则跳过
643+
if (reportPositionScheduled) return
644+
645+
reportPositionScheduled = true
646+
647+
requestAnimationFrame(() => {
648+
reportPositionScheduled = false
649+
650+
if (!barRef.value) return
651+
652+
const now = Date.now()
653+
654+
// 如果缓存未过期,使用缓存值
655+
if (now - cachedPosition.value.timestamp < POSITION_CACHE_TTL) {
656+
emit('bar-mounted', {
657+
id: props.task.id,
658+
left: cachedPosition.value.left,
659+
top: cachedPosition.value.top,
660+
width: cachedPosition.value.width,
661+
height: cachedPosition.value.height,
662+
})
663+
return
664+
}
665+
666+
// 缓存过期或首次调用,读取 DOM 并更新缓存
667+
// TaskBar 传递绝对位置(相对于视口),Timeline 会将其转换为相对位置
600668
const rect = barRef.value.getBoundingClientRect()
601-
emit('bar-mounted', {
602-
id: props.task.id,
669+
670+
// 计算并缓存位置
671+
const position = {
603672
left: rect.left,
604673
top: rect.top,
605674
width: rect.width,
606675
height: rect.height,
676+
}
677+
678+
// 更新缓存
679+
cachedPosition.value = {
680+
...position,
681+
timestamp: now,
682+
}
683+
684+
emit('bar-mounted', {
685+
id: props.task.id,
686+
...position,
607687
})
608-
}
688+
})
609689
}
610690
611691
// 拖拽时的实时日期提示框状态
@@ -1123,8 +1203,20 @@ const handleMouseUp = () => {
11231203
onMounted(() => {
11241204
nextTick(() => {
11251205
reportBarPosition()
1206+
1207+
// 使用 ResizeObserver 监听任务名称宽度变化
11261208
if (taskBarNameRef.value) {
1127-
nameTextWidth.value = taskBarNameRef.value.getBoundingClientRect().width
1209+
const nameResizeObserver = new ResizeObserver((entries) => {
1210+
for (const entry of entries) {
1211+
nameTextWidth.value = entry.contentRect.width
1212+
}
1213+
})
1214+
nameResizeObserver.observe(taskBarNameRef.value)
1215+
1216+
// 组件卸载时清理
1217+
onUnmounted(() => {
1218+
nameResizeObserver.disconnect()
1219+
})
11281220
}
11291221
})
11301222
@@ -1395,7 +1487,7 @@ const stickyStyles = computed(() => {
13951487
} else if (nameNeedsRightSticky) {
13961488
const offset = rightBoundary - taskLeft - nameWidth
13971489
// name 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度
1398-
nameLeft = `${offset - handleWidth - 3}px` // 考虑手柄宽度 + 间距
1490+
nameLeft = `${offset - handleWidth - 10}px` // 考虑手柄宽度 + 间距
13991491
namePosition = 'absolute'
14001492
nameTop = '2px'
14011493
}
@@ -1431,7 +1523,7 @@ const stickyStyles = computed(() => {
14311523
progressTop = '18px'
14321524
} else if (progressNeedsRightSticky) {
14331525
const offset = rightBoundary - taskLeft - progressWidth
1434-
// progress 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度
1526+
// 右侧磁吸时应始终保持与右边框固定距离,需要减去右侧手柄宽度
14351527
progressLeft = `${offset - handleWidth - 3}px` // 考虑手柄宽度 + 间距
14361528
progressPosition = 'absolute'
14371529
progressTop = '18px'

0 commit comments

Comments
 (0)