Skip to content

uiService事件

state-change

  • 详情: UI 状态发生变化时触发,uiService.set() 在写入的新值与旧值不同时触发

  • 事件回调函数: (name: keyof UiState, value: UiState[typeof name], preValue: UiState[typeof name]) => void

    查看 UiState 类型定义
    ts
    /*
     * 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 { 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 { ChangeRecord, FormConfig, 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 {
      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;
    }
    
    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>;
      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;
      /** 是否显示画布参考线,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>;
    // #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> {
      /**
       * 历史记录唯一标识(uuid)。入栈时自动写入(若调用方未指定),
       * 用于精确定位 / 引用某一条历史记录(如 revert、埋点、跨端同步等)。
       * 注意与各自的 `id`(关联的页面 / 代码块 / 数据源 id)区分。
       */
      uuid: string;
      /** 操作类型:新增 / 删除 / 更新(三类历史记录统一携带)。 */
      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;
    }
    // #endregion BaseStepValue
    
    // #region StepValue
    export interface StepValue extends BaseStepValue<MNode> {
      /** 页面信息 */
      data: { name: string; id: Id };
      /** 操作前选中的节点 ID,用于撤销后恢复选择状态 */
      selectedBefore: Id[];
      /** 操作后选中的节点 ID,用于重做后恢复选择状态 */
      selectedAfter: Id[];
      modifiedNodeIds: Map<Id, Id>;
    }
    // #endregion StepValue
    
    // #region CodeBlockStepValue
    /**
     * 代码块历史记录条目。按 codeBlock.id 分组保存到 historyState.codeBlockState。
     * 变更内容统一由 `diff`(单项)表达,每项见 {@link StepDiffItem}
     * - 新增(opType 'add'):仅 `newSchema`(新内容);
     * - 更新(opType 'update'):`oldSchema` + `newSchema`,并可带 `changeRecords` 做局部更新;
     * - 删除(opType 'remove'):仅 `oldSchema`(删除前内容)。
     */
    export interface CodeBlockStepValue extends BaseStepValue<CodeBlockContent> {
      /** 关联的代码块 id */
      id: Id;
    }
    // #endregion CodeBlockStepValue
    
    // #region DataSourceStepValue
    /**
     * 数据源历史记录条目。按 dataSource.id 分组保存到 historyState.dataSourceState。
     * 变更内容统一由 `diff`(单项)表达,每项见 {@link StepDiffItem}
     * - 新增(opType 'add'):仅 `newSchema`(新 schema);
     * - 更新(opType 'update'):`oldSchema` + `newSchema`,并可带 `changeRecords` 做局部更新;
     * - 删除(opType 'remove'):仅 `oldSchema`(删除前 schema)。
     */
    export interface DataSourceStepValue extends BaseStepValue<DataSourceSchema> {
      /** 关联的数据源 id */
      id: Id;
    }
    // #endregion DataSourceStepValue
    
    export interface HistoryState {
      pageId?: Id;
      pageSteps: Record<Id, UndoRedo<StepValue>>;
      canRedo: boolean;
      canUndo: boolean;
      /**
       * 代码块历史栈,按 codeBlock.id 分组(每个代码块独立一份 UndoRedo)。
       * 与页面/节点无关,支持独立 undo/redo。
       */
      codeBlockState: Record<Id, UndoRedo<CodeBlockStepValue>>;
      /**
       * 数据源历史栈,按 dataSource.id 分组(每个数据源独立一份 UndoRedo)。
       * 与页面/节点无关,支持独立 undo/redo。
       */
      dataSourceState: Record<Id, UndoRedo<DataSourceStepValue>>;
    }
    
    // #region PersistedHistoryState
    /**
     * 历史记录的可持久化快照。由 historyService.saveToIndexedDB 写入 IndexedDB,
     * 再由 historyService.restoreFromIndexedDB 读出并重建各 UndoRedo 栈。
     */
    export interface PersistedHistoryState {
      /** 快照结构版本号,便于后续兼容升级。 */
      version: number;
      /** 保存时的活动页 id。 */
      pageId?: Id;
      /** 各页面历史栈的序列化快照,按 pageId 分组。 */
      pageSteps: Record<Id, SerializedUndoRedo<StepValue>>;
      /** 各代码块历史栈的序列化快照,按 codeBlockId 分组。 */
      codeBlockState: Record<Id, SerializedUndoRedo<CodeBlockStepValue>>;
      /** 各数据源历史栈的序列化快照,按 dataSourceId 分组。 */
      dataSourceState: Record<Id, SerializedUndoRedo<DataSourceStepValue>>;
      /** 保存时间戳(毫秒)。 */
      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 PageHistoryStepEntry {
      /** 步骤内容 */
      step: StepValue;
      /** 在所属栈中的索引(0 为最早) */
      index: number;
      /** 是否处于"已应用"段(即位于栈游标之前)。撤销后变为 false。 */
      applied: boolean;
      /** 是否为当前所在的步骤(栈中最近一次已应用的那一步,即 index === cursor - 1)。 */
      isCurrent?: boolean;
    }
    
    /**
     * 页面历史面板分组。
     * - 连续修改同一目标节点(updatedItems[0].oldNode.id 一致)的 'update' 步骤合并成一组;
     * - 多节点更新 / add / remove 始终独立成组(无法明确归属单一目标)。
     * - targetId 为 undefined 表示"无明确目标"(如 add/remove/多节点 update),不参与合并。
     */
    export interface PageHistoryGroup {
      kind: 'page';
      /** 所属页面 id */
      pageId: Id;
      /** 该分组的操作类型 */
      opType: HistoryOpType;
      /**
       * 合并的目标节点 id;只有"单节点 update"才有值,并按此 id 与相邻同 id 的 update 合并。
       * undefined 表示该分组不可被合并(add / remove / 多节点 update)。
       */
      targetId?: Id;
      /** 目标节点的可读名(取最后一步的 newNode.name/type/id) */
      targetName?: string;
      /** 组内所有步骤,按时间正序 */
      steps: PageHistoryStepEntry[];
      /** 组内最后一步是否已应用 */
      applied: boolean;
      /** 是否为当前所在的分组(包含栈中最近一次已应用步骤的那一组)。 */
      isCurrent?: boolean;
    }
    
    /**
     * 代码块历史面板分组。
     * - 同一 codeBlockId 的栈内,相邻的 'update' 操作会合并成一个 group;
     * - 'add' / 'remove' 始终独立成组(语义上是一次性事件)。
     */
    export interface CodeBlockHistoryGroup {
      kind: 'code-block';
      /** 关联的 codeBlock id */
      id: Id;
      /** 该分组的操作类型 */
      opType: HistoryOpType;
      /** 组内所有步骤,按时间正序 */
      steps: { step: CodeBlockStepValue; index: number; applied: boolean; isCurrent?: boolean }[];
      /** 组内最后一步是否已应用,用于整组的状态展示 */
      applied: boolean;
      /** 是否为当前所在的分组(包含该栈最近一次已应用步骤的那一组)。 */
      isCurrent?: boolean;
    }
    
    /**
     * 数据源历史面板分组,结构同 CodeBlockHistoryGroup。
     */
    export interface DataSourceHistoryGroup {
      kind: 'data-source';
      id: Id;
      opType: HistoryOpType;
      steps: { step: DataSourceStepValue; 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
    
    export type CustomContentMenuFunction = (
      menus: (MenuButton | MenuComponent)[],
      type: 'layer' | 'data-source' | 'viewer' | 'code-block',
    ) => (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];
    }
    
    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;
    }
    
    /**
     * 一组「描述 + 可操作性」的判定函数集合。页面 / 数据源 / 代码块及业务自定义历史
     * 各自实现一份,作为整体注入,避免把 describe* / isStep* 拆成多个独立 props 反复透传。
     */
    export interface HistoryRowDescriptor<T extends BaseStepValue = BaseStepValue> {
      /** 组级描述文案生成器,接收一个 group,返回展示文本。 */
      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;
    }
  • 示例:

js
import { uiService } from '@tmagic/editor';

uiService.on('state-change', (name, value, preValue) => {
  console.log(`${name} 从`, preValue, '变为', value);
});

uiService.set('zoom', 1.5);

TIP

  • 新值与旧值相同时不会触发该事件
  • 通过 set('stageRect', value) 修改画布尺寸时,内部会走 setStageRect 逻辑并可能联动更新 zoom,但不会触发 state-change 事件

Powered by 腾讯视频会员平台技术中心