feat(editor): getEvent/getMethod 支持节点上下文参数

便于插件通过 beforeGetEvent/afterGetEvent 等钩子按节点定制事件与方法列表;getMethod 第二参数调整为 { node?, targetId? }。
This commit is contained in:
roymondchen 2026-08-03 20:41:27 +08:00
parent cc34891bb2
commit d2a02e16d2
6 changed files with 116 additions and 31 deletions

View File

@ -65,6 +65,7 @@ eventsService.setEvent('button', [
- **参数:**
- `{string}` type 组件类型
- `{ node?: MNode | null }` data 可选上下文,便于插件按节点定制事件列表(默认 `{}`
- **返回:**
@ -72,15 +73,25 @@ eventsService.setEvent('button', [
- **详情:**
获取指定组件类型的事件列表
获取指定组件类型的事件列表。默认实现仅按 `type` 返回注册事件;可通过 `usePlugin``beforeGetEvent` / `afterGetEvent` 读取 `data.node` 做节点级过滤或扩展。
- **示例:**
```js
import { eventsService } from '@tmagic/editor';
const events = eventsService.getEvent('button');
const events = eventsService.getEvent('button', { node });
console.log(events); // [{ label: '点击', value: 'click' }, ...]
// 插件按节点定制
eventsService.usePlugin({
afterGetEvent(events, type, data) {
if (data?.node?.id === 'btn_1') {
return events.filter((item) => item.value !== 'longpress');
}
return events;
},
});
```
## setMethods
@ -146,7 +157,9 @@ eventsService.setMethod('video', [
- **参数:**
- `{string}` type 组件类型
- `{string | number}` _targetId 目标节点id保留参数便于扩展时按节点定制
- `{ node?: MNode | null; targetId?: Id }` data 可选上下文(默认 `{}`
- `targetId`:目标节点 id
- `node`:目标节点配置,便于插件按节点定制方法列表
- **返回:**
@ -154,15 +167,36 @@ eventsService.setMethod('video', [
- **详情:**
获取指定组件类型的方法列表
获取指定组件类型的方法列表。默认实现仅按 `type` 返回注册方法;可通过 `usePlugin``beforeGetMethod` / `afterGetMethod` 读取 `data` 做节点级过滤或扩展。
::: warning 破坏性变更
第二参数已从 `targetId: Id` 调整为对象 `{ node?, targetId? }`。请同步更新调用方与插件钩子入参,例如:
```js
// 旧
eventsService.getMethod('video', 'video_123');
// 新
eventsService.getMethod('video', { targetId: 'video_123', node });
```
:::
- **示例:**
```js
import { eventsService } from '@tmagic/editor';
const methods = eventsService.getMethod('video', 'video_123');
const methods = eventsService.getMethod('video', { targetId: 'video_123', node });
console.log(methods); // [{ label: '播放', value: 'play' }, ...]
// 插件按节点定制
eventsService.usePlugin({
afterGetMethod(methods, type, data) {
if (data?.targetId === 'video_123') {
return methods.filter((item) => item.value !== 'stop');
}
return methods;
},
});
```
## resetState

View File

@ -311,10 +311,12 @@ const tableConfig = computed(
label: '事件名',
type: eventNameConfig.value.type,
options: (mForm: FormState, { formValue }: any) =>
eventsService.getEvent(formValue.type).map((option: any) => ({
text: option.label,
value: option.value,
})),
eventsService
.getEvent(formValue.type, { node: editorService.getNodeById(formValue.id) })
.map((option: any) => ({
text: option.label,
value: option.value,
})),
},
{
name: 'to',
@ -329,7 +331,7 @@ const tableConfig = computed(
const node = editorService.getNodeById(model.to);
if (!node?.type) return [];
return eventsService.getMethod(node.type, model.to).map((option: any) => ({
return eventsService.getMethod(node.type, { targetId: model.to, node }).map((option: any) => ({
text: option.label,
value: option.value,
}));

View File

@ -20,7 +20,7 @@ import { reactive } from 'vue';
import { cloneDeep } from 'lodash-es';
import type { Writable } from 'type-fest';
import { type EventOption, type Id } from '@tmagic/core';
import type { EventOption, Id, MNode } from '@tmagic/core';
import { toLine } from '@tmagic/utils';
import type { AsyncHookPlugin, SyncHookPlugin } from '@editor/type';
@ -56,7 +56,7 @@ class Events extends BaseService {
eventMap[toLine(type)] = [...events];
}
public getEvent(type: string): EventOption[] {
public getEvent(type: string, _data: { node?: MNode | null } = {}): EventOption[] {
return cloneDeep(eventMap[toLine(type)]) || [];
}
@ -70,7 +70,7 @@ class Events extends BaseService {
methodMap[toLine(type)] = [...method];
}
public getMethod(type: string, _targetId: Id) {
public getMethod(type: string, _data: { node?: MNode | null; targetId?: Id } = {}) {
return cloneDeep(methodMap[toLine(type)]) || [];
}

View File

@ -56,7 +56,8 @@ export const getEventNameOptions = (
}
if (src === 'component') {
let events: EventOption[] | CascaderOption[] = eventsService.getEvent(formValue.type) || [];
const sourceNode = editorService.getNodeById(formValue.id);
let events: EventOption[] | CascaderOption[] = eventsService.getEvent(formValue.type, { node: sourceNode }) || [];
if (formValue.type === 'page-fragment-container' && formValue.pageFragmentId) {
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === formValue.pageFragmentId);
@ -72,9 +73,14 @@ export const getEventNameOptions = (
},
];
(pageFragment.items || []).forEach((node) => {
traverseNode<MComponent | MContainer>(node, (current) => {
const nodeEvents = (current.type && eventsService.getEvent(current.type)) || [];
(pageFragment.items || []).forEach((item) => {
traverseNode<MComponent | MContainer>(item, (current) => {
if (!current.type) {
return;
}
const node = editorService.getNodeById(current.id) || current;
const nodeEvents = eventsService.getEvent(current.type, { node }) || [];
(events as CascaderOption[]).push({
label: `${current.name}_${current.id}`,
value: `${current.id}`,
@ -176,7 +182,7 @@ export const getCompActionOptions = (toId?: Id): EventNameOption[] => {
return [];
}
let methods: EventOption[] | CascaderOption[] = eventsService.getMethod(node.type, toId) || [];
let methods: EventOption[] | CascaderOption[] = eventsService.getMethod(node.type, { targetId: toId, node }) || [];
if (node.type === 'page-fragment-container' && node.pageFragmentId) {
const pageFragment = editorService.get('root')?.items?.find((page) => page.id === node.pageFragmentId);
@ -187,15 +193,22 @@ export const getCompActionOptions = (toId?: Id): EventNameOption[] => {
methods = [];
(pageFragment.items || []).forEach((item: MComponent | MContainer) => {
traverseNode<MComponent | MContainer>(item, (current) => {
const nodeMethods = (current.type && eventsService.getMethod(current.type, current.id)) || [];
const node = editorService.getNodeById(current.id) || current;
if (nodeMethods.length) {
(methods as CascaderOption[]).push({
label: `${current.name}_${current.id}`,
value: `${current.id}`,
children: nodeMethods,
});
if (!current.type) {
return;
}
const nodeMethods = eventsService.getMethod(current.type, { targetId: current.id, node }) || [];
if (!nodeMethods.length) {
return;
}
(methods as CascaderOption[]).push({
label: `${current.name}_${current.id}`,
value: `${current.id}`,
children: nodeMethods,
});
});
});

View File

@ -30,7 +30,7 @@ describe('events', () => {
test('setMethod', () => {
const method = [{ label: '点击', value: 'magic:common:events:click' }];
events.setMethod('button', method);
expect(events.getMethod('button', '')).toHaveLength(1);
expect(events.getMethod('button', { targetId: 'btn_1' })).toHaveLength(1);
});
test('setEvents 批量设置', () => {
@ -46,12 +46,20 @@ describe('events', () => {
events.setMethods({
Image: [{ label: 'show', value: 'show' }],
} as any);
expect(events.getMethod('image', '')).toHaveLength(1);
expect(events.getMethod('image', {})).toHaveLength(1);
});
test('getEvent / getMethod 支持节点上下文参数', () => {
const node = { id: 'n1', type: 'button' } as any;
events.setEvent('button', [{ label: '点击', value: 'click' }]);
events.setMethod('button', [{ label: '打开', value: 'open' }]);
expect(events.getEvent('button', { node })).toEqual([{ label: '点击', value: 'click' }]);
expect(events.getMethod('button', { targetId: 'n1', node })).toEqual([{ label: '打开', value: 'open' }]);
});
test('未注册类型返回空数组', () => {
expect(events.getEvent('not-exist')).toEqual([]);
expect(events.getMethod('not-exist', '')).toEqual([]);
expect(events.getMethod('not-exist')).toEqual([]);
});
test('resetState 清空所有事件 / 方法', () => {

View File

@ -52,8 +52,10 @@ describe('event utils', () => {
});
test('getCompActionOptions 普通组件返回 select options', () => {
editorService.getNodeById.mockReturnValue({ type: 'btn', id: '1' });
const node = { type: 'btn', id: '1' };
editorService.getNodeById.mockReturnValue(node);
expect(getCompActionOptions('1')).toEqual([{ text: 'open', value: 'open' }]);
expect(eventsService.getMethod).toHaveBeenCalledWith('btn', { targetId: '1', node });
});
test('getCompActionOptions 无节点返回空', () => {
@ -63,8 +65,10 @@ describe('event utils', () => {
});
test('getCompActionOptions 页面片容器返回 cascader options', () => {
editorService.getNodeById.mockReturnValue({ type: 'page-fragment-container', id: '1', pageFragmentId: 'pf1' });
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [{ id: 'c1', type: 'btn', name: 'b' }] }] });
const container = { type: 'page-fragment-container', id: '1', pageFragmentId: 'pf1' };
const child = { id: 'c1', type: 'btn', name: 'b' };
editorService.getNodeById.mockImplementation((id: string) => (id === '1' ? container : null));
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [child] }] });
expect(getCompActionOptions('1')).toEqual([
{
label: 'b_c1',
@ -72,6 +76,7 @@ describe('event utils', () => {
children: [{ label: 'open', value: 'open' }],
},
]);
expect(eventsService.getMethod).toHaveBeenCalledWith('btn', { targetId: 'c1', node: child });
});
test('getCompActionAllowedValues 枚举合法动作', () => {
@ -99,6 +104,29 @@ describe('event utils', () => {
expect(collectEventNameOptionValues([{ value: 'a', children: [{ value: 'b' }] }], false)).toEqual(new Set(['a.b']));
});
test('getEventNameOptions 页面片子节点查找失败时回退 current', () => {
const formValue = { type: 'page-fragment-container', id: '1', pageFragmentId: 'pf1' };
const child = { id: 'c1', type: 'btn', name: 'b' };
editorService.getNodeById.mockReturnValue(null);
editorService.get.mockReturnValue({
items: [{ id: 'pf1', name: '页面片', items: [child] }],
});
expect(getEventNameOptions('component', formValue)).toEqual([
{
label: '页面片',
value: 'pf1',
children: [{ label: 'click', value: 'click' }],
},
{
label: 'b_c1',
value: 'c1',
children: [{ label: 'click', value: 'click' }],
},
]);
expect(eventsService.getEvent).toHaveBeenCalledWith('btn', { node: child });
});
test('getEventNameOptions datasource 追加数据变化', () => {
dataSourceService.getDataSourceById.mockReturnValue({ fields: [{ name: 'f1' }] });
const opts = getEventNameOptions('datasource', { type: 'ds', id: 'd1' });