v1.7.0 - add TaskListContextMenu and TaskBarContextMenu components & slots
This commit is contained in:
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.7.0] - 2026-01-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- TaskList和TaskBar声明式定义ContextMenu方式, 使用组件TaskListContextMenu和TaskBarContextMenu
|
||||||
|
- TaskListContextMenu和TaskBarContextMenu的default Slots
|
||||||
|
- TaskListContextMenu和TaskBarContextMenu是否展示属性 - enable-task-list-context-menu和enable-task-bar-context-menu
|
||||||
|
- Declarative definition ContextMenu method for TaskList and TaskBar using the TaskListContextMenu and TaskBarContextMenu components
|
||||||
|
- default Slots for TaskListContextMenu and TaskBarContextMenu
|
||||||
|
- Properties to control the display of TaskListContextMenu and TaskBarContextMenu - enable-task-list-context-menu and enable-task-bar-context-menu
|
||||||
|
|
||||||
|
### Enhancement
|
||||||
|
- 拆分TaskList子组件代码,更易读、易维护
|
||||||
|
- Split TaskList sub-component code for better readability and maintainability
|
||||||
|
|
||||||
## [1.6.2] - 2025-12-23
|
## [1.6.2] - 2025-12-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+308
@@ -197,6 +197,8 @@ npm run dev
|
|||||||
| `autoSortByStartDate` | `boolean` | `false` | Whether to automatically sort tasks by start date |
|
| `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 |
|
| `enableTaskRowMove` | `boolean` | `false` | Whether to allow dragging and dropping TaskRow |
|
||||||
|
| `enableTaskListContextMenu` | `boolean` | `true` | Whether to enable TaskList (TaskRow) context menu. When `true`: uses built-in menu if `task-list-context-menu` slot is not declared, uses custom menu if slot is declared; when `false`: context menu is completely disabled |
|
||||||
|
| `enableTaskBarContextMenu` | `boolean` | `true` | Whether to enable TaskBar context menu. When `true`: uses built-in menu if `task-bar-context-menu` slot is not declared, uses custom menu if slot is declared; when `false`: context menu is completely disabled |
|
||||||
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | Assignee dropdown options in task edit drawer |
|
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | Assignee dropdown options in task edit drawer |
|
||||||
|
|
||||||
#### TaskListColumn Component Props
|
#### TaskListColumn Component Props
|
||||||
@@ -232,6 +234,110 @@ The `TaskListColumn` component is used to define task list columns in declarativ
|
|||||||
> - Column display order is determined by the declaration order of `TaskListColumn` components
|
> - Column display order is determined by the declaration order of `TaskListColumn` components
|
||||||
> - For detailed column content customization and slot usage, see [Slots](#slots) section
|
> - For detailed column content customization and slot usage, see [Slots](#slots) section
|
||||||
|
|
||||||
|
#### TaskListContextMenu Component Props
|
||||||
|
|
||||||
|
The `TaskListContextMenu` component is used to declaratively define the context menu for TaskList (TaskRow). Takes effect when `enableTaskListContextMenu` is `true`.
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
| ---------- | ---------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `taskType` | `string \| string[]` | `undefined` | Specifies which task types should display this context menu. When not set, follows existing logic (all tasks show menu). When set, only specified types show menu. Supports single type (e.g., `'task'`) or multiple types (e.g., `['task', 'milestone']`) |
|
||||||
|
|
||||||
|
**Usage Examples**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-list-context-menu="true"
|
||||||
|
>
|
||||||
|
<!-- Default behavior: all tasks show this context menu -->
|
||||||
|
<TaskListContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item" @click="editTask(scope.row)">Edit</div>
|
||||||
|
<div class="menu-item" @click="deleteTask(scope.row)">Delete</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
|
||||||
|
<!-- Only show for tasks with type='task' -->
|
||||||
|
<TaskListContextMenu task-type="task">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">Task Specific Menu</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
|
||||||
|
<!-- Show for multiple types -->
|
||||||
|
<TaskListContextMenu :task-type="['task', 'milestone']">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">Task and Milestone Menu</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 Tips**:
|
||||||
|
> - The `TaskListContextMenu` component itself does not render any content, it only declares menu configuration
|
||||||
|
> - Must be used inside the `GanttChart` component with `enable-task-list-context-menu="true"` set
|
||||||
|
> - Menu positioning and visibility are automatically managed internally, users only need to focus on menu content HTML structure
|
||||||
|
> - Menu automatically closes when clicking outside or scrolling
|
||||||
|
> - For detailed slot usage, see [Slots](#slots) section
|
||||||
|
|
||||||
|
#### TaskBarContextMenu Component Props
|
||||||
|
|
||||||
|
The `TaskBarContextMenu` component is used to declaratively define the context menu for TaskBar (timeline task bars). Takes effect when `enableTaskBarContextMenu` is `true`.
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
| ---------- | ---------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `taskType` | `string \| string[]` | `undefined` | Specifies which task types should display this context menu. When not set, follows existing logic (all tasks show menu). When set, only specified types show menu. Supports single type (e.g., `'task'`) or multiple types (e.g., `['task', 'milestone']`) |
|
||||||
|
|
||||||
|
**Usage Examples**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-bar-context-menu="true"
|
||||||
|
>
|
||||||
|
<!-- Default behavior: all tasks show this context menu -->
|
||||||
|
<TaskBarContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item" @click="extendTask(scope.row)">Extend Task</div>
|
||||||
|
<div class="menu-item" @click="moveTask(scope.row)">Move Task</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
|
||||||
|
<!-- Only show for tasks with type='task' -->
|
||||||
|
<TaskBarContextMenu task-type="task">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">Task Bar Specific Menu</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
|
||||||
|
<!-- Show for multiple types -->
|
||||||
|
<TaskBarContextMenu :task-type="['task', 'story']">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">Task and Story Menu</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 Tips**:
|
||||||
|
> - The `TaskBarContextMenu` component itself does not render any content, it only declares menu configuration
|
||||||
|
> - Must be used inside the `GanttChart` component with `enable-task-bar-context-menu="true"` set
|
||||||
|
> - Menu positioning and visibility are automatically managed internally, users only need to focus on menu content HTML structure
|
||||||
|
> - Menu automatically closes when clicking outside or scrolling
|
||||||
|
> - For detailed slot usage, see [Slots](#slots) section
|
||||||
|
|
||||||
#### Configuration Object Props
|
#### Configuration Object Props
|
||||||
|
|
||||||
For complete configuration object documentation, see [⚙️ Configuration & Customization](#⚙️-configuration--customization) section.
|
For complete configuration object documentation, see [⚙️ Configuration & Customization](#⚙️-configuration--customization) section.
|
||||||
@@ -2391,6 +2497,208 @@ const props = defineProps<Props>()
|
|||||||
> - TaskRow and TaskBar have different available space, need to adapt layout
|
> - TaskRow and TaskBar have different available space, need to adapt layout
|
||||||
> - Avoid using overly complex components in slot content, may affect performance
|
> - Avoid using overly complex components in slot content, may affect performance
|
||||||
|
|
||||||
|
##### TaskListContextMenu Slots
|
||||||
|
|
||||||
|
Used to customize the context menu content for TaskRow (task list row).
|
||||||
|
|
||||||
|
**Menu Display Logic:**
|
||||||
|
- When `enableTaskListContextMenu=true` and TaskListContextMenu component is **not declared** → Uses **built-in** context menu
|
||||||
|
- When `enableTaskListContextMenu=true` and TaskListContextMenu component is **declared** → Uses **custom** context menu
|
||||||
|
- When `enableTaskListContextMenu=false` → Context menu is **completely disabled** (regardless of component declaration)
|
||||||
|
|
||||||
|
**Slot List:**
|
||||||
|
|
||||||
|
| Slot Name | Parameters | Description |
|
||||||
|
| --------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||||
|
| `default` | `scope: { row: Task, $index: number }` | Custom Task List Context Menu content. Access current task object via `scope.row`, access task index via `scope.$index`. |
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-list-context-menu="true"
|
||||||
|
>
|
||||||
|
<TaskListContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item" @click="handleEdit(task); onClose()">
|
||||||
|
✏️ Edit Task
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="handleDelete(task); onClose()">
|
||||||
|
🗑️ Delete Task
|
||||||
|
</div>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<div class="menu-item" @click="handleDuplicate(task); onClose()">
|
||||||
|
📄 Duplicate Task
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart, TaskListContextMenu } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
import type { Task } from 'jordium-gantt-vue3'
|
||||||
|
|
||||||
|
const tasks = ref<Task[]>([])
|
||||||
|
|
||||||
|
const handleEdit = (task: Task) => {
|
||||||
|
console.log('Edit task:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (task: Task) => {
|
||||||
|
console.log('Delete task:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDuplicate = (task: Task) => {
|
||||||
|
console.log('Duplicate task:', task)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 9999;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
##### TaskBarContextMenu Slots
|
||||||
|
|
||||||
|
Used to customize the context menu content for TaskBar (timeline task bar).
|
||||||
|
|
||||||
|
**Menu Display Logic:**
|
||||||
|
- When `enableTaskBarContextMenu=true` and TaskBarContextMenu component is **not declared** → Uses **built-in** context menu
|
||||||
|
- When `enableTaskBarContextMenu=true` and TaskBarContextMenu component is **declared** → Uses **custom** context menu
|
||||||
|
- When `enableTaskBarContextMenu=false` → Context menu is **completely disabled** (regardless of component declaration)
|
||||||
|
|
||||||
|
**Slot List:**
|
||||||
|
|
||||||
|
| Slot Name | Parameters | Description |
|
||||||
|
| --------- | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||||
|
| `default` | `scope: { row: Task, $index: number }` | Custom Task Bar Context Menu content. Access current task object via `scope.row`, access task index via `scope.$index`. |
|
||||||
|
|
||||||
|
**Usage Example:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-bar-context-menu="true"
|
||||||
|
>
|
||||||
|
<TaskBarContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-context-menu">
|
||||||
|
<div class="menu-item" @click="handleViewDetails(scope.row)">
|
||||||
|
👁️ View Details
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="handleAdjustTime(scope.row)">
|
||||||
|
⏰ Adjust Time
|
||||||
|
</div>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<div class="menu-item" @click="handleSetDependency(scope.row)">
|
||||||
|
🔗 Set Dependency
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart, TaskBarContextMenu } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
import type { Task } from 'jordium-gantt-vue3'
|
||||||
|
|
||||||
|
const tasks = ref<Task[]>([])
|
||||||
|
|
||||||
|
const handleViewDetails = (task: Task) => {
|
||||||
|
console.log('View details:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdjustTime = (task: Task) => {
|
||||||
|
console.log('Adjust time:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetDependency = (task: Task) => {
|
||||||
|
console.log('Set dependency:', task)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 9999;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 Usage Scenarios**:
|
||||||
|
>
|
||||||
|
> - Customize context menu styles and layouts
|
||||||
|
> - Add business-specific operations
|
||||||
|
> - Dynamically show menu items based on task status
|
||||||
|
> - Add permission control logic
|
||||||
|
> - Integrate third-party UI component library menus
|
||||||
|
|
||||||
|
> **⚠️ Notes**:
|
||||||
|
>
|
||||||
|
> - When `enableTaskListContextMenu=false` or `enableTaskBarContextMenu=false`, the menu will not be displayed even if the component is declared
|
||||||
|
> - Context menus will automatically close when scrolling or clicking outside the menu
|
||||||
|
> - Recommended to use `<Teleport to="body">` to render the menu under body to avoid positioning and z-index issues
|
||||||
|
> - Remember to call `onClose()` after menu item clicks to close the menu
|
||||||
|
> - TaskRow and TaskBar context menus are independent and can be customized separately
|
||||||
|
> - By default, the system provides a built-in context menu with common operations
|
||||||
|
> - Declarative components do not render any content, only used for passing configuration
|
||||||
|
|
||||||
##### TaskListColumn Slots
|
##### TaskListColumn Slots
|
||||||
|
|
||||||
The `TaskListColumn` component provides two slots for customizing task list column headers and cell content. Must be used in declarative mode (`taskListColumnRenderMode="declarative"`).
|
The `TaskListColumn` component provides two slots for customizing task list column headers and cell content. Must be used in declarative mode (`taskListColumnRenderMode="declarative"`).
|
||||||
|
|||||||
@@ -195,6 +195,8 @@ npm run dev
|
|||||||
| `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 |
|
| `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 |
|
||||||
| `allowDragAndResize` | `boolean` | `true` | 是否允许拖拽和调整任务/里程碑大小 |
|
| `allowDragAndResize` | `boolean` | `true` | 是否允许拖拽和调整任务/里程碑大小 |
|
||||||
| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow |
|
| `enableTaskRowMove` | `boolean` | `false` | 是否允许拖拽和摆放TaskRow |
|
||||||
|
| `enableTaskListContextMenu` | `boolean` | `true` | 是否启用 TaskList(TaskRow)右键菜单功能。为 `true` 时:未声明 `task-list-context-menu` 插槽则使用内置菜单,声明了插槽则使用自定义菜单;为 `false` 时右键菜单完全禁用 |
|
||||||
|
| `enableTaskBarContextMenu` | `boolean` | `true` | 是否启用 TaskBar 右键菜单功能。为 `true` 时:未声明 `task-bar-context-menu` 插槽则使用内置菜单,声明了插槽则使用自定义菜单;为 `false` 时右键菜单完全禁用 |
|
||||||
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | 任务编辑抽屉中负责人下拉菜单的选项列表 |
|
| `assigneeOptions` | `Array<{ key?: string \| number; value: string \| number; label: string }>` | `[]` | 任务编辑抽屉中负责人下拉菜单的选项列表 |
|
||||||
|
|
||||||
#### TaskListColumn 属性
|
#### TaskListColumn 属性
|
||||||
@@ -230,6 +232,110 @@ npm run dev
|
|||||||
> - 列的显示顺序由 `TaskListColumn` 组件的声明顺序决定
|
> - 列的显示顺序由 `TaskListColumn` 组件的声明顺序决定
|
||||||
> - 关于列内容自定义和插槽的详细使用方法,请参考 [插槽 (Slots)](#插槽-slots) 章节
|
> - 关于列内容自定义和插槽的详细使用方法,请参考 [插槽 (Slots)](#插槽-slots) 章节
|
||||||
|
|
||||||
|
#### TaskListContextMenu 属性
|
||||||
|
|
||||||
|
`TaskListContextMenu` 组件用于声明式定义 TaskList(TaskRow)的右键菜单。当 `enableTaskListContextMenu` 为 `true` 时生效。
|
||||||
|
|
||||||
|
| 属性名 | 类型 | 默认值 | 说明 |
|
||||||
|
| ---------- | ---------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `taskType` | `string \| string[]` | `undefined` | 指定哪些任务类型显示此右键菜单。不设置时遵循现有逻辑(所有任务都显示),设置后仅对指定类型任务显示。支持单个类型(如 `'task'`)或多个类型(如 `['task', 'milestone']`) |
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-list-context-menu="true"
|
||||||
|
>
|
||||||
|
<!-- 默认行为:所有任务都显示此右键菜单 -->
|
||||||
|
<TaskListContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item" @click="editTask(scope.row)">编辑</div>
|
||||||
|
<div class="menu-item" @click="deleteTask(scope.row)">删除</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
|
||||||
|
<!-- 仅对 type='task' 的任务显示此菜单 -->
|
||||||
|
<TaskListContextMenu task-type="task">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">任务专属菜单</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
|
||||||
|
<!-- 对多种类型显示菜单 -->
|
||||||
|
<TaskListContextMenu :task-type="['task', 'milestone']">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">任务和里程碑菜单</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 提示**:
|
||||||
|
> - `TaskListContextMenu` 组件本身不渲染任何内容,仅用于声明菜单配置
|
||||||
|
> - 必须在 `GanttChart` 组件内部使用,且设置 `enable-task-list-context-menu="true"`
|
||||||
|
> - 菜单定位和显示状态由内部自动管理,用户只需关心菜单内容的 HTML 结构
|
||||||
|
> - 菜单会在点击外部或滚动时自动关闭
|
||||||
|
> - 关于插槽的详细使用方法,请参考 [插槽 (Slots)](#插槽-slots) 章节
|
||||||
|
|
||||||
|
#### TaskBarContextMenu 属性
|
||||||
|
|
||||||
|
`TaskBarContextMenu` 组件用于声明式定义 TaskBar(时间线任务条)的右键菜单。当 `enableTaskBarContextMenu` 为 `true` 时生效。
|
||||||
|
|
||||||
|
| 属性名 | 类型 | 默认值 | 说明 |
|
||||||
|
| ---------- | ---------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `taskType` | `string \| string[]` | `undefined` | 指定哪些任务类型显示此右键菜单。不设置时遵循现有逻辑(所有任务都显示),设置后仅对指定类型任务显示。支持单个类型(如 `'task'`)或多个类型(如 `['task', 'milestone']`) |
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-bar-context-menu="true"
|
||||||
|
>
|
||||||
|
<!-- 默认行为:所有任务都显示此右键菜单 -->
|
||||||
|
<TaskBarContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item" @click="extendTask(scope.row)">延长任务</div>
|
||||||
|
<div class="menu-item" @click="moveTask(scope.row)">移动任务</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
|
||||||
|
<!-- 仅对 type='task' 的任务显示此菜单 -->
|
||||||
|
<TaskBarContextMenu task-type="task">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">任务条专属菜单</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
|
||||||
|
<!-- 对多种类型显示菜单 -->
|
||||||
|
<TaskBarContextMenu :task-type="['task', 'story']">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="menu-item">任务和故事菜单</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 提示**:
|
||||||
|
> - `TaskBarContextMenu` 组件本身不渲染任何内容,仅用于声明菜单配置
|
||||||
|
> - 必须在 `GanttChart` 组件内部使用,且设置 `enable-task-bar-context-menu="true"`
|
||||||
|
> - 菜单定位和显示状态由内部自动管理,用户只需关心菜单内容的 HTML 结构
|
||||||
|
> - 菜单会在点击外部或滚动时自动关闭
|
||||||
|
> - 关于插槽的详细使用方法,请参考 [插槽 (Slots)](#插槽-slots) 章节
|
||||||
|
|
||||||
#### 配置对象属性
|
#### 配置对象属性
|
||||||
|
|
||||||
完整的配置对象说明请参考 [⚙️ 配置与扩展](#⚙️-配置与扩展) 章节。
|
完整的配置对象说明请参考 [⚙️ 配置与扩展](#⚙️-配置与扩展) 章节。
|
||||||
@@ -2382,6 +2488,208 @@ const props = defineProps<Props>()
|
|||||||
> - TaskRow 和 TaskBar 的可用空间不同,需要适配布局
|
> - TaskRow 和 TaskBar 的可用空间不同,需要适配布局
|
||||||
> - 避免在插槽内容中使用过于复杂的组件,可能影响性能
|
> - 避免在插槽内容中使用过于复杂的组件,可能影响性能
|
||||||
|
|
||||||
|
##### TaskListContextMenu 插槽
|
||||||
|
|
||||||
|
用于自定义 TaskRow(任务列表行)的右键菜单内容。
|
||||||
|
|
||||||
|
**菜单显示逻辑:**
|
||||||
|
- 当 `enableTaskListContextMenu=true` 且**未声明** TaskListContextMenu 组件时 → 使用**系统内置**的右键菜单
|
||||||
|
- 当 `enableTaskListContextMenu=true` 且**声明了** TaskListContextMenu 组件时 → 使用**自定义**的右键菜单
|
||||||
|
- 当 `enableTaskListContextMenu=false` 时 → 右键菜单**完全禁用**(无论是否声明组件)
|
||||||
|
|
||||||
|
**插槽列表:**
|
||||||
|
|
||||||
|
| 插槽名 | 参数 | 说明 |
|
||||||
|
| --------- | --------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
|
| `default` | `scope: { row: Task, $index: number }` | 自定义TaskRow的右键菜单。通过 `scope.row` 访问当前任务对象,通过 `scope.$index` 访问任务索引。 |
|
||||||
|
|
||||||
|
**使用示例:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-list-context-menu="true"
|
||||||
|
>
|
||||||
|
<TaskListContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="custom-item" @click="handleEdit('extend', scope.row)">
|
||||||
|
✏️ 编辑任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-item" @click="handleDelete('move', scope.row)">
|
||||||
|
🗑️ 删除任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-divider"></div>
|
||||||
|
<div class="custom-item" @click="handleDuplicate('copy', scope.row)">
|
||||||
|
📄 复制任务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart, TaskListContextMenu } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
import type { Task } from 'jordium-gantt-vue3'
|
||||||
|
|
||||||
|
const tasks = ref<Task[]>([])
|
||||||
|
|
||||||
|
const handleEdit = (task: Task) => {
|
||||||
|
console.log('编辑任务:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (task: Task) => {
|
||||||
|
console.log('删除任务:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDuplicate = (task: Task) => {
|
||||||
|
console.log('复制任务:', task)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 9999;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
##### TaskBarContextMenu 插槽
|
||||||
|
|
||||||
|
用于自定义 TaskBar(时间轴任务条)的右键菜单内容。
|
||||||
|
|
||||||
|
**菜单显示逻辑:**
|
||||||
|
- 当 `enableTaskBarContextMenu=true` 且**未声明** TaskBarContextMenu 组件时 → 使用**系统内置**的右键菜单
|
||||||
|
- 当 `enableTaskBarContextMenu=true` 且**声明了** TaskBarContextMenu 组件时 → 使用**自定义**的右键菜单
|
||||||
|
- 当 `enableTaskBarContextMenu=false` 时 → 右键菜单**完全禁用**(无论是否声明组件)
|
||||||
|
|
||||||
|
**插槽列表:**
|
||||||
|
|
||||||
|
| 插槽名 | 参数 | 说明 |
|
||||||
|
| --------- | --------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
|
| `default` | `scope: { row: Task, $index: number }` | 自定义TaskBar的右键菜单。通过 `scope.row` 访问当前任务对象,通过 `scope.$index` 访问任务索引。 |
|
||||||
|
|
||||||
|
**使用示例:**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<GanttChart
|
||||||
|
:tasks="tasks"
|
||||||
|
:enable-task-bar-context-menu="true"
|
||||||
|
>
|
||||||
|
<TaskBarContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-context-menu">
|
||||||
|
<div class="menu-item" @click="handleViewDetails(scope.row)">
|
||||||
|
👁️ 查看详情
|
||||||
|
</div>
|
||||||
|
<div class="menu-item" @click="handleAdjustTime(scope.row)">
|
||||||
|
⏰ 调整时间
|
||||||
|
</div>
|
||||||
|
<div class="menu-divider"></div>
|
||||||
|
<div class="menu-item" @click="handleSetDependency(scope.row)">
|
||||||
|
🔗 设置依赖
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
|
</GanttChart>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { GanttChart, TaskBarContextMenu } from 'jordium-gantt-vue3'
|
||||||
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
import type { Task } from 'jordium-gantt-vue3'
|
||||||
|
|
||||||
|
const tasks = ref<Task[]>([])
|
||||||
|
|
||||||
|
const handleViewDetails = (task: Task) => {
|
||||||
|
console.log('查看详情:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdjustTime = (task: Task) => {
|
||||||
|
console.log('调整时间:', task)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetDependency = (task: Task) => {
|
||||||
|
console.log('设置依赖:', task)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 9999;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **💡 使用场景**:
|
||||||
|
>
|
||||||
|
> - 自定义右键菜单样式和布局
|
||||||
|
> - 添加业务特定的操作项
|
||||||
|
> - 根据任务状态动态显示菜单项
|
||||||
|
> - 添加权限控制逻辑
|
||||||
|
> - 集成第三方 UI 组件库的菜单组件
|
||||||
|
|
||||||
|
> **⚠️ 注意事项**:
|
||||||
|
>
|
||||||
|
> - 当 `enableTaskListContextMenu=false` 或 `enableTaskBarContextMenu=false` 时,即使声明了组件也不会显示菜单
|
||||||
|
> - 右键菜单会在滚动或点击菜单外部时自动关闭
|
||||||
|
> - 推荐使用 `<Teleport to="body">` 将菜单渲染到 body 下,避免定位和层级问题
|
||||||
|
> - 记得在菜单项点击后调用 `onClose()` 关闭菜单
|
||||||
|
> - TaskRow 和 TaskBar 的右键菜单是独立的,可以分别自定义
|
||||||
|
> - 默认情况下,系统会提供内置的右键菜单,包含常用操作
|
||||||
|
> - 声明式组件不会渲染任何内容,仅用于传递配置
|
||||||
|
|
||||||
##### TaskListColumn 插槽
|
##### TaskListColumn 插槽
|
||||||
|
|
||||||
`TaskListColumn` 组件提供了两个插槽,用于自定义任务列表的列头和单元格内容。必须在声明式模式(`taskListColumnRenderMode="declarative"`)下使用。
|
`TaskListColumn` 组件提供了两个插槽,用于自定义任务列表的列头和单元格内容。必须在声明式模式(`taskListColumnRenderMode="declarative"`)下使用。
|
||||||
|
|||||||
+29
-22
@@ -735,9 +735,9 @@ const handleTaskRowMoved = async (payload: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 自定义右键菜单操作处理
|
// 自定义右键菜单操作处理
|
||||||
const handleCustomMenuAction = (action: string, task: Task, onClose: () => void) => {
|
const handleCustomMenuAction = (action: string, task: Task) => {
|
||||||
showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
|
showMessage(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
|
||||||
onClose()
|
// 菜单会自动关闭(通过点击外部或滚动)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1239,7 +1239,6 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
:on-language-change="handleLanguageChange"
|
:on-language-change="handleLanguageChange"
|
||||||
:on-theme-change="handleThemeChange"
|
:on-theme-change="handleThemeChange"
|
||||||
:task-list-column-render-mode="taskListColumnRenderMode"
|
:task-list-column-render-mode="taskListColumnRenderMode"
|
||||||
:use-default-context-menu="true"
|
|
||||||
@milestone-saved="handleMilestoneSaved"
|
@milestone-saved="handleMilestoneSaved"
|
||||||
@milestone-deleted="handleMilestoneDeleted"
|
@milestone-deleted="handleMilestoneDeleted"
|
||||||
@milestone-icon-changed="handleMilestoneIconChanged"
|
@milestone-icon-changed="handleMilestoneIconChanged"
|
||||||
@@ -1371,35 +1370,43 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void)
|
|||||||
<TaskListColumn prop="startDate" :label="t.startDate" width="200" align="center" />
|
<TaskListColumn prop="startDate" :label="t.startDate" width="200" align="center" />
|
||||||
<TaskListColumn prop="endDate" :label="t.endDate" width="200" align="center" />
|
<TaskListColumn prop="endDate" :label="t.endDate" width="200" align="center" />
|
||||||
|
|
||||||
<!-- 自定义 TaskBar 右键菜单 - 声明此 slot 将使用自定义菜单 -->
|
<!-- 使用声明式的 TaskListContextMenu 组件 - 推荐方式 -->
|
||||||
<template #task-list-context-menu="{ task, position, visible, onClose }">
|
<TaskListContextMenu :task-type="['task']">
|
||||||
<Teleport to="body">
|
<template #default="scope">
|
||||||
<div
|
<div class="custom-menu">
|
||||||
v-if="visible && task"
|
<div class="custom-menu-header">声明式 TaskList 菜单</div>
|
||||||
class="custom-menu"
|
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', scope.row)">
|
||||||
:style="{
|
|
||||||
left: `${position.x}px`,
|
|
||||||
top: `${position.y}px`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="custom-menu-header">自定义甘特图菜单</div>
|
|
||||||
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', task, onClose)">
|
|
||||||
➡️ 延长任务
|
➡️ 延长任务
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-menu-item" @click="handleCustomMenuAction('move', task, onClose)">
|
<div class="custom-menu-item" @click="handleCustomMenuAction('move', scope.row)">
|
||||||
📅 移动任务
|
📅 移动任务
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-menu-divider"></div>
|
<div class="custom-menu-divider"></div>
|
||||||
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', task, onClose)">
|
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', scope.row)">
|
||||||
📄 复制任务
|
📄 复制任务
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-menu-divider"></div>
|
|
||||||
<div class="custom-menu-item" @click="onClose">❌ 取消</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
|
||||||
</template>
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
|
||||||
<!-- TaskList 不声明 slot,将使用内置默认菜单 -->
|
<!-- 使用声明式的 TaskBarContextMenu 组件 - 推荐方式 -->
|
||||||
|
<TaskBarContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="custom-menu-header">声明式 TaskBar 菜单</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', scope.row)">
|
||||||
|
➡️ 延长任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('move', scope.row)">
|
||||||
|
📅 移动任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-divider"></div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', scope.row)">
|
||||||
|
📄 复制任务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
</GanttChart>
|
</GanttChart>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -418,5 +418,21 @@
|
|||||||
"🔧 Fixed: Issue with external use of I18n",
|
"🔧 Fixed: Issue with external use of I18n",
|
||||||
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to he-shihao-3687@gitee for their use and feedback</span>"
|
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to he-shihao-3687@gitee for their use and feedback</span>"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.7.0",
|
||||||
|
"date": "2026-01-10",
|
||||||
|
"notes": [
|
||||||
|
"🎉 新增:TaskList和TaskBar声明式定义ContextMenu方式, 使用组件TaskListContextMenu和TaskBarContextMenu",
|
||||||
|
"🎉 新增:TaskListContextMenu和TaskBarContextMenu的default Slots",
|
||||||
|
"🎉 新增:TaskListContextMenu和TaskBarContextMenu是否展示属性 - enable-task-list-context-menu和enable-task-bar-context-menu",
|
||||||
|
"🎉 优化:拆分TaskList子组件代码,更易读、易维护",
|
||||||
|
"<span style=\"font-weight: bold; color: #f00;\">特别感谢 fhjfhj@gitee的使用及反馈</span>",
|
||||||
|
"🎉 Added: Declarative definition ContextMenu method for TaskList and TaskBar using the TaskListContextMenu and TaskBarContextMenu components",
|
||||||
|
"🎉 Added: default Slots for TaskListContextMenu and TaskBarContextMenu",
|
||||||
|
"🎉 Added: Properties to control the display of TaskListContextMenu and TaskBarContextMenu - enable-task-list-context-menu and enable-task-bar-context-menu",
|
||||||
|
"🎉 Optimized: Split TaskList sub-component code for better readability and maintainability",
|
||||||
|
"<span style=\"font-weight: bold; color: #f00;\">Special thanks to fhjfhj@gitee for their use and feedback</span>"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { GanttChart, TaskListColumn, useI18n } from 'jordium-gantt-vue3'
|
import { GanttChart, TaskListColumn, useI18n, TaskListContextMenu, TaskBarContextMenu } from 'jordium-gantt-vue3'
|
||||||
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
import 'jordium-gantt-vue3/dist/assets/jordium-gantt-vue3.css'
|
||||||
|
|
||||||
const { t, getTranslation } = useI18n();
|
const { t, getTranslation } = useI18n();
|
||||||
@@ -15,6 +15,26 @@ const tasks = ref([
|
|||||||
department: '管理部',
|
department: '管理部',
|
||||||
departmentCode: 'D001',
|
departmentCode: 'D001',
|
||||||
type: 'task',
|
type: 'task',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '项目启动2',
|
||||||
|
startDate: '2025-11-30',
|
||||||
|
endDate: '2025-12-5',
|
||||||
|
progress: 100,
|
||||||
|
department: '管理部',
|
||||||
|
departmentCode: 'D001',
|
||||||
|
type: 'task',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '项目启动3',
|
||||||
|
startDate: '2025-11-30',
|
||||||
|
endDate: '2025-12-5',
|
||||||
|
progress: 100,
|
||||||
|
department: '管理部',
|
||||||
|
departmentCode: 'D001',
|
||||||
|
type: 'task',
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -62,6 +82,7 @@ const customMessages = {
|
|||||||
|
|
||||||
const showAddTaskDrawer = ref(false);
|
const showAddTaskDrawer = ref(false);
|
||||||
const showAddMilestoneDialog = ref(false);
|
const showAddMilestoneDialog = ref(false);
|
||||||
|
const showTodayLocate = ref(true);
|
||||||
|
|
||||||
// 定义可动态配置的列
|
// 定义可动态配置的列
|
||||||
const availableColumns = ref<TaskListColumnConfig[]>([
|
const availableColumns = ref<TaskListColumnConfig[]>([
|
||||||
@@ -206,6 +227,11 @@ const onTaskAdded = (res) => {
|
|||||||
|
|
||||||
// 不需要手动push,组件已处理
|
// 不需要手动push,组件已处理
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 自定义右键菜单操作处理
|
||||||
|
const handleCustomMenuAction = (action: string, task: Task, onClose: () => void) => {
|
||||||
|
alert(`自定义操作: ${action} - 任务: ${task.name}`, 'info', { closable: true })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -222,8 +248,6 @@ const onTaskAdded = (res) => {
|
|||||||
:allow-drag-and-resize="true"
|
:allow-drag-and-resize="true"
|
||||||
:enable-task-row-move="true"
|
:enable-task-row-move="true"
|
||||||
:assignee-options="assigneeOptions"
|
:assignee-options="assigneeOptions"
|
||||||
task-list-column-render-mode="declarative"
|
|
||||||
@task-row-moved="handleTaskRowMoved"
|
|
||||||
@add-milestone="showAddMilestoneDialog = true"
|
@add-milestone="showAddMilestoneDialog = true"
|
||||||
@task-double-click="onTaskDblclick"
|
@task-double-click="onTaskDblclick"
|
||||||
@task-click="onTaskClick"
|
@task-click="onTaskClick"
|
||||||
@@ -244,12 +268,51 @@ const onTaskAdded = (res) => {
|
|||||||
</template>
|
</template>
|
||||||
</TaskListColumn>
|
</TaskListColumn>
|
||||||
<TaskListColumn prop="endDate" :label="getTranslation('gantt.planEndDate', '结束的时间')" width="250" />
|
<TaskListColumn prop="endDate" :label="getTranslation('gantt.planEndDate', '结束的时间')" width="250" />
|
||||||
|
|
||||||
|
<!-- 使用声明式的 TaskListContextMenu 组件 - 推荐方式 -->
|
||||||
|
<TaskListContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="custom-menu-header">声明式 TaskList 菜单</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', scope.row)">
|
||||||
|
➡️ 延长任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('move', scope.row)">
|
||||||
|
📅 移动任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-divider"></div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', scope.row)">
|
||||||
|
📄 复制任务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskListContextMenu>
|
||||||
|
|
||||||
|
<!-- 使用声明式的 TaskBarContextMenu 组件 - 推荐方式 -->
|
||||||
|
<TaskBarContextMenu>
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="custom-menu">
|
||||||
|
<div class="custom-menu-header">声明式 TaskBar 菜单</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('extend', scope.row)">
|
||||||
|
➡️ 延长任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('move', scope.row)">
|
||||||
|
📅 移动任务
|
||||||
|
</div>
|
||||||
|
<div class="custom-menu-divider"></div>
|
||||||
|
<div class="custom-menu-item" @click="handleCustomMenuAction('copy', scope.row)">
|
||||||
|
📄 复制任务
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TaskBarContextMenu>
|
||||||
</GanttChart>
|
</GanttChart>
|
||||||
</div>
|
</div>
|
||||||
<!-- 自定义添加任务按钮 -->
|
<!-- 自定义添加任务按钮 -->
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-primary" @click="showAddTaskDrawer = true">添加任务</button>
|
<button class="btn btn-primary" @click="showAddTaskDrawer = true">添加任务</button>
|
||||||
<button class="btn btn-primary" @click="showAddMilestoneDialog = true">添加里程碑</button>
|
<button class="btn btn-primary" @click="showAddMilestoneDialog = true">添加里程碑</button>
|
||||||
|
<button class="btn btn-primary" @click="showTodayLocate = !showTodayLocate">开启/关闭今日按钮</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
<!-- 自定义抽屉组件 (原生HTML替代 el-drawer) -->
|
||||||
@@ -458,4 +521,83 @@ const onTaskAdded = (res) => {
|
|||||||
from { transform: translateX(100%); }
|
from { transform: translateX(100%); }
|
||||||
to { transform: translateX(0); }
|
to { transform: translateX(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义右键菜单样式 */
|
||||||
|
.custom-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999999 !important;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-header {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item {
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item.danger {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item.danger:hover {
|
||||||
|
background: #fff1f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #eee;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下的自定义菜单 */
|
||||||
|
:global(html[data-theme='dark']) .custom-menu {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-header {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border-bottom-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item:hover {
|
||||||
|
background: #353535;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item.danger {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-item.danger:hover {
|
||||||
|
background: #3a2020;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme='dark']) .custom-menu-divider {
|
||||||
|
background: #444;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jordium-gantt-vue3",
|
"name": "jordium-gantt-vue3",
|
||||||
"version": "1.6.2",
|
"version": "1.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
"main": "npm-package/dist/jordium-gantt-vue3.cjs.js",
|
||||||
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
"module": "npm-package/dist/jordium-gantt-vue3.es.js",
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import jsPDF from 'jspdf'
|
|||||||
import html2canvas from 'html2canvas'
|
import html2canvas from 'html2canvas'
|
||||||
import type { Task } from '../models/classes/Task'
|
import type { Task } from '../models/classes/Task'
|
||||||
import type { Milestone } from '../models/classes/Milestone'
|
import type { Milestone } from '../models/classes/Milestone'
|
||||||
|
import { useTaskListContextMenu } from './TaskList/composables/taskList/useTaskListContextMenu'
|
||||||
|
import { useTaskBarContextMenu } from './Timeline/composables/useTaskBarContextMenu'
|
||||||
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
import type { ToolbarConfig } from '../models/configs/ToolbarConfig'
|
||||||
import type { TaskListConfig } from '../models/configs/TaskListConfig'
|
import type { TaskListConfig } from '../models/configs/TaskListConfig'
|
||||||
import {
|
import {
|
||||||
@@ -54,7 +56,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
assigneeOptions: () => [],
|
assigneeOptions: () => [],
|
||||||
taskListRowClassName: undefined,
|
taskListRowClassName: undefined,
|
||||||
taskListRowStyle: undefined,
|
taskListRowStyle: undefined,
|
||||||
useDefaultContextMenu: true,
|
enableTaskListContextMenu: true,
|
||||||
|
enableTaskBarContextMenu: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
@@ -94,29 +97,24 @@ const slots = useSlots()
|
|||||||
provide('gantt-column-slots', slots)
|
provide('gantt-column-slots', slots)
|
||||||
|
|
||||||
// 提供右键菜单配置给子组件
|
// 提供右键菜单配置给子组件
|
||||||
provide('use-default-context-menu', computed(() => props.useDefaultContextMenu))
|
provide('enable-task-list-context-menu', computed(() => props.enableTaskListContextMenu))
|
||||||
|
provide('enable-task-bar-context-menu', computed(() => props.enableTaskBarContextMenu))
|
||||||
|
|
||||||
// 计算是否使用自定义 TaskList 右键菜单
|
// 使用声明式右键菜单 composables
|
||||||
const hasTaskListContextMenuSlot = computed(() => Boolean(slots['task-list-context-menu']))
|
const { hasDeclarativeContextMenu: hasDeclarativeTaskListContextMenu, declarativeContextMenu: declarativeTaskListContextMenu } =
|
||||||
const shouldUseCustomTaskListContextMenu = computed(() => {
|
useTaskListContextMenu(slots)
|
||||||
if (!props.useDefaultContextMenu) {
|
const { hasDeclarativeContextMenu: hasDeclarativeTaskBarContextMenu, declarativeContextMenu: declarativeTaskBarContextMenu } =
|
||||||
return hasTaskListContextMenuSlot.value
|
useTaskBarContextMenu(slots)
|
||||||
}
|
|
||||||
return hasTaskListContextMenuSlot.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算是否使用自定义 TaskBar 右键菜单
|
// 计算是否使用自定义 TaskList 右键菜单(声明式方式)
|
||||||
const hasTaskBarContextMenuSlot = computed(() => Boolean(slots['task-bar-context-menu']))
|
const hasTaskListContextMenuSlot = computed(() => hasDeclarativeTaskListContextMenu.value)
|
||||||
const shouldUseCustomTaskBarContextMenu = computed(() => {
|
provide('task-list-context-menu-slot', hasTaskListContextMenuSlot)
|
||||||
if (!props.useDefaultContextMenu) {
|
provide('declarative-task-list-context-menu', declarativeTaskListContextMenu)
|
||||||
return hasTaskBarContextMenuSlot.value
|
|
||||||
}
|
|
||||||
return hasTaskBarContextMenuSlot.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 提供右键菜单 slot 给子组件
|
// 计算是否使用自定义 TaskBar 右键菜单(声明式方式)
|
||||||
provide('task-list-context-menu-slot', computed(() => shouldUseCustomTaskListContextMenu.value))
|
const hasTaskBarContextMenuSlot = computed(() => hasDeclarativeTaskBarContextMenu.value)
|
||||||
provide('task-bar-context-menu-slot', computed(() => shouldUseCustomTaskBarContextMenu.value))
|
provide('task-bar-context-menu-slot', hasTaskBarContextMenuSlot)
|
||||||
|
provide('declarative-task-bar-context-menu', declarativeTaskBarContextMenu)
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// 任务数据
|
// 任务数据
|
||||||
@@ -181,10 +179,16 @@ interface Props {
|
|||||||
// 任务行自定义样式,支持对象或函数,优先级高于 taskListRowClassName
|
// 任务行自定义样式,支持对象或函数,优先级高于 taskListRowClassName
|
||||||
// 函数形式:(row: Task, rowIndex: number) => StyleValue
|
// 函数形式:(row: Task, rowIndex: number) => StyleValue
|
||||||
taskListRowStyle?: StyleValue | ((row: Task, rowIndex: number) => StyleValue)
|
taskListRowStyle?: StyleValue | ((row: Task, rowIndex: number) => StyleValue)
|
||||||
// 是否使用默认的右键菜单(默认为 true)
|
// 是否启用 TaskList(TaskRow)右键菜单(默认为 true)
|
||||||
// 当设置为 false 时,将关闭内置的 TaskRow 和 TaskBar 右键菜单
|
// 当设置为 true 且未声明 TaskListContextMenu 组件时,使用系统默认菜单
|
||||||
// 可以通过 task-list-context-menu 和 task-bar-context-menu 插槽自定义菜单
|
// 当设置为 true 且声明了 TaskListContextMenu 组件时,使用自定义菜单
|
||||||
useDefaultContextMenu?: boolean
|
// 当设置为 false 时,无论是否声明组件,TaskRow 右键菜单都失效
|
||||||
|
enableTaskListContextMenu?: boolean
|
||||||
|
// 是否启用 TaskBar 右键菜单(默认为 true)
|
||||||
|
// 当设置为 true 且未声明 TaskBarContextMenu 组件时,使用系统默认菜单
|
||||||
|
// 当设置为 true 且声明了 TaskBarContextMenu 组件时,使用自定义菜单
|
||||||
|
// 当设置为 false 时,无论是否声明组件,TaskBar 右键菜单都失效
|
||||||
|
enableTaskBarContextMenu?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
// TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间)
|
||||||
@@ -2389,13 +2393,6 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="rowScope">
|
||||||
<slot name="custom-task-content" v-bind="rowScope" />
|
<slot name="custom-task-content" v-bind="rowScope" />
|
||||||
</template>
|
</template>
|
||||||
<!-- 传递 task-list-context-menu slot -->
|
|
||||||
<template
|
|
||||||
v-if="$slots['task-list-context-menu']"
|
|
||||||
#task-list-context-menu="contextMenuScope"
|
|
||||||
>
|
|
||||||
<slot name="task-list-context-menu" v-bind="contextMenuScope" />
|
|
||||||
</template>
|
|
||||||
</TaskList>
|
</TaskList>
|
||||||
</div>
|
</div>
|
||||||
<div class="gantt-splitter" @mousedown="onMouseDown">
|
<div class="gantt-splitter" @mousedown="onMouseDown">
|
||||||
@@ -2454,13 +2451,6 @@ function handleMilestoneDialogDelete(milestoneId: number) {
|
|||||||
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
<template v-if="$slots['custom-task-content']" #custom-task-content="barScope">
|
||||||
<slot name="custom-task-content" v-bind="barScope" />
|
<slot name="custom-task-content" v-bind="barScope" />
|
||||||
</template>
|
</template>
|
||||||
<!-- 传递 task-bar-context-menu slot -->
|
|
||||||
<template
|
|
||||||
v-if="$slots['task-bar-context-menu']"
|
|
||||||
#task-bar-context-menu="contextMenuScope"
|
|
||||||
>
|
|
||||||
<slot name="task-bar-context-menu" v-bind="contextMenuScope" />
|
|
||||||
</template>
|
|
||||||
</Timeline>
|
</Timeline>
|
||||||
|
|
||||||
<!-- 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 -->
|
<!-- 关闭聚焦按钮 - 固定在gantt-panel-right底部居中 -->
|
||||||
|
|||||||
+66
-48
@@ -17,6 +17,7 @@ interface Props {
|
|||||||
startDate: Date
|
startDate: Date
|
||||||
isParent?: boolean
|
isParent?: boolean
|
||||||
onClick?: (task: Task, event: MouseEvent) => void
|
onClick?: (task: Task, event: MouseEvent) => void
|
||||||
|
rowIndex?: number
|
||||||
// 新增:用于粘性文字显示的滚动位置信息
|
// 新增:用于粘性文字显示的滚动位置信息
|
||||||
scrollLeft?: number
|
scrollLeft?: number
|
||||||
containerWidth?: number
|
containerWidth?: number
|
||||||
@@ -92,47 +93,50 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
'custom-task-content'(props: TaskBarSlotProps): unknown
|
'custom-task-content'(props: TaskBarSlotProps): unknown
|
||||||
'task-bar-context-menu'(props: {
|
|
||||||
task: Task
|
|
||||||
position: { x: number; y: number }
|
|
||||||
visible: boolean
|
|
||||||
allTasks?: Task[]
|
|
||||||
onClose: () => void
|
|
||||||
onStartTimer: () => void
|
|
||||||
onStopTimer: () => void
|
|
||||||
onAddPredecessor: () => void
|
|
||||||
onAddSuccessor: () => void
|
|
||||||
onDelete: (task: Task, deleteChildren?: boolean) => void
|
|
||||||
onDeleteLink: (event: { sourceTaskId: number; targetTaskId: number }) => void
|
|
||||||
}): unknown
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
// 注入右键菜单配置
|
// 注入右键菜单配置
|
||||||
const useDefaultContextMenu = inject<ComputedRef<boolean>>('use-default-context-menu', computed(() => true))
|
const enableTaskBarContextMenu = inject<ComputedRef<boolean>>('enable-task-bar-context-menu', computed(() => true))
|
||||||
const hasTaskBarContextMenuSlot = inject<ComputedRef<boolean>>('task-bar-context-menu-slot', computed(() => false))
|
const hasTaskBarContextMenuSlot = inject<ComputedRef<boolean>>('task-bar-context-menu-slot', computed(() => false))
|
||||||
|
const declarativeTaskBarContextMenu = inject<ComputedRef<any>>('declarative-task-bar-context-menu', computed(() => null))
|
||||||
|
|
||||||
// 判断是否应该显示任何右键菜单
|
// 判断是否应该显示任何右键菜单
|
||||||
const shouldShowAnyContextMenu = computed(() => {
|
const shouldShowAnyContextMenu = computed(() => {
|
||||||
// 如果 useDefaultContextMenu 为 false 且没有自定义 slot,则不显示任何菜单
|
// 如果 enableTaskBarContextMenu 为 false,则不显示任何菜单
|
||||||
if (!useDefaultContextMenu.value && !hasTaskBarContextMenuSlot.value) {
|
if (!enableTaskBarContextMenu.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否显示默认右键菜单(没有自定义 slot 时显示)
|
// 判断是否显示默认右键菜单(enableTaskBarContextMenu=true 且没有自定义 slot 时显示)
|
||||||
const shouldShowDefaultContextMenu = computed(() => {
|
const shouldShowDefaultContextMenu = computed(() => {
|
||||||
if (!useDefaultContextMenu.value) {
|
if (!enableTaskBarContextMenu.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return !hasTaskBarContextMenuSlot.value
|
return !hasTaskBarContextMenuSlot.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否显示自定义右键菜单(有自定义 slot 时显示)
|
// 判断是否显示自定义右键菜单(enableTaskBarContextMenu=true 且有自定义 slot 时显示)
|
||||||
const shouldShowCustomContextMenu = computed(() => {
|
const shouldShowCustomContextMenu = computed(() => {
|
||||||
return hasTaskBarContextMenuSlot.value
|
if (!enableTaskBarContextMenu.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!hasTaskBarContextMenuSlot.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 taskType 过滤
|
||||||
|
const config = declarativeTaskBarContextMenu.value
|
||||||
|
if (config?.taskType !== undefined) {
|
||||||
|
const taskType = props.task.type || 'task'
|
||||||
|
const allowedTypes = Array.isArray(config.taskType) ? config.taskType : [config.taskType]
|
||||||
|
return allowedTypes.includes(taskType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getTranslation } = useI18n()
|
const { getTranslation } = useI18n()
|
||||||
@@ -1330,6 +1334,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
// 监听全局关闭菜单事件
|
// 监听全局关闭菜单事件
|
||||||
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
document.addEventListener('click', handleDocumentClick)
|
||||||
|
|
||||||
// 清理函数
|
// 清理函数
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -1342,12 +1347,19 @@ onMounted(() => {
|
|||||||
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate)
|
||||||
window.removeEventListener('timeline-force-recalculate', handleForceRecalculate)
|
window.removeEventListener('timeline-force-recalculate', handleForceRecalculate)
|
||||||
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
document.removeEventListener('click', handleDocumentClick)
|
||||||
|
|
||||||
// 清除长按定时器
|
// 清除长按定时器
|
||||||
if (longPressTimer.value !== null) {
|
if (longPressTimer.value !== null) {
|
||||||
clearTimeout(longPressTimer.value)
|
clearTimeout(longPressTimer.value)
|
||||||
longPressTimer.value = null
|
longPressTimer.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理单击定时器
|
||||||
|
if (clickTimer !== null) {
|
||||||
|
clearTimeout(clickTimer)
|
||||||
|
clickTimer = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2303,7 +2315,8 @@ function handleContextMenu(event: MouseEvent) {
|
|||||||
|
|
||||||
// 先广播关闭所有TaskBar菜单
|
// 先广播关闭所有TaskBar菜单
|
||||||
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
||||||
if (props.task.type !== 'task' && props.task.type !== 'story') {
|
const taskType = props.task.type || 'task'
|
||||||
|
if (taskType !== 'task' && taskType !== 'story') {
|
||||||
// 为了排除里程碑类型
|
// 为了排除里程碑类型
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
contextMenuVisible.value = false
|
contextMenuVisible.value = false
|
||||||
@@ -2317,6 +2330,20 @@ function closeContextMenu() {
|
|||||||
contextMenuVisible.value = false
|
contextMenuVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理文档点击事件,点击菜单外部时关闭
|
||||||
|
function handleDocumentClick(event: MouseEvent) {
|
||||||
|
if (!contextMenuVisible.value) return
|
||||||
|
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
// 检查点击是否在右键菜单内部
|
||||||
|
const contextMenuElement = document.querySelector('.task-context-menu')
|
||||||
|
if (contextMenuElement && contextMenuElement.contains(target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
// 触发删除事件
|
// 触发删除事件
|
||||||
emit('delete', task, deleteChildren)
|
emit('delete', task, deleteChildren)
|
||||||
@@ -2370,19 +2397,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor'
|
|||||||
task: props.task,
|
task: props.task,
|
||||||
type: anchorEvent.type,
|
type: anchorEvent.type,
|
||||||
})
|
})
|
||||||
}// 监听全局关闭菜单事件
|
|
||||||
onMounted(() => {
|
|
||||||
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
|
||||||
})
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
|
||||||
|
|
||||||
// 清理单击定时器
|
|
||||||
if (clickTimer !== null) {
|
|
||||||
clearTimeout(clickTimer)
|
|
||||||
clickTimer = null
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -2594,23 +2609,26 @@ onUnmounted(() => {
|
|||||||
@delete-link="handleDeleteLink"
|
@delete-link="handleDeleteLink"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 自定义右键菜单 Slot -->
|
<!-- 声明式右键菜单 -->
|
||||||
<slot
|
<Teleport to="body">
|
||||||
v-if="shouldShowCustomContextMenu"
|
<div
|
||||||
name="task-bar-context-menu"
|
v-if="shouldShowCustomContextMenu && contextMenuVisible && declarativeTaskBarContextMenu?.defaultSlot"
|
||||||
:task="contextMenuTask"
|
class="gantt-context-menu-wrapper"
|
||||||
:position="contextMenuPosition"
|
:style="{
|
||||||
:visible="contextMenuVisible"
|
position: 'fixed',
|
||||||
:all-tasks="allTasks"
|
left: `${contextMenuPosition.x}px`,
|
||||||
:on-close="closeContextMenu"
|
top: `${contextMenuPosition.y}px`,
|
||||||
:on-start-timer="() => $emit('start-timer', props.task)"
|
zIndex: 9999,
|
||||||
:on-stop-timer="() => $emit('stop-timer', props.task)"
|
}"
|
||||||
:on-add-predecessor="() => $emit('add-predecessor', props.task)"
|
>
|
||||||
:on-add-successor="() => $emit('add-successor', props.task)"
|
<component
|
||||||
:on-delete="handleTaskDelete"
|
:is="declarativeTaskBarContextMenu.defaultSlot"
|
||||||
:on-delete-link="handleDeleteLink"
|
:row="contextMenuTask"
|
||||||
|
:$index="props.rowIndex ?? -1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tooltip 弹窗 -->
|
<!-- Tooltip 弹窗 -->
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* TaskListContextMenu 声明式右键菜单组件
|
||||||
|
* 用于在 GanttChart 中声明式定义 TaskList (TaskRow) 的右键菜单
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 显式定义组件名称,用于识别
|
||||||
|
defineOptions({
|
||||||
|
name: 'TaskListContextMenu',
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// 指定哪些任务类型显示此右键菜单
|
||||||
|
// 不设置时遵循现有逻辑,设置后仅对指定类型任务显示
|
||||||
|
taskType?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
// 定义 slot
|
||||||
|
defineSlots<{
|
||||||
|
// 默认插槽,接收菜单上下文对象
|
||||||
|
default?: (scope: {
|
||||||
|
row: any
|
||||||
|
$index: number
|
||||||
|
}) => any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 注意:此组件不渲染任何内容,仅用于声明菜单配置
|
||||||
|
// 实际渲染由 TaskRow 处理,菜单定位和显示状态由内部自动管理
|
||||||
|
// 菜单会在点击外部或滚动时自动关闭
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 此组件不渲染内容 -->
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { computed, type VNode, type Slots, type VNodeChild, type Component } from 'vue'
|
||||||
|
import type { Task } from '../../../../models/classes/Task'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声明式右键菜单配置接口
|
||||||
|
*/
|
||||||
|
export interface DeclarativeContextMenuConfig {
|
||||||
|
// slot 渲染函数
|
||||||
|
defaultSlot?: (scope: {
|
||||||
|
row: Task
|
||||||
|
$index: number
|
||||||
|
}) => VNodeChild
|
||||||
|
// 指定哪些任务类型显示此右键菜单
|
||||||
|
taskType?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 VNode 中提取 task-list-context-menu 的配置
|
||||||
|
*/
|
||||||
|
export function extractTaskListContextMenuConfig(
|
||||||
|
vnode: VNode,
|
||||||
|
): DeclarativeContextMenuConfig | null {
|
||||||
|
// 检查是否是 TaskListContextMenu 组件
|
||||||
|
let isTaskListContextMenu = false
|
||||||
|
|
||||||
|
if (typeof vnode.type === 'object') {
|
||||||
|
const component = vnode.type as Component & {
|
||||||
|
__name?: string
|
||||||
|
__file?: string
|
||||||
|
}
|
||||||
|
// 检查组件名称的多种可能性
|
||||||
|
isTaskListContextMenu =
|
||||||
|
component.name === 'TaskListContextMenu' ||
|
||||||
|
component.__name === 'TaskListContextMenu' ||
|
||||||
|
// 检查文件名(setup script 组件)
|
||||||
|
(component.__file !== undefined && component.__file.includes('TaskListContextMenu'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTaskListContextMenu) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 props
|
||||||
|
const props = vnode.props || {}
|
||||||
|
|
||||||
|
// 提取 slots
|
||||||
|
type ChildrenType =
|
||||||
|
| {
|
||||||
|
default?: (scope: {
|
||||||
|
row: Task
|
||||||
|
$index: number
|
||||||
|
}) => VNodeChild
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
const children = vnode.children as ChildrenType
|
||||||
|
|
||||||
|
const defaultSlot = children?.default
|
||||||
|
|
||||||
|
if (!defaultSlot) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultSlot,
|
||||||
|
taskType: props.taskType || props['task-type'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析声明式右键菜单配置
|
||||||
|
* 从默认 slot 的 VNode 中提取 TaskListContextMenu 的配置
|
||||||
|
*/
|
||||||
|
export function parseDeclarativeTaskListContextMenu(
|
||||||
|
slots: Slots,
|
||||||
|
): DeclarativeContextMenuConfig | null {
|
||||||
|
const defaultSlot = slots.default?.()
|
||||||
|
|
||||||
|
if (!defaultSlot || !Array.isArray(defaultSlot)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查找 TaskListContextMenu
|
||||||
|
const findContextMenu = (vnodes: VNode[]): DeclarativeContextMenuConfig | null => {
|
||||||
|
for (const vnode of vnodes) {
|
||||||
|
if (!vnode) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 Fragment 或其他 Symbol 类型,直接处理子节点
|
||||||
|
if (typeof vnode.type === 'symbol') {
|
||||||
|
if (vnode.children && Array.isArray(vnode.children)) {
|
||||||
|
const result = findContextMenu(vnode.children as VNode[])
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = extractTaskListContextMenuConfig(vnode)
|
||||||
|
if (config) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子节点
|
||||||
|
if (vnode.children && Array.isArray(vnode.children)) {
|
||||||
|
const result = findContextMenu(vnode.children as VNode[])
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return findContextMenu(defaultSlot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskList Context Menu Composable
|
||||||
|
* 用于管理 TaskList 的右键菜单(支持声明式)
|
||||||
|
*/
|
||||||
|
export function useTaskListContextMenu(slots: Slots) {
|
||||||
|
// 声明式菜单配置
|
||||||
|
const declarativeContextMenu = computed(() => {
|
||||||
|
return parseDeclarativeTaskListContextMenu(slots)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否有声明式菜单
|
||||||
|
const hasDeclarativeContextMenu = computed(() => {
|
||||||
|
return !!declarativeContextMenu.value
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
declarativeContextMenu,
|
||||||
|
hasDeclarativeContextMenu,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,6 +149,9 @@ export function useTaskListEventHandlers(options: TaskListEventHandlersOptions)
|
|||||||
const scrollTop = target.scrollTop
|
const scrollTop = target.scrollTop
|
||||||
taskListScrollTop.value = scrollTop
|
taskListScrollTop.value = scrollTop
|
||||||
|
|
||||||
|
// 滚动时关闭所有右键菜单
|
||||||
|
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent('task-list-vertical-scroll', {
|
new CustomEvent('task-list-vertical-scroll', {
|
||||||
detail: { scrollTop },
|
detail: { scrollTop },
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ export function useTaskRowContextMenu(
|
|||||||
const handleContextMenu = (event: MouseEvent) => {
|
const handleContextMenu = (event: MouseEvent) => {
|
||||||
// 先广播关闭所有TaskRow菜单
|
// 先广播关闭所有TaskRow菜单
|
||||||
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
||||||
if (task.value.type !== 'task' && task.value.type !== 'story') {
|
const taskType = task.value.type || 'task'
|
||||||
|
if (taskType !== 'task' && taskType !== 'story') {
|
||||||
// 为了排除里程碑类型
|
// 为了排除里程碑类型
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
contextMenuVisible.value = false
|
contextMenuVisible.value = false
|
||||||
@@ -95,6 +96,20 @@ export function useTaskRowContextMenu(
|
|||||||
contextMenuVisible.value = false
|
contextMenuVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理文档点击事件,点击菜单外部时关闭
|
||||||
|
const handleDocumentClick = (event: MouseEvent) => {
|
||||||
|
if (!contextMenuVisible.value) return
|
||||||
|
|
||||||
|
const target = event.target as HTMLElement
|
||||||
|
// 检查点击是否在右键菜单内部
|
||||||
|
const contextMenuElement = document.querySelector('.task-context-menu')
|
||||||
|
if (contextMenuElement && contextMenuElement.contains(target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeContextMenu()
|
||||||
|
}
|
||||||
|
|
||||||
// 处理任务删除
|
// 处理任务删除
|
||||||
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
const handleTaskDelete = (task: Task, deleteChildren?: boolean) => {
|
||||||
emit('delete', task, deleteChildren)
|
emit('delete', task, deleteChildren)
|
||||||
@@ -104,10 +119,12 @@ export function useTaskRowContextMenu(
|
|||||||
// 生命周期钩子 - 注册事件监听器
|
// 生命周期钩子 - 注册事件监听器
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
window.addEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
document.addEventListener('click', handleDocumentClick)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
window.removeEventListener('close-all-taskbar-menus', closeContextMenu)
|
||||||
|
document.removeEventListener('click', handleDocumentClick)
|
||||||
// 清理定时器和watch
|
// 清理定时器和watch
|
||||||
if (timerInterval.value) {
|
if (timerInterval.value) {
|
||||||
clearInterval(timerInterval.value)
|
clearInterval(timerInterval.value)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function useTaskRowDeclarativeColumns(
|
|||||||
isStoryTask: Ref<boolean>,
|
isStoryTask: Ref<boolean>,
|
||||||
hasChildren: Ref<boolean>,
|
hasChildren: Ref<boolean>,
|
||||||
showTaskIcon: Ref<boolean | undefined>,
|
showTaskIcon: Ref<boolean | undefined>,
|
||||||
|
rowIndex: Ref<number | undefined>,
|
||||||
) {
|
) {
|
||||||
// 判断是否是第一列
|
// 判断是否是第一列
|
||||||
const isFirstColumn = (index: number) => index === 0
|
const isFirstColumn = (index: number) => index === 0
|
||||||
@@ -47,7 +48,7 @@ export function useTaskRowDeclarativeColumns(
|
|||||||
},
|
},
|
||||||
[column.defaultSlot({
|
[column.defaultSlot({
|
||||||
row: task.value,
|
row: task.value,
|
||||||
$index: index,
|
$index: rowIndex.value ?? -1,
|
||||||
})],
|
})],
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
@@ -128,7 +129,7 @@ export function useTaskRowDeclarativeColumns(
|
|||||||
if (column.defaultSlot) {
|
if (column.defaultSlot) {
|
||||||
return column.defaultSlot({
|
return column.defaultSlot({
|
||||||
row: task.value,
|
row: task.value,
|
||||||
$index: index,
|
$index: rowIndex.value ?? -1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,17 +72,6 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
defineSlots<{
|
defineSlots<{
|
||||||
'custom-task-content'(props: TaskRowSlotProps): unknown
|
'custom-task-content'(props: TaskRowSlotProps): unknown
|
||||||
'task-list-context-menu'(props: {
|
|
||||||
task: Task
|
|
||||||
position: { x: number; y: number }
|
|
||||||
visible: boolean
|
|
||||||
onClose: () => void
|
|
||||||
onStartTimer: () => void
|
|
||||||
onStopTimer: () => void
|
|
||||||
onAddPredecessor: () => void
|
|
||||||
onAddSuccessor: () => void
|
|
||||||
onDelete: (task: Task, deleteChildren?: boolean) => void
|
|
||||||
}): unknown
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -94,29 +83,45 @@ const slots = useSlots()
|
|||||||
const hasContentSlot = computed(() => Boolean(slots['custom-task-content']))
|
const hasContentSlot = computed(() => Boolean(slots['custom-task-content']))
|
||||||
|
|
||||||
// 注入右键菜单配置
|
// 注入右键菜单配置
|
||||||
const useDefaultContextMenu = inject<ComputedRef<boolean>>('use-default-context-menu', computed(() => true))
|
const enableTaskListContextMenu = inject<ComputedRef<boolean>>('enable-task-list-context-menu', computed(() => true))
|
||||||
const hasTaskListContextMenuSlot = inject<ComputedRef<boolean>>('task-list-context-menu-slot', computed(() => false))
|
const hasTaskListContextMenuSlot = inject<ComputedRef<boolean>>('task-list-context-menu-slot', computed(() => false))
|
||||||
|
const declarativeTaskListContextMenu = inject<ComputedRef<any>>('declarative-task-list-context-menu', computed(() => null))
|
||||||
|
|
||||||
// 判断是否应该显示任何右键菜单
|
// 判断是否应该显示任何右键菜单
|
||||||
const shouldShowAnyContextMenu = computed(() => {
|
const shouldShowAnyContextMenu = computed(() => {
|
||||||
// 如果 useDefaultContextMenu 为 false 且没有自定义 slot,则不显示任何菜单
|
// 如果 enableTaskListContextMenu 为 false,则不显示任何菜单
|
||||||
if (!useDefaultContextMenu.value && !hasTaskListContextMenuSlot.value) {
|
if (!enableTaskListContextMenu.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否显示默认右键菜单(没有自定义 slot 时显示)
|
// 判断是否显示默认右键菜单(enableTaskListContextMenu=true 且没有自定义 slot 时显示)
|
||||||
const shouldShowDefaultContextMenu = computed(() => {
|
const shouldShowDefaultContextMenu = computed(() => {
|
||||||
if (!useDefaultContextMenu.value) {
|
if (!enableTaskListContextMenu.value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return !hasTaskListContextMenuSlot.value
|
return !hasTaskListContextMenuSlot.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// 判断是否显示自定义右键菜单(有自定义 slot 时显示)
|
// 判断是否显示自定义右键菜单(enableTaskListContextMenu=true 且有自定义 slot 时显示)
|
||||||
const shouldShowCustomContextMenu = computed(() => {
|
const shouldShowCustomContextMenu = computed(() => {
|
||||||
return hasTaskListContextMenuSlot.value
|
if (!enableTaskListContextMenu.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!hasTaskListContextMenuSlot.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 taskType 过滤
|
||||||
|
const config = declarativeTaskListContextMenu.value
|
||||||
|
if (config?.taskType !== undefined) {
|
||||||
|
const taskType = props.task.type || 'task'
|
||||||
|
const allowedTypes = Array.isArray(config.taskType) ? config.taskType : [config.taskType]
|
||||||
|
return allowedTypes.includes(taskType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// 性能优化:只对会变化的props使用toRef,其他直接传值
|
// 性能优化:只对会变化的props使用toRef,其他直接传值
|
||||||
@@ -143,6 +148,7 @@ const { isFirstColumn, getDeclarativeColumnAlign, renderDeclarativeColumn } =
|
|||||||
isStoryTask,
|
isStoryTask,
|
||||||
hasChildren,
|
hasChildren,
|
||||||
computed(() => props.showTaskIcon),
|
computed(() => props.showTaskIcon),
|
||||||
|
computed(() => props.rowIndex),
|
||||||
)
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -431,21 +437,26 @@ const slotPayload = computed(() => ({
|
|||||||
@delete="handleTaskDelete"
|
@delete="handleTaskDelete"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 自定义右键菜单 Slot -->
|
<!-- 声明式右键菜单 -->
|
||||||
<slot
|
<Teleport to="body">
|
||||||
v-if="shouldShowCustomContextMenu"
|
<div
|
||||||
name="task-list-context-menu"
|
v-if="shouldShowCustomContextMenu && contextMenuVisible && declarativeTaskListContextMenu?.defaultSlot"
|
||||||
:task="contextMenuTask"
|
class="gantt-context-menu-wrapper"
|
||||||
:position="contextMenuPosition"
|
:style="{
|
||||||
:visible="contextMenuVisible"
|
position: 'fixed',
|
||||||
:on-close="closeContextMenu"
|
left: `${contextMenuPosition.x}px`,
|
||||||
:on-start-timer="() => $emit('start-timer', props.task)"
|
top: `${contextMenuPosition.y}px`,
|
||||||
:on-stop-timer="() => $emit('stop-timer', props.task)"
|
zIndex: 9999,
|
||||||
:on-add-predecessor="() => $emit('add-predecessor', props.task)"
|
}"
|
||||||
:on-add-successor="() => $emit('add-successor', props.task)"
|
>
|
||||||
:on-delete="handleTaskDelete"
|
<component
|
||||||
|
:is="declarativeTaskListContextMenu.defaultSlot"
|
||||||
|
:row="contextMenuTask"
|
||||||
|
:$index="props.rowIndex ?? -1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3061,6 +3061,9 @@ const handleTimelineScroll = (event: Event) => {
|
|||||||
// 立即更新关键滚动位置信息(用于虚拟滚动)
|
// 立即更新关键滚动位置信息(用于虚拟滚动)
|
||||||
timelineScrollLeft.value = scrollLeft
|
timelineScrollLeft.value = scrollLeft
|
||||||
|
|
||||||
|
// 滚动时关闭所有右键菜单
|
||||||
|
window.dispatchEvent(new CustomEvent('close-all-taskbar-menus'))
|
||||||
|
|
||||||
// 优化:滚动时失效 bodyRect 缓存(用于连接线拖拽)
|
// 优化:滚动时失效 bodyRect 缓存(用于连接线拖拽)
|
||||||
bodyRectInvalidated = true
|
bodyRectInvalidated = true
|
||||||
|
|
||||||
@@ -4306,6 +4309,7 @@ const handleAddSuccessor = (task: Task) => {
|
|||||||
v-else-if="task.type !== 'milestone-group' && task.type !== 'milestone'"
|
v-else-if="task.type !== 'milestone-group' && task.type !== 'milestone'"
|
||||||
:key="`taskbar-${task.id}-${taskBarRenderKey}`"
|
:key="`taskbar-${task.id}-${taskBarRenderKey}`"
|
||||||
:task="task"
|
:task="task"
|
||||||
|
:row-index="originalIndex"
|
||||||
:row-height="50"
|
:row-height="50"
|
||||||
:day-width="dayWidth"
|
:day-width="dayWidth"
|
||||||
:start-date="
|
:start-date="
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* TaskBarContextMenu 声明式右键菜单组件
|
||||||
|
* 用于在 GanttChart 中声明式定义 TaskBar 的右键菜单
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 显式定义组件名称,用于识别
|
||||||
|
defineOptions({
|
||||||
|
name: 'TaskBarContextMenu',
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
// 指定哪些任务类型显示此右键菜单
|
||||||
|
// 不设置时遵循现有逻辑,设置后仅对指定类型任务显示
|
||||||
|
taskType?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
// 定义 slot
|
||||||
|
defineSlots<{
|
||||||
|
// 默认插槽,接收菜单上下文对象
|
||||||
|
default?: (scope: {
|
||||||
|
row: any
|
||||||
|
$index: number
|
||||||
|
}) => any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 注意:此组件不渲染任何内容,仅用于声明菜单配置
|
||||||
|
// 实际渲染由 TaskBar 处理,菜单定位和显示状态由内部自动管理
|
||||||
|
// 菜单会在点击外部或滚动时自动关闭
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 此组件不渲染内容 -->
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { computed, type VNode, type Slots, type VNodeChild, type Component } from 'vue'
|
||||||
|
import type { Task } from '../../../models/classes/Task'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 声明式右键菜单配置接口
|
||||||
|
*/
|
||||||
|
export interface DeclarativeContextMenuConfig {
|
||||||
|
// slot 渲染函数
|
||||||
|
defaultSlot?: (scope: {
|
||||||
|
row: Task
|
||||||
|
$index: number
|
||||||
|
}) => VNodeChild
|
||||||
|
// 指定哪些任务类型显示此右键菜单
|
||||||
|
taskType?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 VNode 中提取 task-bar-context-menu 的配置
|
||||||
|
*/
|
||||||
|
export function extractTaskBarContextMenuConfig(vnode: VNode): DeclarativeContextMenuConfig | null {
|
||||||
|
// 检查是否是 TaskBarContextMenu 组件
|
||||||
|
let isTaskBarContextMenu = false
|
||||||
|
|
||||||
|
if (typeof vnode.type === 'object') {
|
||||||
|
const component = vnode.type as Component & {
|
||||||
|
__name?: string
|
||||||
|
__file?: string
|
||||||
|
}
|
||||||
|
// 检查组件名称的多种可能性
|
||||||
|
isTaskBarContextMenu =
|
||||||
|
component.name === 'TaskBarContextMenu' ||
|
||||||
|
component.__name === 'TaskBarContextMenu' ||
|
||||||
|
// 检查文件名(setup script 组件)
|
||||||
|
(component.__file !== undefined && component.__file.includes('TaskBarContextMenu'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTaskBarContextMenu) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 props
|
||||||
|
const props = vnode.props || {}
|
||||||
|
|
||||||
|
// 提取 slots
|
||||||
|
type ChildrenType =
|
||||||
|
| {
|
||||||
|
default?: (scope: {
|
||||||
|
row: Task
|
||||||
|
$index: number
|
||||||
|
}) => VNodeChild
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
const children = vnode.children as ChildrenType
|
||||||
|
|
||||||
|
const defaultSlot = children?.default
|
||||||
|
|
||||||
|
if (!defaultSlot) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultSlot,
|
||||||
|
taskType: props.taskType || props['task-type'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析声明式右键菜单配置
|
||||||
|
* 从默认 slot 的 VNode 中提取 TaskBarContextMenu 的配置
|
||||||
|
*/
|
||||||
|
export function parseDeclarativeTaskBarContextMenu(
|
||||||
|
slots: Slots,
|
||||||
|
): DeclarativeContextMenuConfig | null {
|
||||||
|
const defaultSlot = slots.default?.()
|
||||||
|
|
||||||
|
if (!defaultSlot || !Array.isArray(defaultSlot)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归查找 TaskBarContextMenu
|
||||||
|
const findContextMenu = (vnodes: VNode[]): DeclarativeContextMenuConfig | null => {
|
||||||
|
for (const vnode of vnodes) {
|
||||||
|
if (!vnode) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是 Fragment 或其他 Symbol 类型,直接处理子节点
|
||||||
|
if (typeof vnode.type === 'symbol') {
|
||||||
|
if (vnode.children && Array.isArray(vnode.children)) {
|
||||||
|
const result = findContextMenu(vnode.children as VNode[])
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = extractTaskBarContextMenuConfig(vnode)
|
||||||
|
if (config) {
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子节点
|
||||||
|
if (vnode.children && Array.isArray(vnode.children)) {
|
||||||
|
const result = findContextMenu(vnode.children as VNode[])
|
||||||
|
if (result) return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return findContextMenu(defaultSlot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TaskBar Context Menu Composable
|
||||||
|
* 用于管理 TaskBar 的右键菜单(支持声明式)
|
||||||
|
*/
|
||||||
|
export function useTaskBarContextMenu(slots: Slots) {
|
||||||
|
// 声明式菜单配置
|
||||||
|
const declarativeContextMenu = computed(() => {
|
||||||
|
return parseDeclarativeTaskBarContextMenu(slots)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 是否有声明式菜单
|
||||||
|
const hasDeclarativeContextMenu = computed(() => {
|
||||||
|
return !!declarativeContextMenu.value
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
declarativeContextMenu,
|
||||||
|
hasDeclarativeContextMenu,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ export { default as GanttChart } from './GanttChart.vue'
|
|||||||
export { default as GanttToolbar } from './GanttToolbar.vue'
|
export { default as GanttToolbar } from './GanttToolbar.vue'
|
||||||
export { default as TaskList } from './TaskList/TaskList.vue'
|
export { default as TaskList } from './TaskList/TaskList.vue'
|
||||||
export { default as TaskListColumn } from './TaskList/TaskListColumn.vue'
|
export { default as TaskListColumn } from './TaskList/TaskListColumn.vue'
|
||||||
|
export { default as TaskListContextMenu } from './TaskList/TaskListContextMenu.vue'
|
||||||
export { default as TaskRow } from './TaskList/taskRow/TaskRow.vue'
|
export { default as TaskRow } from './TaskList/taskRow/TaskRow.vue'
|
||||||
export { default as Timeline } from './Timeline.vue'
|
export { default as Timeline } from './Timeline.vue'
|
||||||
export { default as TaskBar } from './TaskBar.vue'
|
export { default as TaskBar } from './TaskBar.vue'
|
||||||
|
export { default as TaskBarContextMenu } from './Timeline/TaskBarContextMenu.vue'
|
||||||
export { default as MilestonePoint } from './MilestonePoint.vue'
|
export { default as MilestonePoint } from './MilestonePoint.vue'
|
||||||
export { default as MilestoneDialog } from './MilestoneDialog.vue'
|
export { default as MilestoneDialog } from './MilestoneDialog.vue'
|
||||||
export { default as TaskDrawer } from './TaskDrawer.vue'
|
export { default as TaskDrawer } from './TaskDrawer.vue'
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
export { default as GanttChart } from './components/GanttChart.vue'
|
export { default as GanttChart } from './components/GanttChart.vue'
|
||||||
export { default as TaskList } from './components/TaskList/TaskList.vue'
|
export { default as TaskList } from './components/TaskList/TaskList.vue'
|
||||||
export { default as TaskListColumn } from './components/TaskList/TaskListColumn.vue'
|
export { default as TaskListColumn } from './components/TaskList/TaskListColumn.vue'
|
||||||
|
export { default as TaskListContextMenu } from './components/TaskList/TaskListContextMenu.vue'
|
||||||
export { default as Timeline } from './components/Timeline.vue'
|
export { default as Timeline } from './components/Timeline.vue'
|
||||||
export { default as TaskBar } from './components/TaskBar.vue'
|
export { default as TaskBar } from './components/TaskBar.vue'
|
||||||
|
export { default as TaskBarContextMenu } from './components/Timeline/TaskBarContextMenu.vue'
|
||||||
export { default as TaskDrawer } from './components/TaskDrawer.vue'
|
export { default as TaskDrawer } from './components/TaskDrawer.vue'
|
||||||
export { default as MilestonePoint } from './components/MilestonePoint.vue'
|
export { default as MilestonePoint } from './components/MilestonePoint.vue'
|
||||||
export { default as MilestoneDialog } from './components/MilestoneDialog.vue'
|
export { default as MilestoneDialog } from './components/MilestoneDialog.vue'
|
||||||
@@ -31,10 +33,14 @@ import './styles/theme-variables.css'
|
|||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
import GanttChart from './components/GanttChart.vue'
|
import GanttChart from './components/GanttChart.vue'
|
||||||
import TaskListColumn from './components/TaskList/TaskListColumn.vue'
|
import TaskListColumn from './components/TaskList/TaskListColumn.vue'
|
||||||
|
import TaskListContextMenu from './components/TaskList/TaskListContextMenu.vue'
|
||||||
|
import TaskBarContextMenu from './components/Timeline/TaskBarContextMenu.vue'
|
||||||
|
|
||||||
export const install = (app: App) => {
|
export const install = (app: App) => {
|
||||||
app.component('GanttChart', GanttChart)
|
app.component('GanttChart', GanttChart)
|
||||||
app.component('TaskListColumn', TaskListColumn)
|
app.component('TaskListColumn', TaskListColumn)
|
||||||
|
app.component('TaskListContextMenu', TaskListContextMenu)
|
||||||
|
app.component('TaskBarContextMenu', TaskBarContextMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
Reference in New Issue
Block a user