feat(editor): 扩展 customContentMenu 支持 getTarget 获取右键目标

为数据源与代码块面板的右键菜单传入 getTarget 回调,便于业务在自定义菜单 handler 中读取当前节点 id 与原始数据。
This commit is contained in:
roymondchen 2026-07-14 15:55:19 +08:00
parent 734e6a8ee6
commit bbe73aae64
10 changed files with 110 additions and 17 deletions

View File

@ -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 = {
</template>
<script setup>
const customContentMenu = (menus, { node }) => {
// 为特定类型的组件添加自定义菜单
if (node?.type === 'container') {
const customContentMenu = (menus, type, getTarget) => {
// 为数据源右键菜单追加自定义菜单
if (type === 'data-source') {
menus.push({
type: 'button',
text: '清空容器',
text: '查看详情',
handler: () => {
// 清空容器的逻辑
// getTarget 仅在数据源 / 代码块面板下传入
const target = getTarget?.();
if (target) {
console.log(target.id, target.data);
}
},
});
}
// 可以过滤掉某些菜单项
return menus.filter(menu => menu.text !== '删除');
return menus.filter((menu) => menu.text !== '删除');
};
</script>
```

View File

@ -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),
);
</script>

View File

@ -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>('eventBus');
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('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,
};
};

View File

@ -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),
);
</script>

View File

@ -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>('eventBus');
const menuRef = useTemplateRef<InstanceType<typeof ContentMenu>>('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,
};
};

View File

@ -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 {

View File

@ -48,6 +48,7 @@ vi.mock('@editor/layouts/sidebar/code-block/useContentMenu', () => ({
return menuDataState.items;
},
contentMenuHideHandler,
getTarget: () => null,
}),
}));

View File

@ -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();
});
});

View File

@ -38,12 +38,14 @@ vi.mock('@editor/hooks/use-data-source-edit', () => ({
const nodeContentMenuHandler = vi.fn();
const contentMenuHideHandler = vi.fn();
const menuData = ref<any[]>([{ 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,
}),
}));

View File

@ -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();
});
});