From bbe73aae6437cabc8cfada24367c3d969687faa9 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Tue, 14 Jul 2026 15:55:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E6=89=A9=E5=B1=95=20customCont?= =?UTF-8?q?entMenu=20=E6=94=AF=E6=8C=81=20getTarget=20=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=8F=B3=E9=94=AE=E7=9B=AE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为数据源与代码块面板的右键菜单传入 getTarget 回调,便于业务在自定义菜单 handler 中读取当前节点 id 与原始数据。 --- docs/api/editor/props.md | 45 ++++++++++++++----- .../sidebar/code-block/CodeBlockListPanel.vue | 5 ++- .../sidebar/code-block/useContentMenu.ts | 14 +++++- .../data-source/DataSourceListPanel.vue | 4 +- .../sidebar/data-source/useContentMenu.ts | 14 +++++- packages/editor/src/type.ts | 17 ++++++- .../code-block/CodeBlockListPanel.spec.ts | 1 + .../sidebar/code-block/useContentMenu.spec.ts | 12 +++++ .../data-source/DataSourceListPanel.spec.ts | 4 +- .../data-source/useContentMenu.spec.ts | 11 +++++ 10 files changed, 110 insertions(+), 17 deletions(-) diff --git a/docs/api/editor/props.md b/docs/api/editor/props.md index 66dceec9..175ccc94 100644 --- a/docs/api/editor/props.md +++ b/docs/api/editor/props.md @@ -1365,13 +1365,34 @@ const guidesOptions = { - **详情:** - 用于自定义组件树与画布的右键菜单 + 用于自定义组件树(图层)、画布、数据源、代码块的右键菜单 - 该函数会在显示右键菜单前被调用,接收默认菜单项作为参数,返回最终显示的菜单项 + 该函数会在显示右键菜单前被调用,接收默认菜单项与菜单类型作为参数,返回最终显示的菜单项 + + 第二个参数 `type` 用于区分菜单来源:`'layer'`(图层)、`'viewer'`(画布)、`'data-source'`(数据源)、`'code-block'`(代码块) + + 第三个参数 `getTarget` 仅在**数据源**与**代码块**面板下传入,用于读取当前右键目标(节点 `id` 与原始数据);图层 / 画布不会传入该参数 - **默认值:** `(menus) => menus` -- **类型:** `(menus: (MenuButton | MenuComponent)[], data: { node?: MNode; page?: MPage; parent?: MContainer; stage?: StageCore }) => (MenuButton | MenuComponent)[]` +- **类型:** + +```ts +interface ContentMenuTarget { + /** 目标 ID */ + id: string; + /** 原始节点数据(树节点等) */ + data?: TreeNodeData; +} + +type ContentMenuType = 'layer' | 'data-source' | 'viewer' | 'code-block'; + +type CustomContentMenuFunction = ( + menus: (MenuButton | MenuComponent)[], + type: ContentMenuType, + getTarget?: () => ContentMenuTarget | null, +) => (MenuButton | MenuComponent)[]; +``` - **示例:** @@ -1381,20 +1402,24 @@ const guidesOptions = { ``` diff --git a/packages/editor/src/layouts/sidebar/code-block/CodeBlockListPanel.vue b/packages/editor/src/layouts/sidebar/code-block/CodeBlockListPanel.vue index 47f83419..27cdb60b 100644 --- a/packages/editor/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +++ b/packages/editor/src/layouts/sidebar/code-block/CodeBlockListPanel.vue @@ -121,8 +121,11 @@ const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler, + getTarget, } = useContentMenu((id: string) => { codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' }); }); -const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block')); +const menuData = computed<(MenuButton | MenuComponent)[]>(() => + props.customContentMenu(contentMenuData, 'code-block', getTarget), +); diff --git a/packages/editor/src/layouts/sidebar/code-block/useContentMenu.ts b/packages/editor/src/layouts/sidebar/code-block/useContentMenu.ts index 47808b3a..c0eed424 100644 --- a/packages/editor/src/layouts/sidebar/code-block/useContentMenu.ts +++ b/packages/editor/src/layouts/sidebar/code-block/useContentMenu.ts @@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue'; import { cloneDeep } from 'lodash-es'; import ContentMenu from '@editor/components/ContentMenu.vue'; -import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type'; +import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type'; export const useContentMenu = (deleteCode: (id: string) => void) => { const eventBus = inject('eventBus'); const menuRef = useTemplateRef>('menu'); let selectId = ''; + let selectData: TreeNodeData | null = null; + + const getTarget = (): ContentMenuTarget | null => { + if (!selectId) { + return null; + } + return { id: selectId, data: selectData ?? undefined }; + }; const menuData: (MenuButton | MenuComponent)[] = [ { @@ -65,19 +73,23 @@ export const useContentMenu = (deleteCode: (id: string) => void) => { menuRef.value?.show(event); if (data.id) { selectId = `${data.id}`; + selectData = data; } else { selectId = ''; + selectData = null; } } }; const contentMenuHideHandler = () => { selectId = ''; + selectData = null; }; return { menuData, nodeContentMenuHandler, contentMenuHideHandler, + getTarget, }; }; diff --git a/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue b/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue index 735d074f..b04964ce 100644 --- a/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue +++ b/packages/editor/src/layouts/sidebar/data-source/DataSourceListPanel.vue @@ -171,8 +171,8 @@ eventBus?.on('remove-data-source', (id: string) => { removeHandler(id); }); -const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler } = useContentMenu(); +const { nodeContentMenuHandler, menuData: contentMenuData, contentMenuHideHandler, getTarget } = useContentMenu(); const menuData = computed<(MenuButton | MenuComponent)[]>(() => - props.customContentMenu(contentMenuData, 'data-source'), + props.customContentMenu(contentMenuData, 'data-source', getTarget), ); diff --git a/packages/editor/src/layouts/sidebar/data-source/useContentMenu.ts b/packages/editor/src/layouts/sidebar/data-source/useContentMenu.ts index 2a2e704b..5c1c650e 100644 --- a/packages/editor/src/layouts/sidebar/data-source/useContentMenu.ts +++ b/packages/editor/src/layouts/sidebar/data-source/useContentMenu.ts @@ -3,13 +3,21 @@ import { CopyDocument, Delete, Edit } from '@element-plus/icons-vue'; import { cloneDeep } from 'lodash-es'; import ContentMenu from '@editor/components/ContentMenu.vue'; -import type { EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type'; +import type { ContentMenuTarget, EventBus, MenuButton, MenuComponent, TreeNodeData } from '@editor/type'; export const useContentMenu = () => { const eventBus = inject('eventBus'); const menuRef = useTemplateRef>('menu'); let selectId = ''; + let selectData: TreeNodeData | null = null; + + const getTarget = (): ContentMenuTarget | null => { + if (!selectId) { + return null; + } + return { id: selectId, data: selectData ?? undefined }; + }; const menuData: (MenuButton | MenuComponent)[] = [ { @@ -63,19 +71,23 @@ export const useContentMenu = () => { menuRef.value?.show(event); if (data.id) { selectId = `${data.id}`; + selectData = data; } else { selectId = ''; + selectData = null; } } }; const contentMenuHideHandler = () => { selectId = ''; + selectData = null; }; return { menuData, nodeContentMenuHandler, contentMenuHideHandler, + getTarget, }; }; diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index 0d302bd7..a43d477c 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -1207,9 +1207,24 @@ export interface PageBarSortOptions extends PartSortableOptions { } // #endregion PageBarSortOptions +/** + * 右键菜单当前目标(侧栏树节点等)。 + * 数据源 / 代码块面板通过 `customContentMenu` 的 `getTarget` 暴露,业务在 handler 内自行读取。 + */ +export interface ContentMenuTarget { + /** 目标 ID */ + id: string; + /** 原始节点数据(树节点等) */ + data?: TreeNodeData; +} + +export type ContentMenuType = 'layer' | 'data-source' | 'viewer' | 'code-block'; + export type CustomContentMenuFunction = ( menus: (MenuButton | MenuComponent)[], - type: 'layer' | 'data-source' | 'viewer' | 'code-block', + type: ContentMenuType, + /** 读取当前右键目标;数据源 / 代码块面板会传入,图层 / 画布一般不需要 */ + getTarget?: () => ContentMenuTarget | null, ) => (MenuButton | MenuComponent)[]; export interface EditorEvents { diff --git a/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockListPanel.spec.ts b/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockListPanel.spec.ts index ca653ee2..d9bb20be 100644 --- a/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockListPanel.spec.ts +++ b/packages/editor/tests/unit/layouts/sidebar/code-block/CodeBlockListPanel.spec.ts @@ -48,6 +48,7 @@ vi.mock('@editor/layouts/sidebar/code-block/useContentMenu', () => ({ return menuDataState.items; }, contentMenuHideHandler, + getTarget: () => null, }), })); diff --git a/packages/editor/tests/unit/layouts/sidebar/code-block/useContentMenu.spec.ts b/packages/editor/tests/unit/layouts/sidebar/code-block/useContentMenu.spec.ts index c153fca1..9a99feb3 100644 --- a/packages/editor/tests/unit/layouts/sidebar/code-block/useContentMenu.spec.ts +++ b/packages/editor/tests/unit/layouts/sidebar/code-block/useContentMenu.spec.ts @@ -133,4 +133,16 @@ describe('code-block useContentMenu', () => { (result.menuData[2] as any).handler({}); expect(deleteCode).not.toHaveBeenCalled(); }); + + test('getTarget 返回当前右键目标', () => { + const deleteCode = vi.fn(); + const { result } = mountHook(deleteCode); + expect(result.getTarget()).toBeNull(); + + result.nodeContentMenuHandler({ preventDefault: vi.fn() } as any, { type: 'code', id: 'c1', name: 'code1' } as any); + expect(result.getTarget()).toEqual({ id: 'c1', data: { type: 'code', id: 'c1', name: 'code1' } }); + + result.contentMenuHideHandler(); + expect(result.getTarget()).toBeNull(); + }); }); diff --git a/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts b/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts index 335065fa..6b0d2244 100644 --- a/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts +++ b/packages/editor/tests/unit/layouts/sidebar/data-source/DataSourceListPanel.spec.ts @@ -38,12 +38,14 @@ vi.mock('@editor/hooks/use-data-source-edit', () => ({ const nodeContentMenuHandler = vi.fn(); const contentMenuHideHandler = vi.fn(); -const menuData = ref([{ type: 'button', text: 'Edit' }]); +const menuData = [{ type: 'button', text: 'Edit' }]; +const getTarget = vi.fn(() => null); vi.mock('@editor/layouts/sidebar/data-source/useContentMenu', () => ({ useContentMenu: () => ({ nodeContentMenuHandler, menuData, contentMenuHideHandler, + getTarget, }), })); diff --git a/packages/editor/tests/unit/layouts/sidebar/data-source/useContentMenu.spec.ts b/packages/editor/tests/unit/layouts/sidebar/data-source/useContentMenu.spec.ts index 45a09443..a1b35e51 100644 --- a/packages/editor/tests/unit/layouts/sidebar/data-source/useContentMenu.spec.ts +++ b/packages/editor/tests/unit/layouts/sidebar/data-source/useContentMenu.spec.ts @@ -126,4 +126,15 @@ describe('data-source useContentMenu', () => { (result.menuData[2] as any).handler({}); expect(eventBus.emit).not.toHaveBeenCalled(); }); + + test('getTarget 返回当前右键目标', () => { + const { result } = mountHook(); + expect(result.getTarget()).toBeNull(); + + result.nodeContentMenuHandler({ preventDefault: vi.fn() } as any, { type: 'ds', id: 'd1', name: 'ds1' } as any); + expect(result.getTarget()).toEqual({ id: 'd1', data: { type: 'ds', id: 'd1', name: 'ds1' } }); + + result.contentMenuHideHandler(); + expect(result.getTarget()).toBeNull(); + }); });