mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-10 01:12:01 +00:00
Compare commits
9 Commits
v1.8.0-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614f12adf3 | ||
|
|
bddc6f343c | ||
|
|
be3a900e6a | ||
|
|
bc555ebdc0 | ||
|
|
b7d1cea7c1 | ||
|
|
3bd0eecb42 | ||
|
|
cd19dec790 | ||
|
|
10b70c36bb | ||
|
|
27b2c2c685 |
16
CHANGELOG.md
16
CHANGELOG.md
@ -1,3 +1,19 @@
|
||||
# [1.8.0-beta.4](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.3...v1.8.0-beta.4) (2026-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 修复历史对比样式配置显示 ([cd19dec](https://github.com/Tencent/tmagic-editor/commit/cd19dec7907cac5cff775f1cbde24cb3f384e87b))
|
||||
* **editor:** 修复合并历史记录信息展示 ([3bd0eec](https://github.com/Tencent/tmagic-editor/commit/3bd0eecb42d06f06f50cc4736ecc31cc07cc1886))
|
||||
* **editor:** 禁止缺少变更记录的历史回滚 ([10b70c3](https://github.com/Tencent/tmagic-editor/commit/10b70c36bbace6af48bf6fa63f2df0704c6861af))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor:** 历史记录支持操作来源 ([27b2c2c](https://github.com/Tencent/tmagic-editor/commit/27b2c2c68598264e97a1e1ecc34121829851c85e))
|
||||
|
||||
|
||||
|
||||
# [1.8.0-beta.3](https://github.com/Tencent/tmagic-editor/compare/v1.8.0-beta.2...v1.8.0-beta.3) (2026-06-04)
|
||||
|
||||
|
||||
|
||||
@ -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>}`
|
||||
@ -226,6 +235,86 @@
|
||||
`newContent=null` 的删除记录;不存在的 id 不会入历史。传入 `doNotPushHistory: true` 也可显式跳过写入历史栈。
|
||||
:::
|
||||
|
||||
## setCodeDslByIdAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [setCodeDslById](#setcodedslbyid)
|
||||
|
||||
- **返回:**
|
||||
- {`Promise<string | null>`} 本次写入历史记录的 uuid;未写入历史(`doNotPushHistory: true` 等)时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [setCodeDslById](#setcodedslbyid) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
|
||||
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
import { codeBlockService } from "@tmagic/editor";
|
||||
|
||||
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", {
|
||||
name: "代码块1",
|
||||
content: "() => {}",
|
||||
});
|
||||
console.log(historyId); // 本次变更对应的历史记录 uuid,或 null
|
||||
```
|
||||
|
||||
## setCodeDslByIdSyncAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [setCodeDslByIdSync](#setcodedslbyidsync)
|
||||
|
||||
- **返回:**
|
||||
- {`string | null`} 本次写入历史记录的 uuid;未写入历史(`doNotPushHistory: true`、或 `force=false` 跳过等)时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [setCodeDslByIdSync](#setcodedslbyidsync) 行为完全一致(同步),仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## deleteCodeDslByIdsAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [deleteCodeDslByIds](#deletecodedslbyids)
|
||||
|
||||
- **返回:**
|
||||
- {`Promise<string[]>`} 本次写入的全部历史记录 uuid(按删除顺序);未写入任何历史时返回空数组 `[]`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [deleteCodeDslByIds](#deletecodedslbyids) 行为完全一致。由于一次可删除多个代码块、会产生多条历史记录,因此返回的是 uuid 数组(每条删除记录一个 uuid);不存在的 id 不会入历史,也不会出现在返回数组中。
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
import { codeBlockService } from "@tmagic/editor";
|
||||
|
||||
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(["code_1", "code_2"]);
|
||||
console.log(historyIds); // ['xxxx', 'yyyy'],或 []
|
||||
```
|
||||
|
||||
## revertById
|
||||
|
||||
- **参数:**
|
||||
- `{string}` uuid 目标历史记录的 uuid(通常由 [setCodeDslByIdAndGetHistoryId](#setcodedslbyidandgethistoryid) 等方法返回)
|
||||
|
||||
- **返回:**
|
||||
- {`Promise<CodeBlockStepValue | null>`} 反向应用后产生的新 step;找不到对应 uuid / 该步未应用时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
通过历史记录 uuid「回滚」某条代码块历史步骤(类 git revert 语义),语义同按 `(id, index)` 回滚,
|
||||
仅无需调用方再传 `codeBlockId` 与 `index`:内部会按 uuid 在全部代码块栈中定位对应步骤后再回滚。
|
||||
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
import { codeBlockService } from "@tmagic/editor";
|
||||
|
||||
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId("code_1234", { name: "代码块1" });
|
||||
if (historyId) {
|
||||
await codeBlockService.revertById(historyId);
|
||||
}
|
||||
```
|
||||
|
||||
## undo
|
||||
|
||||
- **参数:**
|
||||
|
||||
@ -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}`
|
||||
@ -400,6 +406,78 @@ import { dataSourceService } from "@tmagic/editor";
|
||||
dataSourceService.remove("ds_123");
|
||||
```
|
||||
|
||||
## addAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [add](#add)
|
||||
|
||||
- **返回:**
|
||||
- {`string` | null} 本次写入历史记录的 uuid;未写入历史(`doNotPushHistory: true` 等)时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,可用于精确引用 / 定位该条历史记录。
|
||||
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
import { dataSourceService } from "@tmagic/editor";
|
||||
|
||||
const historyId = dataSourceService.addAndGetHistoryId({
|
||||
type: "http",
|
||||
title: "用户信息",
|
||||
url: "/api/user",
|
||||
});
|
||||
console.log(historyId); // 本次新增对应的历史记录 uuid,或 null
|
||||
```
|
||||
|
||||
## updateAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [update](#update)
|
||||
|
||||
- **返回:**
|
||||
- {`string` | null} 本次写入历史记录的 uuid;未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## removeAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [remove](#remove)
|
||||
|
||||
- **返回:**
|
||||
- {`string` | null} 本次写入历史记录的 uuid;删除的 id 不存在或未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## revertById
|
||||
|
||||
- **参数:**
|
||||
- `{string}` uuid 目标历史记录的 uuid(通常由 [addAndGetHistoryId](#addandgethistoryid) 等方法返回)
|
||||
|
||||
- **返回:**
|
||||
- {`DataSourceStepValue` | null} 反向应用后产生的新 step;找不到对应 uuid / 该步未应用时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
通过历史记录 uuid「回滚」某条数据源历史步骤(类 git revert 语义),语义同按 `(id, index)` 回滚,
|
||||
仅无需调用方再传 `dataSourceId` 与 `index`:内部会按 uuid 在全部数据源栈中定位对应步骤后再回滚。
|
||||
参见 [editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
import { dataSourceService } from "@tmagic/editor";
|
||||
|
||||
const historyId = dataSourceService.addAndGetHistoryId({ type: "http", title: "用户信息" });
|
||||
if (historyId) {
|
||||
dataSourceService.revertById(historyId);
|
||||
}
|
||||
```
|
||||
|
||||
## createId
|
||||
|
||||
- **[扩展支持](../../guide/editor-expand#行为扩展):** 是
|
||||
|
||||
@ -1,5 +1,48 @@
|
||||
# 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`),便于历史面板区分来源。
|
||||
|
||||
## 历史记录 uuid 与 \*AndGetHistoryId
|
||||
|
||||
每条历史记录入栈时都会自动生成一个唯一标识 `uuid`(见 [StepValue](#undo)),可用于精确引用 / 定位某一条历史记录(如埋点、回滚、跨端同步等)。
|
||||
|
||||
DSL 操作方法(`add` / `remove` / `update` 等)默认返回操作结果(节点 / 节点集合 / void),不会返回 `uuid`。若需要拿到本次写入历史记录的 `uuid`,可改用对应的 `*AndGetHistoryId` 方法:它们与原方法行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`(`string`)。当本次操作未写入历史(`doNotPushHistory: true`、无实际变更或提前返回)时返回 `null`。
|
||||
|
||||
| 原方法 | 取 uuid 的方法 | 返回值 |
|
||||
| --- | --- | --- |
|
||||
| [add](#add) | [addAndGetHistoryId](#addandgethistoryid) | `Promise<string \| null>` |
|
||||
| [remove](#remove) | [removeAndGetHistoryId](#removeandgethistoryid) | `Promise<string \| null>` |
|
||||
| [update](#update) | [updateAndGetHistoryId](#updateandgethistoryid) | `Promise<string \| null>` |
|
||||
| [moveLayer](#movelayer) | [moveLayerAndGetHistoryId](#movelayerandgethistoryid) | `Promise<string \| null>` |
|
||||
| [moveToContainer](#movetocontainer) | [moveToContainerAndGetHistoryId](#movetocontainerandgethistoryid) | `Promise<string \| null>` |
|
||||
| [dragTo](#dragto) | [dragToAndGetHistoryId](#dragtoandgethistoryid) | `Promise<string \| null>` |
|
||||
|
||||
[dataSourceService](./dataSourceServiceMethods.md) / [codeBlockService](./codeBlockServiceMethods.md) 也提供了同名约定的 `*AndGetHistoryId` 方法。
|
||||
|
||||
拿到 `uuid` 后,可在需要时按 uuid「回滚」对应的历史记录(类 git revert 语义,详见[历史记录面板](../../guide/advanced/history-list.md))。相比按 index 回滚,uuid 不会随栈内步骤增删而变化,更适合业务侧持有引用后再回滚:
|
||||
|
||||
- 页面:[editorService.revertPageStepById(uuid)](#revertpagestepbyid)
|
||||
- 数据源:[dataSourceService.revertById(uuid)](./dataSourceServiceMethods.md#revertbyid)
|
||||
- 代码块:[codeBlockService.revertById(uuid)](./codeBlockServiceMethods.md#revertbyid)
|
||||
|
||||
::: 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 +402,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 +450,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 +506,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 +549,7 @@ editorService.highlight("text_123");
|
||||
- `{boolean}` doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
||||
- `{boolean}` doNotSwitchPage 排序后是否不切换当前页面(排序只发生在同一父节点内,方法内为空操作;保留以与其它 DSL 操作 API 一致)
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -568,6 +618,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 +658,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 +682,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- `{number | 'top' | 'bottom'}` offset
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -649,6 +705,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 +723,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- `{number}` targetIndex 目标位置索引
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotPushHistory 是否不写入历史记录(默认 false)
|
||||
- `{string}` historyDescription 见[历史记录相关 options](#历史记录相关-options)
|
||||
- `{HistoryOpSource}` historySource 见[历史记录相关 options](#历史记录相关-options)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -673,6 +733,115 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
|
||||
将节点(支持多选)拖拽到目标容器的指定位置,会自动处理跨容器布局切换并记录历史
|
||||
|
||||
## addAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [add](#add)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [add](#add) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`,见[历史记录 uuid 与 \*AndGetHistoryId](#历史记录-uuid-与-andgethistoryid)
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
import { editorService } from "@tmagic/editor";
|
||||
|
||||
const historyId = await editorService.addAndGetHistoryId(
|
||||
{ type: "text", text: "hello" },
|
||||
parent,
|
||||
{ historySource: "api" },
|
||||
);
|
||||
console.log(historyId); // 本次新增对应的历史记录 uuid,或 null
|
||||
```
|
||||
|
||||
## removeAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [remove](#remove)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [remove](#remove) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## updateAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [update](#update)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [update](#update) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## moveLayerAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [moveLayer](#movelayer)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [moveLayer](#movelayer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## moveToContainerAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [moveToContainer](#movetocontainer)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [moveToContainer](#movetocontainer) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## dragToAndGetHistoryId
|
||||
|
||||
- **参数:** 同 [dragTo](#dragto)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`string` | null>} 本次写入历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid);未写入历史时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
与 [dragTo](#dragto) 行为完全一致,仅把返回值换成本次写入历史记录的 `uuid`
|
||||
|
||||
## revertPageStepById
|
||||
|
||||
- **参数:**
|
||||
- `{string}` uuid 目标历史记录的 [uuid](#历史记录-uuid-与-andgethistoryid)(通常由 `*AndGetHistoryId` 方法返回)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`StepValue` | null>} 反向应用后产生的新 step;找不到对应 uuid / 该步未应用 / 反向失败时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
通过历史记录 uuid「回滚」当前页面的某条历史步骤(类 git revert 语义):不移动游标、不丢弃任何步骤,而是把目标 step 的修改**反向应用为一条全新的步骤**压入栈顶。语义与按 index 回滚一致,仅入参从 index 改为 uuid,更适合业务侧持有引用后再回滚。
|
||||
|
||||
::: tip
|
||||
`opType: 'update'` 的步骤必须携带 `changeRecords` 才支持回滚(否则只能整节点替换,会冲掉后续无关变更);未应用(已被撤销)的步骤无法回滚。
|
||||
:::
|
||||
|
||||
- **示例:**
|
||||
|
||||
```js
|
||||
import { editorService } from "@tmagic/editor";
|
||||
|
||||
// 执行操作时拿到本次历史记录 uuid
|
||||
const historyId = await editorService.addAndGetHistoryId({ type: "text", text: "hello" });
|
||||
|
||||
// 之后任意时机按 uuid 回滚该步骤
|
||||
if (historyId) {
|
||||
await editorService.revertPageStepById(historyId);
|
||||
}
|
||||
```
|
||||
|
||||
## undo
|
||||
|
||||
- **[扩展支持](../../guide/editor-expand#行为扩展):** 是
|
||||
@ -685,6 +854,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 +870,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 +893,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}
|
||||
:::
|
||||
|
||||
@ -67,3 +73,36 @@
|
||||
- 删除触发的 step 中 `newSchema` 为 `null`
|
||||
- `undo` / `redo` 返回 `null`(边界状态)时不会触发该事件
|
||||
:::
|
||||
|
||||
## mark-saved
|
||||
|
||||
- **详情:** 调用 `markSaved` / `markPageSaved` / `markCodeBlockSaved` / `markDataSourceSaved` 标记「已保存」记录时触发
|
||||
|
||||
- **事件回调函数:** `(payload: { kind: 'all' | 'page' | 'code-block' | 'data-source'; id?: Id }) => void`
|
||||
|
||||
::: tip
|
||||
- `markSaved` 触发时 `kind` 为 `all`,无 `id`
|
||||
- 细粒度方法触发时 `kind` 对应类别,`id` 为目标页面 / 代码块 / 数据源 id
|
||||
:::
|
||||
|
||||
## save-to-indexed-db
|
||||
|
||||
- **详情:** `saveToIndexedDB` 把历史记录写入本地 IndexedDB 成功时触发
|
||||
|
||||
- **事件回调函数:** `(snapshot: PersistedHistoryState) => void`
|
||||
|
||||
::: details 查看 PersistedHistoryState 类型定义
|
||||
<<< @/../packages/editor/src/type.ts#PersistedHistoryState{ts}
|
||||
|
||||
<<< @/../packages/editor/src/utils/undo-redo.ts#SerializedUndoRedo{ts}
|
||||
:::
|
||||
|
||||
## restore-from-indexed-db
|
||||
|
||||
- **详情:** `restoreFromIndexedDB` 从本地 IndexedDB 读取并重建历史记录成功时触发(找不到记录时不触发)
|
||||
|
||||
- **事件回调函数:** `(snapshot: PersistedHistoryState) => void`
|
||||
|
||||
::: details 查看 PersistedHistoryState 类型定义
|
||||
<<< @/../packages/editor/src/type.ts#PersistedHistoryState{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,14 @@
|
||||
`opType: 'update'` 的每个 `updatedItems[i]` 上可携带 `changeRecords`,用于撤销 / 重做时仅按
|
||||
`propPath` 局部更新对应字段,避免整节点替换冲掉同节点上的其它无关变更;不带
|
||||
`changeRecords` 时退化为整节点替换(如 `sort` / `moveLayer` / 拖动等纯快照场景)。
|
||||
|
||||
`StepValue` 上的 `historyDescription` / `source` 仅用于历史面板展示与埋点,不影响 undo/redo 行为。
|
||||
|
||||
入栈时会为每条记录自动生成唯一标识 `uuid`(调用方未指定时),可用于精确引用 / 定位某一条历史记录。
|
||||
若需要在执行 DSL 操作后拿到本次写入记录的 `uuid`,可使用 editorService / dataSourceService /
|
||||
codeBlockService 提供的 `*AndGetHistoryId` 方法,参见
|
||||
[editorService 历史记录 uuid 与 \*AndGetHistoryId](./editorServiceMethods.md#历史记录-uuid-与-andgethistoryid)。
|
||||
`pushCodeBlock` / `pushDataSource` 同样会自动写入 `uuid`。
|
||||
:::
|
||||
|
||||
## undo
|
||||
@ -91,10 +101,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 +186,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}
|
||||
:::
|
||||
|
||||
@ -242,6 +260,122 @@
|
||||
|
||||
指定数据源当前是否可重做。栈不存在时返回 `false`。
|
||||
|
||||
## markSaved
|
||||
|
||||
- **详情:**
|
||||
|
||||
标记「整份 DSL 已保存」:把页面 / 代码块 / 数据源所有栈当前游标所在的记录都标记为已保存(`saved = true`)。
|
||||
|
||||
同一栈内任意时刻最多保留一条已保存记录(标记前会清除该栈内全部旧标记);某个栈处于「全部已撤销」(cursor 为 0)时不会留下已保存记录,从 IndexedDB 恢复时其游标会回到 0。
|
||||
|
||||
通常在 DSL 整体落库(保存到后端 / 本地)成功后调用,配合 [`restoreFromIndexedDB`](#restorefromindexeddb) 把游标恢复到此处。仅保存了其中一类时请改用更细粒度的 `markPageSaved` / `markCodeBlockSaved` / `markDataSourceSaved`。
|
||||
|
||||
调用后会触发 `mark-saved` 事件(`{ kind: 'all' }`)。
|
||||
|
||||
## markPageSaved
|
||||
|
||||
- **参数:**
|
||||
- `{Id} pageId` 可选;缺省为当前活动页
|
||||
|
||||
- **详情:**
|
||||
|
||||
标记指定页面(缺省当前活动页)历史栈的当前记录为已保存,仅影响该页面自己的栈。触发 `mark-saved` 事件(`{ kind: 'page', id }`)。
|
||||
|
||||
## markCodeBlockSaved
|
||||
|
||||
- **参数:**
|
||||
- `{Id} codeBlockId`
|
||||
|
||||
- **详情:**
|
||||
|
||||
标记指定代码块历史栈的当前记录为已保存,仅影响该代码块自己的栈。触发 `mark-saved` 事件(`{ kind: 'code-block', id }`)。
|
||||
|
||||
## markDataSourceSaved
|
||||
|
||||
- **参数:**
|
||||
- `{Id} dataSourceId`
|
||||
|
||||
- **详情:**
|
||||
|
||||
标记指定数据源历史栈的当前记录为已保存,仅影响该数据源自己的栈。触发 `mark-saved` 事件(`{ kind: 'data-source', id }`)。
|
||||
|
||||
## clearPage
|
||||
|
||||
- **参数:**
|
||||
- `{Id} pageId` 可选;缺省为当前活动页
|
||||
|
||||
- **详情:**
|
||||
|
||||
清空指定页面(缺省当前活动页)的历史记录栈。仅删除撤销/重做记录,不会改动当前 DSL;清空后该页将无法再撤销/重做之前的操作。清空当前活动页时会同步刷新 `canUndo` / `canRedo` 并触发 `change` 事件。
|
||||
|
||||
## clearCodeBlock
|
||||
|
||||
- **参数:**
|
||||
- `{Id} codeBlockId` 可选;缺省清空全部代码块
|
||||
|
||||
- **详情:**
|
||||
|
||||
清空代码块历史记录栈:传入 `codeBlockId` 仅清空该代码块,缺省清空全部代码块。仅删除撤销/重做记录,不会改动代码块本身。
|
||||
|
||||
## clearDataSource
|
||||
|
||||
- **参数:**
|
||||
- `{Id} dataSourceId` 可选;缺省清空全部数据源
|
||||
|
||||
- **详情:**
|
||||
|
||||
清空数据源历史记录栈:传入 `dataSourceId` 仅清空该数据源,缺省清空全部数据源。仅删除撤销/重做记录,不会改动数据源本身。
|
||||
|
||||
## saveToIndexedDB
|
||||
|
||||
- **参数:**
|
||||
- `{HistoryPersistOptions} options` 可选
|
||||
|
||||
::: details 查看 HistoryPersistOptions / PersistedHistoryState 类型定义
|
||||
<<< @/../packages/editor/src/type.ts#HistoryPersistOptions{ts}
|
||||
|
||||
<<< @/../packages/editor/src/type.ts#PersistedHistoryState{ts}
|
||||
|
||||
<<< @/../packages/editor/src/utils/undo-redo.ts#SerializedUndoRedo{ts}
|
||||
:::
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<PersistedHistoryState>}` 写入成功的快照对象
|
||||
|
||||
- **详情:**
|
||||
|
||||
把当前内存中的全部历史栈(页面 / 代码块 / 数据源)连同各自游标、容量序列化后写入本地 IndexedDB。
|
||||
|
||||
- 最终库名为 `${dbName}-${当前 DSL app id}`,按应用隔离;
|
||||
- `key` 用于在同一 store 下区分不同记录,缺省为 `default`;
|
||||
- 历史记录里可能包含函数(代码块内容 / 节点事件等),内部使用 `serialize-javascript` 序列化为字符串后写入,恢复时再用 `parseDSL` 还原,因此可安全持久化函数 / `Map` 等;
|
||||
- 不支持 IndexedDB 的环境(如 SSR)会 reject。
|
||||
|
||||
写入成功后触发 `save-to-indexed-db` 事件。
|
||||
|
||||
::: warning
|
||||
`beforeunload` / `pagehide` 阶段浏览器不会等待异步 IndexedDB 事务提交,单纯依赖卸载时写入可能丢失最近一次编辑。建议在历史变更时(防抖)即调用本方法持久化,确保刷新后能完整恢复。
|
||||
:::
|
||||
|
||||
## restoreFromIndexedDB
|
||||
|
||||
- **参数:**
|
||||
- `{HistoryPersistOptions} options` 可选
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<PersistedHistoryState | null>}` 找不到记录时返回 `null`
|
||||
|
||||
- **详情:**
|
||||
|
||||
从本地 IndexedDB 读取此前保存的历史快照并重建全部撤销/重做栈。
|
||||
|
||||
- 每个栈都会按 `listMaxSize` 裁剪并还原游标;
|
||||
- 若某个栈存在已保存记录(见 `markSaved`),其游标会被定位到「最近一条已保存记录」之后,使恢复后的状态与落库的 DSL 对齐;
|
||||
- 会整体覆盖当前内存中的历史状态,并把活动页恢复为快照中的 `pageId`;
|
||||
- 找不到对应记录时返回 `null` 且不改动当前状态;不支持 IndexedDB 的环境会 reject。
|
||||
|
||||
成功后触发 `restore-from-indexed-db` 与 `change` 事件。
|
||||
|
||||
## destroy
|
||||
|
||||
- **详情:**
|
||||
|
||||
@ -61,6 +61,12 @@ const menu = ref({
|
||||
- 数据源:`dataSourceService.revert(id, index)`
|
||||
- 代码块:`codeBlockService.revert(id, index)`
|
||||
|
||||
如果业务侧在执行操作时已通过 `*AndGetHistoryId` 拿到了该条记录的 [uuid](/api/editor/editorServiceMethods.md#历史记录-uuid-与-andgethistoryid),也可以直接按 uuid 回滚(无需再关心 index / id,且 uuid 不会随栈内步骤增删而变化):
|
||||
|
||||
- 页面:`editorService.revertPageStepById(uuid)`
|
||||
- 数据源:`dataSourceService.revertById(uuid)`
|
||||
- 代码块:`codeBlockService.revertById(uuid)`
|
||||
|
||||
### 4. 差异对比
|
||||
|
||||
在前后值都存在的 `update` 步骤上提供「查看差异」入口,点击后弹出差异对话框。对话框支持两个维度的切换:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "tmagic",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/cli",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/core",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/data-source",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/dep",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/design",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/editor",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -152,6 +152,24 @@ const showDiff = ({ curValue, lastValue, config }: { curValue: any; lastValue: a
|
||||
return !isEqual(curValue, lastValue);
|
||||
};
|
||||
|
||||
const removeStyleDisplayConfig = (formConfig: FormConfig): FormConfig =>
|
||||
formConfig.map((item) => {
|
||||
if (!('type' in item)) return item;
|
||||
if (item?.type !== 'tab' || !Array.isArray(item.items)) return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
items: item.items.map((tabPane) => {
|
||||
if (tabPane?.title !== '样式' || !Array.isArray(tabPane.items)) return tabPane;
|
||||
|
||||
return {
|
||||
...tabPane,
|
||||
display: true,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 内置的默认 FormConfig 加载逻辑:按 `category` 从对应 service / 工具取配置。
|
||||
* 作为 ctx.defaultLoadConfig 透传给自定义 `loadConfig`,方便复用与二次加工。
|
||||
@ -162,7 +180,9 @@ const defaultLoadConfig = async (): Promise<FormConfig> => {
|
||||
if (!props.type) {
|
||||
return [];
|
||||
}
|
||||
return await propsService.getPropsConfig(props.type);
|
||||
return removeStyleDisplayConfig(
|
||||
await propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
|
||||
);
|
||||
}
|
||||
case 'data-source': {
|
||||
return dataSourceService.getFormConfig(props.type || 'base');
|
||||
|
||||
@ -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,17 +15,20 @@
|
||||
: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"
|
||||
:sub-steps="
|
||||
group.steps.map((s: any) => ({
|
||||
group.steps.map((s) => ({
|
||||
index: s.index,
|
||||
applied: s.applied,
|
||||
isCurrent: s.isCurrent,
|
||||
saved: s.step.saved,
|
||||
desc: describeStep(s.step),
|
||||
diffable: isStepDiffable ? isStepDiffable(s.step) : false,
|
||||
revertable: s.applied,
|
||||
revertable: s.applied && (isStepRevertable ? isStepRevertable(s.step) : true),
|
||||
source: s.step.source,
|
||||
time: formatHistoryTime(s.step.timestamp),
|
||||
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
||||
}))
|
||||
@ -53,12 +56,13 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T extends BaseStepValue = BaseStepValue">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { HistoryOpType } from '@editor/type';
|
||||
import type { BaseStepValue } from '@editor/type';
|
||||
|
||||
import { formatHistoryFullTime, formatHistoryTime, groupTimestamp } from './composables';
|
||||
import type { HistoryBucketGroup } from './composables';
|
||||
import { formatHistoryFullTime, formatHistoryTime, groupSource, groupTimestamp } from './composables';
|
||||
import GroupRow from './GroupRow.vue';
|
||||
import InitialRow from './InitialRow.vue';
|
||||
|
||||
@ -80,18 +84,15 @@ const props = withDefaults(
|
||||
/** 是否展示底部「回到初始状态」入口,默认 true。无 undo cursor 语义的自定义历史可传 false 关闭。 */
|
||||
showInitial?: boolean;
|
||||
/** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
|
||||
groups: {
|
||||
applied: boolean;
|
||||
isCurrent?: boolean;
|
||||
opType: HistoryOpType;
|
||||
steps: { index: number; applied: boolean; isCurrent?: boolean; step: any }[];
|
||||
}[];
|
||||
groups: HistoryBucketGroup<T>[];
|
||||
/** 组级描述文案生成器,接收一个 group,返回展示文本。由父组件按业务类型注入。 */
|
||||
describeGroup: (_group: any) => string;
|
||||
/** 单步描述文案生成器,接收一个 step,返回展示文本。用于合并组展开后的子步列表。 */
|
||||
describeStep: (_step: any) => string;
|
||||
describeStep: (_step: T) => string;
|
||||
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
|
||||
isStepDiffable?: (_step: any) => boolean;
|
||||
isStepDiffable?: (_step: T) => boolean;
|
||||
/** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。由父组件按业务类型注入;不传则已应用即可回滚。 */
|
||||
isStepRevertable?: (_step: T) => boolean;
|
||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
||||
expanded: Record<string, boolean>;
|
||||
/** 是否支持「跳转到该记录」(goto)。默认 true。 */
|
||||
|
||||
@ -1,31 +1,40 @@
|
||||
<template>
|
||||
<div v-if="!buckets.length" class="m-editor-history-list-empty">暂无操作记录</div>
|
||||
<TMagicScrollbar v-else max-height="360px">
|
||||
<Bucket
|
||||
v-for="bucket in buckets"
|
||||
:key="`${prefix}-${bucket.id}`"
|
||||
:title="title"
|
||||
:bucket-id="bucket.id"
|
||||
:prefix="prefix"
|
||||
:groups="bucket.groups"
|
||||
:describe-group="describeGroup"
|
||||
:describe-step="describeStep"
|
||||
:is-step-diffable="isStepDiffable"
|
||||
:expanded="expanded"
|
||||
:goto-enabled="gotoEnabled"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(id: string | number, index: number) => $emit('goto', id, index)"
|
||||
@goto-initial="(id: string | number) => $emit('goto-initial', id)"
|
||||
@diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
|
||||
@revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
|
||||
/>
|
||||
</TMagicScrollbar>
|
||||
<template v-else>
|
||||
<div class="m-editor-history-list-toolbar">
|
||||
<span class="m-editor-history-list-clear" :title="`清空${title}的历史记录`" @click="$emit('clear')">清空</span>
|
||||
</div>
|
||||
<TMagicScrollbar max-height="360px">
|
||||
<Bucket
|
||||
v-for="bucket in buckets"
|
||||
:key="`${prefix}-${bucket.id}`"
|
||||
:title="title"
|
||||
:bucket-id="bucket.id"
|
||||
:prefix="prefix"
|
||||
:groups="bucket.groups"
|
||||
:describe-group="describeGroup"
|
||||
:describe-step="describeStep"
|
||||
:is-step-diffable="isStepDiffable"
|
||||
:is-step-revertable="isStepRevertable"
|
||||
:expanded="expanded"
|
||||
:goto-enabled="gotoEnabled"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(id: string | number, index: number) => $emit('goto', id, index)"
|
||||
@goto-initial="(id: string | number) => $emit('goto-initial', id)"
|
||||
@diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
|
||||
@revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
|
||||
/>
|
||||
</TMagicScrollbar>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="T extends BaseStepValue = BaseStepValue">
|
||||
import { TMagicScrollbar } from '@tmagic/design';
|
||||
|
||||
import type { BaseStepValue } from '@editor/type';
|
||||
|
||||
import Bucket from './Bucket.vue';
|
||||
import type { HistoryBucketGroup } from './composables';
|
||||
|
||||
defineOptions({
|
||||
name: 'MEditorHistoryListBucketTab',
|
||||
@ -41,13 +50,15 @@ withDefaults(
|
||||
* 已按目标 id 聚拢成的 bucket 列表,每个 bucket 内部的 groups 已按时间倒序排好。
|
||||
* 空数组时显示空态。
|
||||
*/
|
||||
buckets: { id: string | number; groups: any[] }[];
|
||||
buckets: { id: string | number; groups: HistoryBucketGroup<T>[] }[];
|
||||
/** 组级描述文案生成器,由父组件按业务类型注入。 */
|
||||
describeGroup: (_group: any) => string;
|
||||
/** 单步描述文案生成器,由父组件按业务类型注入。 */
|
||||
describeStep: (_step: any) => string;
|
||||
describeStep: (_step: T) => string;
|
||||
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入。 */
|
||||
isStepDiffable: (_step: any) => boolean;
|
||||
isStepDiffable: (_step: T) => boolean;
|
||||
/** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。由父组件按业务类型注入;不传则已应用即可回滚。 */
|
||||
isStepRevertable?: (_step: T) => boolean;
|
||||
/**
|
||||
* 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
|
||||
* 本 tab 使用 `${prefix}-${id}-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
||||
@ -73,5 +84,7 @@ defineEmits<{
|
||||
(_e: 'diff-step', _targetId: string | number, _index: number): void;
|
||||
/** 透传 Bucket 的 revert-step 事件,携带目标 id 与 step 索引(类 git revert)。 */
|
||||
(_e: 'revert-step', _targetId: string | number, _index: number): void;
|
||||
/** 用户点击"清空"按钮,请求清空该类(数据源 / 代码块)的全部历史记录(由上层弹窗二次确认后执行)。 */
|
||||
(_e: 'clear'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@ -13,7 +13,16 @@
|
||||
<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="time" class="m-editor-history-list-item-time" :title="timeTitle || time">{{ time }}</span>
|
||||
<span v-if="headSaved" class="m-editor-history-list-item-saved" title="该记录为最近一次保存的状态">已保存</span>
|
||||
|
||||
<span
|
||||
v-if="!merged && sourceLabel(source)"
|
||||
class="m-editor-history-list-item-source"
|
||||
:title="`操作途径:${sourceLabel(source)}`"
|
||||
>{{ sourceLabel(source) }}</span
|
||||
>
|
||||
|
||||
<span v-if="!merged && 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 +59,13 @@
|
||||
>
|
||||
<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="s.saved" class="m-editor-history-list-item-saved" title="该记录为最近一次保存的状态">已保存</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 +96,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 +116,8 @@ const props = withDefaults(
|
||||
opType: HistoryOpType;
|
||||
/** 组的整体描述文案,由上层根据 step / group 计算后传入,例如 "修改 button · style.color"。 */
|
||||
desc: string;
|
||||
/** 组的操作途径(一般取组内最近一步),用于头部展示「画布 / 树面板 / 配置面板…」标签。 */
|
||||
source?: HistoryOpSource;
|
||||
/** 组头部展示的时间文案(一般为组内最近一步的时间),为空时不渲染。 */
|
||||
time?: string;
|
||||
/** 组头部时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
|
||||
@ -112,9 +130,13 @@ const props = withDefaults(
|
||||
applied: boolean;
|
||||
desc: string;
|
||||
isCurrent?: boolean;
|
||||
/** 该子步是否为最近一次保存的记录,用于展示「已保存」标记。 */
|
||||
saved?: boolean;
|
||||
diffable?: boolean;
|
||||
/** 是否可对该子步执行「回滚」(已应用 + 业务侧确认支持反向)。父级根据 step 与 applied 决定。 */
|
||||
revertable?: boolean;
|
||||
/** 该子步的操作途径,用于展示「画布 / 树面板 / 配置面板…」标签。 */
|
||||
source?: HistoryOpSource;
|
||||
/** 该子步的时间文案,为空时不渲染。 */
|
||||
time?: string;
|
||||
/** 该子步时间的 title 悬浮提示(完整时间),缺省时回退为 time。 */
|
||||
@ -196,6 +218,15 @@ const subStepTitle = (s: { isCurrent?: boolean }) => {
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* 头部是否展示「已保存」标记:
|
||||
* - 单步组:取该唯一子步的 saved;
|
||||
* - 合并组:组内任一子步为已保存即在头部提示(具体落在哪一步可展开查看)。
|
||||
*/
|
||||
const headSaved = computed(() =>
|
||||
props.merged ? props.subSteps.some((s) => s.saved) : Boolean(props.subSteps[0]?.saved),
|
||||
);
|
||||
|
||||
/** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
|
||||
const headDiffable = computed(() => !props.merged && Boolean(props.subSteps[0]?.diffable));
|
||||
|
||||
|
||||
@ -193,7 +193,8 @@ const targetText = computed(() => {
|
||||
'data-source': '数据源',
|
||||
'code-block': '代码块',
|
||||
};
|
||||
const prefix = categoryText[payload.value.category] || '';
|
||||
const { category } = payload.value;
|
||||
const prefix = category ? categoryText[category] : '';
|
||||
const label = payload.value.targetLabel || payload.value.type || '';
|
||||
const { id } = payload.value;
|
||||
const labelWithId = id !== undefined && id !== '' ? `${label}(${id})` : label;
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
@goto-initial="onPageGotoInitial"
|
||||
@diff-step="onPageDiff"
|
||||
@revert-step="onPageRevert"
|
||||
@clear="onPageClear"
|
||||
/>
|
||||
</component>
|
||||
|
||||
@ -44,11 +45,13 @@
|
||||
:describe-group="describeDataSourceGroup"
|
||||
:describe-step="describeDataSourceStep"
|
||||
:is-step-diffable="isDataSourceStepDiffable"
|
||||
:is-step-revertable="isDataSourceStepRevertable"
|
||||
@toggle="toggleGroup"
|
||||
@goto="onDataSourceGoto"
|
||||
@goto-initial="onDataSourceGotoInitial"
|
||||
@diff-step="onDataSourceDiff"
|
||||
@revert-step="onDataSourceRevert"
|
||||
@clear="onDataSourceClear"
|
||||
/>
|
||||
</component>
|
||||
|
||||
@ -65,11 +68,13 @@
|
||||
:describe-group="describeCodeBlockGroup"
|
||||
:describe-step="describeCodeBlockStep"
|
||||
:is-step-diffable="isCodeBlockStepDiffable"
|
||||
:is-step-revertable="isCodeBlockStepRevertable"
|
||||
@toggle="toggleGroup"
|
||||
@goto="onCodeBlockGoto"
|
||||
@goto-initial="onCodeBlockGotoInitial"
|
||||
@diff-step="onCodeBlockDiff"
|
||||
@revert-step="onCodeBlockRevert"
|
||||
@clear="onCodeBlockClear"
|
||||
/>
|
||||
</component>
|
||||
|
||||
@ -128,7 +133,14 @@
|
||||
import { computed, inject, markRaw, ref, shallowRef, useTemplateRef, watch } from 'vue';
|
||||
import { Clock, Close } from '@element-plus/icons-vue';
|
||||
|
||||
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
||||
import {
|
||||
getDesignConfig,
|
||||
TMagicButton,
|
||||
tMagicMessageBox,
|
||||
TMagicPopover,
|
||||
TMagicTabs,
|
||||
TMagicTooltip,
|
||||
} from '@tmagic/design';
|
||||
import type { FormState } from '@tmagic/form';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
@ -141,6 +153,8 @@ import {
|
||||
describeCodeBlockStep,
|
||||
describeDataSourceGroup,
|
||||
describeDataSourceStep,
|
||||
isCodeBlockStepRevertable,
|
||||
isDataSourceStepRevertable,
|
||||
useHistoryList,
|
||||
} from './composables';
|
||||
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
||||
@ -377,4 +391,60 @@ const onCodeBlockRevert = (id: string | number, index: number) => {
|
||||
const onDiffDialogClose = () => {
|
||||
onConfirmRevert.value = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 「清空历史记录」入口:先弹出二次确认,确认后清空对应类别的历史栈。
|
||||
* 仅删除撤销/重做记录,不会改动当前 DSL / 数据源 / 代码块本身。
|
||||
* 用户取消(confirm reject)时静默忽略。
|
||||
*/
|
||||
const confirmClear = async (message: string): Promise<boolean> => {
|
||||
try {
|
||||
await tMagicMessageBox.confirm(message, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
return true;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 把内存中(已清空对应类别后的)历史状态重新写回 IndexedDB,
|
||||
* 使本地持久化的那份与内存保持一致——即「连同本地保存的一并删除」。
|
||||
* 不支持 IndexedDB 或写入失败时静默忽略(内存清空已生效)。
|
||||
*/
|
||||
const syncIndexedDB = async () => {
|
||||
try {
|
||||
await historyService.saveToIndexedDB();
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
} catch (e) {
|
||||
// ignore: 内存清空已生效,本地同步失败不阻塞交互
|
||||
}
|
||||
};
|
||||
|
||||
const onPageClear = async () => {
|
||||
if (
|
||||
await confirmClear('确定清空当前页面的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')
|
||||
) {
|
||||
historyService.clearPage();
|
||||
await syncIndexedDB();
|
||||
}
|
||||
};
|
||||
|
||||
const onDataSourceClear = async () => {
|
||||
if (await confirmClear('确定清空数据源的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')) {
|
||||
historyService.clearDataSource();
|
||||
await syncIndexedDB();
|
||||
}
|
||||
};
|
||||
|
||||
const onCodeBlockClear = async () => {
|
||||
if (await confirmClear('确定清空代码块的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')) {
|
||||
historyService.clearCodeBlock();
|
||||
await syncIndexedDB();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,44 +1,52 @@
|
||||
<template>
|
||||
<div v-if="!list.length" class="m-editor-history-list-empty">暂无操作记录</div>
|
||||
<TMagicScrollbar v-else max-height="360px">
|
||||
<ul class="m-editor-history-list-ul">
|
||||
<GroupRow
|
||||
v-for="group in list"
|
||||
:key="`pg-${group.steps[0]?.index}`"
|
||||
:group-key="`pg-${group.steps[0]?.index}`"
|
||||
:applied="group.applied"
|
||||
:merged="group.steps.length > 1"
|
||||
:op-type="group.opType"
|
||||
:desc="describePageGroup(group)"
|
||||
:time="formatHistoryTime(groupTimestamp(group))"
|
||||
:time-title="formatHistoryFullTime(groupTimestamp(group))"
|
||||
:step-count="group.steps.length"
|
||||
:sub-steps="
|
||||
group.steps.map((s) => ({
|
||||
index: s.index,
|
||||
applied: s.applied,
|
||||
isCurrent: s.isCurrent,
|
||||
desc: describePageStep(s.step),
|
||||
diffable: isPageStepDiffable(s.step),
|
||||
revertable: s.applied,
|
||||
time: formatHistoryTime(s.step.timestamp),
|
||||
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
||||
}))
|
||||
"
|
||||
:is-current="group.isCurrent"
|
||||
:expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(index: number) => $emit('goto', index)"
|
||||
@diff-step="(index: number) => $emit('diff-step', index)"
|
||||
@revert-step="(index: number) => $emit('revert-step', index)"
|
||||
/>
|
||||
<!--
|
||||
<template v-else>
|
||||
<div class="m-editor-history-list-toolbar">
|
||||
<span class="m-editor-history-list-clear" title="清空当前页面的历史记录" @click="$emit('clear')">清空</span>
|
||||
</div>
|
||||
<TMagicScrollbar max-height="360px">
|
||||
<ul class="m-editor-history-list-ul">
|
||||
<GroupRow
|
||||
v-for="group in list"
|
||||
:key="`pg-${group.steps[0]?.index}`"
|
||||
:group-key="`pg-${group.steps[0]?.index}`"
|
||||
:applied="group.applied"
|
||||
: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"
|
||||
:sub-steps="
|
||||
group.steps.map((s) => ({
|
||||
index: s.index,
|
||||
applied: s.applied,
|
||||
isCurrent: s.isCurrent,
|
||||
saved: s.step.saved,
|
||||
desc: describePageStep(s.step),
|
||||
diffable: isPageStepDiffable(s.step),
|
||||
revertable: s.applied && isPageStepRevertable(s.step),
|
||||
source: s.step.source,
|
||||
time: formatHistoryTime(s.step.timestamp),
|
||||
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
||||
}))
|
||||
"
|
||||
:is-current="group.isCurrent"
|
||||
:expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
|
||||
@toggle="(key: string) => $emit('toggle', key)"
|
||||
@goto="(index: number) => $emit('goto', index)"
|
||||
@diff-step="(index: number) => $emit('diff-step', index)"
|
||||
@revert-step="(index: number) => $emit('revert-step', index)"
|
||||
/>
|
||||
<!--
|
||||
初始状态项:永远位于列表底部(页面 tab 倒序展示,最底部=最早),
|
||||
作为"未修改"零点。当所有 group 都未 applied 时它即为当前位置。
|
||||
-->
|
||||
<InitialRow :is-current="isInitial" @goto-initial="$emit('goto-initial')" />
|
||||
</ul>
|
||||
</TMagicScrollbar>
|
||||
<InitialRow :is-current="isInitial" @goto-initial="$emit('goto-initial')" />
|
||||
</ul>
|
||||
</TMagicScrollbar>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -53,7 +61,9 @@ import {
|
||||
describePageStep,
|
||||
formatHistoryFullTime,
|
||||
formatHistoryTime,
|
||||
groupSource,
|
||||
groupTimestamp,
|
||||
isPageStepRevertable,
|
||||
} from './composables';
|
||||
import GroupRow from './GroupRow.vue';
|
||||
import InitialRow from './InitialRow.vue';
|
||||
@ -84,6 +94,8 @@ defineEmits<{
|
||||
(_e: 'diff-step', _index: number): void;
|
||||
/** 用户点击"回滚"按钮,携带目标 step 在栈中的索引,类 git revert。 */
|
||||
(_e: 'revert-step', _index: number): void;
|
||||
/** 用户点击"清空"按钮,请求清空当前页面的历史记录(由上层弹窗二次确认后执行)。 */
|
||||
(_e: 'clear'): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
|
||||
@ -4,15 +4,32 @@ import { datetimeFormatter } from '@tmagic/form';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import type {
|
||||
BaseStepValue,
|
||||
CodeBlockHistoryGroup,
|
||||
CodeBlockStepValue,
|
||||
DataSourceHistoryGroup,
|
||||
DataSourceStepValue,
|
||||
HistoryOpSource,
|
||||
HistoryOpType,
|
||||
PageHistoryGroup,
|
||||
StepValue,
|
||||
} from '@editor/type';
|
||||
|
||||
/**
|
||||
* 通用 bucket 分组(数据源 / 代码块及业务自定义历史)在面板中的展示结构。
|
||||
* 由 Bucket / BucketTab 复用,step 类型通过泛型 T 收窄(约束为 {@link BaseStepValue})。
|
||||
*/
|
||||
export interface HistoryBucketGroup<T extends BaseStepValue = BaseStepValue> {
|
||||
/** 组内最后一步是否已应用 */
|
||||
applied: boolean;
|
||||
/** 是否为当前所在的分组 */
|
||||
isCurrent?: boolean;
|
||||
/** 该分组的操作类型 */
|
||||
opType: HistoryOpType;
|
||||
/** 组内所有步骤 */
|
||||
steps: { index: number; applied: boolean; isCurrent?: boolean; step: T }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史记录面板共享逻辑:
|
||||
* - 暴露三类历史的聚合数据(页面 / 数据源 / 代码块);
|
||||
@ -107,6 +124,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 ?? ''}`;
|
||||
|
||||
@ -224,3 +267,36 @@ export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
|
||||
const target = labelWithId(rawName, group.id);
|
||||
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 页面 step 是否支持「回滚」(类 git revert):
|
||||
* - 新增 / 删除:不依赖 changeRecords,反向应用即删除 / 加回,始终可回滚;
|
||||
* - 更新:必须每个被更新节点都带有 changeRecords,才支持按 propPath 局部反向 patch。
|
||||
* 缺失 changeRecords 的更新只能整节点替换,会冲掉该节点后续的无关变更,因此不支持回滚。
|
||||
*/
|
||||
export const isPageStepRevertable = (step: StepValue): boolean => {
|
||||
if (step.opType !== 'update') return true;
|
||||
const items = step.updatedItems ?? [];
|
||||
if (!items.length) return false;
|
||||
return items.every((item) => Boolean(item.changeRecords?.length));
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据源 step 是否支持「回滚」:
|
||||
* - 新增(oldSchema=null)/ 删除(newSchema=null):不依赖 changeRecords,始终可回滚;
|
||||
* - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
||||
*/
|
||||
export const isDataSourceStepRevertable = (step: DataSourceStepValue): boolean => {
|
||||
if (step.oldSchema === null || step.newSchema === null) return true;
|
||||
return Boolean(step.changeRecords?.length);
|
||||
};
|
||||
|
||||
/**
|
||||
* 代码块 step 是否支持「回滚」:
|
||||
* - 新增(oldContent=null)/ 删除(newContent=null):不依赖 changeRecords,始终可回滚;
|
||||
* - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
||||
*/
|
||||
export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
|
||||
if (step.oldContent === null || step.newContent === null) return true;
|
||||
return Boolean(step.changeRecords?.length);
|
||||
};
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -69,6 +69,17 @@ class CodeBlock extends BaseService {
|
||||
paramsColConfig: undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* 最近一次写入历史栈的代码块历史记录 uuid(单条写入场景:新增 / 更新)。
|
||||
* 供 setCodeDslById(Sync)AndGetHistoryId 取回,普通方法不读取它。
|
||||
*/
|
||||
private lastPushedHistoryId: string | null = null;
|
||||
/**
|
||||
* deleteCodeDslByIds 一次删除多个代码块时,按写入顺序收集的历史记录 uuid 列表。
|
||||
* 在 deleteCodeDslByIds 入口处重置,供 deleteCodeDslByIdsAndGetHistoryId 取回。
|
||||
*/
|
||||
private lastDeletedHistoryIds: string[] = [];
|
||||
|
||||
constructor() {
|
||||
super([
|
||||
...canUsePluginMethods.async.map((methodName) => ({ name: methodName, isAsync: true })),
|
||||
@ -120,9 +131,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 +162,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 +198,14 @@ class CodeBlock extends BaseService {
|
||||
const newContent = cloneDeep(codeDsl[id]);
|
||||
|
||||
if (!doNotPushHistory) {
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent, changeRecords, historyDescription });
|
||||
this.lastPushedHistoryId =
|
||||
historyService.pushCodeBlock(id, {
|
||||
oldContent,
|
||||
newContent,
|
||||
changeRecords,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
})?.uuid ?? null;
|
||||
}
|
||||
|
||||
this.emit('addOrUpdate', id, codeDsl[id]);
|
||||
@ -268,12 +301,14 @@ 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();
|
||||
|
||||
if (!currentDsl) return;
|
||||
|
||||
this.lastDeletedHistoryIds = [];
|
||||
|
||||
codeIds.forEach((id) => {
|
||||
// 历史记录:删除前快照内容;不存在的 id 直接跳过历史推入
|
||||
const oldContent: CodeBlockContent | null = currentDsl[id] ? cloneDeep(currentDsl[id]) : null;
|
||||
@ -281,13 +316,62 @@ class CodeBlock extends BaseService {
|
||||
delete currentDsl[id];
|
||||
|
||||
if (oldContent && !doNotPushHistory) {
|
||||
historyService.pushCodeBlock(id, { oldContent, newContent: null, historyDescription });
|
||||
const uuid = historyService.pushCodeBlock(id, {
|
||||
oldContent,
|
||||
newContent: null,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
})?.uuid;
|
||||
if (uuid) this.lastDeletedHistoryIds.push(uuid);
|
||||
}
|
||||
|
||||
this.emit('remove', id);
|
||||
});
|
||||
}
|
||||
|
||||
// #region AndGetHistoryId
|
||||
/**
|
||||
* 下列 *AndGetHistoryId 方法与对应的写入方法行为完全一致,
|
||||
* 唯一区别是返回值为本次写入历史栈的历史记录 uuid({@link CodeBlockStepValue.uuid}),
|
||||
* 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
|
||||
*
|
||||
* 当本次操作未写入历史(doNotPushHistory 为 true、或无对应记录)时:单条写入返回 null,批量删除返回空数组。
|
||||
*/
|
||||
|
||||
/** 等价于 {@link setCodeDslById},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public async setCodeDslByIdAndGetHistoryId(
|
||||
id: Id,
|
||||
codeConfig: Partial<CodeBlockContent>,
|
||||
options: HistoryOpOptionsWithChangeRecords = {},
|
||||
): Promise<string | null> {
|
||||
this.lastPushedHistoryId = null;
|
||||
await this.setCodeDslById(id, codeConfig, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link setCodeDslByIdSync},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public setCodeDslByIdSyncAndGetHistoryId(
|
||||
id: Id,
|
||||
codeConfig: Partial<CodeBlockContent>,
|
||||
force = true,
|
||||
options: HistoryOpOptionsWithChangeRecords = {},
|
||||
): string | null {
|
||||
this.lastPushedHistoryId = null;
|
||||
this.setCodeDslByIdSync(id, codeConfig, force, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等价于 {@link deleteCodeDslByIds},但返回本次写入的全部历史记录 uuid(按删除顺序)。
|
||||
* 一次删除多个代码块会产生多条历史记录,因此返回数组;未写入任何历史时返回空数组。
|
||||
*/
|
||||
public async deleteCodeDslByIdsAndGetHistoryId(codeIds: Id[], options: HistoryOpOptions = {}): Promise<string[]> {
|
||||
this.lastDeletedHistoryIds = [];
|
||||
await this.deleteCodeDslByIds(codeIds, options);
|
||||
return [...this.lastDeletedHistoryIds];
|
||||
}
|
||||
// #endregion AndGetHistoryId
|
||||
|
||||
public setParamsColConfig(config: TableColumnConfig): void {
|
||||
this.state.paramsColConfig = config;
|
||||
}
|
||||
@ -373,10 +457,26 @@ class CodeBlock extends BaseService {
|
||||
const list = historyService.getCodeBlockStepList(id);
|
||||
const entry = list[index];
|
||||
if (!entry?.applied) return null;
|
||||
// 更新类步骤(前后 content 都存在)必须带 changeRecords 才支持回滚,否则只能整内容替换,会冲掉后续无关变更。
|
||||
if (entry.step.oldContent && entry.step.newContent && !entry.step.changeRecords?.length) return null;
|
||||
const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
|
||||
return await this.applyRevertStep(entry.step, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过历史记录 uuid 回滚某条代码块历史步骤,语义同 {@link revert},
|
||||
* 仅无需调用方再传 codeBlockId 与 index:内部会按 uuid({@link CodeBlockStepValue.uuid})
|
||||
* 在全部代码块栈中定位对应步骤后再回滚。
|
||||
*
|
||||
* @param uuid 目标历史记录的 uuid,通常由 {@link setCodeDslByIdAndGetHistoryId} 等方法返回
|
||||
* @returns 反向后产生的新 step;找不到对应 uuid / 未应用时返回 null
|
||||
*/
|
||||
public async revertById(uuid: string): Promise<CodeBlockStepValue | null> {
|
||||
const location = historyService.findCodeBlockStepLocationByUuid(uuid);
|
||||
if (!location) return null;
|
||||
return await this.revert(location.id, location.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成代码块唯一id
|
||||
* @returns {Id} 代码块唯一id
|
||||
@ -471,13 +571,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 +600,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;
|
||||
}
|
||||
|
||||
|
||||
@ -78,6 +78,13 @@ class DataSource extends BaseService {
|
||||
methods: {},
|
||||
});
|
||||
|
||||
/**
|
||||
* 最近一次写入历史栈的数据源历史记录 uuid。
|
||||
* 供 *AndGetHistoryId 系列方法在调用 add / update / remove 后取回本次产生的历史记录 id;
|
||||
* 普通方法不读取它,调用前由 *AndGetHistoryId 重置为 null。
|
||||
*/
|
||||
private lastPushedHistoryId: string | null = null;
|
||||
|
||||
constructor() {
|
||||
super(canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false })));
|
||||
}
|
||||
@ -129,7 +136,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 +148,13 @@ class DataSource extends BaseService {
|
||||
this.get('dataSources').push(newConfig);
|
||||
|
||||
if (!doNotPushHistory) {
|
||||
historyService.pushDataSource(newConfig.id, { oldSchema: null, newSchema: newConfig, historyDescription });
|
||||
this.lastPushedHistoryId =
|
||||
historyService.pushDataSource(newConfig.id, {
|
||||
oldSchema: null,
|
||||
newSchema: newConfig,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
})?.uuid ?? null;
|
||||
}
|
||||
|
||||
this.emit('add', newConfig);
|
||||
@ -156,7 +172,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');
|
||||
|
||||
@ -168,12 +189,14 @@ class DataSource extends BaseService {
|
||||
dataSources[index] = newConfig;
|
||||
|
||||
if (!doNotPushHistory) {
|
||||
historyService.pushDataSource(newConfig.id, {
|
||||
oldSchema: oldConfig ? cloneDeep(oldConfig) : null,
|
||||
newSchema: newConfig,
|
||||
changeRecords,
|
||||
historyDescription,
|
||||
});
|
||||
this.lastPushedHistoryId =
|
||||
historyService.pushDataSource(newConfig.id, {
|
||||
oldSchema: oldConfig ? cloneDeep(oldConfig) : null,
|
||||
newSchema: newConfig,
|
||||
changeRecords,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
})?.uuid ?? null;
|
||||
}
|
||||
|
||||
this.emit('update', newConfig, {
|
||||
@ -191,19 +214,59 @@ 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 });
|
||||
this.lastPushedHistoryId =
|
||||
historyService.pushDataSource(id, {
|
||||
oldSchema: cloneDeep(oldConfig),
|
||||
newSchema: null,
|
||||
historyDescription,
|
||||
source: historySource,
|
||||
})?.uuid ?? null;
|
||||
}
|
||||
|
||||
this.emit('remove', id);
|
||||
}
|
||||
|
||||
// #region AndGetHistoryId
|
||||
/**
|
||||
* 下列 *AndGetHistoryId 方法与对应的 add / update / remove 行为完全一致,
|
||||
* 唯一区别是返回值为本次写入历史栈的历史记录 uuid({@link DataSourceStepValue.uuid}),
|
||||
* 而非数据源配置。可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
|
||||
*
|
||||
* 当本次操作未写入历史(doNotPushHistory 为 true、或无对应记录)时返回 null。
|
||||
*/
|
||||
|
||||
/** 等价于 {@link add},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public addAndGetHistoryId(config: DataSourceSchema, options: HistoryOpOptions = {}): string | null {
|
||||
this.lastPushedHistoryId = null;
|
||||
this.add(config, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link update},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public updateAndGetHistoryId(
|
||||
config: DataSourceSchema,
|
||||
options: HistoryOpOptionsWithChangeRecords = {},
|
||||
): string | null {
|
||||
this.lastPushedHistoryId = null;
|
||||
this.update(config, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link remove},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public removeAndGetHistoryId(id: string, options: HistoryOpOptions = {}): string | null {
|
||||
this.lastPushedHistoryId = null;
|
||||
this.remove(id, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
// #endregion AndGetHistoryId
|
||||
|
||||
/**
|
||||
* 撤销指定数据源的最近一次变更。
|
||||
*
|
||||
@ -278,10 +341,26 @@ class DataSource extends BaseService {
|
||||
const list = historyService.getDataSourceStepList(id);
|
||||
const entry = list[index];
|
||||
if (!entry?.applied) return null;
|
||||
// 更新类步骤(前后 schema 都存在)必须带 changeRecords 才支持回滚,否则只能整 schema 替换,会冲掉后续无关变更。
|
||||
if (entry.step.oldSchema && entry.step.newSchema && !entry.step.changeRecords?.length) return null;
|
||||
const description = `回滚 #${index + 1}: ${describeRevertDataSourceStep(entry.step)}`;
|
||||
return this.applyRevertStep(entry.step, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过历史记录 uuid 回滚某条数据源历史步骤,语义同 {@link revert},
|
||||
* 仅无需调用方再传 dataSourceId 与 index:内部会按 uuid({@link DataSourceStepValue.uuid})
|
||||
* 在全部数据源栈中定位对应步骤后再回滚。
|
||||
*
|
||||
* @param uuid 目标历史记录的 uuid,通常由 {@link addAndGetHistoryId} 等方法返回
|
||||
* @returns 反向后产生的新 step;找不到对应 uuid / 未应用时返回 null
|
||||
*/
|
||||
public revertById(uuid: string): DataSourceStepValue | null {
|
||||
const location = historyService.findDataSourceStepLocationByUuid(uuid);
|
||||
if (!location) return null;
|
||||
return this.revert(location.id, location.index);
|
||||
}
|
||||
|
||||
public createId(): string {
|
||||
return `ds_${guid()}`;
|
||||
}
|
||||
@ -366,13 +445,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 +474,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;
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,15 @@ import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions }
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import type { ChangeRecord } from '@tmagic/form';
|
||||
import { isFixed } from '@tmagic/stage';
|
||||
import { getNodeInfo, getNodePath, getValueByKeyPath, isPage, isPageFragment, setValueByKeyPath } from '@tmagic/utils';
|
||||
import {
|
||||
getNodeInfo,
|
||||
getNodePath,
|
||||
getValueByKeyPath,
|
||||
guid,
|
||||
isPage,
|
||||
isPageFragment,
|
||||
setValueByKeyPath,
|
||||
} from '@tmagic/utils';
|
||||
|
||||
import BaseService from '@editor/services//BaseService';
|
||||
import propsService from '@editor/services//props';
|
||||
@ -36,6 +44,7 @@ import type {
|
||||
DslOpOptions,
|
||||
EditorEvents,
|
||||
EditorNodeInfo,
|
||||
HistoryOpSource,
|
||||
HistoryOpType,
|
||||
PastePosition,
|
||||
StepValue,
|
||||
@ -115,6 +124,12 @@ class Editor extends BaseService {
|
||||
alwaysMultiSelect: false,
|
||||
});
|
||||
private selectionBeforeOp: Id[] | null = null;
|
||||
/**
|
||||
* 最近一次 pushOpHistory 写入的历史记录 uuid。
|
||||
* 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
|
||||
* 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
|
||||
*/
|
||||
private lastPushedHistoryId: string | null = null;
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
@ -406,7 +421,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 +487,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 +498,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 +598,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 +633,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 +736,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 +759,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 +769,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 +796,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 +816,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 +869,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 +896,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 +938,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 +964,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 +1008,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 +1042,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 +1111,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 +1128,7 @@ class Editor extends BaseService {
|
||||
config: MNode | MNode[],
|
||||
targetParent: MContainer,
|
||||
targetIndex: number,
|
||||
{ doNotPushHistory = false }: DslOpOptions = {},
|
||||
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
||||
) {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
@ -1127,7 +1191,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;
|
||||
}
|
||||
@ -1135,6 +1204,86 @@ class Editor extends BaseService {
|
||||
this.emit('drag-to', { targetIndex, configs, targetParent });
|
||||
}
|
||||
|
||||
// #region AndGetHistoryId
|
||||
/**
|
||||
* 下列 *AndGetHistoryId 方法与对应的普通操作(add / remove / update ...)行为完全一致,
|
||||
* 唯一区别是返回值为本次写入历史栈的历史记录 uuid({@link StepValue.uuid}),
|
||||
* 而非节点 / 节点数组。可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
|
||||
*
|
||||
* 当本次操作未写入历史(doNotPushHistory 为 true、或操作无实际变更 / 提前返回)时返回 null。
|
||||
*/
|
||||
|
||||
/** 等价于 {@link add},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public async addAndGetHistoryId(
|
||||
addNode: AddMNode | MNode[],
|
||||
parent?: MContainer | null,
|
||||
options: DslOpOptions = {},
|
||||
): Promise<string | null> {
|
||||
this.lastPushedHistoryId = null;
|
||||
await this.add(addNode, parent, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link remove},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public async removeAndGetHistoryId(
|
||||
nodeOrNodeList: MNode | MNode[],
|
||||
options: DslOpOptions = {},
|
||||
): Promise<string | null> {
|
||||
this.lastPushedHistoryId = null;
|
||||
await this.remove(nodeOrNodeList, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link update},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public async updateAndGetHistoryId(
|
||||
config: MNode | MNode[],
|
||||
data: {
|
||||
changeRecords?: ChangeRecord[];
|
||||
changeRecordList?: ChangeRecord[][];
|
||||
doNotPushHistory?: boolean;
|
||||
historyDescription?: string;
|
||||
historySource?: HistoryOpSource;
|
||||
} = {},
|
||||
): Promise<string | null> {
|
||||
this.lastPushedHistoryId = null;
|
||||
await this.update(config, data);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link moveLayer},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public async moveLayerAndGetHistoryId(
|
||||
offset: number | LayerOffset,
|
||||
options: DslOpOptions = {},
|
||||
): Promise<string | null> {
|
||||
this.lastPushedHistoryId = null;
|
||||
await this.moveLayer(offset, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link moveToContainer},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public async moveToContainerAndGetHistoryId(
|
||||
config: MNode | MNode[],
|
||||
targetId: Id,
|
||||
options: DslOpOptions = {},
|
||||
): Promise<string | null> {
|
||||
this.lastPushedHistoryId = null;
|
||||
await this.moveToContainer(config, targetId, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
|
||||
/** 等价于 {@link dragTo},但返回本次写入历史记录的 uuid(未入栈时返回 null)。 */
|
||||
public async dragToAndGetHistoryId(
|
||||
config: MNode | MNode[],
|
||||
targetParent: MContainer,
|
||||
targetIndex: number,
|
||||
options: DslOpOptions = {},
|
||||
): Promise<string | null> {
|
||||
this.lastPushedHistoryId = null;
|
||||
await this.dragTo(config, targetParent, targetIndex, options);
|
||||
return this.lastPushedHistoryId;
|
||||
}
|
||||
// #endregion AndGetHistoryId
|
||||
|
||||
/**
|
||||
* 撤销当前操作
|
||||
* @returns 被撤销的操作
|
||||
@ -1184,6 +1333,12 @@ class Editor extends BaseService {
|
||||
const root = this.get('root');
|
||||
if (!root) return null;
|
||||
|
||||
// 更新类步骤必须带 changeRecords 才支持回滚:缺失时只能整节点替换,会冲掉后续无关变更,故不支持。
|
||||
if (step.opType === 'update') {
|
||||
const items = step.updatedItems ?? [];
|
||||
if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
|
||||
}
|
||||
|
||||
// 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
|
||||
let revertedStep: StepValue | null = null;
|
||||
const captureRevert = (s: StepValue) => {
|
||||
@ -1193,7 +1348,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 +1396,7 @@ class Editor extends BaseService {
|
||||
return cloneDeep(oldNode);
|
||||
});
|
||||
if (configs.length) {
|
||||
await this.update(configs, { historyDescription });
|
||||
await this.update(configs, { historyDescription, historySource: 'rollback' });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -1259,6 +1414,20 @@ class Editor extends BaseService {
|
||||
return revertedStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过历史记录 uuid 回滚当前页面的某条历史步骤,语义与 {@link revertPageStep} 完全一致,
|
||||
* 仅入参从 index 改为 uuid({@link StepValue.uuid})。uuid 不随栈内步骤增删而变化,
|
||||
* 更适合业务侧持有引用后再回滚(埋点、跨端同步等场景)。
|
||||
*
|
||||
* @param uuid 目标历史记录的 uuid,通常由 *AndGetHistoryId 方法返回
|
||||
* @returns 反向后产生的新 step;找不到对应 uuid / 未应用 / 反向失败时返回 null
|
||||
*/
|
||||
public async revertPageStepById(uuid: string): Promise<StepValue | null> {
|
||||
const index = historyService.getPageStepIndexByUuid(uuid);
|
||||
if (index < 0) return null;
|
||||
return this.revertPageStep(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转当前页面历史栈到指定游标位置。
|
||||
*
|
||||
@ -1285,14 +1454,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,11 +1526,20 @@ 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;
|
||||
},
|
||||
): string | null {
|
||||
const step: StepValue = {
|
||||
uuid: guid(),
|
||||
data: pageData,
|
||||
opType,
|
||||
selectedBefore: this.selectionBeforeOp ?? [],
|
||||
@ -1363,10 +1548,15 @@ class Editor extends BaseService {
|
||||
...extra,
|
||||
};
|
||||
if (historyDescription) step.historyDescription = historyDescription;
|
||||
if (source) step.source = source;
|
||||
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
|
||||
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
|
||||
historyService.push(step, pageData.id);
|
||||
const pushed = historyService.push(step, pageData.id);
|
||||
// push 返回 null 表示当前没有可写入的页面栈(未真正入栈),此时不应返回 uuid。
|
||||
const historyId = pushed ? step.uuid : null;
|
||||
this.lastPushedHistoryId = historyId;
|
||||
this.selectionBeforeOp = null;
|
||||
return historyId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -18,23 +18,37 @@
|
||||
|
||||
import { reactive } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import serialize from 'serialize-javascript';
|
||||
|
||||
import type { CodeBlockContent, DataSourceSchema, Id, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { ChangeRecord } from '@tmagic/form';
|
||||
import { guid } from '@tmagic/utils';
|
||||
|
||||
import type {
|
||||
CodeBlockHistoryGroup,
|
||||
CodeBlockStepValue,
|
||||
DataSourceHistoryGroup,
|
||||
DataSourceStepValue,
|
||||
HistoryOpSource,
|
||||
HistoryPersistOptions,
|
||||
HistoryState,
|
||||
PageHistoryGroup,
|
||||
PageHistoryStepEntry,
|
||||
PersistedHistoryState,
|
||||
StepValue,
|
||||
} from '@editor/type';
|
||||
import { getEditorConfig } from '@editor/utils/config';
|
||||
import { idbGet, idbSet } from '@editor/utils/indexed-db';
|
||||
import { UndoRedo } from '@editor/utils/undo-redo';
|
||||
|
||||
import BaseService from './BaseService';
|
||||
import editorService from './editor';
|
||||
|
||||
/** 历史记录持久化快照的默认存储位置与结构版本。 */
|
||||
const DEFAULT_DB_NAME = 'tmagic-editor';
|
||||
const DEFAULT_STORE_NAME = 'history';
|
||||
const DEFAULT_KEY: IDBValidKey = 'default';
|
||||
const PERSIST_VERSION = 1;
|
||||
|
||||
class History extends BaseService {
|
||||
/**
|
||||
@ -194,6 +208,45 @@ class History extends BaseService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把单个栈当前游标所在记录标记为已保存:先清除该栈内全部旧标记,保证同一栈最多一条 `saved`。
|
||||
* 栈处于「全部已撤销」(cursor 为 0)时不会留下已保存记录,恢复时其游标回到 0。
|
||||
*/
|
||||
private static markStackSaved<S extends { saved?: boolean }>(undoRedo?: UndoRedo<S>): void {
|
||||
if (!undoRedo) return;
|
||||
undoRedo.updateElements((element) => {
|
||||
element.saved = false;
|
||||
});
|
||||
undoRedo.updateCurrentElement((element) => {
|
||||
element.saved = true;
|
||||
});
|
||||
}
|
||||
|
||||
/** 把 `Record<Id, UndoRedo>` 整体序列化为 `Record<Id, SerializedUndoRedo>`。 */
|
||||
private static serializeStacks<T>(stacks: Record<Id, UndoRedo<T>>) {
|
||||
const result: Record<Id, ReturnType<UndoRedo<T>['serialize']>> = {};
|
||||
Object.entries(stacks).forEach(([id, undoRedo]) => {
|
||||
if (undoRedo) result[id] = undoRedo.serialize();
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 `Record<Id, SerializedUndoRedo>` 整体还原为 `Record<Id, UndoRedo>`。
|
||||
* 还原时把每个栈的游标定位到最近一条已保存(`saved === true`)记录之后。
|
||||
*/
|
||||
private static deserializeStacks<T extends { saved?: boolean }>(
|
||||
stacks: Record<Id, ReturnType<UndoRedo<T>['serialize']>> = {},
|
||||
): Record<Id, UndoRedo<T>> {
|
||||
const result: Record<Id, UndoRedo<T>> = {};
|
||||
Object.entries(stacks).forEach(([id, serialized]) => {
|
||||
if (serialized) {
|
||||
result[id] = UndoRedo.fromSerialized<T>(serialized, { isSavedStep: (element) => element.saved === true });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public state = reactive<HistoryState>({
|
||||
pageSteps: {},
|
||||
pageId: undefined,
|
||||
@ -254,6 +307,7 @@ class History extends BaseService {
|
||||
public push(state: StepValue, pageId?: Id): StepValue | null {
|
||||
const undoRedo = this.getUndoRedo(pageId);
|
||||
if (!undoRedo) return null;
|
||||
if (state.uuid === undefined) state.uuid = guid();
|
||||
if (state.timestamp === undefined) state.timestamp = Date.now();
|
||||
undoRedo.pushElement(state);
|
||||
// 仅当推入的是当前活动页时才需要刷新 canUndo/canRedo —— 其它页栈对当前 UI 状态没影响。
|
||||
@ -280,16 +334,20 @@ class History extends BaseService {
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 可选的人类可读描述(如「修改按钮颜色」),仅用于历史面板展示。 */
|
||||
historyDescription?: string;
|
||||
/** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
|
||||
source?: HistoryOpSource;
|
||||
},
|
||||
): CodeBlockStepValue | null {
|
||||
if (!codeBlockId) return null;
|
||||
|
||||
const step: CodeBlockStepValue = {
|
||||
uuid: guid(),
|
||||
id: codeBlockId,
|
||||
oldContent: payload.oldContent ? cloneDeep(payload.oldContent) : null,
|
||||
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,16 +368,20 @@ class History extends BaseService {
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 可选的人类可读描述,仅用于历史面板展示。 */
|
||||
historyDescription?: string;
|
||||
/** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
|
||||
source?: HistoryOpSource;
|
||||
},
|
||||
): DataSourceStepValue | null {
|
||||
if (!dataSourceId) return null;
|
||||
|
||||
const step: DataSourceStepValue = {
|
||||
uuid: guid(),
|
||||
id: dataSourceId,
|
||||
oldSchema: payload.oldSchema ? cloneDeep(payload.oldSchema) : null,
|
||||
newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
|
||||
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
|
||||
historyDescription: payload.historyDescription,
|
||||
source: payload.source,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
@ -406,6 +468,137 @@ class History extends BaseService {
|
||||
this.removeAllPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空指定页面(缺省当前活动页)的历史记录栈。
|
||||
* 仅删除撤销/重做记录,不会改动当前 DSL;清空后该页将无法再撤销/重做之前的操作。
|
||||
*/
|
||||
public clearPage(pageId?: Id): void {
|
||||
const targetPageId = pageId ?? this.state.pageId;
|
||||
if (!targetPageId) return;
|
||||
this.state.pageSteps[targetPageId] = new UndoRedo<StepValue>();
|
||||
if (`${targetPageId}` === `${this.state.pageId}`) {
|
||||
this.setCanUndoRedo();
|
||||
this.emit('change', null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空数据源历史记录栈:传入 `dataSourceId` 仅清空该数据源,缺省清空全部数据源。
|
||||
* 仅删除撤销/重做记录,不会改动数据源本身。
|
||||
*/
|
||||
public clearDataSource(dataSourceId?: Id): void {
|
||||
if (dataSourceId !== undefined) {
|
||||
delete this.state.dataSourceState[dataSourceId];
|
||||
} else {
|
||||
this.state.dataSourceState = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空代码块历史记录栈:传入 `codeBlockId` 仅清空该代码块,缺省清空全部代码块。
|
||||
* 仅删除撤销/重做记录,不会改动代码块本身。
|
||||
*/
|
||||
public clearCodeBlock(codeBlockId?: Id): void {
|
||||
if (codeBlockId !== undefined) {
|
||||
delete this.state.codeBlockState[codeBlockId];
|
||||
} else {
|
||||
this.state.codeBlockState = {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记「整份 DSL 已保存」:把页面 / 代码块 / 数据源所有栈当前游标所在的记录都标为 `saved`。
|
||||
* 适用于「整体落库」场景;若只保存了其中一类,请改用更细粒度的
|
||||
* {@link markPageSaved} / {@link markCodeBlockSaved} / {@link markDataSourceSaved}。
|
||||
*/
|
||||
public markSaved(): void {
|
||||
Object.values(this.state.pageSteps).forEach(History.markStackSaved);
|
||||
Object.values(this.state.codeBlockState).forEach(History.markStackSaved);
|
||||
Object.values(this.state.dataSourceState).forEach(History.markStackSaved);
|
||||
this.emit('mark-saved', { kind: 'all' });
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记指定页面(缺省为当前活动页)的历史栈当前记录为已保存。
|
||||
* 仅影响该页面自己的栈,不波及代码块 / 数据源 / 其它页面。
|
||||
*/
|
||||
public markPageSaved(pageId?: Id): void {
|
||||
const targetPageId = pageId ?? this.state.pageId;
|
||||
if (!targetPageId) return;
|
||||
History.markStackSaved(this.state.pageSteps[targetPageId]);
|
||||
this.emit('mark-saved', { kind: 'page', id: targetPageId });
|
||||
}
|
||||
|
||||
/** 标记指定代码块的历史栈当前记录为已保存,仅影响该代码块自己的栈。 */
|
||||
public markCodeBlockSaved(codeBlockId: Id): void {
|
||||
if (!codeBlockId) return;
|
||||
History.markStackSaved(this.state.codeBlockState[codeBlockId]);
|
||||
this.emit('mark-saved', { kind: 'code-block', id: codeBlockId });
|
||||
}
|
||||
|
||||
/** 标记指定数据源的历史栈当前记录为已保存,仅影响该数据源自己的栈。 */
|
||||
public markDataSourceSaved(dataSourceId: Id): void {
|
||||
if (!dataSourceId) return;
|
||||
History.markStackSaved(this.state.dataSourceState[dataSourceId]);
|
||||
this.emit('mark-saved', { kind: 'data-source', id: dataSourceId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 把当前内存中的全部历史栈(页面 / 代码块 / 数据源)序列化后写入本地 IndexedDB。
|
||||
*
|
||||
* - 每个 UndoRedo 栈连同其游标、容量一并保存,恢复后可继续 undo/redo;
|
||||
* - `key` 用于区分不同活动页 / 项目(同一 store 下可保存多份快照),缺省为 `default`;
|
||||
* - 返回写入成功的快照对象,便于调用方记录 savedAt 等信息;
|
||||
* - 不支持 IndexedDB 的环境(如 SSR)会 reject。
|
||||
*/
|
||||
public async saveToIndexedDB(options: HistoryPersistOptions = {}): Promise<PersistedHistoryState> {
|
||||
const { dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME, key = DEFAULT_KEY } = options;
|
||||
|
||||
const snapshot: PersistedHistoryState = {
|
||||
version: PERSIST_VERSION,
|
||||
pageId: this.state.pageId,
|
||||
pageSteps: History.serializeStacks(this.state.pageSteps),
|
||||
codeBlockState: History.serializeStacks(this.state.codeBlockState),
|
||||
dataSourceState: History.serializeStacks(this.state.dataSourceState),
|
||||
savedAt: Date.now(),
|
||||
};
|
||||
|
||||
// 历史记录里可能包含函数(如代码块内容 / 节点事件 / 数据源方法),IndexedDB 的结构化克隆无法写入函数,
|
||||
// 因此用 serialize-javascript 序列化成字符串后再写入(支持函数 / Map 等),读取时用 parseDSL 还原。
|
||||
await idbSet(this.resolveDbName(dbName), storeName, key, serialize(snapshot));
|
||||
this.emit('save-to-indexed-db', snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地 IndexedDB 读取此前保存的历史快照并重建全部撤销/重做栈。
|
||||
*
|
||||
* - 读取到的每个栈都会经 {@link UndoRedo.fromSerialized} 还原(含游标),随后可直接 undo/redo;
|
||||
* - 会整体覆盖当前内存中的历史状态,并把活动页恢复为快照中的 pageId;
|
||||
* - 找不到对应记录时返回 null,且不改动当前状态;
|
||||
* - 不支持 IndexedDB 的环境(如 SSR)会 reject。
|
||||
*/
|
||||
public async restoreFromIndexedDB(options: HistoryPersistOptions = {}): Promise<PersistedHistoryState | null> {
|
||||
const { dbName = DEFAULT_DB_NAME, storeName = DEFAULT_STORE_NAME, key = DEFAULT_KEY } = options;
|
||||
|
||||
const raw = await idbGet<string | PersistedHistoryState>(this.resolveDbName(dbName), storeName, key);
|
||||
if (!raw) return null;
|
||||
|
||||
// 新版以序列化字符串存储(含函数),用 parseDSL 还原;兼容历史上以对象形式存入的旧数据。
|
||||
const snapshot = (typeof raw === 'string' ? getEditorConfig('parseDSL')(`(${raw})`) : raw) as PersistedHistoryState;
|
||||
if (!snapshot) return null;
|
||||
|
||||
this.state.pageSteps = History.deserializeStacks(snapshot.pageSteps);
|
||||
this.state.codeBlockState = History.deserializeStacks(snapshot.codeBlockState);
|
||||
this.state.dataSourceState = History.deserializeStacks(snapshot.dataSourceState);
|
||||
this.state.pageId = snapshot.pageId;
|
||||
|
||||
this.setCanUndoRedo();
|
||||
this.emit('restore-from-indexed-db', snapshot);
|
||||
this.emit('change', null);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出当前活动页的历史步骤平铺列表(包含已应用 + 已撤销)。
|
||||
* 列表按时间正序,最早一步在最前面。
|
||||
@ -503,6 +696,41 @@ class History extends BaseService {
|
||||
return list.map((step, index) => ({ step, index, applied: index < cursor }));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按历史记录 uuid 在指定页面(默认当前活动页)的栈中查找其索引。
|
||||
* 找不到时返回 -1。供「按 uuid 回滚」等需要把 uuid 映射回 index 的场景使用。
|
||||
*/
|
||||
public getPageStepIndexByUuid(uuid: string, pageId?: Id): number {
|
||||
if (!uuid) return -1;
|
||||
return this.getPageStepList(pageId).findIndex((entry) => entry.step.uuid === uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按历史记录 uuid 在全部代码块栈中查找其所属 codeBlockId 与索引。
|
||||
* 找不到时返回 null。
|
||||
*/
|
||||
public findCodeBlockStepLocationByUuid(uuid: string): { id: Id; index: number } | null {
|
||||
if (!uuid) return null;
|
||||
for (const id of Object.keys(this.state.codeBlockState)) {
|
||||
const index = this.getCodeBlockStepList(id).findIndex((entry) => entry.step.uuid === uuid);
|
||||
if (index >= 0) return { id, index };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按历史记录 uuid 在全部数据源栈中查找其所属 dataSourceId 与索引。
|
||||
* 找不到时返回 null。
|
||||
*/
|
||||
public findDataSourceStepLocationByUuid(uuid: string): { id: Id; index: number } | null {
|
||||
if (!uuid) return null;
|
||||
for (const id of Object.keys(this.state.dataSourceState)) {
|
||||
const index = this.getDataSourceStepList(id).findIndex((entry) => entry.step.uuid === uuid);
|
||||
if (index >= 0) return { id, index };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出全部数据源的历史栈,按 dataSourceId 分组。同上。
|
||||
*/
|
||||
@ -533,6 +761,15 @@ class History extends BaseService {
|
||||
return this.state.pageSteps[targetPageId];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把基础 dbName 与当前 DSL(root app)的 id 拼成最终库名,实现不同应用历史隔离。
|
||||
* 取不到 app id(如尚未加载 DSL)时退回基础 dbName。
|
||||
*/
|
||||
private resolveDbName(dbName: string): string {
|
||||
const appId = editorService.get('root')?.id;
|
||||
return appId ? `${dbName}-${appId}` : dbName;
|
||||
}
|
||||
|
||||
private setCanUndoRedo(): void {
|
||||
const undoRedo = this.getUndoRedo();
|
||||
this.state.canRedo = undoRedo?.canRedo() || false;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
@ -45,6 +45,28 @@
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// 历史列表工具条:放置「清空」等列表级操作,右对齐。
|
||||
.m-editor-history-list-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 4px 4px;
|
||||
}
|
||||
|
||||
// 「清空」按钮:红色文字按钮,强调破坏性操作(点击后会二次确认)。
|
||||
.m-editor-history-list-clear {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #f56c6c;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(245, 108, 108, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.m-editor-history-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -281,6 +303,34 @@
|
||||
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; // 防止被合并组头部的粗体继承
|
||||
}
|
||||
|
||||
// 「已保存」徽标:绿色实心胶囊,标记最近一次保存对应的历史记录(与 historyService.markSaved 对应)。
|
||||
.m-editor-history-list-item-saved {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
background-color: #67c23a;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
// 「合并 N 步」徽标:紫色实心胶囊,与合并组卡片色系一致,醒目区分单步条目。
|
||||
.m-editor-history-list-item-merge {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
z-index: 30;
|
||||
z-index: 32;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
@ -70,7 +70,7 @@
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 60px;
|
||||
z-index: 30;
|
||||
z-index: 31;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
@ -82,7 +82,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
z-index: 31;
|
||||
}
|
||||
|
||||
.m-editor-resizer {
|
||||
|
||||
@ -56,7 +56,7 @@ 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';
|
||||
import type { SerializedUndoRedo, UndoRedo } from './utils/undo-redo';
|
||||
|
||||
export type EditorSlots = FrameworkSlots &
|
||||
WorkspaceSlots &
|
||||
@ -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,8 +683,80 @@ 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 BaseStepValue
|
||||
/**
|
||||
* 历史记录条目公共字段,被 {@link StepValue} / {@link CodeBlockStepValue} / {@link DataSourceStepValue} 复用。
|
||||
*/
|
||||
export interface BaseStepValue {
|
||||
/**
|
||||
* 历史记录唯一标识(uuid)。入栈时自动写入(若调用方未指定),
|
||||
* 用于精确定位 / 引用某一条历史记录(如 revert、埋点、跨端同步等)。
|
||||
* 注意与各自的 `id`(关联的页面 / 代码块 / 数据源 id)区分。
|
||||
*/
|
||||
uuid: string;
|
||||
/**
|
||||
* 调用方可选传入的人类可读描述(如「调整按钮颜色」),用于历史面板展示。
|
||||
* 不影响 undo/redo 行为;缺省时面板会根据节点 / propPath 自动生成描述。
|
||||
*/
|
||||
historyDescription?: string;
|
||||
/**
|
||||
* 操作途径:标记本次变更由哪条交互入口触发,取值见 {@link HistoryOpSource}
|
||||
* (画布 / 树面板 / 组件面板 / 配置面板 / 源码编辑器 / 右键菜单 / 工具栏 / 快捷键 / 回滚 / 接口 等)。
|
||||
* 仅用于历史面板展示与业务埋点,不影响 undo/redo 行为;缺省时面板视为「未知」。
|
||||
*/
|
||||
source?: HistoryOpSource;
|
||||
/**
|
||||
* 入栈时间戳(毫秒)。入栈时自动写入(若调用方未指定),仅用于历史面板展示。
|
||||
*/
|
||||
timestamp?: number;
|
||||
/**
|
||||
* 是否为「已保存」记录:DSL 落库(如保存到后端 / 本地)时由 historyService.markSaved 标记。
|
||||
* 同一栈内任意时刻最多只有一条记录为 true;从 IndexedDB 恢复时游标会被定位到最近一条已保存记录之后。
|
||||
*/
|
||||
saved?: boolean;
|
||||
}
|
||||
// #endregion BaseStepValue
|
||||
|
||||
// #region StepValue
|
||||
export interface StepValue {
|
||||
export interface StepValue extends BaseStepValue {
|
||||
/** 页面信息 */
|
||||
data: { name: string; id: Id };
|
||||
opType: HistoryOpType;
|
||||
@ -708,15 +780,6 @@ export interface StepValue {
|
||||
* 缺省(未传 / 空数组)才退化为整节点替换。
|
||||
*/
|
||||
updatedItems?: { oldNode: MNode; newNode: MNode; changeRecords?: ChangeRecord[] }[];
|
||||
/**
|
||||
* 调用方可选传入的人类可读描述(如「调整按钮颜色」),用于历史面板展示。
|
||||
* 不影响 undo/redo 行为;缺省时面板会根据节点 / propPath 自动生成描述。
|
||||
*/
|
||||
historyDescription?: string;
|
||||
/**
|
||||
* 入栈时间戳(毫秒)。在 historyService.push 时自动写入(若调用方未指定),仅用于历史面板展示。
|
||||
*/
|
||||
timestamp?: number;
|
||||
}
|
||||
// #endregion StepValue
|
||||
|
||||
@ -727,7 +790,7 @@ export interface StepValue {
|
||||
* - 更新:oldContent / newContent 都为对应内容
|
||||
* - 删除:newContent = null,oldContent = 删除前内容
|
||||
*/
|
||||
export interface CodeBlockStepValue {
|
||||
export interface CodeBlockStepValue extends BaseStepValue {
|
||||
/** 关联的代码块 id */
|
||||
id: Id;
|
||||
/** 变更前的代码块内容,新增时为 null */
|
||||
@ -739,10 +802,6 @@ export interface CodeBlockStepValue {
|
||||
* 缺省才退化为整内容替换。新增/删除场景通常无 changeRecords。
|
||||
*/
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 调用方可选传入的人类可读描述,用于历史面板展示;不影响 undo/redo 行为。 */
|
||||
historyDescription?: string;
|
||||
/** 入栈时间戳(毫秒),入栈时自动写入,仅用于历史面板展示。 */
|
||||
timestamp?: number;
|
||||
}
|
||||
// #endregion CodeBlockStepValue
|
||||
|
||||
@ -753,7 +812,7 @@ export interface CodeBlockStepValue {
|
||||
* - 更新:oldSchema / newSchema 都为对应 schema
|
||||
* - 删除:newSchema = null,oldSchema = 删除前 schema
|
||||
*/
|
||||
export interface DataSourceStepValue {
|
||||
export interface DataSourceStepValue extends BaseStepValue {
|
||||
/** 关联的数据源 id */
|
||||
id: Id;
|
||||
/** 变更前的数据源 schema,新增时为 null */
|
||||
@ -765,10 +824,6 @@ export interface DataSourceStepValue {
|
||||
* 缺省才退化为整 schema 替换。新增/删除场景通常无 changeRecords。
|
||||
*/
|
||||
changeRecords?: ChangeRecord[];
|
||||
/** 调用方可选传入的人类可读描述,用于历史面板展示;不影响 undo/redo 行为。 */
|
||||
historyDescription?: string;
|
||||
/** 入栈时间戳(毫秒),入栈时自动写入,仅用于历史面板展示。 */
|
||||
timestamp?: number;
|
||||
}
|
||||
// #endregion DataSourceStepValue
|
||||
|
||||
@ -789,6 +844,39 @@ export interface HistoryState {
|
||||
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;
|
||||
}
|
||||
// #endregion HistoryPersistOptions
|
||||
|
||||
// #region HistoryListEntry
|
||||
/**
|
||||
* 历史面板用:当前页面的一条历史步骤(包含位置和是否已应用)。
|
||||
@ -1099,16 +1187,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 +1209,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,11 +1221,12 @@ export interface DslOpOptions extends HistoryOpOptions {
|
||||
doNotSelect?: boolean;
|
||||
doNotSwitchPage?: boolean;
|
||||
}
|
||||
// #endregion DslOpOptions
|
||||
|
||||
/** 差异对话框的入参 */
|
||||
export interface DiffDialogPayload {
|
||||
/** 表单类别 */
|
||||
category: CompareCategory;
|
||||
category?: CompareCategory;
|
||||
/** 节点类型 / 数据源类型 */
|
||||
type?: string;
|
||||
/** 代码块场景下的数据源类型 */
|
||||
|
||||
@ -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);
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
@ -27,5 +27,6 @@ export * from './dep/idle-task';
|
||||
export * from './scroll-viewer';
|
||||
export * from './tree';
|
||||
export * from './undo-redo';
|
||||
export * from './indexed-db';
|
||||
export * from './const';
|
||||
export { default as loadMonaco } from './monaco-editor';
|
||||
|
||||
122
packages/editor/src/utils/indexed-db.ts
Normal file
122
packages/editor/src/utils/indexed-db.ts
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 一组极简的、基于原生 IndexedDB 的 Promise KV 工具,避免引入额外依赖。
|
||||
* 仅用于浏览器环境;在不支持 IndexedDB 的环境(如 SSR / 部分测试环境)下会 reject。
|
||||
*/
|
||||
|
||||
/** 是否处于支持 IndexedDB 的环境。 */
|
||||
export const isIndexedDBSupported = (): boolean => typeof indexedDB !== 'undefined' && indexedDB !== null;
|
||||
|
||||
/**
|
||||
* 打开(必要时升级)数据库,确保目标 objectStore 存在后返回连接。
|
||||
*
|
||||
* 由于 objectStore 只能在 `onupgradeneeded` 内创建,这里先以当前版本打开,
|
||||
* 若发现 store 不存在则关闭连接、以更高版本重开来按需创建,兼容动态 storeName。
|
||||
*/
|
||||
export const openIndexedDB = (dbName: string, storeName: string): Promise<IDBDatabase> =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!isIndexedDBSupported()) {
|
||||
reject(new Error('当前环境不支持 IndexedDB'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = indexedDB.open(dbName);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(storeName)) {
|
||||
db.createObjectStore(storeName);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
if (db.objectStoreNames.contains(storeName)) {
|
||||
resolve(db);
|
||||
return;
|
||||
}
|
||||
|
||||
// store 不存在:以更高版本重开,在 onupgradeneeded 中创建。
|
||||
const nextVersion = db.version + 1;
|
||||
db.close();
|
||||
const upgradeRequest = indexedDB.open(dbName, nextVersion);
|
||||
upgradeRequest.onupgradeneeded = () => {
|
||||
const upgradeDb = upgradeRequest.result;
|
||||
if (!upgradeDb.objectStoreNames.contains(storeName)) {
|
||||
upgradeDb.createObjectStore(storeName);
|
||||
}
|
||||
};
|
||||
upgradeRequest.onerror = () => reject(upgradeRequest.error);
|
||||
upgradeRequest.onsuccess = () => resolve(upgradeRequest.result);
|
||||
};
|
||||
});
|
||||
|
||||
/** 写入(覆盖)一条记录。value 通过结构化克隆存储,支持 Map / Set 等结构。 */
|
||||
export const idbSet = async (dbName: string, storeName: string, key: IDBValidKey, value: unknown): Promise<void> => {
|
||||
const db = await openIndexedDB(dbName, storeName);
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
tx.objectStore(storeName).put(value, key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
};
|
||||
|
||||
/** 读取一条记录,不存在时返回 undefined。 */
|
||||
export const idbGet = async <T = unknown>(
|
||||
dbName: string,
|
||||
storeName: string,
|
||||
key: IDBValidKey,
|
||||
): Promise<T | undefined> => {
|
||||
const db = await openIndexedDB(dbName, storeName);
|
||||
try {
|
||||
return await new Promise<T | undefined>((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readonly');
|
||||
const request = tx.objectStore(storeName).get(key);
|
||||
request.onsuccess = () => resolve(request.result as T | undefined);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
};
|
||||
|
||||
/** 删除一条记录。 */
|
||||
export const idbDelete = async (dbName: string, storeName: string, key: IDBValidKey): Promise<void> => {
|
||||
const db = await openIndexedDB(dbName, storeName);
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
tx.objectStore(storeName).delete(key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onabort = () => reject(tx.error);
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
};
|
||||
@ -18,8 +18,59 @@
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
// #region SerializedUndoRedo
|
||||
/**
|
||||
* UndoRedo 栈的可序列化快照,用于持久化(如写入 IndexedDB)后再还原。
|
||||
*/
|
||||
export interface SerializedUndoRedo<T = any> {
|
||||
/** 栈内全部元素(按时间正序,索引 0 为最早一步)。 */
|
||||
elementList: T[];
|
||||
/** 游标位置(已应用步骤数量)。 */
|
||||
listCursor: number;
|
||||
/** 栈容量上限。 */
|
||||
listMaxSize: number;
|
||||
}
|
||||
// #endregion SerializedUndoRedo
|
||||
|
||||
// #region UndoRedo
|
||||
export class UndoRedo<T = any> {
|
||||
/**
|
||||
* 由 {@link UndoRedo.serialize} 产出的快照重建一个 UndoRedo 实例。
|
||||
* 游标会被夹紧到 [0, length] 区间,避免脏数据导致越界。
|
||||
*
|
||||
* @param options.isSavedStep 可选谓词:若提供,则把游标定位到「最近一条满足该谓词的记录」之后
|
||||
* (即恢复到最近一个已保存点);找不到匹配记录时退回快照中的原游标。
|
||||
*/
|
||||
public static fromSerialized<T = any>(
|
||||
data: SerializedUndoRedo<T>,
|
||||
options: { isSavedStep?: (element: T) => boolean } = {},
|
||||
): UndoRedo<T> {
|
||||
const undoRedo = new UndoRedo<T>(data.listMaxSize);
|
||||
const list = Array.isArray(data.elementList) ? data.elementList.map((item) => cloneDeep(item)) : [];
|
||||
let cursor = Number.isFinite(data.listCursor) ? data.listCursor : list.length;
|
||||
|
||||
// 本地数据同样遵循容量上限:超出时裁掉最旧的记录(与 pushElement 的 shift 行为一致),并同步回退游标。
|
||||
const overflow = list.length - undoRedo.listMaxSize;
|
||||
if (overflow > 0) {
|
||||
list.splice(0, overflow);
|
||||
cursor -= overflow;
|
||||
}
|
||||
|
||||
// 若指定了「已保存」谓词,则把游标移动到最近一条已保存记录之后;在裁剪后的 list 上查找以保证索引正确。
|
||||
if (options.isSavedStep) {
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
if (options.isSavedStep(list[i])) {
|
||||
cursor = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
undoRedo.elementList = list;
|
||||
undoRedo.listCursor = Math.max(0, Math.min(cursor, list.length));
|
||||
return undoRedo;
|
||||
}
|
||||
|
||||
private elementList: T[];
|
||||
private listCursor: number;
|
||||
private listMaxSize: number;
|
||||
@ -31,6 +82,18 @@ export class UndoRedo<T = any> {
|
||||
this.listMaxSize = listMaxSize > minListMaxSize ? listMaxSize : minListMaxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出当前栈的可序列化快照(深克隆,避免外部改动污染内部状态)。
|
||||
* 配合 {@link UndoRedo.fromSerialized} 可在持久化后完整还原撤销/重做栈。
|
||||
*/
|
||||
public serialize(): SerializedUndoRedo<T> {
|
||||
return {
|
||||
elementList: this.elementList.map((item) => cloneDeep(item)),
|
||||
listCursor: this.listCursor,
|
||||
listMaxSize: this.listMaxSize,
|
||||
};
|
||||
}
|
||||
|
||||
public pushElement(element: T): void {
|
||||
// 新元素进来时,把游标之外的元素全部丢弃,并把新元素放进来
|
||||
this.elementList.splice(this.listCursor, this.elementList.length - this.listCursor, cloneDeep(element));
|
||||
@ -76,6 +139,20 @@ export class UndoRedo<T = any> {
|
||||
return cloneDeep(this.elementList[this.listCursor - 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对当前游标所在元素(cursor - 1)做就地更新;cursor 为 0(全部已撤销)时不做任何操作。
|
||||
* 用于给「当前步骤」打标记(如标记为已保存)等元数据写入场景。
|
||||
*/
|
||||
public updateCurrentElement(updater: (element: T) => void): void {
|
||||
if (this.listCursor < 1) return;
|
||||
updater(this.elementList[this.listCursor - 1]);
|
||||
}
|
||||
|
||||
/** 对栈内全部元素做就地更新。用于批量清理元数据(如清空所有元素的已保存标记)。 */
|
||||
public updateElements(updater: (element: T, index: number) => void): void {
|
||||
this.elementList.forEach(updater);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回栈内全部元素的浅克隆数组(按时间顺序,索引 0 为最早一步)。
|
||||
* 仅用于历史面板等只读展示场景,不应直接修改返回值。
|
||||
|
||||
@ -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 事件', () => {
|
||||
|
||||
@ -17,6 +17,9 @@ import {
|
||||
formatHistoryFullTime,
|
||||
formatHistoryTime,
|
||||
groupTimestamp,
|
||||
isCodeBlockStepRevertable,
|
||||
isDataSourceStepRevertable,
|
||||
isPageStepRevertable,
|
||||
opLabel,
|
||||
useHistoryList,
|
||||
} from '@editor/layouts/history-list/composables';
|
||||
@ -607,3 +610,82 @@ describe('useHistoryList', () => {
|
||||
expect(buckets.map((b) => b.id).sort()).toEqual(['code_1', 'code_2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPageStepRevertable', () => {
|
||||
test('add / remove 始终可回滚', () => {
|
||||
expect(isPageStepRevertable({ opType: 'add', nodes: [{ id: 'n1' }] } as any)).toBe(true);
|
||||
expect(isPageStepRevertable({ opType: 'remove', removedItems: [{ node: { id: 'n1' } }] } as any)).toBe(true);
|
||||
});
|
||||
|
||||
test('update 每项都有 changeRecords 才可回滚', () => {
|
||||
expect(
|
||||
isPageStepRevertable({
|
||||
opType: 'update',
|
||||
updatedItems: [{ oldNode: { id: 'n1' }, newNode: { id: 'n1' }, changeRecords: [{ propPath: 'style.color' }] }],
|
||||
} as any),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('update 缺少 changeRecords 不可回滚', () => {
|
||||
expect(
|
||||
isPageStepRevertable({
|
||||
opType: 'update',
|
||||
updatedItems: [{ oldNode: { id: 'n1' }, newNode: { id: 'n1' } }],
|
||||
} as any),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('update 多项中任一缺少 changeRecords 不可回滚', () => {
|
||||
expect(
|
||||
isPageStepRevertable({
|
||||
opType: 'update',
|
||||
updatedItems: [
|
||||
{ oldNode: { id: 'n1' }, newNode: { id: 'n1' }, changeRecords: [{ propPath: 'a' }] },
|
||||
{ oldNode: { id: 'n2' }, newNode: { id: 'n2' } },
|
||||
],
|
||||
} as any),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('update 无 updatedItems 不可回滚', () => {
|
||||
expect(isPageStepRevertable({ opType: 'update' } as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDataSourceStepRevertable', () => {
|
||||
test('新增 / 删除 始终可回滚', () => {
|
||||
expect(isDataSourceStepRevertable({ oldSchema: null, newSchema: { id: 'ds_1' } } as any)).toBe(true);
|
||||
expect(isDataSourceStepRevertable({ oldSchema: { id: 'ds_1' }, newSchema: null } as any)).toBe(true);
|
||||
});
|
||||
|
||||
test('更新有 changeRecords 才可回滚', () => {
|
||||
expect(
|
||||
isDataSourceStepRevertable({
|
||||
oldSchema: { id: 'ds_1' },
|
||||
newSchema: { id: 'ds_1' },
|
||||
changeRecords: [{ propPath: 'title' }],
|
||||
} as any),
|
||||
).toBe(true);
|
||||
expect(isDataSourceStepRevertable({ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' } } as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCodeBlockStepRevertable', () => {
|
||||
test('新增 / 删除 始终可回滚', () => {
|
||||
expect(isCodeBlockStepRevertable({ oldContent: null, newContent: { id: 'code_1' } } as any)).toBe(true);
|
||||
expect(isCodeBlockStepRevertable({ oldContent: { id: 'code_1' }, newContent: null } as any)).toBe(true);
|
||||
});
|
||||
|
||||
test('更新有 changeRecords 才可回滚', () => {
|
||||
expect(
|
||||
isCodeBlockStepRevertable({
|
||||
oldContent: { id: 'code_1' },
|
||||
newContent: { id: 'code_1' },
|
||||
changeRecords: [{ propPath: 'content' }],
|
||||
} as any),
|
||||
).toBe(true);
|
||||
expect(isCodeBlockStepRevertable({ oldContent: { id: 'code_1' }, newContent: { id: 'code_1' } } as any)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -231,6 +231,98 @@ describe('CodeBlockService - 历史记录接入', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodeBlockService - *AndGetHistoryId', () => {
|
||||
const lastStepUuid = (id: string) => {
|
||||
const list = historyService.getCodeBlockStepList(id);
|
||||
return list[list.length - 1]?.step.uuid;
|
||||
};
|
||||
|
||||
test('setCodeDslByIdSyncAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
|
||||
const historyId = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid('a'));
|
||||
// 与默认行为一致:内容仍被写入
|
||||
expect(codeBlockService.getCodeContentById('a')?.name).toBe('A');
|
||||
});
|
||||
|
||||
test('setCodeDslByIdSyncAndGetHistoryId - force=false 已存在时返回 null', async () => {
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
|
||||
const historyId = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'NEW' } as any, false);
|
||||
expect(historyId).toBeNull();
|
||||
});
|
||||
|
||||
test('setCodeDslByIdSyncAndGetHistoryId - doNotPushHistory 时返回 null', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
const historyId = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any, true, {
|
||||
doNotPushHistory: true,
|
||||
});
|
||||
expect(historyId).toBeNull();
|
||||
});
|
||||
|
||||
test('setCodeDslByIdAndGetHistoryId(async)返回本次写入历史记录的 uuid', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
|
||||
const historyId = await codeBlockService.setCodeDslByIdAndGetHistoryId('a', { name: 'A' } as any);
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid('a'));
|
||||
});
|
||||
|
||||
test('deleteCodeDslByIdsAndGetHistoryId 返回每条删除记录的 uuid 数组', async () => {
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' }, b: { name: 'B' } } as any);
|
||||
|
||||
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(['a', 'b']);
|
||||
expect(Array.isArray(historyIds)).toBe(true);
|
||||
expect(historyIds).toHaveLength(2);
|
||||
expect(historyIds[0]).toBe(lastStepUuid('a'));
|
||||
expect(historyIds[1]).toBe(lastStepUuid('b'));
|
||||
});
|
||||
|
||||
test('deleteCodeDslByIdsAndGetHistoryId - 不存在的 id 不计入返回数组', async () => {
|
||||
await codeBlockService.setCodeDsl({ a: { name: 'A' } } as any);
|
||||
|
||||
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(['a', 'ghost']);
|
||||
expect(historyIds).toHaveLength(1);
|
||||
expect(historyIds[0]).toBe(lastStepUuid('a'));
|
||||
});
|
||||
|
||||
test('deleteCodeDslByIdsAndGetHistoryId - 全部不存在时返回空数组', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
const historyIds = await codeBlockService.deleteCodeDslByIdsAndGetHistoryId(['ghost']);
|
||||
expect(historyIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodeBlockService - revertById', () => {
|
||||
test('通过 uuid 回滚新增(删除代码块内容)', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
const uuid = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
|
||||
expect(typeof uuid).toBe('string');
|
||||
expect(codeBlockService.getCodeContentById('a')?.name).toBe('A');
|
||||
|
||||
const reverted = await codeBlockService.revertById(uuid!);
|
||||
expect(reverted).not.toBeNull();
|
||||
expect(codeBlockService.getCodeContentById('a')).toBeNull();
|
||||
});
|
||||
|
||||
test('按 uuid 能定位到对应 (id, index)', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
const uuid = codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
|
||||
|
||||
const location = historyService.findCodeBlockStepLocationByUuid(uuid!);
|
||||
expect(location).toEqual({ id: 'a', index: 0 });
|
||||
});
|
||||
|
||||
test('找不到 uuid 时返回 null', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
codeBlockService.setCodeDslByIdSyncAndGetHistoryId('a', { name: 'A' } as any);
|
||||
|
||||
await expect(codeBlockService.revertById('not-exist')).resolves.toBeNull();
|
||||
await expect(codeBlockService.revertById('')).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodeBlockService - undo / redo', () => {
|
||||
test('undo / redo - 新增场景:撤销=删除,重做=再写回', async () => {
|
||||
await codeBlockService.setCodeDsl({} as any);
|
||||
|
||||
@ -187,6 +187,79 @@ describe('DataSource service - 历史记录接入', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataSource service - *AndGetHistoryId', () => {
|
||||
const lastStepUuid = (id: string) => {
|
||||
const list = historyService.getDataSourceStepList(id);
|
||||
return list[list.length - 1]?.step.uuid;
|
||||
};
|
||||
|
||||
test('addAndGetHistoryId 返回本次写入历史记录的 uuid', () => {
|
||||
const ds = dataSource.add({ id: 'temp', title: 'a', type: 'base' } as any);
|
||||
historyService.reset();
|
||||
|
||||
const historyId = dataSource.addAndGetHistoryId({ id: 'ds_new', title: 'a', type: 'base' } as any);
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid('ds_new'));
|
||||
// 与默认 add 行为一致:仍会写入数据源
|
||||
expect(dataSource.getDataSourceById('ds_new')).toBeDefined();
|
||||
expect(ds).toBeDefined();
|
||||
});
|
||||
|
||||
test('addAndGetHistoryId 传 doNotPushHistory 时返回 null', () => {
|
||||
const historyId = dataSource.addAndGetHistoryId({ id: 'ds_x', title: 'a', type: 'base' } as any, {
|
||||
doNotPushHistory: true,
|
||||
});
|
||||
expect(historyId).toBeNull();
|
||||
});
|
||||
|
||||
test('updateAndGetHistoryId 返回本次写入历史记录的 uuid', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
historyService.reset();
|
||||
|
||||
const historyId = dataSource.updateAndGetHistoryId({ ...created, title: 'b' } as any);
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid(created.id!));
|
||||
});
|
||||
|
||||
test('removeAndGetHistoryId 返回本次写入历史记录的 uuid;不存在的 id 返回 null', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
historyService.reset();
|
||||
|
||||
const historyId = dataSource.removeAndGetHistoryId(created.id!);
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid(created.id!));
|
||||
|
||||
expect(dataSource.removeAndGetHistoryId('ghost')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataSource service - revertById', () => {
|
||||
test('通过 uuid 回滚 add(移除数据源)', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
const uuid = historyService.getDataSourceStepList(created.id!).slice(-1)[0]?.step.uuid;
|
||||
expect(typeof uuid).toBe('string');
|
||||
expect(dataSource.getDataSourceById(created.id!)).toBeDefined();
|
||||
|
||||
const reverted = dataSource.revertById(uuid!);
|
||||
expect(reverted).not.toBeNull();
|
||||
expect(dataSource.getDataSourceById(created.id!)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('通过 uuid 回滚等价于按 (id, index) 回滚', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
const uuid = historyService.getDataSourceStepList(created.id!).slice(-1)[0]?.step.uuid;
|
||||
|
||||
const location = historyService.findDataSourceStepLocationByUuid(uuid!);
|
||||
expect(location).toEqual({ id: created.id, index: 0 });
|
||||
});
|
||||
|
||||
test('找不到 uuid 时返回 null', () => {
|
||||
dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
expect(dataSource.revertById('not-exist')).toBeNull();
|
||||
expect(dataSource.revertById('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DataSource service - undo / redo', () => {
|
||||
test('undo / redo - 新增场景:撤销=移除,重做=再添加', () => {
|
||||
const created = dataSource.add({ title: 'a', type: 'base' } as any);
|
||||
|
||||
@ -711,3 +711,99 @@ describe('undo redo', () => {
|
||||
expect(editorService.getNodeById(NodeId.NODE_ID)?.style?.width).toBe(270);
|
||||
});
|
||||
});
|
||||
|
||||
describe('*AndGetHistoryId', () => {
|
||||
const lastStepUuid = () => {
|
||||
const list = historyService.getPageStepList();
|
||||
return list[list.length - 1]?.step.uuid;
|
||||
};
|
||||
|
||||
test('addAndGetHistoryId 返回本次写入历史记录的 uuid,且与栈顶 step 一致', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const historyId = await editorService.addAndGetHistoryId({ type: 'text' });
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBeTruthy();
|
||||
expect(historyId).toBe(lastStepUuid());
|
||||
});
|
||||
|
||||
test('addAndGetHistoryId 传 doNotPushHistory 时返回 null', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const historyId = await editorService.addAndGetHistoryId({ type: 'text' }, null, { doNotPushHistory: true });
|
||||
expect(historyId).toBeNull();
|
||||
});
|
||||
|
||||
test('updateAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const historyId = await editorService.updateAndGetHistoryId({ id: NodeId.NODE_ID, type: 'text', text: 'x' });
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid());
|
||||
});
|
||||
|
||||
test('removeAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const historyId = await editorService.removeAndGetHistoryId({ id: NodeId.NODE_ID, type: 'text' });
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid());
|
||||
});
|
||||
|
||||
test('moveLayerAndGetHistoryId 返回本次写入历史记录的 uuid', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.NODE_ID);
|
||||
|
||||
const historyId = await editorService.moveLayerAndGetHistoryId(1);
|
||||
expect(typeof historyId).toBe('string');
|
||||
expect(historyId).toBe(lastStepUuid());
|
||||
});
|
||||
});
|
||||
|
||||
describe('revertPageStepById', () => {
|
||||
test('通过 uuid 回滚 add 步骤(删除被新增节点)', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const uuid = await editorService.addAndGetHistoryId({ type: 'text' });
|
||||
expect(typeof uuid).toBe('string');
|
||||
|
||||
const addedStep = historyService.getPageStepList().find((e) => e.step.uuid === uuid)!.step;
|
||||
const addedId = addedStep.nodes![0].id;
|
||||
expect(editorService.getNodeById(addedId)).toBeTruthy();
|
||||
|
||||
const reverted = await editorService.revertPageStepById(uuid!);
|
||||
expect(reverted).not.toBeNull();
|
||||
// 回滚(git revert 语义)会把被新增的节点删掉
|
||||
expect(editorService.getNodeById(addedId)).toBeNull();
|
||||
});
|
||||
|
||||
test('与按 index 回滚结果一致', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const uuid = await editorService.addAndGetHistoryId({ type: 'text' });
|
||||
const index = historyService.getPageStepIndexByUuid(uuid!);
|
||||
expect(index).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('找不到 uuid 时返回 null', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
historyService.reset();
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
expect(await editorService.revertPageStepById('not-exist')).toBeNull();
|
||||
expect(await editorService.revertPageStepById('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
240
packages/editor/tests/unit/services/history-persist.spec.ts
Normal file
240
packages/editor/tests/unit/services/history-persist.spec.ts
Normal file
@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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 { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import history from '@editor/services/history';
|
||||
import { setEditorConfig } from '@editor/utils/config';
|
||||
import * as indexedDb from '@editor/utils/indexed-db';
|
||||
|
||||
// 用内存实现 mock 掉 IndexedDB 读写工具,避免依赖真实 IndexedDB(happy-dom 不提供)。
|
||||
vi.mock('@editor/utils/indexed-db', () => {
|
||||
const store = new Map<string, any>();
|
||||
const k = (db: string, s: string, key: any) => `${db}__${s}__${String(key)}`;
|
||||
return {
|
||||
isIndexedDBSupported: () => true,
|
||||
openIndexedDB: vi.fn(),
|
||||
idbSet: vi.fn(async (db: string, s: string, key: any, value: any) => {
|
||||
store.set(k(db, s, key), value);
|
||||
}),
|
||||
idbGet: vi.fn(async (db: string, s: string, key: any) => store.get(k(db, s, key))),
|
||||
idbDelete: vi.fn(async (db: string, s: string, key: any) => {
|
||||
store.delete(k(db, s, key));
|
||||
}),
|
||||
__store: store,
|
||||
};
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
// restoreFromIndexedDB 通过 parseDSL 还原序列化字符串(默认实现即 eval)。
|
||||
// eslint-disable-next-line no-eval
|
||||
setEditorConfig({ parseDSL: (dsl: string) => eval(dsl) } as any);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
(indexedDb as any).__store.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
history.reset();
|
||||
});
|
||||
|
||||
const pageStep = (id = 'p1') => ({ data: { id, name: '' }, modifiedNodeIds: new Map() }) as any;
|
||||
|
||||
describe('history service - markSaved', () => {
|
||||
test('markSaved 标记页面 / 代码块 / 数据源各栈的当前记录', () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
|
||||
|
||||
history.markSaved();
|
||||
|
||||
expect((history.state.pageSteps as any).p1.getCurrentElement().saved).toBe(true);
|
||||
expect((history.state.codeBlockState as any).code_1.getCurrentElement().saved).toBe(true);
|
||||
expect((history.state.dataSourceState as any).ds_1.getCurrentElement().saved).toBe(true);
|
||||
});
|
||||
|
||||
test('markSaved 派发 mark-saved 事件并带 kind=all', () => {
|
||||
const fn = vi.fn();
|
||||
history.on('mark-saved', fn);
|
||||
history.markSaved();
|
||||
expect(fn).toHaveBeenCalledWith({ kind: 'all' });
|
||||
history.off('mark-saved', fn);
|
||||
});
|
||||
|
||||
test('同一栈最多保留一条 saved:再次标记会清除旧标记', () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
history.markPageSaved();
|
||||
history.push(pageStep());
|
||||
history.markPageSaved();
|
||||
|
||||
const list = (history.state.pageSteps as any).p1.getElementList();
|
||||
expect(list.filter((s: any) => s.saved)).toHaveLength(1);
|
||||
// 最新一条才是 saved
|
||||
expect(list[list.length - 1].saved).toBe(true);
|
||||
expect(list[0].saved).toBeFalsy();
|
||||
});
|
||||
|
||||
test('markPageSaved / markCodeBlockSaved / markDataSourceSaved 仅影响对应栈', () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
|
||||
history.markCodeBlockSaved('code_1');
|
||||
expect((history.state.codeBlockState as any).code_1.getCurrentElement().saved).toBe(true);
|
||||
// 页面栈未被标记
|
||||
expect((history.state.pageSteps as any).p1.getCurrentElement().saved).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('history service - clear', () => {
|
||||
test('clearPage 清空当前页面历史并复位 canUndo/canRedo', () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
history.push(pageStep());
|
||||
expect(history.state.canUndo).toBe(true);
|
||||
|
||||
history.clearPage();
|
||||
expect((history.state.pageSteps as any).p1.getLength()).toBe(0);
|
||||
expect(history.state.canUndo).toBe(false);
|
||||
expect(history.state.canRedo).toBe(false);
|
||||
});
|
||||
|
||||
test('clearCodeBlock 传 id 清单个,缺省清全部', () => {
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.pushCodeBlock('code_2', { oldContent: null, newContent: { name: 'B' } as any });
|
||||
|
||||
history.clearCodeBlock('code_1');
|
||||
expect((history.state.codeBlockState as any).code_1).toBeUndefined();
|
||||
expect((history.state.codeBlockState as any).code_2).toBeDefined();
|
||||
|
||||
history.clearCodeBlock();
|
||||
expect(Object.keys(history.state.codeBlockState)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('clearDataSource 传 id 清单个,缺省清全部', () => {
|
||||
history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
|
||||
history.pushDataSource('ds_2', { oldSchema: null, newSchema: { id: 'ds_2' } as any });
|
||||
|
||||
history.clearDataSource('ds_1');
|
||||
expect((history.state.dataSourceState as any).ds_1).toBeUndefined();
|
||||
expect((history.state.dataSourceState as any).ds_2).toBeDefined();
|
||||
|
||||
history.clearDataSource();
|
||||
expect(Object.keys(history.state.dataSourceState)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('history service - IndexedDB 持久化', () => {
|
||||
test('saveToIndexedDB 以序列化字符串写入并返回快照对象', async () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
|
||||
const snapshot = await history.saveToIndexedDB();
|
||||
expect(snapshot.version).toBe(1);
|
||||
expect(snapshot.pageId).toBe('p1');
|
||||
// 实际写入 IndexedDB 的是字符串(serialize-javascript 结果)
|
||||
expect(indexedDb.idbSet).toHaveBeenCalled();
|
||||
const written = (indexedDb.idbSet as any).mock.calls[0][3];
|
||||
expect(typeof written).toBe('string');
|
||||
});
|
||||
|
||||
test('restoreFromIndexedDB 还原页面 / 代码块 / 数据源全部栈与游标', async () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
history.push(pageStep());
|
||||
history.undo(); // page cursor = 1
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
|
||||
|
||||
await history.saveToIndexedDB();
|
||||
history.reset();
|
||||
expect(Object.keys(history.state.pageSteps)).toHaveLength(0);
|
||||
|
||||
const restored = await history.restoreFromIndexedDB();
|
||||
expect(restored).not.toBeNull();
|
||||
expect(history.state.pageId).toBe('p1');
|
||||
expect(history.getPageCursor('p1')).toBe(1);
|
||||
expect((history.state.codeBlockState as any).code_1).toBeDefined();
|
||||
expect((history.state.dataSourceState as any).ds_1).toBeDefined();
|
||||
});
|
||||
|
||||
test('restoreFromIndexedDB 把游标恢复到最近一个已保存记录', async () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
history.push(pageStep());
|
||||
history.markPageSaved(); // 标记 index 1(cursor=2)
|
||||
history.push(pageStep()); // cursor=3,saved 仍在 index 1
|
||||
|
||||
await history.saveToIndexedDB();
|
||||
history.reset();
|
||||
|
||||
await history.restoreFromIndexedDB();
|
||||
// 恢复后游标定位到已保存记录之后:index 1 -> cursor 2
|
||||
expect(history.getPageCursor('p1')).toBe(2);
|
||||
});
|
||||
|
||||
test('restoreFromIndexedDB 能还原内容中的函数(serialize + parseDSL 往返)', async () => {
|
||||
history.pushCodeBlock('code_1', {
|
||||
oldContent: null,
|
||||
newContent: {
|
||||
name: 'A',
|
||||
code() {
|
||||
return 42;
|
||||
},
|
||||
} as any,
|
||||
});
|
||||
|
||||
await history.saveToIndexedDB();
|
||||
history.reset();
|
||||
await history.restoreFromIndexedDB();
|
||||
|
||||
const current = (history.state.codeBlockState as any).code_1.getCurrentElement();
|
||||
expect(typeof current.newContent.code).toBe('function');
|
||||
expect(current.newContent.code()).toBe(42);
|
||||
});
|
||||
|
||||
test('restoreFromIndexedDB 找不到记录时返回 null 且不改动当前状态', async () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
|
||||
const restored = await history.restoreFromIndexedDB();
|
||||
expect(restored).toBeNull();
|
||||
// 当前状态保持不变
|
||||
expect((history.state.pageSteps as any).p1.getLength()).toBe(1);
|
||||
});
|
||||
|
||||
test('saveToIndexedDB 派发 save-to-indexed-db、restoreFromIndexedDB 派发 restore-from-indexed-db', async () => {
|
||||
const saveFn = vi.fn();
|
||||
const restoreFn = vi.fn();
|
||||
history.on('save-to-indexed-db', saveFn);
|
||||
history.on('restore-from-indexed-db', restoreFn);
|
||||
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
history.push(pageStep());
|
||||
await history.saveToIndexedDB();
|
||||
await history.restoreFromIndexedDB();
|
||||
|
||||
expect(saveFn).toHaveBeenCalledTimes(1);
|
||||
expect(restoreFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
history.off('save-to-indexed-db', saveFn);
|
||||
history.off('restore-from-indexed-db', restoreFn);
|
||||
});
|
||||
});
|
||||
@ -103,6 +103,32 @@ describe('history service', () => {
|
||||
} as any);
|
||||
expect(step?.timestamp).toBe(123456);
|
||||
});
|
||||
|
||||
test('push 未带 uuid 时自动生成 uuid', () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
const step = history.push({ data: { id: 'p1', name: '' }, modifiedNodeIds: new Map() } as any);
|
||||
expect(typeof step?.uuid).toBe('string');
|
||||
expect(step?.uuid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('push 已带 uuid 时保留调用方指定的值', () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
const step = history.push({
|
||||
uuid: 'my-uuid',
|
||||
data: { id: 'p1', name: '' },
|
||||
modifiedNodeIds: new Map(),
|
||||
} as any);
|
||||
expect(step?.uuid).toBe('my-uuid');
|
||||
});
|
||||
|
||||
test('push 为每条记录生成不同的 uuid', () => {
|
||||
history.changePage({ id: 'p1' } as any);
|
||||
const s1 = history.push({ data: { id: 'p1', name: '' }, modifiedNodeIds: new Map() } as any);
|
||||
const s2 = history.push({ data: { id: 'p1', name: '' }, modifiedNodeIds: new Map() } as any);
|
||||
expect(s1?.uuid).toBeTruthy();
|
||||
expect(s2?.uuid).toBeTruthy();
|
||||
expect(s1?.uuid).not.toBe(s2?.uuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('history service - codeBlock', () => {
|
||||
@ -138,6 +164,12 @@ describe('history service - codeBlock', () => {
|
||||
expect(step?.timestamp).toBeLessThanOrEqual(after);
|
||||
});
|
||||
|
||||
test('pushCodeBlock 自动生成 uuid', () => {
|
||||
const step = history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
expect(typeof step?.uuid).toBe('string');
|
||||
expect(step?.uuid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('undoCodeBlock / redoCodeBlock 走对应 id 的 UndoRedo 栈', () => {
|
||||
history.pushCodeBlock('code_1', { oldContent: null, newContent: { name: 'A' } as any });
|
||||
history.pushCodeBlock('code_1', {
|
||||
@ -218,6 +250,12 @@ describe('history service - dataSource', () => {
|
||||
expect(step?.timestamp).toBeLessThanOrEqual(after);
|
||||
});
|
||||
|
||||
test('pushDataSource 自动生成 uuid', () => {
|
||||
const step = history.pushDataSource('ds_1', { oldSchema: null, newSchema: { id: 'ds_1' } as any });
|
||||
expect(typeof step?.uuid).toBe('string');
|
||||
expect(step?.uuid).toBeTruthy();
|
||||
});
|
||||
|
||||
test('undoDataSource / redoDataSource 走对应 id 的 UndoRedo 栈', () => {
|
||||
history.pushDataSource('ds_1', {
|
||||
oldSchema: null,
|
||||
|
||||
@ -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 行为校验', () => {
|
||||
|
||||
@ -161,3 +161,120 @@ describe('list max size', () => {
|
||||
expect(small.canUndo()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCurrentElement / updateElements', () => {
|
||||
test('updateCurrentElement 就地更新当前游标元素', () => {
|
||||
const undoRedo = new UndoRedo();
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.updateCurrentElement((el: any) => {
|
||||
el.saved = true;
|
||||
});
|
||||
expect(undoRedo.getCurrentElement()).toEqual({ a: 2, saved: true });
|
||||
// 撤销后当前元素是更早的那条,不应被标记
|
||||
expect(undoRedo.undo()).toEqual({ a: 2, saved: true });
|
||||
expect(undoRedo.getCurrentElement()).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test('updateCurrentElement 在 cursor 为 0 时不做任何操作', () => {
|
||||
const undoRedo = new UndoRedo();
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.undo();
|
||||
let called = false;
|
||||
undoRedo.updateCurrentElement(() => {
|
||||
called = true;
|
||||
});
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
|
||||
test('updateElements 遍历就地更新全部元素', () => {
|
||||
const undoRedo = new UndoRedo();
|
||||
undoRedo.pushElement({ a: 1, saved: true });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.updateElements((el: any) => {
|
||||
el.saved = false;
|
||||
});
|
||||
const list = undoRedo.getElementList() as any[];
|
||||
expect(list.every((el) => el.saved === false)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serialize / fromSerialized', () => {
|
||||
test('serialize 导出快照并 fromSerialized 完整还原(含游标)', () => {
|
||||
const undoRedo = new UndoRedo(50);
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.pushElement({ a: 3 });
|
||||
undoRedo.undo(); // cursor = 2
|
||||
|
||||
const data = undoRedo.serialize();
|
||||
expect(data.elementList).toHaveLength(3);
|
||||
expect(data.listCursor).toBe(2);
|
||||
expect(data.listMaxSize).toBe(50);
|
||||
|
||||
const restored = UndoRedo.fromSerialized(data);
|
||||
expect(restored.getCursor()).toBe(2);
|
||||
expect(restored.getLength()).toBe(3);
|
||||
expect(restored.getCurrentElement()).toEqual({ a: 2 });
|
||||
expect(restored.canRedo()).toBe(true);
|
||||
expect(restored.redo()).toEqual({ a: 3 });
|
||||
});
|
||||
|
||||
test('serialize 为深克隆,修改原栈不影响快照', () => {
|
||||
const undoRedo = new UndoRedo();
|
||||
const el: any = { a: 1 };
|
||||
undoRedo.pushElement(el);
|
||||
const data = undoRedo.serialize();
|
||||
undoRedo.updateCurrentElement((cur: any) => {
|
||||
cur.a = 999;
|
||||
});
|
||||
expect((data.elementList[0] as any).a).toBe(1);
|
||||
});
|
||||
|
||||
test('fromSerialized 超出 listMaxSize 时裁掉最旧记录并回退游标', () => {
|
||||
const restored = UndoRedo.fromSerialized({
|
||||
elementList: [{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }],
|
||||
listCursor: 4,
|
||||
listMaxSize: 2,
|
||||
});
|
||||
expect(restored.getLength()).toBe(2);
|
||||
// 保留最近两条,cursor 同步回退到 2
|
||||
expect(restored.getElementList()).toEqual([{ a: 3 }, { a: 4 }]);
|
||||
expect(restored.getCursor()).toBe(2);
|
||||
});
|
||||
|
||||
test('fromSerialized 游标越界时夹紧到 [0, length]', () => {
|
||||
const restored = UndoRedo.fromSerialized({
|
||||
elementList: [{ a: 1 }, { a: 2 }],
|
||||
listCursor: 99,
|
||||
listMaxSize: 100,
|
||||
});
|
||||
expect(restored.getCursor()).toBe(2);
|
||||
});
|
||||
|
||||
test('fromSerialized isSavedStep 把游标定位到最近一条已保存记录之后', () => {
|
||||
const restored = UndoRedo.fromSerialized<{ a: number; saved?: boolean }>(
|
||||
{
|
||||
elementList: [{ a: 1 }, { a: 2, saved: true }, { a: 3 }, { a: 4 }],
|
||||
listCursor: 4,
|
||||
listMaxSize: 100,
|
||||
},
|
||||
{ isSavedStep: (el) => el.saved === true },
|
||||
);
|
||||
// 最近一条已保存记录在 index 1,游标应为 2
|
||||
expect(restored.getCursor()).toBe(2);
|
||||
expect(restored.getCurrentElement()).toEqual({ a: 2, saved: true });
|
||||
});
|
||||
|
||||
test('fromSerialized isSavedStep 无匹配时退回原游标', () => {
|
||||
const restored = UndoRedo.fromSerialized<{ a: number; saved?: boolean }>(
|
||||
{
|
||||
elementList: [{ a: 1 }, { a: 2 }],
|
||||
listCursor: 1,
|
||||
listMaxSize: 100,
|
||||
},
|
||||
{ isSavedStep: (el) => el.saved === true },
|
||||
);
|
||||
expect(restored.getCursor()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/element-plus-adapter",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/form-schema",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/form",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/schema",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/stage",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/table",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/tdesign-vue-next-adapter",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"name": "@tmagic/utils",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tmagic-playground",
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -12,11 +12,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@tmagic/core": "1.8.0-beta.3",
|
||||
"@tmagic/design": "1.8.0-beta.3",
|
||||
"@tmagic/editor": "1.8.0-beta.3",
|
||||
"@tmagic/element-plus-adapter": "1.8.0-beta.3",
|
||||
"@tmagic/tdesign-vue-next-adapter": "1.8.0-beta.3",
|
||||
"@tmagic/core": "1.8.0-beta.4",
|
||||
"@tmagic/design": "1.8.0-beta.4",
|
||||
"@tmagic/editor": "1.8.0-beta.4",
|
||||
"@tmagic/element-plus-adapter": "1.8.0-beta.4",
|
||||
"@tmagic/tdesign-vue-next-adapter": "1.8.0-beta.4",
|
||||
"@tmagic/tmagic-form-runtime": "1.1.3",
|
||||
"element-plus": "^2.11.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@ -47,12 +47,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, ref, shallowRef, toRaw } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, toRaw } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
import type { MApp, MContainer, MNode } from '@tmagic/core';
|
||||
import type { DatasourceTypeOption } from '@tmagic/editor';
|
||||
import {
|
||||
editorService,
|
||||
historyService,
|
||||
propsService,
|
||||
serializeConfig,
|
||||
TMagicDialog,
|
||||
@ -96,6 +98,8 @@ const { moveableOptions } = useEditorMoveableOptions(editor);
|
||||
const save = () => {
|
||||
localStorage.setItem('magicDSL', serializeConfig(toRaw(value.value)));
|
||||
editor.value?.editorService.resetModifiedNodeId();
|
||||
// 标记当前历史记录为已保存,从 IndexedDB 恢复时会把游标定位到此处。
|
||||
historyService.markSaved();
|
||||
};
|
||||
|
||||
const { menu, deviceGroup, iframe, previewVisible } = useEditorMenu(value, save);
|
||||
@ -133,7 +137,46 @@ propsService.usePlugin({
|
||||
beforeFillConfig: (config) => [config, '100px'],
|
||||
});
|
||||
|
||||
// 把当前历史记录持久化到 IndexedDB(按 DSL app id 隔离)。
|
||||
// 注意:beforeunload / pagehide 阶段无法 await 异步 IndexedDB 事务,能写多少算多少。
|
||||
const persistHistory = () => {
|
||||
historyService.saveToIndexedDB().catch((error) => console.error('持久化历史记录失败', error));
|
||||
};
|
||||
|
||||
// 历史变更后防抖持久化:页面 / 数据源 / 代码块任一栈变化都及时写入 IndexedDB。
|
||||
// 仅靠 beforeunload/pagehide 的异步写入不可靠(事务可能来不及提交),会导致刷新后最近一次
|
||||
// 编辑(尤其是本次会话新增的代码块 / 数据源历史)丢失,这里改为变更即落库以保证恢复完整。
|
||||
const schedulePersist = debounce(persistHistory, 500);
|
||||
|
||||
// 进入页面时从 IndexedDB 恢复历史记录,并对齐到当前激活页,保证 undo/redo 作用于正在编辑的页面。
|
||||
const restoreHistory = async () => {
|
||||
try {
|
||||
const snapshot = await historyService.restoreFromIndexedDB();
|
||||
if (!snapshot) return;
|
||||
const page = editorService.get('page');
|
||||
if (page) historyService.changePage(page);
|
||||
} catch (error) {
|
||||
console.error('恢复历史记录失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
restoreHistory();
|
||||
historyService.on('change', schedulePersist);
|
||||
historyService.on('code-block-history-change', schedulePersist);
|
||||
historyService.on('data-source-history-change', schedulePersist);
|
||||
window.addEventListener('beforeunload', persistHistory);
|
||||
window.addEventListener('pagehide', persistHistory);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
schedulePersist.cancel();
|
||||
persistHistory();
|
||||
historyService.off('change', schedulePersist);
|
||||
historyService.off('code-block-history-change', schedulePersist);
|
||||
historyService.off('data-source-history-change', schedulePersist);
|
||||
window.removeEventListener('beforeunload', persistHistory);
|
||||
window.removeEventListener('pagehide', persistHistory);
|
||||
editorService.removeAllPlugins();
|
||||
});
|
||||
|
||||
|
||||
228
pnpm-lock.yaml
generated
228
pnpm-lock.yaml
generated
@ -557,23 +557,23 @@ importers:
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/core':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(typescript@6.0.3)
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/design':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/editor':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/element-plus-adapter':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/tdesign-vue-next-adapter':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/tmagic-form-runtime':
|
||||
specifier: 1.1.3
|
||||
version: 1.1.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
version: 1.1.3(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
element-plus:
|
||||
specifier: ^2.11.8
|
||||
version: 2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
|
||||
@ -913,14 +913,14 @@ importers:
|
||||
runtime/react:
|
||||
dependencies:
|
||||
'@tmagic/core':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(typescript@6.0.3)
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/react-runtime-help':
|
||||
specifier: 0.2.2
|
||||
version: 0.2.2(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.17.21)(react@18.3.1)(typescript@6.0.3)
|
||||
version: 0.2.2(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.17.21)(react@18.3.1)(typescript@6.0.3)
|
||||
'@tmagic/stage':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
axios:
|
||||
specifier: ^1.13.2
|
||||
version: 1.13.2
|
||||
@ -935,8 +935,8 @@ importers:
|
||||
version: 18.3.1(react@18.3.1)
|
||||
devDependencies:
|
||||
'@tmagic/cli':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(typescript@6.0.3)
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
@ -1011,14 +1011,14 @@ importers:
|
||||
runtime/vue:
|
||||
dependencies:
|
||||
'@tmagic/core':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(typescript@6.0.3)
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/stage':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/vue-runtime-help':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.2(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
version: 2.0.2(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
axios:
|
||||
specifier: ^1.13.2
|
||||
version: 1.13.2
|
||||
@ -1027,8 +1027,8 @@ importers:
|
||||
version: 3.5.34(typescript@6.0.3)
|
||||
devDependencies:
|
||||
'@tmagic/cli':
|
||||
specifier: 1.8.0-beta.3
|
||||
version: 1.8.0-beta.3(typescript@6.0.3)
|
||||
specifier: 1.8.0-beta.4
|
||||
version: 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
@ -2912,8 +2912,8 @@ packages:
|
||||
'@sxzz/popperjs-es@2.11.7':
|
||||
resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
|
||||
|
||||
'@tmagic/cli@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-cLQ5qrXq3iubQXeOow9FsiukYCAG+woZAQG7l9w9FIQUAnedkUyDzAAlIH72vBNAItC9ZpWesI4I6Ncmg9g9OA==}
|
||||
'@tmagic/cli@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-lpuG/LbpdgtwWkugFXaybP9AgqJEH3qE1PBZaEQnNZQSrma+r9vC2BvR/oMzBh2/WLkI6dMHvS20YauwnrjYOQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@ -2940,8 +2940,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/core@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-V+H4upUCZ92xCuTLQMgU6QEMr4dnE8CHO7ScBG0bf/rCLRIIMI1M3kXhmyZErU8D+FwdN5FyRZvcsKv775uguA==}
|
||||
'@tmagic/core@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-DWx0YQdwtcu/mxyGLPi8OVSAknzP4mpraP0n2so4yghrxfw4rpgrzqoBPC2VZdSDhkUemxJ+MqZE/YNKzKBqlw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
typescript: ^6.0.3
|
||||
@ -2969,11 +2969,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/data-source@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-Dzrt9jWggSaBk46DHC0qUBcOR023WYNtOTx0SopynabNGUo4Ku8BQRsSpeZXY/Vtg7d8621XqGIiC+039JUJrA==}
|
||||
'@tmagic/data-source@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-GhMPhuCvJDw0DQvbvmiZJyvgNMAb9zWvRhBgUzTRiM4rkANmQM72KPTNT0zWCbGISrZ7iR0ltWxwr80DWrxOTg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.3
|
||||
'@tmagic/core': 1.8.0-beta.4
|
||||
typescript: ^6.0.3
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
@ -3001,12 +3001,12 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/dep@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-48ZpvczcC6Tsi+YGE7rhDKwYswhuywHx9HCgN5Bl2Ow3076il/zaDxTYsc0ZIR5HkE6aIxGNgH9nqzMSiOcacQ==}
|
||||
'@tmagic/dep@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-znTuExAWttbuTJW05q5dPJrrLM9mWBVWrfz2CVcZwB5jJETKW+bLLDReozgH3SufyQuH4cpLJyRW2AGt59iNxw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/schema': 1.8.0-beta.3
|
||||
'@tmagic/utils': 1.8.0-beta.3
|
||||
'@tmagic/schema': 1.8.0-beta.4
|
||||
'@tmagic/utils': 1.8.0-beta.4
|
||||
typescript: ^6.0.3
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
@ -3022,8 +3022,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/design@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-eWjzWSS4AdGpK1Rz0sOVjs/APFmkA+ouFE3S4JOa43BmND+Ol1vQrMa0HDm5kCOTeZL7XLacA9wJEzH6LAjRqw==}
|
||||
'@tmagic/design@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-ATSuHWynVkH2OkS59O1dsu7xO6lNsZAiuUyJqfDYUHW7c5APrid5mDadlRZiTTPLbMauXWfgyNCK3X7kgCj59Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
typescript: ^6.0.3
|
||||
@ -3044,11 +3044,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/editor@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-VZpaVkWxlGL6OL0IWIDTGUIpoC7Vnh9H55H2qEf85ktcLMAhtx5Qzal+mixcvfI8Kh93q/HYZorHgmoTp3DAsA==}
|
||||
'@tmagic/editor@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-n1xOKm53mnsh3qFWoaL6zZfB9MyGllXK14e7wzeuAfxaMgRINde5RiNE7JGBPBdUUrmyYs2yAcDcZIhGx/zKbA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.3
|
||||
'@tmagic/core': 1.8.0-beta.4
|
||||
monaco-editor: '^0.55.1 '
|
||||
type-fest: ^5.2.0
|
||||
typescript: ^6.0.3
|
||||
@ -3059,11 +3059,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/element-plus-adapter@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-aUY8PGGiQ1XUlzR5sR6DJc2mqVVH6PLGF6YtP+7oihhqVEG7EyFzwgQvVesV5sUxTuJm7CQaZKfnyg2gBceOWQ==}
|
||||
'@tmagic/element-plus-adapter@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-2qBsqDix6PuIQdG28bUyunkbtcpuGiMmSl1vFGNkuObzj27x98GbSBk6ONEwAB/OvMG5TVHWZDK/5FVIUAAmZg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/design': 1.8.0-beta.3
|
||||
'@tmagic/design': 1.8.0-beta.4
|
||||
element-plus: '>=2.9.0'
|
||||
typescript: ^6.0.3
|
||||
vue: ^3.5.34
|
||||
@ -3080,8 +3080,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/form-schema@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-YT8b3Td+6IYrpM2YQO/srmHxobEcS+fHzKBq8t54P4EO2gg6XOIA3yQcGwwV9m0814Wtg2YMpbXxuhcJYyAxnQ==}
|
||||
'@tmagic/form-schema@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-IPeONO+CZAplOf4PqCb7P6VuhrqLfvAtqpVPgGTKoy3aK9y+bl58e51Wf8tcqSyHosIIgnC7dUv0ygMIRWVMjw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
typescript: ^6.0.3
|
||||
@ -3102,13 +3102,13 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/form@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-/i4Sgm5Gaq9AUvKq3/F3b88GNvmLc2Q1yadKBjbBWDVispqeLljzScKrsNPhXZfmJmUSX2BnClaI9Aoyae5Lsg==}
|
||||
'@tmagic/form@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-wWOZJFECrTtYFFJPAPspL9qkWrDWqRPZSQlG0zTokDjYIsU2l0CIH9tOumzlKoMHc2WyainF/R3aLx/y5Dd3RQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/design': 1.8.0-beta.3
|
||||
'@tmagic/form-schema': 1.8.0-beta.3
|
||||
'@tmagic/utils': 1.8.0-beta.3
|
||||
'@tmagic/design': 1.8.0-beta.4
|
||||
'@tmagic/form-schema': 1.8.0-beta.4
|
||||
'@tmagic/utils': 1.8.0-beta.4
|
||||
typescript: ^6.0.3
|
||||
vue: ^3.5.34
|
||||
peerDependenciesMeta:
|
||||
@ -3150,8 +3150,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/schema@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-eoeGyAqGQ9SX+PFNlYZ1YfWmNQyOkAYLDGEvumWYmXWT0gs7WDmcWt3AaDvQqpCrpaCMAfICWCehR6wSkY0o3g==}
|
||||
'@tmagic/schema@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-yt7TPnrfl7Ensv4YNLB/01cMdUxzse7B6V3st4Ftw1j0QkFYsquaPg3Us3CoSad0NjNIttOfewYVtq9NKEsOXQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
typescript: ^6.0.3
|
||||
@ -3179,11 +3179,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/stage@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-UKPFxxmQUvzy3I0bytlw3LezUKUez/ZNNDLuuvLiWCAHyZblwiOTtCfx4WE3pcONpI+qLIPhZP2+A6DQQ99xKQ==}
|
||||
'@tmagic/stage@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-E/qZLtA9XWSgWHM+mxQh3qluqDZhQ8v+d1oL2d/l4drgA9Hlsj74KqbYIHbG5n9oiqeydt3gD6yNnfp4RoTiPA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.3
|
||||
'@tmagic/core': 1.8.0-beta.4
|
||||
typescript: ^6.0.3
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
@ -3201,24 +3201,24 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/table@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-CvUKRWnrWu/t/85cQz2NPeMzszvGWBg0PYvedqRWPT+K3oNOokVGCxa3cA+MxKt72ijs2qW+wOwoEDgj0068qA==}
|
||||
'@tmagic/table@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-eOPZNMlh7IkVsDMAPBA5uJnC0t7mYqcTM7fTPVq6SrqK43s7zAhy0PnrvzqLBv4Vi3P8aDdfZTiPdhv9akPxVA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/design': 1.8.0-beta.3
|
||||
'@tmagic/form': 1.8.0-beta.3
|
||||
'@tmagic/utils': 1.8.0-beta.3
|
||||
'@tmagic/design': 1.8.0-beta.4
|
||||
'@tmagic/form': 1.8.0-beta.4
|
||||
'@tmagic/utils': 1.8.0-beta.4
|
||||
typescript: ^6.0.3
|
||||
vue: ^3.5.34
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/tdesign-vue-next-adapter@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-OHeP+wBpMZvYCBDhzlQCYEGlsaH9m0SmnymAyOCJ3wP2JjSv79hXNd+fBeUQA0C8bAU0eI4HUP9GCCn4zKEy2w==}
|
||||
'@tmagic/tdesign-vue-next-adapter@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-5Dz54TOXmsb73bX8SzHSez+LJRpY8vC0C7X7mcQl257Amwnuk7LoY42XxP+fEnAy9VWDUpGo/OXpLhPbh4R0ag==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/design': 1.8.0-beta.3
|
||||
'@tmagic/design': 1.8.0-beta.4
|
||||
tdesign-vue-next: ^1.17.1
|
||||
typescript: ^6.0.3
|
||||
vue: ^3.5.34
|
||||
@ -3261,11 +3261,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@tmagic/utils@1.8.0-beta.3':
|
||||
resolution: {integrity: sha512-XVH5Lu5AXbJ5W8lLiw3Dr3WeZ09OSbfqboyqF/W20/iBbfhEb2eOhDPJeleiRxZCzbqnzTJRf75dl9w8UgK1gQ==}
|
||||
'@tmagic/utils@1.8.0-beta.4':
|
||||
resolution: {integrity: sha512-LPLlZdnbDeo5ylMWIWzpbuTqsKFSi+N21Sq4IS8mlsW9OR+PmH960ghQJZfIehaDkBt9HjCsu1OYNOUK6K1/mg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@tmagic/schema': 1.8.0-beta.3
|
||||
'@tmagic/schema': 1.8.0-beta.4
|
||||
typescript: ^6.0.3
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
@ -8699,7 +8699,7 @@ snapshots:
|
||||
|
||||
'@sxzz/popperjs-es@2.11.7': {}
|
||||
|
||||
'@tmagic/cli@1.8.0-beta.3(typescript@6.0.3)':
|
||||
'@tmagic/cli@1.8.0-beta.4(typescript@6.0.3)':
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
chokidar: 3.6.0
|
||||
@ -8734,12 +8734,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/core@1.8.0-beta.3(typescript@6.0.3)':
|
||||
'@tmagic/core@1.8.0-beta.4(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@tmagic/data-source': 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/dep': 1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/schema': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/utils': 1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/data-source': 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/dep': 1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/schema': 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/utils': 1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
events: 3.3.0
|
||||
lodash-es: 4.17.21
|
||||
optionalDependencies:
|
||||
@ -8763,9 +8763,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/data-source@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)':
|
||||
'@tmagic/data-source@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@tmagic/core': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/core': 1.8.0-beta.4(typescript@6.0.3)
|
||||
deep-state-observer: 5.5.14
|
||||
events: 3.3.0
|
||||
lodash-es: 4.17.21
|
||||
@ -8786,10 +8786,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/dep@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)':
|
||||
'@tmagic/dep@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@tmagic/schema': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/utils': 1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/schema': 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/utils': 1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@ -8800,7 +8800,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
vue: 3.5.34(typescript@6.0.3)
|
||||
@ -8834,15 +8834,15 @@ snapshots:
|
||||
- '@tmagic/form-schema'
|
||||
- '@tmagic/schema'
|
||||
|
||||
'@tmagic/editor@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/editor@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/core': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/design': 1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/form': 1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/stage': 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/table': 1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/utils': 1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/core': 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/design': 1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/form': 1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/stage': 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/table': 1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/utils': 1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
buffer: 6.0.3
|
||||
deep-object-diff: 1.1.9
|
||||
emmet-monaco-es: 5.7.0(monaco-editor@0.55.1)
|
||||
@ -8862,9 +8862,9 @@ snapshots:
|
||||
- '@tmagic/form-schema'
|
||||
- '@tmagic/schema'
|
||||
|
||||
'@tmagic/element-plus-adapter@1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/element-plus-adapter@1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tmagic/design': 1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/design': 1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
element-plus: 2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
|
||||
vue: 3.5.34(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
@ -8876,9 +8876,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3)':
|
||||
'@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@tmagic/schema': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/schema': 1.8.0-beta.4(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@ -8896,13 +8896,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/form@1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/form@1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@element-plus/icons-vue': 2.3.2(vue@3.5.34(typescript@6.0.3))
|
||||
'@popperjs/core': 2.11.8
|
||||
'@tmagic/design': 1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/form-schema': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/utils': 1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/design': 1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/form-schema': 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/utils': 1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
dayjs: 1.11.19
|
||||
lodash-es: 4.17.21
|
||||
sortablejs: 1.15.6
|
||||
@ -8910,13 +8910,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/react-runtime-help@0.2.2(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.17.21)(react@18.3.1)(typescript@6.0.3)':
|
||||
'@tmagic/react-runtime-help@0.2.2(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(lodash-es@4.17.21)(react@18.3.1)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
lodash-es: 4.17.21
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/stage': 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/core': 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/stage': 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/schema@1.7.0(typescript@6.0.3)':
|
||||
@ -8927,7 +8927,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/schema@1.8.0-beta.3(typescript@6.0.3)':
|
||||
'@tmagic/schema@1.8.0-beta.4(typescript@6.0.3)':
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@ -8957,10 +8957,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/stage@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)':
|
||||
'@tmagic/stage@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@scena/guides': 0.29.2
|
||||
'@tmagic/core': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/core': 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@zumer/snapdom': 2.8.0
|
||||
events: 3.3.0
|
||||
keycon: 1.4.0
|
||||
@ -8980,31 +8980,31 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/table@1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/table@1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form@1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tmagic/design': 1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/form': 1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/utils': 1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/design': 1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/form': 1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/utils': 1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
lodash-es: 4.17.21
|
||||
vue: 3.5.34(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/tdesign-vue-next-adapter@1.8.0-beta.3(@tmagic/design@1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/tdesign-vue-next-adapter@1.8.0-beta.4(@tmagic/design@1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(tdesign-vue-next@1.17.3(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tmagic/design': 1.8.0-beta.3(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/design': 1.8.0-beta.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
tdesign-vue-next: 1.17.3(vue@3.5.34(typescript@6.0.3))
|
||||
vue: 3.5.34(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/tmagic-form-runtime@1.1.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/tmagic-form-runtime@1.1.3(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/editor@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(element-plus@2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
'@tmagic/editor': 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.3(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
'@tmagic/editor': 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/form-schema@1.8.0-beta.4(typescript@6.0.3))(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(monaco-editor@0.55.1)(type-fest@5.2.0)(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||
element-plus: 2.11.8(@vue/composition-api@1.7.2(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
|
||||
vue: 3.5.34(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/core': 1.8.0-beta.4(typescript@6.0.3)
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/utils@1.7.0(@tmagic/schema@1.7.0(typescript@6.0.3))(typescript@6.0.3)':
|
||||
@ -9021,19 +9021,19 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/utils@1.8.0-beta.3(@tmagic/schema@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)':
|
||||
'@tmagic/utils@1.8.0-beta.4(@tmagic/schema@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@tmagic/schema': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/schema': 1.8.0-beta.4(typescript@6.0.3)
|
||||
lodash-es: 4.17.21
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tmagic/vue-runtime-help@2.0.2(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
'@tmagic/vue-runtime-help@2.0.2(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(@tmagic/stage@1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3))(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))':
|
||||
dependencies:
|
||||
vue: 3.5.34(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
'@tmagic/core': 1.8.0-beta.3(typescript@6.0.3)
|
||||
'@tmagic/stage': 1.8.0-beta.3(@tmagic/core@1.8.0-beta.3(typescript@6.0.3))(typescript@6.0.3)
|
||||
'@tmagic/core': 1.8.0-beta.4(typescript@6.0.3)
|
||||
'@tmagic/stage': 1.8.0-beta.4(@tmagic/core@1.8.0-beta.4(typescript@6.0.3))(typescript@6.0.3)
|
||||
typescript: 6.0.3
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "runtime-react",
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"engines": {
|
||||
@ -16,16 +16,16 @@
|
||||
"build:playground": "node scripts/build.mjs --type=playground"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmagic/core": "1.8.0-beta.3",
|
||||
"@tmagic/core": "1.8.0-beta.4",
|
||||
"@tmagic/react-runtime-help": "0.2.2",
|
||||
"@tmagic/stage": "1.8.0-beta.3",
|
||||
"@tmagic/stage": "1.8.0-beta.4",
|
||||
"axios": "^1.13.2",
|
||||
"qrcode": "^1.5.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tmagic/cli": "1.8.0-beta.3",
|
||||
"@tmagic/cli": "1.8.0-beta.4",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "runtime-vue",
|
||||
"version": "1.8.0-beta.3",
|
||||
"version": "1.8.0-beta.4",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"engines": {
|
||||
@ -16,14 +16,14 @@
|
||||
"build:playground": "node scripts/build.mjs --type=playground"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmagic/core": "1.8.0-beta.3",
|
||||
"@tmagic/stage": "1.8.0-beta.3",
|
||||
"@tmagic/core": "1.8.0-beta.4",
|
||||
"@tmagic/stage": "1.8.0-beta.4",
|
||||
"@tmagic/vue-runtime-help": "^2.0.1",
|
||||
"axios": "^1.13.2",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tmagic/cli": "1.8.0-beta.3",
|
||||
"@tmagic/cli": "1.8.0-beta.4",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^24.0.10",
|
||||
"@vitejs/plugin-legacy": "^8.0.1",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user