From d2a02e16d26a4424681ac02592da1de850ebb78d Mon Sep 17 00:00:00 2001 From: roymondchen Date: Mon, 3 Aug 2026 20:41:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20getEvent/getMethod=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=8A=82=E7=82=B9=E4=B8=8A=E4=B8=8B=E6=96=87=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 便于插件通过 beforeGetEvent/afterGetEvent 等钩子按节点定制事件与方法列表;getMethod 第二参数调整为 { node?, targetId? }。 --- docs/api/editor/eventsServiceMethods.md | 44 ++++++++++++++++--- packages/editor/src/fields/EventSelect.vue | 12 ++--- packages/editor/src/services/events.ts | 6 +-- packages/editor/src/utils/event.ts | 37 +++++++++++----- .../editor/tests/unit/services/events.spec.ts | 14 ++++-- .../editor/tests/unit/utils/event.spec.ts | 34 ++++++++++++-- 6 files changed, 116 insertions(+), 31 deletions(-) diff --git a/docs/api/editor/eventsServiceMethods.md b/docs/api/editor/eventsServiceMethods.md index 19991f64..e3b417ea 100644 --- a/docs/api/editor/eventsServiceMethods.md +++ b/docs/api/editor/eventsServiceMethods.md @@ -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 diff --git a/packages/editor/src/fields/EventSelect.vue b/packages/editor/src/fields/EventSelect.vue index 948adf7e..8ab8bd95 100644 --- a/packages/editor/src/fields/EventSelect.vue +++ b/packages/editor/src/fields/EventSelect.vue @@ -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, })); diff --git a/packages/editor/src/services/events.ts b/packages/editor/src/services/events.ts index a32b3cc6..e051abdc 100644 --- a/packages/editor/src/services/events.ts +++ b/packages/editor/src/services/events.ts @@ -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)]) || []; } diff --git a/packages/editor/src/utils/event.ts b/packages/editor/src/utils/event.ts index 6ee80f1b..4725a547 100644 --- a/packages/editor/src/utils/event.ts +++ b/packages/editor/src/utils/event.ts @@ -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(node, (current) => { - const nodeEvents = (current.type && eventsService.getEvent(current.type)) || []; + (pageFragment.items || []).forEach((item) => { + traverseNode(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(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, + }); }); }); diff --git a/packages/editor/tests/unit/services/events.spec.ts b/packages/editor/tests/unit/services/events.spec.ts index bb698253..7a9af293 100644 --- a/packages/editor/tests/unit/services/events.spec.ts +++ b/packages/editor/tests/unit/services/events.spec.ts @@ -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 清空所有事件 / 方法', () => { diff --git a/packages/editor/tests/unit/utils/event.spec.ts b/packages/editor/tests/unit/utils/event.spec.ts index c23f2b3a..3d5b8298 100644 --- a/packages/editor/tests/unit/utils/event.spec.ts +++ b/packages/editor/tests/unit/utils/event.spec.ts @@ -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' });