diff --git a/CHANGELOG.md b/CHANGELOG.md index 77fd36d..2ca4b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.9.4] - 2026-03-06 + +### Fixed +- 🔧 修复:日期视图下,SplitterBar拖拽以及TaskList收起/展开后,计算导致2月份周期丢失问题 +- 🔧 修复:任务视图切换至资源视图后今日定位偏移问题 +- 🔧 修复:任务视图切换至资源视图后最大和最小日期范围计算问题 +- 🔧 修复:资源视图下,小时/日/周视图切换后,最大值和最小值范围问题 +- 🔧 Fixed: In date view, after dragging the SplitterBar and collapsing/expanding the TaskList, the calculation caused the February cycle to be lost +- 🔧 Fixed: After switching from task view to resource view, the today marker offset issue +- 🔧 Fixed: After switching from task view to resource view, the max and min date range calculation issue +- 🔧 Fixed: In resource view, after switching between hourly/daily/weekly views, the max and min date range issue + ## [1.9.3] - 2026-03-05 ### Added diff --git a/demo/data-small.json b/demo/data-small.json new file mode 100644 index 0000000..1766aa7 --- /dev/null +++ b/demo/data-small.json @@ -0,0 +1,37 @@ +{ + "tasks": [ + { + "id": 1001, + "name": "上午会议", + "assignee": "测试用户", + "startDate": "2026-03-02 09:00", + "endDate": "2026-03-10 10:30", + "progress": 50, + "estimatedHours": 1.5, + "actualHours": 0.75, + "type": "task" + }, + { + "id": 1002, + "name": "午后开发", + "assignee": "开发者", + "startDate": "2026-03-02 09:00", + "endDate": "2026-03-06 16:15", + "progress": 30, + "estimatedHours": 2.25, + "actualHours": 1.33, + "type": "task" + }, + { + "id": 1003, + "name": "测试任务", + "assignee": "测试员", + "startDate": "2026-03-02 09:00", + "endDate": "2026-03-08 17:45", + "progress": 0, + "estimatedHours": 1.25, + "actualHours": 0, + "type": "task" + } + ] +} diff --git a/demo/version-history.json b/demo/version-history.json index d20246f..10ea53a 100644 --- a/demo/version-history.json +++ b/demo/version-history.json @@ -588,5 +588,21 @@ "🔧 Fixed: TaskBar width (duration) shrinking after dragging left and right in weekly view", "Special thanks to xuhang513@gitee for their use and feedback" ] + }, + { + "version": "1.9.4", + "date": "2026-03-06", + "notes": [ + "🔧 修复:日期视图下,SplitterBar拖拽以及TaskList收起/展开后,计算导致2月份周期丢失问题", + "🔧 修复:任务视图切换至资源视图后今日定位偏移问题", + "🔧 修复:任务视图切换至资源视图后最大和最小日期范围计算问题", + "🔧 修复:资源视图下,小时/日/周视图切换后,最大值和最小值范围问题", + "特别感谢来自jwuxin@gitee的使用与反馈", + "🔧 Fixed: In date view, after dragging the SplitterBar and collapsing/expanding the TaskList, the calculation caused the February cycle to be lost", + "🔧 Fixed: After switching from task view to resource view, the today marker offset issue", + "🔧 Fixed: After switching from task view to resource view, the max and min date range calculation issue", + "🔧 Fixed: In resource view, after switching between hourly/daily/weekly views, the max and min date range issue", + "Special thanks to jwuxin@gitee for their use and feedback" + ] } ] diff --git a/package.json b/package.json index 3abb684..c2b5505 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jordium-gantt-vue3", - "version": "1.9.3", + "version": "1.9.4", "type": "module", "main": "npm-package/dist/jordium-gantt-vue3.cjs.js", "module": "npm-package/dist/jordium-gantt-vue3.es.js", diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue index 3425902..83aaea9 100644 --- a/src/components/GanttChart.vue +++ b/src/components/GanttChart.vue @@ -813,6 +813,10 @@ const dragging = ref(false) // v1.9.5 P2-4优化 - 提供Split Bar拖拽状态给子组件 provide('isSplitBarDragging', dragging) +// TaskList 切换动画进行中的标志(提供给 Timeline,避免 toggle 期间误触发 resize 重算) +const isTaskListToggling = ref(false) +provide('isTaskListToggling', isTaskListToggling) + function onMouseDown(e: MouseEvent) { // 检查事件目标是否是task-list-toggle按钮或其子元素 const target = e.target as HTMLElement @@ -991,6 +995,9 @@ const toggleTaskList = () => { // 如果正在动画中,忽略点击 if (isAnimating.value) return + // 标记 toggle 进行中,阻止 Timeline 内 containerResizeObserver 误触发日期范围重算 + isTaskListToggling.value = true + // 触发撞击动画 isAnimating.value = true @@ -1012,7 +1019,9 @@ const toggleTaskList = () => { isAnimating.value = false animationClass.value = '' - // 手动切换TaskList后,通知Timeline重新计算半圆 + // toggle 结束:先清除标志(Timeline watch 会响应并只更新 sticky/bubble,跳过日期范围重算) + // 再通知 Timeline 重新计算半圆和 TaskBar 位置 + isTaskListToggling.value = false nextTick(() => { window.dispatchEvent( new CustomEvent('timeline-container-resized', { @@ -1025,11 +1034,14 @@ const toggleTaskList = () => { // 监听Timeline的TaskList切换事件 const handleToggleTaskList = (event: CustomEvent) => { + // 标记 toggle 进行中,阻止 Timeline 内 containerResizeObserver 误触发日期范围重算 + isTaskListToggling.value = true isTaskListVisible.value = event.detail // TaskList切换会改变Timeline容器宽度,需要通知Timeline重新计算半圆 - // 派发事件通知Timeline容器宽度发生了变化 + // toggle 结束:清除标志后再派发事件,Timeline watch 响应时只更新 sticky/bubble nextTick(() => { + isTaskListToggling.value = false window.dispatchEvent( new CustomEvent('timeline-container-resized', { detail: { source: 'task-list-toggle' }, @@ -1069,11 +1081,16 @@ onMounted(() => { // 监听 timeline 容器宽度变化 nextTick(() => { - // 监听右侧面板(timeline 的可视容器)的宽度 - const rightPanel = document.querySelector('.gantt-panel-right') - if (rightPanel) { + // 监听 gantt-body(整体布局容器)宽度变化作为日期范围规划的基准 + // 使用 .gantt-body 而非 .gantt-panel-right,原因: + // 1. 拖拽 SplitterBar 和 TaskList toggle 只改变左右面板的内部分割,.gantt-body 总宽度不变 + // → 不会误触发日期范围重算 + // 2. 窗口 resize / 全屏切换才改变 .gantt-body 宽度 → 正确触发重算 + // 3. 作为「最大可能宽度」兜底:TaskList 隐藏时 Timeline 最大可展开至此宽度 + const ganttBody = document.querySelector('.gantt-body') + if (ganttBody) { // 初始化宽度 - timelineContainerWidth.value = rightPanel.clientWidth + timelineContainerWidth.value = ganttBody.clientWidth // 使用 ResizeObserver 监听宽度变化 resizeObserver = new ResizeObserver(entries => { @@ -1081,7 +1098,7 @@ onMounted(() => { timelineContainerWidth.value = entry.contentRect.width } }) - resizeObserver.observe(rightPanel) + resizeObserver.observe(ganttBody) } }) }) @@ -1720,8 +1737,18 @@ const timelineDateRange = computed(() => { } let allTasks: Task[] = [] - if (props.tasks) allTasks = allTasks.concat(flattenTasks(props.tasks)) - if (props.milestones) allTasks = allTasks.concat(props.milestones) + + if (currentViewMode.value === 'resource' && props.resources) { + // 资源视图:从 resource.tasks 提取日期,避免传入基于任务视图的错误范围 + for (const resource of (props.resources as Resource[])) { + if (resource.tasks && Array.isArray(resource.tasks)) { + allTasks = allTasks.concat(flattenTasks(resource.tasks as Task[])) + } + } + } else { + if (props.tasks) allTasks = allTasks.concat(flattenTasks(props.tasks)) + if (props.milestones) allTasks = allTasks.concat(props.milestones) + } // 过滤出有日期的任务 const startDates = allTasks diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index b3595da..4e656cb 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -168,6 +168,9 @@ provide('positionCache', positionCache) // v1.9.5 P2-4优化 - 注入Split Bar拖拽状态 const isSplitBarDragging = inject>('isSplitBarDragging', ref(false)) +// 注入 TaskList toggle 状态(toggle 期间阻止 containerResizeObserver 误触发日期范围重算) +const isTaskListToggling = inject>('isTaskListToggling', ref(false)) + // v1.9.5 注入showConflicts配置 const showConflicts = inject>('gantt-show-conflicts', computed(() => true)) @@ -292,7 +295,10 @@ const currentTimeScale = ref(TimelineScale.DAY) // 响应外部props变化,动态更新timelineConfig watch([timelineStartDate, timelineEndDate], ([newStart, newEnd]) => { - // 所有视图都正常响应props变化 + // 资源视图下不响应 props 变化: + // GanttChart 的 timelineDateRange 计算可能传入基于任务视图的日期范围(已 computed 修复,但保留守卫作为二重保险) + // 资源视图的日期范围由 updateTimeScale / debouncedUpdateTimelineRange 内部管理 + if (viewMode.value === 'resource') return if (props.startDate || props.endDate) { if (!isUpdatingTimelineConfig) { timelineConfig.value.startDate = newStart @@ -496,9 +502,13 @@ const getDayTimelineRange = () => { const { minDate, maxDate } = taskRange - // 开始日期:任务最小开始日期-30天 - let startDate = new Date(minDate) - startDate.setDate(startDate.getDate() - 30) + // 开始日期:取任务最小日期所在月的前一个月月初(第1天) + // 采用月运算而非减30天,原因: + // 减30天后若落在月末(如1月31日),normalize 到月初会多出一整个月(1月→1月1日), + // 而语义上只需在任务月份前保留一个完整月作为 buffer。 + // 直接用 new Date(year, month-1, 1) 使 startDate 始终为月初且精确为前一个月, + // generateDayTimelineData 以月为单位迭代时 setMonth(+1) 不会因日期溢出跳过任何月份。 + let startDate = new Date(minDate.getFullYear(), minDate.getMonth() - 1, 1) // 结束日期:任务最大结束日期+30天 let endDate = new Date(maxDate) @@ -1418,6 +1428,14 @@ let scrollTimeout: number | null = null const timelineScrollLeft = ref(0) const timelineContainerWidth = ref(0) +// 跳过 debouncedUpdateTimelineRange 的一次性标志 +// 用于 drag-end / toggle-end 配置 trigger 的宽度更新,只需要更新 sticky/bubble,无需重算日期范围 +let skipTimelineRangeUpdate = false + +// 允许 updateTimelineRange 在资源视图下执行一次的一次性标志 +// 只在任务视图 → 资源视图切换时设置,计算完成后自动清除 +let forceTimelineRangeInResourceView = false + // 半圆气泡控制状态 const hideBubbles = ref(true) // 初始时隐藏半圆,等待初始滚动完成 const isInitialScrolling = ref(true) // 跟踪初始滚动状态 @@ -2352,10 +2370,13 @@ const getOtherMilestonesInfo = (currentId: number) => { watch(isSplitBarDragging, (dragging) => { if (!dragging) { // v1.9.9 拖拽结束后,手动触发一次容器尺寸更新(因为拖拽期间ResizeObserver被暂停) + // 设置 skip 标志:拖拽不改变日期范围,只更新 sticky/bubble if (timelineContainerElement.value) { const newWidth = timelineContainerElement.value.clientWidth if (Math.abs(newWidth - timelineContainerWidth.value) > 1) { + skipTimelineRangeUpdate = true timelineContainerWidth.value = newWidth + nextTick(() => { skipTimelineRangeUpdate = false }) } } @@ -2388,6 +2409,23 @@ const handleSplitterDragEnd = () => { // 清理工作已移至watch(isSplitBarDragging) } +// 监听 TaskList toggle 结束,手动更新 Timeline 内部容器宽度 +// 跳过日期范围重算,只让 sticky/bubble 重算和 TaskBar 重渲染生效 +watch(isTaskListToggling, (toggling) => { + if (!toggling) { + // toggle 结束:读取 .timeline 滚动容器的真实新宽度 + // 使用 skip 标志,避免 watch(timelineContainerWidth) 触发 debouncedUpdateTimelineRange + if (timelineContainerElement.value) { + const newWidth = timelineContainerElement.value.clientWidth + if (Math.abs(newWidth - timelineContainerWidth.value) > 1) { + skipTimelineRangeUpdate = true + timelineContainerWidth.value = newWidth + nextTick(() => { skipTimelineRangeUpdate = false }) + } + } + } +}) + // 处理Timeline容器resize事件(如TaskList切换等) const handleTimelineContainerResized = () => { // Timeline容器大小发生变化,需要强制重新计算半圆显示状态 @@ -3763,6 +3801,11 @@ onMounted(() => { if (isSplitBarDragging.value) { return } + // TaskList toggle 期间不处理宽度变化,外层 .gantt-body 已屏蔽日期范围重算 + // toggle 结束后由 watch(isTaskListToggling) 手动更新宽度 + if (isTaskListToggling.value) { + return + } for (const entry of entries) { const newWidth = entry.contentRect.width @@ -4587,10 +4630,11 @@ const debouncedUpdateTimelineRange = (delay = 50) => { const updateTimelineRange = () => { // 资源视图下不自动调整时间范围,保持Timeline背景层(header + 竖列)稳定 // 避免滚动时重新生成timelineData导致header闪烁 - // 虚拟滚动通过visibleTimeRange过滤TaskBar即可 - if (viewMode.value === 'resource') { + // 例外:任务视图 → 资源视图切换时,需要一次性重算以实现基于资源任务的日期范围 + if (viewMode.value === 'resource' && !forceTimelineRangeInResourceView) { return } + forceTimelineRangeInResourceView = false let newRange: { startDate: Date; endDate: Date } | null = null @@ -4625,6 +4669,9 @@ watch( // 拖拽 splitter 时跳过重新计算 if (isSplitterDragging.value) return + // drag-end / toggle-end 触发的宽度更新:只需要 sticky/bubble/TaskBar 重渲染,跳过日期范围重算 + if (skipTimelineRangeUpdate) return + // 只在容器宽度从 0 变为有效值,或容器宽度发生显著变化时重新计算 if (!oldWidth || oldWidth === 0 || Math.abs(newWidth - oldWidth) > 50) { if (newWidth > 0) { @@ -4672,12 +4719,13 @@ watch( debouncedUpdateTimelineRange() } else if (newViewMode === 'resource') { if (viewModeChanged) { - // bugfix: 切换到资源视图时直接调用 scrollToTodayCenter - // 原因:updateTimelineRange() 在 resource 视图下直接返回(不更新 timelineData/timelineConfig), - // 导致内层 watch 无法触发,timeline 保留任务视图中拖拽后的偏移位置 - // 注意:切换视图不重新挂载 DOM,timelineContainerElement 始终可用, - // nextTick 后直接调用即可,scrollToTodayCenter 内部有重试机制无需额外 setTimeout - nextTick(() => scrollToTodayCenter()) + // 切换到资源视图:基于资源任务重算日期范围,重算完成后由 watch([timelineData,...]) 触发滚到今日 + // 重置 hasInitialAutoScroll,确保 watch([timelineData, timelineConfig...]) 在重算完成后能滚到今日 + // 不在此处直接调用 scrollToTodayCenter(),因为 debouncedUpdateTimelineRange 有 50ms 延迟, + // nextTick 会在 50ms 之前就执行,导致用旧的 task 视图坐标滚到错误位置 + hasInitialAutoScroll = false + forceTimelineRangeInResourceView = true + debouncedUpdateTimelineRange() } } }, diff --git a/src/components/Timeline/GanttConflicts.vue b/src/components/Timeline/GanttConflicts.vue index 9ae224f..a40259d 100644 --- a/src/components/Timeline/GanttConflicts.vue +++ b/src/components/Timeline/GanttConflicts.vue @@ -91,7 +91,21 @@ const canvasStyle = computed(() => ({ // 监听任务列表变化 watch(() => props.tasks, () => { if (isDraggingTaskBar.value) { + // 拖拽进行中:标记需要重算,等拖拽结束后再执行 needsRecalculation.value = true + } else { + // 拖拽结束后的已提交数据变更:isDraggingTaskBar 已先于数据更新变为 false, + // 此处直接触发增量/全量重算,避免冲突层不刷新。 + nextTick(() => { + if (lastChangedTaskId.value !== null) { + recalculateConflictsIncremental(lastChangedTaskId.value) + lastChangedTaskId.value = null + } else { + texturePatterns.value = { light: null, medium: null, severe: null } + coordsCache.clear() + recalculateConflicts() + } + }) } }, { deep: true })