/* * 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, 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 { 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 { 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 WorkspaceSlots { 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; export type GetConfig = (config: FormConfig) => Promise | FormConfig; export interface EditorInstallOptions { parseDSL: (dsl: string) => T; customCreateMonacoEditor: ( monaco: typeof import('monaco-editor'), codeEditorEl: HTMLElement, options: Monaco.editor.IStandaloneEditorConstructionOptions & { editorCustomType?: string }, ) => Promise | Monaco.editor.IStandaloneCodeEditor; customCreateMonacoDiffEditor: ( monaco: typeof import('monaco-editor'), codeEditorEl: HTMLElement, options: Monaco.editor.IStandaloneEditorConstructionOptions & { editorCustomType?: string }, ) => Promise | Monaco.editor.IStandaloneDiffEditor; [key: string]: any; } 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; } export interface StageOptions { runtimeUrl?: string; autoScrollIntoView?: boolean; containerHighlightClassName?: string; containerHighlightDuration?: number; containerHighlightType?: ContainerHighlightType; disabledDragStart?: boolean; render?: (stage: StageCore) => HTMLDivElement | void | Promise; moveableOptions?: CustomizeMoveableOptions; canSelect?: (el: HTMLElement) => boolean | Promise; isContainer?: (el: HTMLElement) => boolean | Promise; updateDragEl?: UpdateDragEl; renderType?: RenderType; guidesOptions?: Partial; disabledMultiSelect?: boolean; disabledRule?: boolean; zoom?: number; /** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */ beforeDblclick?: (event: MouseEvent) => Promise | 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; pageLength: number; pageFragmentLength: number; disabledMultiSelect: boolean; } export type StoreStateKey = keyof StoreState; export interface PropsState { propsConfigMap: Record; propsValueMap: Record>; relateIdMap: Record; /** 禁用数据源 */ 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[]; } export enum ColumnLayout { LEFT = 'left', CENTER = 'center', RIGHT = 'right', } 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[]; // navMenu 的宽高 navMenuRect: { left: number; top: number; width: number; height: number; }; frameworkRect: { left: number; top: number; width: number; height: number; }; } export interface EditorNodeInfo { node: MNode | null; parent: MContainer | null; page: MPage | MPageFragment | null; } export interface AddMNode { type: string; name?: string; inputEvent?: DragEvent; [key: string]: any; } export interface PastePosition { left?: number; top?: number; /** * 粘贴位置X方向偏移量 */ offsetX?: number; /** * 粘贴位置Y方向偏移量 */ offsetY?: number; } /** * 菜单按钮 */ 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; className?: string; /** type为dropdown时,下拉的菜单列表, 或者有子菜单时 */ items?: MenuButton[]; /** 唯一标识,用于高亮 */ id?: string | number; } export interface MenuComponent { type: 'component'; /** Vue3组件 */ component: any; /** 传入组件的props对象 */ props?: Record; /** 组件监听的事件对象,如:{ click: () => { console.log('click'); } } */ listeners?: Record; slots?: Record; /** 是否显示,默认为true */ className?: string; display?: boolean | ((data: Services) => Promise | boolean); [key: string]: any; } /** * '/': 分隔符 * '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': 缩放以适应 */ export type MenuItem = | '/' | 'delete' | 'undo' | 'redo' | 'zoom' | 'zoom-in' | 'zoom-out' | 'guides' | 'rule' | 'scale-to-original' | 'scale-to-fit' | MenuButton | MenuComponent | string; /** 工具栏 */ export interface MenuBarData { /** 顶部工具栏左边项 */ [ColumnLayout.LEFT]?: MenuItem[]; /** 顶部工具栏中间项 */ [ColumnLayout.CENTER]?: MenuItem[]; /** 顶部工具栏右边项 */ [ColumnLayout.RIGHT]?: MenuItem[]; } export interface SideComponent extends MenuComponent { /** 显示文案 */ text: string; /** tab样式 */ tabStyle?: string | Record; /** vue组件或url */ icon?: any; /** slide 唯一标识 key */ $key: string; /** 是否可以将面板拖出,默认为true */ draggable?: boolean; /** 点击切换tab前调用,返回false阻止切换 */ beforeClick?: (config: SideComponent) => boolean | Promise; /** 组件扩展参数 */ boxComponentConfig?: { /** Vue3组件 */ component?: any; /** 传入组件的props对象 */ props?: Record; }; } export enum SideItemKey { COMPONENT_LIST = 'component-list', LAYER = 'layer', CODE_BLOCK = 'code-block', DATA_SOURCE = 'data-source', } /** * component-list: 组件列表 * layer: 已选组件树 * code-block: 代码块 */ export type SideItem = `${SideItemKey}` | SideComponent; /** 工具栏 */ export interface SideBarData { /** 容器类型 */ type: 'tabs'; /** 默认激活的内容 */ status: string; /** panel列表 */ items: SideItem[]; } export interface ComponentItem { /** 显示文案 */ text: string; /** 详情,用于tooltip */ desc?: string; /** 组件类型 */ type: string; /** Vue组件或url */ icon?: string | Component<{}, {}, any>; /** 新增组件时需要透传到组价节点上的数据 */ data?: { [key: string]: any; }; } export interface ComponentGroup { /** 显示文案 */ title: string; /** 组内列表 */ items: ComponentItem[]; } export enum LayerOffset { TOP = 'top', BOTTOM = 'bottom', } /** 容器布局 */ export enum Layout { FLEX = 'flex', FIXED = 'fixed', RELATIVE = 'relative', ABSOLUTE = 'absolute', } 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; [key: string]: any; } export type HistoryOpType = 'add' | 'remove' | 'update'; export interface StepValue { /** 页面信息 */ data: { name: string; id: Id }; opType: HistoryOpType; /** 操作前选中的节点 ID,用于撤销后恢复选择状态 */ selectedBefore: Id[]; /** 操作后选中的节点 ID,用于重做后恢复选择状态 */ selectedAfter: Id[]; modifiedNodeIds: Map; /** opType 'add': 新增的节点 */ nodes?: MNode[]; /** opType 'add': 父节点 ID */ parentId?: Id; /** opType 'add': 每个新增节点在父节点 items 中的索引 */ indexMap?: Record; /** opType 'remove': 被删除的节点及其位置信息 */ removedItems?: { node: MNode; parentId: Id; index: number }[]; /** opType 'update': 变更前后的节点快照 */ updatedItems?: { oldNode: MNode; newNode: MNode }[]; } export interface HistoryState { pageId?: Id; pageSteps: Record>; canRedo: boolean; canUndo: boolean; } 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; } /** 可新增的数据源类型选项 */ export interface DatasourceTypeOption { /** 数据源类型 */ type: string; /** 数据源名称 */ text: string; } /** 组件树节点状态 */ export interface LayerNodeStatus { /** 显隐 */ visible: boolean; /** 展开子节点 */ expand: boolean; /** 选中 */ selected: boolean; /** 是否可拖拽 */ draggable: boolean; } /** 拖拽类型 */ export enum DragType { /** 从组件列表拖到画布 */ COMPONENT_LIST = 'component-list', /** 拖动组件树节点 */ LAYER_TREE = 'layer-tree', } export interface TreeNodeData { id: Id; name?: string; items?: TreeNodeData[]; [key: string]: any; } export type AsyncBeforeHook, C extends Record any>> = { [K in Value[number]]?: (...args: Parameters) => Promise> | Parameters; }; export type AsyncAfterHook, C extends Record any>> = { [K in Value[number]]?: ( result: Awaited>, ...args: Parameters ) => ReturnType | Awaited>; }; export type SyncBeforeHook, C extends Record any>> = { [K in Value[number]]?: (...args: Parameters) => Parameters; }; export type SyncAfterHook, C extends Record any>> = { [K in Value[number]]?: (result: ReturnType, ...args: Parameters) => ReturnType; }; export type AddPrefixToObject = { [K in keyof T as K extends string ? `${P}${K}` : never]: T[K]; }; export type AsyncHookPlugin< T extends Array, C extends Record any>, > = AddPrefixToObject>, 'before'> & AddPrefixToObject>, 'after'>; export type SyncHookPlugin< T extends Array, C extends Record any>, > = AddPrefixToObject>, 'before'> & AddPrefixToObject>, 'after'>; export interface EventBusEvent { 'edit-data-source': [id: string]; 'remove-data-source': [id: string]; 'edit-code': [id: string]; } export interface EventBus extends EventEmitter { on( eventName: Name, listener: (...args: Param) => void, ): this; emit(eventName: Name, ...args: Param): boolean; } export type PropsFormConfigFunction = (data: { editorService: EditorService }) => FormConfig; export type PropsFormValueFunction = (data: { editorService: EditorService }) => Partial; export type PartSortableOptions = Omit; export interface PageBarSortOptions extends PartSortableOptions { /** 在onUpdate之后调用 */ afterUpdate?: (event: SortableEvent, sortable: Sortable) => void | Promise; /** 在onStart之前调用 */ beforeStart?: (event: SortableEvent, sortable: Sortable) => void | Promise; } 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']]; 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']>;