feat(editor): 新增"已选组件"面板节点双击事件 layer-node-dblclick 与 beforeLayerNodeDblclick 钩子

- TreeNode/Tree 增加 node-dblclick 事件透传
- LayerPanel 默认双击切换可展开节点的展开/收起状态,并向上抛出 node-dblclick
- Sidebar/Editor 暴露 layer-node-dblclick 事件与 beforeLayerNodeDblclick 拦截钩子
- 补充 props/events 文档

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
roymondchen 2026-05-09 16:52:15 +08:00
parent 5af9f6e27a
commit 2475a4f901
9 changed files with 129 additions and 1 deletions

View File

@ -31,3 +31,25 @@
- **事件回调函数:** (e: any) => void
注意:`Editor.vue` 中该 emit 的类型签名为 `[e: any]`,运行时通常为 `Error` 实例(来自 `submitForm` 抛错),但也可能是 element-plus 校验返回的 `invalidFields` 结构,业务侧消费时建议先做类型判断
## layer-node-dblclick
- **详情:** "已选组件"面板中组件树节点被双击时触发
默认行为(切换可展开节点的展开/收起状态)会先于该事件执行;可通过 [`beforeLayerNodeDblclick`](./props.md#beforelayernodedblclick) 钩子拦截,返回 `false` 时该事件不会被触发
- **事件回调函数:** (event: MouseEvent, data: [TreeNodeData](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/type.ts)) => void
- **示例:**
```html
<template>
<m-editor @layer-node-dblclick="onLayerNodeDblclick"></m-editor>
</template>
<script setup>
const onLayerNodeDblclick = (event, data) => {
console.log('双击节点', data.id, data.type);
};
</script>
```

View File

@ -1292,6 +1292,49 @@ const layerNodeIsExpandable = (data, nodeStatusMap) => {
第三方业务可从 `@tmagic/editor` 直接导入 `defaultIsExpandable` 复用默认逻辑作为兜底。
:::
## beforeLayerNodeDblclick
- **详情:**
"已选组件"面板组件树节点双击前的钩子函数
在用户双击组件树节点时,先于默认行为执行;返回 `false` 时阻止默认行为(默认行为是切换可展开节点的展开/收起状态)。返回其他值(包括 `true``undefined``Promise`)则继续执行默认行为,并向上抛出 [`layer-node-dblclick`](./events.md#layer-node-dblclick) 事件。
常见用途:拦截特定类型节点的双击行为,或在双击时执行业务自定义动作(如重命名、打开抽屉等)后阻断默认展开/收起。
- **默认值:** `undefined`
- **类型:** `(event: MouseEvent, data: TreeNodeData) => boolean | void | Promise<boolean | void>`
- **示例:**
```html
<template>
<m-editor
:before-layer-node-dblclick="beforeLayerNodeDblclick"
@layer-node-dblclick="onLayerNodeDblclick"
></m-editor>
</template>
<script setup>
// 双击 page 节点时阻止默认的展开/收起行为
const beforeLayerNodeDblclick = (event, data) => {
if (data.type === 'page') {
return false;
}
};
const onLayerNodeDblclick = (event, data) => {
console.log('双击节点', data.id);
};
</script>
```
::: tip
- 该钩子仅作用于"已选组件"面板的组件树节点,不影响画布上的双击行为(画布双击请使用 [`beforeDblclick`](#beforedblclick))。
- 返回 `false` 时,会同时阻断默认的"展开/收起"行为以及向上抛出的 [`layer-node-dblclick`](./events.md#layer-node-dblclick) 事件;返回其他值则继续触发默认行为并抛出事件。
:::
## extendFormState
- **详情:**

View File

@ -28,6 +28,8 @@
:next-level-indent-increment="treeNextLevelIndentIncrement"
:layer-node-is-expandable="layerNodeIsExpandable"
:can-drop-in="canDropIn"
:before-layer-node-dblclick="beforeLayerNodeDblclick"
@layer-node-dblclick="layerNodeDblclickHandler"
>
<template #layer-panel-header>
<slot name="layer-panel-header"></slot>
@ -157,6 +159,7 @@ import uiService from './services/ui';
import keybindingConfig from './utils/keybinding-config';
import { defaultEditorProps, EditorProps } from './editorProps';
import { initServiceEvents, initServiceState } from './initService';
import type { TreeNodeData } from './type';
import type { EditorSlots, EventBus, Services, StageOptions } from './type';
defineSlots<EditorSlots>();
@ -171,6 +174,7 @@ const emit = defineEmits<{
'update:modelValue': [value: MApp | null];
'props-form-error': [e: any];
'props-submit-error': [e: any];
'layer-node-dblclick': [event: MouseEvent, data: TreeNodeData];
}>();
const props = withDefaults(defineProps<EditorProps>(), defaultEditorProps);
@ -242,5 +246,9 @@ const propsPanelFormErrorHandler = (e: any) => {
emit('props-form-error', e);
};
const layerNodeDblclickHandler = (event: MouseEvent, data: TreeNodeData) => {
emit('layer-node-dblclick', event, data);
};
defineExpose(services);
</script>

View File

@ -56,6 +56,7 @@ const emit = defineEmits<{
'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
'node-mouseenter': [event: MouseEvent, data: TreeNodeData];
'node-click': [event: MouseEvent, data: TreeNodeData];
'node-dblclick': [event: MouseEvent, data: TreeNodeData];
}>();
provide('treeEmit', emit);

View File

@ -25,7 +25,7 @@
@click="expandHandler"
></MIcon>
<div class="tree-node-content" @click="nodeClickHandler">
<div class="tree-node-content" @click="nodeClickHandler" @dblclick="nodeDblclickHandler">
<slot name="tree-node-content" :data="data">
<div class="tree-node-label">
<slot name="tree-node-label" :data="data">{{ `${data.name} (${data.id})` }}</slot>
@ -89,6 +89,7 @@ const emit = defineEmits<{
'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
'node-mouseenter': [event: MouseEvent, data: TreeNodeData];
'node-click': [event: MouseEvent, data: TreeNodeData];
'node-dblclick': [event: MouseEvent, data: TreeNodeData];
}>();
const treeEmit = inject<typeof emit>('treeEmit');
@ -156,4 +157,8 @@ const expandHandler = () => {
const nodeClickHandler = (event: MouseEvent) => {
treeEmit?.('node-click', event, props.data);
};
const nodeDblclickHandler = (event: MouseEvent) => {
treeEmit?.('node-dblclick', event, props.data);
};
</script>

View File

@ -22,6 +22,7 @@ import type {
PageBarSortOptions,
SideBarData,
StageRect,
TreeNodeData,
} from './type';
export interface EditorProps {
@ -114,6 +115,8 @@ export interface EditorProps {
canDropIn?: CanDropInFunction;
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeLayerNodeDblclick?: (event: MouseEvent, data: TreeNodeData) => Promise<boolean | void> | boolean | void;
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/** 页面顺序拖拽配置参数 */
pageBarSortOptions?: PageBarSortOptions;

View File

@ -173,6 +173,7 @@ import {
type SideComponent,
type SideItem,
SideItemKey,
type TreeNodeData,
} from '@editor/type';
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
@ -186,6 +187,10 @@ defineOptions({
name: 'MEditorSidebar',
});
const emit = defineEmits<{
'layer-node-dblclick': [event: MouseEvent, data: TreeNodeData];
}>();
const props = withDefaults(
defineProps<{
data?: SideBarData;
@ -197,6 +202,8 @@ const props = withDefaults(
layerNodeIsExpandable?: IsExpandableFunction;
/** 自定义判断当前正在拖动的源是否可以拖入目标节点内部,详见 EditorProps.canDropIn */
canDropIn?: CanDropInFunction;
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeLayerNodeDblclick?: (_event: MouseEvent, _data: TreeNodeData) => Promise<boolean | void> | boolean | void;
}>(),
{
data: () => ({
@ -256,6 +263,10 @@ const getItemConfig = (data: SideItem): SideComponent => {
nextLevelIndentIncrement: props.nextLevelIndentIncrement,
isExpandable: props.layerNodeIsExpandable,
canDropIn: props.canDropIn,
beforeNodeDblclick: props.beforeLayerNodeDblclick,
},
listeners: {
'node-dblclick': (event: MouseEvent, nodeData: TreeNodeData) => emit('layer-node-dblclick', event, nodeData),
},
component: LayerPanel,
slots: {},

View File

@ -20,6 +20,7 @@
@node-contextmenu="nodeContentMenuHandler"
@node-mouseenter="mouseenterHandler"
@node-click="nodeClickHandler"
@node-dblclick="dblclickHandler"
>
<template #tree-node-content="{ data: nodeData }">
<slot name="layer-node-content" :data="nodeData"> </slot>
@ -89,6 +90,12 @@ const props = defineProps<{
isExpandable?: IsExpandableFunction;
/** 自定义判断当前拖动节点是否可以拖入目标节点内部的函数,返回 false 则禁止拖入 */
canDropIn?: CanDropInFunction;
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeNodeDblclick?: (_event: MouseEvent, _data: TreeNodeData) => Promise<boolean | void> | boolean | void;
}>();
const emit = defineEmits<{
'node-dblclick': [event: MouseEvent, data: TreeNodeData];
}>();
const services = useServices();
@ -134,7 +141,19 @@ const { handleDragStart, handleDragEnd, handleDragLeave, handleDragOver } = useD
const menuRef = useTemplateRef<InstanceType<typeof LayerMenu>>('menu');
const {
nodeClickHandler,
nodeDblclickHandler,
nodeContentMenuHandler,
highlightHandler: mouseenterHandler,
} = useClick(services, isCtrlKeyDown, nodeStatusMap, menuRef);
const dblclickHandler = async (event: MouseEvent, data: TreeNodeData) => {
if (props.beforeNodeDblclick) {
const result = await props.beforeNodeDblclick(event, data);
if (result === false) return;
}
nodeDblclickHandler(event, data);
emit('node-dblclick', event, data);
};
</script>

View File

@ -100,11 +100,27 @@ export const useClick = (
});
};
const nodeDblclickHandler = (event: MouseEvent, data: TreeNodeData): void => {
if (!nodeStatusMap?.value) return;
if (uiService.get('uiSelectMode')) return;
// 双击切换展开/收起状态
if (data.items && data.items.length > 0) {
const status = nodeStatusMap.value.get(data.id);
updateStatus(nodeStatusMap.value, data.id, {
expand: !status?.expand,
});
}
};
return {
menuRef,
nodeClickHandler,
nodeDblclickHandler,
nodeContentMenuHandler(event: MouseEvent, data: TreeNodeData): void {
event.preventDefault();