import{At as e,E as t,Ot as n,Xt as r}from"./chunks/framework.ECw2lWOt.js";var i=JSON.parse(`{"title":"editorService方法","description":"","frontmatter":{},"headers":[],"relativePath":"api/editor/editorServiceMethods.md","filePath":"api/editor/editorServiceMethods.md"}`),a={name:`api/editor/editorServiceMethods.md`};function o(t,i,a,o,s,c){return r(),n(`div`,null,[...i[0]||=[e(`
下列 DSL 操作方法(add、remove、update 等)的 options / data 参数,以及 codeBlockService / dataSourceService 的 options,在 doNotPushHistory 之外还可传入:
{string} historyDescription:入栈时附带的人类可读描述,用于历史面板展示;不影响 undo/redo 行为,缺省时面板会自动生成描述{HistoryOpSource} historySource:操作途径,用于历史面板展示与埋点;不影响 undo/redo 行为,缺省时面板视为「未知」编辑器内置交互(画布、树面板、配置面板、右键菜单、快捷键等)会自动传入对应的 historySource; 业务侧程序化调用时建议显式传入(如 api),便于历史面板区分来源。
每条历史记录入栈时都会自动生成一个唯一标识 uuid(见 StepValue),可用于精确引用 / 定位某一条历史记录(如埋点、回滚、跨端同步等)。
DSL 操作方法(add / remove / update 等)默认返回操作结果(节点 / 节点集合 / void),不会返回 uuid。若需要同时拿到原操作结果与本次写入历史记录的 uuid,可改用对应的 *AndGetHistoryId 方法:它们与原方法行为完全一致,返回值类型为 DslOpWithHistoryIdsResult<T>(result 为原方法返回值,historyIds 为本次写入的 uuid 列表)。当本次操作未写入历史(doNotPushHistory: true、无实际变更或提前返回)时 historyIds 为 [];单次操作通常返回含一个 uuid 的数组。
| 原方法 | 取 uuid 的方法 | 返回值 |
|---|---|---|
| add | addAndGetHistoryId | Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> |
| remove | removeAndGetHistoryId | Promise<DslOpWithHistoryIdsResult<void>> |
| update | updateAndGetHistoryId | Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> |
| moveLayer | moveLayerAndGetHistoryId | Promise<DslOpWithHistoryIdsResult<void>> |
| moveToContainer | moveToContainerAndGetHistoryId | Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> |
| dragTo | dragToAndGetHistoryId | Promise<DslOpWithHistoryIdsResult<void>> |
dataSourceService / codeBlockService 也提供了同名约定的 *AndGetHistoryId 方法,返回值约定相同(result + historyIds)。
拿到 uuid 后,可在需要时按 uuid「回滚」对应的历史记录(类 git revert 语义,详见历史记录面板)。相比按 index 回滚,uuid 不会随栈内步骤增删而变化,更适合业务侧持有引用后再回滚:
/**
* 历史记录写入相关的通用配置(codeBlock / dataSource / editor 共用)
* - doNotPushHistory: 操作完成后是否不要将本次操作压入历史栈(撤销/重做记录),默认 false
* - historyDescription: 入栈时附带的人类可读描述,用于历史面板展示;不影响 undo/redo 行为,缺省时面板会自动生成描述
* - historySource: 操作途径,取值见 {@link HistoryOpSource}(画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等),用于历史面板展示与埋点;不影响 undo/redo 行为
*/
export interface HistoryOpOptions {
doNotPushHistory?: boolean;
historyDescription?: string;
historySource?: HistoryOpSource;
}/**
* DSL 修改类操作的通用配置
* - doNotSelect: 操作后是否不要自动触发选中(不调用 this.select / this.multiSelect / stage.select / stage.multiSelect)
* - doNotSwitchPage: 操作若会引发当前页面切换(如新增 / 删除 / 跨页移动),是否跳过这次切换
*/
export interface DslOpOptions extends HistoryOpOptions {
doNotSelect?: boolean;
doNotSwitchPage?: boolean;
}/** *AndGetHistoryId 系列方法返回值:原操作结果 + 本次写入历史记录的 uuid 列表(未入栈时为 \`[]\`)。 */
export type DslOpWithHistoryIdsResult<T> = {
result: T;
historyIds: string[];
};/**
* 历史记录的「操作途径」——标记本次变更由哪条交互入口触发,仅用于历史面板展示 / 业务埋点,
* 不影响 undo/redo 行为。缺省(未传)时 UI 视为「未知」。
*
* - \`stage\`:画布(拖拽 / 缩放 / 排序等舞台直接操作)
* - \`tree\`:树形面板(图层 / 数据源 / 代码块等树形结构里的拖拽 / 菜单操作)
* - \`component-panel\`:组件面板(左侧组件列表点击 / 拖拽新增组件)
* - \`props\`:配置面板表单(属性表单字段编辑)
* - \`code\`:源码编辑器(配置面板「源码」面板里直接编辑 JSON/代码后保存)
* - \`stage-contextmenu\`:画布右键菜单(舞台上节点的右键上下文菜单)
* - \`tree-contextmenu\`:树面板右键菜单(图层 / 数据源 / 代码块等树形列表上的右键上下文菜单)
* - \`toolbar\`:工具栏菜单(顶部导航工具栏按钮)
* - \`shortcut\`:键盘快捷键
* - \`rollback\`:历史回滚(历史面板里对某条历史「回滚」,反向应用为一条新记录,类 git revert)
* - \`api\`:代码 / 接口调用(程序化触发)
* - \`ai\`:AI 生成 / 智能助手触发的变更
* - \`unknown\`:未知来源
*
* 通过 \`(string & {})\` 允许业务侧扩展自定义途径字符串,同时保留内置值的自动补全。
*/
export type HistoryOpSource =
| 'initial'
| 'stage'
| 'tree'
| 'component-panel'
| 'props'
| 'code'
| 'root-code'
| 'stage-contextmenu'
| 'tree-contextmenu'
| 'toolbar'
| 'shortcut'
| 'rollback'
| 'api'
| 'ai'
// 同步
| 'sync'
| 'unknown'
| (string & {});参数:
{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name返回:
{any} value详情:
获取当前指指定name的值
'root': 当前整个配置,也就是当前编辑器的值
'page': 当前正在编辑的页面配置
'parent': 当前选中的节点的父节点
'node': 当前选中的第一个节点
'highlightNode': 当前高亮的节点
'nodes': 当前选中的所有节点
'modifiedNodeIds': 当前页面所有改动过的节点id
'invalidNodeIds': 校验失败的节点错误信息(Map<Id, NodeInvalidInfo>),供组件树标红提示与保存拦截读取
'pageLength': 所以页面个数
'pageFragmentLength': 页面片个数
'stage': StageCore实例
'stageLoading': 画布是否加载中
'disabledMultiSelect': 是否禁用多选
'alwaysMultiSelect': 是否始终启用多选模式(无需按住 Ctrl/Meta)
示例:
import { editorService } from "@tmagic/editor";
const node = editorService.get("node");{'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength' | 'stage' | 'stageLoading' | 'disabledMultiSelect' | 'alwaysMultiSelect'} name
{any} value
详情: 参考get方法
示例:
import { editorService } from "@tmagic/editor";
const node = editorService.get("node");
editorService.set("node", {
...node,
name: "new name",
});{number | string} id 组件id{boolean} raw 是否使用toRaw,默认为trueTIP
如果raw为false,对获取到的对象进行操作会触发vue响应式处理
返回:
EditorNodeInfo}export interface EditorNodeInfo {
node: MNode | null;
parent: MContainer | null;
page: MPage | MPageFragment | null;
path: MNode[];
}export type MNode = MComponent | MContainer | MIteratorContainer | MPage | MApp | MPageFragment;export interface MContainer extends MComponent {
/** 容器类型,默认为'container' */
type?: NodeType.CONTAINER | string;
/** 容器子元素 */
items: (MComponent | MContainer)[];
}export interface MPage extends MContainer {
/** 页面类型 */
type: NodeType.PAGE;
}export interface MPageFragment extends MContainer {
/** 页面类型 */
type: NodeType.PAGE_FRAGMENT;
}详情:
根据id获取组件、组件的父组件以及组件所属的页面节点
示例:
import { editorService } from "@tmagic/editor";
const info = editorService.getNodeInfo("text_123");
console.log(info.node);
console.log(info.parent);
console.log(info.page);参数:
{number | string} id{boolean} raw 是否使用toRaw,默认为true返回:
MNode} 组件节点配置export type MNode = MComponent | MContainer | MIteratorContainer | MPage | MApp | MPageFragment;export interface MComponent {
/** 组件ID,默认为\${type}_\${number}}形式, 如:page_123 */
id: Id;
/** 组件类型 */
type?: string;
/** 组件显示名称 */
name?: string;
/** 组件根Dom上的class */
className?: string;
/* 关联事件集合 */
events?: EventConfig[];
/** 是否隐藏 */
visible?: boolean;
/** 显示条件中配置的数据源条件的编译结果 */
condResult?: boolean;
/** 组件根Dom的style */
style?: StyleSchema;
[NODE_CONDS_KEY]?: DisplayCond[];
[NODE_CONDS_RESULT_KEY]?: boolean;
[key: string]: any;
}export interface MContainer extends MComponent {
/** 容器类型,默认为'container' */
type?: NodeType.CONTAINER | string;
/** 容器子元素 */
items: (MComponent | MContainer)[];
}export interface MIteratorContainer extends MContainer {
type: 'iterator-container';
iteratorData: any[];
dsField: string[];
itemConfig: {
layout: string;
[NODE_CONDS_KEY]: DisplayCond[];
style: Record<string, string | number>;
};
}export interface MPage extends MContainer {
/** 页面类型 */
type: NodeType.PAGE;
}export interface MApp extends MComponent {
/** App页面类型,app作为整个结构的根节点;有且只有一个 */
type: NodeType.ROOT;
/** */
items: (MPage | MPageFragment)[];
/** 代码块 */
codeBlocks?: CodeBlockDSL;
dataSources?: DataSourceSchema[];
dataSourceDeps?: DataSourceDeps;
dataSourceCondDeps?: DataSourceDeps;
dataSourceMethodDeps?: DataSourceDeps;
}export interface MPageFragment extends MContainer {
/** 页面类型 */
type: NodeType.PAGE_FRAGMENT;
}详情:
根据id获取组件的信息
示例:
import { editorService } from "@tmagic/editor";
const node = editorService.getNodeById("text_123");
console.log(node);参数:
{number | string} id{boolean} raw 是否使用toRaw,默认为true返回:
MNode} 指点组件的父节点配置详情:
根据ID获取指点节点的父节点配置
示例:
import { editorService } from "@tmagic/editor";
const parent = editorService.getParentById("text_123");
console.log(parent);参数:
MNode} node 节点配置返回:
{boolean} true 表示该节点位于非当前页面(即选中该节点将会引起当前页面切换)详情:
判断给定节点是否位于非当前页面,通常用于配合 doNotSwitchPage 选项判断 DSL 操作是否会引起页面切换
示例:
import { editorService } from "@tmagic/editor";
const otherPageNode = editorService.getNodeById("text_456");
if (editorService.isOnDifferentPage(otherPageNode)) {
console.log("该节点在其它页面,操作会触发页面切换");
}扩展支持: 是
参数:
MNode} parentMNode} node 可选返回:
Layout>} 当前布局模式/** 容器布局 */
export enum Layout {
FLEX = 'flex',
FIXED = 'fixed',
RELATIVE = 'relative',
ABSOLUTE = 'absolute',
}详情:
只有容器拥有布局,目前支持的布局有流式布局(relative),绝对定位布局(absolute),固定定位布局(fixed)
TIP
固定定位布局需要从当前选中节点判断,固需要传递可选参数 node
其他布局则是从父组件(容器)来判断
示例:
import { editorService } from "@tmagic/editor";
const parent = editorService.getParentById("text_123");
editorService.getLayout(parent).then((layout) => {
console.log(parent);
});扩展支持: 是
参数:
MNode} config 需要选中的节点或节点ID返回:
MNode>} 当前选中的节点配置详情:
选中指点节点(将指点节点设置成当前选中状态)
TIP
editorService.select只是设置了编辑器的选中状态,并没有设置画布的选中状态,所以根据实际情况可以调用stage.select来设置画布的选中态
示例:
import { editorService } from "@tmagic/editor";
editorService.select("text_123");
editorService.get("stage")?.select("text_123");参数:
返回:
{Promise<void>}详情:
选中多个节点
TIP
editorService.multiSelect只是设置了编辑器的选中状态,并没有设置画布的选中状态,所以根据实际情况可以调用stage.multiSelect来设置画布的选中态
示例:
import { editorService } from "@tmagic/editor";
editorService.multiSelect(["text_123", "button_123"]);
editorService.get("stage")?.multiSelect(["text_123", "button_123"]);返回:
MNode | null>} 选中后的节点配置详情:
选中当前节点同层级(同一父节点)的下一个节点,已经是最后一个时回到第一个
返回:
MNode>} 选中后的页面配置详情:
选中下一页,已经是最后一页时回到第一页
返回:
{void}详情:
选中根节点(root),同时清空当前选中的页面、父节点、画布及高亮节点
扩展支持: 是
参数:
MNode} config 需要高亮的节点或节点ID返回:
{Promise<void>}详情:
高亮指定节点
示例:
import { editorService } from "@tmagic/editor";
editorService.highlight("text_123");扩展支持: 是
参数:
{MNode} node 新组件节点
{MContainer} parent 指定的容器节点
返回:
MNode>} 新增的组件详情:
往指定的容器中添加组件
扩展支持: 是
参数:
{MNode | MNode[]} node 新组件节点配置或多个节点集合
{MContainer} parent 指定的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
{Object} options 可选配置
{boolean} doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点){boolean} doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作){boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
MNode | MNode[]>} 新增的组件或组件集合详情:
往指定的容器或当前容器中添加组件
扩展支持: 是
参数:
MNode} node 要删除的节点{Object} options 可选配置 {boolean} doNotSelect 删除后是否不更新当前选中节点(默认 false){boolean} doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)返回:
{Promise<void>}详情:
删除指定的组件或者页面
TIP
无论是否传入 doNotSelect / doNotSwitchPage,当被删除节点在当前选中列表中时,state 都会自动移除该节点的引用;当被删除的正好是当前页面时,state.page 也会同步清空,避免持有已删除节点
扩展支持: 是
参数:
MNode | MNode[])} node 要删除的节点或节点集合{Object} options 可选配置 {boolean} doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面){boolean} doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面){boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
{Promise<void>}详情:
删除指定的组件或者页面或组件集合
扩展支持: 是
参数:
MNode} config 新的节点{Object} data 可选配置 ChangeRecord[]} changeRecords 变更记录返回:
{Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }>} 更新前后的节点信息export interface ChangeRecord {
propPath?: string;
value: any;
}详情:
更新节点
TIP
节点中应该要有id,不然不知道要更新哪个节点
当被更新节点正好在当前选中列表中时,state 会自动同步到新的节点引用,无需调用方处理
当被更新节点正好是当前页面时,state.page 也会同步到新的节点引用;更新非当前页面(不同 ID)时不会把编辑器切到该页
扩展支持: 是
参数:
MNode | MNode[]} config 新的节点或节点集合{Object} data 可选配置 ChangeRecord[]} changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)ChangeRecord[][]} changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords{boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options{Object} invalidInfo 启用 enablePropsFormValidate 时,属性面板提交携带的校验错误信息,在写入历史记录之前落库,使历史快照与本次变更对齐 {Id} id 节点 id{'props' | 'style'} source 错误来源:属性表单 / 样式表单{string} error 错误文案(可为含 <br> 的 HTML);为空时表示清除该来源的错误记录export interface ChangeRecord {
propPath?: string;
value: any;
}返回:
MNode | MNode[]>} 新的节点或节点集合详情:
更新单个或多个节点
TIP
与doUpdate的区别:
update可以支持一次更新多个组件,update是通过调用doUpdate来最终实现更新的。
编辑器内部更新组件都是调用update来实现的,update除了更新操作外,还会记录历史堆,还会更新代码块关系链。
TIP
多节点场景必须使用 changeRecordList:每个节点应保留自己独立的 records,不能把多个节点的 records 合并到同一个 changeRecords 数组里,否则 doUpdate / 依赖收集 / 历史回放都会按错误的 propPath 处理。
写入历史时,每个节点的 records 会单独保存到 updatedItems[i].changeRecords;撤销/重做时若有 records,则仅按 propPath 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;缺省 才退化为整节点替换(如内部 sort / moveLayer / 拖动等纯快照场景)。
扩展支持: 是
参数:
{ string | number } id1{ string | number } id2{Object} options 可选配置 {boolean} doNotSelect 排序后是否不更新当前选中节点(默认 false){boolean} doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致){boolean} doNotPushHistory 是否不写入历史记录(默认 false){HistoryOpSource} historySource 见历史记录相关 options返回:
{Promise<void>}详情:
将id为id1的组件移动到id为id2的组件位置上,例如:[1,2,3,4] -> sort(1,3) -> [2,1,3,4]
用于流式布局下的组件拖动更新
扩展支持: 是
参数:
MNode | MNode[]} config 需要复制的节点或节点集合返回:
{void}详情:
复制组件节点或节点集合
通过storageService.setItem,将组件节点配置存储到localStorage中
参数:
MNode | MNode[]} config 需要复制的节点或节点集合{TargetOptions} collectorOptions 可选的依赖收集器配置返回:
{void}详情:
复制节点时会同时收集组件关联的依赖(如 dataSource、codeBlock 等),并一起存储到 localStorage 中,便于粘贴时一起带入
粘贴前置操作:返回分配了新id以及校准了坐标的配置
扩展支持: 是
参数:
PastePosition} position 粘贴的坐标export interface PastePosition {
left?: number;
top?: number;
/**
* 粘贴位置X方向偏移量
*/
offsetX?: number;
/**
* 粘贴位置Y方向偏移量
*/
offsetY?: number;
}{TargetOptions} collectorOptions 可选的依赖收集器配置{Object} options 可选配置 {boolean} doNotSelect 粘贴后是否不更新当前选中节点(默认 false){boolean} doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换){boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
MNode | MNode[]>} 添加后的组件节点配置详情:
粘贴组件节点或节点集合
通过storageService.getItem,从localStorage中获取节点,然后添加到当前容器中
扩展支持: 是
参数:
MNode | MNode[]} config 需要居中的组件或者组件集合{Object} options 可选配置 {boolean} doNotSelect 居中后是否不更新当前选中节点(默认 false){boolean} doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致){boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
MNode | MNode[]>}详情:
水平居中组件或者组件集合,仅在流式布局下有效
TIP
与doAlignCenter的区别:
alignCenter可以支持一次水平居中多个组件,alignCenter是通过调用doAlignCenter来获取到已设置好水平居中的位置信息的节点,然后调用update更新。
扩展支持: 是
参数:
{number | 'top' | 'bottom'} offset{Object} options 可选配置 {boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
{Promise<void>}详情:
移动当前选中节点位置
用于实现上移一层、下移一层、置顶、置底
扩展支持: 是
参数:
MNode} config 需要移动的节点{string | number} targetId 容器ID{Object} options 可选配置 {boolean} doNotSelect 移动后是否不更新当前选中节点(默认 false){boolean} doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换){boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
MNode | undefined>详情:
移动到指定容器中
参数:
MNode | MNode[]} config 需要拖拽的节点或节点集合MContainer} targetParent 目标父容器{number} targetIndex 目标位置索引{Object} options 可选配置 {boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
{Promise<void>}详情:
将节点(支持多选)拖拽到目标容器的指定位置,会自动处理跨容器布局切换并记录历史
参数: 同 add
返回:
DslOpWithHistoryIdsResult<MNode | MNode[]>>} 详情:
与 add 行为完全一致,并在返回值中额外提供 historyIds,见历史记录 uuid 与 *AndGetHistoryId
示例:
import { editorService } from "@tmagic/editor";
const { result, historyIds } = await editorService.addAndGetHistoryId(
{ type: "text", text: "hello" },
parent,
{ historySource: "api" },
);
console.log(result); // 新增节点
console.log(historyIds); // 本次新增对应的历史记录 uuid 列表,或 []参数: 同 remove
返回:
DslOpWithHistoryIdsResult<void>>} historyIds:本次写入历史记录的 uuid 列表;未写入历史时为 []详情:
与 remove 行为完全一致,并在返回值中额外提供 historyIds
参数: 同 update
返回:
DslOpWithHistoryIdsResult<MNode | MNode[]>>} 详情:
与 update 行为完全一致,并在返回值中额外提供 historyIds
参数: 同 moveLayer
返回:
DslOpWithHistoryIdsResult<void>>} historyIds:本次写入历史记录的 uuid 列表;未写入历史时为 []详情:
与 moveLayer 行为完全一致,并在返回值中额外提供 historyIds
参数: 同 moveToContainer
返回:
DslOpWithHistoryIdsResult<MNode | MNode[]>>} result:同 moveToContainer 的返回值(移动后的节点 / 节点数组)historyIds:本次写入历史记录的 uuid 列表;未写入历史时为 []详情:
与 moveToContainer 行为完全一致,并在返回值中额外提供 historyIds
参数: 同 dragTo
返回:
DslOpWithHistoryIdsResult<void>>} historyIds:本次写入历史记录的 uuid 列表;未写入历史时为 []详情:
与 dragTo 行为完全一致,并在返回值中额外提供 historyIds
参数:
{string[]} uuids 目标历史记录的 uuid 列表(通常由 *AndGetHistoryId 方法返回的 historyIds)返回:
StepValue | null)[]>} 与入参同序的回滚结果列表,某项失败时为 null详情:
通过历史记录 uuid「回滚」当前页面的历史步骤(类 git revert 语义):不移动游标、不丢弃任何步骤,而是把目标 step 的修改反向应用为一条全新的步骤压入栈顶。按数组顺序依次回滚。
TIP
opType: 'update' 的步骤必须携带 changeRecords 才支持回滚(否则只能整节点替换,会冲掉后续无关变更);未应用(已被撤销)的步骤无法回滚。
示例:
import { editorService } from "@tmagic/editor";
// 执行操作时拿到本次历史记录 uuid
const { historyIds } = await editorService.addAndGetHistoryId({ type: "text", text: "hello" });
// 之后任意时机按 uuid 回滚(支持单个或整批 historyIds)
await editorService.revertPageStepById(historyIds);扩展支持: 是
返回:
StepValue | null>}/**
* 页面节点历史记录条目(\`diff\` 内容为 {@link MNode})。结构已与代码块 / 数据源统一收敛到
* {@link BaseStepValue}:关联 id 见 \`data.id\`,选区等上下文见 \`extra\`。
*/
export type StepValue = BaseStepValue<MNode, StepExtra>;/**
* 历史记录条目公共字段,被 {@link StepValue} / {@link CodeBlockStepValue} / {@link DataSourceStepValue} 复用。
*
* 泛型 \`T\` 为 \`diff\` 中变化内容的快照类型(页面节点 \`MNode\` / 代码块 \`CodeBlockContent\` / 数据源 \`DataSourceSchema\`)。
*/
export interface BaseStepValue<T = unknown, U extends Record<string, any> = {}> {
/**
* 历史记录唯一标识(uuid)。入栈时自动写入(若调用方未指定),
* 用于精确定位 / 引用某一条历史记录(如 revert、埋点、跨端同步等)。
* 注意与 \`data.id\`(关联的页面 / 代码块 / 数据源 id)区分。
*/
uuid: string;
/**
* 关联目标信息:\`id\` 为关联的页面 / 代码块 / 数据源等资源 id(也是历史栈的分组 key),
* \`name\` 为展示名。所有历史类型统一携带。
*/
data: { name: string; id: Id };
/** 操作类型:新增 / 删除 / 更新(三类历史记录统一携带)。 */
opType: HistoryOpType;
/**
* 本次变更的内容(统一 diff 表达),每项见 {@link StepDiffItem}。
* 页面节点(add/remove 多节点、update 多节点)会有多项,代码块 / 数据源通常只有一项。
*/
diff: StepDiffItem<T>[];
/**
* 调用方可选传入的人类可读描述(如「调整按钮颜色」),用于历史面板展示。
* 不影响 undo/redo 行为;缺省时面板会根据节点 / propPath 自动生成描述。
*/
historyDescription?: string;
/**
* 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource}
* (画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等)。
* 仅用于历史面板展示与业务埋点,不影响 undo/redo 行为;缺省时面板视为「未知」。
*/
source?: HistoryOpSource;
/**
* 入栈时间戳(毫秒)。入栈时自动写入(若调用方未指定),仅用于历史面板展示。
*/
timestamp?: number;
/**
* 是否为「已保存」记录:DSL 落库(如保存到后端 / 本地)时由 historyService.markSaved 标记。
* 同一栈内任意时刻最多只有一条记录为 true;从 IndexedDB 恢复时游标会被定位到最近一条已保存记录之后。
*/
saved?: boolean;
/**
* 是否为「整体设置 root」(set root)产生的记录(由 {@link Editor.pushRootDiffHistory} 写入)。
* 用于「连续 set root 合并」:当某页栈最新一条已是 root 记录时,下一条 set root 会替换它而非新增,
* 避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录。
*/
rootStep?: boolean;
/** 操作人 */
operator?: string;
/** 扩展信息 */
extra?: U;
}/**
* 历史记录的扩展上下文({@link BaseStepValue.extra})。
* 内置字段供 \`page\` 类型在撤销 / 重做时恢复选区与受影响节点;扩展类型可自由附加其它键。
*/
export interface StepExtra {
/** 操作前选中的节点 ID,用于撤销后恢复选择状态(page 类型) */
selectedBefore?: Id[];
/** 操作后选中的节点 ID,用于重做后恢复选择状态(page 类型) */
selectedAfter?: Id[];
/** 本次操作涉及的节点 id 集合(page 类型) */
modifiedNodeIds?: Map<Id, Id>;
/** 操作前的节点校验错误快照,撤销后还原(使撤销一个「校验失败」的改动后错误消失) */
invalidNodeIdsBefore?: Map<Id, NodeInvalidInfo>;
/** 操作后的节点校验错误快照,重做后还原(使重做后错误恢复) */
invalidNodeIdsAfter?: Map<Id, NodeInvalidInfo>;
[key: string]: any;
}/**
* 历史记录操作类型:
* - \`add\` / \`remove\` / \`update\`:普通可撤销/重做的节点变更;
* - \`initial\`:页面「未修改的初始状态」基线(设置 root 时生成),作为页面栈 index 0 的固定底线 step。
* 该 step 不可被撤销/回滚(cursor 不会低于它),仅用于历史面板底部的初始行展示。
*/
export type HistoryOpType = 'add' | 'remove' | 'update' | 'initial';/**
* 历史记录的「操作途径」——标记本次变更由哪条交互入口触发,仅用于历史面板展示 / 业务埋点,
* 不影响 undo/redo 行为。缺省(未传)时 UI 视为「未知」。
*
* - \`stage\`:画布(拖拽 / 缩放 / 排序等舞台直接操作)
* - \`tree\`:树形面板(图层 / 数据源 / 代码块等树形结构里的拖拽 / 菜单操作)
* - \`component-panel\`:组件面板(左侧组件列表点击 / 拖拽新增组件)
* - \`props\`:配置面板表单(属性表单字段编辑)
* - \`code\`:源码编辑器(配置面板「源码」面板里直接编辑 JSON/代码后保存)
* - \`stage-contextmenu\`:画布右键菜单(舞台上节点的右键上下文菜单)
* - \`tree-contextmenu\`:树面板右键菜单(图层 / 数据源 / 代码块等树形列表上的右键上下文菜单)
* - \`toolbar\`:工具栏菜单(顶部导航工具栏按钮)
* - \`shortcut\`:键盘快捷键
* - \`rollback\`:历史回滚(历史面板里对某条历史「回滚」,反向应用为一条新记录,类 git revert)
* - \`api\`:代码 / 接口调用(程序化触发)
* - \`ai\`:AI 生成 / 智能助手触发的变更
* - \`unknown\`:未知来源
*
* 通过 \`(string & {})\` 允许业务侧扩展自定义途径字符串,同时保留内置值的自动补全。
*/
export type HistoryOpSource =
| 'initial'
| 'stage'
| 'tree'
| 'component-panel'
| 'props'
| 'code'
| 'root-code'
| 'stage-contextmenu'
| 'tree-contextmenu'
| 'toolbar'
| 'shortcut'
| 'rollback'
| 'api'
| 'ai'
// 同步
| 'sync'
| 'unknown'
| (string & {});export type Id = string | number;详情:
撤销当前操作
扩展支持: 是
返回:
StepValue | null>}/**
* 页面节点历史记录条目(\`diff\` 内容为 {@link MNode})。结构已与代码块 / 数据源统一收敛到
* {@link BaseStepValue}:关联 id 见 \`data.id\`,选区等上下文见 \`extra\`。
*/
export type StepValue = BaseStepValue<MNode, StepExtra>;/**
* 历史记录条目公共字段,被 {@link StepValue} / {@link CodeBlockStepValue} / {@link DataSourceStepValue} 复用。
*
* 泛型 \`T\` 为 \`diff\` 中变化内容的快照类型(页面节点 \`MNode\` / 代码块 \`CodeBlockContent\` / 数据源 \`DataSourceSchema\`)。
*/
export interface BaseStepValue<T = unknown, U extends Record<string, any> = {}> {
/**
* 历史记录唯一标识(uuid)。入栈时自动写入(若调用方未指定),
* 用于精确定位 / 引用某一条历史记录(如 revert、埋点、跨端同步等)。
* 注意与 \`data.id\`(关联的页面 / 代码块 / 数据源 id)区分。
*/
uuid: string;
/**
* 关联目标信息:\`id\` 为关联的页面 / 代码块 / 数据源等资源 id(也是历史栈的分组 key),
* \`name\` 为展示名。所有历史类型统一携带。
*/
data: { name: string; id: Id };
/** 操作类型:新增 / 删除 / 更新(三类历史记录统一携带)。 */
opType: HistoryOpType;
/**
* 本次变更的内容(统一 diff 表达),每项见 {@link StepDiffItem}。
* 页面节点(add/remove 多节点、update 多节点)会有多项,代码块 / 数据源通常只有一项。
*/
diff: StepDiffItem<T>[];
/**
* 调用方可选传入的人类可读描述(如「调整按钮颜色」),用于历史面板展示。
* 不影响 undo/redo 行为;缺省时面板会根据节点 / propPath 自动生成描述。
*/
historyDescription?: string;
/**
* 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource}
* (画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等)。
* 仅用于历史面板展示与业务埋点,不影响 undo/redo 行为;缺省时面板视为「未知」。
*/
source?: HistoryOpSource;
/**
* 入栈时间戳(毫秒)。入栈时自动写入(若调用方未指定),仅用于历史面板展示。
*/
timestamp?: number;
/**
* 是否为「已保存」记录:DSL 落库(如保存到后端 / 本地)时由 historyService.markSaved 标记。
* 同一栈内任意时刻最多只有一条记录为 true;从 IndexedDB 恢复时游标会被定位到最近一条已保存记录之后。
*/
saved?: boolean;
/**
* 是否为「整体设置 root」(set root)产生的记录(由 {@link Editor.pushRootDiffHistory} 写入)。
* 用于「连续 set root 合并」:当某页栈最新一条已是 root 记录时,下一条 set root 会替换它而非新增,
* 避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录。
*/
rootStep?: boolean;
/** 操作人 */
operator?: string;
/** 扩展信息 */
extra?: U;
}/**
* 历史记录的扩展上下文({@link BaseStepValue.extra})。
* 内置字段供 \`page\` 类型在撤销 / 重做时恢复选区与受影响节点;扩展类型可自由附加其它键。
*/
export interface StepExtra {
/** 操作前选中的节点 ID,用于撤销后恢复选择状态(page 类型) */
selectedBefore?: Id[];
/** 操作后选中的节点 ID,用于重做后恢复选择状态(page 类型) */
selectedAfter?: Id[];
/** 本次操作涉及的节点 id 集合(page 类型) */
modifiedNodeIds?: Map<Id, Id>;
/** 操作前的节点校验错误快照,撤销后还原(使撤销一个「校验失败」的改动后错误消失) */
invalidNodeIdsBefore?: Map<Id, NodeInvalidInfo>;
/** 操作后的节点校验错误快照,重做后还原(使重做后错误恢复) */
invalidNodeIdsAfter?: Map<Id, NodeInvalidInfo>;
[key: string]: any;
}/**
* 历史记录操作类型:
* - \`add\` / \`remove\` / \`update\`:普通可撤销/重做的节点变更;
* - \`initial\`:页面「未修改的初始状态」基线(设置 root 时生成),作为页面栈 index 0 的固定底线 step。
* 该 step 不可被撤销/回滚(cursor 不会低于它),仅用于历史面板底部的初始行展示。
*/
export type HistoryOpType = 'add' | 'remove' | 'update' | 'initial';/**
* 历史记录的「操作途径」——标记本次变更由哪条交互入口触发,仅用于历史面板展示 / 业务埋点,
* 不影响 undo/redo 行为。缺省(未传)时 UI 视为「未知」。
*
* - \`stage\`:画布(拖拽 / 缩放 / 排序等舞台直接操作)
* - \`tree\`:树形面板(图层 / 数据源 / 代码块等树形结构里的拖拽 / 菜单操作)
* - \`component-panel\`:组件面板(左侧组件列表点击 / 拖拽新增组件)
* - \`props\`:配置面板表单(属性表单字段编辑)
* - \`code\`:源码编辑器(配置面板「源码」面板里直接编辑 JSON/代码后保存)
* - \`stage-contextmenu\`:画布右键菜单(舞台上节点的右键上下文菜单)
* - \`tree-contextmenu\`:树面板右键菜单(图层 / 数据源 / 代码块等树形列表上的右键上下文菜单)
* - \`toolbar\`:工具栏菜单(顶部导航工具栏按钮)
* - \`shortcut\`:键盘快捷键
* - \`rollback\`:历史回滚(历史面板里对某条历史「回滚」,反向应用为一条新记录,类 git revert)
* - \`api\`:代码 / 接口调用(程序化触发)
* - \`ai\`:AI 生成 / 智能助手触发的变更
* - \`unknown\`:未知来源
*
* 通过 \`(string & {})\` 允许业务侧扩展自定义途径字符串,同时保留内置值的自动补全。
*/
export type HistoryOpSource =
| 'initial'
| 'stage'
| 'tree'
| 'component-panel'
| 'props'
| 'code'
| 'root-code'
| 'stage-contextmenu'
| 'tree-contextmenu'
| 'toolbar'
| 'shortcut'
| 'rollback'
| 'api'
| 'ai'
// 同步
| 'sync'
| 'unknown'
| (string & {});export type Id = string | number;详情:
恢复到下一步
扩展支持: 是
参数:
{number} left{number} top{Object} options 可选配置 {boolean} doNotPushHistory 是否不写入历史记录(默认 false){string} historyDescription 见历史记录相关 options{HistoryOpSource} historySource 见历史记录相关 options返回:
{Promise<void>}详情:
更新当前选中组件位置,通常用于键盘上下左右快捷键操作
重置当前记录的修改过的节点id记录,通常用于保存之后
参数:
{Id} id 节点 id{'props' | 'style'} source 错误来源:属性表单(props)/ 样式表单(style){string} message 错误文案(可能为包含 <br> 的 HTML)详情:
记录(或覆盖)某个节点在指定来源上的校验错误信息,写入 invalidNodeIds 状态并发出 invalid-node-change 事件。
属性表单与样式表单是两个独立的 FormPanel 且均指向同一节点,故以来源为键分别保存,避免某个面板校验通过时误清另一个面板记录的错误。节点视为存在错误当且仅当任一来源存在非空文本。
示例:
import { editorService } from "@tmagic/editor";
// 标记 text_123 的属性表单校验错误
editorService.setInvalidNode("text_123", "props", "标题不能为空");参数:
{Id} id 节点 id{'props' | 'style'} source 可选;指定来源则仅删除该来源错误,不传则删除该节点全部来源的错误详情:
删除节点的校验错误记录。仅当该来源被清空且另一来源也无错误时,节点整体错误记录才会被移除;随后发出 invalid-node-change 事件。
返回:
Map<Id, NodeInvalidInfo>} 当前存在校验错误的节点错误 Map(key 为节点 id)详情:
获取当前存在校验错误的节点错误 Map,供组件树标红提示与保存拦截读取。
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { AppContext, Component } from 'vue';
import type EventEmitter from 'events';
import type * as Monaco from 'monaco-editor';
import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
import type { PascalCasedProperties, Writable } from 'type-fest';
import type {
CodeBlockContent,
CodeBlockDSL,
DataSourceSchema,
Id,
MApp,
MContainer,
MNode,
MPage,
MPageFragment,
} from '@tmagic/core';
import type { FieldSize } from '@tmagic/design';
import type { ChangeRecord, FormConfig, FormState, TableColumnConfig, TypeFunction } from '@tmagic/form';
import type StageCore from '@tmagic/stage';
import type {
CanDropIn,
ContainerHighlightType,
CustomizeMoveableOptions,
GuidesOptions,
RenderType,
UpdateDragEl,
} from '@tmagic/stage';
import type { CodeBlockService } from './services/codeBlock';
import type { ComponentListService } from './services/componentList';
import type { DataSourceService } from './services/dataSource';
import type { DepService } from './services/dep';
import type { EditorService } from './services/editor';
import type { EventsService } from './services/events';
import type { HistoryService } from './services/history';
import type { KeybindingService } from './services/keybinding';
import type { PropsService } from './services/props';
import type { StageOverlayService } from './services/stageOverlay';
import type { StorageService } from './services/storage';
import type { UiService } from './services/ui';
import type { SerializedUndoRedo, UndoRedo } from './utils/undo-redo';
export type EditorSlots = FrameworkSlots &
WorkspaceSlots &
SidebarSlots &
PropsPanelSlots & {
workspace(props: { editorService: EditorService }): any;
'workspace-content'(props: { editorService: EditorService }): any;
};
export interface FrameworkSlots {
header(props: {}): any;
nav(props: {}): any;
'content-before'(props: {}): any;
'content-after'(props: {}): any;
'src-code'(props: {}): any;
sidebar(props: {}): any;
empty(props: {}): any;
workspace(props: {}): any;
'props-panel'(props: {}): any;
footer(props: {}): any;
'page-bar'(props: {}): any;
'page-bar-add-button'(props: {}): any;
'page-bar-title'(props: { page: MPage | MPageFragment }): any;
'page-bar-popover'(props: { page: MPage | MPageFragment }): any;
'page-list-popover'(props: { list: (MPage | MPageFragment)[] }): any;
}
export interface ScrollViewerSlots {
before(props: {}): any;
content(props: {}): any;
default(props: {}): any;
}
export interface StageSlots extends ScrollViewerSlots {
'stage-top'(props: {}): any;
}
export interface WorkspaceSlots extends StageSlots {
stage(props: {}): any;
'workspace-content'(props: {}): any;
}
export interface ComponentListPanelSlots {
'component-list-panel-header'(props: {}): any;
'component-list'(props: { componentGroupList: ComponentGroup[] }): any;
'component-list-item'(props: { component: ComponentItem }): any;
}
export interface CodeBlockListPanelSlots extends CodeBlockListSlots {
'code-block-panel-search'(props: {}): any;
'code-block-panel-header'(props: {}): any;
}
export interface CodeBlockListSlots {
'code-block-panel-tool'(props: { id: Id; data: any }): any;
}
export interface DataSourceListSlots {
'data-source-panel-tool'(props: { data: any }): any;
'data-source-panel-search'(props: {}): any;
}
export interface LayerNodeSlots {
'layer-node-content'(props: { data: MNode }): any;
'layer-node-tool'(props: { data: MNode }): any;
'layer-node-label'(props: { data: MNode }): any;
}
export interface LayerPanelSlots extends LayerNodeSlots {
'layer-panel-header'(props: {}): any;
}
export interface PropsPanelSlots {
'props-panel-header'(props: {}): any;
}
export type SidebarSlots = LayerPanelSlots & CodeBlockListPanelSlots & ComponentListPanelSlots & DataSourceListSlots;
export type BeforeAdd = (config: MNode, parent: MContainer) => Promise<MNode> | MNode;
export type GetConfig = (config: FormConfig) => Promise<FormConfig> | FormConfig;
export interface EditorInstallOptions {
flat?: boolean;
parseDSL: <T = any>(dsl: string) => T;
customCreateMonacoEditor: (
monaco: typeof import('monaco-editor'),
codeEditorEl: HTMLElement,
options: Monaco.editor.IStandaloneEditorConstructionOptions & { editorCustomType?: string },
) => Promise<Monaco.editor.IStandaloneCodeEditor> | Monaco.editor.IStandaloneCodeEditor;
customCreateMonacoDiffEditor: (
monaco: typeof import('monaco-editor'),
codeEditorEl: HTMLElement,
options: Monaco.editor.IStandaloneDiffEditorConstructionOptions & { editorCustomType?: string },
) => Promise<Monaco.editor.IStandaloneDiffEditor> | Monaco.editor.IStandaloneDiffEditor;
[key: string]: any;
}
// #region Services
export interface Services {
editorService: EditorService;
historyService: HistoryService;
storageService: StorageService;
eventsService: EventsService;
propsService: PropsService;
componentListService: ComponentListService;
uiService: UiService;
codeBlockService: CodeBlockService;
depService: DepService;
dataSourceService: DataSourceService;
keybindingService: KeybindingService;
stageOverlayService: StageOverlayService;
}
// #endregion Services
export interface StageOptions {
runtimeUrl?: string;
autoScrollIntoView?: boolean;
containerHighlightClassName?: string;
containerHighlightDuration?: number;
containerHighlightType?: ContainerHighlightType;
disabledDragStart?: boolean;
render?: (stage: StageCore) => HTMLDivElement | void | Promise<HTMLDivElement | void>;
moveableOptions?: CustomizeMoveableOptions;
canSelect?: (el: HTMLElement) => boolean | Promise<boolean>;
isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
/**
* 画布上拖入组件(包括从组件列表拖入新组件、画布上拖动已有组件)时,
* 对已通过 isContainer 命中的候选容器进行二次过滤;返回 false 时阻止该容器被高亮命中
* - 在画布上拖动已有组件时:sourceIds 为被拖动组件的 id 列表
* - 从组件列表拖入新组件时:sourceIds 为空数组(尚无 id,仅可依据 targetId 判断)
* 该选项会被透传给 StageCore 的 canDropIn
*/
canDropIn?: CanDropIn;
updateDragEl?: UpdateDragEl;
renderType?: RenderType;
guidesOptions?: Partial<GuidesOptions>;
disabledMultiSelect?: boolean;
/**
* 始终启用多选模式(无需按住 Ctrl/Meta),默认 false。
* 当 \`disabledMultiSelect\` 为 true 时本配置失效。
*/
alwaysMultiSelect?: boolean;
disabledRule?: boolean;
/**
* 禁用「非点击画布选中组件时(如从图层树、面包屑等外部选中),对选中区域做高亮闪烁提示」,
* 默认 false(即默认开启闪烁)
*/
disabledFlashTip?: boolean;
zoom?: number;
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
}
/**
* 节点校验错误信息,按来源(属性表单 / 样式表单)分别保存错误文案。
* 属性表单与样式表单是两个独立的 FormPanel,均指向同一节点,故以来源为键,
* 避免某个面板校验通过时误清另一个面板记录的错误。
* 节点视为存在错误当且仅当任一来源存在非空文本。
*/
export interface NodeInvalidInfo {
/** 属性表单校验错误文案(可能为包含 <br> 的 HTML) */
props?: string;
/** 样式表单校验错误文案(可能为包含 <br> 的 HTML) */
style?: string;
}
/** 节点校验错误来源 */
export type NodeInvalidSource = keyof NodeInvalidInfo;
export interface StoreState {
root: MApp | null;
page: MPage | MPageFragment | null;
parent: MContainer | null;
node: MNode | null;
highlightNode: MNode | null;
nodes: MNode[];
stage: StageCore | null;
stageLoading: boolean;
modifiedNodeIds: Map<Id, Id>;
/** 校验失败的节点错误信息,按节点 id 存储,供组件树标记与保存拦截读取 */
invalidNodeIds: Map<Id, NodeInvalidInfo>;
pageLength: number;
pageFragmentLength: number;
disabledMultiSelect: boolean;
/** 是否始终启用多选模式(无需按住 Ctrl/Meta) */
alwaysMultiSelect: boolean;
}
export type StoreStateKey = keyof StoreState;
export interface PropsState {
propsConfigMap: Record<string, FormConfig>;
propsValueMap: Record<string, Partial<MNode>>;
relateIdMap: Record<Id, Id>;
/** 禁用数据源 */
disabledDataSource: boolean;
/** 禁用代码块 */
disabledCodeBlock: boolean;
}
export interface StageOverlayState {
wrapDiv: HTMLDivElement;
sourceEl: HTMLElement | null;
contentEl: HTMLElement | null;
stage: StageCore | null;
stageOptions: StageOptions | null;
wrapWidth: number;
wrapHeight: number;
stageOverlayVisible: boolean;
}
export interface ComponentGroupState {
list: ComponentGroup[];
}
// #region ColumnLayout
export enum ColumnLayout {
LEFT = 'left',
CENTER = 'center',
RIGHT = 'right',
}
// #endregion ColumnLayout
export interface SetColumnWidth {
[ColumnLayout.LEFT]?: number;
[ColumnLayout.CENTER]?: number | 'auto';
[ColumnLayout.RIGHT]?: number;
}
export interface GetColumnWidth {
[ColumnLayout.LEFT]: number;
[ColumnLayout.CENTER]: number;
[ColumnLayout.RIGHT]: number;
}
export interface StageRect {
width: number | string;
height: number | string;
}
export interface UiState {
/** 当前点击画布是否触发选中,true: 不触发,false: 触发,默认为false */
uiSelectMode: boolean;
/** 是否显示整个配置源码, true: 显示, false: 不显示,默认为false */
showSrc: boolean;
/** 是否将样式配置单独一列显示, true: 显示, false: 不显示,默认为true */
showStylePanel: boolean;
/** 画布显示放大倍数,默认为 1 */
zoom: number;
/** 画布容器的宽高 */
stageContainerRect: {
width: number;
height: number;
};
/** 画布顶层div的宽高,可用于改变画布的大小 */
stageRect: StageRect;
/** 编辑器列布局每一列的宽度,分为左中右三列 */
columnWidth: GetColumnWidth;
/** 编辑器列布局左侧列最小宽度 */
minLeftColumnWidth: number;
/** 编辑器列布局中间列最小宽度 */
minCenterColumnWidth: number;
/** 编辑器列布局右侧列最小宽度 */
minRightColumnWidth: number;
/** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
showGuides: boolean;
/** 画布上是否存在参考线 */
hasGuides: boolean;
/** 是否显示标尺,true: 显示,false: 不显示,默认为true */
showRule: boolean;
/** 用于控制该属性配置表单内组件的尺寸 */
propsPanelSize: 'large' | 'default' | 'small';
/** 是否显示新增页面按钮 */
showAddPageButton: boolean;
/** 是否在页面工具栏显示呼起页面列表按钮 */
showPageListButton: boolean;
/** 是否隐藏侧边栏 */
hideSlideBar: boolean;
/** 侧边栏面板配置 */
sideBarItems: SideComponent[];
/** 当前激活的侧边栏面板 */
sideBarActiveTabName: string;
// navMenu 的宽高
navMenuRect: {
left: number;
top: number;
width: number;
height: number;
};
frameworkRect: {
left: number;
top: number;
width: number;
height: number;
};
}
// #region EditorNodeInfo
export interface EditorNodeInfo {
node: MNode | null;
parent: MContainer | null;
page: MPage | MPageFragment | null;
path: MNode[];
}
// #endregion EditorNodeInfo
export interface AddMNode {
type: string;
name?: string;
inputEvent?: DragEvent;
[key: string]: any;
}
// #region PastePosition
export interface PastePosition {
left?: number;
top?: number;
/**
* 粘贴位置X方向偏移量
*/
offsetX?: number;
/**
* 粘贴位置Y方向偏移量
*/
offsetY?: number;
}
// #endregion PastePosition
// #region MenuButton
/**
* 菜单按钮
*/
export interface MenuButton {
/**
* 按钮类型
* button: 只有文字不带边框的按钮
* text: 纯文本
* divider: 分割线
* dropdown: 下拉菜单
*/
type: 'button' | 'text' | 'divider' | 'dropdown';
/** 当type为divider时有效,分割线方向, 默认vertical */
direction?: 'horizontal' | 'vertical';
/** 展示的文案 */
text?: string;
/** 鼠标悬浮是显示的气泡中的文案 */
tooltip?: string;
/** Vue组件或url */
icon?: string | Component<{}, {}, any>;
/** 是否置灰,默认为false */
disabled?: boolean | ((data: Services) => boolean);
/** 是否显示,默认为true */
display?: boolean | ((data: Services) => boolean);
/** type为button/dropdown时点击运行的方法 */
handler?: (data: Services, event: MouseEvent) => Promise<any> | any;
className?: string;
/** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */
items?: MenuButton[];
/** 唯一标识,用于高亮 */
id?: string | number;
buttonProps?: {
type?: string;
};
}
// #endregion MenuButton
// #region MenuComponent
export interface MenuComponent {
type: 'component';
/** Vue3组件 */
component: any;
/** 传入组件的props对象 */
props?: Record<string, any>;
/** 组件监听的事件对象,如:{ click: () => { console.log('click'); } } */
listeners?: Record<string, Function>;
slots?: Record<string, any>;
/** 是否显示,默认为true */
className?: string;
display?: boolean | ((data: Services) => Promise<boolean> | boolean);
[key: string]: any;
}
// #endregion MenuComponent
/**
* '/': 分隔符
* 'delete': 删除按钮
* 'undo': 撤销按钮
* 'redo': 恢复按钮
* 'zoom': 'zoom-in', 'zoom-out', 'scale-to-original', 'scale-to-fit' 的集合
* 'zoom-in': 放大按钮
* 'zoom-out': 缩小按钮
* 'guides': 显示隐藏参考线
* 'rule': 显示隐藏标尺
* 'scale-to-original': 缩放到实际大小
* 'scale-to-fit': 缩放以适应
* 'history-list': 历史记录面板(按 页面 / 数据源 / 代码块 三个 tab 展示,相邻同目标修改自动合并)
*/
// #region MenuItem
export type MenuItem =
| '/'
| 'delete'
| 'undo'
| 'redo'
| 'zoom'
| 'zoom-in'
| 'zoom-out'
| 'guides'
| 'rule'
| 'scale-to-original'
| 'scale-to-fit'
| 'history-list'
| MenuButton
| MenuComponent
| string;
// #endregion MenuItem
// #region MenuBarData
/** 工具栏 */
export interface MenuBarData {
/** 顶部工具栏左边项 */
[ColumnLayout.LEFT]?: MenuItem[];
/** 顶部工具栏中间项 */
[ColumnLayout.CENTER]?: MenuItem[];
/** 顶部工具栏右边项 */
[ColumnLayout.RIGHT]?: MenuItem[];
}
// #endregion MenuBarData
// #region SideComponent
export interface SideComponent extends MenuComponent {
/** 显示文案 */
text: string;
/** tab样式 */
tabStyle?: string | Record<string, any>;
/** vue组件或url */
icon?: any;
/** slide 唯一标识 key */
$key: string;
/** 是否可以将面板拖出,默认为true */
draggable?: boolean;
/** 点击切换tab前调用,返回false阻止切换 */
beforeClick?: (config: SideComponent) => boolean | Promise<boolean>;
/** 组件扩展参数 */
boxComponentConfig?: {
/** Vue3组件 */
component?: any;
/** 传入组件的props对象 */
props?: Record<string, any>;
};
}
// #endregion SideComponent
// #region HistoryListExtraTab
/**
* 历史记录面板(HistoryListPanel)的自定义扩展 tab。
*
* 业务方可通过 Editor 的 \`historyListExtraTabs\` 注入额外的历史记录 tab,
* 例如某个自定义模块维护自己的操作历史时,可以在历史记录面板中增加一个
* 独立的 tab 来展示与回滚。内置的「页面 / 数据源 / 代码块」三个 tab 之后
* 会依次追加这些扩展 tab。
*/
export interface HistoryListExtraTab {
/** tab 唯一标识,作为 TMagicTabs 的 name */
name: string;
/** tab 显示文案,支持传入函数以展示动态内容(如记录数量) */
label: string | (() => string);
/** tab 内容区渲染的组件(Vue 组件或字符串标签) */
component: any;
/** 传入内容组件的 props */
props?: Record<string, any>;
/** 内容组件的事件监听 */
listeners?: Record<string, (..._args: any[]) => any>;
}
// #endregion HistoryListExtraTab
// #region CompareForm
/**
* 对比表单(CompareForm)的对比类型:
* - node: 节点组件,按 \`type\` 从 propsService 获取属性表单配置
* - data-source: 数据源,按 \`type\`(base/http/...) 从 dataSourceService 获取数据源表单配置
* - code-block: 数据源代码块,使用内置的代码块表单配置
*/
export type CompareCategory = 'node' | 'data-source' | 'code-block' | string;
/**
* 自定义 \`loadConfig\` 时回传的上下文,聚合了组件当前的对比入参,
* 方便调用方在外部按需拼装 FormConfig。
*/
export interface CompareFormLoadConfigContext {
/** 对比类型,见 CompareCategory */
category: string;
/** 节点 / 数据源类型 */
type?: string;
/** 数据源代码块场景下的数据源类型 */
dataSourceType?: string;
/**
* 内置的默认 FormConfig 加载逻辑(按 \`category\` 从 propsService / dataSourceService /
* 代码块工具取配置)。自定义 \`loadConfig\` 可调用它复用默认结果,再做二次加工。
*/
defaultLoadConfig: () => Promise<FormConfig>;
}
/**
* 自定义 FormConfig 加载逻辑。传入后将接管内置的按 \`category\` 取配置逻辑,
* 可通过 \`ctx.defaultLoadConfig()\` 复用默认结果再做二次加工。
*/
export type CompareFormLoadConfig = (ctx: CompareFormLoadConfigContext) => FormConfig | Promise<FormConfig>;
/**
* CompareForm / ViewForm 共用的基础 props。
* 两者都基于同一套「按 category 加载 FormConfig + 注入 services/stage」的逻辑(见 useCompareForm),
* 差异仅在于是否做新旧值对比。这里抽出公共字段避免重复定义。
*/
export interface CompareFormBaseProps {
/** 当前值(对比场景下为修改后的值) */
value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
/**
* 类型说明:
* - \`category\` 为 \`node\` 时,\`type\` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
* - \`category\` 为 \`data-source\` 时,\`type\` 为数据源类型,例如 'base'、'http'
* - \`category\` 为 \`code-block\` 时,\`type\` 可不传
*/
type?: string;
/** 表单配置类别,决定从哪里取 FormConfig */
category?: CompareCategory;
/** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
dataSourceType?: string;
labelWidth?: string;
/**
* 外层容器高度。设置后表单内容超出时会在组件内部出现滚动条,
* 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 \`60vh\` / \`400px\` / \`100%\`。
*/
height?: string;
/**
* 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 \`extendFormState\`、
* PropsPanel 的 \`extend-state\` 语义一致。表单 item 的 \`display\` / \`disabled\` 等
* filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
* 因此在对比 / 展示场景下也需要透传,避免出现 \`formState.xxx is undefined\` 的运行时错误。
*/
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
* 组件会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
*/
baseFormState?: FormState;
/**
* 表单内组件的尺寸(透传给 MForm 的 \`size\`),可选 'large' | 'default' | 'small'。
* 缺省时使用 MForm 内置默认尺寸。
*/
size?: FieldSize;
/**
* 自定义 FormConfig 加载逻辑。传入后将接管内置的按 \`category\`(node/data-source/code-block)
* 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
* \`ctx.defaultLoadConfig()\` 复用默认结果再做二次加工。
*/
loadConfig?: CompareFormLoadConfig;
/** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
services?: Services;
}
// #endregion CompareForm
// #region SideItemKey
export enum SideItemKey {
COMPONENT_LIST = 'component-list',
LAYER = 'layer',
CODE_BLOCK = 'code-block',
DATA_SOURCE = 'data-source',
}
// #endregion SideItemKey
// #region SideItem
/**
* component-list: 组件列表
* layer: 已选组件树
* code-block: 代码块
*/
export type SideItem = \`\${SideItemKey}\` | SideComponent;
// #endregion SideItem
// #region SideBarData
/** 工具栏 */
export interface SideBarData {
/** 容器类型 */
type: 'tabs';
/** 默认激活的内容 */
status: string;
/** panel列表 */
items: SideItem[];
}
// #endregion SideBarData
// #region ComponentItem
export interface ComponentItem {
/** 显示文案 */
text: string;
/** 详情,用于tooltip */
desc?: string;
/** 组件类型 */
type: string;
/** Vue组件或url */
icon?: string | Component<{}, {}, any>;
/** 新增组件时需要透传到组价节点上的数据 */
data?: {
[key: string]: any;
};
}
// #endregion ComponentItem
// #region ComponentGroup
export interface ComponentGroup {
/** 显示文案 */
title: string;
/** 组内列表 */
items: ComponentItem[];
}
// #endregion ComponentGroup
export enum LayerOffset {
TOP = 'top',
BOTTOM = 'bottom',
}
// #region Layout
/** 容器布局 */
export enum Layout {
FLEX = 'flex',
FIXED = 'fixed',
RELATIVE = 'relative',
ABSOLUTE = 'absolute',
}
// #endregion Layout
export enum Keys {
ESCAPE = 'Space',
}
export interface ScrollViewerEvent {
scrollLeft: number;
scrollTop: number;
scrollHeight: number;
scrollWidth: number;
}
export type CodeState = {
/** 代码块DSL数据源 */
codeDsl: CodeBlockDSL | null;
/** 代码块是否可编辑 */
editable: boolean;
/** list模式下左侧展示的代码列表 */
combineIds: string[];
/** 为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除) */
undeletableList: Id[];
paramsColConfig?: TableColumnConfig;
};
export type CodeRelation = {
/** 组件id:[代码id1,代码id2] */
[compId: Id]: Id[];
};
export interface CodeDslItem {
/** 代码块id */
id: Id;
/** 代码块名称 */
name: string;
/** 代码块函数内容 */
codeBlockContent?: CodeBlockContent;
/** 是否展示代码绑定关系 */
showRelation?: boolean;
/** 代码块对应绑定的组件信息 */
combineInfo?: CombineInfo[];
}
export interface CombineInfo {
/** 组件id */
compId: Id;
/** 组件名称 */
compName: string;
}
export interface ListState {
/** 代码块列表 */
codeList: CodeDslItem[];
}
export enum CodeDeleteErrorType {
/** 代码块存在于不可删除列表中 */
UNDELETEABLE = 'undeleteable',
/** 代码块存在绑定关系 */
BIND = 'bind',
}
// 代码块草稿localStorage key
export const CODE_DRAFT_STORAGE_KEY = 'magicCodeDraft';
export interface CodeParamStatement {
/** 参数名称 */
name: string;
/** 参数类型 */
type?: string | TypeFunction<string>;
[key: string]: any;
}
// #region HistoryOpType
/**
* 历史记录操作类型:
* - \`add\` / \`remove\` / \`update\`:普通可撤销/重做的节点变更;
* - \`initial\`:页面「未修改的初始状态」基线(设置 root 时生成),作为页面栈 index 0 的固定底线 step。
* 该 step 不可被撤销/回滚(cursor 不会低于它),仅用于历史面板底部的初始行展示。
*/
export type HistoryOpType = 'add' | 'remove' | 'update' | 'initial';
// #endregion HistoryOpType
// #region HistoryOpSource
/**
* 历史记录的「操作途径」——标记本次变更由哪条交互入口触发,仅用于历史面板展示 / 业务埋点,
* 不影响 undo/redo 行为。缺省(未传)时 UI 视为「未知」。
*
* - \`stage\`:画布(拖拽 / 缩放 / 排序等舞台直接操作)
* - \`tree\`:树形面板(图层 / 数据源 / 代码块等树形结构里的拖拽 / 菜单操作)
* - \`component-panel\`:组件面板(左侧组件列表点击 / 拖拽新增组件)
* - \`props\`:配置面板表单(属性表单字段编辑)
* - \`code\`:源码编辑器(配置面板「源码」面板里直接编辑 JSON/代码后保存)
* - \`stage-contextmenu\`:画布右键菜单(舞台上节点的右键上下文菜单)
* - \`tree-contextmenu\`:树面板右键菜单(图层 / 数据源 / 代码块等树形列表上的右键上下文菜单)
* - \`toolbar\`:工具栏菜单(顶部导航工具栏按钮)
* - \`shortcut\`:键盘快捷键
* - \`rollback\`:历史回滚(历史面板里对某条历史「回滚」,反向应用为一条新记录,类 git revert)
* - \`api\`:代码 / 接口调用(程序化触发)
* - \`ai\`:AI 生成 / 智能助手触发的变更
* - \`unknown\`:未知来源
*
* 通过 \`(string & {})\` 允许业务侧扩展自定义途径字符串,同时保留内置值的自动补全。
*/
export type HistoryOpSource =
| 'initial'
| 'stage'
| 'tree'
| 'component-panel'
| 'props'
| 'code'
| 'root-code'
| 'stage-contextmenu'
| 'tree-contextmenu'
| 'toolbar'
| 'shortcut'
| 'rollback'
| 'api'
| 'ai'
// 同步
| 'sync'
| 'unknown'
| (string & {});
// #endregion HistoryOpSource
// #region DslOpWithHistoryIdsResult
/** *AndGetHistoryId 系列方法返回值:原操作结果 + 本次写入历史记录的 uuid 列表(未入栈时为 \`[]\`)。 */
export type DslOpWithHistoryIdsResult<T> = {
result: T;
historyIds: string[];
};
// #endregion DslOpWithHistoryIdsResult
// #region StepDiffItem
/**
* 单条变更的 diff 描述,统一表达「页面节点 / 代码块 / 数据源」的变化内容,
* 被 {@link StepValue} / {@link CodeBlockStepValue} / {@link DataSourceStepValue} 的 \`diff\` 复用。
*
* 按 \`opType\` 区分携带的字段:
* - \`add\`:仅 \`newSchema\`(页面节点还带 \`parentId\` / \`index\`);
* - \`remove\`:仅 \`oldSchema\`(页面节点还带 \`parentId\` / \`index\`);
* - \`update\`:\`oldSchema\` + \`newSchema\`,并可带 \`changeRecords\` 做局部更新。
*
* 泛型 \`T\` 为变化内容的快照类型:页面节点为 \`MNode\`,代码块为 \`CodeBlockContent\`,数据源为 \`DataSourceSchema\`。
*/
export interface StepDiffItem<T = unknown> {
/** 变更后的内容快照。\`opType\` 为 \`add\` / \`update\` 时有,\`remove\` 时无。 */
newSchema?: T;
/** 变更前的内容快照。\`opType\` 为 \`remove\` / \`update\` 时有,\`add\` 时无。 */
oldSchema?: T;
/** 父节点 id。仅页面节点有(数据源 / 代码块没有父节点)。 */
parentId?: Id;
/** 在父节点 items 数组中的索引。仅页面节点有(数据源 / 代码块无需排序)。 */
index?: number;
/**
* form 端 propPath/value 变更列表,仅 \`opType\` 为 \`update\` 时有;
* 撤销/重做时若有则按 propPath 局部更新,缺省才退化为整内容替换。
*/
changeRecords?: ChangeRecord[];
}
// #endregion StepDiffItem
// #region BaseStepValue
/**
* 历史记录条目公共字段,被 {@link StepValue} / {@link CodeBlockStepValue} / {@link DataSourceStepValue} 复用。
*
* 泛型 \`T\` 为 \`diff\` 中变化内容的快照类型(页面节点 \`MNode\` / 代码块 \`CodeBlockContent\` / 数据源 \`DataSourceSchema\`)。
*/
export interface BaseStepValue<T = unknown, U extends Record<string, any> = {}> {
/**
* 历史记录唯一标识(uuid)。入栈时自动写入(若调用方未指定),
* 用于精确定位 / 引用某一条历史记录(如 revert、埋点、跨端同步等)。
* 注意与 \`data.id\`(关联的页面 / 代码块 / 数据源 id)区分。
*/
uuid: string;
/**
* 关联目标信息:\`id\` 为关联的页面 / 代码块 / 数据源等资源 id(也是历史栈的分组 key),
* \`name\` 为展示名。所有历史类型统一携带。
*/
data: { name: string; id: Id };
/** 操作类型:新增 / 删除 / 更新(三类历史记录统一携带)。 */
opType: HistoryOpType;
/**
* 本次变更的内容(统一 diff 表达),每项见 {@link StepDiffItem}。
* 页面节点(add/remove 多节点、update 多节点)会有多项,代码块 / 数据源通常只有一项。
*/
diff: StepDiffItem<T>[];
/**
* 调用方可选传入的人类可读描述(如「调整按钮颜色」),用于历史面板展示。
* 不影响 undo/redo 行为;缺省时面板会根据节点 / propPath 自动生成描述。
*/
historyDescription?: string;
/**
* 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource}
* (画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等)。
* 仅用于历史面板展示与业务埋点,不影响 undo/redo 行为;缺省时面板视为「未知」。
*/
source?: HistoryOpSource;
/**
* 入栈时间戳(毫秒)。入栈时自动写入(若调用方未指定),仅用于历史面板展示。
*/
timestamp?: number;
/**
* 是否为「已保存」记录:DSL 落库(如保存到后端 / 本地)时由 historyService.markSaved 标记。
* 同一栈内任意时刻最多只有一条记录为 true;从 IndexedDB 恢复时游标会被定位到最近一条已保存记录之后。
*/
saved?: boolean;
/**
* 是否为「整体设置 root」(set root)产生的记录(由 {@link Editor.pushRootDiffHistory} 写入)。
* 用于「连续 set root 合并」:当某页栈最新一条已是 root 记录时,下一条 set root 会替换它而非新增,
* 避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录。
*/
rootStep?: boolean;
/** 操作人 */
operator?: string;
/** 扩展信息 */
extra?: U;
}
// #endregion BaseStepValue
// #region StepExtra
/**
* 历史记录的扩展上下文({@link BaseStepValue.extra})。
* 内置字段供 \`page\` 类型在撤销 / 重做时恢复选区与受影响节点;扩展类型可自由附加其它键。
*/
export interface StepExtra {
/** 操作前选中的节点 ID,用于撤销后恢复选择状态(page 类型) */
selectedBefore?: Id[];
/** 操作后选中的节点 ID,用于重做后恢复选择状态(page 类型) */
selectedAfter?: Id[];
/** 本次操作涉及的节点 id 集合(page 类型) */
modifiedNodeIds?: Map<Id, Id>;
/** 操作前的节点校验错误快照,撤销后还原(使撤销一个「校验失败」的改动后错误消失) */
invalidNodeIdsBefore?: Map<Id, NodeInvalidInfo>;
/** 操作后的节点校验错误快照,重做后还原(使重做后错误恢复) */
invalidNodeIdsAfter?: Map<Id, NodeInvalidInfo>;
[key: string]: any;
}
// #endregion StepExtra
// #region StepValue
/**
* 页面节点历史记录条目(\`diff\` 内容为 {@link MNode})。结构已与代码块 / 数据源统一收敛到
* {@link BaseStepValue}:关联 id 见 \`data.id\`,选区等上下文见 \`extra\`。
*/
export type StepValue = BaseStepValue<MNode, StepExtra>;
// #endregion StepValue
// #region CodeBlockStepValue
/**
* 代码块历史记录条目(\`diff\` 内容为 {@link CodeBlockContent}),按 \`data.id\`(codeBlock.id)
* 分组保存到 historyState.steps.codeBlock。结构与 {@link StepValue} / {@link DataSourceStepValue}
* 一致,仅 \`diff\` 快照类型不同。
*/
export type CodeBlockStepValue = BaseStepValue<CodeBlockContent>;
// #endregion CodeBlockStepValue
// #region DataSourceStepValue
/**
* 数据源历史记录条目(\`diff\` 内容为 {@link DataSourceSchema}),按 \`data.id\`(dataSource.id)
* 分组保存到 historyState.steps.dataSource。结构与 {@link StepValue} / {@link CodeBlockStepValue}
* 一致,仅 \`diff\` 快照类型不同。
*/
export type DataSourceStepValue = BaseStepValue<DataSourceSchema>;
// #endregion DataSourceStepValue
// #region HistorySteps
/**
* 历史记录类型标识:内置 \`page\` / \`codeBlock\` / \`dataSource\`,并允许业务扩展自定义类型。
* \`(string & {})\` 保留对内置字面量的智能提示,同时不限制扩展取值。
*/
export type HistoryStepType = 'page' | 'codeBlock' | 'dataSource' | (string & {});
/**
* 全部历史栈的统一容器,按「类型 -> id -> UndoRedo 栈」两级分组。
*
* - \`page\`:页面历史栈,按 page.id 分组(每页一份 UndoRedo);
* - \`codeBlock\`:代码块历史栈,按 codeBlock.id 分组;
* - \`dataSource\`:数据源历史栈,按 dataSource.id 分组;
* - 其余键:业务通过 {@link HistoryService.registerStepType} 注册的自定义历史类型。
*
* 所有类型(含扩展类型)一视同仁:均按 id 独立分栈、独立 undo/redo,且都可通过
* {@link HistoryService.setMarker} 在 index 0 种入 \`initial\` 基线(撤销 / 回滚不会越过该基线)。
*/
export interface HistorySteps {
page: Record<Id, UndoRedo<StepValue>>;
codeBlock: Record<Id, UndoRedo<CodeBlockStepValue>>;
dataSource: Record<Id, UndoRedo<DataSourceStepValue>>;
/** 扩展历史类型:按 id 分组的 UndoRedo 栈。 */
[stepType: string]: Record<Id, UndoRedo<any>>;
}
// #endregion HistorySteps
export interface HistoryState {
/**
* 全部历史栈的统一容器(页面 / 代码块 / 数据源 / 扩展类型),见 {@link HistorySteps}。
* 各类型互不影响,支持按 id 独立 undo/redo;是否可撤销 / 重做改用 {@link HistoryService.canUndo} /
* {@link HistoryService.canRedo}(按 stepType + id 查询)替代旧的全局 canUndo / canRedo 字段。
*/
steps: HistorySteps;
/**
* 各历史类型的展示名称,用于历史面板({@link HistorySteps} 的 tab / 分组标题等)。
* 内置 \`page\` / \`codeBlock\` / \`dataSource\` 有默认中文名(页面 / 代码块 / 数据源),
* 扩展类型可通过 {@link HistoryService.registerStepType} 的 \`name\` 选项或
* {@link HistoryService.setStepName} 登记;读取请用 {@link HistoryService.getStepName}。
*/
stepNames: Record<string, string>;
}
// #region PersistedHistoryState
/**
* 历史记录的可持久化快照。由 historyService.saveToIndexedDB 写入 IndexedDB,
* 再由 historyService.restoreFromIndexedDB 读出并重建各 UndoRedo 栈。
*/
export interface PersistedHistoryState {
/** 快照结构版本号,便于后续兼容升级。 */
version: number;
/**
* 全部历史栈的序列化快照,按「类型 -> id」两级分组,与 {@link HistorySteps} 对应。
* 内置 \`page\` / \`codeBlock\` / \`dataSource\`,并包含业务注册的扩展类型。
*/
steps: {
page: Record<Id, SerializedUndoRedo<StepValue>>;
codeBlock: Record<Id, SerializedUndoRedo<CodeBlockStepValue>>;
dataSource: Record<Id, SerializedUndoRedo<DataSourceStepValue>>;
[stepType: string]: Record<Id, SerializedUndoRedo<any>>;
};
/** 保存时间戳(毫秒)。 */
savedAt: number;
}
// #endregion PersistedHistoryState
// #region HistoryPersistOptions
/** historyService 持久化相关 API 的可选配置。 */
export interface HistoryPersistOptions {
/** IndexedDB 数据库名,默认 \`tmagic-editor\`(最终库名会拼上当前 DSL app id)。 */
dbName?: string;
/** objectStore 名,默认 \`history\`。 */
storeName?: string;
/** 记录 key,用于区分不同活动页 / 项目,默认 \`default\`。 */
key?: IDBValidKey;
/**
* 显式指定用于库名隔离的 DSL app id。
* 缺省时回退到当前 editorService 的 \`root.id\`;在「先恢复历史再 set root」场景下 root 尚未设置,
* 需由调用方(如从待加载 DSL 取 id)显式传入,否则会读 / 写到未按 app 隔离的默认库。
*/
appId?: Id;
}
// #endregion HistoryPersistOptions
// #region HistoryListEntry
/**
* 历史面板用:当前页面的一条历史步骤(包含位置和是否已应用)。
*/
export interface HistoryStepEntry<T> {
/** 步骤内容 */
step: T;
/** 在所属栈中的索引(0 为最早) */
index: number;
/** 是否处于"已应用"段(即位于栈游标之前)。撤销后变为 false。 */
applied: boolean;
/** 是否为当前所在的步骤(栈中最近一次已应用的那一步,即 index === cursor - 1)。 */
isCurrent?: boolean;
}
/**
* 历史面板分组(页面 / 数据源 / 代码块 / 扩展类型统一结构)。
*
* 把指定历史栈的步骤列表按"目标"做相邻合并:
* - 连续修改同一目标(单实体 update,targetId 一致)的多步合并成一组,组内可展开查看每步;
* - add / remove / 多实体 update 始终独立成组(无法明确归属单一目标);
* - targetId 为 undefined 表示"无明确目标",不参与合并。
*
* 各类型仅 \`kind\` 与 step 快照类型不同,统一由泛型描述:
* - 页面:\`HistoryGroup<StepValue>\`,\`kind: 'page'\`,\`id\` 为 pageId,\`targetId\` 为被改节点 id;
* - 数据源:\`HistoryGroup<DataSourceStepValue>\`,\`kind: 'data-source'\`,\`id\` 为 dataSource.id;
* - 代码块:\`HistoryGroup<CodeBlockStepValue>\`,\`kind: 'code-block'\`,\`id\` 为 codeBlock.id。
*/
export interface HistoryGroup<T extends BaseStepValue = BaseStepValue> {
/** 历史类型标识:page / code-block / data-source(扩展类型同理)。 */
kind: string;
/** 所属栈 id(page 为 pageId,代码块 / 数据源为对应资源 id)。 */
id: Id;
/** 该分组的操作类型。 */
opType: HistoryOpType;
/**
* 合并的目标 id:仅"单实体 update"有值,并按此与相邻同 id 的 update 合并。
* undefined 表示该分组不可被合并(add / remove / 多实体 update)。
*/
targetId?: Id;
/** 目标可读名(取最后一步快照的 name/type/id)。 */
targetName?: string;
/** 组内所有步骤,按时间正序。 */
steps: { step: T; index: number; applied: boolean; isCurrent?: boolean }[];
/** 组内最后一步是否已应用。 */
applied: boolean;
/** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组)。 */
isCurrent?: boolean;
}
// #endregion HistoryListEntry
export enum KeyBindingCommand {
/** 复制 */
COPY_NODE = 'tmagic-system-copy-node',
/** 粘贴 */
PASTE_NODE = 'tmagic-system-paste-node',
/** 删除 */
DELETE_NODE = 'tmagic-system-delete-node',
/** 剪切 */
CUT_NODE = 'tmagic-system-cut-node',
/** 撤销 */
UNDO = 'tmagic-system-undo',
/** 重做 */
REDO = 'tmagic-system-redo',
/** 放大 */
ZOOM_IN = 'tmagic-system-zoom-in',
/** 缩小 */
ZOOM_OUT = 'tmagic-system-zoom-out',
/** 缩放到实际大小 */
ZOOM_RESET = 'tmagic-system-zoom-reset',
/** 缩放以适应 */
ZOOM_FIT = 'tmagic-system-zoom-fit',
/** 向上移动1px */
MOVE_UP_1 = 'tmagic-system-move-up-1',
/** 向下移动1px */
MOVE_DOWN_1 = 'tmagic-system-move-down-1',
/** 向左移动1px */
MOVE_LEFT_1 = 'tmagic-system-move-left-1',
/** 向右移动1px */
MOVE_RIGHT_1 = 'tmagic-system-move-right-1',
/** 向上移动10px */
MOVE_UP_10 = 'tmagic-system-move-up-10',
/** 向下移动10px */
MOVE_DOWN_10 = 'tmagic-system-move-down-10',
/** 向左移动10px */
MOVE_LEFT_10 = 'tmagic-system-move-left-10',
/** 向右移动10px */
MOVE_RIGHT_10 = 'tmagic-system-move-right-10',
/** 切换组件 */
SWITCH_NODE = 'tmagic-system-switch-node',
}
export interface KeyBindingItem {
command: KeyBindingCommand | string;
keybinding?: string | string[];
when: [string, 'keyup' | 'keydown'][];
}
export interface KeyBindingCacheItem {
type: string;
command: KeyBindingCommand | string;
keybinding?: string | string[];
eventType: 'keyup' | 'keydown';
bound: boolean;
}
// #region DatasourceTypeOption
/** 可新增的数据源类型选项 */
export interface DatasourceTypeOption {
/** 数据源类型 */
type: string;
/** 数据源名称 */
text: string;
}
// #endregion DatasourceTypeOption
/** 组件树节点状态 */
export interface LayerNodeStatus {
/** 显隐 */
visible: boolean;
/** 展开子节点 */
expand: boolean;
/** 选中 */
selected: boolean;
/** 是否可拖拽 */
draggable: boolean;
}
/** 拖拽类型 */
export enum DragType {
/** 从组件列表拖到画布 */
COMPONENT_LIST = 'component-list',
/** 拖动组件树节点 */
LAYER_TREE = 'layer-tree',
}
// #region TreeNodeData
export interface TreeNodeData {
id: Id;
name?: string;
items?: TreeNodeData[];
[key: string]: any;
}
// #endregion TreeNodeData
/** 判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
export type IsExpandableFunction = (_data: TreeNodeData, _nodeStatusMap: Map<Id, LayerNodeStatus>) => boolean;
/** canDropIn 的调用场景 */
export type CanDropInScene =
/** 在"已选组件"面板的组件树中拖动节点 */
| 'layer'
/** 在画布上拖动已有组件(被拖动组件本身已经存在于画布中,sourceIds 包含其 id) */
| 'stage-drag'
/** 从组件列表拖入新组件到画布(被拖入的组件尚不存在,sourceIds 为空数组) */
| 'stage-add';
/**
* 判断当前正在拖动的源节点是否可以拖入目标节点内部的函数
* @param _sourceIds 当前正在拖动的源节点 id 列表
* - \`layer\`:被拖动的组件树节点 id(单选时长度为 1)
* - \`stage-drag\`:被拖动组件的 id 列表(多选拖动时为多个)
* - \`stage-add\`:始终为空数组(从组件列表新增的组件尚无 id)
* @param _targetId 目标容器的节点 id
* @param _scene 调用场景:见 {@link CanDropInScene}
* @returns
* - \`false\`:阻止该容器被视为合法拖入目标
* - \`layer\`:禁用 inner 高亮(before/after 仍然可用)
* - \`stage-drag\`:阻止该容器被高亮命中
* - \`stage-add\`:阻止该容器被高亮命中并退化为放入当前页面
* - \`Id\`(string | number):将拖入目标重定向到该 id 对应的节点
* (例如把命中的"卡片外壳"节点重定向到其内层"卡片内容"容器节点)
* - 其他(\`true\` / \`void\` / \`undefined\`):按原 targetId 正常拖入
*/
export type CanDropInFunction = (_sourceIds: Id[], _targetId: Id, _scene: CanDropInScene) => Id | boolean | void;
export type AsyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (...args: Parameters<C[K]>) => Promise<Parameters<C[K]>> | Parameters<C[K]>;
};
export type AsyncAfterHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (
result: Awaited<ReturnType<C[K]>>,
...args: Parameters<C[K]>
) => ReturnType<C[K]> | Awaited<ReturnType<C[K]>>;
};
export type SyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (...args: Parameters<C[K]>) => Parameters<C[K]>;
};
export type SyncAfterHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (result: ReturnType<C[K]>, ...args: Parameters<C[K]>) => ReturnType<C[K]>;
};
export type AddPrefixToObject<T, P extends string> = {
[K in keyof T as K extends string ? \`\${P}\${K}\` : never]: T[K];
};
export type AsyncHookPlugin<
T extends Array<string>,
C extends Record<T[number], (...args: any) => any>,
> = AddPrefixToObject<PascalCasedProperties<AsyncBeforeHook<T, C>>, 'before'> &
AddPrefixToObject<PascalCasedProperties<AsyncAfterHook<T, C>>, 'after'>;
export type SyncHookPlugin<
T extends Array<string>,
C extends Record<T[number], (...args: any) => any>,
> = AddPrefixToObject<PascalCasedProperties<SyncBeforeHook<T, C>>, 'before'> &
AddPrefixToObject<PascalCasedProperties<SyncAfterHook<T, C>>, 'after'>;
export interface EventBusEvent {
'edit-data-source': [id: string];
'edit-data-source-method': [id: string, methodName: string];
'edit-data-source-field': [id: string, fieldPath: string[]];
'remove-data-source': [id: string];
'edit-code': [id: string];
}
export interface EventBus extends EventEmitter {
on<Name extends keyof EventBusEvent, Param extends EventBusEvent[Name]>(
eventName: Name,
listener: (...args: Param) => void,
): this;
emit<Name extends keyof EventBusEvent, Param extends EventBusEvent[Name]>(eventName: Name, ...args: Param): boolean;
}
// #region PropsFormConfigFunction
export type PropsFormConfigFunction = (data: { editorService: EditorService }) => FormConfig;
// #endregion PropsFormConfigFunction
export type PropsFormValueFunction = (data: { editorService: EditorService }) => Partial<MNode>;
// #region PageBarSortOptions
export type PartSortableOptions = Omit<Options, 'onStart' | 'onUpdate'>;
export interface PageBarSortOptions extends PartSortableOptions {
/** 在onUpdate之后调用 */
afterUpdate?: (event: SortableEvent, sortable: Sortable) => void | Promise<void>;
/** 在onStart之前调用 */
beforeStart?: (event: SortableEvent, sortable: Sortable) => void | Promise<void>;
}
// #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: ContentMenuType,
/** 读取当前右键目标;数据源 / 代码块面板会传入,图层 / 画布一般不需要 */
getTarget?: () => ContentMenuTarget | null,
) => (MenuButton | MenuComponent)[];
export interface EditorEvents {
'root-change': [
value: StoreState['root'],
preValue?: StoreState['root'],
options?: { historySource?: HistoryOpSource },
];
select: [node: MNode | null];
add: [nodes: MNode[]];
remove: [nodes: MNode[]];
update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
'move-layer': [offset: number | LayerOffset];
'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
'history-change': [data: MPage | MPageFragment];
/**
* DSL 发生变更后统一触发,免去分别监听 add / remove / update / move-layer / drag-to。
* 回调参数为 {@link EditorChangeEvent},按 \`type\` 区分操作类型并携带各自的操作内容(payload)
* 以及变更所在的当前 page(可能为 null)。撤销 / 重做内部同样会经由
* add / remove / update 触发本事件;如需区分「用户操作」与「撤销重做」请配合 \`history-change\`。
*/
change: [event: EditorChangeEvent];
/** 节点校验错误状态发生变化时触发,携带当前完整的错误 Map(供非响应式消费方订阅) */
'invalid-node-change': [invalidNodeIds: Map<Id, NodeInvalidInfo>];
}
// #region EditorChangeEvent
/** \`change\` 事件中单个变更项:变更的 node 及其所属的 page(可能为 null)。 */
export interface EditorChangeItem {
node: MNode;
page: StoreState['page'];
}
/** \`update\` 类型变更项:node 为前后快照及 form 端变更记录,page 为其所属页面。 */
export interface EditorUpdateChangeItem {
node: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] };
page: StoreState['page'];
}
/** {@link EditorEvents.change} 各操作类型共有的历史相关字段。 */
export interface EditorChangeEventHistoryMeta {
/**
* 本次变更的「历史来源」(调用 DSL 操作时传入的 {@link HistoryOpOptions.historySource},
* 撤销 / 重做时则为被应用 step 上记录的 \`source\`),未携带时为 undefined。
*/
historySource?: HistoryOpSource;
/**
* 本次操作是否未写入历史记录(即调用时传入的 {@link HistoryOpOptions.doNotPushHistory},缺省为 false);
* 撤销 / 重做路径补发的事件恒为 true(撤销/重做本身不再入栈)。
*/
doNotPushHistory?: boolean;
}
/**
* {@link EditorEvents.change} 的回调参数:以 \`type\` 区分操作类型,并携带对应的操作内容。
* \`data\` 为本次变更的节点列表,每项包含 node 及其所属的 page(可能为 null);
* \`move-layer\` 额外带层级偏移 \`offset\`,\`drag-to\` 额外带目标位置 \`targetIndex\` / \`targetParent\`;
* 历史相关字段(historySource / doNotPushHistory)见 {@link EditorChangeEventHistoryMeta}。
*/
export type EditorChangeEvent =
| ({ type: 'add'; data: EditorChangeItem[] } & EditorChangeEventHistoryMeta)
| ({ type: 'remove'; data: EditorChangeItem[] } & EditorChangeEventHistoryMeta)
| ({ type: 'update'; data: EditorUpdateChangeItem[] } & EditorChangeEventHistoryMeta)
| ({ type: 'move-layer'; data: EditorChangeItem[]; offset: number | LayerOffset } & EditorChangeEventHistoryMeta)
| ({
type: 'drag-to';
data: EditorChangeItem[];
targetIndex: number;
targetParent: MContainer;
} & EditorChangeEventHistoryMeta);
// #endregion EditorChangeEvent
export interface HistoryEvents {
change: [
state: BaseStepValue | StepValue | CodeBlockStepValue | DataSourceStepValue,
stepType: HistoryStepType,
id: Id,
];
'code-block-history-change': [id: Id, state: CodeBlockStepValue];
'data-source-history-change': [id: Id, state: DataSourceStepValue];
'restore-from-indexed-db': [snapshot: PersistedHistoryState | null];
'save-to-indexed-db': [snapshot: PersistedHistoryState];
'mark-saved': [{ kind: HistoryStepType; id?: Id }];
clear: [{ id: Id; stepType: HistoryStepType }];
'marker-change': [{ id: Id; marker: StepValue; stepType: HistoryStepType }];
/**
* 页面 / 页面片结构变更(新增 / 删除)时派发,见 {@link HistoryService.notifyPageStructureChange}。
* 一次操作(add / remove / setRoot 整体替换)涉及多个页面时合并为**一个**事件;
* \`add\` / \`remove\` 分别为本次新增与删除的页面列表(其一可为空数组)。
*/
'page-structure-change': [change: { add: (MPage | MPageFragment)[]; remove: (MPage | MPageFragment)[] }];
}
export const canUsePluginMethods = {
async: [
'getLayout',
'highlight',
'select',
'multiSelect',
'doAdd',
'add',
'doRemove',
'remove',
'doUpdate',
'update',
'sort',
'copy',
'paste',
'doPaste',
'doAlignCenter',
'alignCenter',
'moveLayer',
'moveToContainer',
'dragTo',
'undo',
'redo',
'move',
] as const,
sync: [],
};
export type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
// #region HistoryOpOptions
/**
* 历史记录写入相关的通用配置(codeBlock / dataSource / editor 共用)
* - doNotPushHistory: 操作完成后是否不要将本次操作压入历史栈(撤销/重做记录),默认 false
* - historyDescription: 入栈时附带的人类可读描述,用于历史面板展示;不影响 undo/redo 行为,缺省时面板会自动生成描述
* - historySource: 操作途径,取值见 {@link HistoryOpSource}(画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等),用于历史面板展示与埋点;不影响 undo/redo 行为
*/
export interface HistoryOpOptions {
doNotPushHistory?: boolean;
historyDescription?: string;
historySource?: HistoryOpSource;
}
// #endregion HistoryOpOptions
// #region HistoryOpOptionsWithChangeRecords
/**
* 在 HistoryOpOptions 基础上携带 form 端 propPath/value 变更记录,
* 用于历史记录的精细化撤销/重做(按 propPath 局部 patch)。
*/
export interface HistoryOpOptionsWithChangeRecords extends HistoryOpOptions {
changeRecords?: ChangeRecord[];
}
// #endregion HistoryOpOptionsWithChangeRecords
// #region DslOpOptions
/**
* DSL 修改类操作的通用配置
* - doNotSelect: 操作后是否不要自动触发选中(不调用 this.select / this.multiSelect / stage.select / stage.multiSelect)
* - doNotSwitchPage: 操作若会引发当前页面切换(如新增 / 删除 / 跨页移动),是否跳过这次切换
*/
export interface DslOpOptions extends HistoryOpOptions {
doNotSelect?: boolean;
doNotSwitchPage?: boolean;
}
// #endregion DslOpOptions
/** 差异对话框的入参 */
export interface DiffDialogPayload {
/** 表单类别 */
category?: CompareCategory;
/** 节点类型 / 数据源类型 */
type?: string;
/** 代码块场景下的数据源类型 */
dataSourceType?: string;
/** 该 step 修改前的值(oldNode / oldSchema / oldContent) */
lastValue: Record<string, any>;
/** 该 step 修改后的值(newNode / newSchema / newContent) */
value: Record<string, any>;
/** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */
currentValue?: Record<string, any> | null;
/** 用于标题展示的目标名称 */
targetLabel?: string;
/** 用于标题展示的目标 id */
id?: string | number;
/**
* 指定打开时的初始对比模式:
* - before:该步骤修改前 vs 修改后
* - current:该步骤修改后 vs 当前最新值
* 不传时按是否存在「修改后的值」/「当前值」自动推断。
* 若指定的模式当前不可用(对应数据缺失),将回退到自动推断结果。
*/
mode?: 'before' | 'current';
}
/**
* 一组「描述 + 可操作性」的判定函数集合。页面 / 数据源 / 代码块及业务自定义历史
* 各自实现一份,作为整体注入,避免把 describe* / isStep* 拆成多个独立 props 反复透传。
*/
export interface HistoryRowDescriptor<T extends BaseStepValue = BaseStepValue> {
/**
* 组级描述文案生成器,接收一个 group,返回展示文本。
* 不传时回退到对组内最后一步调用 {@link describeStep}(适用于不做相邻合并、每组恒为单步的历史,如数据源/代码块)。
*/
describeGroup?: (_group: any) => string;
/** 单步描述文案生成器,接收一个 step,返回展示文本(合并组展开后的子步列表用)。 */
describeStep: (_step: T) => string;
/** 判断某个 step 是否可查看差异(前后值都存在)。不传则一律不展示差异入口。 */
isStepDiffable?: (_step: T) => boolean;
/** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。不传则已应用即可回滚。 */
isStepRevertable?: (_step: T) => boolean;
}
/**
* 通用 bucket(数据源 / 代码块 / 业务自定义历史)的整体渲染配置。
* 把原先散落在 Bucket / BucketTab 上的 title / prefix / describe* / isStep* / showInitial / gotoEnabled
* 收敛成一个对象作为单一 prop 传递,调用方一次配齐、组件内部按需读取。
*/
export interface HistoryBucketConfig<T extends BaseStepValue = BaseStepValue> extends HistoryRowDescriptor<T> {
/** bucket 头部标题,例如 "数据源" / "代码块"。 */
title: string;
/** 子项 key 的命名空间前缀(\`ds\` 数据源 / \`cb\` 代码块 / 业务自定义如 \`mod\`)。 */
prefix: string;
/** 是否展示底部「回到初始状态」入口,默认 true。无 undo cursor 语义的自定义历史可传 false。 */
showInitial?: boolean;
/** 是否支持「跳转到该记录」(goto),默认 true。 */
gotoEnabled?: boolean;
/** 是否展示顶部「清空」按钮,默认 true。无需提供清空能力的自定义历史可传 false。 */
showClear?: boolean;
}
export interface UseHistoryRevertOptions {
/**
* 父级应用上下文,用于让动态挂载的「差异确认弹窗」继承全局组件 / 指令 / provide / 插件
* (Element Plus、@tmagic/form 字段组件等)。未显式传入时,会自动取调用方所在组件的 appContext
* (\`getCurrentInstance()?.appContext\`)。业务方若在组件 setup 之外调用,需手动传入(如 \`editorApp._context\`)。
*/
appContext?: AppContext | null;
/**
* 透传给差异确认弹窗的 \`extendState\`(即 Editor 的 \`extendFormState\`),
* 使对比表单中依赖业务上下文的 \`display\` / \`disabled\` 等 filterFunction 正常工作。
*/
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 返回 PropsPanel 主属性表单(FormPanel -> MForm)的 formState。
* 仅页面历史「查看差异 / 回滚确认」场景会使用该 formState 覆盖 CompareForm 中同名扩展字段,
* 以保证两处 filterFunction 读取到一致的运行态上下文。
*/
getPropsPanelFormState?: () => FormState | undefined;
/**
* 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 TMagicDialog 的 \`width\`),
* 如 \`'1200px'\` / \`'80%'\`。缺省时使用弹窗内置默认宽度(900px)。
* 业务自有历史(\`viewDiff\` / \`confirmAndRevert\`)可在调用时通过各自入参的 \`width\` 单独覆盖。
*/
dialogWidth?: string;
}
/**
* 业务自有历史(如管理台「模块」)做差异对比时所需的额外渲染入参。
* 内置的页面 / 数据源 / 代码块按 \`category\` 自动取表单配置,无需传这些;
* 业务自有类别可通过 \`loadConfig\` 注入自定义表单配置加载逻辑。
*/
export interface CustomDiffFormOptions {
/**
* 自定义差异表单配置加载逻辑(如「模块」按 c_type 重建表单配置),
* 透传给弹窗内部的 CompareForm;缺省时按 \`category\` 走内置加载。
*/
loadConfig?: CompareFormLoadConfig;
/** 需要走 self diff 的字段类型(如模块的 mod-cond)。 */
selfDiffFieldTypes?: string[];
/**
* 可选:外部提供的 formState(通常来自 PropsPanel 主表单),
* 对比弹窗会用它覆盖 CompareForm 中同名扩展字段,避免上下文不一致。
*/
compareFormState?: FormState;
/**
* 差异 / 确认回滚弹窗宽度(透传给 HistoryDiffDialog 的 TMagicDialog \`width\`),
* 如 \`'1200px'\` / \`'80%'\`。缺省时使用弹窗内置默认宽度(900px)。
*/
width?: string;
/**
* 差异 / 确认回滚弹窗内 form 表单的尺寸(透传给 CompareForm 的 \`size\`),
* 可选 \`'large' | 'default' | 'small'\`,缺省时使用表单内置默认尺寸。
*/
size?: FieldSize;
}
/**
* 业务自有历史复用「单步回滚」交互({@link useHistoryRevert} 的 \`confirmAndRevert\`)的入参。
* 与内置页面 / 数据源 / 代码块回滚共用「目标校验 → 差异/二次确认弹窗 → 反向回滚」流程,
* 业务方只需提供:差异弹窗入参(可选)、表单配置加载(可选)、实际回滚执行函数。
*/
export interface ConfirmAndRevertOptions<T = unknown> extends CustomDiffFormOptions {
/**
* 差异确认弹窗入参;可对比的步骤(单实体 update)传入后弹差异确认弹窗,
* 传 null / 省略则退化为普通二次确认框(add / remove / 不可对比)。
*/
diffPayload?: DiffDialogPayload | null;
/** 回滚前置校验:返回 true 表示目标数据已删除等不可回滚,给出统一提示并中止。 */
isTargetMissing?: () => boolean;
/** 用户确认后执行的实际回滚逻辑。 */
revert: () => T | Promise<T>;
}/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { AppContext, Component } from 'vue';
import type EventEmitter from 'events';
import type * as Monaco from 'monaco-editor';
import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
import type { PascalCasedProperties, Writable } from 'type-fest';
import type {
CodeBlockContent,
CodeBlockDSL,
DataSourceSchema,
Id,
MApp,
MContainer,
MNode,
MPage,
MPageFragment,
} from '@tmagic/core';
import type { FieldSize } from '@tmagic/design';
import type { ChangeRecord, FormConfig, FormState, TableColumnConfig, TypeFunction } from '@tmagic/form';
import type StageCore from '@tmagic/stage';
import type {
CanDropIn,
ContainerHighlightType,
CustomizeMoveableOptions,
GuidesOptions,
RenderType,
UpdateDragEl,
} from '@tmagic/stage';
import type { CodeBlockService } from './services/codeBlock';
import type { ComponentListService } from './services/componentList';
import type { DataSourceService } from './services/dataSource';
import type { DepService } from './services/dep';
import type { EditorService } from './services/editor';
import type { EventsService } from './services/events';
import type { HistoryService } from './services/history';
import type { KeybindingService } from './services/keybinding';
import type { PropsService } from './services/props';
import type { StageOverlayService } from './services/stageOverlay';
import type { StorageService } from './services/storage';
import type { UiService } from './services/ui';
import type { SerializedUndoRedo, UndoRedo } from './utils/undo-redo';
export type EditorSlots = FrameworkSlots &
WorkspaceSlots &
SidebarSlots &
PropsPanelSlots & {
workspace(props: { editorService: EditorService }): any;
'workspace-content'(props: { editorService: EditorService }): any;
};
export interface FrameworkSlots {
header(props: {}): any;
nav(props: {}): any;
'content-before'(props: {}): any;
'content-after'(props: {}): any;
'src-code'(props: {}): any;
sidebar(props: {}): any;
empty(props: {}): any;
workspace(props: {}): any;
'props-panel'(props: {}): any;
footer(props: {}): any;
'page-bar'(props: {}): any;
'page-bar-add-button'(props: {}): any;
'page-bar-title'(props: { page: MPage | MPageFragment }): any;
'page-bar-popover'(props: { page: MPage | MPageFragment }): any;
'page-list-popover'(props: { list: (MPage | MPageFragment)[] }): any;
}
export interface ScrollViewerSlots {
before(props: {}): any;
content(props: {}): any;
default(props: {}): any;
}
export interface StageSlots extends ScrollViewerSlots {
'stage-top'(props: {}): any;
}
export interface WorkspaceSlots extends StageSlots {
stage(props: {}): any;
'workspace-content'(props: {}): any;
}
export interface ComponentListPanelSlots {
'component-list-panel-header'(props: {}): any;
'component-list'(props: { componentGroupList: ComponentGroup[] }): any;
'component-list-item'(props: { component: ComponentItem }): any;
}
export interface CodeBlockListPanelSlots extends CodeBlockListSlots {
'code-block-panel-search'(props: {}): any;
'code-block-panel-header'(props: {}): any;
}
export interface CodeBlockListSlots {
'code-block-panel-tool'(props: { id: Id; data: any }): any;
}
export interface DataSourceListSlots {
'data-source-panel-tool'(props: { data: any }): any;
'data-source-panel-search'(props: {}): any;
}
export interface LayerNodeSlots {
'layer-node-content'(props: { data: MNode }): any;
'layer-node-tool'(props: { data: MNode }): any;
'layer-node-label'(props: { data: MNode }): any;
}
export interface LayerPanelSlots extends LayerNodeSlots {
'layer-panel-header'(props: {}): any;
}
export interface PropsPanelSlots {
'props-panel-header'(props: {}): any;
}
export type SidebarSlots = LayerPanelSlots & CodeBlockListPanelSlots & ComponentListPanelSlots & DataSourceListSlots;
export type BeforeAdd = (config: MNode, parent: MContainer) => Promise<MNode> | MNode;
export type GetConfig = (config: FormConfig) => Promise<FormConfig> | FormConfig;
export interface EditorInstallOptions {
flat?: boolean;
parseDSL: <T = any>(dsl: string) => T;
customCreateMonacoEditor: (
monaco: typeof import('monaco-editor'),
codeEditorEl: HTMLElement,
options: Monaco.editor.IStandaloneEditorConstructionOptions & { editorCustomType?: string },
) => Promise<Monaco.editor.IStandaloneCodeEditor> | Monaco.editor.IStandaloneCodeEditor;
customCreateMonacoDiffEditor: (
monaco: typeof import('monaco-editor'),
codeEditorEl: HTMLElement,
options: Monaco.editor.IStandaloneDiffEditorConstructionOptions & { editorCustomType?: string },
) => Promise<Monaco.editor.IStandaloneDiffEditor> | Monaco.editor.IStandaloneDiffEditor;
[key: string]: any;
}
// #region Services
export interface Services {
editorService: EditorService;
historyService: HistoryService;
storageService: StorageService;
eventsService: EventsService;
propsService: PropsService;
componentListService: ComponentListService;
uiService: UiService;
codeBlockService: CodeBlockService;
depService: DepService;
dataSourceService: DataSourceService;
keybindingService: KeybindingService;
stageOverlayService: StageOverlayService;
}
// #endregion Services
export interface StageOptions {
runtimeUrl?: string;
autoScrollIntoView?: boolean;
containerHighlightClassName?: string;
containerHighlightDuration?: number;
containerHighlightType?: ContainerHighlightType;
disabledDragStart?: boolean;
render?: (stage: StageCore) => HTMLDivElement | void | Promise<HTMLDivElement | void>;
moveableOptions?: CustomizeMoveableOptions;
canSelect?: (el: HTMLElement) => boolean | Promise<boolean>;
isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
/**
* 画布上拖入组件(包括从组件列表拖入新组件、画布上拖动已有组件)时,
* 对已通过 isContainer 命中的候选容器进行二次过滤;返回 false 时阻止该容器被高亮命中
* - 在画布上拖动已有组件时:sourceIds 为被拖动组件的 id 列表
* - 从组件列表拖入新组件时:sourceIds 为空数组(尚无 id,仅可依据 targetId 判断)
* 该选项会被透传给 StageCore 的 canDropIn
*/
canDropIn?: CanDropIn;
updateDragEl?: UpdateDragEl;
renderType?: RenderType;
guidesOptions?: Partial<GuidesOptions>;
disabledMultiSelect?: boolean;
/**
* 始终启用多选模式(无需按住 Ctrl/Meta),默认 false。
* 当 \`disabledMultiSelect\` 为 true 时本配置失效。
*/
alwaysMultiSelect?: boolean;
disabledRule?: boolean;
/**
* 禁用「非点击画布选中组件时(如从图层树、面包屑等外部选中),对选中区域做高亮闪烁提示」,
* 默认 false(即默认开启闪烁)
*/
disabledFlashTip?: boolean;
zoom?: number;
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
}
/**
* 节点校验错误信息,按来源(属性表单 / 样式表单)分别保存错误文案。
* 属性表单与样式表单是两个独立的 FormPanel,均指向同一节点,故以来源为键,
* 避免某个面板校验通过时误清另一个面板记录的错误。
* 节点视为存在错误当且仅当任一来源存在非空文本。
*/
export interface NodeInvalidInfo {
/** 属性表单校验错误文案(可能为包含 <br> 的 HTML) */
props?: string;
/** 样式表单校验错误文案(可能为包含 <br> 的 HTML) */
style?: string;
}
/** 节点校验错误来源 */
export type NodeInvalidSource = keyof NodeInvalidInfo;
export interface StoreState {
root: MApp | null;
page: MPage | MPageFragment | null;
parent: MContainer | null;
node: MNode | null;
highlightNode: MNode | null;
nodes: MNode[];
stage: StageCore | null;
stageLoading: boolean;
modifiedNodeIds: Map<Id, Id>;
/** 校验失败的节点错误信息,按节点 id 存储,供组件树标记与保存拦截读取 */
invalidNodeIds: Map<Id, NodeInvalidInfo>;
pageLength: number;
pageFragmentLength: number;
disabledMultiSelect: boolean;
/** 是否始终启用多选模式(无需按住 Ctrl/Meta) */
alwaysMultiSelect: boolean;
}
export type StoreStateKey = keyof StoreState;
export interface PropsState {
propsConfigMap: Record<string, FormConfig>;
propsValueMap: Record<string, Partial<MNode>>;
relateIdMap: Record<Id, Id>;
/** 禁用数据源 */
disabledDataSource: boolean;
/** 禁用代码块 */
disabledCodeBlock: boolean;
}
export interface StageOverlayState {
wrapDiv: HTMLDivElement;
sourceEl: HTMLElement | null;
contentEl: HTMLElement | null;
stage: StageCore | null;
stageOptions: StageOptions | null;
wrapWidth: number;
wrapHeight: number;
stageOverlayVisible: boolean;
}
export interface ComponentGroupState {
list: ComponentGroup[];
}
// #region ColumnLayout
export enum ColumnLayout {
LEFT = 'left',
CENTER = 'center',
RIGHT = 'right',
}
// #endregion ColumnLayout
export interface SetColumnWidth {
[ColumnLayout.LEFT]?: number;
[ColumnLayout.CENTER]?: number | 'auto';
[ColumnLayout.RIGHT]?: number;
}
export interface GetColumnWidth {
[ColumnLayout.LEFT]: number;
[ColumnLayout.CENTER]: number;
[ColumnLayout.RIGHT]: number;
}
export interface StageRect {
width: number | string;
height: number | string;
}
export interface UiState {
/** 当前点击画布是否触发选中,true: 不触发,false: 触发,默认为false */
uiSelectMode: boolean;
/** 是否显示整个配置源码, true: 显示, false: 不显示,默认为false */
showSrc: boolean;
/** 是否将样式配置单独一列显示, true: 显示, false: 不显示,默认为true */
showStylePanel: boolean;
/** 画布显示放大倍数,默认为 1 */
zoom: number;
/** 画布容器的宽高 */
stageContainerRect: {
width: number;
height: number;
};
/** 画布顶层div的宽高,可用于改变画布的大小 */
stageRect: StageRect;
/** 编辑器列布局每一列的宽度,分为左中右三列 */
columnWidth: GetColumnWidth;
/** 编辑器列布局左侧列最小宽度 */
minLeftColumnWidth: number;
/** 编辑器列布局中间列最小宽度 */
minCenterColumnWidth: number;
/** 编辑器列布局右侧列最小宽度 */
minRightColumnWidth: number;
/** 是否显示画布参考线,true: 显示,false: 不显示,默认为true */
showGuides: boolean;
/** 画布上是否存在参考线 */
hasGuides: boolean;
/** 是否显示标尺,true: 显示,false: 不显示,默认为true */
showRule: boolean;
/** 用于控制该属性配置表单内组件的尺寸 */
propsPanelSize: 'large' | 'default' | 'small';
/** 是否显示新增页面按钮 */
showAddPageButton: boolean;
/** 是否在页面工具栏显示呼起页面列表按钮 */
showPageListButton: boolean;
/** 是否隐藏侧边栏 */
hideSlideBar: boolean;
/** 侧边栏面板配置 */
sideBarItems: SideComponent[];
/** 当前激活的侧边栏面板 */
sideBarActiveTabName: string;
// navMenu 的宽高
navMenuRect: {
left: number;
top: number;
width: number;
height: number;
};
frameworkRect: {
left: number;
top: number;
width: number;
height: number;
};
}
// #region EditorNodeInfo
export interface EditorNodeInfo {
node: MNode | null;
parent: MContainer | null;
page: MPage | MPageFragment | null;
path: MNode[];
}
// #endregion EditorNodeInfo
export interface AddMNode {
type: string;
name?: string;
inputEvent?: DragEvent;
[key: string]: any;
}
// #region PastePosition
export interface PastePosition {
left?: number;
top?: number;
/**
* 粘贴位置X方向偏移量
*/
offsetX?: number;
/**
* 粘贴位置Y方向偏移量
*/
offsetY?: number;
}
// #endregion PastePosition
// #region MenuButton
/**
* 菜单按钮
*/
export interface MenuButton {
/**
* 按钮类型
* button: 只有文字不带边框的按钮
* text: 纯文本
* divider: 分割线
* dropdown: 下拉菜单
*/
type: 'button' | 'text' | 'divider' | 'dropdown';
/** 当type为divider时有效,分割线方向, 默认vertical */
direction?: 'horizontal' | 'vertical';
/** 展示的文案 */
text?: string;
/** 鼠标悬浮是显示的气泡中的文案 */
tooltip?: string;
/** Vue组件或url */
icon?: string | Component<{}, {}, any>;
/** 是否置灰,默认为false */
disabled?: boolean | ((data: Services) => boolean);
/** 是否显示,默认为true */
display?: boolean | ((data: Services) => boolean);
/** type为button/dropdown时点击运行的方法 */
handler?: (data: Services, event: MouseEvent) => Promise<any> | any;
className?: string;
/** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */
items?: MenuButton[];
/** 唯一标识,用于高亮 */
id?: string | number;
buttonProps?: {
type?: string;
};
}
// #endregion MenuButton
// #region MenuComponent
export interface MenuComponent {
type: 'component';
/** Vue3组件 */
component: any;
/** 传入组件的props对象 */
props?: Record<string, any>;
/** 组件监听的事件对象,如:{ click: () => { console.log('click'); } } */
listeners?: Record<string, Function>;
slots?: Record<string, any>;
/** 是否显示,默认为true */
className?: string;
display?: boolean | ((data: Services) => Promise<boolean> | boolean);
[key: string]: any;
}
// #endregion MenuComponent
/**
* '/': 分隔符
* 'delete': 删除按钮
* 'undo': 撤销按钮
* 'redo': 恢复按钮
* 'zoom': 'zoom-in', 'zoom-out', 'scale-to-original', 'scale-to-fit' 的集合
* 'zoom-in': 放大按钮
* 'zoom-out': 缩小按钮
* 'guides': 显示隐藏参考线
* 'rule': 显示隐藏标尺
* 'scale-to-original': 缩放到实际大小
* 'scale-to-fit': 缩放以适应
* 'history-list': 历史记录面板(按 页面 / 数据源 / 代码块 三个 tab 展示,相邻同目标修改自动合并)
*/
// #region MenuItem
export type MenuItem =
| '/'
| 'delete'
| 'undo'
| 'redo'
| 'zoom'
| 'zoom-in'
| 'zoom-out'
| 'guides'
| 'rule'
| 'scale-to-original'
| 'scale-to-fit'
| 'history-list'
| MenuButton
| MenuComponent
| string;
// #endregion MenuItem
// #region MenuBarData
/** 工具栏 */
export interface MenuBarData {
/** 顶部工具栏左边项 */
[ColumnLayout.LEFT]?: MenuItem[];
/** 顶部工具栏中间项 */
[ColumnLayout.CENTER]?: MenuItem[];
/** 顶部工具栏右边项 */
[ColumnLayout.RIGHT]?: MenuItem[];
}
// #endregion MenuBarData
// #region SideComponent
export interface SideComponent extends MenuComponent {
/** 显示文案 */
text: string;
/** tab样式 */
tabStyle?: string | Record<string, any>;
/** vue组件或url */
icon?: any;
/** slide 唯一标识 key */
$key: string;
/** 是否可以将面板拖出,默认为true */
draggable?: boolean;
/** 点击切换tab前调用,返回false阻止切换 */
beforeClick?: (config: SideComponent) => boolean | Promise<boolean>;
/** 组件扩展参数 */
boxComponentConfig?: {
/** Vue3组件 */
component?: any;
/** 传入组件的props对象 */
props?: Record<string, any>;
};
}
// #endregion SideComponent
// #region HistoryListExtraTab
/**
* 历史记录面板(HistoryListPanel)的自定义扩展 tab。
*
* 业务方可通过 Editor 的 \`historyListExtraTabs\` 注入额外的历史记录 tab,
* 例如某个自定义模块维护自己的操作历史时,可以在历史记录面板中增加一个
* 独立的 tab 来展示与回滚。内置的「页面 / 数据源 / 代码块」三个 tab 之后
* 会依次追加这些扩展 tab。
*/
export interface HistoryListExtraTab {
/** tab 唯一标识,作为 TMagicTabs 的 name */
name: string;
/** tab 显示文案,支持传入函数以展示动态内容(如记录数量) */
label: string | (() => string);
/** tab 内容区渲染的组件(Vue 组件或字符串标签) */
component: any;
/** 传入内容组件的 props */
props?: Record<string, any>;
/** 内容组件的事件监听 */
listeners?: Record<string, (..._args: any[]) => any>;
}
// #endregion HistoryListExtraTab
// #region CompareForm
/**
* 对比表单(CompareForm)的对比类型:
* - node: 节点组件,按 \`type\` 从 propsService 获取属性表单配置
* - data-source: 数据源,按 \`type\`(base/http/...) 从 dataSourceService 获取数据源表单配置
* - code-block: 数据源代码块,使用内置的代码块表单配置
*/
export type CompareCategory = 'node' | 'data-source' | 'code-block' | string;
/**
* 自定义 \`loadConfig\` 时回传的上下文,聚合了组件当前的对比入参,
* 方便调用方在外部按需拼装 FormConfig。
*/
export interface CompareFormLoadConfigContext {
/** 对比类型,见 CompareCategory */
category: string;
/** 节点 / 数据源类型 */
type?: string;
/** 数据源代码块场景下的数据源类型 */
dataSourceType?: string;
/**
* 内置的默认 FormConfig 加载逻辑(按 \`category\` 从 propsService / dataSourceService /
* 代码块工具取配置)。自定义 \`loadConfig\` 可调用它复用默认结果,再做二次加工。
*/
defaultLoadConfig: () => Promise<FormConfig>;
}
/**
* 自定义 FormConfig 加载逻辑。传入后将接管内置的按 \`category\` 取配置逻辑,
* 可通过 \`ctx.defaultLoadConfig()\` 复用默认结果再做二次加工。
*/
export type CompareFormLoadConfig = (ctx: CompareFormLoadConfigContext) => FormConfig | Promise<FormConfig>;
/**
* CompareForm / ViewForm 共用的基础 props。
* 两者都基于同一套「按 category 加载 FormConfig + 注入 services/stage」的逻辑(见 useCompareForm),
* 差异仅在于是否做新旧值对比。这里抽出公共字段避免重复定义。
*/
export interface CompareFormBaseProps {
/** 当前值(对比场景下为修改后的值) */
value: Partial<MNode> | Partial<DataSourceSchema> | Partial<CodeBlockContent> | Record<string, any>;
/**
* 类型说明:
* - \`category\` 为 \`node\` 时,\`type\` 为节点组件的类型,例如 'text'、'button'、'page'、'container' 等
* - \`category\` 为 \`data-source\` 时,\`type\` 为数据源类型,例如 'base'、'http'
* - \`category\` 为 \`code-block\` 时,\`type\` 可不传
*/
type?: string;
/** 表单配置类别,决定从哪里取 FormConfig */
category?: CompareCategory;
/** 数据源代码块场景下的数据源类型(base/http),用于代码块表单中"执行时机"展示 */
dataSourceType?: string;
labelWidth?: string;
/**
* 外层容器高度。设置后表单内容超出时会在组件内部出现滚动条,
* 避免 dialog / 面板使用方需要自行处理滚动。可传任意 CSS 长度,例如 \`60vh\` / \`400px\` / \`100%\`。
*/
height?: string;
/**
* 用户自定义注入到 MForm.formState 的扩展字段,与 Editor 顶层的 \`extendFormState\`、
* PropsPanel 的 \`extend-state\` 语义一致。表单 item 的 \`display\` / \`disabled\` 等
* filterFunction 经常依赖这里注入的字段(如 stage、自定义业务上下文等),
* 因此在对比 / 展示场景下也需要透传,避免出现 \`formState.xxx is undefined\` 的运行时错误。
*/
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 外部透传的基础 formState(通常来自 PropsPanel 主属性表单)。
* 组件会提取其中的扩展字段覆盖到自己的 formState,保证 filterFunction 上下文一致。
*/
baseFormState?: FormState;
/**
* 表单内组件的尺寸(透传给 MForm 的 \`size\`),可选 'large' | 'default' | 'small'。
* 缺省时使用 MForm 内置默认尺寸。
*/
size?: FieldSize;
/**
* 自定义 FormConfig 加载逻辑。传入后将接管内置的按 \`category\`(node/data-source/code-block)
* 取配置逻辑,调用方可根据业务自行返回(或异步返回)表单配置。可通过
* \`ctx.defaultLoadConfig()\` 复用默认结果再做二次加工。
*/
loadConfig?: CompareFormLoadConfig;
/** 编辑器服务集合,由调用方传入(不再通过 inject('services') 获取)。 */
services?: Services;
}
// #endregion CompareForm
// #region SideItemKey
export enum SideItemKey {
COMPONENT_LIST = 'component-list',
LAYER = 'layer',
CODE_BLOCK = 'code-block',
DATA_SOURCE = 'data-source',
}
// #endregion SideItemKey
// #region SideItem
/**
* component-list: 组件列表
* layer: 已选组件树
* code-block: 代码块
*/
export type SideItem = \`\${SideItemKey}\` | SideComponent;
// #endregion SideItem
// #region SideBarData
/** 工具栏 */
export interface SideBarData {
/** 容器类型 */
type: 'tabs';
/** 默认激活的内容 */
status: string;
/** panel列表 */
items: SideItem[];
}
// #endregion SideBarData
// #region ComponentItem
export interface ComponentItem {
/** 显示文案 */
text: string;
/** 详情,用于tooltip */
desc?: string;
/** 组件类型 */
type: string;
/** Vue组件或url */
icon?: string | Component<{}, {}, any>;
/** 新增组件时需要透传到组价节点上的数据 */
data?: {
[key: string]: any;
};
}
// #endregion ComponentItem
// #region ComponentGroup
export interface ComponentGroup {
/** 显示文案 */
title: string;
/** 组内列表 */
items: ComponentItem[];
}
// #endregion ComponentGroup
export enum LayerOffset {
TOP = 'top',
BOTTOM = 'bottom',
}
// #region Layout
/** 容器布局 */
export enum Layout {
FLEX = 'flex',
FIXED = 'fixed',
RELATIVE = 'relative',
ABSOLUTE = 'absolute',
}
// #endregion Layout
export enum Keys {
ESCAPE = 'Space',
}
export interface ScrollViewerEvent {
scrollLeft: number;
scrollTop: number;
scrollHeight: number;
scrollWidth: number;
}
export type CodeState = {
/** 代码块DSL数据源 */
codeDsl: CodeBlockDSL | null;
/** 代码块是否可编辑 */
editable: boolean;
/** list模式下左侧展示的代码列表 */
combineIds: string[];
/** 为业务逻辑预留的不可删除的代码块列表,由业务逻辑维护(如代码块上线后不可删除) */
undeletableList: Id[];
paramsColConfig?: TableColumnConfig;
};
export type CodeRelation = {
/** 组件id:[代码id1,代码id2] */
[compId: Id]: Id[];
};
export interface CodeDslItem {
/** 代码块id */
id: Id;
/** 代码块名称 */
name: string;
/** 代码块函数内容 */
codeBlockContent?: CodeBlockContent;
/** 是否展示代码绑定关系 */
showRelation?: boolean;
/** 代码块对应绑定的组件信息 */
combineInfo?: CombineInfo[];
}
export interface CombineInfo {
/** 组件id */
compId: Id;
/** 组件名称 */
compName: string;
}
export interface ListState {
/** 代码块列表 */
codeList: CodeDslItem[];
}
export enum CodeDeleteErrorType {
/** 代码块存在于不可删除列表中 */
UNDELETEABLE = 'undeleteable',
/** 代码块存在绑定关系 */
BIND = 'bind',
}
// 代码块草稿localStorage key
export const CODE_DRAFT_STORAGE_KEY = 'magicCodeDraft';
export interface CodeParamStatement {
/** 参数名称 */
name: string;
/** 参数类型 */
type?: string | TypeFunction<string>;
[key: string]: any;
}
// #region HistoryOpType
/**
* 历史记录操作类型:
* - \`add\` / \`remove\` / \`update\`:普通可撤销/重做的节点变更;
* - \`initial\`:页面「未修改的初始状态」基线(设置 root 时生成),作为页面栈 index 0 的固定底线 step。
* 该 step 不可被撤销/回滚(cursor 不会低于它),仅用于历史面板底部的初始行展示。
*/
export type HistoryOpType = 'add' | 'remove' | 'update' | 'initial';
// #endregion HistoryOpType
// #region HistoryOpSource
/**
* 历史记录的「操作途径」——标记本次变更由哪条交互入口触发,仅用于历史面板展示 / 业务埋点,
* 不影响 undo/redo 行为。缺省(未传)时 UI 视为「未知」。
*
* - \`stage\`:画布(拖拽 / 缩放 / 排序等舞台直接操作)
* - \`tree\`:树形面板(图层 / 数据源 / 代码块等树形结构里的拖拽 / 菜单操作)
* - \`component-panel\`:组件面板(左侧组件列表点击 / 拖拽新增组件)
* - \`props\`:配置面板表单(属性表单字段编辑)
* - \`code\`:源码编辑器(配置面板「源码」面板里直接编辑 JSON/代码后保存)
* - \`stage-contextmenu\`:画布右键菜单(舞台上节点的右键上下文菜单)
* - \`tree-contextmenu\`:树面板右键菜单(图层 / 数据源 / 代码块等树形列表上的右键上下文菜单)
* - \`toolbar\`:工具栏菜单(顶部导航工具栏按钮)
* - \`shortcut\`:键盘快捷键
* - \`rollback\`:历史回滚(历史面板里对某条历史「回滚」,反向应用为一条新记录,类 git revert)
* - \`api\`:代码 / 接口调用(程序化触发)
* - \`ai\`:AI 生成 / 智能助手触发的变更
* - \`unknown\`:未知来源
*
* 通过 \`(string & {})\` 允许业务侧扩展自定义途径字符串,同时保留内置值的自动补全。
*/
export type HistoryOpSource =
| 'initial'
| 'stage'
| 'tree'
| 'component-panel'
| 'props'
| 'code'
| 'root-code'
| 'stage-contextmenu'
| 'tree-contextmenu'
| 'toolbar'
| 'shortcut'
| 'rollback'
| 'api'
| 'ai'
// 同步
| 'sync'
| 'unknown'
| (string & {});
// #endregion HistoryOpSource
// #region DslOpWithHistoryIdsResult
/** *AndGetHistoryId 系列方法返回值:原操作结果 + 本次写入历史记录的 uuid 列表(未入栈时为 \`[]\`)。 */
export type DslOpWithHistoryIdsResult<T> = {
result: T;
historyIds: string[];
};
// #endregion DslOpWithHistoryIdsResult
// #region StepDiffItem
/**
* 单条变更的 diff 描述,统一表达「页面节点 / 代码块 / 数据源」的变化内容,
* 被 {@link StepValue} / {@link CodeBlockStepValue} / {@link DataSourceStepValue} 的 \`diff\` 复用。
*
* 按 \`opType\` 区分携带的字段:
* - \`add\`:仅 \`newSchema\`(页面节点还带 \`parentId\` / \`index\`);
* - \`remove\`:仅 \`oldSchema\`(页面节点还带 \`parentId\` / \`index\`);
* - \`update\`:\`oldSchema\` + \`newSchema\`,并可带 \`changeRecords\` 做局部更新。
*
* 泛型 \`T\` 为变化内容的快照类型:页面节点为 \`MNode\`,代码块为 \`CodeBlockContent\`,数据源为 \`DataSourceSchema\`。
*/
export interface StepDiffItem<T = unknown> {
/** 变更后的内容快照。\`opType\` 为 \`add\` / \`update\` 时有,\`remove\` 时无。 */
newSchema?: T;
/** 变更前的内容快照。\`opType\` 为 \`remove\` / \`update\` 时有,\`add\` 时无。 */
oldSchema?: T;
/** 父节点 id。仅页面节点有(数据源 / 代码块没有父节点)。 */
parentId?: Id;
/** 在父节点 items 数组中的索引。仅页面节点有(数据源 / 代码块无需排序)。 */
index?: number;
/**
* form 端 propPath/value 变更列表,仅 \`opType\` 为 \`update\` 时有;
* 撤销/重做时若有则按 propPath 局部更新,缺省才退化为整内容替换。
*/
changeRecords?: ChangeRecord[];
}
// #endregion StepDiffItem
// #region BaseStepValue
/**
* 历史记录条目公共字段,被 {@link StepValue} / {@link CodeBlockStepValue} / {@link DataSourceStepValue} 复用。
*
* 泛型 \`T\` 为 \`diff\` 中变化内容的快照类型(页面节点 \`MNode\` / 代码块 \`CodeBlockContent\` / 数据源 \`DataSourceSchema\`)。
*/
export interface BaseStepValue<T = unknown, U extends Record<string, any> = {}> {
/**
* 历史记录唯一标识(uuid)。入栈时自动写入(若调用方未指定),
* 用于精确定位 / 引用某一条历史记录(如 revert、埋点、跨端同步等)。
* 注意与 \`data.id\`(关联的页面 / 代码块 / 数据源 id)区分。
*/
uuid: string;
/**
* 关联目标信息:\`id\` 为关联的页面 / 代码块 / 数据源等资源 id(也是历史栈的分组 key),
* \`name\` 为展示名。所有历史类型统一携带。
*/
data: { name: string; id: Id };
/** 操作类型:新增 / 删除 / 更新(三类历史记录统一携带)。 */
opType: HistoryOpType;
/**
* 本次变更的内容(统一 diff 表达),每项见 {@link StepDiffItem}。
* 页面节点(add/remove 多节点、update 多节点)会有多项,代码块 / 数据源通常只有一项。
*/
diff: StepDiffItem<T>[];
/**
* 调用方可选传入的人类可读描述(如「调整按钮颜色」),用于历史面板展示。
* 不影响 undo/redo 行为;缺省时面板会根据节点 / propPath 自动生成描述。
*/
historyDescription?: string;
/**
* 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource}
* (画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等)。
* 仅用于历史面板展示与业务埋点,不影响 undo/redo 行为;缺省时面板视为「未知」。
*/
source?: HistoryOpSource;
/**
* 入栈时间戳(毫秒)。入栈时自动写入(若调用方未指定),仅用于历史面板展示。
*/
timestamp?: number;
/**
* 是否为「已保存」记录:DSL 落库(如保存到后端 / 本地)时由 historyService.markSaved 标记。
* 同一栈内任意时刻最多只有一条记录为 true;从 IndexedDB 恢复时游标会被定位到最近一条已保存记录之后。
*/
saved?: boolean;
/**
* 是否为「整体设置 root」(set root)产生的记录(由 {@link Editor.pushRootDiffHistory} 写入)。
* 用于「连续 set root 合并」:当某页栈最新一条已是 root 记录时,下一条 set root 会替换它而非新增,
* 避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录。
*/
rootStep?: boolean;
/** 操作人 */
operator?: string;
/** 扩展信息 */
extra?: U;
}
// #endregion BaseStepValue
// #region StepExtra
/**
* 历史记录的扩展上下文({@link BaseStepValue.extra})。
* 内置字段供 \`page\` 类型在撤销 / 重做时恢复选区与受影响节点;扩展类型可自由附加其它键。
*/
export interface StepExtra {
/** 操作前选中的节点 ID,用于撤销后恢复选择状态(page 类型) */
selectedBefore?: Id[];
/** 操作后选中的节点 ID,用于重做后恢复选择状态(page 类型) */
selectedAfter?: Id[];
/** 本次操作涉及的节点 id 集合(page 类型) */
modifiedNodeIds?: Map<Id, Id>;
/** 操作前的节点校验错误快照,撤销后还原(使撤销一个「校验失败」的改动后错误消失) */
invalidNodeIdsBefore?: Map<Id, NodeInvalidInfo>;
/** 操作后的节点校验错误快照,重做后还原(使重做后错误恢复) */
invalidNodeIdsAfter?: Map<Id, NodeInvalidInfo>;
[key: string]: any;
}
// #endregion StepExtra
// #region StepValue
/**
* 页面节点历史记录条目(\`diff\` 内容为 {@link MNode})。结构已与代码块 / 数据源统一收敛到
* {@link BaseStepValue}:关联 id 见 \`data.id\`,选区等上下文见 \`extra\`。
*/
export type StepValue = BaseStepValue<MNode, StepExtra>;
// #endregion StepValue
// #region CodeBlockStepValue
/**
* 代码块历史记录条目(\`diff\` 内容为 {@link CodeBlockContent}),按 \`data.id\`(codeBlock.id)
* 分组保存到 historyState.steps.codeBlock。结构与 {@link StepValue} / {@link DataSourceStepValue}
* 一致,仅 \`diff\` 快照类型不同。
*/
export type CodeBlockStepValue = BaseStepValue<CodeBlockContent>;
// #endregion CodeBlockStepValue
// #region DataSourceStepValue
/**
* 数据源历史记录条目(\`diff\` 内容为 {@link DataSourceSchema}),按 \`data.id\`(dataSource.id)
* 分组保存到 historyState.steps.dataSource。结构与 {@link StepValue} / {@link CodeBlockStepValue}
* 一致,仅 \`diff\` 快照类型不同。
*/
export type DataSourceStepValue = BaseStepValue<DataSourceSchema>;
// #endregion DataSourceStepValue
// #region HistorySteps
/**
* 历史记录类型标识:内置 \`page\` / \`codeBlock\` / \`dataSource\`,并允许业务扩展自定义类型。
* \`(string & {})\` 保留对内置字面量的智能提示,同时不限制扩展取值。
*/
export type HistoryStepType = 'page' | 'codeBlock' | 'dataSource' | (string & {});
/**
* 全部历史栈的统一容器,按「类型 -> id -> UndoRedo 栈」两级分组。
*
* - \`page\`:页面历史栈,按 page.id 分组(每页一份 UndoRedo);
* - \`codeBlock\`:代码块历史栈,按 codeBlock.id 分组;
* - \`dataSource\`:数据源历史栈,按 dataSource.id 分组;
* - 其余键:业务通过 {@link HistoryService.registerStepType} 注册的自定义历史类型。
*
* 所有类型(含扩展类型)一视同仁:均按 id 独立分栈、独立 undo/redo,且都可通过
* {@link HistoryService.setMarker} 在 index 0 种入 \`initial\` 基线(撤销 / 回滚不会越过该基线)。
*/
export interface HistorySteps {
page: Record<Id, UndoRedo<StepValue>>;
codeBlock: Record<Id, UndoRedo<CodeBlockStepValue>>;
dataSource: Record<Id, UndoRedo<DataSourceStepValue>>;
/** 扩展历史类型:按 id 分组的 UndoRedo 栈。 */
[stepType: string]: Record<Id, UndoRedo<any>>;
}
// #endregion HistorySteps
export interface HistoryState {
/**
* 全部历史栈的统一容器(页面 / 代码块 / 数据源 / 扩展类型),见 {@link HistorySteps}。
* 各类型互不影响,支持按 id 独立 undo/redo;是否可撤销 / 重做改用 {@link HistoryService.canUndo} /
* {@link HistoryService.canRedo}(按 stepType + id 查询)替代旧的全局 canUndo / canRedo 字段。
*/
steps: HistorySteps;
/**
* 各历史类型的展示名称,用于历史面板({@link HistorySteps} 的 tab / 分组标题等)。
* 内置 \`page\` / \`codeBlock\` / \`dataSource\` 有默认中文名(页面 / 代码块 / 数据源),
* 扩展类型可通过 {@link HistoryService.registerStepType} 的 \`name\` 选项或
* {@link HistoryService.setStepName} 登记;读取请用 {@link HistoryService.getStepName}。
*/
stepNames: Record<string, string>;
}
// #region PersistedHistoryState
/**
* 历史记录的可持久化快照。由 historyService.saveToIndexedDB 写入 IndexedDB,
* 再由 historyService.restoreFromIndexedDB 读出并重建各 UndoRedo 栈。
*/
export interface PersistedHistoryState {
/** 快照结构版本号,便于后续兼容升级。 */
version: number;
/**
* 全部历史栈的序列化快照,按「类型 -> id」两级分组,与 {@link HistorySteps} 对应。
* 内置 \`page\` / \`codeBlock\` / \`dataSource\`,并包含业务注册的扩展类型。
*/
steps: {
page: Record<Id, SerializedUndoRedo<StepValue>>;
codeBlock: Record<Id, SerializedUndoRedo<CodeBlockStepValue>>;
dataSource: Record<Id, SerializedUndoRedo<DataSourceStepValue>>;
[stepType: string]: Record<Id, SerializedUndoRedo<any>>;
};
/** 保存时间戳(毫秒)。 */
savedAt: number;
}
// #endregion PersistedHistoryState
// #region HistoryPersistOptions
/** historyService 持久化相关 API 的可选配置。 */
export interface HistoryPersistOptions {
/** IndexedDB 数据库名,默认 \`tmagic-editor\`(最终库名会拼上当前 DSL app id)。 */
dbName?: string;
/** objectStore 名,默认 \`history\`。 */
storeName?: string;
/** 记录 key,用于区分不同活动页 / 项目,默认 \`default\`。 */
key?: IDBValidKey;
/**
* 显式指定用于库名隔离的 DSL app id。
* 缺省时回退到当前 editorService 的 \`root.id\`;在「先恢复历史再 set root」场景下 root 尚未设置,
* 需由调用方(如从待加载 DSL 取 id)显式传入,否则会读 / 写到未按 app 隔离的默认库。
*/
appId?: Id;
}
// #endregion HistoryPersistOptions
// #region HistoryListEntry
/**
* 历史面板用:当前页面的一条历史步骤(包含位置和是否已应用)。
*/
export interface HistoryStepEntry<T> {
/** 步骤内容 */
step: T;
/** 在所属栈中的索引(0 为最早) */
index: number;
/** 是否处于"已应用"段(即位于栈游标之前)。撤销后变为 false。 */
applied: boolean;
/** 是否为当前所在的步骤(栈中最近一次已应用的那一步,即 index === cursor - 1)。 */
isCurrent?: boolean;
}
/**
* 历史面板分组(页面 / 数据源 / 代码块 / 扩展类型统一结构)。
*
* 把指定历史栈的步骤列表按"目标"做相邻合并:
* - 连续修改同一目标(单实体 update,targetId 一致)的多步合并成一组,组内可展开查看每步;
* - add / remove / 多实体 update 始终独立成组(无法明确归属单一目标);
* - targetId 为 undefined 表示"无明确目标",不参与合并。
*
* 各类型仅 \`kind\` 与 step 快照类型不同,统一由泛型描述:
* - 页面:\`HistoryGroup<StepValue>\`,\`kind: 'page'\`,\`id\` 为 pageId,\`targetId\` 为被改节点 id;
* - 数据源:\`HistoryGroup<DataSourceStepValue>\`,\`kind: 'data-source'\`,\`id\` 为 dataSource.id;
* - 代码块:\`HistoryGroup<CodeBlockStepValue>\`,\`kind: 'code-block'\`,\`id\` 为 codeBlock.id。
*/
export interface HistoryGroup<T extends BaseStepValue = BaseStepValue> {
/** 历史类型标识:page / code-block / data-source(扩展类型同理)。 */
kind: string;
/** 所属栈 id(page 为 pageId,代码块 / 数据源为对应资源 id)。 */
id: Id;
/** 该分组的操作类型。 */
opType: HistoryOpType;
/**
* 合并的目标 id:仅"单实体 update"有值,并按此与相邻同 id 的 update 合并。
* undefined 表示该分组不可被合并(add / remove / 多实体 update)。
*/
targetId?: Id;
/** 目标可读名(取最后一步快照的 name/type/id)。 */
targetName?: string;
/** 组内所有步骤,按时间正序。 */
steps: { step: T; index: number; applied: boolean; isCurrent?: boolean }[];
/** 组内最后一步是否已应用。 */
applied: boolean;
/** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组)。 */
isCurrent?: boolean;
}
// #endregion HistoryListEntry
export enum KeyBindingCommand {
/** 复制 */
COPY_NODE = 'tmagic-system-copy-node',
/** 粘贴 */
PASTE_NODE = 'tmagic-system-paste-node',
/** 删除 */
DELETE_NODE = 'tmagic-system-delete-node',
/** 剪切 */
CUT_NODE = 'tmagic-system-cut-node',
/** 撤销 */
UNDO = 'tmagic-system-undo',
/** 重做 */
REDO = 'tmagic-system-redo',
/** 放大 */
ZOOM_IN = 'tmagic-system-zoom-in',
/** 缩小 */
ZOOM_OUT = 'tmagic-system-zoom-out',
/** 缩放到实际大小 */
ZOOM_RESET = 'tmagic-system-zoom-reset',
/** 缩放以适应 */
ZOOM_FIT = 'tmagic-system-zoom-fit',
/** 向上移动1px */
MOVE_UP_1 = 'tmagic-system-move-up-1',
/** 向下移动1px */
MOVE_DOWN_1 = 'tmagic-system-move-down-1',
/** 向左移动1px */
MOVE_LEFT_1 = 'tmagic-system-move-left-1',
/** 向右移动1px */
MOVE_RIGHT_1 = 'tmagic-system-move-right-1',
/** 向上移动10px */
MOVE_UP_10 = 'tmagic-system-move-up-10',
/** 向下移动10px */
MOVE_DOWN_10 = 'tmagic-system-move-down-10',
/** 向左移动10px */
MOVE_LEFT_10 = 'tmagic-system-move-left-10',
/** 向右移动10px */
MOVE_RIGHT_10 = 'tmagic-system-move-right-10',
/** 切换组件 */
SWITCH_NODE = 'tmagic-system-switch-node',
}
export interface KeyBindingItem {
command: KeyBindingCommand | string;
keybinding?: string | string[];
when: [string, 'keyup' | 'keydown'][];
}
export interface KeyBindingCacheItem {
type: string;
command: KeyBindingCommand | string;
keybinding?: string | string[];
eventType: 'keyup' | 'keydown';
bound: boolean;
}
// #region DatasourceTypeOption
/** 可新增的数据源类型选项 */
export interface DatasourceTypeOption {
/** 数据源类型 */
type: string;
/** 数据源名称 */
text: string;
}
// #endregion DatasourceTypeOption
/** 组件树节点状态 */
export interface LayerNodeStatus {
/** 显隐 */
visible: boolean;
/** 展开子节点 */
expand: boolean;
/** 选中 */
selected: boolean;
/** 是否可拖拽 */
draggable: boolean;
}
/** 拖拽类型 */
export enum DragType {
/** 从组件列表拖到画布 */
COMPONENT_LIST = 'component-list',
/** 拖动组件树节点 */
LAYER_TREE = 'layer-tree',
}
// #region TreeNodeData
export interface TreeNodeData {
id: Id;
name?: string;
items?: TreeNodeData[];
[key: string]: any;
}
// #endregion TreeNodeData
/** 判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
export type IsExpandableFunction = (_data: TreeNodeData, _nodeStatusMap: Map<Id, LayerNodeStatus>) => boolean;
/** canDropIn 的调用场景 */
export type CanDropInScene =
/** 在"已选组件"面板的组件树中拖动节点 */
| 'layer'
/** 在画布上拖动已有组件(被拖动组件本身已经存在于画布中,sourceIds 包含其 id) */
| 'stage-drag'
/** 从组件列表拖入新组件到画布(被拖入的组件尚不存在,sourceIds 为空数组) */
| 'stage-add';
/**
* 判断当前正在拖动的源节点是否可以拖入目标节点内部的函数
* @param _sourceIds 当前正在拖动的源节点 id 列表
* - \`layer\`:被拖动的组件树节点 id(单选时长度为 1)
* - \`stage-drag\`:被拖动组件的 id 列表(多选拖动时为多个)
* - \`stage-add\`:始终为空数组(从组件列表新增的组件尚无 id)
* @param _targetId 目标容器的节点 id
* @param _scene 调用场景:见 {@link CanDropInScene}
* @returns
* - \`false\`:阻止该容器被视为合法拖入目标
* - \`layer\`:禁用 inner 高亮(before/after 仍然可用)
* - \`stage-drag\`:阻止该容器被高亮命中
* - \`stage-add\`:阻止该容器被高亮命中并退化为放入当前页面
* - \`Id\`(string | number):将拖入目标重定向到该 id 对应的节点
* (例如把命中的"卡片外壳"节点重定向到其内层"卡片内容"容器节点)
* - 其他(\`true\` / \`void\` / \`undefined\`):按原 targetId 正常拖入
*/
export type CanDropInFunction = (_sourceIds: Id[], _targetId: Id, _scene: CanDropInScene) => Id | boolean | void;
export type AsyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (...args: Parameters<C[K]>) => Promise<Parameters<C[K]>> | Parameters<C[K]>;
};
export type AsyncAfterHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (
result: Awaited<ReturnType<C[K]>>,
...args: Parameters<C[K]>
) => ReturnType<C[K]> | Awaited<ReturnType<C[K]>>;
};
export type SyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (...args: Parameters<C[K]>) => Parameters<C[K]>;
};
export type SyncAfterHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
[K in Value[number]]?: (result: ReturnType<C[K]>, ...args: Parameters<C[K]>) => ReturnType<C[K]>;
};
export type AddPrefixToObject<T, P extends string> = {
[K in keyof T as K extends string ? \`\${P}\${K}\` : never]: T[K];
};
export type AsyncHookPlugin<
T extends Array<string>,
C extends Record<T[number], (...args: any) => any>,
> = AddPrefixToObject<PascalCasedProperties<AsyncBeforeHook<T, C>>, 'before'> &
AddPrefixToObject<PascalCasedProperties<AsyncAfterHook<T, C>>, 'after'>;
export type SyncHookPlugin<
T extends Array<string>,
C extends Record<T[number], (...args: any) => any>,
> = AddPrefixToObject<PascalCasedProperties<SyncBeforeHook<T, C>>, 'before'> &
AddPrefixToObject<PascalCasedProperties<SyncAfterHook<T, C>>, 'after'>;
export interface EventBusEvent {
'edit-data-source': [id: string];
'edit-data-source-method': [id: string, methodName: string];
'edit-data-source-field': [id: string, fieldPath: string[]];
'remove-data-source': [id: string];
'edit-code': [id: string];
}
export interface EventBus extends EventEmitter {
on<Name extends keyof EventBusEvent, Param extends EventBusEvent[Name]>(
eventName: Name,
listener: (...args: Param) => void,
): this;
emit<Name extends keyof EventBusEvent, Param extends EventBusEvent[Name]>(eventName: Name, ...args: Param): boolean;
}
// #region PropsFormConfigFunction
export type PropsFormConfigFunction = (data: { editorService: EditorService }) => FormConfig;
// #endregion PropsFormConfigFunction
export type PropsFormValueFunction = (data: { editorService: EditorService }) => Partial<MNode>;
// #region PageBarSortOptions
export type PartSortableOptions = Omit<Options, 'onStart' | 'onUpdate'>;
export interface PageBarSortOptions extends PartSortableOptions {
/** 在onUpdate之后调用 */
afterUpdate?: (event: SortableEvent, sortable: Sortable) => void | Promise<void>;
/** 在onStart之前调用 */
beforeStart?: (event: SortableEvent, sortable: Sortable) => void | Promise<void>;
}
// #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: ContentMenuType,
/** 读取当前右键目标;数据源 / 代码块面板会传入,图层 / 画布一般不需要 */
getTarget?: () => ContentMenuTarget | null,
) => (MenuButton | MenuComponent)[];
export interface EditorEvents {
'root-change': [
value: StoreState['root'],
preValue?: StoreState['root'],
options?: { historySource?: HistoryOpSource },
];
select: [node: MNode | null];
add: [nodes: MNode[]];
remove: [nodes: MNode[]];
update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
'move-layer': [offset: number | LayerOffset];
'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
'history-change': [data: MPage | MPageFragment];
/**
* DSL 发生变更后统一触发,免去分别监听 add / remove / update / move-layer / drag-to。
* 回调参数为 {@link EditorChangeEvent},按 \`type\` 区分操作类型并携带各自的操作内容(payload)
* 以及变更所在的当前 page(可能为 null)。撤销 / 重做内部同样会经由
* add / remove / update 触发本事件;如需区分「用户操作」与「撤销重做」请配合 \`history-change\`。
*/
change: [event: EditorChangeEvent];
/** 节点校验错误状态发生变化时触发,携带当前完整的错误 Map(供非响应式消费方订阅) */
'invalid-node-change': [invalidNodeIds: Map<Id, NodeInvalidInfo>];
}
// #region EditorChangeEvent
/** \`change\` 事件中单个变更项:变更的 node 及其所属的 page(可能为 null)。 */
export interface EditorChangeItem {
node: MNode;
page: StoreState['page'];
}
/** \`update\` 类型变更项:node 为前后快照及 form 端变更记录,page 为其所属页面。 */
export interface EditorUpdateChangeItem {
node: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] };
page: StoreState['page'];
}
/** {@link EditorEvents.change} 各操作类型共有的历史相关字段。 */
export interface EditorChangeEventHistoryMeta {
/**
* 本次变更的「历史来源」(调用 DSL 操作时传入的 {@link HistoryOpOptions.historySource},
* 撤销 / 重做时则为被应用 step 上记录的 \`source\`),未携带时为 undefined。
*/
historySource?: HistoryOpSource;
/**
* 本次操作是否未写入历史记录(即调用时传入的 {@link HistoryOpOptions.doNotPushHistory},缺省为 false);
* 撤销 / 重做路径补发的事件恒为 true(撤销/重做本身不再入栈)。
*/
doNotPushHistory?: boolean;
}
/**
* {@link EditorEvents.change} 的回调参数:以 \`type\` 区分操作类型,并携带对应的操作内容。
* \`data\` 为本次变更的节点列表,每项包含 node 及其所属的 page(可能为 null);
* \`move-layer\` 额外带层级偏移 \`offset\`,\`drag-to\` 额外带目标位置 \`targetIndex\` / \`targetParent\`;
* 历史相关字段(historySource / doNotPushHistory)见 {@link EditorChangeEventHistoryMeta}。
*/
export type EditorChangeEvent =
| ({ type: 'add'; data: EditorChangeItem[] } & EditorChangeEventHistoryMeta)
| ({ type: 'remove'; data: EditorChangeItem[] } & EditorChangeEventHistoryMeta)
| ({ type: 'update'; data: EditorUpdateChangeItem[] } & EditorChangeEventHistoryMeta)
| ({ type: 'move-layer'; data: EditorChangeItem[]; offset: number | LayerOffset } & EditorChangeEventHistoryMeta)
| ({
type: 'drag-to';
data: EditorChangeItem[];
targetIndex: number;
targetParent: MContainer;
} & EditorChangeEventHistoryMeta);
// #endregion EditorChangeEvent
export interface HistoryEvents {
change: [
state: BaseStepValue | StepValue | CodeBlockStepValue | DataSourceStepValue,
stepType: HistoryStepType,
id: Id,
];
'code-block-history-change': [id: Id, state: CodeBlockStepValue];
'data-source-history-change': [id: Id, state: DataSourceStepValue];
'restore-from-indexed-db': [snapshot: PersistedHistoryState | null];
'save-to-indexed-db': [snapshot: PersistedHistoryState];
'mark-saved': [{ kind: HistoryStepType; id?: Id }];
clear: [{ id: Id; stepType: HistoryStepType }];
'marker-change': [{ id: Id; marker: StepValue; stepType: HistoryStepType }];
/**
* 页面 / 页面片结构变更(新增 / 删除)时派发,见 {@link HistoryService.notifyPageStructureChange}。
* 一次操作(add / remove / setRoot 整体替换)涉及多个页面时合并为**一个**事件;
* \`add\` / \`remove\` 分别为本次新增与删除的页面列表(其一可为空数组)。
*/
'page-structure-change': [change: { add: (MPage | MPageFragment)[]; remove: (MPage | MPageFragment)[] }];
}
export const canUsePluginMethods = {
async: [
'getLayout',
'highlight',
'select',
'multiSelect',
'doAdd',
'add',
'doRemove',
'remove',
'doUpdate',
'update',
'sort',
'copy',
'paste',
'doPaste',
'doAlignCenter',
'alignCenter',
'moveLayer',
'moveToContainer',
'dragTo',
'undo',
'redo',
'move',
] as const,
sync: [],
};
export type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
// #region HistoryOpOptions
/**
* 历史记录写入相关的通用配置(codeBlock / dataSource / editor 共用)
* - doNotPushHistory: 操作完成后是否不要将本次操作压入历史栈(撤销/重做记录),默认 false
* - historyDescription: 入栈时附带的人类可读描述,用于历史面板展示;不影响 undo/redo 行为,缺省时面板会自动生成描述
* - historySource: 操作途径,取值见 {@link HistoryOpSource}(画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等),用于历史面板展示与埋点;不影响 undo/redo 行为
*/
export interface HistoryOpOptions {
doNotPushHistory?: boolean;
historyDescription?: string;
historySource?: HistoryOpSource;
}
// #endregion HistoryOpOptions
// #region HistoryOpOptionsWithChangeRecords
/**
* 在 HistoryOpOptions 基础上携带 form 端 propPath/value 变更记录,
* 用于历史记录的精细化撤销/重做(按 propPath 局部 patch)。
*/
export interface HistoryOpOptionsWithChangeRecords extends HistoryOpOptions {
changeRecords?: ChangeRecord[];
}
// #endregion HistoryOpOptionsWithChangeRecords
// #region DslOpOptions
/**
* DSL 修改类操作的通用配置
* - doNotSelect: 操作后是否不要自动触发选中(不调用 this.select / this.multiSelect / stage.select / stage.multiSelect)
* - doNotSwitchPage: 操作若会引发当前页面切换(如新增 / 删除 / 跨页移动),是否跳过这次切换
*/
export interface DslOpOptions extends HistoryOpOptions {
doNotSelect?: boolean;
doNotSwitchPage?: boolean;
}
// #endregion DslOpOptions
/** 差异对话框的入参 */
export interface DiffDialogPayload {
/** 表单类别 */
category?: CompareCategory;
/** 节点类型 / 数据源类型 */
type?: string;
/** 代码块场景下的数据源类型 */
dataSourceType?: string;
/** 该 step 修改前的值(oldNode / oldSchema / oldContent) */
lastValue: Record<string, any>;
/** 该 step 修改后的值(newNode / newSchema / newContent) */
value: Record<string, any>;
/** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */
currentValue?: Record<string, any> | null;
/** 用于标题展示的目标名称 */
targetLabel?: string;
/** 用于标题展示的目标 id */
id?: string | number;
/**
* 指定打开时的初始对比模式:
* - before:该步骤修改前 vs 修改后
* - current:该步骤修改后 vs 当前最新值
* 不传时按是否存在「修改后的值」/「当前值」自动推断。
* 若指定的模式当前不可用(对应数据缺失),将回退到自动推断结果。
*/
mode?: 'before' | 'current';
}
/**
* 一组「描述 + 可操作性」的判定函数集合。页面 / 数据源 / 代码块及业务自定义历史
* 各自实现一份,作为整体注入,避免把 describe* / isStep* 拆成多个独立 props 反复透传。
*/
export interface HistoryRowDescriptor<T extends BaseStepValue = BaseStepValue> {
/**
* 组级描述文案生成器,接收一个 group,返回展示文本。
* 不传时回退到对组内最后一步调用 {@link describeStep}(适用于不做相邻合并、每组恒为单步的历史,如数据源/代码块)。
*/
describeGroup?: (_group: any) => string;
/** 单步描述文案生成器,接收一个 step,返回展示文本(合并组展开后的子步列表用)。 */
describeStep: (_step: T) => string;
/** 判断某个 step 是否可查看差异(前后值都存在)。不传则一律不展示差异入口。 */
isStepDiffable?: (_step: T) => boolean;
/** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。不传则已应用即可回滚。 */
isStepRevertable?: (_step: T) => boolean;
}
/**
* 通用 bucket(数据源 / 代码块 / 业务自定义历史)的整体渲染配置。
* 把原先散落在 Bucket / BucketTab 上的 title / prefix / describe* / isStep* / showInitial / gotoEnabled
* 收敛成一个对象作为单一 prop 传递,调用方一次配齐、组件内部按需读取。
*/
export interface HistoryBucketConfig<T extends BaseStepValue = BaseStepValue> extends HistoryRowDescriptor<T> {
/** bucket 头部标题,例如 "数据源" / "代码块"。 */
title: string;
/** 子项 key 的命名空间前缀(\`ds\` 数据源 / \`cb\` 代码块 / 业务自定义如 \`mod\`)。 */
prefix: string;
/** 是否展示底部「回到初始状态」入口,默认 true。无 undo cursor 语义的自定义历史可传 false。 */
showInitial?: boolean;
/** 是否支持「跳转到该记录」(goto),默认 true。 */
gotoEnabled?: boolean;
/** 是否展示顶部「清空」按钮,默认 true。无需提供清空能力的自定义历史可传 false。 */
showClear?: boolean;
}
export interface UseHistoryRevertOptions {
/**
* 父级应用上下文,用于让动态挂载的「差异确认弹窗」继承全局组件 / 指令 / provide / 插件
* (Element Plus、@tmagic/form 字段组件等)。未显式传入时,会自动取调用方所在组件的 appContext
* (\`getCurrentInstance()?.appContext\`)。业务方若在组件 setup 之外调用,需手动传入(如 \`editorApp._context\`)。
*/
appContext?: AppContext | null;
/**
* 透传给差异确认弹窗的 \`extendState\`(即 Editor 的 \`extendFormState\`),
* 使对比表单中依赖业务上下文的 \`display\` / \`disabled\` 等 filterFunction 正常工作。
*/
extendState?: (_state: FormState) => Record<string, any> | Promise<Record<string, any>>;
/**
* 返回 PropsPanel 主属性表单(FormPanel -> MForm)的 formState。
* 仅页面历史「查看差异 / 回滚确认」场景会使用该 formState 覆盖 CompareForm 中同名扩展字段,
* 以保证两处 filterFunction 读取到一致的运行态上下文。
*/
getPropsPanelFormState?: () => FormState | undefined;
/**
* 内置页面 / 数据源 / 代码块的差异 / 回滚确认弹窗默认宽度(透传给 TMagicDialog 的 \`width\`),
* 如 \`'1200px'\` / \`'80%'\`。缺省时使用弹窗内置默认宽度(900px)。
* 业务自有历史(\`viewDiff\` / \`confirmAndRevert\`)可在调用时通过各自入参的 \`width\` 单独覆盖。
*/
dialogWidth?: string;
}
/**
* 业务自有历史(如管理台「模块」)做差异对比时所需的额外渲染入参。
* 内置的页面 / 数据源 / 代码块按 \`category\` 自动取表单配置,无需传这些;
* 业务自有类别可通过 \`loadConfig\` 注入自定义表单配置加载逻辑。
*/
export interface CustomDiffFormOptions {
/**
* 自定义差异表单配置加载逻辑(如「模块」按 c_type 重建表单配置),
* 透传给弹窗内部的 CompareForm;缺省时按 \`category\` 走内置加载。
*/
loadConfig?: CompareFormLoadConfig;
/** 需要走 self diff 的字段类型(如模块的 mod-cond)。 */
selfDiffFieldTypes?: string[];
/**
* 可选:外部提供的 formState(通常来自 PropsPanel 主表单),
* 对比弹窗会用它覆盖 CompareForm 中同名扩展字段,避免上下文不一致。
*/
compareFormState?: FormState;
/**
* 差异 / 确认回滚弹窗宽度(透传给 HistoryDiffDialog 的 TMagicDialog \`width\`),
* 如 \`'1200px'\` / \`'80%'\`。缺省时使用弹窗内置默认宽度(900px)。
*/
width?: string;
/**
* 差异 / 确认回滚弹窗内 form 表单的尺寸(透传给 CompareForm 的 \`size\`),
* 可选 \`'large' | 'default' | 'small'\`,缺省时使用表单内置默认尺寸。
*/
size?: FieldSize;
}
/**
* 业务自有历史复用「单步回滚」交互({@link useHistoryRevert} 的 \`confirmAndRevert\`)的入参。
* 与内置页面 / 数据源 / 代码块回滚共用「目标校验 → 差异/二次确认弹窗 → 反向回滚」流程,
* 业务方只需提供:差异弹窗入参(可选)、表单配置加载(可选)、实际回滚执行函数。
*/
export interface ConfirmAndRevertOptions<T = unknown> extends CustomDiffFormOptions {
/**
* 差异确认弹窗入参;可对比的步骤(单实体 update)传入后弹差异确认弹窗,
* 传 null / 省略则退化为普通二次确认框(add / remove / 不可对比)。
*/
diffPayload?: DiffDialogPayload | null;
/** 回滚前置校验:返回 true 表示目标数据已删除等不可回滚,给出统一提示并中止。 */
isTargetMissing?: () => boolean;
/** 用户确认后执行的实际回滚逻辑。 */
revert: () => T | Promise<T>;
}export type Id = string | number;示例:
import { editorService } from "@tmagic/editor";
// 保存前检查是否存在校验错误的组件
const invalidNodeIds = editorService.getInvalidNodeIds();
if (invalidNodeIds.size > 0) {
const names = [...invalidNodeIds.keys()].map((id) => {
const node = editorService.getNodeById(id);
return node?.name ? \`\${node.name}(\${id})\` : \`\${id}\`;
});
console.warn(\`以下组件存在配置校验错误,请修复后再保存:\${names.join("、")}\`);
}参数:
{Id} id 节点 id返回:
NodeInvalidInfo | undefined} 指定节点的校验错误信息(含 props / style 来源的错误文案)详情:
获取指定节点的校验错误信息。
详情:
清空全部校验错误记录(即 invalidNodeIds 状态),随后发出 invalid-node-change 事件。
清空state
详情:
销毁editorService
移除所有事件监听,清空state,移除所有插件
详情:
usePlugin支持灵活细致的扩展, 上述方法中标记有扩展支持: 是的方法都支持使用usePlugin扩展
每个支持扩展的方法都支持定制before、after两个hook来干预原有方法的行为,before可以用于修改传入参数,after可以用于修改返回的值
import { editorService } from "@tmagic/editor";
editorService.usePlugin({
// 添加组件的时候设置一个添加时间
beforeDoAdd: (config, parent) => {
config.addTime = new Date().getTime();
return [config, parent];
},
});删掉当前设置的所有扩展
`,140)]])}var s=t(a,[[`render`,o]]);export{i as __pageData,s as default};