mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-06 07:30:16 +00:00
feat(editor): 历史记录支持操作来源
This commit is contained in:
parent
a8a9cf372d
commit
27b2c2c685
@ -1,5 +1,8 @@
|
||||
# codeBlockService方法
|
||||
|
||||
写入历史栈的方法([setCodeDslById](#setcodedslbyid)、[setCodeDslByIdSync](#setcodedslbyidsync)、[deleteCodeDslByIds](#deletecodedslbyids) 等)的 `options` 支持
|
||||
[historyDescription / historySource](./editorServiceMethods.md#历史记录相关-options),会透传到 `historyService.pushCodeBlock` 的 `historyDescription` / `source` 字段。
|
||||
|
||||
## setCodeDsl
|
||||
|
||||
- **参数:**
|
||||
@ -51,6 +54,8 @@
|
||||
- `{Object}` options 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords form 端变更记录,用于历史记录的精细化撤销/重做
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
|
||||
::: details 查看 ChangeRecord 类型定义
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
@ -72,6 +77,8 @@
|
||||
- `{Object}` options 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords form 端变更记录,用于历史记录的精细化撤销/重做
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{void}`
|
||||
@ -213,6 +220,8 @@
|
||||
- `{(string | number)[]}` codeIds 需要删除的代码块id数组
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
|
||||
@ -300,6 +300,8 @@ dataSourceService.setFormMethod("http", [
|
||||
- {`DataSourceSchema`} config 数据源配置
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- {`DataSourceSchema`} 添加后的数据源配置
|
||||
@ -338,6 +340,8 @@ console.log(newDs.id); // 自动生成的id
|
||||
- `{Object}` options 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords 变更记录
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
|
||||
::: details 查看 ChangeRecord 类型定义
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
@ -379,6 +383,8 @@ console.log(updatedDs);
|
||||
- `{string}` id 数据源id
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见 [editorService 历史记录相关 options](./editorServiceMethods.md#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{void}`
|
||||
|
||||
@ -1,5 +1,25 @@
|
||||
# editorService方法
|
||||
|
||||
## 历史记录相关 options
|
||||
|
||||
下列 DSL 操作方法([add](#add)、[remove](#remove)、[update](#update) 等)的 `options` / `data` 参数,以及
|
||||
[codeBlockService](./codeBlockServiceMethods.md) / [dataSourceService](./dataSourceServiceMethods.md)
|
||||
的 `options`,在 `doNotPushHistory` 之外还可传入:
|
||||
|
||||
- `{string}` **historyDescription**:入栈时附带的人类可读描述,用于历史面板展示;不影响 undo/redo 行为,缺省时面板会自动生成描述
|
||||
- `{HistoryOpSource}` **historySource**:操作途径,用于历史面板展示与埋点;不影响 undo/redo 行为,缺省时面板视为「未知」
|
||||
|
||||
编辑器内置交互(画布、树面板、配置面板、右键菜单、快捷键等)会自动传入对应的 `historySource`;
|
||||
业务侧程序化调用时建议显式传入(如 `api`),便于历史面板区分来源。
|
||||
|
||||
::: details 查看 HistoryOpOptions / DslOpOptions / HistoryOpSource 类型定义
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpOptions{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#DslOpOptions{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
:::
|
||||
|
||||
## get
|
||||
|
||||
- **参数:**
|
||||
@ -359,6 +379,8 @@ editorService.highlight("text_123");
|
||||
- `{boolean}` doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
||||
- `{boolean}` doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>} 新增的组件或组件集合
|
||||
@ -405,6 +427,8 @@ editorService.highlight("text_123");
|
||||
- `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
|
||||
- `{boolean}` doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -459,6 +483,8 @@ editorService.highlight("text_123");
|
||||
- {`ChangeRecord`[]} changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 `changeRecordList`)
|
||||
- {`ChangeRecord`[][]} changeRecordList 多节点 form 端变更记录列表,按 `config` 数组同序对应每个节点;优先级高于 `changeRecords`
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
::: details 查看 ChangeRecord 类型定义
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
@ -500,6 +526,7 @@ editorService.highlight("text_123");
|
||||
- `{boolean}` doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
||||
- `{boolean}` doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致)
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -568,6 +595,8 @@ editorService.highlight("text_123");
|
||||
- `{boolean}` doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
|
||||
- `{boolean}` doNotSwitchPage 粘贴后是否不切换当前页面(默认 false;跨页粘贴时为 true 会跳过页面切换)
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>} 添加后的组件节点配置
|
||||
@ -606,6 +635,8 @@ editorService.highlight("text_123");
|
||||
- `{boolean}` doNotSelect 居中后是否不更新当前选中节点(默认 false)
|
||||
- `{boolean}` doNotSwitchPage 居中后是否不切换当前页面(居中只更新节点 style,方法内为空操作;保留以与其它 DSL 操作 API 一致)
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>}
|
||||
@ -628,6 +659,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- `{number | 'top' | 'bottom'}` offset
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -649,6 +682,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- `{boolean}` doNotSelect 移动后是否不更新当前选中节点(默认 false)
|
||||
- `{boolean}` doNotSwitchPage 移动后是否不切换当前页面(默认 false;目标容器位于其它页面时为 true 会跳过自动选中以避免页面切换)
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- Promise<`MNode` | undefined>
|
||||
@ -665,6 +700,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- `{number}` targetIndex 目标位置索引
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -685,6 +722,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
:::
|
||||
|
||||
@ -699,6 +738,16 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- **返回:**
|
||||
- {Promise<`StepValue` | null>}
|
||||
|
||||
::: details 查看 StepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#StepValue{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
:::
|
||||
|
||||
- **详情:**
|
||||
|
||||
恢复到下一步
|
||||
@ -712,6 +761,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- `{number}` top
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#MNode{ts}
|
||||
@ -39,6 +41,8 @@
|
||||
::: details 查看 CodeBlockStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
@ -59,6 +63,8 @@
|
||||
::: details 查看 DataSourceStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
:::
|
||||
|
||||
|
||||
@ -43,6 +43,8 @@
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpType{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#Id{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#MNode{ts}
|
||||
@ -61,6 +63,8 @@
|
||||
`opType: 'update'` 的每个 `updatedItems[i]` 上可携带 `changeRecords`,用于撤销 / 重做时仅按
|
||||
`propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;不带
|
||||
`changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。
|
||||
|
||||
`StepValue` 上的 `historyDescription` / `source` 仅用于历史面板展示与埋点,不影响 undo/redo 行为。
|
||||
:::
|
||||
|
||||
## undo
|
||||
@ -91,10 +95,14 @@
|
||||
- `{CodeBlockContent | null} oldContent` 变更前的代码块内容;新增时为 `null`
|
||||
- `{CodeBlockContent | null} newContent` 变更后的代码块内容;删除时为 `null`
|
||||
- `{ChangeRecord[]} changeRecords` 可选;form 端 propPath/value 变更列表,撤销/重做时若有则按 propPath 局部更新;缺省(或空数组)才退化为整内容替换
|
||||
- `{string}` historyDescription 可选;人类可读描述,用于历史面板展示;不影响 undo/redo 行为
|
||||
- `{HistoryOpSource}` source 可选;操作途径,用于历史面板展示与埋点;不影响 undo/redo 行为
|
||||
|
||||
::: details 查看 CodeBlockStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#CodeBlockStepValue{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/schema/src/index.ts#CodeBlockContent{ts}
|
||||
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
@ -172,10 +180,14 @@
|
||||
- `{DataSourceSchema | null} oldSchema` 变更前的数据源 schema;新增时为 `null`
|
||||
- `{DataSourceSchema | null} newSchema` 变更后的数据源 schema;删除时为 `null`
|
||||
- `{ChangeRecord[]} changeRecords` 可选;form 端 propPath/value 变更列表,撤销/重做时若有则按 propPath 局部更新;缺省(或空数组)才退化为整 schema 替换
|
||||
- `{string}` historyDescription 可选;人类可读描述,用于历史面板展示;不影响 undo/redo 行为
|
||||
- `{HistoryOpSource}` source 可选;操作途径,用于历史面板展示与埋点;不影响 undo/redo 行为
|
||||
|
||||
::: details 查看 DataSourceStepValue 及关联类型定义
|
||||
<<< @/../packages/editor/src/type.ts#DataSourceStepValue{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#HistoryOpSource{ts}
|
||||
|
||||
<<< @/../packages/form-schema/src/base.ts#ChangeRecord{ts}
|
||||
:::
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { tMagicMessage } from '@tmagic/design';
|
||||
import type { ContainerChangeEventData } from '@tmagic/form';
|
||||
|
||||
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
||||
import type { Services } from '@editor/type';
|
||||
import type { HistoryOpSource, Services } from '@editor/type';
|
||||
|
||||
export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService']) => {
|
||||
const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
|
||||
@ -58,8 +58,8 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
|
||||
};
|
||||
|
||||
// 删除代码块
|
||||
const deleteCode = async (key: string) => {
|
||||
codeBlockService.deleteCodeDslByIds([key]);
|
||||
const deleteCode = async (key: string, { historySource }: { historySource?: HistoryOpSource } = {}) => {
|
||||
codeBlockService.deleteCodeDslByIds([key], { historySource });
|
||||
};
|
||||
|
||||
const submitCodeBlockHandler = async (values: CodeBlockContent, eventData?: ContainerChangeEventData) => {
|
||||
@ -67,6 +67,7 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
|
||||
|
||||
await codeBlockService.setCodeDslById(codeId.value, values, {
|
||||
changeRecords: eventData?.changeRecords,
|
||||
historySource: 'props',
|
||||
});
|
||||
|
||||
codeBlockEditorRef.value?.hide();
|
||||
|
||||
@ -27,9 +27,9 @@ export const useDataSourceEdit = (dataSourceService: Services['dataSourceService
|
||||
|
||||
const submitDataSourceHandler = (value: DataSourceSchema, eventData: ContainerChangeEventData) => {
|
||||
if (value.id) {
|
||||
dataSourceService.update(value, { changeRecords: eventData.changeRecords });
|
||||
dataSourceService.update(value, { changeRecords: eventData.changeRecords, historySource: 'props' });
|
||||
} else {
|
||||
dataSourceService.add(value);
|
||||
dataSourceService.add(value, { historySource: 'props' });
|
||||
}
|
||||
|
||||
editDialog.value?.hide();
|
||||
|
||||
@ -130,16 +130,16 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
});
|
||||
if (configs.length === 0) return;
|
||||
|
||||
editorService.update(configs, { changeRecordList });
|
||||
editorService.update(configs, { changeRecordList, historySource: 'stage' });
|
||||
});
|
||||
|
||||
stage.on('sort', (ev: SortEventData) => {
|
||||
editorService.sort(ev.src, ev.dist);
|
||||
editorService.sort(ev.src, ev.dist, { historySource: 'stage' });
|
||||
});
|
||||
|
||||
stage.on('remove', (ev: RemoveEventData) => {
|
||||
const nodes = ev.data.map(({ el }) => editorService.getNodeById(getIdFromEl()(el) || ''));
|
||||
editorService.remove(nodes.filter((node) => Boolean(node)) as MNode[]);
|
||||
editorService.remove(nodes.filter((node) => Boolean(node)) as MNode[], { historySource: 'stage' });
|
||||
});
|
||||
|
||||
stage.on('select-parent', () => {
|
||||
|
||||
@ -80,7 +80,7 @@ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
|
||||
disabled: () => editorService.get('node')?.type === NodeType.PAGE,
|
||||
handler: () => {
|
||||
const node = editorService.get('node');
|
||||
node && editorService.remove(node);
|
||||
node && editorService.remove(node, { historySource: 'toolbar' });
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
:merged="group.steps.length > 1"
|
||||
:op-type="group.opType"
|
||||
:desc="describeGroup(group)"
|
||||
:source="groupSource(group)"
|
||||
:time="formatHistoryTime(groupTimestamp(group))"
|
||||
:time-title="formatHistoryFullTime(groupTimestamp(group))"
|
||||
:step-count="group.steps.length"
|
||||
@ -26,6 +27,7 @@
|
||||
desc: describeStep(s.step),
|
||||
diffable: isStepDiffable ? isStepDiffable(s.step) : false,
|
||||
revertable: s.applied,
|
||||
source: s.step.source,
|
||||
time: formatHistoryTime(s.step.timestamp),
|
||||
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
||||
}))
|
||||
@ -58,7 +60,7 @@ import { computed } from 'vue';
|
||||
|
||||
import type { HistoryOpType } from '@editor/type';
|
||||
|
||||
import { formatHistoryFullTime, formatHistoryTime, groupTimestamp } from './composables';
|
||||
import { formatHistoryFullTime, formatHistoryTime, groupSource, groupTimestamp } from './composables';
|
||||
import GroupRow from './GroupRow.vue';
|
||||
import InitialRow from './InitialRow.vue';
|
||||
|
||||
|
||||
@ -13,6 +13,13 @@
|
||||
<span class="m-editor-history-list-item-op" :class="`op-${opType}`">{{ opLabel(opType) }}</span>
|
||||
<span class="m-editor-history-list-item-desc">{{ desc }}</span>
|
||||
|
||||
<span
|
||||
v-if="sourceLabel(source)"
|
||||
class="m-editor-history-list-item-source"
|
||||
:title="`操作途径:${sourceLabel(source)}`"
|
||||
>{{ sourceLabel(source) }}</span
|
||||
>
|
||||
|
||||
<span v-if="time" class="m-editor-history-list-item-time" :title="timeTitle || time">{{ time }}</span>
|
||||
|
||||
<span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
|
||||
@ -50,6 +57,12 @@
|
||||
>
|
||||
<span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
|
||||
<span class="m-editor-history-list-substep-desc">{{ s.desc }}</span>
|
||||
<span
|
||||
v-if="sourceLabel(s.source)"
|
||||
class="m-editor-history-list-item-source"
|
||||
:title="`操作途径:${sourceLabel(s.source)}`"
|
||||
>{{ sourceLabel(s.source) }}</span
|
||||
>
|
||||
<span v-if="s.time" class="m-editor-history-list-item-time" :title="s.timeTitle || s.time">{{ s.time }}</span>
|
||||
<span
|
||||
v-if="s.revertable"
|
||||
@ -80,9 +93,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { HistoryOpType } from '@editor/type';
|
||||
import type { HistoryOpSource, HistoryOpType } from '@editor/type';
|
||||
|
||||
import { opLabel } from './composables';
|
||||
import { opLabel, sourceLabel } from './composables';
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorHistoryListGroupRow',
|
||||
@ -100,6 +113,8 @@ const props = withDefaults(
|
||||
opType: HistoryOpType;
|
||||
/** 组的整体描述文案,由上层根据 step / group 计算后传入,例如 "修改 button · style.color"。 */
|
||||
desc: string;
|
||||
/** 组的操作途径(一般取组内最近一步),用于头部展示「画布 / 树面板 / 配置面板…」标签。 */
|
||||
source?: HistoryOpSource;
|
||||
/** 组头部展示的时间文案(一般为组内最近一步的时间),为空时不渲染。 */
|
||||
time?: string;
|
||||
/** 组头部时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
|
||||
@ -115,6 +130,8 @@ const props = withDefaults(
|
||||
diffable?: boolean;
|
||||
/** 是否可对该子步执行「回滚」(已应用 + 业务侧确认支持反向)。父级根据 step 与 applied 决定。 */
|
||||
revertable?: boolean;
|
||||
/** 该子步的操作途径,用于展示「画布 / 树面板 / 配置面板…」标签。 */
|
||||
source?: HistoryOpSource;
|
||||
/** 该子步的时间文案,为空时不渲染。 */
|
||||
time?: string;
|
||||
/** 该子步时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
:merged="group.steps.length > 1"
|
||||
:op-type="group.opType"
|
||||
:desc="describePageGroup(group)"
|
||||
:source="groupSource(group)"
|
||||
:time="formatHistoryTime(groupTimestamp(group))"
|
||||
:time-title="formatHistoryFullTime(groupTimestamp(group))"
|
||||
:step-count="group.steps.length"
|
||||
@ -21,6 +22,7 @@
|
||||
desc: describePageStep(s.step),
|
||||
diffable: isPageStepDiffable(s.step),
|
||||
revertable: s.applied,
|
||||
source: s.step.source,
|
||||
time: formatHistoryTime(s.step.timestamp),
|
||||
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
||||
}))
|
||||
@ -53,6 +55,7 @@ import {
|
||||
describePageStep,
|
||||
formatHistoryFullTime,
|
||||
formatHistoryTime,
|
||||
groupSource,
|
||||
groupTimestamp,
|
||||
} from './composables';
|
||||
import GroupRow from './GroupRow.vue';
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
CodeBlockStepValue,
|
||||
DataSourceHistoryGroup,
|
||||
DataSourceStepValue,
|
||||
HistoryOpSource,
|
||||
HistoryOpType,
|
||||
PageHistoryGroup,
|
||||
StepValue,
|
||||
@ -107,6 +108,32 @@ export const opLabel = (op: HistoryOpType) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 内置操作途径的中文文案;自定义来源直接回显原值,未知 / 缺省返回空串(UI 据此不渲染)。 */
|
||||
const HISTORY_SOURCE_LABELS: Record<string, string> = {
|
||||
stage: '画布',
|
||||
tree: '树面板',
|
||||
'component-panel': '组件面板',
|
||||
props: '配置面板',
|
||||
code: '源码',
|
||||
'stage-contextmenu': '画布菜单',
|
||||
'tree-contextmenu': '树菜单',
|
||||
toolbar: '工具栏',
|
||||
shortcut: '快捷键',
|
||||
rollback: '回滚',
|
||||
api: '接口',
|
||||
ai: 'AI',
|
||||
unknown: '未知',
|
||||
};
|
||||
|
||||
/** 操作途径文案:用于历史面板展示「画布 / 树面板 / 配置面板…」标签。 */
|
||||
export const sourceLabel = (source: HistoryOpSource = 'unknown'): string => {
|
||||
return HISTORY_SOURCE_LABELS[source] ?? `${source}`;
|
||||
};
|
||||
|
||||
/** 取一组历史步骤里最后一步(最近一次)的操作途径,用于组头部展示。 */
|
||||
export const groupSource = (group: { steps: { step: { source?: HistoryOpSource } }[] }): HistoryOpSource | undefined =>
|
||||
group.steps[group.steps.length - 1]?.step.source;
|
||||
|
||||
const nameOf = (node: { name?: string; id?: string | number; type?: string }) =>
|
||||
node?.name || node?.type || `${node?.id ?? ''}`;
|
||||
|
||||
|
||||
@ -151,7 +151,11 @@ const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
|
||||
});
|
||||
}
|
||||
|
||||
editorService.update(newValue, { changeRecords: eventData?.changeRecords });
|
||||
// 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
|
||||
// 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
|
||||
const historySource = eventData ? 'props' : 'code';
|
||||
|
||||
editorService.update(newValue, { changeRecords: eventData?.changeRecords, historySource });
|
||||
} catch (e: any) {
|
||||
emit('submit-error', e);
|
||||
}
|
||||
|
||||
@ -94,11 +94,15 @@ let clientX: number;
|
||||
let clientY: number;
|
||||
|
||||
const appendComponent = ({ text, type, data = {} }: ComponentItem): void => {
|
||||
editorService.add({
|
||||
name: text,
|
||||
type,
|
||||
...data,
|
||||
});
|
||||
editorService.add(
|
||||
{
|
||||
name: text,
|
||||
type,
|
||||
...data,
|
||||
},
|
||||
undefined,
|
||||
{ historySource: 'component-panel' },
|
||||
);
|
||||
};
|
||||
|
||||
const dragstartHandler = ({ text, type, data = {} }: ComponentItem, e: DragEvent) => {
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editCode(`${data.key}`)"></Icon>
|
||||
</TMagicTooltip>
|
||||
<TMagicTooltip v-if="data.type === 'code' && editable" effect="dark" content="删除" placement="bottom">
|
||||
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}`)"></Icon>
|
||||
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}`, { historySource: 'tree' })"></Icon>
|
||||
</TMagicTooltip>
|
||||
<slot name="code-block-panel-tool" :id="data.key" :data="data"></slot>
|
||||
</template>
|
||||
@ -44,7 +44,7 @@ import Tree from '@editor/components/Tree.vue';
|
||||
import { useFilter } from '@editor/hooks/use-filter';
|
||||
import { useNodeStatus } from '@editor/hooks/use-node-status';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import { type CodeBlockListSlots, CodeDeleteErrorType, type TreeNodeData } from '@editor/type';
|
||||
import { type CodeBlockListSlots, CodeDeleteErrorType, HistoryOpSource, type TreeNodeData } from '@editor/type';
|
||||
|
||||
defineSlots<CodeBlockListSlots>();
|
||||
|
||||
@ -60,7 +60,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [id: string];
|
||||
remove: [id: string];
|
||||
remove: [id: string, { historySource?: HistoryOpSource }];
|
||||
'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
|
||||
}>();
|
||||
|
||||
@ -142,7 +142,7 @@ const editCode = (id: string) => {
|
||||
emit('edit', id);
|
||||
};
|
||||
|
||||
const deleteCode = async (id: string) => {
|
||||
const deleteCode = async (id: string, { historySource }: { historySource?: HistoryOpSource } = {}) => {
|
||||
const currentCode = codeList.value.find((codeItem) => codeItem.id === id);
|
||||
const existBinds = Boolean(currentCode?.items?.length);
|
||||
const undeleteableList = codeBlockService.getUndeletableList() || [];
|
||||
@ -154,7 +154,7 @@ const deleteCode = async (id: string) => {
|
||||
});
|
||||
|
||||
// 无绑定关系,且不在不可删除列表中
|
||||
emit('remove', id);
|
||||
emit('remove', id, { historySource });
|
||||
} else {
|
||||
if (typeof props.customError === 'function') {
|
||||
props.customError(id, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
|
||||
|
||||
@ -122,7 +122,7 @@ const {
|
||||
menuData: contentMenuData,
|
||||
contentMenuHideHandler,
|
||||
} = useContentMenu((id: string) => {
|
||||
codeBlockListRef.value?.deleteCode(id);
|
||||
codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
|
||||
});
|
||||
const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block'));
|
||||
</script>
|
||||
|
||||
@ -41,7 +41,7 @@ export const useContentMenu = (deleteCode: (id: string) => void) => {
|
||||
|
||||
const newCodeId = await codeBlockService.getUniqueId();
|
||||
|
||||
codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock));
|
||||
codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock), { historySource: 'tree-contextmenu' });
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -129,7 +129,7 @@ const removeHandler = async (id: string) => {
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
dataSourceService.remove(id);
|
||||
dataSourceService.remove(id, { historySource: 'tree-contextmenu' });
|
||||
};
|
||||
|
||||
const dataSourceListRef = useTemplateRef<InstanceType<typeof DataSourceList>>('dataSourceList');
|
||||
|
||||
@ -39,7 +39,7 @@ export const useContentMenu = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dataSourceService.add(cloneDeep(ds));
|
||||
dataSourceService.add(cloneDeep(ds), { historySource: 'tree-contextmenu' });
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -41,11 +41,15 @@ const createMenuItems = (group: ComponentGroup): MenuButton[] =>
|
||||
type: 'button',
|
||||
icon: component.icon,
|
||||
handler: () => {
|
||||
editorService.add({
|
||||
name: component.text,
|
||||
type: component.type,
|
||||
...(component.data || {}),
|
||||
});
|
||||
editorService.add(
|
||||
{
|
||||
name: component.text,
|
||||
type: component.type,
|
||||
...(component.data || {}),
|
||||
},
|
||||
undefined,
|
||||
{ historySource: 'tree-contextmenu' },
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
@ -57,9 +61,13 @@ const getSubMenuData = computed<MenuButton[]>(() => {
|
||||
type: 'button',
|
||||
icon: Files,
|
||||
handler: () => {
|
||||
editorService.add({
|
||||
type: 'tab-pane',
|
||||
});
|
||||
editorService.add(
|
||||
{
|
||||
type: 'tab-pane',
|
||||
},
|
||||
undefined,
|
||||
{ historySource: 'tree-contextmenu' },
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -106,9 +114,9 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
items: getSubMenuData.value,
|
||||
},
|
||||
useCopyMenu(),
|
||||
usePasteMenu(),
|
||||
useDeleteMenu(),
|
||||
useMoveToMenu(services),
|
||||
usePasteMenu('tree-contextmenu'),
|
||||
useDeleteMenu('tree-contextmenu'),
|
||||
useMoveToMenu(services, 'tree-contextmenu'),
|
||||
...props.layerContentMenu,
|
||||
],
|
||||
'layer',
|
||||
|
||||
@ -25,9 +25,12 @@ const props = defineProps<{
|
||||
const { editorService } = useServices();
|
||||
|
||||
const setNodeVisible = (visible: boolean) => {
|
||||
editorService.update({
|
||||
id: props.data.id,
|
||||
visible,
|
||||
});
|
||||
editorService.update(
|
||||
{
|
||||
id: props.data.id,
|
||||
visible,
|
||||
},
|
||||
{ historySource: 'tree' },
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -380,7 +380,7 @@ const dropHandler = async (e: DragEvent) => {
|
||||
|
||||
config.data.inputEvent = e;
|
||||
|
||||
editorService.add(config.data, parent);
|
||||
editorService.add(config.data, parent, { historySource: 'component-panel' });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -49,11 +49,11 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
display: () => canCenter.value,
|
||||
handler: () => {
|
||||
if (!nodes.value) return;
|
||||
editorService.alignCenter(nodes.value);
|
||||
editorService.alignCenter(nodes.value, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
},
|
||||
useCopyMenu(),
|
||||
usePasteMenu(menuRef),
|
||||
usePasteMenu('stage-contextmenu', menuRef),
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
@ -68,7 +68,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
icon: markRaw(Top),
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(1);
|
||||
editorService.moveLayer(1, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -77,7 +77,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
icon: markRaw(Bottom),
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(-1);
|
||||
editorService.moveLayer(-1, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -86,7 +86,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
icon: markRaw(Top),
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(LayerOffset.TOP);
|
||||
editorService.moveLayer(LayerOffset.TOP, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -95,16 +95,16 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
||||
icon: markRaw(Bottom),
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
handler: () => {
|
||||
editorService.moveLayer(LayerOffset.BOTTOM);
|
||||
editorService.moveLayer(LayerOffset.BOTTOM, { historySource: 'stage-contextmenu' });
|
||||
},
|
||||
},
|
||||
useMoveToMenu(services),
|
||||
useMoveToMenu(services, 'stage-contextmenu'),
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
display: () => !isPage(node.value) && !isPageFragment(node.value) && !props.isMultiSelect,
|
||||
},
|
||||
useDeleteMenu(),
|
||||
useDeleteMenu('stage-contextmenu'),
|
||||
{
|
||||
type: 'divider',
|
||||
direction: 'horizontal',
|
||||
|
||||
@ -120,9 +120,19 @@ class CodeBlock extends BaseService {
|
||||
public async setCodeDslById(
|
||||
id: Id,
|
||||
codeConfig: Partial<CodeBlockContent>,
|
||||
{ changeRecords, doNotPushHistory = false }: HistoryOpOptionsWithChangeRecords = {},
|
||||
{
|
||||
changeRecords,
|
||||
doNotPushHistory = false,
|
||||
historyDescription,
|
||||
historySource,
|
||||
}: HistoryOpOptionsWithChangeRecords = {},
|
||||
): Promise<void> {
|
||||
this.setCodeDslByIdSync(id, codeConfig, true, { changeRecords, doNotPushHistory });
|
||||
this.setCodeDslByIdSync(id, codeConfig, true, {
|
||||
changeRecords,
|
||||
doNotPushHistory,
|
||||
historyDescription,
|
||||
historySource,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,7 +151,12 @@ class CodeBlock extends BaseService {
|
||||
id: Id,
|
||||
codeConfig: Partial<CodeBlockContent>,
|
||||
force = true,
|
||||
{ changeRecords, doNotPushHistory = false, historyDescription }: HistoryOpOptionsWithChangeRecords = {},
|
||||
{
|
||||
changeRecords,
|
||||
doNotPushHistory = false,
|
||||
historyDescription,
|
||||
historySource,
|
||||
}: HistoryOpOptionsWithChangeRecords = {},
|
||||
): void {
|
||||
const codeDsl = this.getCodeDsl();
|
||||
|
||||
@ -172,7 +187,13 @@ class CodeBlock extends BaseService {
|
||||
const newContent = cloneDeep(codeDsl[id]);
|
||||
|
||||
if (!doNotPushHistory) {
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent, changeRecords, historyDescription });
|
||||
historyService.pushCodeBlock(id, {
|
||||
oldContent,
|
||||
newContent,
|
||||
changeRecords,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('addOrUpdate', id, codeDsl[id]);
|
||||
@ -268,7 +289,7 @@ class CodeBlock extends BaseService {
|
||||
*/
|
||||
public async deleteCodeDslByIds(
|
||||
codeIds: Id[],
|
||||
{ doNotPushHistory = false, historyDescription }: HistoryOpOptions = {},
|
||||
{ doNotPushHistory = false, historyDescription, historySource }: HistoryOpOptions = {},
|
||||
): Promise<void> {
|
||||
const currentDsl = await this.getCodeDsl();
|
||||
|
||||
@ -281,7 +302,7 @@ class CodeBlock extends BaseService {
|
||||
delete currentDsl[id];
|
||||
|
||||
if (oldContent && !doNotPushHistory) {
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription });
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription, source: historySource });
|
||||
}
|
||||
|
||||
this.emit('remove', id);
|
||||
@ -471,13 +492,13 @@ class CodeBlock extends BaseService {
|
||||
|
||||
// 原本是新增 → revert 即删除
|
||||
if (oldContent === null && newContent) {
|
||||
await this.deleteCodeDslByIds([id], { historyDescription });
|
||||
await this.deleteCodeDslByIds([id], { historyDescription, historySource: 'rollback' });
|
||||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
// 原本是删除 → revert 即写回
|
||||
if (oldContent && newContent === null) {
|
||||
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
|
||||
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription, historySource: 'rollback' });
|
||||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
@ -500,11 +521,12 @@ class CodeBlock extends BaseService {
|
||||
this.setCodeDslByIdSync(id, fallbackToFullReplace ? cloneDeep(oldContent) : patched, true, {
|
||||
changeRecords,
|
||||
historyDescription,
|
||||
historySource: 'rollback',
|
||||
});
|
||||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription });
|
||||
this.setCodeDslByIdSync(id, cloneDeep(oldContent), true, { historyDescription, historySource: 'rollback' });
|
||||
return historyService.getCodeBlockStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
|
||||
@ -129,7 +129,10 @@ class DataSource extends BaseService {
|
||||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
* @param options.historyDescription 入栈时附带的人类可读描述,用于历史面板展示
|
||||
*/
|
||||
public add(config: DataSourceSchema, { doNotPushHistory = false, historyDescription }: HistoryOpOptions = {}) {
|
||||
public add(
|
||||
config: DataSourceSchema,
|
||||
{ doNotPushHistory = false, historyDescription, historySource }: HistoryOpOptions = {},
|
||||
) {
|
||||
const newConfig = {
|
||||
...config,
|
||||
id: config.id && !this.getDataSourceById(config.id) ? config.id : this.createId(),
|
||||
@ -138,7 +141,12 @@ class DataSource extends BaseService {
|
||||
this.get('dataSources').push(newConfig);
|
||||
|
||||
if (!doNotPushHistory) {
|
||||
historyService.pushDataSource(newConfig.id, { oldSchema: null, newSchema: newConfig, historyDescription });
|
||||
historyService.pushDataSource(newConfig.id, {
|
||||
oldSchema: null,
|
||||
newSchema: newConfig,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('add', newConfig);
|
||||
@ -156,7 +164,12 @@ class DataSource extends BaseService {
|
||||
*/
|
||||
public update(
|
||||
config: DataSourceSchema,
|
||||
{ changeRecords = [], doNotPushHistory = false, historyDescription }: HistoryOpOptionsWithChangeRecords = {},
|
||||
{
|
||||
changeRecords = [],
|
||||
doNotPushHistory = false,
|
||||
historyDescription,
|
||||
historySource,
|
||||
}: HistoryOpOptionsWithChangeRecords = {},
|
||||
) {
|
||||
const dataSources = this.get('dataSources');
|
||||
|
||||
@ -173,6 +186,7 @@ class DataSource extends BaseService {
|
||||
newSchema: newConfig,
|
||||
changeRecords,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
});
|
||||
}
|
||||
|
||||
@ -191,14 +205,19 @@ class DataSource extends BaseService {
|
||||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
* @param options.historyDescription 入栈时附带的人类可读描述,用于历史面板展示
|
||||
*/
|
||||
public remove(id: string, { doNotPushHistory = false, historyDescription }: HistoryOpOptions = {}) {
|
||||
public remove(id: string, { doNotPushHistory = false, historyDescription, historySource }: HistoryOpOptions = {}) {
|
||||
const dataSources = this.get('dataSources');
|
||||
const index = dataSources.findIndex((ds) => ds.id === id);
|
||||
const oldConfig = index !== -1 ? dataSources[index] : null;
|
||||
dataSources.splice(index, 1);
|
||||
|
||||
if (oldConfig && !doNotPushHistory) {
|
||||
historyService.pushDataSource(id, { oldSchema: cloneDeep(oldConfig), newSchema: null, historyDescription });
|
||||
historyService.pushDataSource(id, {
|
||||
oldSchema: cloneDeep(oldConfig),
|
||||
newSchema: null,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
});
|
||||
}
|
||||
|
||||
this.emit('remove', id);
|
||||
@ -366,13 +385,13 @@ class DataSource extends BaseService {
|
||||
|
||||
// 原本是新增 → revert 即删除
|
||||
if (oldSchema === null && newSchema) {
|
||||
this.remove(`${id}`, { historyDescription });
|
||||
this.remove(`${id}`, { historyDescription, historySource: 'rollback' });
|
||||
return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
// 原本是删除 → revert 即重新加回
|
||||
if (oldSchema && newSchema === null) {
|
||||
this.add(cloneDeep(oldSchema), { historyDescription });
|
||||
this.add(cloneDeep(oldSchema), { historyDescription, historySource: 'rollback' });
|
||||
return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
@ -395,11 +414,12 @@ class DataSource extends BaseService {
|
||||
this.update(fallbackToFullReplace ? cloneDeep(oldSchema) : patched, {
|
||||
changeRecords,
|
||||
historyDescription,
|
||||
historySource: 'rollback',
|
||||
});
|
||||
return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
this.update(cloneDeep(oldSchema), { historyDescription });
|
||||
this.update(cloneDeep(oldSchema), { historyDescription, historySource: 'rollback' });
|
||||
return historyService.getDataSourceStepList(id).slice(-1)[0]?.step ?? null;
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ import type {
|
||||
DslOpOptions,
|
||||
EditorEvents,
|
||||
EditorNodeInfo,
|
||||
HistoryOpSource,
|
||||
HistoryOpType,
|
||||
PastePosition,
|
||||
StepValue,
|
||||
@ -406,7 +407,13 @@ class Editor extends BaseService {
|
||||
public async add(
|
||||
addNode: AddMNode | MNode[],
|
||||
parent?: MContainer | null,
|
||||
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription }: DslOpOptions = {},
|
||||
{
|
||||
doNotSelect = false,
|
||||
doNotSwitchPage = false,
|
||||
doNotPushHistory = false,
|
||||
historyDescription,
|
||||
historySource,
|
||||
}: DslOpOptions = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
@ -466,9 +473,8 @@ class Editor extends BaseService {
|
||||
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
||||
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
||||
if (!doNotPushHistory) {
|
||||
this.pushOpHistory(
|
||||
'add',
|
||||
{
|
||||
this.pushOpHistory('add', {
|
||||
extra: {
|
||||
nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
|
||||
parentId: (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id,
|
||||
indexMap: Object.fromEntries(
|
||||
@ -478,9 +484,10 @@ class Editor extends BaseService {
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
historyDescription,
|
||||
);
|
||||
source: historySource,
|
||||
});
|
||||
} else {
|
||||
this.selectionBeforeOp = null;
|
||||
}
|
||||
@ -577,7 +584,13 @@ class Editor extends BaseService {
|
||||
*/
|
||||
public async remove(
|
||||
nodeOrNodeList: MNode | MNode[],
|
||||
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false, historyDescription }: DslOpOptions = {},
|
||||
{
|
||||
doNotSelect = false,
|
||||
doNotSwitchPage = false,
|
||||
doNotPushHistory = false,
|
||||
historyDescription,
|
||||
historySource,
|
||||
}: DslOpOptions = {},
|
||||
): Promise<void> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
@ -606,7 +619,12 @@ class Editor extends BaseService {
|
||||
|
||||
if (removedItems.length > 0 && pageForOp) {
|
||||
if (!doNotPushHistory) {
|
||||
this.pushOpHistory('remove', { removedItems }, pageForOp, historyDescription);
|
||||
this.pushOpHistory('remove', {
|
||||
extra: { removedItems },
|
||||
pageData: pageForOp,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
});
|
||||
} else {
|
||||
this.selectionBeforeOp = null;
|
||||
}
|
||||
@ -704,11 +722,12 @@ class Editor extends BaseService {
|
||||
changeRecordList?: ChangeRecord[][];
|
||||
doNotPushHistory?: boolean;
|
||||
historyDescription?: string;
|
||||
historySource?: HistoryOpSource;
|
||||
} = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription } = data;
|
||||
const { doNotPushHistory = false, changeRecordList, changeRecords, historyDescription, historySource } = data;
|
||||
|
||||
const nodes = Array.isArray(config) ? config : [config];
|
||||
|
||||
@ -726,9 +745,8 @@ class Editor extends BaseService {
|
||||
if (curNodes.length) {
|
||||
if (!doNotPushHistory) {
|
||||
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
|
||||
this.pushOpHistory(
|
||||
'update',
|
||||
{
|
||||
this.pushOpHistory('update', {
|
||||
extra: {
|
||||
updatedItems: updateData.map((d) => ({
|
||||
oldNode: cloneDeep(d.oldNode),
|
||||
newNode: cloneDeep(toRaw(d.newNode)),
|
||||
@ -737,9 +755,10 @@ class Editor extends BaseService {
|
||||
changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
|
||||
})),
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
historyDescription,
|
||||
);
|
||||
source: historySource,
|
||||
});
|
||||
} else {
|
||||
this.selectionBeforeOp = null;
|
||||
}
|
||||
@ -763,7 +782,7 @@ class Editor extends BaseService {
|
||||
public async sort(
|
||||
id1: Id,
|
||||
id2: Id,
|
||||
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
||||
{ doNotSelect = false, doNotPushHistory = false, historySource }: DslOpOptions = {},
|
||||
): Promise<void> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
@ -783,7 +802,7 @@ class Editor extends BaseService {
|
||||
|
||||
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
||||
|
||||
await this.update(parent, { doNotPushHistory });
|
||||
await this.update(parent, { doNotPushHistory, historySource });
|
||||
if (!doNotSelect) {
|
||||
await this.select(node);
|
||||
}
|
||||
@ -836,7 +855,13 @@ class Editor extends BaseService {
|
||||
public async paste(
|
||||
position: PastePosition = {},
|
||||
collectorOptions?: TargetOptions,
|
||||
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false }: DslOpOptions = {},
|
||||
{
|
||||
doNotSelect = false,
|
||||
doNotSwitchPage = false,
|
||||
doNotPushHistory = false,
|
||||
historyDescription,
|
||||
historySource,
|
||||
}: DslOpOptions = {},
|
||||
): Promise<MNode | MNode[] | void> {
|
||||
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
|
||||
if (!Array.isArray(config)) return;
|
||||
@ -857,7 +882,13 @@ class Editor extends BaseService {
|
||||
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
|
||||
}
|
||||
|
||||
return this.add(pasteConfigs, parent, { doNotSelect, doNotSwitchPage, doNotPushHistory });
|
||||
return this.add(pasteConfigs, parent, {
|
||||
doNotSelect,
|
||||
doNotSwitchPage,
|
||||
doNotPushHistory,
|
||||
historyDescription,
|
||||
historySource,
|
||||
});
|
||||
}
|
||||
|
||||
public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
|
||||
@ -893,14 +924,14 @@ class Editor extends BaseService {
|
||||
*/
|
||||
public async alignCenter(
|
||||
config: MNode | MNode[],
|
||||
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
||||
{ doNotSelect = false, doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
const nodes = Array.isArray(config) ? config : [config];
|
||||
const stage = this.get('stage');
|
||||
|
||||
const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
|
||||
|
||||
const newNode = await this.update(newNodes, { doNotPushHistory });
|
||||
const newNode = await this.update(newNodes, { doNotPushHistory, historyDescription, historySource });
|
||||
|
||||
if (!doNotSelect) {
|
||||
if (newNodes.length > 1) {
|
||||
@ -919,7 +950,10 @@ class Editor extends BaseService {
|
||||
* @param options 可选配置
|
||||
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
*/
|
||||
public async moveLayer(offset: number | LayerOffset, { doNotPushHistory = false }: DslOpOptions = {}): Promise<void> {
|
||||
public async moveLayer(
|
||||
offset: number | LayerOffset,
|
||||
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
||||
): Promise<void> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const root = this.get('root');
|
||||
@ -960,10 +994,15 @@ class Editor extends BaseService {
|
||||
const pageForOp = this.getNodeInfo(node.id, false).page;
|
||||
this.pushOpHistory(
|
||||
'update',
|
||||
|
||||
{
|
||||
updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
|
||||
extra: {
|
||||
updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
|
||||
},
|
||||
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
);
|
||||
} else {
|
||||
this.selectionBeforeOp = null;
|
||||
@ -989,7 +1028,13 @@ class Editor extends BaseService {
|
||||
public async moveToContainer(
|
||||
config: MNode | MNode[],
|
||||
targetId: Id,
|
||||
{ doNotSelect = false, doNotSwitchPage = false, doNotPushHistory = false }: DslOpOptions = {},
|
||||
{
|
||||
doNotSelect = false,
|
||||
doNotSwitchPage = false,
|
||||
doNotPushHistory = false,
|
||||
historyDescription,
|
||||
historySource,
|
||||
}: DslOpOptions = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
const isBatch = Array.isArray(config);
|
||||
const configs = (isBatch ? config : [config]).filter((item) => !(isPage(item) || isPageFragment(item)));
|
||||
@ -1052,7 +1097,12 @@ class Editor extends BaseService {
|
||||
newNode: cloneDeep(toRaw(this.getNodeById(id, false))) as MNode,
|
||||
}));
|
||||
const historyPage = moves[0].pageForOp ?? { name: '', id: target.id };
|
||||
this.pushOpHistory('update', { updatedItems }, historyPage);
|
||||
this.pushOpHistory('update', {
|
||||
extra: { updatedItems },
|
||||
pageData: historyPage,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
});
|
||||
} else {
|
||||
this.selectionBeforeOp = null;
|
||||
}
|
||||
@ -1064,7 +1114,7 @@ class Editor extends BaseService {
|
||||
config: MNode | MNode[],
|
||||
targetParent: MContainer,
|
||||
targetIndex: number,
|
||||
{ doNotPushHistory = false }: DslOpOptions = {},
|
||||
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
||||
) {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
@ -1127,7 +1177,12 @@ class Editor extends BaseService {
|
||||
}
|
||||
if (!doNotPushHistory) {
|
||||
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
|
||||
this.pushOpHistory('update', { updatedItems }, { name: pageForOp?.name || '', id: pageForOp!.id });
|
||||
this.pushOpHistory('update', {
|
||||
extra: { updatedItems },
|
||||
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
});
|
||||
} else {
|
||||
this.selectionBeforeOp = null;
|
||||
}
|
||||
@ -1193,7 +1248,7 @@ class Editor extends BaseService {
|
||||
|
||||
const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
|
||||
// revert 走 public add/remove/update,让操作以一条普通新 step 入栈;不要切换选区与页面,避免打断用户。
|
||||
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription } as const;
|
||||
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription, historySource: 'rollback' } as const;
|
||||
|
||||
try {
|
||||
switch (step.opType) {
|
||||
@ -1241,7 +1296,7 @@ class Editor extends BaseService {
|
||||
return cloneDeep(oldNode);
|
||||
});
|
||||
if (configs.length) {
|
||||
await this.update(configs, { historyDescription });
|
||||
await this.update(configs, { historyDescription, historySource: 'rollback' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -1285,14 +1340,21 @@ class Editor extends BaseService {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public async move(left: number, top: number, { doNotPushHistory = false }: DslOpOptions = {}) {
|
||||
public async move(
|
||||
left: number,
|
||||
top: number,
|
||||
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
||||
) {
|
||||
const node = toRaw(this.get('node'));
|
||||
if (!node || isPage(node)) return;
|
||||
|
||||
const newStyle = calcMoveStyle(node.style || {}, left, top);
|
||||
if (!newStyle) return;
|
||||
|
||||
await this.update({ id: node.id, type: node.type, style: newStyle }, { doNotPushHistory });
|
||||
await this.update(
|
||||
{ id: node.id, type: node.type, style: newStyle },
|
||||
{ doNotPushHistory, historyDescription, historySource },
|
||||
);
|
||||
}
|
||||
|
||||
public resetState() {
|
||||
@ -1350,9 +1412,17 @@ class Editor extends BaseService {
|
||||
|
||||
private pushOpHistory(
|
||||
opType: HistoryOpType,
|
||||
extra: Partial<StepValue>,
|
||||
pageData: { name: string; id: Id },
|
||||
historyDescription?: string,
|
||||
{
|
||||
extra,
|
||||
pageData,
|
||||
historyDescription,
|
||||
source,
|
||||
}: {
|
||||
extra: Partial<StepValue>;
|
||||
pageData: { name: string; id: Id };
|
||||
historyDescription?: string;
|
||||
source?: HistoryOpSource;
|
||||
},
|
||||
) {
|
||||
const step: StepValue = {
|
||||
data: pageData,
|
||||
@ -1363,6 +1433,7 @@ class Editor extends BaseService {
|
||||
...extra,
|
||||
};
|
||||
if (historyDescription) step.historyDescription = historyDescription;
|
||||
if (source) step.source = source;
|
||||
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
|
||||
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
|
||||
historyService.push(step, pageData.id);
|
||||
|
||||
@ -27,6 +27,7 @@ import type {
|
||||
CodeBlockStepValue,
|
||||
DataSourceHistoryGroup,
|
||||
DataSourceStepValue,
|
||||
HistoryOpSource,
|
||||
HistoryState,
|
||||
PageHistoryGroup,
|
||||
PageHistoryStepEntry,
|
||||
@ -280,6 +281,8 @@ class History extends BaseService {
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 可选的人类可读描述(如「修改按钮颜色」),仅用于历史面板展示。 */
|
||||
historyDescription?: string;
|
||||
/** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
|
||||
source?: HistoryOpSource;
|
||||
},
|
||||
): CodeBlockStepValue | null {
|
||||
if (!codeBlockId) return null;
|
||||
@ -290,6 +293,7 @@ class History extends BaseService {
|
||||
newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
|
||||
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
|
||||
historyDescription: payload.historyDescription,
|
||||
source: payload.source,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
@ -310,6 +314,8 @@ class History extends BaseService {
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 可选的人类可读描述,仅用于历史面板展示。 */
|
||||
historyDescription?: string;
|
||||
/** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
|
||||
source?: HistoryOpSource;
|
||||
},
|
||||
): DataSourceStepValue | null {
|
||||
if (!dataSourceId) return null;
|
||||
@ -320,6 +326,7 @@ class History extends BaseService {
|
||||
newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
|
||||
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
|
||||
historyDescription: payload.historyDescription,
|
||||
source: payload.source,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ class Keybinding extends BaseService {
|
||||
const nodes = editorService.get('nodes');
|
||||
|
||||
if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
|
||||
editorService.remove(nodes);
|
||||
editorService.remove(nodes, { historySource: 'shortcut' });
|
||||
},
|
||||
[KeyBindingCommand.COPY_NODE]: () => {
|
||||
const nodes = editorService.get('nodes');
|
||||
@ -31,11 +31,11 @@ class Keybinding extends BaseService {
|
||||
|
||||
if (!nodes || isPage(nodes[0]) || isPageFragment(nodes[0])) return;
|
||||
editorService.copy(nodes);
|
||||
editorService.remove(nodes);
|
||||
editorService.remove(nodes, { historySource: 'shortcut' });
|
||||
},
|
||||
[KeyBindingCommand.PASTE_NODE]: () => {
|
||||
const nodes = editorService.get('nodes');
|
||||
nodes && editorService.paste({ offsetX: 10, offsetY: 10 });
|
||||
nodes && editorService.paste({ offsetX: 10, offsetY: 10 }, undefined, { historySource: 'shortcut' });
|
||||
},
|
||||
[KeyBindingCommand.UNDO]: () => {
|
||||
editorService.undo();
|
||||
|
||||
@ -281,6 +281,20 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 「操作途径」徽标:浅灰描边胶囊,弱化展示来源(画布 / 图层 / 配置面板…),不抢占描述焦点。
|
||||
.m-editor-history-list-item-source {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
white-space: nowrap;
|
||||
font-weight: 400; // 防止被合并组头部的粗体继承
|
||||
}
|
||||
|
||||
// 「合并 N 步」徽标:紫色实心胶囊,与合并组卡片色系一致,醒目区分单步条目。
|
||||
.m-editor-history-list-item-merge {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@ -510,7 +510,7 @@ export interface HistoryListExtraTab {
|
||||
* - data-source: 数据源,按 `type`(base/http/...) 从 dataSourceService 获取数据源表单配置
|
||||
* - code-block: 数据源代码块,使用内置的代码块表单配置
|
||||
*/
|
||||
export type CompareCategory = 'node' | 'data-source' | 'code-block';
|
||||
export type CompareCategory = 'node' | 'data-source' | 'code-block' | string;
|
||||
|
||||
/**
|
||||
* 自定义 `loadConfig` 时回传的上下文,聚合了组件当前的对比入参,
|
||||
@ -683,6 +683,44 @@ export interface CodeParamStatement {
|
||||
export type HistoryOpType = 'add' | 'remove' | 'update';
|
||||
// #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 =
|
||||
| 'stage'
|
||||
| 'tree'
|
||||
| 'component-panel'
|
||||
| 'props'
|
||||
| 'code'
|
||||
| 'stage-contextmenu'
|
||||
| 'tree-contextmenu'
|
||||
| 'toolbar'
|
||||
| 'shortcut'
|
||||
| 'rollback'
|
||||
| 'api'
|
||||
| 'ai'
|
||||
| 'unknown'
|
||||
| (string & {});
|
||||
// #endregion HistoryOpSource
|
||||
|
||||
// #region StepValue
|
||||
export interface StepValue {
|
||||
/** 页面信息 */
|
||||
@ -713,6 +751,12 @@ export interface StepValue {
|
||||
* 不影响 undo/redo 行为;缺省时面板会根据节点 / propPath 自动生成描述。
|
||||
*/
|
||||
historyDescription?: string;
|
||||
/**
|
||||
* 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource}
|
||||
* (画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等)。
|
||||
* 仅用于历史面板展示与业务埋点,不影响 undo/redo 行为;缺省时面板视为「未知」。
|
||||
*/
|
||||
source?: HistoryOpSource;
|
||||
/**
|
||||
* 入栈时间戳(毫秒)。在 historyService.push 时自动写入(若调用方未指定),仅用于历史面板展示。
|
||||
*/
|
||||
@ -741,6 +785,8 @@ export interface CodeBlockStepValue {
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 调用方可选传入的人类可读描述,用于历史面板展示;不影响 undo/redo 行为。 */
|
||||
historyDescription?: string;
|
||||
/** 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource};仅用于历史面板展示与埋点,不影响 undo/redo 行为。 */
|
||||
source?: HistoryOpSource;
|
||||
/** 入栈时间戳(毫秒),入栈时自动写入,仅用于历史面板展示。 */
|
||||
timestamp?: number;
|
||||
}
|
||||
@ -767,6 +813,8 @@ export interface DataSourceStepValue {
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 调用方可选传入的人类可读描述,用于历史面板展示;不影响 undo/redo 行为。 */
|
||||
historyDescription?: string;
|
||||
/** 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource};仅用于历史面板展示与埋点,不影响 undo/redo 行为。 */
|
||||
source?: HistoryOpSource;
|
||||
/** 入栈时间戳(毫秒),入栈时自动写入,仅用于历史面板展示。 */
|
||||
timestamp?: number;
|
||||
}
|
||||
@ -1099,16 +1147,21 @@ export const canUsePluginMethods = {
|
||||
|
||||
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)。
|
||||
@ -1116,7 +1169,9 @@ export interface HistoryOpOptions {
|
||||
export interface HistoryOpOptionsWithChangeRecords extends HistoryOpOptions {
|
||||
changeRecords?: ChangeRecord[];
|
||||
}
|
||||
// #endregion HistoryOpOptionsWithChangeRecords
|
||||
|
||||
// #region DslOpOptions
|
||||
/**
|
||||
* DSL 修改类操作的通用配置
|
||||
* - doNotSelect: 操作后是否不要自动触发选中(不调用 this.select / this.multiSelect / stage.select / stage.multiSelect)
|
||||
@ -1126,6 +1181,7 @@ export interface DslOpOptions extends HistoryOpOptions {
|
||||
doNotSelect?: boolean;
|
||||
doNotSwitchPage?: boolean;
|
||||
}
|
||||
// #endregion DslOpOptions
|
||||
|
||||
/** 差异对话框的入参 */
|
||||
export interface DiffDialogPayload {
|
||||
|
||||
@ -5,11 +5,16 @@ import { cloneDeep, Id, MContainer, NodeType } from '@tmagic/core';
|
||||
import { calcValueByFontsize, isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import ContentMenu from '@editor/components/ContentMenu.vue';
|
||||
import type { MenuButton, Services } from '@editor/type';
|
||||
import type { HistoryOpSource, MenuButton, Services } from '@editor/type';
|
||||
|
||||
import { COPY_STORAGE_KEY } from './editor';
|
||||
|
||||
export const useDeleteMenu = (): MenuButton => ({
|
||||
/**
|
||||
* 共享的右键菜单项构造器(画布 ViewerMenu 与图层树 LayerMenu 共用)。
|
||||
* `historySource` 用于标记本次操作的途径,调用方按所在面板传入:
|
||||
* 画布传 `'stage-contextmenu'`,树形面板传 `'tree-contextmenu'`。
|
||||
*/
|
||||
export const useDeleteMenu = (historySource?: HistoryOpSource): MenuButton => ({
|
||||
type: 'button',
|
||||
text: '删除',
|
||||
icon: Delete,
|
||||
@ -19,7 +24,7 @@ export const useDeleteMenu = (): MenuButton => ({
|
||||
},
|
||||
handler: ({ editorService }) => {
|
||||
const nodes = editorService.get('nodes');
|
||||
nodes && editorService.remove(nodes);
|
||||
nodes && editorService.remove(nodes, { historySource });
|
||||
},
|
||||
});
|
||||
|
||||
@ -33,7 +38,10 @@ export const useCopyMenu = (): MenuButton => ({
|
||||
},
|
||||
});
|
||||
|
||||
export const usePasteMenu = (menu?: ShallowRef<InstanceType<typeof ContentMenu> | null>): MenuButton => ({
|
||||
export const usePasteMenu = (
|
||||
historySource?: HistoryOpSource,
|
||||
menu?: ShallowRef<InstanceType<typeof ContentMenu> | null>,
|
||||
): MenuButton => ({
|
||||
type: 'button',
|
||||
text: '粘贴',
|
||||
icon: markRaw(DocumentCopy),
|
||||
@ -52,14 +60,14 @@ export const usePasteMenu = (menu?: ShallowRef<InstanceType<typeof ContentMenu>
|
||||
const initialTop =
|
||||
calcValueByFontsize(stage?.renderer?.getDocument(), (rect.top || 0) - (parentRect?.top || 0)) /
|
||||
uiService.get('zoom');
|
||||
editorService.paste({ left: initialLeft, top: initialTop });
|
||||
editorService.paste({ left: initialLeft, top: initialTop }, undefined, { historySource });
|
||||
} else {
|
||||
editorService.paste();
|
||||
editorService.paste(undefined, undefined, { historySource });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const moveTo = async (id: Id, { editorService }: Services) => {
|
||||
const moveTo = async (id: Id, { editorService }: Services, historySource?: HistoryOpSource) => {
|
||||
const nodes = editorService.get('nodes') || [];
|
||||
const parent = editorService.getNodeById(id) as MContainer;
|
||||
|
||||
@ -69,10 +77,11 @@ const moveTo = async (id: Id, { editorService }: Services) => {
|
||||
// 不要再走 remove + add 两步,否则会被切成两条历史(且语义也不正确)。
|
||||
await editorService.moveToContainer(cloneDeep(nodes), parent.id, {
|
||||
doNotSwitchPage: true,
|
||||
historySource,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMoveToMenu = ({ editorService }: Services): MenuButton => {
|
||||
export const useMoveToMenu = ({ editorService }: Services, historySource?: HistoryOpSource): MenuButton => {
|
||||
const root = computed(() => editorService.get('root'));
|
||||
|
||||
return {
|
||||
@ -89,7 +98,7 @@ export const useMoveToMenu = ({ editorService }: Services): MenuButton => {
|
||||
text: `${page.name}(${page.id})`,
|
||||
type: 'button',
|
||||
handler: (services: Services) => {
|
||||
moveTo(page.id, services);
|
||||
moveTo(page.id, services, historySource);
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
@ -104,7 +104,7 @@ describe('useCodeBlockEdit', () => {
|
||||
const deleteCodeDslByIds = vi.fn();
|
||||
const hook = mountHook({ deleteCodeDslByIds });
|
||||
await hook.deleteCode('k');
|
||||
expect(deleteCodeDslByIds).toHaveBeenCalledWith(['k']);
|
||||
expect(deleteCodeDslByIds).toHaveBeenCalledWith(['k'], { historySource: undefined });
|
||||
});
|
||||
|
||||
test('submitCodeBlockHandler - 没有 codeId 时跳过', async () => {
|
||||
@ -119,7 +119,14 @@ describe('useCodeBlockEdit', () => {
|
||||
const hook = mountHook({ setCodeDslById });
|
||||
hook.codeId.value = 'id1';
|
||||
await hook.submitCodeBlockHandler({ name: 'b' } as any);
|
||||
expect(setCodeDslById).toHaveBeenCalledWith('id1', { name: 'b' }, { changeRecords: undefined });
|
||||
expect(setCodeDslById).toHaveBeenCalledWith(
|
||||
'id1',
|
||||
{ name: 'b' },
|
||||
{
|
||||
changeRecords: undefined,
|
||||
historySource: 'props',
|
||||
},
|
||||
);
|
||||
expect(hideMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -129,6 +136,13 @@ describe('useCodeBlockEdit', () => {
|
||||
hook.codeId.value = 'id1';
|
||||
const records = [{ propPath: 'name', value: 'b' }];
|
||||
await hook.submitCodeBlockHandler({ name: 'b' } as any, { changeRecords: records } as any);
|
||||
expect(setCodeDslById).toHaveBeenCalledWith('id1', { name: 'b' }, { changeRecords: records });
|
||||
expect(setCodeDslById).toHaveBeenCalledWith(
|
||||
'id1',
|
||||
{ name: 'b' },
|
||||
{
|
||||
changeRecords: records,
|
||||
historySource: 'props',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -235,7 +235,7 @@ describe('useStage', () => {
|
||||
test('sort 事件', () => {
|
||||
useStage({} as any);
|
||||
stageInstance.handlers.sort[0]({ src: 'a', dist: 'b' });
|
||||
expect(editorService.sort).toHaveBeenCalledWith('a', 'b');
|
||||
expect(editorService.sort).toHaveBeenCalledWith('a', 'b', { historySource: 'stage' });
|
||||
});
|
||||
|
||||
test('remove 事件', () => {
|
||||
|
||||
@ -97,7 +97,9 @@ describe('ComponentListPanel', () => {
|
||||
test('点击 component-item 调用 editorService.add', async () => {
|
||||
const wrapper = mount(ComponentListPanel);
|
||||
await wrapper.find('.component-item').trigger('click');
|
||||
expect(editorService.add).toHaveBeenCalledWith({ name: '按钮', type: 'button' });
|
||||
expect(editorService.add).toHaveBeenCalledWith({ name: '按钮', type: 'button' }, undefined, {
|
||||
historySource: 'component-panel',
|
||||
});
|
||||
});
|
||||
|
||||
test('搜索过滤组件', async () => {
|
||||
|
||||
@ -70,7 +70,13 @@ describe('code-block useContentMenu', () => {
|
||||
setCodeDslById: vi.fn(),
|
||||
};
|
||||
await (result.menuData[1] as any).handler({ codeBlockService });
|
||||
expect(codeBlockService.setCodeDslById).toHaveBeenCalledWith('newId', { name: 'a' });
|
||||
expect(codeBlockService.setCodeDslById).toHaveBeenCalledWith(
|
||||
'newId',
|
||||
{ name: 'a' },
|
||||
{
|
||||
historySource: 'tree-contextmenu',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('复制按钮: 未选中时不触发', async () => {
|
||||
|
||||
@ -187,7 +187,7 @@ describe('DataSourceListPanel', () => {
|
||||
await wrapper.find('.remove-btn').trigger('click');
|
||||
expect(messageBoxConfirm).toHaveBeenCalled();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
expect(dataSourceService.remove).toHaveBeenCalledWith('d1');
|
||||
expect(dataSourceService.remove).toHaveBeenCalledWith('d1', { historySource: 'tree-contextmenu' });
|
||||
await wrapper.find('.ctx-btn').trigger('click');
|
||||
expect(nodeContentMenuHandler).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -67,7 +67,7 @@ describe('data-source useContentMenu', () => {
|
||||
add: vi.fn(),
|
||||
};
|
||||
(result.menuData[1] as any).handler({ dataSourceService });
|
||||
expect(dataSourceService.add).toHaveBeenCalledWith({ name: 'a' });
|
||||
expect(dataSourceService.add).toHaveBeenCalledWith({ name: 'a' }, { historySource: 'tree-contextmenu' });
|
||||
});
|
||||
|
||||
test('复制按钮: 未选中时不触发', () => {
|
||||
|
||||
@ -117,7 +117,9 @@ describe('LayerMenu', () => {
|
||||
const addItem = arg.find((m: any) => m.text === '新增');
|
||||
expect(addItem.items[0].text).toBe('标签页');
|
||||
addItem.items[0].handler();
|
||||
expect(editorService.add).toHaveBeenCalledWith({ type: 'tab-pane' });
|
||||
expect(editorService.add).toHaveBeenCalledWith({ type: 'tab-pane' }, undefined, {
|
||||
historySource: 'tree-contextmenu',
|
||||
});
|
||||
});
|
||||
|
||||
test('node.items 时根据组件列表生成子菜单 (含分隔)', () => {
|
||||
@ -151,6 +153,8 @@ describe('LayerMenu', () => {
|
||||
const arg = customContentMenu.mock.calls[0][0];
|
||||
const addItem = arg.find((m: any) => m.text === '新增');
|
||||
addItem.items[0].handler();
|
||||
expect(editorService.add).toHaveBeenCalledWith({ name: 'btn', type: 'button' });
|
||||
expect(editorService.add).toHaveBeenCalledWith({ name: 'btn', type: 'button' }, undefined, {
|
||||
historySource: 'tree-contextmenu',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -39,7 +39,7 @@ describe('LayerNodeTool', () => {
|
||||
props: { data: { id: 'n1', type: 'text', visible: true } as any },
|
||||
});
|
||||
await wrapper.find('button').trigger('click');
|
||||
expect(editorService.update).toHaveBeenCalledWith({ id: 'n1', visible: false });
|
||||
expect(editorService.update).toHaveBeenCalledWith({ id: 'n1', visible: false }, { historySource: 'tree' });
|
||||
});
|
||||
|
||||
test('点击按钮切换 visible 状态 (false -> true)', async () => {
|
||||
@ -48,6 +48,6 @@ describe('LayerNodeTool', () => {
|
||||
props: { data: { id: 'n2', type: 'text', visible: false } as any },
|
||||
});
|
||||
await wrapper.find('button').trigger('click');
|
||||
expect(editorService.update).toHaveBeenCalledWith({ id: 'n2', visible: true });
|
||||
expect(editorService.update).toHaveBeenCalledWith({ id: 'n2', visible: true }, { historySource: 'tree' });
|
||||
});
|
||||
});
|
||||
|
||||
@ -126,9 +126,9 @@ describe('ViewerMenu.vue', () => {
|
||||
});
|
||||
const menuData = wrapper.findComponent({ name: 'FakeContentMenu' }).props('menuData') as any[];
|
||||
menuData.find((m: any) => m.text === '上移一层').handler();
|
||||
expect(editorService.moveLayer).toHaveBeenCalledWith(1);
|
||||
expect(editorService.moveLayer).toHaveBeenCalledWith(1, { historySource: 'stage-contextmenu' });
|
||||
menuData.find((m: any) => m.text === '下移一层').handler();
|
||||
expect(editorService.moveLayer).toHaveBeenCalledWith(-1);
|
||||
expect(editorService.moveLayer).toHaveBeenCalledWith(-1, { historySource: 'stage-contextmenu' });
|
||||
menuData.find((m: any) => m.text === '置顶').handler();
|
||||
menuData.find((m: any) => m.text === '置底').handler();
|
||||
expect(editorService.moveLayer).toHaveBeenCalledTimes(4);
|
||||
|
||||
@ -114,9 +114,13 @@ describe('content-menu utils', () => {
|
||||
toJSON: () => ({}),
|
||||
});
|
||||
const menu = ref<any>({ $el: menuEl });
|
||||
const m = usePasteMenu(menu);
|
||||
const m = usePasteMenu('stage-contextmenu', menu);
|
||||
(m as any).handler({ editorService, uiService: { get: () => 2 } });
|
||||
expect(paste).toHaveBeenCalledWith(expect.objectContaining({ left: expect.any(Number), top: expect.any(Number) }));
|
||||
expect(paste).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ left: expect.any(Number), top: expect.any(Number) }),
|
||||
undefined,
|
||||
{ historySource: 'stage-contextmenu' },
|
||||
);
|
||||
});
|
||||
|
||||
test('useMoveToMenu - display 行为校验', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user