merge src/master
This commit is contained in:
@@ -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
|
||||
|
||||
+220
-6
@@ -23,7 +23,8 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">中文</a> |
|
||||
<a href="./README-EN.md">English</a>
|
||||
<a href="./README-EN.md">English</a> |
|
||||
<a href="./CHANGELOG.md">Release Notes</a>
|
||||
</p>
|
||||
|
||||
<p align="center">A modern Vue 3 Gantt chart component library providing complete solutions for project management and task scheduling</p>
|
||||
@@ -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
|
||||
<template>
|
||||
<div style="height: 600px;">
|
||||
<GanttChart :tasks="tasks" />
|
||||
<GanttChart :tasks="tasks" :assignee-options="assigneeOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -267,6 +271,12 @@ const tasks = ref([
|
||||
progress: 100,
|
||||
},
|
||||
])
|
||||
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'alice', label: 'Alice' },
|
||||
{ value: 'bob', label: 'Bob' },
|
||||
{ value: 'charlie', label: 'Charlie' },
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -275,7 +285,7 @@ const tasks = ref([
|
||||
```vue
|
||||
<template>
|
||||
<div style="height: 600px;">
|
||||
<GanttChart :tasks="tasks" :milestones="milestones" />
|
||||
<GanttChart :tasks="tasks" :milestones="milestones" :assignee-options="assigneeOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -303,6 +313,12 @@ const milestones = ref([
|
||||
icon: 'diamond',
|
||||
},
|
||||
])
|
||||
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'alice', label: 'Alice' },
|
||||
{ value: 'bob', label: 'Bob' },
|
||||
{ value: 'charlie', label: 'Charlie' },
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -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]`<br/>**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
|
||||
<div style="height: 600px;">
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:assignee-options="assigneeOptions"
|
||||
@add-task="handleAddTask"
|
||||
@task-added="handleTaskAdded"
|
||||
@task-updated="handleTaskUpdated"
|
||||
@@ -500,6 +528,12 @@ const tasks = ref<Task[]>([
|
||||
},
|
||||
])
|
||||
|
||||
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
|
||||
<template>
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:assignee-options="assigneeOptions"
|
||||
@predecessor-added="handlePredecessorAdded"
|
||||
@successor-added="handleSuccessorAdded"
|
||||
/>
|
||||
@@ -614,6 +649,12 @@ const tasks = ref<Task[]>([
|
||||
},
|
||||
])
|
||||
|
||||
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
|
||||
<template>
|
||||
<div style="height: 600px;">
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:enable-task-row-move="true"
|
||||
:assignee-options="assigneeOptions"
|
||||
@task-row-moved="handleTaskRowMoved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { GanttChart } from 'jordium-gantt-vue3'
|
||||
import type { Task } from 'jordium-gantt-vue3'
|
||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||
|
||||
const tasks = ref<Task[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Project Planning',
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-01-10',
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Requirements Analysis',
|
||||
startDate: '2025-01-11',
|
||||
endDate: '2025-01-20',
|
||||
progress: 60,
|
||||
parentId: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'System Design',
|
||||
startDate: '2025-01-21',
|
||||
endDate: '2025-01-30',
|
||||
progress: 40,
|
||||
},
|
||||
])
|
||||
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'alice', label: 'Alice' },
|
||||
{ value: 'bob', label: 'Bob' },
|
||||
{ value: 'charlie', label: 'Charlie' },
|
||||
])
|
||||
|
||||
// Task row drag completed event (optional)
|
||||
const handleTaskRowMoved = async (payload: {
|
||||
draggedTask: Task
|
||||
targetTask: Task
|
||||
position: 'after' | 'child'
|
||||
oldParent: Task | null
|
||||
newParent: Task | null
|
||||
}) => {
|
||||
const { draggedTask, targetTask, position, oldParent, newParent } = payload
|
||||
|
||||
// Component has automatically completed task move, parentId update and TaskList/Timeline sync
|
||||
// Listening to this event is completely optional, only for:
|
||||
|
||||
// 1. Show custom notification message
|
||||
const oldParentName = oldParent?.name || 'Root'
|
||||
const newParentName = newParent?.name || 'Root'
|
||||
const positionText = position === 'after' ? 'after target task' : 'as child of target task'
|
||||
showMessage(`Task [${draggedTask.name}] moved from [${oldParentName}] to [${newParentName}] (${positionText})`, 'success')
|
||||
|
||||
// 2. Call backend API to save new task hierarchy
|
||||
try {
|
||||
await api.updateTaskHierarchy({
|
||||
taskId: draggedTask.id,
|
||||
targetTaskId: targetTask.id,
|
||||
position: position,
|
||||
oldParentId: oldParent?.id,
|
||||
newParentId: newParent?.id,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Save task hierarchy failed:', error)
|
||||
showMessage('Save failed, please refresh page', 'error')
|
||||
}
|
||||
|
||||
// 3. Trigger other business logic (like updating related data, recording operation logs, etc.)
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**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)。
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>If this project helps you, please give it a ⭐️ to support it!</sub>
|
||||
If this project helps you, please give it a ⭐️ to support it!
|
||||
</p>
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">中文</a> |
|
||||
<a href="./README-EN.md">English</a>
|
||||
<a href="./README-EN.md">English</a> |
|
||||
<a href="./CHANGELOG.md">更新日志</a>
|
||||
</p>
|
||||
|
||||
<p align="center">现代化的 Vue 3 甘特图组件库,为项目管理和任务调度提供完整解决方案</p>
|
||||
@@ -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
|
||||
<template>
|
||||
<div style="height: 600px;">
|
||||
<GanttChart :tasks="tasks" />
|
||||
<GanttChart :tasks="tasks" :assignee-options="assigneeOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -265,6 +269,12 @@ const tasks = ref([
|
||||
progress: 100,
|
||||
},
|
||||
])
|
||||
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'zhangsan', label: '张三' },
|
||||
{ value: 'lisi', label: '李四' },
|
||||
{ value: 'wangwu', label: '王五' },
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -273,7 +283,7 @@ const tasks = ref([
|
||||
```vue
|
||||
<template>
|
||||
<div style="height: 600px;">
|
||||
<GanttChart :tasks="tasks" :milestones="milestones" />
|
||||
<GanttChart :tasks="tasks" :milestones="milestones" :assignee-options="assigneeOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -301,6 +311,12 @@ const milestones = ref([
|
||||
icon: 'diamond',
|
||||
},
|
||||
])
|
||||
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'zhangsan', label: '张三' },
|
||||
{ value: 'lisi', label: '李四' },
|
||||
{ value: 'wangwu', label: '王五' },
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -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]`<br/>**兼容格式**:也支持字符串 `'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 => {
|
||||
<div style="height: 600px;">
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:assignee-options="assigneeOptions"
|
||||
@add-task="handleAddTask"
|
||||
@task-added="handleTaskAdded"
|
||||
@task-updated="handleTaskUpdated"
|
||||
@@ -498,6 +526,12 @@ const tasks = ref<Task[]>([
|
||||
},
|
||||
])
|
||||
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'zhangsan', label: '张三' },
|
||||
{ value: 'lisi', label: '李四' },
|
||||
{ value: 'wangwu', label: '王五' },
|
||||
])
|
||||
|
||||
// 工具栏"添加任务"按钮点击事件
|
||||
const handleAddTask = () => {
|
||||
console.log('准备新增任务...')
|
||||
@@ -550,6 +584,7 @@ const handleTaskDragEnd = (task: Task) => {
|
||||
<template>
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:assignee-options="assigneeOptions"
|
||||
@predecessor-added="handlePredecessorAdded"
|
||||
@successor-added="handleSuccessorAdded"
|
||||
/>
|
||||
@@ -612,6 +647,12 @@ const tasks = ref<Task[]>([
|
||||
},
|
||||
])
|
||||
|
||||
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
|
||||
<template>
|
||||
<div style="height: 600px;">
|
||||
<GanttChart
|
||||
:tasks="tasks"
|
||||
:enable-task-row-move="true"
|
||||
:assignee-options="assigneeOptions"
|
||||
@task-row-moved="handleTaskRowMoved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { GanttChart } from 'jordium-gantt-vue3'
|
||||
import type { Task } from 'jordium-gantt-vue3'
|
||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||
|
||||
const tasks = ref<Task[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: '项目规划',
|
||||
startDate: '2025-01-01',
|
||||
endDate: '2025-01-10',
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '需求分析',
|
||||
startDate: '2025-01-11',
|
||||
endDate: '2025-01-20',
|
||||
progress: 60,
|
||||
parentId: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '系统设计',
|
||||
startDate: '2025-01-21',
|
||||
endDate: '2025-01-30',
|
||||
progress: 40,
|
||||
},
|
||||
])
|
||||
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'zhangsan', label: '张三' },
|
||||
{ value: 'lisi', label: '李四' },
|
||||
{ value: 'wangwu', label: '王五' },
|
||||
])
|
||||
|
||||
// 任务行拖拽完成事件(可选)
|
||||
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' ? '在目标任务之后' : '作为目标任务的子任务'
|
||||
showMessage(`任务 [${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)
|
||||
showMessage('保存失败,请刷新页面', 'error')
|
||||
}
|
||||
|
||||
// 3. 触发其他业务逻辑(如更新关联数据、记录操作日志等)
|
||||
// ...
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**拖拽排序说明**:
|
||||
|
||||
- **启用拖拽**:设置 `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/
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<sub>如果这个项目对你有帮助,请给一个 ⭐️ 支持一下!</sub>
|
||||
如果这个项目对你有帮助,请给一个 ⭐️ 支持一下!
|
||||
</p>
|
||||
|
||||
+122
-6
@@ -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<Task[]>([])
|
||||
const milestones = ref<Task[]>([])
|
||||
@@ -182,6 +184,21 @@ const taskListConfig = computed<TaskListConfig>(() => ({
|
||||
// 控制是否允许拖拽和拉伸
|
||||
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<Task | null>(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 })
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -907,6 +1009,17 @@ function taskDebug(item: any) {
|
||||
{{ t.taskBarConfig.mistouch.allowDragOnClickHint }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<label class="taskbar-control">
|
||||
<input v-model="enableTaskRowMove" type="checkbox" />
|
||||
<span class="taskbar-label">
|
||||
启用TaskRow拖拽移动
|
||||
</span>
|
||||
</label>
|
||||
<span class="control-hint">
|
||||
允许通过拖拽TaskRow来调整任务的层级和顺序
|
||||
</span>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<label class="control-label">
|
||||
{{ 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"
|
||||
>
|
||||
<template #custom-task-content="item">
|
||||
<HtmlContent
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
visible?: boolean
|
||||
title?: string
|
||||
message?: string
|
||||
confirmText?: string
|
||||
cancelText?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
title: '确认操作',
|
||||
message: '',
|
||||
confirmText: '确认',
|
||||
cancelText: '取消',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: []
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="visible" class="confirm-overlay" @click="handleCancel">
|
||||
<div class="confirm-dialog" @click.stop>
|
||||
<div class="confirm-header">
|
||||
<h3>{{ title }}</h3>
|
||||
<button class="close-btn" @click="handleCancel">×</button>
|
||||
</div>
|
||||
<div class="confirm-body">
|
||||
<div class="confirm-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="#409eff" stroke-width="2" />
|
||||
<path d="M12 8v5M12 16h.01" stroke="#409eff" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="confirm-message">{{ message }}</p>
|
||||
</div>
|
||||
<div class="confirm-footer">
|
||||
<button class="btn btn-cancel" @click="handleCancel">{{ cancelText }}</button>
|
||||
<button class="btn btn-confirm" @click="handleConfirm">{{ confirmText }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.confirm-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-dialog {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
min-width: 420px;
|
||||
max-width: 90vw;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.confirm-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 28px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.confirm-body {
|
||||
padding: 32px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.confirm-message {
|
||||
font-size: 15px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.confirm-footer {
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f5f7fa;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: #e8eaed;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
.btn-confirm:active {
|
||||
background: #3a8ee6;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 暗色主题支持 - 非scoped样式以便访问html元素 */
|
||||
html[data-theme='dark'] .confirm-dialog {
|
||||
background: #1d1e1f;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .confirm-header {
|
||||
border-bottom-color: #414243;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .confirm-header h3 {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .confirm-message {
|
||||
color: #b4b6b9;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .confirm-footer {
|
||||
border-top-color: #414243;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .btn-cancel {
|
||||
background: #2c2d2e;
|
||||
color: #b4b6b9;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .btn-cancel:hover {
|
||||
background: #363738;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .close-btn {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .close-btn:hover {
|
||||
background: #2c2d2e;
|
||||
color: #b4b6b9;
|
||||
}
|
||||
</style>
|
||||
+58
-1
@@ -4,7 +4,64 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/public/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Jordium Gantt Vue3 Demo</title>
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>Jordium Gantt Vue3 - Modern Gantt Chart Component for Vue 3</title>
|
||||
<meta name="title" content="Jordium Gantt Vue3 - Modern Gantt Chart Component for Vue 3" />
|
||||
<meta name="description" content="A modern, feature-rich Gantt chart component for Vue 3 with TypeScript support. Interactive task management, drag-and-drop, milestone tracking, and multiple timeline views. Perfect for project management and task scheduling." />
|
||||
<meta name="keywords" content="vue3, gantt chart, gantt, project management, task scheduler, timeline, vue component, typescript, drag and drop, milestone, task tracking" />
|
||||
<meta name="author" content="nelson820125" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="https://nelson820125.github.io/jordium-gantt-vue3/" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://nelson820125.github.io/jordium-gantt-vue3/" />
|
||||
<meta property="og:title" content="Jordium Gantt Vue3 - Modern Gantt Chart Component for Vue 3" />
|
||||
<meta property="og:description" content="A modern, feature-rich Gantt chart component for Vue 3 with TypeScript support. Interactive task management, drag-and-drop, milestone tracking, and multiple timeline views." />
|
||||
<meta property="og:image" content="https://nelson820125.github.io/jordium-gantt-vue3/assets/jordium-gantt-vue3-logo.svg" />
|
||||
<meta property="og:site_name" content="Jordium Gantt Vue3" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://nelson820125.github.io/jordium-gantt-vue3/" />
|
||||
<meta name="twitter:title" content="Jordium Gantt Vue3 - Modern Gantt Chart Component for Vue 3" />
|
||||
<meta name="twitter:description" content="A modern, feature-rich Gantt chart component for Vue 3 with TypeScript support. Interactive task management, drag-and-drop, milestone tracking." />
|
||||
<meta name="twitter:image" content="https://nelson820125.github.io/jordium-gantt-vue3/assets/jordium-gantt-vue3-logo.svg" />
|
||||
|
||||
<!-- Additional SEO Tags -->
|
||||
<meta name="theme-color" content="#4FC08D" />
|
||||
<meta name="application-name" content="Jordium Gantt Vue3" />
|
||||
|
||||
<!-- Links to documentation and repository -->
|
||||
<link rel="alternate" type="application/json+oembed" href="https://github.com/nelson820125/jordium-gantt-vue3" />
|
||||
|
||||
<!-- Schema.org structured data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Jordium Gantt Vue3",
|
||||
"applicationCategory": "DeveloperApplication",
|
||||
"operatingSystem": "Web Browser",
|
||||
"description": "A modern, feature-rich Gantt chart component for Vue 3 with TypeScript support. Interactive task management, drag-and-drop, milestone tracking, and multiple timeline views.",
|
||||
"url": "https://nelson820125.github.io/jordium-gantt-vue3/",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "nelson820125",
|
||||
"url": "https://github.com/nelson820125"
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "USD"
|
||||
},
|
||||
"softwareVersion": "1.4.6",
|
||||
"programmingLanguage": "TypeScript",
|
||||
"codeRepository": "https://github.com/nelson820125/jordium-gantt-vue3",
|
||||
"license": "https://opensource.org/licenses/MIT"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -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}]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}】"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
google-site-verification: google1487963ebfd13afd.html
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://nelson820125.github.io/jordium-gantt-vue3/</loc>
|
||||
<lastmod>2025-12-11</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -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<LocaleKey, LocaleMessages> = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
}
|
||||
|
||||
const currentLocale = ref<LocaleKey>('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<string, string>) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -322,5 +322,45 @@
|
||||
"Added: Large data loading demonstration",
|
||||
"Fix: jspdf vulnerabilies"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.4.4",
|
||||
"date": "2025-12-06",
|
||||
"notes": [
|
||||
"新增:任务列表中任务项的移动拖放功能",
|
||||
"<span style=\"font-weight: bold; color: #f00;\">特别感谢 jun201607@github的使用及反馈的宝贵意见</span>",
|
||||
"Added: Task item move and drag-and-drop functionality in the task list",
|
||||
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to jun201607@github for their valuable use and feedback</span>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.4.5",
|
||||
"date": "2025-12-06",
|
||||
"notes": [
|
||||
"优化:任务列表中任务项的移动拖放功能",
|
||||
"<span style=\"font-weight: bold; color: #f00;\">特别感谢 jun201607@github的使用及反馈的宝贵意见</span>",
|
||||
"Optimized: Task item move and drag-and-drop functionality in the task list",
|
||||
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to jun201607@github for their valuable use and feedback</span>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.4.6",
|
||||
"date": "2025-12-09",
|
||||
"notes": [
|
||||
"修复:内置TaskDrawer中负责人列表可以外部初始化",
|
||||
"修复:未设置startDate和endDate时,任务无法正确显示的问题",
|
||||
"<span style=\"font-weight: bold; color: #f00;\">特别感谢 yue-xiaochuan & YQ6494@gitee的使用及反馈的宝贵意见</span>",
|
||||
"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",
|
||||
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to yue-xiaochuan & YQ6494@gitee for their valuable use and feedback</span>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.4.6-patch.1",
|
||||
"date": "2025-12-09",
|
||||
"notes": [
|
||||
"修复:Github编译错误",
|
||||
"Fixed: The build errors on Github"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
<meta name="google-site-verification" content="your-verification-code" />
|
||||
```
|
||||
2. 将此标签添加到 `demo/index.html` 的 `<head>` 部分
|
||||
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 的 `<link>` 标签或页面内容来关联,不能放在 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` 中的 `<lastmod>` 日期
|
||||
- 在 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)
|
||||
@@ -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<TaskListColumnConfig[]>([
|
||||
{ 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);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -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"
|
||||
>
|
||||
</GanttChart>
|
||||
</div>
|
||||
<!-- 自定义添加任务按钮 -->
|
||||
<div>
|
||||
<button class="btn btn-primary" @click="showAddTaskDrawer = true">添加任务</button>
|
||||
<button class="btn btn-primary" @click="showAddMilestoneDialog = true">添加里程碑</button>
|
||||
</div>
|
||||
|
||||
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
||||
<div v-if="showAddTaskDrawer" class="drawer-overlay" @click="showAddTaskDrawer = false">
|
||||
@@ -170,8 +259,8 @@ const onMilestoneDblclick = (milestone) => {
|
||||
|
||||
<!-- 自定义Dialog组件基于element plus -->
|
||||
<el-dialog
|
||||
v-model="showAddMilestoneDialog"
|
||||
title="自定义添加里程碑组件 - Element Plus"
|
||||
v-model="showAddMilestoneDialog"
|
||||
width="400px"
|
||||
@close="newTask = { name: '', startDate: '', endDate: '' }"
|
||||
>
|
||||
|
||||
+211
-148
@@ -1,150 +1,3 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { GanttChart } from 'jordium-gantt-vue3'
|
||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||
|
||||
// 定义类型接口
|
||||
interface TaskListColumnConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
visible: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface ToolbarConfig {
|
||||
showAddTask?: boolean;
|
||||
showAddMilestone?: boolean;
|
||||
showTodayLocate?: boolean;
|
||||
showExportCsv?: boolean;
|
||||
showExportPdf?: boolean;
|
||||
showLanguage?: boolean;
|
||||
showTheme?: boolean;
|
||||
showFullscreen?: boolean;
|
||||
showTimeScale?: boolean;
|
||||
timeScaleDimensions?: string[];
|
||||
defaultTimeScale?: string;
|
||||
showExpandCollapse?: boolean;
|
||||
}
|
||||
|
||||
const tasks = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '项目启动',
|
||||
startDate: '2025-10-30',
|
||||
endDate: '2025-11-5',
|
||||
progress: 100,
|
||||
department: '管理部',
|
||||
departmentCode: 'D001',
|
||||
type: 'task',
|
||||
},
|
||||
])
|
||||
|
||||
const milestones = ref([
|
||||
{
|
||||
id: 101,
|
||||
name: '项目立项',
|
||||
startDate: '2025-10-29',
|
||||
type: 'milestone',
|
||||
icon: 'diamond',
|
||||
},
|
||||
])
|
||||
|
||||
const customMessages = {
|
||||
'zh-CN': {
|
||||
department: '部门',
|
||||
departmentCode: '部门编号',
|
||||
},
|
||||
'en-US': {
|
||||
department: 'Department',
|
||||
departmentCode: 'Department Code',
|
||||
},
|
||||
}
|
||||
// const tasks = ref([])
|
||||
|
||||
// const milestones = ref([])
|
||||
const showAddTaskDrawer = ref(false)
|
||||
const showAddMilestoneDialog = ref(false)
|
||||
|
||||
// 定义可动态配置的列
|
||||
const availableColumns = ref<TaskListColumnConfig[]>([
|
||||
{ key: 'startDate', label: '开始日期', visible: true },
|
||||
{ key: 'endDate', label: '结束日期', visible: true },
|
||||
{ key: 'progress', label: '进度', visible: true },
|
||||
{ key: 'department', label: '部门', visible: true, width: 120 },
|
||||
{ key: 'departmentCode', label: '部门编号', visible: true },
|
||||
])
|
||||
|
||||
// TaskList宽度配置示例
|
||||
const taskListConfig = {
|
||||
defaultWidth: '50%', // 默认展开宽度50%
|
||||
minWidth: '300px', // 最小宽度300px(默认280px)
|
||||
maxWidth: '1200px', // 最大宽度1200px(默认1160px)
|
||||
columns: availableColumns.value,
|
||||
}
|
||||
|
||||
// toolbar配置示例
|
||||
const toolbarConfig: ToolbarConfig = {
|
||||
showAddTask: true, // 显示添加任务按钮
|
||||
showAddMilestone: true, // 显示添加里程碑按钮
|
||||
showTodayLocate: true, // 显示定位到今天按钮
|
||||
showExportCsv: true, // 显示导出CSV按钮
|
||||
showExportPdf: true, // 显示导出PDF按钮
|
||||
showLanguage: true, // 显示语言切换按钮
|
||||
showTheme: true, // 显示主题切换按钮
|
||||
showFullscreen: true, // 显示全屏按钮
|
||||
showTimeScale: true, // 显示时间刻度按钮组
|
||||
timeScaleDimensions: [ // 显示所有时间刻度维度
|
||||
'hour', 'day', 'week', 'month', 'quarter', 'year',
|
||||
],
|
||||
defaultTimeScale: 'week', // 默认选中周视图
|
||||
showExpandCollapse: false, // 显示展开/折叠按钮
|
||||
}
|
||||
|
||||
const newTask = ref({
|
||||
name: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
})
|
||||
|
||||
const addTask = () => {
|
||||
tasks.value.push({
|
||||
id: tasks.value.length + 1,
|
||||
name: newTask.value.name,
|
||||
startDate: newTask.value.startDate,
|
||||
endDate: newTask.value.endDate,
|
||||
progress: 0,
|
||||
department: '未分配',
|
||||
departmentCode: 'D000',
|
||||
type: 'task',
|
||||
})
|
||||
newTask.value = { name: '', startDate: '', endDate: '' }
|
||||
showAddTaskDrawer.value = false
|
||||
}
|
||||
|
||||
const addMilestone = () => {
|
||||
milestones.value.push({
|
||||
id: milestones.value.length + 101,
|
||||
name: newTask.value.name,
|
||||
startDate: newTask.value.startDate,
|
||||
type: 'milestone',
|
||||
icon: 'diamond',
|
||||
})
|
||||
console.log('milestones: ', milestones.value)
|
||||
newTask.value = { name: '', startDate: '', endDate: '' }
|
||||
showAddMilestoneDialog.value = false
|
||||
}
|
||||
|
||||
const onTaskDblclick = (task: any) => {
|
||||
alert(`双击任务: ${task.name}`)
|
||||
}
|
||||
const onTaskClick = (task: any) => {
|
||||
alert(`单击任务: ${task.name}`)
|
||||
}
|
||||
const onMilestoneDblclick = (milestone: any) => {
|
||||
alert(`双击里程碑: ${milestone.name}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="height: 600px;">
|
||||
@@ -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"
|
||||
>
|
||||
</GanttChart>
|
||||
</div>
|
||||
@@ -205,8 +62,8 @@ const onMilestoneDblclick = (milestone: any) => {
|
||||
|
||||
<!-- 自定义Dialog组件基于element plus -->
|
||||
<el-dialog
|
||||
v-model="showAddMilestoneDialog"
|
||||
title="自定义添加里程碑组件 - Element Plus"
|
||||
v-model="showAddMilestoneDialog"
|
||||
width="400px"
|
||||
@close="newTask = { name: '', startDate: '', endDate: '' }"
|
||||
>
|
||||
@@ -230,6 +87,212 @@ const onMilestoneDblclick = (milestone: any) => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { GanttChart, Task } from 'jordium-gantt-vue3'
|
||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||
|
||||
// 定义类型接口
|
||||
interface TaskListColumnConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
visible: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
interface ToolbarConfig {
|
||||
showAddTask?: boolean;
|
||||
showAddMilestone?: boolean;
|
||||
showTodayLocate?: boolean;
|
||||
showExportCsv?: boolean;
|
||||
showExportPdf?: boolean;
|
||||
showLanguage?: boolean;
|
||||
showTheme?: boolean;
|
||||
showFullscreen?: boolean;
|
||||
showTimeScale?: boolean;
|
||||
timeScaleDimensions?: string[];
|
||||
defaultTimeScale?: string;
|
||||
showExpandCollapse?: boolean;
|
||||
}
|
||||
|
||||
const tasks = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '项目启动',
|
||||
startDate: '2025-10-30',
|
||||
endDate: '2025-11-5',
|
||||
progress: 100,
|
||||
department: '管理部',
|
||||
departmentCode: 'D001',
|
||||
type: 'task',
|
||||
assignee: '',
|
||||
assigneeName: ''
|
||||
}
|
||||
])
|
||||
|
||||
const milestones = ref([
|
||||
{
|
||||
id: 101,
|
||||
name: '项目立项',
|
||||
startDate: '2025-10-29',
|
||||
type: 'milestone',
|
||||
icon: 'diamond'
|
||||
}
|
||||
])
|
||||
|
||||
const customMessages = {
|
||||
'zh-CN': {
|
||||
department: '部门',
|
||||
departmentCode: '部门编号',
|
||||
},
|
||||
'en-US': {
|
||||
department: 'Department',
|
||||
departmentCode: 'Department Code',
|
||||
}
|
||||
}
|
||||
// const tasks = ref([])
|
||||
|
||||
// const milestones = ref([])
|
||||
const showAddTaskDrawer = ref(false);
|
||||
const showAddMilestoneDialog = ref(false);
|
||||
|
||||
// 定义可动态配置的列
|
||||
const availableColumns = ref<TaskListColumnConfig[]>([
|
||||
{ key: 'startDate', label: '开始日期', visible: true },
|
||||
{ key: 'endDate', label: '结束日期', visible: true },
|
||||
{ key: 'progress', label: '进度', visible: true },
|
||||
{ key: 'department', label: '部门', visible: true, width: 120 },
|
||||
{ key: 'departmentCode', label: '部门编号', visible: true },
|
||||
{ key: 'assigneeName', label: '负责人', visible: true },
|
||||
])
|
||||
|
||||
// 自定义负责人列表
|
||||
const assigneeOptions = ref([
|
||||
{ value: 'zhangsan', label: '张三' },
|
||||
{ value: 'lisi', label: '李四' },
|
||||
{ value: 'wangwu', label: '王五' },
|
||||
])
|
||||
|
||||
// TaskList宽度配置示例
|
||||
const taskListConfig = {
|
||||
defaultWidth: '50%', // 默认展开宽度50%
|
||||
minWidth: '300px', // 最小宽度300px(默认280px)
|
||||
maxWidth: '1200px', // 最大宽度1200px(默认1160px)
|
||||
columns: availableColumns.value
|
||||
}
|
||||
|
||||
// toolbar配置示例
|
||||
const toolbarConfig: ToolbarConfig = {
|
||||
showAddTask: true, // 显示添加任务按钮
|
||||
showAddMilestone: true, // 显示添加里程碑按钮
|
||||
showTodayLocate: true, // 显示定位到今天按钮
|
||||
showExportCsv: true, // 显示导出CSV按钮
|
||||
showExportPdf: true, // 显示导出PDF按钮
|
||||
showLanguage: true, // 显示语言切换按钮
|
||||
showTheme: true, // 显示主题切换按钮
|
||||
showFullscreen: true, // 显示全屏按钮
|
||||
showTimeScale: true, // 显示时间刻度按钮组
|
||||
timeScaleDimensions: [ // 显示所有时间刻度维度
|
||||
'hour', 'day', 'week', 'month', 'quarter', 'year'
|
||||
],
|
||||
defaultTimeScale: 'week', // 默认选中周视图
|
||||
showExpandCollapse: false // 显示展开/折叠按钮
|
||||
}
|
||||
|
||||
|
||||
const newTask = ref({
|
||||
name: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
});
|
||||
|
||||
const addTask = () => {
|
||||
tasks.value.push({
|
||||
id: tasks.value.length + 1,
|
||||
name: newTask.value.name,
|
||||
startDate: newTask.value.startDate,
|
||||
endDate: newTask.value.endDate,
|
||||
progress: 0,
|
||||
department: '未分配',
|
||||
departmentCode: 'D000',
|
||||
assignee: '',
|
||||
assigneeName: '',
|
||||
type: 'task',
|
||||
});
|
||||
newTask.value = { name: '', startDate: '', endDate: '' };
|
||||
showAddTaskDrawer.value = false;
|
||||
};
|
||||
|
||||
const addMilestone = () => {
|
||||
milestones.value.push({
|
||||
id: milestones.value.length + 101,
|
||||
name: newTask.value.name,
|
||||
startDate: newTask.value.startDate,
|
||||
type: 'milestone',
|
||||
icon: 'diamond'
|
||||
});
|
||||
console.log('milestones: ', milestones.value)
|
||||
newTask.value = { name: '', startDate: '', endDate: '' };
|
||||
showAddMilestoneDialog.value = false;
|
||||
}
|
||||
|
||||
const onTaskDblclick = (task: any) => {
|
||||
alert(`双击任务: ${task.name}`)
|
||||
}
|
||||
const onTaskClick = (task: any) => {
|
||||
alert(`单击任务: ${task.name}`)
|
||||
}
|
||||
const onMilestoneDblclick = (milestone: any) => {
|
||||
alert(`双击里程碑: ${milestone.name}`)
|
||||
}
|
||||
|
||||
// 任务行拖拽完成事件
|
||||
const handleTaskRowMoved = (payload: {
|
||||
draggedTask: Task
|
||||
targetTask: Task
|
||||
position: 'after' | 'child'
|
||||
}) => {
|
||||
const { draggedTask, targetTask, position } = payload
|
||||
|
||||
alert(`任务 [${draggedTask.name}] 被拖拽到任务 [${targetTask.name}] ${position === 'after' ? '之后' : '下方作为子任务'}`)
|
||||
|
||||
// 组件已自动更新任务的层级关系和位置
|
||||
// position === 'after': 任务被放置在目标任务之后(同级)
|
||||
// position === 'child': 任务被放置为目标任务的子任务(第一个子任务位置)
|
||||
|
||||
// 这里可以:
|
||||
// 1. 显示确认对话框,让用户确认是否移动
|
||||
// 2. 调用后端 API 保存新的任务层级关系
|
||||
// 3. 更新相关的依赖关系
|
||||
|
||||
// 示例:调用后端 API
|
||||
// await api.updateTaskHierarchy({
|
||||
// taskId: draggedTask.id,
|
||||
// targetTaskId: targetTask.id,
|
||||
// position: position
|
||||
// })
|
||||
}
|
||||
|
||||
// 任务添加后回调
|
||||
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);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 抽屉遮罩层 */
|
||||
.drawer-overlay {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jordium-gantt-vue3",
|
||||
"version": "1.4.3",
|
||||
"version": "1.4.6-patch.1",
|
||||
"type": "module",
|
||||
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
||||
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
||||
|
||||
@@ -47,6 +47,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
taskBarConfig: undefined,
|
||||
autoSortByStartDate: false,
|
||||
allowDragAndResize: true,
|
||||
enableTaskRowMove: false,
|
||||
assigneeOptions: () => [],
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
@@ -75,6 +77,8 @@ const emit = defineEmits([
|
||||
// 任务关系事件
|
||||
'add-predecessor',
|
||||
'add-successor',
|
||||
// TaskRow拖拽事件
|
||||
'task-row-moved',
|
||||
])
|
||||
|
||||
const { showMessage } = useMessage()
|
||||
@@ -128,6 +132,12 @@ interface Props {
|
||||
autoSortByStartDate?: boolean
|
||||
// 是否允许拖拽和拉伸(默认为 true)
|
||||
allowDragAndResize?: boolean
|
||||
// 是否启用TaskRow拖拽移动功能(默认为 false)
|
||||
enableTaskRowMove?: boolean
|
||||
// 指派人员选项列表(用于TaskDrawer中的assignee下拉菜单)
|
||||
// 格式:{ key?: string | number, value: string | number, label: string }
|
||||
// key 为可选项,若不存在则使用 value 作为选项的唯一标识
|
||||
assigneeOptions?: Array<{ key?: string | number; value: string | number; label: string }>
|
||||
}
|
||||
|
||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||
@@ -285,6 +295,16 @@ const timelineContainerWidth = ref<number>(0)
|
||||
// 任务拖拽/拉伸触发器(用于触发timeline范围重新计算)
|
||||
const updateTaskTrigger = ref<number>(0)
|
||||
|
||||
// 监听props.tasks变化,自动触发Timeline更新
|
||||
// 这对于TaskRow移动等操作很重要,因为外部更新tasks后需要通知Timeline重新渲染
|
||||
watch(
|
||||
() => props.tasks,
|
||||
() => {
|
||||
updateTaskTrigger.value++
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 时间刻度状态
|
||||
const currentTimeScale = ref<TimelineScale>(TimelineScale.DAY)
|
||||
|
||||
@@ -597,6 +617,22 @@ const handleExpandAll = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理TaskRow移动事件
|
||||
const handleTaskRowMoved = (payload: {
|
||||
draggedTask: Task
|
||||
targetTask: Task
|
||||
position: 'after' | 'child'
|
||||
oldParent: Task | null
|
||||
newParent: Task | null
|
||||
}) => {
|
||||
// TaskList已经通过对象引用直接修改了props.tasks的内容
|
||||
// 但computed不会因为对象内部变化而重新计算,需要手动触发
|
||||
updateTaskTrigger.value++
|
||||
|
||||
// 向外传递事件,让外部组件进行额外处理(完全可选)
|
||||
emit('task-row-moved', payload)
|
||||
}
|
||||
|
||||
// 全部折叠任务
|
||||
const handleCollapseAll = () => {
|
||||
if (props.onCollapseAll && typeof props.onCollapseAll === 'function') {
|
||||
@@ -2262,12 +2298,14 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
||||
:tasks="tasksForTaskList"
|
||||
:use-default-drawer="props.useDefaultDrawer"
|
||||
:task-list-config="props.taskListConfig"
|
||||
:enable-task-row-move="props.enableTaskRowMove"
|
||||
@task-collapse-change="handleTaskCollapseChange"
|
||||
@start-timer="handleStartTimer"
|
||||
@stop-timer="handleStopTimer"
|
||||
@add-predecessor="handleAddPredecessor"
|
||||
@add-successor="handleAddSuccessor"
|
||||
@delete="handleTaskDelete"
|
||||
@task-row-moved="handleTaskRowMoved"
|
||||
>
|
||||
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
||||
<slot name="custom-task-content" v-bind="rowScope" />
|
||||
@@ -2363,6 +2401,7 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
||||
v-model:visible="taskDrawerVisible"
|
||||
:task="taskDrawerTask"
|
||||
:is-edit="taskDrawerEditMode"
|
||||
:assignee-options="props.assigneeOptions"
|
||||
@submit="handleTaskDrawerSubmit"
|
||||
@close="taskDrawerVisible = false"
|
||||
@start-timer="handleStartTimer"
|
||||
|
||||
+48
-15
@@ -261,7 +261,26 @@ const taskBarStyle = computed(() => {
|
||||
const startDate = createLocalDate(currentStartDate)
|
||||
const endDate = createLocalDate(currentEndDate)
|
||||
const baseStart = parsedBaseStartDate.value
|
||||
if (!startDate || !endDate || !baseStart) {
|
||||
|
||||
// 如果startDate和endDate都不存在,返回0宽度(实际不会渲染,由shouldRenderTaskBar控制)
|
||||
if (!startDate && !endDate) {
|
||||
return {
|
||||
left: '0px',
|
||||
width: '0px',
|
||||
height: `${props.rowHeight - 10}px`,
|
||||
top: '4px',
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染时使用有效日期:如果只有startDate或endDate,用它来计算开始和结束位置
|
||||
// 注意:不修改原始的startDate/endDate变量,只用于渲染计算
|
||||
const renderStartDate = startDate || endDate
|
||||
const renderEndDate = endDate || startDate
|
||||
const renderBaseStart = baseStart
|
||||
|
||||
// 安全检查:renderStartDate和renderEndDate必定存在(因为上面已经检查过)
|
||||
// 但baseStart可能不存在,如果不存在则无法计算位置
|
||||
if (!renderStartDate || !renderEndDate || !renderBaseStart) {
|
||||
return {
|
||||
left: '0px',
|
||||
width: '0px',
|
||||
@@ -276,12 +295,12 @@ const taskBarStyle = computed(() => {
|
||||
// 小时视图:按分钟精确计算位置(需要考虑时间部分)
|
||||
if (props.currentTimeScale === TimelineScale.HOUR) {
|
||||
// 确保 baseStart 是当天的 00:00:00
|
||||
const baseStartOfDay = new Date(baseStart)
|
||||
const baseStartOfDay = new Date(renderBaseStart)
|
||||
baseStartOfDay.setHours(0, 0, 0, 0)
|
||||
|
||||
// 处理没有时间部分的日期字符串
|
||||
let adjustedStartDate = startDate
|
||||
let adjustedEndDate = endDate
|
||||
let adjustedStartDate = renderStartDate
|
||||
let adjustedEndDate = renderEndDate
|
||||
|
||||
// 检查原始日期字符串是否包含时间部分
|
||||
const originalStartStr = currentStartDate || props.task.startDate
|
||||
@@ -292,13 +311,13 @@ const taskBarStyle = computed(() => {
|
||||
typeof originalStartStr === 'string' &&
|
||||
/^\d{4}-\d{2}-\d{2}$/.test(originalStartStr.trim())
|
||||
) {
|
||||
adjustedStartDate = new Date(startDate)
|
||||
adjustedStartDate = new Date(renderStartDate)
|
||||
adjustedStartDate.setHours(0, 0, 0, 0)
|
||||
}
|
||||
|
||||
// 如果endDate没有时间部分(格式为YYYY-MM-DD),设置为次日00:00
|
||||
if (typeof originalEndStr === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(originalEndStr.trim())) {
|
||||
adjustedEndDate = new Date(endDate)
|
||||
adjustedEndDate = new Date(renderEndDate)
|
||||
adjustedEndDate.setDate(adjustedEndDate.getDate() + 1)
|
||||
adjustedEndDate.setHours(0, 0, 0, 0)
|
||||
}
|
||||
@@ -317,15 +336,19 @@ const taskBarStyle = computed(() => {
|
||||
|
||||
// 将日期标准化为当天的00:00:00,忽略时间部分
|
||||
const startDateOnly = new Date(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth(),
|
||||
startDate.getDate(),
|
||||
renderStartDate.getFullYear(),
|
||||
renderStartDate.getMonth(),
|
||||
renderStartDate.getDate(),
|
||||
)
|
||||
const endDateOnly = new Date(
|
||||
renderEndDate.getFullYear(),
|
||||
renderEndDate.getMonth(),
|
||||
renderEndDate.getDate(),
|
||||
)
|
||||
const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
|
||||
const baseStartOnly = new Date(
|
||||
baseStart.getFullYear(),
|
||||
baseStart.getMonth(),
|
||||
baseStart.getDate(),
|
||||
renderBaseStart.getFullYear(),
|
||||
renderBaseStart.getMonth(),
|
||||
renderBaseStart.getDate(),
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -447,6 +470,15 @@ const parsedEndDate = computed(() => createLocalDate(props.task.endDate || ''))
|
||||
// 缓存解析后的基准开始日期
|
||||
const parsedBaseStartDate = computed(() => createLocalDate(props.startDate))
|
||||
|
||||
// 判断是否应该渲染TaskBar:只考虑startDate和endDate,都不存在时不渲染
|
||||
const shouldRenderTaskBar = computed(() => {
|
||||
const currentStartDate = tempTaskData.value?.startDate || props.task.startDate
|
||||
const currentEndDate = tempTaskData.value?.endDate || props.task.endDate
|
||||
|
||||
// 只要startDate或endDate有一个存在就渲染
|
||||
return !!(currentStartDate || currentEndDate)
|
||||
})
|
||||
|
||||
// 计算任务状态和颜色
|
||||
const taskStatus = computed(() => {
|
||||
// 父级任务(Story类型)使用与新建按钮一致的配色
|
||||
@@ -2309,6 +2341,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="shouldRenderTaskBar"
|
||||
ref="barRef"
|
||||
class="task-bar"
|
||||
:data-task-id="task.id"
|
||||
@@ -2408,8 +2441,8 @@ onUnmounted(() => {
|
||||
<!-- 图片头像 -->
|
||||
<img v-if="task.avatar" :src="task.avatar" :alt="task.assignee || 'avatar'" />
|
||||
<!-- 文字头像(负责人首字母) -->
|
||||
<span v-else-if="task.assignee" class="avatar-text">
|
||||
{{ task.assignee.charAt(0).toUpperCase() }}
|
||||
<span v-else-if="task.assigneeName" class="avatar-text">
|
||||
{{ task.assigneeName.charAt(0).toUpperCase() }}
|
||||
</span>
|
||||
<!-- 默认灰色用户图标 -->
|
||||
<svg v-else class="avatar-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -9,11 +9,18 @@ import ConfirmTimerDialog from './ConfirmTimerDialog.vue'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import '../styles/app.css'
|
||||
|
||||
interface AssigneeOption {
|
||||
key?: string | number
|
||||
value: string | number
|
||||
label: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
task?: Task | null
|
||||
isEdit?: boolean
|
||||
onDelete?: (task: Task) => void
|
||||
assigneeOptions?: AssigneeOption[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -21,6 +28,13 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
task: null,
|
||||
isEdit: false,
|
||||
onDelete: undefined,
|
||||
assigneeOptions: () => [
|
||||
{ value: 'zhangsan', label: '张三' },
|
||||
{ value: 'lisi', label: '李四' },
|
||||
{ value: 'wangwu', label: '王五' },
|
||||
{ value: 'zhaoliu', label: '赵六' },
|
||||
{ value: 'qianqi', label: '钱七' },
|
||||
],
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -628,6 +642,18 @@ function confirmTimer(desc: string) {
|
||||
// desc 可用于后续业务
|
||||
handleStartTimer(desc)
|
||||
}
|
||||
|
||||
// 处理负责人变更
|
||||
const handleAssigneeChanged = (event: Event) => {
|
||||
const value = (event.target as HTMLSelectElement).value
|
||||
// 通过value过滤props.assigneeOptions获取对应的label
|
||||
const selected = props.assigneeOptions?.find(option => option.value === value)
|
||||
if (selected) {
|
||||
// 这里可以根据需要处理选中的负责人信息
|
||||
// 例如,可以将负责人名称存储在formData中
|
||||
formData.assigneeName = selected.label
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -788,13 +814,15 @@ function confirmTimer(desc: string) {
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-assignee">{{ t.assignee }}</label>
|
||||
<select id="task-assignee" v-model="formData.assignee" class="form-select">
|
||||
<select id="task-assignee" v-model="formData.assignee" class="form-select" @change="handleAssigneeChanged">
|
||||
<option value="">{{ t.selectAssignee }}</option>
|
||||
<option value="张三">张三</option>
|
||||
<option value="李四">李四</option>
|
||||
<option value="王五">王五</option>
|
||||
<option value="赵六">赵六</option>
|
||||
<option value="钱七">钱七</option>
|
||||
<option
|
||||
v-for="assignee in props.assigneeOptions"
|
||||
:key="assignee.key ?? assignee.value"
|
||||
:value="assignee.value"
|
||||
>
|
||||
{{ assignee.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
+94
-44
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, useSlots, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, useSlots, computed, nextTick } from 'vue'
|
||||
import TaskRow from './TaskRow.vue'
|
||||
import { useI18n } from '../composables/useI18n'
|
||||
import type { Task } from '../models/classes/Task'
|
||||
import type { TaskListConfig } from '../models/configs/TaskListConfig'
|
||||
import { DEFAULT_TASK_LIST_COLUMNS } from '../models/configs/TaskListConfig'
|
||||
import { useTaskRowDrag } from '../composables/useTaskRowDrag'
|
||||
import { moveTask } from '../utils/taskTreeUtils'
|
||||
|
||||
interface Props {
|
||||
tasks?: Task[]
|
||||
useDefaultDrawer?: boolean
|
||||
taskListConfig?: TaskListConfig
|
||||
enableTaskRowMove?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
@@ -26,6 +29,15 @@ const emit = defineEmits<{
|
||||
'add-predecessor': [task: Task] // 新增:添加前置任务事件
|
||||
'add-successor': [task: Task] // 新增:添加后置任务事件
|
||||
delete: [task: Task, deleteChildren?: boolean]
|
||||
'task-row-moved': [
|
||||
payload: {
|
||||
draggedTask: Task
|
||||
targetTask: Task
|
||||
position: 'after' | 'child' // 'after': 放在目标任务之后(同级), 'child': 作为目标任务的子任务
|
||||
oldParent: Task | null
|
||||
newParent: Task | null
|
||||
},
|
||||
]
|
||||
}>()
|
||||
const slots = useSlots()
|
||||
const hasRowSlot = computed(() => Boolean(slots['custom-task-content']))
|
||||
@@ -86,8 +98,9 @@ const visibleColumns = computed(() => {
|
||||
return columns.filter(col => col.visible !== false)
|
||||
})
|
||||
|
||||
// 内部响应式任务列表
|
||||
const localTasks = ref<Task[]>([])
|
||||
// 使用 props.tasks 的引用,不创建副本
|
||||
// 这样可以直接修改对象,触发响应式更新,不需要外部监听事件
|
||||
const localTasks = computed(() => props.tasks || [])
|
||||
|
||||
// 悬停状态管理
|
||||
const hoveredTaskId = ref<number | null>(null)
|
||||
@@ -206,31 +219,26 @@ const calculateParentTaskData = (
|
||||
|
||||
// 更新所有父级任务的进度和日期范围
|
||||
const updateParentTasksData = () => {
|
||||
const updateParentTask = (taskList: Task[]): Task[] => {
|
||||
return taskList.map(task => {
|
||||
if (!props.tasks) return
|
||||
|
||||
const updateParentTask = (taskList: Task[]): void => {
|
||||
taskList.forEach(task => {
|
||||
if (task.children && task.children.length > 0) {
|
||||
// 先更新子任务
|
||||
const updatedChildren = updateParentTask(task.children)
|
||||
updateParentTask(task.children)
|
||||
|
||||
// 计算父级任务的进度和日期范围
|
||||
const parentData = calculateParentTaskData({
|
||||
...task,
|
||||
children: updatedChildren,
|
||||
})
|
||||
const parentData = calculateParentTaskData(task)
|
||||
|
||||
return {
|
||||
...task,
|
||||
progress: parentData.progress,
|
||||
startDate: parentData.startDate,
|
||||
endDate: parentData.endDate,
|
||||
children: updatedChildren,
|
||||
}
|
||||
// 直接修改对象属性
|
||||
task.progress = parentData.progress
|
||||
task.startDate = parentData.startDate
|
||||
task.endDate = parentData.endDate
|
||||
}
|
||||
return task
|
||||
})
|
||||
}
|
||||
|
||||
localTasks.value = updateParentTask(localTasks.value)
|
||||
updateParentTask(props.tasks)
|
||||
}
|
||||
|
||||
// 获取所有任务的扁平化列表(包括子任务)
|
||||
@@ -305,16 +313,7 @@ const endSpacerHeight = computed(() => {
|
||||
return Math.max(0, totalContentHeight.value - startSpacerHeight.value - visibleHeight)
|
||||
})
|
||||
|
||||
// 监听外部传入的 tasks 数据变化
|
||||
watch(
|
||||
() => props.tasks,
|
||||
newTasks => {
|
||||
localTasks.value = newTasks || []
|
||||
// 更新父级任务数据
|
||||
updateParentTasksData()
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
// 不再需要 watch,直接使用 props.tasks 的引用
|
||||
|
||||
function toggleCollapse(task: Task) {
|
||||
task.collapsed = !task.collapsed
|
||||
@@ -325,26 +324,28 @@ function toggleCollapse(task: Task) {
|
||||
|
||||
// 更新任务数据
|
||||
const updateTaskData = (updatedTask: Task) => {
|
||||
const updateTaskInTree = (taskList: Task[]): Task[] => {
|
||||
return taskList.map(task => {
|
||||
if (!props.tasks) return
|
||||
|
||||
const updateTaskInTree = (taskList: Task[]): boolean => {
|
||||
for (const task of taskList) {
|
||||
if (task.id === updatedTask.id) {
|
||||
return {
|
||||
...task,
|
||||
...updatedTask, // 完整更新所有字段
|
||||
children: task.children, // 保留子任务结构
|
||||
// 直接修改对象属性
|
||||
Object.assign(task, updatedTask)
|
||||
// 保留子任务结构
|
||||
if (updatedTask.children === undefined) {
|
||||
// 如果 updatedTask 没有 children,保持原来的
|
||||
// task.children 保持不变
|
||||
}
|
||||
return true
|
||||
}
|
||||
if (task.children) {
|
||||
return {
|
||||
...task,
|
||||
children: updateTaskInTree(task.children),
|
||||
}
|
||||
if (task.children && updateTaskInTree(task.children)) {
|
||||
return true
|
||||
}
|
||||
return task
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
localTasks.value = updateTaskInTree(localTasks.value)
|
||||
updateTaskInTree(props.tasks)
|
||||
|
||||
// 更新父级任务的进度和日期范围
|
||||
updateParentTasksData()
|
||||
@@ -359,7 +360,9 @@ const handleTaskUpdated = (event: CustomEvent) => {
|
||||
// 监听Timeline的任务新增事件
|
||||
const handleTaskAdded = (event: CustomEvent) => {
|
||||
const newTask = event.detail
|
||||
localTasks.value.push(newTask)
|
||||
if (props.tasks) {
|
||||
props.tasks.push(newTask)
|
||||
}
|
||||
|
||||
// 更新父级任务数据
|
||||
updateParentTasksData()
|
||||
@@ -470,6 +473,50 @@ const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||
emit('delete', task, deleteChildren)
|
||||
}
|
||||
|
||||
// 处理TaskRow拖拽移动事件(核心逻辑在组件内部)
|
||||
const handleTaskRowMoved = (payload: {
|
||||
draggedTask: Task
|
||||
targetTask: Task
|
||||
position: 'after' | 'child'
|
||||
}) => {
|
||||
const { draggedTask, targetTask, position } = payload
|
||||
|
||||
if (!props.tasks) return
|
||||
|
||||
// 执行移动操作(直接修改 props.tasks 对象,保持引用不变)
|
||||
const result = moveTask(props.tasks, draggedTask.id, targetTask.id, position)
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
// moveTask 已直接修改了 props.tasks,无需手动 splice
|
||||
// 强制触发下一次更新,确保视图刷新
|
||||
nextTick(() => {
|
||||
// 更新父级任务数据(重新计算进度和日期)
|
||||
updateParentTasksData()
|
||||
|
||||
// 触发事件,通知外部组件(可选)
|
||||
// 外部可以监听此事件进行额外处理,如调用API保存、显示提示等
|
||||
// 但不需要更新 tasks,组件已自动完成
|
||||
emit('task-row-moved', {
|
||||
draggedTask: result.movedTask,
|
||||
targetTask,
|
||||
position,
|
||||
oldParent: result.oldParent,
|
||||
newParent: result.newParent,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 全局拖拽管理器(在TaskList层面,所有TaskRow共享)
|
||||
const { startDrag, handleDragOver } = useTaskRowDrag({
|
||||
enabled: props.enableTaskRowMove ?? false,
|
||||
onDrop: (draggedTask, targetTask, position) => {
|
||||
handleTaskRowMoved({ draggedTask, targetTask, position })
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化 ResizeObserver 监听容器宽度变化
|
||||
if (taskListRef.value) {
|
||||
@@ -575,6 +622,9 @@ onUnmounted(() => {
|
||||
:get-column-width-style="getColumnWidthStyle"
|
||||
:disable-children-render="true"
|
||||
:show-task-icon="props.taskListConfig?.showTaskIcon"
|
||||
:enable-drag="props.enableTaskRowMove"
|
||||
:drag-start="startDrag"
|
||||
:drag-over="handleDragOver"
|
||||
@toggle="toggleCollapse"
|
||||
@dblclick="handleTaskRowDoubleClick"
|
||||
@contextmenu="handleTaskRowContextMenu"
|
||||
|
||||
@@ -39,6 +39,9 @@ interface Props {
|
||||
getColumnWidthStyle?: (column: { width?: number | string }) => StyleValue
|
||||
disableChildrenRender?: boolean
|
||||
showTaskIcon?: boolean // 是否显示任务图标,默认true
|
||||
enableDrag?: boolean
|
||||
dragStart?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
||||
dragOver?: (task: Task, element: HTMLElement, event: MouseEvent) => void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits([
|
||||
@@ -276,17 +279,54 @@ const slotPayload = computed(() => ({
|
||||
progressClass: progressClass.value,
|
||||
}))
|
||||
|
||||
// TaskRow拖拽功能
|
||||
const taskRowRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
// 如果未启用拖拽,直接返回
|
||||
if (!props.enableDrag) return
|
||||
|
||||
// 如果点击的是按钮、输入框等交互元素,不触发拖拽
|
||||
const target = event.target as HTMLElement
|
||||
if (
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'INPUT' ||
|
||||
target.tagName === 'SELECT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.closest('.collapse-btn') ||
|
||||
target.closest('.task-context-menu')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (taskRowRef.value && props.dragStart) {
|
||||
props.dragStart(props.task, taskRowRef.value, event)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理全局拖拽悬停事件
|
||||
const handleTaskRowDragOver = (event: CustomEvent) => {
|
||||
if (!props.enableDrag || !taskRowRef.value || !props.dragOver) return
|
||||
|
||||
const { taskId, event: mouseEvent } = event.detail
|
||||
if (taskId === props.task.id) {
|
||||
props.dragOver(props.task, taskRowRef.value, mouseEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期钩子 - 注册事件监听器
|
||||
onMounted(() => {
|
||||
window.addEventListener('splitter-drag-start', handleSplitterDragStart)
|
||||
window.addEventListener('splitter-drag-end', handleSplitterDragEnd)
|
||||
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||
window.addEventListener('task-row-drag-over', handleTaskRowDragOver as EventListener)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('splitter-drag-start', handleSplitterDragStart)
|
||||
window.removeEventListener('splitter-drag-end', handleSplitterDragEnd)
|
||||
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||
window.removeEventListener('task-row-drag-over', handleTaskRowDragOver as EventListener)
|
||||
if (timerInterval.value) {
|
||||
clearInterval(timerInterval.value)
|
||||
}
|
||||
@@ -296,7 +336,9 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
ref="taskRowRef"
|
||||
class="task-row"
|
||||
:data-task-id="props.task.id"
|
||||
:class="{
|
||||
'task-row-hovered': isHovered,
|
||||
'parent-task': isParentTask,
|
||||
@@ -307,6 +349,7 @@ onUnmounted(() => {
|
||||
}"
|
||||
@click="handleRowClick"
|
||||
@dblclick="handleTaskRowDoubleClick"
|
||||
@mousedown="handleMouseDown"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@contextmenu="handleContextMenu"
|
||||
@@ -494,6 +537,9 @@ onUnmounted(() => {
|
||||
:get-column-width-style="props.getColumnWidthStyle"
|
||||
:disable-children-render="props.disableChildrenRender"
|
||||
:show-task-icon="props.showTaskIcon"
|
||||
:enable-drag="props.enableDrag"
|
||||
:drag-start="props.dragStart"
|
||||
:drag-over="props.dragOver"
|
||||
@toggle="emit('toggle', $event)"
|
||||
@dblclick="emit('dblclick', $event)"
|
||||
@start-timer="emit('start-timer', $event)"
|
||||
@@ -956,4 +1002,29 @@ onUnmounted(() => {
|
||||
:global(html[data-theme='dark']) .timer-dot {
|
||||
background-color: #85ce61; /* 暗色主题下的绿色 */
|
||||
}
|
||||
|
||||
/* TaskRow拖拽样式 */
|
||||
.task-row-dragging {
|
||||
opacity: 0.6 !important;
|
||||
cursor: move !important;
|
||||
}
|
||||
|
||||
/* 拖拽目标高亮样式 */
|
||||
.task-row-drop-target.drop-after {
|
||||
border-bottom: 3px solid var(--gantt-primary, #409eff) !important;
|
||||
background-color: rgba(64, 158, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
.task-row-drop-target.drop-child {
|
||||
border: 2px solid var(--gantt-primary, #409eff) !important;
|
||||
background-color: rgba(64, 158, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
:global(html[data-theme='dark']) .task-row-drop-target.drop-after {
|
||||
background-color: rgba(125, 180, 240, 0.1) !important;
|
||||
}
|
||||
|
||||
:global(html[data-theme='dark']) .task-row-drop-target.drop-child {
|
||||
background-color: rgba(125, 180, 240, 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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<DragState>({
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user