Compare commits

..
10 Commits
24 changed files with 1428 additions and 452 deletions
+30
View File
@@ -5,6 +5,36 @@ 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.10.0] - 2026-03-07
### Fixed
- 🔧 优化:优化算法,提升插件性能,更好兼容移动设备的访问
- 🔧 优化:来自Github一位用户的共同探讨,带来此次性能的飞跃,同时他也提供了另外一种应用层的优化方案,请参考: [discussion #16](https://github.com/nelson820125/jordium-gantt-vue3/discussions/16)
- 🔧 Optimized: Algorithm optimizations for improved plugin performance and better compatibility with mobile device access
- 🔧 Optimized: Collaborative discussions with a Github user led to this performance leap, and they also provided another application-level optimization方案, please refer to: [discussion #16](https://github.com/nelson820125/jordium-gantt-vue3/discussions/16)
## [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
- 🎉 新增:GanttChart 新增 enableTaskDrawerAutoClose 属性,设置为 false 时禁用 TaskDrawer 的自动关闭行为
- 🎉 Added: GanttChart new prop enableTaskDrawerAutoClose — set to false to disable the automatic closing behavior of the TaskDrawer
### Fixed
- 🔧 修复:周视图下拖拽 TaskBar 左右移动后任务宽度(持续时间)自动缩短的问题
- 🔧 Fixed: TaskBar width (duration) shrinking after dragging left and right in weekly view
## [1.9.2] - 2026-02-26
### Added
+9 -2
View File
@@ -162,7 +162,7 @@ const milestones = ref([
{
id: 101,
name: 'Project Approval',
date: '2025-01-01',
startDate: '2025-01-01',
type: 'milestone',
},
])
@@ -226,6 +226,7 @@ npm run dev
| `showTaskbarTab` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to display resource tab on TaskBar (shows resource allocation label on TaskBar in resource view) |
| `enableTaskListCollapsible` ![v1.9.2](https://img.shields.io/badge/v1.9.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to allow collapsing/expanding the TaskList panel. When `false`: forcibly hides TaskList, SplitterBar and collapse button; Timeline takes full width |
| `taskListVisible` ![v1.9.2](https://img.shields.io/badge/v1.9.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Controls TaskList visibility (reactive). Only effective when `enableTaskListCollapsible=true` |
| `enableTaskDrawerAutoClose` ![v1.9.3](https://img.shields.io/badge/v1.9.3-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | Whether to allow TaskDrawer to auto-close (closes on outside click or Esc key). Set to `false` to disable auto-close — the drawer can only be closed via its internal close button |
#### TaskListColumn Component Props
@@ -3637,7 +3638,13 @@ View the complete [Contributors list](./CONTRIBUTORS.md)。
---
## 📄 Open Source License
## Troubleshooting
For common issues and solutions, please refer to [Troubleshooting-EN.md](./Troubleshooting-EN.md).
---
## 📄 Open Source License
[MIT License](./LICENSE) © 2025 JORDIUM.COM
+8 -1
View File
@@ -224,6 +224,7 @@ npm run dev
| `showTaskbarTab` ![v1.9.0](https://img.shields.io/badge/v1.9.0-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar的资源分配标签) |
| `enableTaskListCollapsible` ![v1.9.2](https://img.shields.io/badge/v1.9.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否允许折叠/展开 TaskList 面板。`false` 时强制隐藏 TaskList、SplitterBar 及折叠按钮,Timeline 独占全宽 |
| `taskListVisible` ![v1.9.2](https://img.shields.io/badge/v1.9.2-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 控制 TaskList 的显隐状态(响应式)。仅在 `enableTaskListCollapsible=true` 时有效 |
| `enableTaskDrawerAutoClose` ![v1.9.3](https://img.shields.io/badge/v1.9.3-409EFF?style=flat-square&labelColor=ECF5FF) | `boolean` | `true` | 是否允许 TaskDrawer 自动关闭(外总点击或按 Esc 时自动关闭)。设为 `false` 时禁用自动关闭,仅可通过内部按钮手动关闭 |
#### TaskListColumn 属性
@@ -3630,7 +3631,13 @@ TaskList 组件经过深度重构,采用模块化设计,提升了代码可
---
## 📄 开源协议
## Troubleshooting
常见问题与解决方案请参阅 [Troubleshooting.md](./Troubleshooting.md)。
---
## 📄 开源协议
[MIT License](./LICENSE) © 2025 JORDIUM.COM
+69
View File
@@ -0,0 +1,69 @@
# Troubleshooting
Common issues and solutions for `jordium-gantt-vue3`.
---
## Table of Contents
- [High Memory Usage / Browser Freeze with Large Datasets](#issue-13-high-memory-usage--browser-freeze-with-large-datasets)
---
## Issue #13: High Memory Usage / Browser Freeze with Large Datasets
**Labels**: `performance`, `answer`
**Status**: ✅ Resolved
**Issue**: [#13 Memory leak and high CPU use](https://github.com/nelson820125/jordium-gantt-vue3/issues/13) → converted to Discussion [#16](https://github.com/nelson820125/jordium-gantt-vue3/discussions/16)
### Symptoms
When loading ~80+ tasks, the page becomes severely sluggish, memory grows continuously, and the browser may crash.
### Root Cause
The problem is **not in the component itself**, but in how the caller binds data:
1. **Using `reactive` to deeply wrap the task array** — Vue creates deep watchers for every object in the array. With a large dataset, the number of watchers explodes and memory can exceed 10 GB.
2. **Incorrect date format** — The API returns ISO datetime strings (e.g. `"2026-03-07T08:34:00.230"`), but the component expects plain date strings (`"2026-03-07"`).
### Solution (credit: [@eldrift](https://github.com/eldrift))
#### 1. Use `shallowRef` instead of `reactive` to store the task list
```ts
// ❌ Wrong — deep reactivity, performance disaster
const data = reactive({ tasks: [] as Task[] })
// ✅ Correct — Vue only watches the array reference, not each object deeply
const tasks = shallowRef<Task[]>([])
```
#### 2. Truncate ISO datetime strings to plain date format
```ts
tasks.value = res.data.map((t: Task) => ({
...t,
startDate: t.startDate?.substring(0, 10) ?? t.startDate,
endDate: t.endDate?.substring(0, 10) ?? t.endDate,
}))
```
#### 3. Use the correct Task type
Ensure the data passed to the component conforms to the `Task` interface to avoid hidden computation overhead from type mismatches.
### References
- Full discussion & report: [Discussion #16](https://github.com/nelson820125/jordium-gantt-vue3/discussions/16)
- Original issue: [#13 Memory leak and high CPU use](https://github.com/nelson820125/jordium-gantt-vue3/issues/13)
---
## Feedback
If you encounter an issue not listed here, feel free to submit one:
- [GitHub Issues](https://github.com/nelson820125/jordium-gantt-vue3/issues)
- [Gitee Issues](https://gitee.com/jordium/jordium-gantt-vue3/issues)
- 📧 ning.li@jordium.com
+69
View File
@@ -0,0 +1,69 @@
# Troubleshooting
常见问题与解决方案 / Common issues and solutions.
---
## 目录 / Table of Contents
- [内存占用过高 / 浏览器卡死(大数据量场景)](#issue-13-内存占用过高--浏览器卡死大数据量场景)
---
## Issue #13: 内存占用过高 / 浏览器卡死(大数据量场景)
**标签 / Labels**: `performance`, `answer`
**状态 / Status**: ✅ 已解决 / Resolved
**Issue 地址**: [#13 Memory leak and high CPU use](https://github.com/nelson820125/jordium-gantt-vue3/issues/13) → 已转为 Discussion [#16](https://github.com/nelson820125/jordium-gantt-vue3/discussions/16)
### 现象
在加载约 80+ 条任务数据时,页面出现严重卡顿、内存持续增长甚至浏览器崩溃。
### 根本原因
问题**不在组件本身**,而在于调用方的数据绑定方式:
1. **使用 `reactive` 深层包裹任务数组** — Vue 会为数组内每一个对象建立深层侦听器,数据量较大时侦听器数量爆炸,导致内存超过 10 GB。
2. **日期格式错误** — API 返回 ISO 日期时间字符串(如 `"2026-03-07T08:34:00.230"`),但组件要求纯日期格式(`"2026-03-07"`)。
### 解决方案(来自 [@eldrift](https://github.com/eldrift)
#### 1. 使用 `shallowRef` 替代 `reactive` 存储任务列表
```ts
// ❌ 错误 — 深层响应性,性能灾难
const data = reactive({ tasks: [] as Task[] })
// ✅ 正确 — Vue 只监听数组引用,不深入每个对象
const tasks = shallowRef<Task[]>([])
```
#### 2. 截断 ISO 日期时间为纯日期格式
```ts
tasks.value = res.data.map((t: Task) => ({
...t,
startDate: t.startDate?.substring(0, 10) ?? t.startDate,
endDate: t.endDate?.substring(0, 10) ?? t.endDate,
}))
```
#### 3. 使用正确的 Task 类型
确保传入组件的数据符合 `Task` 接口定义,避免类型不匹配导致的隐性计算开销。
### 参考
- 完整讨论与报告:[Discussion #16](https://github.com/nelson820125/jordium-gantt-vue3/discussions/16)
- 原始 Issue[#13 Memory leak and high CPU use](https://github.com/nelson820125/jordium-gantt-vue3/issues/13)
---
## 反馈 / Feedback
如果遇到未收录的问题,欢迎提交 Issue:
- [GitHub Issues](https://github.com/nelson820125/jordium-gantt-vue3/issues)
- [Gitee Issues](https://gitee.com/jordium/jordium-gantt-vue3/issues)
- 📧 ning.li@jordium.com
+2 -1
View File
@@ -1312,7 +1312,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
</svg>
Jordium Gantt Vue3 Demo
<span class="version-badge" style="cursor: pointer" @click="showVersionDrawer = true">{{
packageInfo.version
packageInfo.version + ' ' + demoMessages.releaseNotes
}}</span>
</div>
<!-- <div class="title-center">
@@ -2377,6 +2377,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
:complete-task-background-color="completeTaskBackgroundColor"
:ongoing-task-background-color="ongoingTaskBackgroundColor"
:use-default-drawer="useDefaultDrawer"
:enable-task-drawer-auto-close="false"
@milestone-saved="handleMilestoneSaved"
@milestone-deleted="handleMilestoneDeleted"
@milestone-icon-changed="handleMilestoneIconChanged"
+37
View File
@@ -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"
}
]
}
+2 -1
View File
@@ -148,5 +148,6 @@
"label": "Enable TaskRow Drag and Drop",
"hint": "Allow dragging TaskRow to adjust task hierarchy and order"
}
}
},
"releaseNotes": "Click to view release notes"
}
+2 -1
View File
@@ -148,5 +148,6 @@
"label": "启用TaskRow拖拽移动",
"hint": "允许通过拖拽TaskRow来调整任务的层级和顺序"
}
}
},
"releaseNotes": "点击查看更新日志"
}
+1 -1
View File
@@ -2,7 +2,7 @@ User-agent: *
Allow: /
# Sitemap location
Sitemap: https://jordium.gitee.io/jordium-gantt-vue3/sitemap.xml
Sitemap: https://nelson820125.github.io/jordium-gantt-vue3/sitemap.xml
# Crawl-delay for polite crawling
Crawl-delay: 1
+4 -4
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://jordium.gitee.io/jordium-gantt-vue3/</loc>
<lastmod>2025-12-14</lastmod>
<changefreq>weekly</changefreq>
<priority>1.6.1</priority>
<loc>https://nelson820125.github.io/jordium-gantt-vue3/</loc>
<lastmod>2026-02-26</lastmod>
<changefreq>monthly</changefreq>
<priority>1.9.2</priority>
</url>
</urlset>
+42
View File
@@ -576,5 +576,47 @@
"🔧 Fixed: GanttToolbar showViewMode:false had no effect due to missing v-if guard",
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to Andrés Jorge from Berlin, Germany for their use and feedback</span>"
]
},
{
"version": "1.9.3",
"date": "2026-03-05",
"notes": [
"🎉 新增:GanttChart 新增 enableTaskDrawerAutoClose 属性,设置为 false 时禁用 TaskDrawer 的自动关闭行为",
"🔧 修复:周视图下拖拽 TaskBar 左右移动后任务宽度(持续时间)自动缩短的问题",
"<span style=\"font-weight: bold; color: #f00;\">特别感谢来自xuhang513@gitee的使用与反馈</span>",
"🎉 Added: GanttChart new prop enableTaskDrawerAutoClose — set to false to disable the automatic closing behavior of the TaskDrawer",
"🔧 Fixed: TaskBar width (duration) shrinking after dragging left and right in weekly view",
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to xuhang513@gitee for their use and feedback</span>"
]
},
{
"version": "1.9.4",
"date": "2026-03-06",
"notes": [
"🔧 修复:日期视图下,SplitterBar拖拽以及TaskList收起/展开后,计算导致2月份周期丢失问题",
"🔧 修复:任务视图切换至资源视图后今日定位偏移问题",
"🔧 修复:任务视图切换至资源视图后最大和最小日期范围计算问题",
"🔧 修复:资源视图下,小时/日/周视图切换后,最大值和最小值范围问题",
"🔧 修复:包弱点处理,jspdf增加了依赖包dompurify,用于HTML的XSS消毒",
"<span style=\"font-weight: bold; color: #f00;\">特别感谢来自jwuxin@gitee的使用与反馈</span>",
"🔧 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",
"🔧 Fixed: Vulnerabilities fixed, jspdf added dependency dompurify for HTML XSS sanitization",
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to jwuxin@gitee for their use and feedback</span>"
]
},
{
"version": "1.10.0",
"date": "2026-03-07",
"notes": [
"🔧 优化:优化算法,提升插件性能,更好兼容移动设备的访问",
"🔧 优化:来自Github一位用户的共同探讨,是带来此次性能飞跃的关键,同时他也提供了另外一种应用层的优化方案,请参考: <a href=\"https://github.com/nelson820125/jordium-gantt-vue3/discussions/16\">discussion #16</a>",
"<span style=\"font-weight: bold; color: #f00;\">特别感谢来自eldrift@github的使用与反馈</span>",
"🔧 Optimized: Algorithm optimizations for improved plugin performance and better compatibility with mobile device access",
"🔧 Optimized: Collaborative discussions with a Github user led to this performance leap, and they also provided another application-level optimization方案, please refer to: <a href=\"https://github.com/nelson820125/jordium-gantt-vue3/discussions/16\">discussion #16</a>",
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to eldritf@github for their use and feedback</span>"
]
}
]
+699 -300
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "jordium-gantt-vue3",
"version": "1.9.2",
"version": "1.10.0",
"type": "module",
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
@@ -75,13 +75,14 @@
},
"dependencies": {
"date-fns": "^4.1.0",
"dompurify": "^3.3.2",
"html2canvas": "^1.4.1",
"jspdf": "^4.0.0",
"vue": "^3.5.13"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"@vitejs/plugin-vue": "^5.2.3",
"@vitest/coverage-v8": "^4.0.16",
"@vitest/ui": "^4.0.16",
+62 -28
View File
@@ -77,6 +77,7 @@ const props = withDefaults(defineProps<Props>(), {
theme: undefined, //
enableTaskListCollapsible: true,
taskListVisible: true,
enableTaskDrawerAutoClose: true,
})
const emit = defineEmits([
@@ -559,6 +560,12 @@ interface Props {
* 可通过 prop 传入或通过 defineExpose 暴露的方法控制
*/
taskListVisible?: boolean
/**
* 是否允许点击遮罩层关闭 TaskDrawer默认为 true保持原有行为
* 设置为 false 可防止 TaskDrawer 在失去焦点点击遮罩区域时自动关闭
* 仅在 useDefaultDrawer=true 时有效
*/
enableTaskDrawerAutoClose?: boolean
}
// TaskList +
@@ -806,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
@@ -984,6 +995,9 @@ const toggleTaskList = () => {
//
if (isAnimating.value) return
// toggle Timeline containerResizeObserver
isTaskListToggling.value = true
//
isAnimating.value = true
@@ -1005,7 +1019,9 @@ const toggleTaskList = () => {
isAnimating.value = false
animationClass.value = ''
// TaskListTimeline
// toggle Timeline watch sticky/bubble
// Timeline TaskBar
isTaskListToggling.value = false
nextTick(() => {
window.dispatchEvent(
new CustomEvent('timeline-container-resized', {
@@ -1018,11 +1034,14 @@ const toggleTaskList = () => {
// TimelineTaskList
const handleToggleTaskList = (event: CustomEvent) => {
// toggle Timeline containerResizeObserver
isTaskListToggling.value = true
isTaskListVisible.value = event.detail
// TaskListTimelineTimeline
// Timeline
// toggle Timeline watch sticky/bubble
nextTick(() => {
isTaskListToggling.value = false
window.dispatchEvent(
new CustomEvent('timeline-container-resized', {
detail: { source: 'task-list-toggle' },
@@ -1062,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 => {
@@ -1074,7 +1098,7 @@ onMounted(() => {
timelineContainerWidth.value = entry.contentRect.width
}
})
resizeObserver.observe(rightPanel)
resizeObserver.observe(ganttBody)
}
})
})
@@ -1601,31 +1625,30 @@ const tasksForTimeline = computed(() => {
}
//
// { ...task }
// smartFlattenTasks spread level
const updateParentDateRanges = (tasks: Task[]): Task[] => {
return tasks.map(task => {
let updatedTask = { ...task }
//
if (task.children && task.children.length > 0) {
updatedTask.children = updateParentDateRanges(task.children)
//
if (!task.children || task.children.length === 0) {
return task
}
//
const isParent =
task.type === 'story' || (updatedTask.children && updatedTask.children.length > 0)
updatedTask.isParent = isParent
//
const updatedChildren = updateParentDateRanges(task.children)
const isParent = task.type === 'story' || true
//
if (isParent && updatedTask.children && updatedTask.children.length > 0) {
const { startDate, endDate } = calculateParentDateRange(updatedTask)
updatedTask = {
...updatedTask,
startDate,
endDate,
}
//
const taskWithChildren = { ...task, children: updatedChildren }
const { startDate, endDate } = calculateParentDateRange(taskWithChildren)
return {
...task,
children: updatedChildren,
isParent,
startDate,
endDate,
}
return updatedTask
})
}
@@ -1714,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
@@ -3530,6 +3563,7 @@ defineExpose({
:delay-task-background-color="props.delayTaskBackgroundColor"
:complete-task-background-color="props.completeTaskBackgroundColor"
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
:enable-close-on-overlay-click="props.enableTaskDrawerAutoClose"
@submit="handleTaskDrawerSubmit"
@close="taskDrawerVisible = false"
@start-timer="handleStartTimer"
+23
View File
@@ -33,6 +33,8 @@ interface Props {
verticalLines?: VerticalLine[]
showVerticalLines?: boolean
// injectprops
// canvas
isScrolling?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@@ -40,6 +42,7 @@ const props = withDefaults(defineProps<Props>(), {
showVerticalLines: true,
offsetLeft: 0,
offsetTop: 0,
isScrolling: false,
})
// inject
@@ -54,6 +57,8 @@ const canvasManager = new CanvasContextManager()
// requestAnimationFrame
let rafId: number | null = null
let pendingRedraw = false
//
let pendingRedrawOnScrollStop = false
// 线
const isDarkTheme = computed(() => injectedTheme.value === 'dark')
@@ -406,8 +411,15 @@ const drawArrowOptimized = (
/**
* 使用 requestAnimationFrame 优化的重绘调度器
* 合并多个连续的重绘请求为单次绘制
* 垂直滚动期间跳过重绘等滚动停止后补绘一次避免每帧重绘 canvas
*/
const scheduleRedraw = () => {
//
if (props.isScrolling) {
pendingRedrawOnScrollStop = true
return
}
if (pendingRedraw) {
//
return
@@ -428,6 +440,17 @@ const scheduleRedraw = () => {
})
}
//
watch(
() => props.isScrolling,
(scrolling) => {
if (!scrolling && pendingRedrawOnScrollStop) {
pendingRedrawOnScrollStop = false
scheduleRedraw()
}
},
)
// Canvas
watch(
[
+9 -2
View File
@@ -1569,8 +1569,14 @@ const handleMouseMove = (e: MouseEvent) => {
} else {
// 使
const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
const duration = dragStartWidth.value / props.dayWidth
const newEndDate = addDaysToLocalDate(newStartDate, duration - 1)
// dragStartWidth / dayWidth
// WEEK dayWidth=60/7 parseInt
const originalStartDate = createLocalDate(props.task.startDate) || props.startDate
const originalEndDate = createLocalDate(props.task.endDate) || props.startDate
const durationMs = originalEndDate.getTime() - originalStartDate.getTime()
const duration = Math.round(durationMs / (1000 * 60 * 60 * 24))
const newEndDate = addDaysToLocalDate(newStartDate, duration)
//
tempTaskData.value = {
@@ -3688,6 +3694,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
position: absolute;
border-radius: 4px;
user-select: none;
touch-action: none; /* JS 完全接管拖拽手势,不影响父层水平滚动 */
/* 阴影通过JS动态设置,使用TaskBar的颜色 */
transition:
box-shadow 0.2s,
+6 -1
View File
@@ -27,6 +27,8 @@ interface Props {
delayTaskBackgroundColor?: string
completeTaskBackgroundColor?: string
ongoingTaskBackgroundColor?: string
/** 是否允许点击遮罩层关闭抽屉,默认为 true(保持原有行为)。设置为 false 可防止失去焦点时自动关闭 */
enableCloseOnOverlayClick?: boolean
}
const props = withDefaults(defineProps<Props>(), {
@@ -45,6 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
delayTaskBackgroundColor: '#f56c6c',
completeTaskBackgroundColor: '#67c23a',
ongoingTaskBackgroundColor: '#409eff',
enableCloseOnOverlayClick: true,
})
const emit = defineEmits<{
@@ -484,7 +487,9 @@ const handleClose = () => {
//
const handleOverlayClick = () => {
handleClose()
if (props.enableCloseOnOverlayClick) {
handleClose()
}
}
//
@@ -142,6 +142,9 @@ export function useTaskListEventHandlers(options: TaskListEventHandlersOptions)
// ==================== 滚动同步事件 ====================
// 滚动同步标志位:防止 Timeline → TaskList 写入后反向再次派发(2跳链路优化)
let _isSyncingScrollFromTimeline = false
const handleTaskListScroll = (event: Event) => {
const target = event.target as HTMLElement
if (!target) return
@@ -149,6 +152,9 @@ export function useTaskListEventHandlers(options: TaskListEventHandlersOptions)
const scrollTop = target.scrollTop
taskListScrollTop.value = scrollTop
// 由 Timeline → TaskList 同步写入触发时,不再反向派发,避免 2 跳循环
if (_isSyncingScrollFromTimeline) return
// 滚动时关闭所有右键菜单
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
@@ -166,7 +172,11 @@ export function useTaskListEventHandlers(options: TaskListEventHandlersOptions)
taskListScrollTop.value = scrollTop
if (taskListBodyElement && Math.abs(taskListBodyElement.scrollTop - scrollTop) > 1) {
// 设置标志位:当前是由 Timeline 触发的同步写入
// 防止 handleTaskListScroll 被触发后反向再次派发事件(2跳链路)
_isSyncingScrollFromTimeline = true
taskListBodyElement.scrollTop = scrollTop
Promise.resolve().then(() => { _isSyncingScrollFromTimeline = false })
}
}
@@ -65,41 +65,31 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
const flattenedTasks = computed(() => getFlattenedVisibleTasks(tasks.value))
/**
* v1.9.0 /
* +
* O(n)
*/
const cumulativeHeights = computed<number[]>(() => {
if (viewMode.value === 'resource') {
// 资源视图:使用每个资源的实际高度
const resources = dataSource.value as Resource[]
const heights: number[] = [0] // 第一个位置是0
let cumulative = 0
if (viewMode.value !== 'resource') return []
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 resources = dataSource.value as Resource[]
const heights: number[] = [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
})
/**
* /
*
*/
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) {
@@ -108,38 +98,37 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
right = mid
}
}
return Math.max(0, left - 1)
}
/**
*
*
* - O(1) cumulativeHeights
* - 使 +
*/
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 }
}
if (viewMode.value === 'resource') {
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 total = (dataSource.value as Resource[]).length
let startIndex = Math.max(0, findIndexByScrollTop(scrollTop) - VERTICAL_BUFFER)
const scrollBottom = scrollTop + containerHeight
let endIndex = Math.min(heights.length - 1, findIndexByScrollTop(scrollBottom) + VERTICAL_BUFFER + 1)
return {
startIndex: Math.min(startIndex, total),
endIndex: Math.min(endIndex, total),
}
} else {
// 任务视图:固定行高 ROW_HEIGHTO(1) 直接计算,不访问 cumulativeHeights
const total = flattenedTasks.value.length
if (total === 0) return { startIndex: 0, endIndex: 0 }
const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - VERTICAL_BUFFER)
const endIndex = Math.min(total, Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + VERTICAL_BUFFER)
return { startIndex, endIndex }
}
})
@@ -149,7 +138,6 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
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,
@@ -157,22 +145,31 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
})
/**
* Spacer
* Spacer
* O(1)
*/
const totalContentHeight = computed(() => {
const heights = cumulativeHeights.value
return heights.length > 0 ? heights[heights.length - 1] : 0
if (viewMode.value === 'resource') {
const heights = cumulativeHeights.value
return heights.length > 0 ? heights[heights.length - 1] : 0
}
return flattenedTasks.value.length * ROW_HEIGHT
})
const startSpacerHeight = computed(() => {
const startIdx = visibleTaskRange.value.startIndex
return cumulativeHeights.value[startIdx] || 0
if (viewMode.value === 'resource') {
return cumulativeHeights.value[visibleTaskRange.value.startIndex] || 0
}
return visibleTaskRange.value.startIndex * ROW_HEIGHT
})
const endSpacerHeight = computed(() => {
const endIdx = visibleTaskRange.value.endIndex
const endHeight = cumulativeHeights.value[endIdx] || 0
return Math.max(0, totalContentHeight.value - endHeight)
if (viewMode.value === 'resource') {
const endIdx = visibleTaskRange.value.endIndex
const endHeight = cumulativeHeights.value[endIdx] || 0
return Math.max(0, totalContentHeight.value - endHeight)
}
return Math.max(0, (flattenedTasks.value.length - visibleTaskRange.value.endIndex) * ROW_HEIGHT)
})
return {
@@ -1,5 +1,6 @@
import { onMounted, onUnmounted, type Ref } from 'vue'
import type { Task } from '../../../../models/classes/Task'
import { registerDragOver, unregisterDragOver } from '../../../../utils/dragOverRegistry'
/**
* TaskRow
@@ -98,27 +99,25 @@ export function useTaskRowEventHandlers(
}
}
// 处理全局拖拽悬停事件
const handleTaskRowDragOver = (event: CustomEvent) => {
// 处理全局拖拽悬停事件(通过 dragOverRegistry 委托,不再直接监听 window)
// 优化:从每行独立注册 window 监听器改为全局单一监听器 + Map 直接查找 O(1)
const handleTaskRowDragOver = (mouseEvent: MouseEvent) => {
if (!enableDrag.value || !taskRowRef.value || !dragOver) return
const { taskId, event: mouseEvent } = event.detail
if (taskId === task.value.id) {
dragOver(task.value, taskRowRef.value, mouseEvent)
}
dragOver(task.value, taskRowRef.value, mouseEvent)
}
// 生命周期钩子 - 注册事件监听器
onMounted(() => {
window.addEventListener('splitter-drag-start', handleSplitterDragStart)
window.addEventListener('splitter-drag-end', handleSplitterDragEnd)
window.addEventListener('task-row-drag-over', handleTaskRowDragOver as EventListener)
// 使用 dragOverRegistry 委托注册,避免每个 TaskRow 各自占用一个 window 监听器
registerDragOver(task.value.id, handleTaskRowDragOver)
})
onUnmounted(() => {
window.removeEventListener('splitter-drag-start', handleSplitterDragStart)
window.removeEventListener('splitter-drag-end', handleSplitterDragEnd)
window.removeEventListener('task-row-drag-over', handleTaskRowDragOver as EventListener)
unregisterDragOver(task.value.id)
})
return {
+224 -43
View File
@@ -168,6 +168,9 @@ provide('positionCache', positionCache)
// v1.9.5 P2-4 - Split Bar
const isSplitBarDragging = inject<Ref<boolean>>('isSplitBarDragging', ref(false))
// TaskList toggle toggle containerResizeObserver
const isTaskListToggling = inject<Ref<boolean>>('isTaskListToggling', ref(false))
// v1.9.5 showConflicts
const showConflicts = inject<ComputedRef<boolean>>('gantt-show-conflicts', computed(() => true))
@@ -175,6 +178,11 @@ const showConflicts = inject<ComputedRef<boolean>>('gantt-show-conflicts', compu
const ROW_HEIGHT = 51 // 51px (50px + 1px border)
const VERTICAL_BUFFER = 5 //
const timelineBodyScrollTop = ref(0) //
// TaskList Timeline 2
let _isSyncingScrollFromTaskList = false
// GanttLinks canvas
const isTimelineScrolling = ref(false)
let _scrollIdleTimer: ReturnType<typeof setTimeout> | null = null
const timelineBodyHeight = ref(0) //
// v1.9.9 GanttChart
@@ -292,7 +300,10 @@ const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
// propstimelineConfig
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 +507,13 @@ const getDayTimelineRange = () => {
const { minDate, maxDate } = taskRange
// -30
let startDate = new Date(minDate)
startDate.setDate(startDate.getDate() - 30)
// 1
// 30
// 30131normalize 111
// 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 +1433,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) //
@@ -1727,6 +1750,74 @@ const visibleResources = computed(() => {
}))
})
// TaskBar
// getBoundingClientRect reflow
const TASK_VIEW_BATCH_SIZE = 3 // 3
let _taskBatchRafId: number | null = null
let _taskBatchQueue: { task: Task; originalIndex: number }[] = []
// P2 ID number | string Set<number> ID
const _taskRenderedIds = new Set<number | string>()
const taskRenderedItems = shallowRef<{ task: Task; originalIndex: number }[]>([])
watch(
[visibleTasks, viewMode] as const,
([newVisible, mode]) => {
if (mode !== 'task') {
//
if (_taskBatchRafId !== null) { cancelAnimationFrame(_taskBatchRafId); _taskBatchRafId = null }
_taskBatchQueue = []
_taskRenderedIds.clear()
taskRenderedItems.value = []
return
}
// Set<number | string> _taskRenderedIds ID number
const newIds = new Set<number | string>(newVisible.map(t => t.task.id))
// P1 ID taskBarPositions
// tasks watch
const removedIds: (number | string)[] = []
_taskRenderedIds.forEach(id => {
if (!newIds.has(id)) {
_taskRenderedIds.delete(id)
removedIds.push(id)
}
})
if (removedIds.length > 0) {
const newPositions = { ...taskBarPositions.value }
for (const id of removedIds) {
delete newPositions[id as number]
}
taskBarPositions.value = newPositions
}
//
const toAdd = newVisible.filter(t => !_taskRenderedIds.has(t.task.id))
//
if (_taskBatchRafId !== null) { cancelAnimationFrame(_taskBatchRafId); _taskBatchRafId = null }
_taskBatchQueue = toAdd
//
const firstBatch = _taskBatchQueue.splice(0, TASK_VIEW_BATCH_SIZE)
firstBatch.forEach(t => _taskRenderedIds.add(t.task.id))
taskRenderedItems.value = newVisible.filter(t => _taskRenderedIds.has(t.task.id))
//
if (_taskBatchQueue.length > 0) {
const step = () => {
const batch = _taskBatchQueue.splice(0, TASK_VIEW_BATCH_SIZE)
if (batch.length === 0) { _taskBatchRafId = null; return }
batch.forEach(t => _taskRenderedIds.add(t.task.id))
taskRenderedItems.value = visibleTasks.value.filter(t => _taskRenderedIds.has(t.task.id))
_taskBatchRafId = _taskBatchQueue.length > 0 ? requestAnimationFrame(step) : null
}
_taskBatchRafId = requestAnimationFrame(step)
}
},
{ immediate: true },
)
// v1.9.6 Sprint2(P1+P4) - TaskBar205
const RESOURCE_BATCH_SIZE = 5 // 4: 205
const FRAME_TIME_BUDGET = 8 // 8msTaskBar
@@ -2352,10 +2443,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 +2482,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 })
}
}
}
})
// TimelineresizeTaskList
const handleTimelineContainerResized = () => {
// Timeline
@@ -3518,12 +3629,18 @@ const canvasHeight = ref(0)
const canvasOffsetLeft = ref(0) // Canvas
const canvasOffsetTop = ref(0)
// Canvas
//
// - 5000: (~30MB)
// - 10000: (~60MB) 10 2
const SAFE_CANVAS_WIDTH = 5000 //
const SAFE_CANVAS_HEIGHT = 5000
// Canvas OOM
// / updateSvgSize()
// - = clamp( × 3, 2000, 8000) 2 + 线
// - = clamp( × 3, 1200, 4000) 3 100MB15MB
function computeSafeCanvasDimensions(): { width: number; height: number } {
const vw = Math.max(timelineContainerWidth.value, 300) // 0
const vh = Math.max(timelineBodyHeight.value, 300)
return {
width: Math.min(Math.max(vw * 3, 2000), 8000),
height: Math.min(Math.max(vh * 3, 1200), 4000),
}
}
function updateSvgSize() {
if (bodyContentRef.value) {
@@ -3536,8 +3653,11 @@ function updateSvgSize() {
const scrollLeft = timelineScrollLeft.value
const scrollTop = timelineBodyScrollTop.value
// // Canvas
const { width: SAFE_CANVAS_WIDTH, height: SAFE_CANVAS_HEIGHT } = computeSafeCanvasDimensions()
//
// Canvas 使 offsetLeft
// Canvas 使 offsetLeft
canvasWidth.value = SAFE_CANVAS_WIDTH
// Canvas
@@ -3576,6 +3696,12 @@ function updateSvgSize() {
}
}
// 2 handleBarMounted TaskBar
// N TaskBar N taskBarPositions spread + N GanttLinks RAF
// N TaskBar 1 + 1 GanttLinks
const _pendingBarUpdates: Record<number, { left: number; top: number; width: number; height: number }> = {}
let _barUpdateScheduled = false
function handleBarMounted(payload: {
id: number
left: number
@@ -3584,23 +3710,52 @@ function handleBarMounted(payload: {
height: number
}) {
if (!bodyContentRef.value) return
const baseRect = bodyContentRef.value.getBoundingClientRect()
// bodyContent
// 使 shallowRef
taskBarPositions.value = {
...taskBarPositions.value,
[payload.id]: {
left: payload.left - baseRect.left,
top: payload.top - baseRect.top,
width: payload.width,
height: payload.height,
},
// 使 cachedBodyRect reflow
// bodyRectInvalidated true
if (!cachedBodyRect || bodyRectInvalidated) {
cachedBodyRect = bodyContentRef.value.getBoundingClientRect()
bodyRectCacheTime = Date.now()
bodyRectInvalidated = false
}
const baseRect = cachedBodyRect
// pending
_pendingBarUpdates[payload.id] = {
left: payload.left - baseRect.left,
top: payload.top - baseRect.top,
width: payload.width,
height: payload.height,
}
if (!_barUpdateScheduled) {
// flush_pendingBarUpdates
// GanttLinks isScrolling=false
if (isTimelineScrolling.value) return
_barUpdateScheduled = true
//
Promise.resolve().then(() => {
_barUpdateScheduled = false
// GanttLinks
taskBarPositions.value = { ...taskBarPositions.value, ..._pendingBarUpdates }
for (const key in _pendingBarUpdates) {
delete _pendingBarUpdates[key as unknown as number]
}
setTimeout(() => updateSvgSize(), 200)
})
}
setTimeout(() => {
updateSvgSize()
}, 200)
}
// flush bar
watch(isTimelineScrolling, (scrolling) => {
if (scrolling) return
if (Object.keys(_pendingBarUpdates).length === 0) return
_barUpdateScheduled = false
taskBarPositions.value = { ...taskBarPositions.value, ..._pendingBarUpdates }
for (const key in _pendingBarUpdates) {
delete _pendingBarUpdates[key as unknown as number]
}
setTimeout(() => updateSvgSize(), 200)
})
// TaskBar /
const handleTaskBarDragEnd = (updatedTask: Task) => {
// dataSource
@@ -3763,6 +3918,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
@@ -3816,8 +3976,12 @@ const handleTaskListVerticalScroll = (event: CustomEvent) => {
debouncedUpdateCanvasPosition()
if (timelineBodyElement.value && Math.abs(timelineBodyElement.value.scrollTop - scrollTop) > 1) {
// 使1px
// TaskList
// handleTimelineBodyScroll 2
_isSyncingScrollFromTaskList = true
timelineBodyElement.value.scrollTop = scrollTop
// scroll Promise.resolve
Promise.resolve().then(() => { _isSyncingScrollFromTaskList = false })
}
}
@@ -3834,11 +3998,22 @@ const handleTimelineBodyScroll = (event: Event) => {
// bodyRect 线
bodyRectInvalidated = true
// GanttLinks canvas
isTimelineScrolling.value = true
if (_scrollIdleTimer !== null) clearTimeout(_scrollIdleTimer)
_scrollIdleTimer = setTimeout(() => {
_scrollIdleTimer = null
isTimelineScrolling.value = false
}, 150)
debouncedUpdateCanvasPosition()
//
if (isDragging.value) return
// TaskList Timeline 2
if (_isSyncingScrollFromTaskList) return
// TaskList
if (scrollTop >= 0) {
window.dispatchEvent(
@@ -4587,10 +4762,11 @@ const debouncedUpdateTimelineRange = (delay = 50) => {
const updateTimelineRange = () => {
// Timelineheader +
// timelineDataheader
// visibleTimeRangeTaskBar
if (viewMode.value === 'resource') {
//
if (viewMode.value === 'resource' && !forceTimelineRangeInResourceView) {
return
}
forceTimelineRangeInResourceView = false
let newRange: { startDate: Date; endDate: Date } | null = null
@@ -4625,6 +4801,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 +4851,13 @@ watch(
debouncedUpdateTimelineRange()
} else if (newViewMode === 'resource') {
if (viewModeChanged) {
// bugfix: scrollToTodayCenter
// updateTimelineRange() resource timelineData/timelineConfig
// watch timeline
// DOMtimelineContainerElement
// nextTick scrollToTodayCenter setTimeout
nextTick(() => scrollToTodayCenter())
// watch([timelineData,...])
// hasInitialAutoScroll watch([timelineData, timelineConfig...])
// scrollToTodayCenter() debouncedUpdateTimelineRange 50ms
// nextTick 50ms task
hasInitialAutoScroll = false
forceTimelineRangeInResourceView = true
debouncedUpdateTimelineRange()
}
}
},
@@ -4688,19 +4868,19 @@ watch(
() => tasks.value,
newTasks => {
invalidateTaskDateRangeCache()
// 使 for Set map
const currentTaskIds = new Set<number>()
// P2 task.id number string string
// 使 parseInt(taskIdStr) "res-1" NaN ID
const currentTaskIdStrs = new Set<string>()
for (const task of newTasks) {
currentTaskIds.add(task.id)
currentTaskIdStrs.add(String(task.id))
}
// 使 for...in Object.keys().forEach()
// shallowRef
const newPositions: Record<number, any> = {}
for (const taskIdStr in taskBarPositions.value) {
const taskId = parseInt(taskIdStr)
if (currentTaskIds.has(taskId)) {
newPositions[taskId] = taskBarPositions.value[taskId]
// string Set parseInt ID NaN
if (currentTaskIdStrs.has(taskIdStr)) {
newPositions[taskIdStr as unknown as number] = taskBarPositions.value[taskIdStr as unknown as number]
}
}
taskBarPositions.value = newPositions
@@ -5222,6 +5402,7 @@ const handleAddSuccessor = (task: Task) => {
:hovered-task-id="hoveredTaskId"
:vertical-lines="monthFirstVerticalLines"
:show-vertical-lines="currentTimeScale === TimelineScale.WEEK"
:is-scrolling="isTimelineScrolling"
/>
<!-- 连接线拖拽引导线 - 🚀 优化使用命令式 API RAF 直接调用 draw() -->
@@ -5410,9 +5591,9 @@ const handleAddSuccessor = (task: Task) => {
<!-- 同时需要考虑左侧TaskList包含1px的bottom border -->
<div class="task-bar-container" :style="{ height: `${contentHeight}px` }">
<div class="task-rows" :style="{ height: `${contentHeight}px` }">
<!-- 任务视图使用虚拟滚动渲染可见任务 -->
<!-- 任务视图使用虚拟滚动分批渲染可见任务taskRenderedItems 每帧限流 3 行新增-->
<div
v-for="{ task, originalIndex } in visibleTasks"
v-for="{ task, originalIndex } in taskRenderedItems"
v-if="viewMode === 'task'"
:key="task.id"
class="task-row"
@@ -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 })
+42
View File
@@ -0,0 +1,42 @@
/**
* dragOverRegistry -
*
* TaskRow window 'task-row-drag-over'
* CustomEvent 广O(n)
*
* + Map O(1)
* N 1
*/
type DragOverCallback = (mouseEvent: MouseEvent) => void
const _registry = new Map<number | string, DragOverCallback>()
function _handleGlobalDragOver(e: Event) {
const { taskId, event: mouseEvent } = (e as CustomEvent).detail
const handler = _registry.get(taskId)
if (handler) handler(mouseEvent)
}
/**
*
* window
*/
export function registerDragOver(taskId: number | string, handler: DragOverCallback): void {
const wasEmpty = _registry.size === 0
_registry.set(taskId, handler)
if (wasEmpty) {
window.addEventListener('task-row-drag-over', _handleGlobalDragOver)
}
}
/**
*
* window
*/
export function unregisterDragOver(taskId: number | string): void {
_registry.delete(taskId)
if (_registry.size === 0) {
window.removeEventListener('task-row-drag-over', _handleGlobalDragOver)
}
}