From 7d81f44778576c3dfab629034b8f805c162a9040 Mon Sep 17 00:00:00 2001 From: "LINING-PC\\lining" Date: Sat, 6 Dec 2025 20:34:33 +0800 Subject: [PATCH 1/6] v1.4.5 - add Task drag and drop ender TaskList --- CHANGELOG.md | 12 ++ README-EN.md | 115 +++++++++++ README.md | 115 +++++++++++ demo/App.vue | 116 ++++++++++- demo/TaskMoveConfirmDialog.vue | 241 +++++++++++++++++++++ demo/locales/en-US.json | 24 +++ demo/locales/zh-CN.json | 24 +++ demo/useDemoLocale.ts | 89 ++++++++ demo/version-history.json | 20 ++ npm-demo/src/components/GanttTest.vue | 131 ++++++++---- package.json | 2 +- src/components/GanttChart.vue | 33 +++ src/components/TaskList.vue | 138 +++++++++---- src/components/TaskRow.vue | 73 +++++++ src/composables/useTaskRowDrag.ts | 287 ++++++++++++++++++++++++++ src/utils/taskTreeUtils.ts | 137 ++++++++++++ 16 files changed, 1471 insertions(+), 86 deletions(-) create mode 100644 demo/TaskMoveConfirmDialog.vue create mode 100644 demo/locales/en-US.json create mode 100644 demo/locales/zh-CN.json create mode 100644 demo/useDemoLocale.ts create mode 100644 src/composables/useTaskRowDrag.ts create mode 100644 src/utils/taskTreeUtils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dabca2d..87ce669 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.4.5] - 2025-12-06 + +### Changed +- 优化:任务列表中任务项的移动拖放功能 +- Optimized:Task item move and drag-and-drop functionality in the task list + +## [1.4.4] - 2025-12-06 + +### Added +- 任务列表中任务项的移动拖放功能 +- Task item move and drag-and-drop functionality in the task list + ## [1.4.3] - 2025-11-28 ### Added diff --git a/README-EN.md b/README-EN.md index be593e2..1118b09 100644 --- a/README-EN.md +++ b/README-EN.md @@ -189,6 +189,7 @@ npm run dev | `useDefaultMilestoneDialog` | `boolean` | `true` | Whether to use the built-in milestone edit dialog (MilestoneDialog) | | `autoSortByStartDate` | `boolean` | `false` | Whether to automatically sort tasks by start date | | `allowDragAndResize` | `boolean` | `true` | Whether to allow dragging and resizing tasks/milestones | +| `enableTaskRowMove` | `boolean` | `false` | Whether to alloww dragging and dropping TaskRow #### Configuration Object Props @@ -243,6 +244,7 @@ For complete event documentation, see: | `milestone-deleted` | `{ milestoneId: number }` | Milestone deleted | | `milestone-icon-changed` | `{ milestoneId, icon }` | Milestone icon changed | | `milestone-drag-end` | `(milestone: Task)` | Milestone drag ended | +| `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | TaskRow drag ended (optional) | #### Example 1: Simplest Gantt Chart @@ -424,6 +426,7 @@ Tasks are the core elements of the Gantt chart. The component provides complete | `taskBarConfig` | `TaskBarConfig` | `{}` | Task bar style configuration, see [TaskBarConfig Configuration](#taskbarconfig-configuration) | | `taskListConfig` | `TaskListConfig` | `undefined` | Task list configuration, see [TaskListConfig Configuration](#tasklistconfig-configuration) | | `autoSortByStartDate` | `boolean` | `false` | Whether to automatically sort tasks by start date | +| `enableTaskRowMove` | `boolean` | `false` | Whether to alloww dragging and dropping TaskRow **Configuration Notes**: @@ -449,6 +452,7 @@ Tasks are the core elements of the Gantt chart. The component provides complete | `successor-added` | `{ targetTask: Task, newTask: Task }` | After adding successor via context menu | `targetTask` is the original task, `newTask` is the newly created successor task (its predecessor already contains targetTask.id) | | `timer-started` | `(task: Task) => void` | When task timer starts | Start recording task hours | | `timer-stopped` | `(task: Task) => void` | When task timer stops | Stop recording task hours | +| `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | TaskRow drag ended (optional) | Component has automatically completed data movement and TaskList/Timeline sync via object reference mutation. Listening to this event is completely optional, only for showing messages, calling API, etc. `position`: 'after'=same level, 'child'=as child | **Data Synchronization Notes**: @@ -715,6 +719,117 @@ const handleTaskAdded = e => { > - Hide toolbar + custom buttons + default editor: Custom control bar style while keeping default edit functionality > - Hide toolbar + custom buttons + custom editor: Fully customize all interaction logic +#### Example 4: Task Row Drag and Drop Sorting + +Allow users to adjust task hierarchy and order by dragging TaskRow: + +```vue + + + +``` + +**Drag and Drop Sorting Notes**: + +- **Enable Dragging**: Set `enable-task-row-move="true"` to enable task row dragging (default is `false`) +- **Dragging Algorithms** (automatically executed by component): + - **Algorithm 1 (Place After)**: When target task has no children, dragged task will be placed after target task (same level), `position='after'` + - **Algorithm 2 (As Child)**: When target task has children, dragged task will become first child of target task, `position='child'` +- **Visual Feedback**: + - Semi-transparent following element displayed while dragging + - Blue border hint shown when hovering over valid target tasks + - Tasks without children show blue bottom border + - Tasks with children show blue border on all sides +- **Auto Sync**: Component internally mutates `props.tasks` via object reference, automatically completing task move, `parentId` update, `children` array adjustment, and TaskList/Timeline synchronization +- **Event Listening (Optional)**: + - `task-row-moved` event is completely optional, only used for showing messages, calling API to save, recording logs, etc. + - No need to manually update `tasks.value`, component has automatically completed data synchronization +- **Event Parameters**: + - `draggedTask`: The dragged task + - `targetTask`: The target task + - `position`: Drop position ('after' or 'child') + - `oldParent`: Original parent task (null means root) + - `newParent`: New parent task (null means root) +- **Constraints**: + - Cannot drag onto itself + - Cannot drag onto its own child tasks (avoid circular reference) + - Milestones and milestone groups cannot be dragged + ### Milestone Management ### Milestone Management diff --git a/README.md b/README.md index ad6e7fb..a479489 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ npm run dev | `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog) | | `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 | | `allowDragAndResize` | `boolean` | `true` | 是否允许拖拽和调整任务/里程碑大小 | +| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow | #### 配置对象属性 @@ -241,6 +242,7 @@ npm run dev | `milestone-deleted` | `{ milestoneId: number }` | 里程碑删除 | | `milestone-icon-changed` | `{ milestoneId, icon }` | 里程碑图标变更 | | `milestone-drag-end` | `(milestone: Task)` | 拖拽里程碑结束 | +| `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | 拖拽TaskRow结束(可选) | #### 示例1:最简单的甘特图 @@ -422,6 +424,7 @@ const handleMilestoneSaved = milestone => { | `taskBarConfig` | `TaskBarConfig` | `{}` | 任务条样式配置,详见 [TaskBarConfig 配置](#taskbarconfig-配置) | | `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置,详见 [TaskListConfig 配置](#tasklistconfig-配置) | | `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 | +| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow **配置说明**: @@ -447,6 +450,7 @@ const handleMilestoneSaved = milestone => { | `successor-added` | `{ targetTask: Task, newTask: Task }` | 通过右键菜单添加后置任务后 | `targetTask` 是原任务,`newTask` 是新创建的后置任务(其 predecessor 已包含 targetTask.id) | | `timer-started` | `(task: Task) => void` | 任务计时器启动时 | 开始记录任务工时 | | `timer-stopped` | `(task: Task) => void` | 任务计时器停止时 | 停止记录任务工时 | +| `task-row-moved` | `payload: { draggedTask: Task, targetTask: Task, position: 'after' \| 'child', oldParent: Task \| null, newParent: Task \| null }` | 拖拽TaskRow结束(可选) | 组件已自动完成数据移动和TaskList/Timeline同步。监听此事件为完全可选,仅用于显示提示、调用API保存等。`position`: 'after'=同级放置,'child'=作为子任务 | **数据同步说明**: @@ -713,6 +717,117 @@ const handleTaskAdded = e => { > - 隐藏工具栏 + 自定义按钮 + 默认编辑器:自定义控制栏样式,保留默认编辑功能 > - 隐藏工具栏 + 自定义按钮 + 自定义编辑器:完全自定义所有交互逻辑 +#### 示例4:任务行拖拽排序 + +允许用户通过拖拽 TaskRow 来调整任务的层级关系和前后顺序: + +```vue + + + +``` + +**拖拽排序说明**: + +- **启用拖拽**:设置 `enable-task-row-move="true"` 启用任务行拖拽功能(默认为 `false`) +- **拖拽算法**(组件内部自动执行): + - **算法1(放置在后面)**:当目标任务没有子任务时,被拖拽的任务会放置在目标任务之后(同级),`position='after'` + - **算法2(作为子任务)**:当目标任务有子任务时,被拖拽的任务会成为目标任务的第一个子任务,`position='child'` +- **视觉反馈**: + - 拖拽时会显示半透明的跟随元素 + - 悬停在有效目标任务上时显示蓝色边框提示 + - 无子任务的任务显示蓝色底部边框 + - 有子任务的任务显示蓝色四周边框 +- **自动同步**:组件内部通过对象引用直接修改 `props.tasks`,自动完成任务移动、`parentId` 更新、`children` 数组调整以及 TaskList/Timeline 同步 +- **事件监听(可选)**: + - `task-row-moved` 事件为完全可选,仅用于显示提示、调用API保存、记录日志等额外处理 + - 无需手动更新 `tasks.value`,组件已自动完成数据同步 +- **事件参数**: + - `draggedTask`: 被拖拽的任务 + - `targetTask`: 目标任务 + - `position`: 放置位置('after' 或 'child') + - `oldParent`: 原父任务(null 表示根目录) + - `newParent`: 新父任务(null 表示根目录) +- **限制条件**: + - 不能拖拽到自己身上 + - 不能拖拽到自己的子任务上(避免循环引用) + - 里程碑和里程碑分组不能被拖拽 + ### 里程碑管理 里程碑用于标记项目中的重要时间节点,如项目启动、阶段完成、产品发布等。组件提供了灵活的里程碑编辑配置,默认使用内置的 MilestoneDialog,也支持完全自定义编辑行为。 diff --git a/demo/App.vue b/demo/App.vue index 17b505f..cf39f1e 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -12,6 +12,7 @@ import VersionHistoryDrawer from './VersionHistoryDrawer.vue' import HtmlContent from './HtmlContent.vue' import { useMessage } from '../src/composables/useMessage' import { useI18n } from '../src/composables/useI18n' +import { useDemoLocale } from './useDemoLocale' import { getPredecessorIds, predecessorIdsToString } from '../src/utils/predecessorUtils' import type { Task } from '../src/models/Task' import type { TaskListConfig, TaskListColumnConfig } from '../src/models/configs/TaskListConfig' @@ -19,6 +20,7 @@ import type { TaskBarConfig } from '../src/models/configs/TaskBarConfig' const { showMessage } = useMessage() const { t, formatTranslation } = useI18n() +const { locale: demoLocale, messages: demoMessages, setLocale: setDemoLocale, formatMessage, getTaskTypeName, getParentName } = useDemoLocale() const tasks = ref([]) const milestones = ref([]) @@ -182,6 +184,9 @@ const taskListConfig = computed(() => ({ // 控制是否允许拖拽和拉伸 const allowDragAndResize = ref(true) +// 控制是否启用TaskRow拖拽移动 +const enableTaskRowMove = ref(true) + // TaskBar配置 const taskBarOptions = ref({ showAvatar: true, @@ -236,6 +241,17 @@ const toggleTaskBarConfig = () => { const showTaskClickDialog = ref(false) const clickedTask = ref(null) +// TaskRow Move 相关(已移除确认对话框,直接显示提示消息) + +// 同步语言切换 +const handleLanguageChange = (lang: 'zh-CN' | 'en-US') => { + setDemoLocale(lang) + const languageText = lang === 'zh-CN' ? '中文' : 'English' + showMessage(formatTranslation('languageSwitchedTo', { language: languageText }), 'info', { + closable: true, + }) +} + // 处理任务点击事件 const handleTaskClick = (task: Task) => { clickedTask.value = task @@ -306,12 +322,7 @@ const handleAddMilestone = () => { showMilestoneDialog.value = true } -const handleLanguageChange = (lang: 'zh' | 'en') => { - const languageText = lang === 'zh' ? '中文' : 'English' - showMessage(formatTranslation('languageSwitchedTo', { language: languageText }), 'info', { - closable: true, - }) -} + const handleThemeChange = (isDark: boolean) => { const themeText = isDark ? t.value.darkModeText : t.value.lightModeText @@ -544,6 +555,86 @@ function onTimerStopped(task: Task) { function taskDebug(item: any) { // Placeholder for debugging } + +// TaskRow拖拽移动事件处理器 +const handleTaskRowMoved = async (payload: { + draggedTask: Task + targetTask: Task + position: 'after' | 'child' // 'after': 放在目标任务之后(同级), 'child': 作为目标任务的子任务 + oldParent: Task | null + newParent: Task | null +}) => { + const { draggedTask, targetTask, position, oldParent, newParent } = payload + + // 【注意】组件内部已自动完成数据移动,通过对象引用修改实现 TaskList 和 Timeline 的自动同步 + // 因此监听此事件是完全可选的,仅用于: + // 1. 显示自定义提示消息 + // 2. 调用后端API保存任务层级变更 + // 3. 记录操作日志 + // 4. 触发其他业务逻辑(如更新关联数据) + + // 构建提示消息 + const oldParentName = getParentName(oldParent) + const newParentName = position === 'after' + ? getParentName(newParent) + : getParentName({ type: targetTask.type, name: targetTask.name }) + + let message = '' + const msgs = demoMessages.value.taskMoveConfirm.messages + + if (position === 'after') { + // 算法#1: 放置在目标任务之后 + message = formatMessage(msgs.moveAfter, { + draggedTaskType: getTaskTypeName(draggedTask.type), + draggedTaskName: draggedTask.name, + targetTaskType: getTaskTypeName(targetTask.type), + targetTaskName: targetTask.name, + }) + } else { + // 算法#2: 作为子任务放置 + if (oldParent && oldParent.id !== targetTask.id) { + message = formatMessage(msgs.moveAsChild, { + draggedTaskName: draggedTask.name, + oldParentName, + newParentName, + }) + } else if (!oldParent) { + message = formatMessage(msgs.moveAsChildNoOldParent, { + draggedTaskName: draggedTask.name, + newParentName, + }) + } else { + message = formatMessage(msgs.moveAsChildSameParent, { + draggedTaskName: draggedTask.name, + newParentName, + }) + } + } + + // 显示移动成功提示 + const successMsg = demoMessages.value.taskMoveConfirm.messages.moveSuccess + showMessage(`${successMsg}: ${message}`, 'success', { closable: true }) + + // ⚠️ 重要:必须更新 tasks.value 以同步 TaskList 和 Timeline + // TaskList 已更新内部视图,但 Timeline 依赖 props.tasks + // 此赋值会触发 GanttChart 的 watch,进而触发 Timeline 重新渲染 + tasks.value = updatedTasks + + // 调用后端API保存任务层级变更 + // try { + // await api.updateTaskHierarchy({ + // taskId: draggedTask.id, + // targetTaskId: targetTask.id, + // position: position, // 'after' 或 'child' + // oldParentId: oldParent?.id, + // newParentId: newParent?.id, + // }) + // console.log('任务层级已保存到后端') + // } catch (error) { + // console.error('保存任务层级失败:', error) + // showMessage('保存失败,请刷新页面', 'error', { closable: true }) + // } +}