diff --git a/CHANGELOG.md b/CHANGELOG.md
index dabca2d..ec32d03 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,32 @@ 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.6-patch.1] - 2025-12-09
+
+### Fixed
+- 修复:Github编译错误
+- Fixed: build errors From github
+
+## [1.4.6] - 2025-12-09
+
+### Fixed
+- 修复:内置TaskDrawer中负责人列表可以外部初始化
+- 修复:未设置startDate和endDate时,任务无法正确显示的问题
+- Fixed: The assignee list in the built-in TaskDrawer can be initialized externally
+- Fixed: The issue where tasks could not be displayed correctly when startDate and endDate were not set
+
+## [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..9b67581 100644
--- a/README-EN.md
+++ b/README-EN.md
@@ -23,7 +23,8 @@
中文 |
- English
+ English |
+ Release Notes
A modern Vue 3 Gantt chart component library providing complete solutions for project management and task scheduling
@@ -188,7 +189,9 @@ npm run dev
| `useDefaultDrawer` | `boolean` | `true` | Whether to use the built-in task edit drawer (TaskDrawer) |
| `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 |
+| `allowDragAndResize` | `boolean` | `true` | Whether to allow dragging and resizing tasks/milestones |
+| `enableTaskRowMove` | `boolean` | `false` | Whether to allow dragging and dropping TaskRow |
+| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | Assignee dropdown options in task edit drawer |
#### Configuration Object Props
@@ -243,13 +246,14 @@ 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
```vue
-
+
@@ -267,6 +271,12 @@ const tasks = ref([
progress: 100,
},
])
+
+const assigneeOptions = ref([
+ { value: 'alice', label: 'Alice' },
+ { value: 'bob', label: 'Bob' },
+ { value: 'charlie', label: 'Charlie' },
+])
```
@@ -275,7 +285,7 @@ const tasks = ref([
```vue
-
+
@@ -303,6 +313,12 @@ const milestones = ref([
icon: 'diamond',
},
])
+
+const assigneeOptions = ref([
+ { value: 'alice', label: 'Alice' },
+ { value: 'bob', label: 'Bob' },
+ { value: 'charlie', label: 'Charlie' },
+])
```
@@ -323,6 +339,7 @@ const milestones = ref([
:tasks="tasks"
:milestones="milestones"
:show-toolbar="false"
+ :assignee-options="assigneeOptions"
@task-added="handleTaskAdded"
@milestone-saved="handleMilestoneSaved"
/>
@@ -338,6 +355,12 @@ import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
const tasks = ref([])
const milestones = ref([])
+const assigneeOptions = ref([
+ { value: 'alice', label: 'Alice' },
+ { value: 'bob', label: 'Bob' },
+ { value: 'charlie', label: 'Charlie' },
+])
+
const addTask = () => {
const newTask = {
id: Date.now(),
@@ -385,7 +408,8 @@ Tasks are the core elements of the Gantt chart. The component provides complete
| `endDate` | `string` | - | - | End date, format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:mm' |
| `progress` | `number` | - | `0` | Task progress, range 0-100 |
| `predecessor` | `number[]` | - | - | Array of predecessor task IDs, standard format: `[1, 2, 3]` **Compatible formats**: Also supports string `'1,2,3'` or string array `['1', '2', '3']`, component will auto-parse |
-| `assignee` | `string` | - | - | Task assignee |
+| `assignee` | `string` | - | - | Task assignee, used as the value binding for the assignee dropdown menu |
+| `assigneeName` | `string` | - | - | Task assignee name, automatically obtained from the label in the bound `assigneeOptions` dataset; for custom display, you can set it in the `task-added` callback event of GanttChart |
| `avatar` | `string` | - | - | Avatar URL of task assignee |
| `estimatedHours` | `number` | - | - | Estimated hours |
| `actualHours` | `number` | - | - | Actual hours |
@@ -424,6 +448,8 @@ 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 |
+| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | Assignee dropdown options in task edit drawer |
**Configuration Notes**:
@@ -449,6 +475,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**:
@@ -463,6 +490,7 @@ Tasks are the core elements of the Gantt chart. The component provides complete
([
},
])
+const assigneeOptions = ref([
+ { value: 'alice', label: 'Alice' },
+ { value: 'bob', label: 'Bob' },
+ { value: 'charlie', label: 'Charlie' },
+])
+
// Toolbar "Add Task" button click event
const handleAddTask = () => {
console.log('Preparing to add task...')
@@ -552,6 +586,7 @@ Tasks can configure predecessors via the `predecessor` field, and the component
@@ -614,6 +649,12 @@ const tasks = ref([
},
])
+const assigneeOptions = ref([
+ { value: 'alice', label: 'Alice' },
+ { value: 'bob', label: 'Bob' },
+ { value: 'charlie', label: 'Charlie' },
+])
+
// Triggered when adding predecessor via context menu
const handlePredecessorAdded = (event: { targetTask: Task; newTask: Task }) => {
console.log(`Task [${event.targetTask.name}] added predecessor [${event.newTask.name}]`)
@@ -667,6 +708,7 @@ Suitable for scenarios requiring complete custom control bar:
:show-toolbar="false"
:use-default-drawer="true"
:use-default-milestone-dialog="true"
+ :assignee-options="assigneeOptions"
@add-task="handleAddTask"
@add-milestone="handleAddMilestone"
@task-added="handleTaskAdded"
@@ -682,6 +724,12 @@ import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
const tasks = ref([])
const milestones = ref([])
+const assigneeOptions = ref([
+ { value: 'alice', label: 'Alice' },
+ { value: 'bob', label: 'Bob' },
+ { value: 'charlie', label: 'Charlie' },
+])
+
// Custom button triggers event (component will respond and open built-in editor)
const triggerAddTask = () => {
// Directly trigger component's add-task event
@@ -715,6 +763,124 @@ 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
@@ -1133,6 +1299,54 @@ const handleDelete = () => {
This section details the configuration options and extension capabilities of the GanttChart component, including Component Configuration, Theme & Internationalization, and Custom Extensions.
+### Task Type Definition
+
+Task type (`type` field) is used to distinguish different types of tasks, and the component internally executes different logic based on the type.
+
+#### Built-in Task Types
+
+| Type | Description | Default |
+| ------- | ------------- | ------- |
+| `story` | User Story | - |
+| `task` | Regular Task | ✅ |
+| `bug` | Bug/Issue | - |
+
+#### Feature Differences
+
+Different task types have different functional characteristics in the component:
+
+| Feature | story | task | bug |
+| -------------------- | ----- | ---- | --- |
+| Can be parent task | ✅ | ✅ | ❌ |
+| Can be predecessor | ❌ | ✅ | ❌ |
+| Timer support | ❌ | ✅ | ✅ |
+| Auto parent task | ✅ | ❌ | ❌ |
+| Special delete hint | ✅ | ❌ | ❌ |
+
+#### Important Notes
+
+> ⚠️ **Important**
+>
+> 1. Task type values are used for internal component logic, **do not modify** these enum values arbitrarily
+> 2. When customizing TaskDrawer, you must maintain these three enum values: `story`, `task`, `bug`
+> 3. For additional business labels, use custom property fields such as: `customType`, `category`, `label`, etc.
+
+**Example: Using Custom Labels**
+
+```typescript
+const tasks = ref([
+ {
+ id: 1,
+ name: 'Requirements Analysis',
+ type: 'task', // Keep built-in component type
+ customType: 'requirement', // Custom business type
+ category: 'analysis', // Custom category
+ startDate: '2025-01-01',
+ endDate: '2025-01-10',
+ },
+])
+```
+
### Component Configuration
#### ToolbarConfig (Toolbar Configuration)
@@ -2084,5 +2298,5 @@ View the complete [Contributors list](./CONTRIBUTORS.md)。
---
- If this project helps you, please give it a ⭐️ to support it!
+ If this project helps you, please give it a ⭐️ to support it!
diff --git a/README.md b/README.md
index ad6e7fb..dfc8467 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,8 @@
中文 |
- English
+ English |
+ 更新日志
现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案
@@ -178,15 +179,17 @@ npm run dev
#### 基础属性
-| 属性名 | 类型 | 默认值 | 说明 |
-| --------------------------- | --------- | ------- | -------------------------------------------------------------- |
-| `tasks` | `Task[]` | `[]` | 任务数据数组 |
-| `milestones` | `Task[]` | `[]` | 里程碑数据数组(注意:类型为 Task[],需设置 type='milestone') |
-| `showToolbar` | `boolean` | `true` | 是否显示工具栏 |
-| `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) |
-| `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog) |
-| `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 |
-| `allowDragAndResize` | `boolean` | `true` | 是否允许拖拽和调整任务/里程碑大小 |
+| 属性名 | 类型 | 默认值 | 说明 |
+| --------------------------- | ----------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------- |
+| `tasks` | `Task[]` | `[]` | 任务数据数组 |
+| `milestones` | `Task[]` | `[]` | 里程碑数据数组(注意:类型为 Task[],需设置 type='milestone') |
+| `showToolbar` | `boolean` | `true` | 是否显示工具栏 |
+| `useDefaultDrawer` | `boolean` | `true` | 是否使用内置任务编辑抽屉(TaskDrawer) |
+| `useDefaultMilestoneDialog` | `boolean` | `true` | 是否使用内置里程碑编辑对话框(MilestoneDialog) |
+| `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 |
+| `allowDragAndResize` | `boolean` | `true` | 是否允许拖拽和调整任务/里程碑大小 |
+| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow |
+| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | 任务编辑抽屉中负责人下拉菜单的选项列表 |
#### 配置对象属性
@@ -241,13 +244,14 @@ 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:最简单的甘特图
```vue
-
+
@@ -265,6 +269,12 @@ const tasks = ref([
progress: 100,
},
])
+
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
+])
```
@@ -273,7 +283,7 @@ const tasks = ref([
```vue
-
+
@@ -301,6 +311,12 @@ const milestones = ref([
icon: 'diamond',
},
])
+
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
+])
```
@@ -321,6 +337,7 @@ const milestones = ref([
:tasks="tasks"
:milestones="milestones"
:show-toolbar="false"
+ :assignee-options="assigneeOptions"
@task-added="handleTaskAdded"
@milestone-saved="handleMilestoneSaved"
/>
@@ -336,6 +353,12 @@ import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
const tasks = ref([])
const milestones = ref([])
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
+])
+
const addTask = () => {
const newTask = {
id: Date.now(),
@@ -383,7 +406,8 @@ const handleMilestoneSaved = milestone => {
| `endDate` | `string` | - | - | 结束日期,格式:'YYYY-MM-DD' 或 'YYYY-MM-DD HH:mm' |
| `progress` | `number` | - | `0` | 任务进度,范围 0-100 |
| `predecessor` | `number[]` | - | - | 前置任务 ID 数组,标准格式:`[1, 2, 3]` **兼容格式**:也支持字符串 `'1,2,3'` 或字符串数组 `['1', '2', '3']`,组件会自动解析 |
-| `assignee` | `string` | - | - | 任务负责人 |
+| `assignee` | `string` | - | - | 任务负责人,用作负责人下拉菜单的值绑定 |
+| `assigneeName` | `string` | - | - | 任务负责人姓名,自动从绑定的数据集`assigneeOptions`中获取Label作为显示,如果需要自定义,可以在GanttChart回调事件`task-added`中自定义信息 |
| `avatar` | `string` | - | - | 任务负责人头像 URL |
| `estimatedHours` | `number` | - | - | 预估工时(小时) |
| `actualHours` | `number` | - | - | 实际工时(小时) |
@@ -422,6 +446,8 @@ const handleMilestoneSaved = milestone => {
| `taskBarConfig` | `TaskBarConfig` | `{}` | 任务条样式配置,详见 [TaskBarConfig 配置](#taskbarconfig-配置) |
| `taskListConfig` | `TaskListConfig` | `undefined` | 任务列表配置,详见 [TaskListConfig 配置](#tasklistconfig-配置) |
| `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 |
+| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow |
+| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | 任务编辑抽屉中负责人下拉菜单的选项列表 |
**配置说明**:
@@ -447,6 +473,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'=作为子任务 |
**数据同步说明**:
@@ -461,6 +488,7 @@ const handleMilestoneSaved = milestone => {
([
},
])
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
+])
+
// 工具栏"添加任务"按钮点击事件
const handleAddTask = () => {
console.log('准备新增任务...')
@@ -550,6 +584,7 @@ const handleTaskDragEnd = (task: Task) => {
@@ -612,6 +647,12 @@ const tasks = ref([
},
])
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
+])
+
// 通过右键菜单添加前置任务时触发
const handlePredecessorAdded = (event: { targetTask: Task; newTask: Task }) => {
console.log(`任务 [${event.targetTask.name}] 添加了前置任务 [${event.newTask.name}]`)
@@ -665,6 +706,7 @@ const handleSuccessorAdded = (event: { targetTask: Task; newTask: Task }) => {
:show-toolbar="false"
:use-default-drawer="true"
:use-default-milestone-dialog="true"
+ :assignee-options="assigneeOptions"
@add-task="handleAddTask"
@add-milestone="handleAddMilestone"
@task-added="handleTaskAdded"
@@ -680,6 +722,12 @@ import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
const tasks = ref([])
const milestones = ref([])
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
+])
+
// 自定义按钮触发事件(组件会响应并打开内置编辑器)
const triggerAddTask = () => {
// 直接触发组件的 add-task 事件
@@ -713,6 +761,124 @@ 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,也支持完全自定义编辑行为。
@@ -1124,6 +1290,54 @@ const handleDelete = () => {
本章节详细介绍 GanttChart 组件的配置选项和扩展能力,包括组件配置、主题与国际化、自定义扩展三个部分。
+### 任务类型定义
+
+任务类型(`type` 字段)用于区分不同类型的任务,组件内部会根据类型执行不同的逻辑判断。
+
+#### 内置任务类型
+
+| 类型值 | 说明 | 默认值 |
+| ------- | ---------- | ------ |
+| `story` | 用户故事 | - |
+| `task` | 普通任务 | ✅ |
+| `bug` | 缺陷/问题 | - |
+
+#### 功能区分
+
+不同任务类型在组件中具有不同的功能特性:
+
+| 功能 | story | task | bug |
+| ---------------- | ----- | ---- | --- |
+| 可作为上级任务 | ✅ | ✅ | ❌ |
+| 可作为前置任务 | ❌ | ✅ | ❌ |
+| 支持计时器 | ❌ | ✅ | ✅ |
+| 自动视为父任务 | ✅ | ❌ | ❌ |
+| 删除时特殊提示 | ✅ | ❌ | ❌ |
+
+#### 注意事项
+
+> ⚠️ **重要提示**
+>
+> 1. 任务类型值为组件内置判断使用,**请勿随意修改**这些枚举值
+> 2. 客制化 TaskDrawer 时,必须保持 `story`、`task`、`bug` 这三个枚举值
+> 3. 如需添加其他业务标签,建议使用自定义属性字段,例如:`customType`、`category`、`label` 等
+
+**示例:使用自定义标签**
+
+```typescript
+const tasks = ref([
+ {
+ id: 1,
+ name: '需求分析',
+ type: 'task', // 保持组件内置类型
+ customType: 'requirement', // 自定义业务类型
+ category: 'analysis', // 自定义分类
+ startDate: '2025-01-01',
+ endDate: '2025-01-10',
+ },
+])
+```
+
### 组件配置
#### ToolbarConfig(工具栏配置)
@@ -2075,5 +2289,5 @@ jordium-gantt-vue3/
---
- 如果这个项目对你有帮助,请给一个 ⭐️ 支持一下!
+ 如果这个项目对你有帮助,请给一个 ⭐️ 支持一下!
diff --git a/demo/App.vue b/demo/App.vue
index 17b505f..ea9b516 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,21 @@ const taskListConfig = computed(() => ({
// 控制是否允许拖拽和拉伸
const allowDragAndResize = ref(true)
+// 控制是否启用TaskRow拖拽移动
+const enableTaskRowMove = ref(true)
+
+// 指派人员选项列表
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
+ { value: 'zhaoliu', label: '赵六' },
+ { value: 'qianqi', label: '钱七' },
+ { key: 'user_sunba', value: 'sunba', label: '孙八' }, // 示例:使用自定义 key
+ { value: 'zhoujiu', label: '周九' },
+ { value: 'wushi', label: '吴十' },
+])
+
// TaskBar配置
const taskBarOptions = ref({
showAvatar: true,
@@ -236,6 +253,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 +334,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 +567,85 @@ 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 })
+
+ // ⚠️ 注意:组件内部已通过对象引用自动完成数据移动,TaskList 和 Timeline 自动同步
+ // 无需手动更新 tasks.value,因为移动操作直接修改了原始对象引用
+ // 如果需要触发响应式更新,可以使用: tasks.value = [...tasks.value]
+
+ // 调用后端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 })
+ // }
+}
@@ -907,6 +1009,17 @@ function taskDebug(item: any) {
{{ t.taskBarConfig.mistouch.allowDragOnClickHint }}
+
+
+
+
+ 启用TaskRow拖拽移动
+
+
+
+ 允许通过拖拽TaskRow来调整任务的层级和顺序
+
+
{{ t.taskBarConfig.mistouch.dragThreshold }}:
@@ -985,6 +1098,8 @@ function taskDebug(item: any) {
:working-hours="workingHoursConfig"
:use-default-milestone-dialog="true"
:allow-drag-and-resize="allowDragAndResize"
+ :enable-task-row-move="enableTaskRowMove"
+ :assignee-options="assigneeOptions"
:on-export-csv="handleCustomCsvExport"
:on-language-change="handleLanguageChange"
:on-theme-change="handleThemeChange"
@@ -1011,6 +1126,7 @@ function taskDebug(item: any) {
@task-deleted="handleTaskDeleteEvent"
@task-added="handleTaskAddEvent"
@task-updated="handleTaskUpdateEvent"
+ @task-row-moved="handleTaskRowMoved"
>
+interface Props {
+ visible?: boolean
+ title?: string
+ message?: string
+ confirmText?: string
+ cancelText?: string
+}
+
+withDefaults(defineProps(), {
+ visible: false,
+ title: '确认操作',
+ message: '',
+ confirmText: '确认',
+ cancelText: '取消',
+})
+
+const emit = defineEmits<{
+ confirm: []
+ cancel: []
+}>()
+
+const handleConfirm = () => {
+ emit('confirm')
+}
+
+const handleCancel = () => {
+ emit('cancel')
+}
+
+
+
+
+
+
+
+
+
diff --git a/demo/index.html b/demo/index.html
index 8155fa6..8759c09 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -4,7 +4,64 @@
- Jordium Gantt Vue3 Demo
+
+
+ Jordium Gantt Vue3 - Modern Gantt Chart Component for Vue 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/locales/en-US.json b/demo/locales/en-US.json
new file mode 100644
index 0000000..f34f1ad
--- /dev/null
+++ b/demo/locales/en-US.json
@@ -0,0 +1,24 @@
+{
+ "taskMoveConfirm": {
+ "title": "Confirm Task Move",
+ "confirmText": "Confirm",
+ "cancelText": "Cancel",
+ "messages": {
+ "moveAfter": "{draggedTaskType} [{draggedTaskName}] will be placed after {targetTaskType} [{targetTaskName}]. Continue?",
+ "moveAsChild": "Task [{draggedTaskName}] will be unlinked from {oldParentName} and linked to {newParentName}. Continue?",
+ "moveAsChildNoOldParent": "Task [{draggedTaskName}] will be linked to {newParentName}. Continue?",
+ "moveAsChildSameParent": "Task [{draggedTaskName}] will be moved to the first position under {newParentName}. Continue?",
+ "moveCanceled": "Task move canceled",
+ "moveSuccess": "Task moved successfully"
+ },
+ "taskTypes": {
+ "story": "Story",
+ "task": "Task"
+ },
+ "parentNames": {
+ "root": "Root",
+ "story": "Story [{name}]",
+ "task": "Task [{name}]"
+ }
+ }
+}
diff --git a/demo/locales/zh-CN.json b/demo/locales/zh-CN.json
new file mode 100644
index 0000000..205d91c
--- /dev/null
+++ b/demo/locales/zh-CN.json
@@ -0,0 +1,24 @@
+{
+ "taskMoveConfirm": {
+ "title": "确认移动任务",
+ "confirmText": "确认",
+ "cancelText": "取消",
+ "messages": {
+ "moveAfter": "{draggedTaskType}【{draggedTaskName}】将放置{targetTaskType}【{targetTaskName}】之后,确认是否继续?",
+ "moveAsChild": "任务【{draggedTaskName}】将解除与{oldParentName}的关系,并设置于{newParentName}的关系,确认是否继续?",
+ "moveAsChildNoOldParent": "任务【{draggedTaskName}】将设置于{newParentName}的关系,确认是否继续?",
+ "moveAsChildSameParent": "任务【{draggedTaskName}】将移动到{newParentName}的第一位,确认是否继续?",
+ "moveCanceled": "任务移动已取消",
+ "moveSuccess": "任务移动成功"
+ },
+ "taskTypes": {
+ "story": "需求",
+ "task": "任务"
+ },
+ "parentNames": {
+ "root": "根目录",
+ "story": "需求【{name}】",
+ "task": "任务【{name}】"
+ }
+ }
+}
diff --git a/demo/public/google1487963ebfd13afd.html b/demo/public/google1487963ebfd13afd.html
new file mode 100644
index 0000000..1894d1a
--- /dev/null
+++ b/demo/public/google1487963ebfd13afd.html
@@ -0,0 +1 @@
+google-site-verification: google1487963ebfd13afd.html
\ No newline at end of file
diff --git a/demo/public/robots.txt b/demo/public/robots.txt
new file mode 100644
index 0000000..a20ec64
--- /dev/null
+++ b/demo/public/robots.txt
@@ -0,0 +1,8 @@
+User-agent: *
+Allow: /
+
+# Sitemap location
+Sitemap: https://nelson820125.github.io/jordium-gantt-vue3/sitemap.xml
+
+# Crawl-delay for polite crawling
+Crawl-delay: 1
diff --git a/demo/public/sitemap.xml b/demo/public/sitemap.xml
new file mode 100644
index 0000000..c6f47ee
--- /dev/null
+++ b/demo/public/sitemap.xml
@@ -0,0 +1,9 @@
+
+
+
+ https://nelson820125.github.io/jordium-gantt-vue3/
+ 2025-12-11
+ weekly
+ 1.0
+
+
diff --git a/demo/useDemoLocale.ts b/demo/useDemoLocale.ts
new file mode 100644
index 0000000..2a75c72
--- /dev/null
+++ b/demo/useDemoLocale.ts
@@ -0,0 +1,89 @@
+import { ref, computed } from 'vue'
+import zhCN from './locales/zh-CN.json'
+import enUS from './locales/en-US.json'
+
+export type LocaleKey = 'zh-CN' | 'en-US'
+
+interface LocaleMessages {
+ taskMoveConfirm: {
+ title: string
+ confirmText: string
+ cancelText: string
+ messages: {
+ moveAfter: string
+ moveAsChild: string
+ moveAsChildNoOldParent: string
+ moveAsChildSameParent: string
+ moveCanceled: string
+ moveSuccess: string
+ }
+ taskTypes: {
+ story: string
+ task: string
+ }
+ parentNames: {
+ root: string
+ story: string
+ task: string
+ }
+ }
+}
+
+const localeMessages: Record = {
+ 'zh-CN': zhCN,
+ 'en-US': enUS,
+}
+
+const currentLocale = ref('zh-CN')
+
+export function useDemoLocale() {
+ const locale = computed(() => currentLocale.value)
+
+ const messages = computed(() => localeMessages[currentLocale.value])
+
+ const setLocale = (newLocale: LocaleKey) => {
+ currentLocale.value = newLocale
+ }
+
+ /**
+ * 格式化消息,替换占位符
+ */
+ const formatMessage = (template: string | undefined, params: Record) => {
+ if (!template) return ''
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
+ return params[key] || match
+ })
+ }
+
+ /**
+ * 获取任务类型名称
+ */
+ const getTaskTypeName = (type: string) => {
+ return messages.value.taskMoveConfirm.taskTypes[type as 'story' | 'task'] || type
+ }
+
+ /**
+ * 获取父任务名称
+ */
+ const getParentName = (parent: { type: string; name: string } | null) => {
+ if (!parent) {
+ return messages.value.taskMoveConfirm.parentNames.root
+ }
+
+ const template = messages.value.taskMoveConfirm.parentNames[parent.type as 'story' | 'task']
+ // 如果找不到模板,直接返回任务名称
+ if (!template) {
+ return parent.name
+ }
+ return formatMessage(template, { name: parent.name })
+ }
+
+ return {
+ locale,
+ messages,
+ setLocale,
+ formatMessage,
+ getTaskTypeName,
+ getParentName,
+ }
+}
diff --git a/demo/version-history.json b/demo/version-history.json
index 0c50962..70fa791 100644
--- a/demo/version-history.json
+++ b/demo/version-history.json
@@ -322,5 +322,45 @@
"Added: Large data loading demonstration",
"Fix: jspdf vulnerabilies"
]
+ },
+ {
+ "version": "1.4.4",
+ "date": "2025-12-06",
+ "notes": [
+ "新增:任务列表中任务项的移动拖放功能",
+ "特别感谢 jun201607@github的使用及反馈的宝贵意见 ",
+ "Added: Task item move and drag-and-drop functionality in the task list",
+ "Special thanks to jun201607@github for their valuable use and feedback "
+ ]
+ },
+ {
+ "version": "1.4.5",
+ "date": "2025-12-06",
+ "notes": [
+ "优化:任务列表中任务项的移动拖放功能",
+ "特别感谢 jun201607@github的使用及反馈的宝贵意见 ",
+ "Optimized: Task item move and drag-and-drop functionality in the task list",
+ "Special thanks to jun201607@github for their valuable use and feedback "
+ ]
+ },
+ {
+ "version": "1.4.6",
+ "date": "2025-12-09",
+ "notes": [
+ "修复:内置TaskDrawer中负责人列表可以外部初始化",
+ "修复:未设置startDate和endDate时,任务无法正确显示的问题",
+ "特别感谢 yue-xiaochuan & YQ6494@gitee的使用及反馈的宝贵意见 ",
+ "Fixed: The assignee list in the built-in TaskDrawer can be initialized externally",
+ "Fixed: The issue where tasks could not be displayed correctly when startDate and endDate were not set",
+ "Special thanks to yue-xiaochuan & YQ6494@gitee for their valuable use and feedback "
+ ]
+ },
+ {
+ "version": "1.4.6-patch.1",
+ "date": "2025-12-09",
+ "notes": [
+ "修复:Github编译错误",
+ "Fixed: The build errors on Github"
+ ]
}
]
diff --git a/docs/GOOGLE-SEARCH-CONSOLE-SETUP.md b/docs/GOOGLE-SEARCH-CONSOLE-SETUP.md
new file mode 100644
index 0000000..aebe82d
--- /dev/null
+++ b/docs/GOOGLE-SEARCH-CONSOLE-SETUP.md
@@ -0,0 +1,136 @@
+# Google Search Console Setup Guide
+
+## 提交到 Google Search Console 的步骤
+
+### 1. 访问 Google Search Console
+前往 [Google Search Console](https://search.google.com/search-console)
+
+### 2. 添加资源
+- 选择 "URL 前缀" 方式
+- 输入: `https://nelson820125.github.io/jordium-gantt-vue3/`
+
+### 3. 验证所有权
+
+#### 方法 A: HTML 文件验证(推荐)
+1. 下载 Google 提供的验证文件(例如: `google1234567890abcdef.html`)
+2. 将文件放到 `demo/public/` 目录下
+3. 重新构建并部署
+4. 在 Google Search Console 点击"验证"
+
+#### 方法 B: HTML 标签验证
+1. Google 会提供一个 meta 标签,例如:
+ ```html
+
+ ```
+2. 将此标签添加到 `demo/index.html` 的 `` 部分
+3. 重新构建并部署
+4. 在 Google Search Console 点击"验证"
+
+### 4. 提交 Sitemap
+验证成功后:
+1. 进入左侧菜单 "索引" > "站点地图"
+2. 输入 sitemap URL: `https://nelson820125.github.io/jordium-gantt-vue3/sitemap.xml`
+3. 点击"提交"
+
+### 5. 检查索引状态
+- 在 "概览" 中查看索引覆盖率
+- 在 "网址检查" 中测试具体页面
+- 通常需要几天到几周时间才能被完全索引
+
+## 文件清单
+
+以下文件已为 SEO 优化准备好:
+
+### ✅ 已创建的 SEO 文件:
+- `demo/index.html` - 包含完整的 meta 标签和结构化数据
+- `demo/public/robots.txt` - 搜索引擎爬虫配置
+- `demo/public/sitemap.xml` - 网站地图
+
+### 📋 SEO 特性:
+- ✅ Meta 标签(title, description, keywords)
+- ✅ Open Graph 标签(用于社交媒体分享)
+- ✅ Twitter Cards
+- ✅ Schema.org 结构化数据
+- ✅ Canonical URL
+- ✅ Sitemap
+- ✅ Robots.txt
+
+## 关键 URL
+
+### Sitemap 中包含的 URL:
+1. **Demo 地址**: https://nelson820125.github.io/jordium-gantt-vue3/
+
+> **重要说明**: Sitemap 只能包含与验证域名相同的 URL。外部链接(GitHub、NPM等)应该通过 HTML 的 ` ` 标签或页面内容来关联,不能放在 sitemap 中。
+
+### 相关链接(通过 HTML meta 标签关联):
+2. **仓库地址**: https://github.com/nelson820125/jordium-gantt-vue3
+3. **英文文档**: https://github.com/nelson820125/jordium-gantt-vue3/blob/master/README-EN.md
+4. **中文文档**: https://github.com/nelson820125/jordium-gantt-vue3/blob/master/README.md
+5. **NPM 包**: https://www.npmjs.com/package/jordium-gantt-vue3
+
+## 监控和优化
+
+### 使用 Google Search Console 监控:
+- 点击次数和展示次数
+- 平均排名
+- 索引覆盖率
+- 移动设备可用性
+- Core Web Vitals
+
+### 建议的关键词:
+- vue3 gantt chart
+- vue gantt component
+- typescript gantt
+- project management vue
+- task scheduler vue
+- gantt chart component
+- vue3 timeline
+- interactive gantt
+
+## 常见问题
+
+### ❌ "无法读取此站点地图" 错误
+**原因**: Sitemap 包含了外部域名的 URL(如 github.com、npmjs.com)
+
+**解决方案**:
+- Sitemap **只能包含验证域名下的页面**
+- 对于 `nelson820125.github.io/jordium-gantt-vue3/`,只能包含该域名的页面
+- 外部链接应该通过 HTML 的 meta 标签、结构化数据或页面内容来关联
+- ✅ 已修复:当前 sitemap 只包含 demo 地址本身
+
+### ✅ 如何关联外部资源
+在 `index.html` 中已通过以下方式关联:
+- Schema.org 结构化数据中的 `codeRepository` 属性
+- Open Graph 和 Twitter Cards 元标签
+- Canonical URL
+
+## 注意事项
+
+1. **Sitemap 域名限制**
+ - ⚠️ Sitemap 只能包含当前验证域名下的 URL
+ - ❌ 不能包含 github.com、npmjs.com 等外部域名
+ - ✅ 只包含 nelson820125.github.io/jordium-gantt-vue3/ 下的页面
+
+2. **构建时确保文件复制**
+ - 确保 `robots.txt` 和 `sitemap.xml` 被复制到构建输出目录
+ - 如果使用 Vite,这些文件在 `public` 目录中会自动复制
+
+3. **更新频率**
+ - 每次发布新版本后,更新 `sitemap.xml` 中的 `` 日期
+ - 在 Google Search Console 中重新提交 sitemap
+
+4. **监控 404 错误**
+ - 定期检查 Search Console 的 "覆盖率" 报告
+ - 修复任何爬取错误
+
+## 验证 SEO 设置
+
+在浏览器中测试以下 URL:
+- https://nelson820125.github.io/jordium-gantt-vue3/
+- https://nelson820125.github.io/jordium-gantt-vue3/robots.txt
+- https://nelson820125.github.io/jordium-gantt-vue3/sitemap.xml
+
+使用工具验证:
+- [Google Rich Results Test](https://search.google.com/test/rich-results)
+- [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/)
+- [Twitter Card Validator](https://cards-dev.twitter.com/validator)
diff --git a/npm-demo/src/components/GanttTest.vue b/npm-demo/src/components/GanttTest.vue
index ab18df6..5d6dc1a 100644
--- a/npm-demo/src/components/GanttTest.vue
+++ b/npm-demo/src/components/GanttTest.vue
@@ -13,7 +13,7 @@ const tasks = ref([
department: '管理部',
departmentCode: 'D001',
type: 'task',
- },
+ }
])
const milestones = ref([
@@ -22,41 +22,61 @@ const milestones = ref([
name: '项目立项',
startDate: '2025-10-29',
type: 'milestone',
- icon: 'diamond',
- },
+ icon: 'diamond'
+ }
])
const customMessages = {
'zh-CN': {
department: '部门',
departmentCode: '部门编号',
+ // 其他文本
+ days: '天111',
+ hours: '小时111',
+ overtime: '超时111',
+ overdue: '逾期111',
},
'en-US': {
department: 'Department',
departmentCode: 'Department Code',
- },
+ // 其他文本
+ days: 'Day111',
+ hours: 'Hour111',
+ overtime: 'OverTime111',
+ overdue: 'OverDue111',
+ }
}
// const tasks = ref([])
// const milestones = ref([])
-const showAddTaskDrawer = ref(false)
-const showAddMilestoneDialog = ref(false)
+
+const showAddTaskDrawer = ref(false);
+const showAddMilestoneDialog = ref(false);
// 定义可动态配置的列
const availableColumns = ref([
{ key: 'startDate', label: '开始日期', visible: true },
{ key: 'endDate', label: '结束日期', visible: true },
{ key: 'progress', label: '进度', visible: true },
- { key: 'department', label: '部门', visible: true, width: 120 },
+ { key: 'department', label: '部门', visible: true, width: 200 },
{ key: 'departmentCode', label: '部门编号', visible: true },
+ { key: 'assigneeName', label: '负责人', visible: true },
+])
+
+// 使用内置TaskDrawer组件的时候,可以传入负责人选项
+// 请先设置GanttChart组件属性use-default-drawer="true"
+const assigneeOptions = ref([
+ { value: 'zhangsan', label: '张三' },
+ { value: 'lisi', label: '李四' },
+ { value: 'wangwu', label: '王五' },
])
// TaskList宽度配置示例
const taskListConfig = {
- defaultWidth: 400, // 默认展开宽度400px(默认320px)
- minWidth: 300, // 最小宽度300px(默认280px)
- maxWidth: 1200, // 最大宽度1200px(默认1160px)
- columns: availableColumns.value,
+ defaultWidth: '50%', // 默认展开宽度50%
+ minWidth: '300px', // 最小宽度300px(默认280px)
+ maxWidth: '1200px', // 最大宽度1200px(默认1160px)
+ columns: availableColumns.value
}
// toolbar配置示例
@@ -71,17 +91,18 @@ const toolbarConfig: ToolbarConfig = {
showFullscreen: true, // 显示全屏按钮
showTimeScale: true, // 显示时间刻度按钮组
timeScaleDimensions: [ // 显示所有时间刻度维度
- 'hour', 'day', 'week', 'month', 'quarter', 'year',
+ 'hour', 'day', 'week', 'month', 'quarter', 'year'
],
defaultTimeScale: 'week', // 默认选中周视图
- showExpandCollapse: false, // 显示展开/折叠按钮
+ showExpandCollapse: false // 显示展开/折叠按钮
}
+
const newTask = ref({
name: '',
startDate: '',
- endDate: '',
-})
+ endDate: ''
+});
const addTask = () => {
tasks.value.push({
@@ -90,10 +111,10 @@ const addTask = () => {
startDate: newTask.value.startDate,
endDate: newTask.value.endDate,
progress: 0,
- })
- newTask.value = { name: '', startDate: '', endDate: '' }
- showAddTaskDrawer.value = false
-}
+ });
+ newTask.value = { name: '', startDate: '', endDate: '' };
+ showAddTaskDrawer.value = false;
+};
const addMilestone = () => {
milestones.value.push({
@@ -103,18 +124,76 @@ const addMilestone = () => {
progress: 0,
type: 'milestone',
icon: 'diamond',
- })
- console.log('milestones: ', milestones.value)
- newTask.value = { name: '', startDate: '', endDate: '' }
- showAddMilestoneDialog.value = false
+ });
+ newTask.value = { name: '', startDate: '', endDate: '' };
+ showAddMilestoneDialog.value = false;
}
const onTaskDblclick = (task) => {
alert(`双击任务: ${task.name}`)
}
+const onTaskClick = (task) => {
+ alert(`单击任务: ${task.name}`)
+}
const onMilestoneDblclick = (milestone) => {
alert(`双击里程碑: ${milestone.name}`)
}
+
+// 任务行拖拽完成事件(可选)
+const handleTaskRowMoved = async (payload: {
+ draggedTask: Task
+ targetTask: Task
+ position: 'after' | 'child'
+ oldParent: Task | null
+ newParent: Task | null
+}) => {
+ const { draggedTask, targetTask, position, oldParent, newParent } = payload
+
+ // 组件已自动完成任务移动、parentId更新和TaskList/Timeline同步
+ // 监听此事件为完全可选,仅用于:
+
+ // 1. 显示自定义提示消息
+ const oldParentName = oldParent?.name || '根目录'
+ const newParentName = newParent?.name || '根目录'
+ const positionText = position === 'after' ? '在目标任务之后' : '作为目标任务的子任务'
+ alert(`任务 [${draggedTask.name}] 已从 [${oldParentName}] 移动到 [${newParentName}] (${positionText})`, 'success')
+
+ // 2. 调用后端 API 保存新的任务层级关系
+ try {
+ await api.updateTaskHierarchy({
+ taskId: draggedTask.id,
+ targetTaskId: targetTask.id,
+ position: position,
+ oldParentId: oldParent?.id,
+ newParentId: newParent?.id,
+ })
+ } catch (error) {
+ console.error('保存任务层级失败:', error)
+ alert('保存失败,请刷新页面')
+ }
+
+ // 3. 触发其他业务逻辑(如更新关联数据、记录操作日志等)
+ // ...
+}
+
+// 任务添加后回调
+const onTaskAdded = (res) => {
+ const addedTask = tasks.value.find(t => t.id === res.task.id);
+ if (addedTask) {
+ // 使用addedTask.assignee去查找assigneeOptions的label进行赋值
+ const assigneeOption = assigneeOptions.value.find(option => option.value === addedTask.assignee);
+ if (assigneeOption) {
+ addedTask.assigneeName = assigneeOption.label;
+ }
+ } else {
+ // 使用addedTask.assignee去查找assigneeOptions的label进行赋值
+ const assigneeOption = assigneeOptions.value.find(option => option.value === res.task.assignee);
+ if (assigneeOption) {
+ res.task.assigneeName = assigneeOption.label;
+ }
+ tasks.value.push(res.task);
+ }
+};
@@ -128,13 +207,23 @@ const onMilestoneDblclick = (milestone) => {
:use-default-drawer="false"
:use-default-milestone-dialog="false"
:locale-messages="customMessages"
- @add-task="showAddTaskDrawer = true"
+ :allow-drag-and-resize="true"
+ :enable-task-row-move="true"
+ :assignee-options="assigneeOptions"
+ @task-row-moved="handleTaskRowMoved"
@add-milestone="showAddMilestoneDialog = true"
@task-double-click="onTaskDblclick"
+ @task-click="onTaskClick"
@milestone-double-click="onMilestoneDblclick"
+ @task-added="onTaskAdded"
>
+
+
+ 添加任务
+ 添加里程碑
+
@@ -170,8 +259,8 @@ const onMilestoneDblclick = (milestone) => {
diff --git a/npm-webpack-demo/src/App.vue b/npm-webpack-demo/src/App.vue
index fc297cf..1e226b4 100644
--- a/npm-webpack-demo/src/App.vue
+++ b/npm-webpack-demo/src/App.vue
@@ -1,150 +1,3 @@
-
-
@@ -157,11 +10,15 @@ const onMilestoneDblclick = (milestone: any) => {
:use-default-milestone-dialog="false"
:locale-messages="customMessages"
:allow-drag-and-resize="true"
+ :enable-task-row-move="true"
+ :assignee-options="assigneeOptions"
+ @task-row-moved="handleTaskRowMoved"
@add-task="showAddTaskDrawer = true"
@add-milestone="showAddMilestoneDialog = true"
@task-double-click="onTaskDblclick"
@task-click="onTaskClick"
@milestone-double-click="onMilestoneDblclick"
+ @task-added="onTaskAdded"
>
@@ -205,8 +62,8 @@ const onMilestoneDblclick = (milestone: any) => {
@@ -230,6 +87,212 @@ const onMilestoneDblclick = (milestone: any) => {
+
+
diff --git a/src/composables/useTaskRowDrag.ts b/src/composables/useTaskRowDrag.ts
new file mode 100644
index 0000000..c468a7b
--- /dev/null
+++ b/src/composables/useTaskRowDrag.ts
@@ -0,0 +1,263 @@
+import { ref, onMounted, onUnmounted } from 'vue'
+import type { Task } from '../models/classes/Task'
+
+interface DragState {
+ isDragging: boolean
+ draggedTask: Task | null
+ draggedElement: HTMLElement | null
+ ghostElement: HTMLElement | null
+ dropTargetTask: Task | null
+ dropPosition: 'after' | 'child' | null
+}
+
+interface UseDragOptions {
+ enabled: boolean
+ onDragStart?: (task: Task) => void
+ onDragOver?: (task: Task, position: 'after' | 'child') => void
+ onDrop?: (draggedTask: Task, targetTask: Task, position: 'after' | 'child') => void
+}
+
+export function useTaskRowDrag(options: UseDragOptions) {
+ const dragState = ref({
+ isDragging: false,
+ draggedTask: null,
+ draggedElement: null,
+ ghostElement: null,
+ dropTargetTask: null,
+ dropPosition: null,
+ })
+
+ const startDrag = (task: Task, element: HTMLElement, event: MouseEvent) => {
+ if (!options.enabled) return
+
+ // 防止里程碑分组、里程碑被拖拽
+ if (task.type === 'milestone-group' || task.type === 'milestone') {
+ return
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ dragState.value.isDragging = true
+ dragState.value.draggedTask = task
+ dragState.value.draggedElement = element
+
+ // 创建拖拽的幽灵元素
+ const ghost = element.cloneNode(true) as HTMLElement
+ ghost.style.position = 'fixed'
+ ghost.style.left = '-9999px'
+ ghost.style.top = '-9999px'
+ ghost.style.width = `${element.offsetWidth}px`
+ ghost.style.height = `${element.offsetHeight}px`
+ ghost.style.opacity = '0.6'
+ ghost.style.pointerEvents = 'none'
+ ghost.style.zIndex = '999999'
+ ghost.style.boxShadow = '0 8px 24px rgba(0, 0, 0, 0.3)'
+ ghost.classList.add('task-row-dragging')
+
+ // 查找全屏容器或使用body
+ const fullscreenContainer = document.querySelector('.gantt-fullscreen') || document.body
+ fullscreenContainer.appendChild(ghost)
+ dragState.value.ghostElement = ghost
+
+ // 设置原始元素为半透明
+ element.style.opacity = '0.4'
+
+ // 禁用页面选择
+ document.body.style.userSelect = 'none'
+ document.body.style.webkitUserSelect = 'none'
+ document.body.style.cursor = 'move'
+
+ // 广播拖拽开始事件
+ window.dispatchEvent(
+ new CustomEvent('task-row-drag-start', {
+ detail: { task },
+ }),
+ )
+
+ if (options.onDragStart) {
+ options.onDragStart(task)
+ }
+
+ // 初始定位幽灵元素
+ updateGhostPosition(event)
+ }
+
+ const updateGhostPosition = (event: MouseEvent) => {
+ if (dragState.value.ghostElement) {
+ const ghost = dragState.value.ghostElement
+ ghost.style.left = `${event.clientX + 10}px`
+ ghost.style.top = `${event.clientY - 25}px`
+ }
+ }
+
+ const handleDragOver = (task: Task, element: HTMLElement, event: MouseEvent) => {
+ if (!dragState.value.isDragging || !dragState.value.draggedTask) return
+
+ // 不能拖拽到自己身上
+ if (task.id === dragState.value.draggedTask.id) {
+ clearDropTarget()
+ return
+ }
+
+ // 不能拖拽到自己的子任务上
+ if (isDescendant(dragState.value.draggedTask, task)) {
+ clearDropTarget()
+ return
+ }
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ // 确定放置位置:根据是否有子任务
+ const hasChildren = task.children && task.children.length > 0
+ const position: 'after' | 'child' = hasChildren ? 'child' : 'after'
+
+ dragState.value.dropTargetTask = task
+ dragState.value.dropPosition = position
+
+ // 移除之前的高亮
+ document.querySelectorAll('.task-row-drop-target').forEach(el => {
+ el.classList.remove('task-row-drop-target', 'drop-after', 'drop-child')
+ })
+
+ // 添加新的高亮
+ element.classList.add('task-row-drop-target')
+ if (position === 'after') {
+ element.classList.add('drop-after')
+ } else {
+ element.classList.add('drop-child')
+ }
+
+ if (options.onDragOver) {
+ options.onDragOver(task, position)
+ }
+ }
+
+ const clearDropTarget = () => {
+ dragState.value.dropTargetTask = null
+ dragState.value.dropPosition = null
+ document.querySelectorAll('.task-row-drop-target').forEach(el => {
+ el.classList.remove('task-row-drop-target', 'drop-after', 'drop-child')
+ })
+ }
+
+ const endDrag = () => {
+ if (!dragState.value.isDragging) return
+
+ const draggedTask = dragState.value.draggedTask
+ const dropTargetTask = dragState.value.dropTargetTask
+ const dropPosition = dragState.value.dropPosition
+
+ // 恢复原始元素样式
+ if (dragState.value.draggedElement) {
+ dragState.value.draggedElement.style.opacity = ''
+ }
+
+ // 移除幽灵元素
+ if (dragState.value.ghostElement && dragState.value.ghostElement.parentElement) {
+ dragState.value.ghostElement.parentElement.removeChild(dragState.value.ghostElement)
+ }
+
+ // 清除高亮
+ clearDropTarget()
+
+ // 恢复页面选择
+ document.body.style.userSelect = ''
+ document.body.style.webkitUserSelect = ''
+ document.body.style.cursor = ''
+
+ // 广播拖拽结束事件
+ window.dispatchEvent(new CustomEvent('task-row-drag-end'))
+
+ // 如果有有效的放置目标,触发drop回调
+ if (draggedTask && dropTargetTask && dropPosition && options.onDrop) {
+ options.onDrop(draggedTask, dropTargetTask, dropPosition)
+ }
+
+ // 重置状态
+ dragState.value = {
+ isDragging: false,
+ draggedTask: null,
+ draggedElement: null,
+ ghostElement: null,
+ dropTargetTask: null,
+ dropPosition: null,
+ }
+ }
+
+ const handleMouseMove = (event: MouseEvent) => {
+ if (dragState.value.isDragging) {
+ updateGhostPosition(event)
+
+ // 临时隐藏幽灵元素以检测下方元素
+ const ghost = dragState.value.ghostElement
+ if (ghost) {
+ ghost.style.display = 'none'
+ }
+
+ // 检测鼠标下的元素
+ const elementUnderMouse = document.elementFromPoint(event.clientX, event.clientY)
+
+ // 恢复幽灵元素显示
+ if (ghost) {
+ ghost.style.display = ''
+ }
+
+ if (elementUnderMouse) {
+ // 查找最近的task-row元素
+ const taskRow = elementUnderMouse.closest('.task-row') as HTMLElement
+ if (taskRow && taskRow.dataset.taskId) {
+ const taskId = Number(taskRow.dataset.taskId)
+
+ // 触发全局事件,让TaskRow组件处理
+ window.dispatchEvent(
+ new CustomEvent('task-row-drag-over', {
+ detail: { taskId, event },
+ }),
+ )
+ }
+ }
+ }
+ }
+
+ const handleMouseUp = () => {
+ if (dragState.value.isDragging) {
+ endDrag()
+ }
+ }
+
+ // 检查task2是否是task1的后代
+ const isDescendant = (ancestor: Task, descendant: Task): boolean => {
+ if (!ancestor.children) return false
+
+ for (const child of ancestor.children) {
+ if (child.id === descendant.id) return true
+ if (isDescendant(child, descendant)) return true
+ }
+
+ return false
+ }
+
+ onMounted(() => {
+ document.addEventListener('mousemove', handleMouseMove)
+ document.addEventListener('mouseup', handleMouseUp)
+ })
+
+ onUnmounted(() => {
+ document.removeEventListener('mousemove', handleMouseMove)
+ document.removeEventListener('mouseup', handleMouseUp)
+
+ // 清理可能残留的幽灵元素
+ if (dragState.value.ghostElement && dragState.value.ghostElement.parentElement) {
+ dragState.value.ghostElement.parentElement.removeChild(dragState.value.ghostElement)
+ }
+ })
+
+ return {
+ dragState,
+ startDrag,
+ handleDragOver,
+ endDrag,
+ }
+}
diff --git a/src/models/classes/Task.ts b/src/models/classes/Task.ts
index 5000ec6..4e4f8a2 100644
--- a/src/models/classes/Task.ts
+++ b/src/models/classes/Task.ts
@@ -3,7 +3,8 @@ export interface Task {
id: number
name: string
predecessor?: number[] // 前置任务ID数组
- assignee?: string
+ assignee?: string // 记录唯一键值,如用户ID或用户名
+ assigneeName?: string // 任务负责人名称
avatar?: string // 任务负责人头像URL
startDate?: string
endDate?: string
diff --git a/src/utils/taskTreeUtils.ts b/src/utils/taskTreeUtils.ts
new file mode 100644
index 0000000..2a4a63a
--- /dev/null
+++ b/src/utils/taskTreeUtils.ts
@@ -0,0 +1,137 @@
+import type { Task } from '../models/classes/Task'
+
+/**
+ * 任务树操作工具函数
+ */
+
+/**
+ * 从任务树中查找任务的父任务
+ */
+export function findTaskParent(
+ taskList: Task[],
+ targetId: number,
+ parent: Task | null = null,
+): Task | null {
+ for (const task of taskList) {
+ if (task.id === targetId) {
+ return parent
+ }
+ if (task.children && task.children.length > 0) {
+ const found = findTaskParent(task.children, targetId, task)
+ if (found !== null) return found
+ }
+ }
+ return null
+}
+
+/**
+ * 从任务树中移除指定任务
+ * @returns 被移除的任务,如果未找到则返回 null
+ */
+export function removeTaskFromTree(taskList: Task[], taskId: number): Task | null {
+ for (let i = 0; i < taskList.length; i++) {
+ if (taskList[i].id === taskId) {
+ return taskList.splice(i, 1)[0]
+ }
+ if (taskList[i].children && taskList[i].children!.length > 0) {
+ const removed = removeTaskFromTree(taskList[i].children!, taskId)
+ if (removed) return removed
+ }
+ }
+ return null
+}
+
+/**
+ * 将任务插入到任务树的指定位置
+ * @param taskList 任务列表
+ * @param targetId 目标任务ID
+ * @param taskToInsert 要插入的任务
+ * @param position 插入位置:'after' 表示在目标任务之后,'child' 表示作为目标任务的子任务
+ * @returns 是否插入成功
+ */
+export function insertTaskInTree(
+ taskList: Task[],
+ targetId: number,
+ taskToInsert: Task,
+ position: 'after' | 'child',
+): boolean {
+ for (let i = 0; i < taskList.length; i++) {
+ if (taskList[i].id === targetId) {
+ if (position === 'after') {
+ // 算法#1: 放置在目标任务之后(同级)
+ // 更新 parentId 为目标任务的 parentId
+ taskToInsert.parentId = taskList[i].parentId
+ taskList.splice(i + 1, 0, taskToInsert)
+ } else {
+ // 算法#2: 作为子任务插入(第一个子任务位置)
+ // 重要:里程碑组(milestone-group)不能添加子任务,只能在其后插入
+ if (taskList[i].type === 'milestone-group') {
+ // 降级为 'after' 逻辑
+ taskToInsert.parentId = taskList[i].parentId
+ taskList.splice(i + 1, 0, taskToInsert)
+ } else {
+ // 更新 parentId 为目标任务的 id
+ taskToInsert.parentId = targetId
+ if (!taskList[i].children) {
+ taskList[i].children = []
+ }
+ taskList[i].children!.unshift(taskToInsert)
+ }
+ }
+ return true
+ }
+ if (taskList[i].children && taskList[i].children!.length > 0) {
+ if (insertTaskInTree(taskList[i].children!, targetId, taskToInsert, position)) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+/**
+ * 执行任务移动操作(直接修改传入的 taskList)
+ * @returns 移动信息,如果移动失败则返回 null
+ */
+export function moveTask(
+ taskList: Task[],
+ draggedTaskId: number,
+ targetTaskId: number,
+ position: 'after' | 'child',
+): {
+ movedTask: Task
+ oldParent: Task | null
+ newParent: Task | null
+} | null {
+ // 1. 记录原始父任务
+ const oldParent = findTaskParent(taskList, draggedTaskId)
+
+ // 2. 从原位置移除任务
+ const removedTask = removeTaskFromTree(taskList, draggedTaskId)
+ if (!removedTask) {
+ return null
+ }
+
+ // 3. 插入到新位置
+ const inserted = insertTaskInTree(taskList, targetTaskId, removedTask, position)
+ if (!inserted) {
+ // 插入失败,需要恢复任务到原位置
+ // 如果有父任务,恢复到父任务的 children 中
+ if (oldParent && oldParent.children) {
+ oldParent.children.push(removedTask)
+ } else {
+ // 否则恢复到根级别
+ taskList.push(removedTask)
+ }
+ return null
+ }
+
+ // 4. 计算新的父任务
+ const newParent = findTaskParent(taskList, draggedTaskId)
+
+ return {
+ movedTask: removedTask,
+ oldParent,
+ newParent,
+ }
+}