diff --git a/CHANGELOG.md b/CHANGELOG.md index 978e24b..72c1293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), 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 ### Fixed diff --git a/README-EN.md b/README-EN.md index 8cded0a..47660c7 100644 --- a/README-EN.md +++ b/README-EN.md @@ -197,6 +197,8 @@ npm run dev | `autoSortByStartDate` | `boolean` | `false` | Whether to automatically sort tasks by start date | | `allowDragAndResize` | `boolean` | `true` | Whether to allow dragging and resizing tasks/milestones | | `enableTaskRowMove` | `boolean` | `false` | Whether to 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 | #### 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 > - 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 + + + + + + + + + + + + + + + + +``` + +> **💡 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 + + + + + + + + + + + + + + + + +``` + +> **💡 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 For complete configuration object documentation, see [⚙️ Configuration & Customization](#⚙️-configuration--customization) section. @@ -2391,6 +2497,208 @@ const props = defineProps() > - TaskRow and TaskBar have different available space, need to adapt layout > - 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 + + + + + +``` + +##### 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 + + + + + +``` + +> **💡 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 `` 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 The `TaskListColumn` component provides two slots for customizing task list column headers and cell content. Must be used in declarative mode (`taskListColumnRenderMode="declarative"`). diff --git a/README.md b/README.md index eb9096b..d2c1204 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,8 @@ npm run dev | `autoSortByStartDate` | `boolean` | `false` | 是否根据开始时间自动排序任务 | | `allowDragAndResize` | `boolean` | `true` | 是否允许拖拽和调整任务/里程碑大小 | | `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 }>` | `[]` | 任务编辑抽屉中负责人下拉菜单的选项列表 | #### TaskListColumn 属性 @@ -230,6 +232,110 @@ npm run dev > - 列的显示顺序由 `TaskListColumn` 组件的声明顺序决定 > - 关于列内容自定义和插槽的详细使用方法,请参考 [插槽 (Slots)](#插槽-slots) 章节 +#### TaskListContextMenu 属性 + +`TaskListContextMenu` 组件用于声明式定义 TaskList(TaskRow)的右键菜单。当 `enableTaskListContextMenu` 为 `true` 时生效。 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | ---------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- | +| `taskType` | `string \| string[]` | `undefined` | 指定哪些任务类型显示此右键菜单。不设置时遵循现有逻辑(所有任务都显示),设置后仅对指定类型任务显示。支持单个类型(如 `'task'`)或多个类型(如 `['task', 'milestone']`) | + +**使用示例**: + +```vue + + + + + + + + + + + + + + + + +``` + +> **💡 提示**: +> - `TaskListContextMenu` 组件本身不渲染任何内容,仅用于声明菜单配置 +> - 必须在 `GanttChart` 组件内部使用,且设置 `enable-task-list-context-menu="true"` +> - 菜单定位和显示状态由内部自动管理,用户只需关心菜单内容的 HTML 结构 +> - 菜单会在点击外部或滚动时自动关闭 +> - 关于插槽的详细使用方法,请参考 [插槽 (Slots)](#插槽-slots) 章节 + +#### TaskBarContextMenu 属性 + +`TaskBarContextMenu` 组件用于声明式定义 TaskBar(时间线任务条)的右键菜单。当 `enableTaskBarContextMenu` 为 `true` 时生效。 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | ---------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- | +| `taskType` | `string \| string[]` | `undefined` | 指定哪些任务类型显示此右键菜单。不设置时遵循现有逻辑(所有任务都显示),设置后仅对指定类型任务显示。支持单个类型(如 `'task'`)或多个类型(如 `['task', 'milestone']`) | + +**使用示例**: + +```vue + + + + + + + + + + + + + + + + +``` + +> **💡 提示**: +> - `TaskBarContextMenu` 组件本身不渲染任何内容,仅用于声明菜单配置 +> - 必须在 `GanttChart` 组件内部使用,且设置 `enable-task-bar-context-menu="true"` +> - 菜单定位和显示状态由内部自动管理,用户只需关心菜单内容的 HTML 结构 +> - 菜单会在点击外部或滚动时自动关闭 +> - 关于插槽的详细使用方法,请参考 [插槽 (Slots)](#插槽-slots) 章节 + #### 配置对象属性 完整的配置对象说明请参考 [⚙️ 配置与扩展](#⚙️-配置与扩展) 章节。 @@ -2382,6 +2488,208 @@ const props = defineProps() > - TaskRow 和 TaskBar 的可用空间不同,需要适配布局 > - 避免在插槽内容中使用过于复杂的组件,可能影响性能 +##### TaskListContextMenu 插槽 + +用于自定义 TaskRow(任务列表行)的右键菜单内容。 + +**菜单显示逻辑:** +- 当 `enableTaskListContextMenu=true` 且**未声明** TaskListContextMenu 组件时 → 使用**系统内置**的右键菜单 +- 当 `enableTaskListContextMenu=true` 且**声明了** TaskListContextMenu 组件时 → 使用**自定义**的右键菜单 +- 当 `enableTaskListContextMenu=false` 时 → 右键菜单**完全禁用**(无论是否声明组件) + +**插槽列表:** + +| 插槽名 | 参数 | 说明 | +| --------- | --------------------------------- | ---------------------------------------------------------------------------------------------- | +| `default` | `scope: { row: Task, $index: number }` | 自定义TaskRow的右键菜单。通过 `scope.row` 访问当前任务对象,通过 `scope.$index` 访问任务索引。 | + +**使用示例:** + +```vue + + + + + +``` + +##### TaskBarContextMenu 插槽 + +用于自定义 TaskBar(时间轴任务条)的右键菜单内容。 + +**菜单显示逻辑:** +- 当 `enableTaskBarContextMenu=true` 且**未声明** TaskBarContextMenu 组件时 → 使用**系统内置**的右键菜单 +- 当 `enableTaskBarContextMenu=true` 且**声明了** TaskBarContextMenu 组件时 → 使用**自定义**的右键菜单 +- 当 `enableTaskBarContextMenu=false` 时 → 右键菜单**完全禁用**(无论是否声明组件) + +**插槽列表:** + +| 插槽名 | 参数 | 说明 | +| --------- | --------------------------------- | ---------------------------------------------------------------------------------------------- | +| `default` | `scope: { row: Task, $index: number }` | 自定义TaskBar的右键菜单。通过 `scope.row` 访问当前任务对象,通过 `scope.$index` 访问任务索引。 | + +**使用示例:** + +```vue + + + + + +``` + +> **💡 使用场景**: +> +> - 自定义右键菜单样式和布局 +> - 添加业务特定的操作项 +> - 根据任务状态动态显示菜单项 +> - 添加权限控制逻辑 +> - 集成第三方 UI 组件库的菜单组件 + +> **⚠️ 注意事项**: +> +> - 当 `enableTaskListContextMenu=false` 或 `enableTaskBarContextMenu=false` 时,即使声明了组件也不会显示菜单 +> - 右键菜单会在滚动或点击菜单外部时自动关闭 +> - 推荐使用 `` 将菜单渲染到 body 下,避免定位和层级问题 +> - 记得在菜单项点击后调用 `onClose()` 关闭菜单 +> - TaskRow 和 TaskBar 的右键菜单是独立的,可以分别自定义 +> - 默认情况下,系统会提供内置的右键菜单,包含常用操作 +> - 声明式组件不会渲染任何内容,仅用于传递配置 + ##### TaskListColumn 插槽 `TaskListColumn` 组件提供了两个插槽,用于自定义任务列表的列头和单元格内容。必须在声明式模式(`taskListColumnRenderMode="declarative"`)下使用。 diff --git a/demo/App.vue b/demo/App.vue index 18148c7..e9385d2 100644 --- a/demo/App.vue +++ b/demo/App.vue @@ -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 }) - onClose() + // 菜单会自动关闭(通过点击外部或滚动) } @@ -1239,7 +1239,6 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void) :on-language-change="handleLanguageChange" :on-theme-change="handleThemeChange" :task-list-column-render-mode="taskListColumnRenderMode" - :use-default-context-menu="true" @milestone-saved="handleMilestoneSaved" @milestone-deleted="handleMilestoneDeleted" @milestone-icon-changed="handleMilestoneIconChanged" @@ -1371,35 +1370,43 @@ const handleCustomMenuAction = (action: string, task: Task, onClose: () => void) - - + - + + + + diff --git a/demo/version-history.json b/demo/version-history.json index f95f70d..eb9918c 100644 --- a/demo/version-history.json +++ b/demo/version-history.json @@ -418,5 +418,21 @@ "🔧 Fixed: Issue with external use of I18n", "Special thanks to he-shihao-3687@gitee for their use and feedback" ] + }, + { + "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子组件代码,更易读、易维护", + "特别感谢 fhjfhj@gitee的使用及反馈", + "🎉 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", + "Special thanks to fhjfhj@gitee for their use and feedback" + ] } ] diff --git a/npm-demo/src/components/GanttTest.vue b/npm-demo/src/components/GanttTest.vue index 09804aa..7e8517d 100644 --- a/npm-demo/src/components/GanttTest.vue +++ b/npm-demo/src/components/GanttTest.vue @@ -1,6 +1,6 @@ + + + + + + + + + +
+
@@ -458,4 +521,83 @@ const onTaskAdded = (res) => { from { transform: translateX(100%); } 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; +} \ No newline at end of file diff --git a/package.json b/package.json index 6e16632..f709094 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jordium-gantt-vue3", - "version": "1.6.2", + "version": "1.7.0", "type": "module", "main": "npm-package/dist/jordium-gantt-vue3.cjs.js", "module": "npm-package/dist/jordium-gantt-vue3.es.js", diff --git a/src/components/GanttChart.vue b/src/components/GanttChart.vue index 066d2fb..9824b23 100644 --- a/src/components/GanttChart.vue +++ b/src/components/GanttChart.vue @@ -13,6 +13,8 @@ import jsPDF from 'jspdf' import html2canvas from 'html2canvas' import type { Task } from '../models/classes/Task' 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 { TaskListConfig } from '../models/configs/TaskListConfig' import { @@ -54,7 +56,8 @@ const props = withDefaults(defineProps(), { assigneeOptions: () => [], taskListRowClassName: undefined, taskListRowStyle: undefined, - useDefaultContextMenu: true, + enableTaskListContextMenu: true, + enableTaskBarContextMenu: true, }) const emit = defineEmits([ @@ -94,29 +97,24 @@ const slots = useSlots() 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 右键菜单 -const hasTaskListContextMenuSlot = computed(() => Boolean(slots['task-list-context-menu'])) -const shouldUseCustomTaskListContextMenu = computed(() => { - if (!props.useDefaultContextMenu) { - return hasTaskListContextMenuSlot.value - } - return hasTaskListContextMenuSlot.value -}) +// 使用声明式右键菜单 composables +const { hasDeclarativeContextMenu: hasDeclarativeTaskListContextMenu, declarativeContextMenu: declarativeTaskListContextMenu } = + useTaskListContextMenu(slots) +const { hasDeclarativeContextMenu: hasDeclarativeTaskBarContextMenu, declarativeContextMenu: declarativeTaskBarContextMenu } = + useTaskBarContextMenu(slots) -// 计算是否使用自定义 TaskBar 右键菜单 -const hasTaskBarContextMenuSlot = computed(() => Boolean(slots['task-bar-context-menu'])) -const shouldUseCustomTaskBarContextMenu = computed(() => { - if (!props.useDefaultContextMenu) { - return hasTaskBarContextMenuSlot.value - } - return hasTaskBarContextMenuSlot.value -}) +// 计算是否使用自定义 TaskList 右键菜单(声明式方式) +const hasTaskListContextMenuSlot = computed(() => hasDeclarativeTaskListContextMenu.value) +provide('task-list-context-menu-slot', hasTaskListContextMenuSlot) +provide('declarative-task-list-context-menu', declarativeTaskListContextMenu) -// 提供右键菜单 slot 给子组件 -provide('task-list-context-menu-slot', computed(() => shouldUseCustomTaskListContextMenu.value)) -provide('task-bar-context-menu-slot', computed(() => shouldUseCustomTaskBarContextMenu.value)) +// 计算是否使用自定义 TaskBar 右键菜单(声明式方式) +const hasTaskBarContextMenuSlot = computed(() => hasDeclarativeTaskBarContextMenu.value) +provide('task-bar-context-menu-slot', hasTaskBarContextMenuSlot) +provide('declarative-task-bar-context-menu', declarativeTaskBarContextMenu) interface Props { // 任务数据 @@ -181,10 +179,16 @@ interface Props { // 任务行自定义样式,支持对象或函数,优先级高于 taskListRowClassName // 函数形式:(row: Task, rowIndex: number) => StyleValue taskListRowStyle?: StyleValue | ((row: Task, rowIndex: number) => StyleValue) - // 是否使用默认的右键菜单(默认为 true) - // 当设置为 false 时,将关闭内置的 TaskRow 和 TaskBar 右键菜单 - // 可以通过 task-list-context-menu 和 task-bar-context-menu 插槽自定义菜单 - useDefaultContextMenu?: boolean + // 是否启用 TaskList(TaskRow)右键菜单(默认为 true) + // 当设置为 true 且未声明 TaskListContextMenu 组件时,使用系统默认菜单 + // 当设置为 true 且声明了 TaskListContextMenu 组件时,使用自定义菜单 + // 当设置为 false 时,无论是否声明组件,TaskRow 右键菜单都失效 + enableTaskListContextMenu?: boolean + // 是否启用 TaskBar 右键菜单(默认为 true) + // 当设置为 true 且未声明 TaskBarContextMenu 组件时,使用系统默认菜单 + // 当设置为 true 且声明了 TaskBarContextMenu 组件时,使用自定义菜单 + // 当设置为 false 时,无论是否声明组件,TaskBar 右键菜单都失效 + enableTaskBarContextMenu?: boolean } // TaskList的固定总长度(所有列的最小宽度之和 + 边框等额外空间) @@ -2389,13 +2393,6 @@ function handleMilestoneDialogDelete(milestoneId: number) { - -
@@ -2454,13 +2451,6 @@ function handleMilestoneDialogDelete(milestoneId: number) { - - diff --git a/src/components/TaskBar.vue b/src/components/TaskBar.vue index 0b681d5..b683630 100644 --- a/src/components/TaskBar.vue +++ b/src/components/TaskBar.vue @@ -17,6 +17,7 @@ interface Props { startDate: Date isParent?: boolean onClick?: (task: Task, event: MouseEvent) => void + rowIndex?: number // 新增:用于粘性文字显示的滚动位置信息 scrollLeft?: number containerWidth?: number @@ -92,47 +93,50 @@ const emit = defineEmits([ defineSlots<{ '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 useDefaultContextMenu = inject>('use-default-context-menu', computed(() => true)) +const enableTaskBarContextMenu = inject>('enable-task-bar-context-menu', computed(() => true)) const hasTaskBarContextMenuSlot = inject>('task-bar-context-menu-slot', computed(() => false)) +const declarativeTaskBarContextMenu = inject>('declarative-task-bar-context-menu', computed(() => null)) // 判断是否应该显示任何右键菜单 const shouldShowAnyContextMenu = computed(() => { - // 如果 useDefaultContextMenu 为 false 且没有自定义 slot,则不显示任何菜单 - if (!useDefaultContextMenu.value && !hasTaskBarContextMenuSlot.value) { + // 如果 enableTaskBarContextMenu 为 false,则不显示任何菜单 + if (!enableTaskBarContextMenu.value) { return false } return true }) -// 判断是否显示默认右键菜单(没有自定义 slot 时显示) +// 判断是否显示默认右键菜单(enableTaskBarContextMenu=true 且没有自定义 slot 时显示) const shouldShowDefaultContextMenu = computed(() => { - if (!useDefaultContextMenu.value) { + if (!enableTaskBarContextMenu.value) { return false } return !hasTaskBarContextMenuSlot.value }) -// 判断是否显示自定义右键菜单(有自定义 slot 时显示) +// 判断是否显示自定义右键菜单(enableTaskBarContextMenu=true 且有自定义 slot 时显示) 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() @@ -1330,6 +1334,7 @@ onMounted(() => { // 监听全局关闭菜单事件 window.addEventListener('close-all-taskbar-menus', closeContextMenu) + document.addEventListener('click', handleDocumentClick) // 清理函数 onUnmounted(() => { @@ -1342,12 +1347,19 @@ onMounted(() => { window.removeEventListener('timeline-scale-updated', handleTimelineScaleUpdate) window.removeEventListener('timeline-force-recalculate', handleForceRecalculate) window.removeEventListener('close-all-taskbar-menus', closeContextMenu) + document.removeEventListener('click', handleDocumentClick) // 清除长按定时器 if (longPressTimer.value !== null) { clearTimeout(longPressTimer.value) longPressTimer.value = null } + + // 清理单击定时器 + if (clickTimer !== null) { + clearTimeout(clickTimer) + clickTimer = null + } }) }) @@ -2303,7 +2315,8 @@ function handleContextMenu(event: MouseEvent) { // 先广播关闭所有TaskBar菜单 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() contextMenuVisible.value = false @@ -2317,6 +2330,20 @@ function closeContextMenu() { 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) => { // 触发删除事件 emit('delete', task, deleteChildren) @@ -2370,19 +2397,7 @@ const handleAnchorDragEnd = (anchorEvent: { taskId: number; type: 'predecessor' task: props.task, 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 - } -}) +} diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 7f8a71c..cc5705c 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -3061,6 +3061,9 @@ const handleTimelineScroll = (event: Event) => { // 立即更新关键滚动位置信息(用于虚拟滚动) timelineScrollLeft.value = scrollLeft + // 滚动时关闭所有右键菜单 + window.dispatchEvent(new CustomEvent('close-all-taskbar-menus')) + // 优化:滚动时失效 bodyRect 缓存(用于连接线拖拽) bodyRectInvalidated = true @@ -4306,6 +4309,7 @@ const handleAddSuccessor = (task: Task) => { v-else-if="task.type !== 'milestone-group' && task.type !== 'milestone'" :key="`taskbar-${task.id}-${taskBarRenderKey}`" :task="task" + :row-index="originalIndex" :row-height="50" :day-width="dayWidth" :start-date=" diff --git a/src/components/Timeline/TaskBarContextMenu.vue b/src/components/Timeline/TaskBarContextMenu.vue new file mode 100644 index 0000000..2be0237 --- /dev/null +++ b/src/components/Timeline/TaskBarContextMenu.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/Timeline/composables/useTaskBarContextMenu.ts b/src/components/Timeline/composables/useTaskBarContextMenu.ts new file mode 100644 index 0000000..e153156 --- /dev/null +++ b/src/components/Timeline/composables/useTaskBarContextMenu.ts @@ -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, + } +} diff --git a/src/components/index.ts b/src/components/index.ts index 17fcbb1..a517bc3 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,9 +2,11 @@ export { default as GanttChart } from './GanttChart.vue' export { default as GanttToolbar } from './GanttToolbar.vue' export { default as TaskList } from './TaskList/TaskList.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 Timeline } from './Timeline.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 MilestoneDialog } from './MilestoneDialog.vue' export { default as TaskDrawer } from './TaskDrawer.vue' diff --git a/src/index.ts b/src/index.ts index 8fe62c8..459e4b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,10 @@ export { default as GanttChart } from './components/GanttChart.vue' export { default as TaskList } from './components/TaskList/TaskList.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 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 MilestonePoint } from './components/MilestonePoint.vue' export { default as MilestoneDialog } from './components/MilestoneDialog.vue' @@ -31,10 +33,14 @@ import './styles/theme-variables.css' import type { App } from 'vue' import GanttChart from './components/GanttChart.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) => { app.component('GanttChart', GanttChart) app.component('TaskListColumn', TaskListColumn) + app.component('TaskListContextMenu', TaskListContextMenu) + app.component('TaskBarContextMenu', TaskBarContextMenu) } export default {