v1.9.3 - fix taskbar cal issue
This commit is contained in:
@@ -5,6 +5,16 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [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
|
## [1.9.2] - 2026-02-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+8
-1
@@ -226,6 +226,7 @@ npm run dev
|
|||||||
| `showTaskbarTab`  | `boolean` | `true` | Whether to display resource tab on TaskBar (shows resource allocation label on TaskBar in resource view) |
|
| `showTaskbarTab`  | `boolean` | `true` | Whether to display resource tab on TaskBar (shows resource allocation label on TaskBar in resource view) |
|
||||||
| `enableTaskListCollapsible`  | `boolean` | `true` | Whether to allow collapsing/expanding the TaskList panel. When `false`: forcibly hides TaskList, SplitterBar and collapse button; Timeline takes full width |
|
| `enableTaskListCollapsible`  | `boolean` | `true` | Whether to allow collapsing/expanding the TaskList panel. When `false`: forcibly hides TaskList, SplitterBar and collapse button; Timeline takes full width |
|
||||||
| `taskListVisible`  | `boolean` | `true` | Controls TaskList visibility (reactive). Only effective when `enableTaskListCollapsible=true` |
|
| `taskListVisible`  | `boolean` | `true` | Controls TaskList visibility (reactive). Only effective when `enableTaskListCollapsible=true` |
|
||||||
|
| `enableTaskDrawerAutoClose`  | `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
|
#### 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
|
[MIT License](./LICENSE) © 2025 JORDIUM.COM
|
||||||
|
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ npm run dev
|
|||||||
| `showTaskbarTab`  | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar的资源分配标签) |
|
| `showTaskbarTab`  | `boolean` | `true` | 是否显示TaskBar上的资源Tab标签(资源视图下TaskBar的资源分配标签) |
|
||||||
| `enableTaskListCollapsible`  | `boolean` | `true` | 是否允许折叠/展开 TaskList 面板。`false` 时强制隐藏 TaskList、SplitterBar 及折叠按钮,Timeline 独占全宽 |
|
| `enableTaskListCollapsible`  | `boolean` | `true` | 是否允许折叠/展开 TaskList 面板。`false` 时强制隐藏 TaskList、SplitterBar 及折叠按钮,Timeline 独占全宽 |
|
||||||
| `taskListVisible`  | `boolean` | `true` | 控制 TaskList 的显隐状态(响应式)。仅在 `enableTaskListCollapsible=true` 时有效 |
|
| `taskListVisible`  | `boolean` | `true` | 控制 TaskList 的显隐状态(响应式)。仅在 `enableTaskListCollapsible=true` 时有效 |
|
||||||
|
| `enableTaskDrawerAutoClose`  | `boolean` | `true` | 是否允许 TaskDrawer 自动关闭(外总点击或按 Esc 时自动关闭)。设为 `false` 时禁用自动关闭,仅可通过内部按钮手动关闭 |
|
||||||
|
|
||||||
#### TaskListColumn 属性
|
#### TaskListColumn 属性
|
||||||
|
|
||||||
@@ -3630,7 +3631,13 @@ TaskList 组件经过深度重构,采用模块化设计,提升了代码可
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📄 开源协议
|
## � Troubleshooting
|
||||||
|
|
||||||
|
常见问题与解决方案请参阅 [Troubleshooting.md](./Troubleshooting.md)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## �📄 开源协议
|
||||||
|
|
||||||
[MIT License](./LICENSE) © 2025 JORDIUM.COM
|
[MIT License](./LICENSE) © 2025 JORDIUM.COM
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -2377,6 +2377,7 @@ const handleCustomMenuAction = (action: string, task: Task) => {
|
|||||||
:complete-task-background-color="completeTaskBackgroundColor"
|
:complete-task-background-color="completeTaskBackgroundColor"
|
||||||
:ongoing-task-background-color="ongoingTaskBackgroundColor"
|
:ongoing-task-background-color="ongoingTaskBackgroundColor"
|
||||||
:use-default-drawer="useDefaultDrawer"
|
:use-default-drawer="useDefaultDrawer"
|
||||||
|
:enable-task-drawer-auto-close="false"
|
||||||
@milestone-saved="handleMilestoneSaved"
|
@milestone-saved="handleMilestoneSaved"
|
||||||
@milestone-deleted="handleMilestoneDeleted"
|
@milestone-deleted="handleMilestoneDeleted"
|
||||||
@milestone-icon-changed="handleMilestoneIconChanged"
|
@milestone-icon-changed="handleMilestoneIconChanged"
|
||||||
|
|||||||
@@ -576,5 +576,17 @@
|
|||||||
"🔧 Fixed: GanttToolbar showViewMode:false had no effect due to missing v-if guard",
|
"🔧 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>"
|
"<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>"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.9.2",
|
"version": "1.9.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
||||||
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
theme: undefined, // 不设置默认值,允许自动检测系统主题
|
theme: undefined, // 不设置默认值,允许自动检测系统主题
|
||||||
enableTaskListCollapsible: true,
|
enableTaskListCollapsible: true,
|
||||||
taskListVisible: true,
|
taskListVisible: true,
|
||||||
|
enableTaskDrawerAutoClose: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -559,6 +560,12 @@ interface Props {
|
|||||||
* 可通过 prop 传入或通过 defineExpose 暴露的方法控制
|
* 可通过 prop 传入或通过 defineExpose 暴露的方法控制
|
||||||
*/
|
*/
|
||||||
taskListVisible?: boolean
|
taskListVisible?: boolean
|
||||||
|
/**
|
||||||
|
* 是否允许点击遮罩层关闭 TaskDrawer,默认为 true(保持原有行为)
|
||||||
|
* 设置为 false 可防止 TaskDrawer 在失去焦点(点击遮罩区域)时自动关闭
|
||||||
|
* 仅在 useDefaultDrawer=true 时有效
|
||||||
|
*/
|
||||||
|
enableTaskDrawerAutoClose?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||||
@@ -1601,31 +1608,30 @@ const tasksForTimeline = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 递归更新任务树中所有父任务的时间范围(从叶子节点开始向上)
|
// 递归更新任务树中所有父任务的时间范围(从叶子节点开始向上)
|
||||||
|
// 优化:叶子任务直接返回原引用,避免无效 { ...task } 对象创建
|
||||||
|
// (后续 smartFlattenTasks 会在 spread 时附加 level,不会污染原对象)
|
||||||
const updateParentDateRanges = (tasks: Task[]): Task[] => {
|
const updateParentDateRanges = (tasks: Task[]): Task[] => {
|
||||||
return tasks.map(task => {
|
return tasks.map(task => {
|
||||||
let updatedTask = { ...task }
|
// 叶子任务:无需创建新对象,直接返回原引用
|
||||||
|
if (!task.children || task.children.length === 0) {
|
||||||
// 先递归更新子任务
|
return task
|
||||||
if (task.children && task.children.length > 0) {
|
|
||||||
updatedTask.children = updateParentDateRanges(task.children)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 基于任务类型判断是否为父任务
|
// 父任务:先递归处理子任务
|
||||||
const isParent =
|
const updatedChildren = updateParentDateRanges(task.children)
|
||||||
task.type === 'story' || (updatedTask.children && updatedTask.children.length > 0)
|
const isParent = task.type === 'story' || true
|
||||||
updatedTask.isParent = isParent
|
|
||||||
|
|
||||||
// 如果是父任务且有子任务,重新计算时间范围
|
// 重新计算父任务时间范围
|
||||||
if (isParent && updatedTask.children && updatedTask.children.length > 0) {
|
const taskWithChildren = { ...task, children: updatedChildren }
|
||||||
const { startDate, endDate } = calculateParentDateRange(updatedTask)
|
const { startDate, endDate } = calculateParentDateRange(taskWithChildren)
|
||||||
updatedTask = {
|
|
||||||
...updatedTask,
|
return {
|
||||||
startDate,
|
...task,
|
||||||
endDate,
|
children: updatedChildren,
|
||||||
}
|
isParent,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedTask
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3530,6 +3536,7 @@ defineExpose({
|
|||||||
:delay-task-background-color="props.delayTaskBackgroundColor"
|
:delay-task-background-color="props.delayTaskBackgroundColor"
|
||||||
:complete-task-background-color="props.completeTaskBackgroundColor"
|
:complete-task-background-color="props.completeTaskBackgroundColor"
|
||||||
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
|
:ongoing-task-background-color="props.ongoingTaskBackgroundColor"
|
||||||
|
:enable-close-on-overlay-click="props.enableTaskDrawerAutoClose"
|
||||||
@submit="handleTaskDrawerSubmit"
|
@submit="handleTaskDrawerSubmit"
|
||||||
@close="taskDrawerVisible = false"
|
@close="taskDrawerVisible = false"
|
||||||
@start-timer="handleStartTimer"
|
@start-timer="handleStartTimer"
|
||||||
|
|||||||
@@ -1569,8 +1569,14 @@ const handleMouseMove = (e: MouseEvent) => {
|
|||||||
} else {
|
} else {
|
||||||
// 其他情况:使用原有的简单计算
|
// 其他情况:使用原有的简单计算
|
||||||
const newStartDate = addDaysToLocalDate(props.startDate, newLeft / props.dayWidth)
|
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 = {
|
tempTaskData.value = {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface Props {
|
|||||||
delayTaskBackgroundColor?: string
|
delayTaskBackgroundColor?: string
|
||||||
completeTaskBackgroundColor?: string
|
completeTaskBackgroundColor?: string
|
||||||
ongoingTaskBackgroundColor?: string
|
ongoingTaskBackgroundColor?: string
|
||||||
|
/** 是否允许点击遮罩层关闭抽屉,默认为 true(保持原有行为)。设置为 false 可防止失去焦点时自动关闭 */
|
||||||
|
enableCloseOnOverlayClick?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -45,6 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
delayTaskBackgroundColor: '#f56c6c',
|
delayTaskBackgroundColor: '#f56c6c',
|
||||||
completeTaskBackgroundColor: '#67c23a',
|
completeTaskBackgroundColor: '#67c23a',
|
||||||
ongoingTaskBackgroundColor: '#409eff',
|
ongoingTaskBackgroundColor: '#409eff',
|
||||||
|
enableCloseOnOverlayClick: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -484,7 +487,9 @@ const handleClose = () => {
|
|||||||
|
|
||||||
// 点击遮罩层关闭
|
// 点击遮罩层关闭
|
||||||
const handleOverlayClick = () => {
|
const handleOverlayClick = () => {
|
||||||
handleClose()
|
if (props.enableCloseOnOverlayClick) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
|
|||||||
@@ -65,41 +65,31 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
|
|||||||
const flattenedTasks = computed(() => getFlattenedVisibleTasks(tasks.value))
|
const flattenedTasks = computed(() => getFlattenedVisibleTasks(tasks.value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* v1.9.0 计算每个任务/资源的累计高度位置(支持动态行高)
|
* 资源视图专用:累计高度数组(动态行高,需要数组+二分查找)
|
||||||
|
* 任务视图固定行高,直接用公式计算,不构造此数组,避免 O(n) 分配
|
||||||
*/
|
*/
|
||||||
const cumulativeHeights = computed<number[]>(() => {
|
const cumulativeHeights = computed<number[]>(() => {
|
||||||
if (viewMode.value === 'resource') {
|
if (viewMode.value !== 'resource') return []
|
||||||
// 资源视图:使用每个资源的实际高度
|
|
||||||
const resources = dataSource.value as Resource[]
|
|
||||||
const heights: number[] = [0] // 第一个位置是0
|
|
||||||
let cumulative = 0
|
|
||||||
|
|
||||||
resources.forEach(resource => {
|
const resources = dataSource.value as Resource[]
|
||||||
const layout = resourceTaskLayouts.value.get(resource.id)
|
const heights: number[] = [0]
|
||||||
const height = layout?.totalHeight || ROW_HEIGHT
|
let cumulative = 0
|
||||||
cumulative += height
|
resources.forEach(resource => {
|
||||||
heights.push(cumulative)
|
const layout = resourceTaskLayouts.value.get(resource.id)
|
||||||
})
|
const height = layout?.totalHeight || ROW_HEIGHT
|
||||||
|
cumulative += height
|
||||||
return heights
|
heights.push(cumulative)
|
||||||
} else {
|
})
|
||||||
// 任务视图:使用固定行高
|
return heights
|
||||||
const heights: number[] = [0]
|
|
||||||
for (let i = 1; i <= flattenedTasks.value.length; i++) {
|
|
||||||
heights.push(i * ROW_HEIGHT)
|
|
||||||
}
|
|
||||||
return heights
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据滚动位置查找对应的任务/资源索引(二分查找)
|
* 资源视图专用:根据滚动位置查找资源索引(二分查找)
|
||||||
*/
|
*/
|
||||||
const findIndexByScrollTop = (scrollTop: number): number => {
|
const findIndexByScrollTop = (scrollTop: number): number => {
|
||||||
const heights = cumulativeHeights.value
|
const heights = cumulativeHeights.value
|
||||||
let left = 0
|
let left = 0
|
||||||
let right = heights.length - 1
|
let right = heights.length - 1
|
||||||
|
|
||||||
while (left < right) {
|
while (left < right) {
|
||||||
const mid = Math.floor((left + right) / 2)
|
const mid = Math.floor((left + right) / 2)
|
||||||
if (heights[mid] <= scrollTop) {
|
if (heights[mid] <= scrollTop) {
|
||||||
@@ -108,38 +98,37 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
|
|||||||
right = mid
|
right = mid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.max(0, left - 1)
|
return Math.max(0, left - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算可视区域任务范围(支持动态行高)
|
* 计算可视区域任务范围
|
||||||
|
* - 任务视图:固定行高,O(1) 公式直接计算(不依赖 cumulativeHeights)
|
||||||
|
* - 资源视图:动态行高,使用累计高度数组 + 二分查找
|
||||||
*/
|
*/
|
||||||
const visibleTaskRange = computed<VisibleTaskRange>(() => {
|
const visibleTaskRange = computed<VisibleTaskRange>(() => {
|
||||||
const scrollTop = taskListScrollTop.value
|
const scrollTop = taskListScrollTop.value
|
||||||
const containerHeight = taskListBodyHeight.value || 600
|
const containerHeight = taskListBodyHeight.value || 600
|
||||||
const heights = cumulativeHeights.value
|
|
||||||
|
|
||||||
if (heights.length <= 1) {
|
if (viewMode.value === 'resource') {
|
||||||
return { startIndex: 0, endIndex: 0 }
|
const heights = cumulativeHeights.value
|
||||||
}
|
if (heights.length <= 1) return { startIndex: 0, endIndex: 0 }
|
||||||
|
|
||||||
// 找到起始索引(考虑缓冲区)
|
const total = (dataSource.value as Resource[]).length
|
||||||
let startIndex = findIndexByScrollTop(scrollTop)
|
let startIndex = Math.max(0, findIndexByScrollTop(scrollTop) - VERTICAL_BUFFER)
|
||||||
startIndex = Math.max(0, startIndex - VERTICAL_BUFFER)
|
const scrollBottom = scrollTop + containerHeight
|
||||||
|
let endIndex = Math.min(heights.length - 1, findIndexByScrollTop(scrollBottom) + VERTICAL_BUFFER + 1)
|
||||||
// 找到结束索引(考虑缓冲区)
|
return {
|
||||||
const scrollBottom = scrollTop + containerHeight
|
startIndex: Math.min(startIndex, total),
|
||||||
let endIndex = findIndexByScrollTop(scrollBottom)
|
endIndex: Math.min(endIndex, total),
|
||||||
endIndex = Math.min(heights.length - 1, endIndex + VERTICAL_BUFFER + 1)
|
}
|
||||||
|
} else {
|
||||||
const total = viewMode.value === 'resource'
|
// 任务视图:固定行高 ROW_HEIGHT,O(1) 直接计算,不访问 cumulativeHeights
|
||||||
? (dataSource.value as Resource[]).length
|
const total = flattenedTasks.value.length
|
||||||
: flattenedTasks.value.length
|
if (total === 0) return { startIndex: 0, endIndex: 0 }
|
||||||
|
const startIndex = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - VERTICAL_BUFFER)
|
||||||
return {
|
const endIndex = Math.min(total, Math.ceil((scrollTop + containerHeight) / ROW_HEIGHT) + VERTICAL_BUFFER)
|
||||||
startIndex: Math.min(startIndex, total),
|
return { startIndex, endIndex }
|
||||||
endIndex: Math.min(endIndex, total),
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -149,7 +138,6 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
|
|||||||
const visibleTasks = computed<TaskWithLevelAndIndex[]>(() => {
|
const visibleTasks = computed<TaskWithLevelAndIndex[]>(() => {
|
||||||
const { startIndex, endIndex } = visibleTaskRange.value
|
const { startIndex, endIndex } = visibleTaskRange.value
|
||||||
const slicedTasks = flattenedTasks.value.slice(startIndex, endIndex)
|
const slicedTasks = flattenedTasks.value.slice(startIndex, endIndex)
|
||||||
// 添加 rowIndex(基于在扁平化列表中的实际索引)
|
|
||||||
return slicedTasks.map((item, index) => ({
|
return slicedTasks.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
rowIndex: startIndex + index,
|
rowIndex: startIndex + index,
|
||||||
@@ -157,22 +145,31 @@ export function useTaskListLayout(tasks: Ref<Task[]>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spacer 高度用于撑起滚动区域(支持动态行高)
|
* Spacer 高度撑起滚动区域
|
||||||
|
* 任务视图:O(1) 乘法;资源视图:从累计高度数组读取
|
||||||
*/
|
*/
|
||||||
const totalContentHeight = computed(() => {
|
const totalContentHeight = computed(() => {
|
||||||
const heights = cumulativeHeights.value
|
if (viewMode.value === 'resource') {
|
||||||
return heights.length > 0 ? heights[heights.length - 1] : 0
|
const heights = cumulativeHeights.value
|
||||||
|
return heights.length > 0 ? heights[heights.length - 1] : 0
|
||||||
|
}
|
||||||
|
return flattenedTasks.value.length * ROW_HEIGHT
|
||||||
})
|
})
|
||||||
|
|
||||||
const startSpacerHeight = computed(() => {
|
const startSpacerHeight = computed(() => {
|
||||||
const startIdx = visibleTaskRange.value.startIndex
|
if (viewMode.value === 'resource') {
|
||||||
return cumulativeHeights.value[startIdx] || 0
|
return cumulativeHeights.value[visibleTaskRange.value.startIndex] || 0
|
||||||
|
}
|
||||||
|
return visibleTaskRange.value.startIndex * ROW_HEIGHT
|
||||||
})
|
})
|
||||||
|
|
||||||
const endSpacerHeight = computed(() => {
|
const endSpacerHeight = computed(() => {
|
||||||
const endIdx = visibleTaskRange.value.endIndex
|
if (viewMode.value === 'resource') {
|
||||||
const endHeight = cumulativeHeights.value[endIdx] || 0
|
const endIdx = visibleTaskRange.value.endIndex
|
||||||
return Math.max(0, totalContentHeight.value - endHeight)
|
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 {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user