@tmagic/editor 1.8.0-beta.7 → 1.8.0-manmanyu.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +13 -3
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +6 -5
- package/dist/es/components/ContentMenu.vue_vue_type_script_setup_true_lang.js +11 -4
- package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +9 -4
- package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +68 -43
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +10 -7
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +4 -1
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +8 -2
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +101 -64
- package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/PageFragmentSelect.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +5 -4
- package/dist/es/fields/StyleSetter/components/BackgroundPosition.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/icons/align-items/Center.js +31 -0
- package/dist/es/fields/StyleSetter/icons/align-items/FlexEnd.js +31 -0
- package/dist/es/fields/StyleSetter/icons/align-items/FlexStart.js +31 -0
- package/dist/es/fields/StyleSetter/icons/align-items/SpaceAround.js +50 -0
- package/dist/es/fields/StyleSetter/icons/align-items/SpaceBetween.js +50 -0
- package/dist/es/fields/StyleSetter/icons/background-repeat/NoRepeat.js +6 -6
- package/dist/es/fields/StyleSetter/icons/background-repeat/Repeat.js +5 -2
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatX.js +28 -5
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatY.js +31 -5
- package/dist/es/fields/StyleSetter/icons/flex-direction/Column.js +10 -2
- package/dist/es/fields/StyleSetter/icons/flex-direction/ColumnReverse.js +10 -2
- package/dist/es/fields/StyleSetter/icons/flex-direction/Row.js +10 -2
- package/dist/es/fields/StyleSetter/icons/flex-direction/RowReverse.js +10 -2
- package/dist/es/fields/StyleSetter/icons/justify-content/Center.js +16 -2
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexEnd.js +18 -2
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexStart.js +18 -2
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceAround.js +36 -2
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceBetween.js +32 -2
- package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +11 -6
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +6 -3
- package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +82 -46
- package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/es/icons/DatasourceIcon.js +17 -0
- package/dist/es/index.js +5 -3
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +14 -10
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +38 -26
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +135 -288
- package/dist/es/layouts/history-list/composables.js +23 -55
- package/dist/es/layouts/history-list/useHistoryList.js +56 -0
- package/dist/es/layouts/history-list/useHistoryRevert.js +304 -0
- package/dist/es/services/history.js +18 -1
- package/dist/es/style.css +306 -37
- package/dist/es/utils/const.js +1 -1
- package/dist/es/utils/props.js +137 -31
- package/dist/style.css +306 -37
- package/dist/themes/magic-admin.css +2316 -0
- package/dist/tmagic-editor.umd.cjs +4427 -2871
- package/package.json +7 -7
- package/src/Editor.vue +13 -1
- package/src/components/CompareForm.vue +14 -6
- package/src/components/ContentMenu.vue +10 -2
- package/src/components/FloatingBox.vue +13 -2
- package/src/editorProps.ts +9 -0
- package/src/fields/CodeSelect.vue +51 -40
- package/src/fields/CodeSelectCol.vue +1 -0
- package/src/fields/DataSourceFieldSelect/Index.vue +14 -5
- package/src/fields/DisplayConds.vue +4 -1
- package/src/fields/EventSelect.vue +58 -32
- package/src/fields/StyleSetter/Index.vue +1 -4
- package/src/fields/StyleSetter/components/Border.vue +1 -1
- package/src/fields/StyleSetter/icons/align-items/Center.vue +19 -0
- package/src/fields/StyleSetter/icons/align-items/FlexEnd.vue +19 -0
- package/src/fields/StyleSetter/icons/align-items/FlexStart.vue +19 -0
- package/src/fields/StyleSetter/icons/align-items/SpaceAround.vue +34 -0
- package/src/fields/StyleSetter/icons/align-items/SpaceBetween.vue +34 -0
- package/src/fields/StyleSetter/icons/align-items/index copy.ts +5 -0
- package/src/fields/StyleSetter/icons/align-items/index.ts +5 -0
- package/src/fields/StyleSetter/icons/background-repeat/NoRepeat.vue +20 -4
- package/src/fields/StyleSetter/icons/background-repeat/Repeat.vue +10 -28
- package/src/fields/StyleSetter/icons/background-repeat/RepeatX.vue +4 -5
- package/src/fields/StyleSetter/icons/background-repeat/RepeatY.vue +4 -5
- package/src/fields/StyleSetter/icons/flex-direction/Column.vue +4 -3
- package/src/fields/StyleSetter/icons/flex-direction/ColumnReverse.vue +4 -3
- package/src/fields/StyleSetter/icons/flex-direction/Row.vue +4 -3
- package/src/fields/StyleSetter/icons/flex-direction/RowReverse.vue +4 -3
- package/src/fields/StyleSetter/icons/justify-content/Center.vue +5 -2
- package/src/fields/StyleSetter/icons/justify-content/FlexEnd.vue +15 -2
- package/src/fields/StyleSetter/icons/justify-content/FlexStart.vue +15 -2
- package/src/fields/StyleSetter/icons/justify-content/SpaceAround.vue +28 -3
- package/src/fields/StyleSetter/icons/justify-content/SpaceBetween.vue +28 -2
- package/src/fields/StyleSetter/pro/Background.vue +11 -5
- package/src/fields/StyleSetter/pro/Font.vue +3 -3
- package/src/fields/StyleSetter/pro/Layout.vue +105 -28
- package/src/fields/UISelect.vue +1 -1
- package/src/icons/DatasourceIcon.vue +7 -0
- package/src/index.ts +2 -0
- package/src/layouts/Framework.vue +7 -1
- package/src/layouts/NavMenu.vue +2 -2
- package/src/layouts/history-list/GroupRow.vue +10 -0
- package/src/layouts/history-list/HistoryDiffDialog.vue +6 -1
- package/src/layouts/history-list/HistoryListPanel.vue +53 -250
- package/src/layouts/history-list/PageTab.vue +4 -4
- package/src/layouts/history-list/composables.ts +52 -90
- package/src/layouts/history-list/useHistoryList.ts +60 -0
- package/src/layouts/history-list/useHistoryRevert.ts +400 -0
- package/src/services/codeBlock.ts +30 -29
- package/src/services/dataSource.ts +37 -37
- package/src/services/editor.ts +32 -29
- package/src/services/history.ts +340 -430
- package/src/theme/code-block.scss +37 -0
- package/src/theme/common/var.scss +1 -1
- package/src/theme/component-list-panel.scss +2 -2
- package/src/theme/data-source-field.scss +4 -0
- package/src/theme/display-conds.scss +11 -0
- package/src/theme/event.scss +55 -1
- package/src/theme/history-list-panel.scss +23 -9
- package/src/theme/props-panel.scss +2 -1
- package/src/theme/style-setter/border.scss +14 -5
- package/src/theme/theme.scss +1 -0
- package/src/theme/themes/magic-admin/index.scss +125 -0
- package/src/type.ts +174 -92
- package/src/utils/const.ts +2 -1
- package/src/utils/history.ts +52 -67
- package/src/utils/props.ts +138 -9
- package/types/index.d.ts +156 -13
package/src/services/history.ts
CHANGED
|
@@ -17,31 +17,32 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { reactive } from 'vue';
|
|
20
|
+
import type { Writable } from 'type-fest';
|
|
20
21
|
|
|
21
|
-
import type {
|
|
22
|
-
import type { ChangeRecord } from '@tmagic/form';
|
|
22
|
+
import type { Id } from '@tmagic/core';
|
|
23
23
|
import { guid } from '@tmagic/utils';
|
|
24
24
|
|
|
25
25
|
import type {
|
|
26
|
+
BaseStepValue,
|
|
26
27
|
CodeBlockStepValue,
|
|
27
28
|
DataSourceStepValue,
|
|
29
|
+
HistoryEvents,
|
|
30
|
+
HistoryGroup,
|
|
28
31
|
HistoryOpSource,
|
|
29
32
|
HistoryPersistOptions,
|
|
30
33
|
HistoryState,
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
HistoryStepEntry,
|
|
35
|
+
HistoryStepType,
|
|
33
36
|
PersistedHistoryState,
|
|
34
|
-
StackHistoryGroup,
|
|
35
37
|
StepValue,
|
|
38
|
+
SyncHookPlugin,
|
|
36
39
|
} from '@editor/type';
|
|
37
40
|
import { getEditorConfig } from '@editor/utils/config';
|
|
38
41
|
import {
|
|
39
|
-
createStackStep,
|
|
40
42
|
deserializeStacks,
|
|
41
43
|
getOrCreateStack,
|
|
42
44
|
markStackSaved,
|
|
43
|
-
|
|
44
|
-
mergeStackSteps,
|
|
45
|
+
mergeSteps,
|
|
45
46
|
serializeStacks,
|
|
46
47
|
undoFloor,
|
|
47
48
|
} from '@editor/utils/history';
|
|
@@ -51,86 +52,110 @@ import { UndoRedo } from '@editor/utils/undo-redo';
|
|
|
51
52
|
import BaseService from './BaseService';
|
|
52
53
|
import editorService from './editor';
|
|
53
54
|
|
|
55
|
+
const canUsePluginMethods = {
|
|
56
|
+
sync: ['push', 'undo', 'redo'] as const,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** 各内置历史类型的默认展示名称(用于历史面板 tab / 分组标题等)。扩展类型可通过 registerStepType / setStepName 登记。 */
|
|
60
|
+
const DEFAULT_STEP_NAMES: Record<string, string> = {
|
|
61
|
+
page: '页面',
|
|
62
|
+
codeBlock: '代码块',
|
|
63
|
+
dataSource: '数据源',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** 各历史类型对应的分组 `kind`(展示用):page→page,codeBlock→code-block,dataSource→data-source。扩展类型缺省回退到 stepType 本身。 */
|
|
67
|
+
const STEP_TYPE_KIND: Record<string, string> = {
|
|
68
|
+
page: 'page',
|
|
69
|
+
codeBlock: 'code-block',
|
|
70
|
+
dataSource: 'data-source',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type SyncMethodName = Writable<(typeof canUsePluginMethods)['sync']>;
|
|
74
|
+
|
|
54
75
|
/** 历史记录持久化快照的默认存储位置与结构版本。 */
|
|
55
76
|
const DEFAULT_DB_NAME = 'tmagic-editor';
|
|
56
77
|
const DEFAULT_STORE_NAME = 'history';
|
|
57
78
|
const DEFAULT_KEY: IDBValidKey = 'default';
|
|
58
|
-
//
|
|
59
|
-
|
|
79
|
+
// 仅把每条 step 的 diff 序列化成字符串,其余字段交给 IndexedDB 结构化克隆原生存储(见 saveToIndexedDB);
|
|
80
|
+
// 全部历史栈统一收敛到 steps 字段(见 HistorySteps)。
|
|
81
|
+
const PERSIST_VERSION = 3;
|
|
60
82
|
|
|
61
83
|
class History extends BaseService {
|
|
62
84
|
public state = reactive<HistoryState>({
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
steps: {
|
|
86
|
+
page: {},
|
|
87
|
+
codeBlock: {},
|
|
88
|
+
dataSource: {},
|
|
89
|
+
},
|
|
90
|
+
stepNames: { ...DEFAULT_STEP_NAMES },
|
|
69
91
|
});
|
|
70
92
|
|
|
71
93
|
constructor() {
|
|
72
|
-
super([]);
|
|
73
|
-
|
|
74
|
-
this.on('change', this.setCanUndoRedo);
|
|
94
|
+
super([...canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false }))]);
|
|
75
95
|
}
|
|
76
96
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
/**
|
|
98
|
+
* 注册一个扩展历史类型,使其可与内置 `page` / `codeBlock` / `dataSource` 一样走统一的
|
|
99
|
+
* {@link push} / {@link undo} / {@link redo}(按 id 分栈、独立 undo/redo)。
|
|
100
|
+
*
|
|
101
|
+
* @param stepType 自定义历史类型标识(勿与内置类型重名)
|
|
102
|
+
* @param options.event push/undo/redo 后派发的事件名,缺省为 `${stepType}-history-change`
|
|
103
|
+
* @param options.name 历史面板中的展示名称(tab / 分组标题等),缺省回退到 stepType 本身
|
|
104
|
+
*/
|
|
105
|
+
public registerStepType(stepType: string, options: { event?: string; name?: string } = {}): void {
|
|
106
|
+
this.getStepBucket(stepType);
|
|
107
|
+
if (options.name !== undefined) this.state.stepNames[stepType] = options.name;
|
|
82
108
|
}
|
|
83
109
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
110
|
+
/**
|
|
111
|
+
* 读取指定历史类型的展示名称(用于历史面板 tab / 分组标题等)。
|
|
112
|
+
* 未登记名称时回退到 stepType 本身。
|
|
113
|
+
*/
|
|
114
|
+
public getStepName(stepType: HistoryStepType): string {
|
|
115
|
+
return this.state.stepNames[stepType] ?? `${stepType}`;
|
|
88
116
|
}
|
|
89
117
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
this.setCanUndoRedo();
|
|
118
|
+
/**
|
|
119
|
+
* 设置指定历史类型的展示名称(用于历史面板 tab / 分组标题等)。
|
|
120
|
+
* 内置 `page` / `codeBlock` / `dataSource` 也可在此覆盖默认中文名。
|
|
121
|
+
*/
|
|
122
|
+
public setStepName(stepType: HistoryStepType, name: string): void {
|
|
123
|
+
this.state.stepNames[stepType] = name;
|
|
124
|
+
}
|
|
100
125
|
|
|
101
|
-
|
|
126
|
+
public reset() {
|
|
127
|
+
this.clearAllSteps();
|
|
102
128
|
}
|
|
103
129
|
|
|
104
130
|
public resetState(): void {
|
|
105
|
-
this.
|
|
106
|
-
this.state.pageSteps = {};
|
|
107
|
-
this.state.canRedo = false;
|
|
108
|
-
this.state.canUndo = false;
|
|
109
|
-
this.state.codeBlockState = {};
|
|
110
|
-
this.state.dataSourceState = {};
|
|
131
|
+
this.clearAllSteps();
|
|
111
132
|
}
|
|
112
133
|
|
|
113
134
|
/**
|
|
114
|
-
*
|
|
135
|
+
* 为指定历史栈(默认 `page`,也适用于 codeBlock / dataSource / 扩展类型)种入一条「初始基线」记录。
|
|
115
136
|
*
|
|
116
|
-
* 该记录是一条 `opType: 'initial'` 的
|
|
137
|
+
* 该记录是一条 `opType: 'initial'` 的 step,作为对应栈 **index 0 的固定底线**:
|
|
117
138
|
* - 它是一条真实入栈的 step(随栈一起持久化),但被钉为撤销/回滚的下限——cursor 永不低于它,
|
|
118
|
-
* 因此不会被 undo / goto / revert 触达(详见 {@link undo} / {@link
|
|
119
|
-
* - 历史面板把它过滤出分组列表(见 {@link
|
|
139
|
+
* 因此不会被 undo / goto / revert 触达(详见 {@link undo} / {@link undoFloor});
|
|
140
|
+
* - 历史面板把它过滤出分组列表(见 {@link getHistoryGroups}),改由底部「初始」行展示。
|
|
141
|
+
*
|
|
142
|
+
* 仅当目标栈为空时种入(保证 initial 一定位于 index 0);已存在 initial 底线时不重复种入。
|
|
120
143
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
144
|
+
* @param stepType 历史类型
|
|
145
|
+
* @param id 目标栈 id(page 为 pageId,其余类型为对应资源 id)
|
|
146
|
+
* @param options 基线展示信息(名称 / 描述 / 来源)
|
|
123
147
|
*/
|
|
124
|
-
public
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
public setMarker(
|
|
149
|
+
stepType: HistoryStepType,
|
|
150
|
+
id: Id,
|
|
151
|
+
options: { name?: string; description?: string; source?: HistoryOpSource; extra?: Record<string, any> } = {},
|
|
127
152
|
): StepValue | null {
|
|
128
|
-
if (
|
|
153
|
+
if (!this.isValidStackId(id)) return null;
|
|
129
154
|
|
|
130
|
-
const existing = this.
|
|
155
|
+
const existing = this.getMarker(stepType, id);
|
|
131
156
|
if (existing) return existing;
|
|
132
157
|
|
|
133
|
-
const stack = getOrCreateStack(this.
|
|
158
|
+
const stack = getOrCreateStack(this.getStepBucket(stepType), id);
|
|
134
159
|
// initial 必须是 index 0;栈非空(已有真实记录、却无 initial,如旧数据)时不强行前插,优雅降级为无基线。
|
|
135
160
|
if (stack.getLength() > 0) return null;
|
|
136
161
|
|
|
@@ -138,205 +163,141 @@ class History extends BaseService {
|
|
|
138
163
|
uuid: guid(),
|
|
139
164
|
opType: 'initial',
|
|
140
165
|
diff: [],
|
|
141
|
-
data: { name: options.name || '', id
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
data: { name: options.name || '', id },
|
|
167
|
+
extra: {
|
|
168
|
+
...(options.extra || {}),
|
|
169
|
+
...(stepType === 'page'
|
|
170
|
+
? {
|
|
171
|
+
selectedBefore: [],
|
|
172
|
+
selectedAfter: [],
|
|
173
|
+
modifiedNodeIds: new Map(),
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
176
|
+
},
|
|
145
177
|
historyDescription: options.description || '未修改的初始状态',
|
|
146
178
|
timestamp: Date.now(),
|
|
147
179
|
...(options.source ? { source: options.source } : {}),
|
|
148
180
|
};
|
|
149
181
|
stack.pushElement(marker);
|
|
150
|
-
|
|
151
|
-
this.emit('page-marker-change', marker);
|
|
182
|
+
this.emit('marker-change', { id, marker, stepType });
|
|
152
183
|
return marker;
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
187
|
+
* 读取指定历史栈的初始基线 step(栈 index 0 且 `opType: 'initial'`);
|
|
188
|
+
* 不存在或 id 缺省时返回 undefined。
|
|
158
189
|
*/
|
|
159
|
-
public
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return first?.opType === 'initial' ? first : undefined;
|
|
190
|
+
public getMarker(stepType: HistoryStepType, id?: Id): StepValue | undefined {
|
|
191
|
+
if (!this.isValidStackId(id)) return undefined;
|
|
192
|
+
const first = this.state.steps[stepType]?.[id]?.getElementList()[0];
|
|
193
|
+
return first?.opType === 'initial' ? (first as StepValue) : undefined;
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
/**
|
|
167
|
-
*
|
|
197
|
+
* 把一条步骤推入指定历史栈。统一入口,所有类型(page / codeBlock / dataSource / 扩展)行为完全一致:
|
|
198
|
+
* 按 `stepType` 选择目标栈类型,按 `id`(必填)选择具体栈,按需建栈后入栈,并派发对应的历史变更事件
|
|
199
|
+
* (`page` 为 `change`,其余见 {@link registerStepType}),回调签名统一为 `(id, step)`。
|
|
168
200
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*/
|
|
172
|
-
public push(state: StepValue, pageId?: Id): StepValue | null {
|
|
173
|
-
const undoRedo = this.getUndoRedo(pageId);
|
|
174
|
-
if (!undoRedo) return null;
|
|
175
|
-
if (state.uuid === undefined) state.uuid = guid();
|
|
176
|
-
if (state.timestamp === undefined) state.timestamp = Date.now();
|
|
177
|
-
undoRedo.pushElement(state);
|
|
178
|
-
// 仅当推入的是当前活动页时才需要刷新 canUndo/canRedo —— 其它页栈对当前 UI 状态没影响。
|
|
179
|
-
if (pageId === undefined || `${pageId}` === `${this.state.pageId}`) {
|
|
180
|
-
this.emit('change', state);
|
|
181
|
-
}
|
|
182
|
-
return state;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** 读取指定页面(缺省当前活动页)历史栈当前游标所在的 step(cursor - 1);无则返回 null。 */
|
|
186
|
-
public getCurrentPageStep(pageId?: Id): StepValue | null {
|
|
187
|
-
const targetPageId = pageId ?? this.state.pageId;
|
|
188
|
-
if (!targetPageId) return null;
|
|
189
|
-
return this.state.pageSteps[targetPageId]?.getCurrentElement() ?? null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* 用 `state` 替换指定页面栈当前游标所在的 step(并丢弃其后的重做尾部),游标不变。
|
|
194
|
-
* 用于「连续 set root 记录合并」等就地替换最新一条的场景;替换成功后按需刷新 / 通知。
|
|
195
|
-
*/
|
|
196
|
-
public replaceCurrentPageStep(state: StepValue, pageId?: Id): StepValue | null {
|
|
197
|
-
const undoRedo = this.getUndoRedo(pageId);
|
|
198
|
-
if (!undoRedo) return null;
|
|
199
|
-
if (state.uuid === undefined) state.uuid = guid();
|
|
200
|
-
if (state.timestamp === undefined) state.timestamp = Date.now();
|
|
201
|
-
if (!undoRedo.replaceCurrentElement(state)) return null;
|
|
202
|
-
this.emit('change', state);
|
|
203
|
-
return state;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 推入一条代码块变更记录(与页面/节点完全无关),按 `codeBlockId` 维度独立一份 UndoRedo 栈。
|
|
201
|
+
* 跨页 / 跨资源操作(如 `moveToContainer` 把节点搬到其它页)必须显式传入目标 id,
|
|
202
|
+
* 否则无法落到正确的栈。step 可由 `createStackStep` 等构造后传入。
|
|
208
203
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* - `changeRecords` 来自 form 端,撤销/重做时若有则按 propPath 局部覆盖;缺省才退化为整内容替换。
|
|
213
|
-
* - 不直接驱动 codeBlockService,调用方负责实际写回。
|
|
204
|
+
* @param stepType 历史类型
|
|
205
|
+
* @param step 已构造好的历史记录(缺省自动补全 uuid / timestamp)
|
|
206
|
+
* @param id 目标栈 id(page 为 pageId,其余类型为对应资源 id),必填
|
|
214
207
|
*/
|
|
215
|
-
public
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
source?: HistoryOpSource;
|
|
225
|
-
},
|
|
226
|
-
): CodeBlockStepValue | null {
|
|
227
|
-
const step = createStackStep<CodeBlockContent, CodeBlockStepValue>(codeBlockId, {
|
|
228
|
-
oldValue: payload.oldContent,
|
|
229
|
-
newValue: payload.newContent,
|
|
230
|
-
changeRecords: payload.changeRecords,
|
|
231
|
-
historyDescription: payload.historyDescription,
|
|
232
|
-
source: payload.source,
|
|
233
|
-
});
|
|
234
|
-
if (!step) return null;
|
|
235
|
-
getOrCreateStack(this.state.codeBlockState, codeBlockId).pushElement(step);
|
|
236
|
-
this.emit('code-block-history-change', codeBlockId, step);
|
|
208
|
+
public push(stepType: 'page', step: StepValue, id: Id): StepValue | null;
|
|
209
|
+
public push(stepType: 'codeBlock', step: CodeBlockStepValue, id: Id): CodeBlockStepValue | null;
|
|
210
|
+
public push(stepType: 'dataSource', step: DataSourceStepValue, id: Id): DataSourceStepValue | null;
|
|
211
|
+
public push<T extends BaseStepValue>(stepType: string, step: T, id: Id): T | null;
|
|
212
|
+
public push(stepType: HistoryStepType, step: BaseStepValue, id: Id): BaseStepValue | null {
|
|
213
|
+
if (!this.isValidStackId(id)) return null;
|
|
214
|
+
this.fillStepMeta(step);
|
|
215
|
+
getOrCreateStack(this.getStepBucket(stepType), id).pushElement(step);
|
|
216
|
+
this.emit('change', step, stepType, id);
|
|
237
217
|
return step;
|
|
238
218
|
}
|
|
239
219
|
|
|
240
220
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
221
|
+
* 撤销指定历史栈的最近一次变更。统一入口,所有类型行为一致:
|
|
222
|
+
* 按 `id`(必填)+ `stepType` 定位栈,不会越过 index 0 的 initial 基线(所有类型同等适用),
|
|
223
|
+
* 仅在确有可撤销 step 时派发对应的历史变更事件(`page` 为 `change`,回调签名 `(id, step)`)。
|
|
224
|
+
*
|
|
225
|
+
* @param stepType 历史类型
|
|
226
|
+
* @param id 目标栈 id(page 为 pageId,其余类型为对应资源 id),必填
|
|
243
227
|
*/
|
|
244
|
-
public
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
historyDescription?: string;
|
|
252
|
-
/** 可选的操作途径(配置面板 / 菜单 / 接口等),仅用于历史面板展示与埋点。 */
|
|
253
|
-
source?: HistoryOpSource;
|
|
254
|
-
},
|
|
255
|
-
): DataSourceStepValue | null {
|
|
256
|
-
const step = createStackStep<DataSourceSchema, DataSourceStepValue>(dataSourceId, {
|
|
257
|
-
oldValue: payload.oldSchema,
|
|
258
|
-
newValue: payload.newSchema,
|
|
259
|
-
changeRecords: payload.changeRecords,
|
|
260
|
-
historyDescription: payload.historyDescription,
|
|
261
|
-
source: payload.source,
|
|
262
|
-
});
|
|
263
|
-
if (!step) return null;
|
|
264
|
-
getOrCreateStack(this.state.dataSourceState, dataSourceId).pushElement(step);
|
|
265
|
-
this.emit('data-source-history-change', dataSourceId, step);
|
|
266
|
-
return step;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** 撤销指定代码块的最近一次变更。 */
|
|
270
|
-
public undoCodeBlock(codeBlockId: Id): CodeBlockStepValue | null {
|
|
271
|
-
const undoRedo = this.state.codeBlockState[codeBlockId];
|
|
228
|
+
public undo(stepType: 'page', id: Id): StepValue | null;
|
|
229
|
+
public undo(stepType: 'codeBlock', id: Id): CodeBlockStepValue | null;
|
|
230
|
+
public undo(stepType: 'dataSource', id: Id): DataSourceStepValue | null;
|
|
231
|
+
public undo<T extends BaseStepValue>(stepType: string, id: Id): T | null;
|
|
232
|
+
public undo(stepType: HistoryStepType, id: Id): BaseStepValue | null {
|
|
233
|
+
if (!this.isValidStackId(id)) return null;
|
|
234
|
+
const undoRedo = this.state.steps[stepType]?.[id];
|
|
272
235
|
if (!undoRedo) return null;
|
|
236
|
+
// 不允许撤销越过初始基线(index 0 的 initial step);无基线时 floor 为 0。
|
|
237
|
+
if (undoRedo.getCursor() <= undoFloor(undoRedo)) return null;
|
|
273
238
|
const step = undoRedo.undo();
|
|
274
|
-
if (step) this.emit('
|
|
239
|
+
if (step) this.emit('change', step, stepType, id);
|
|
275
240
|
return step;
|
|
276
241
|
}
|
|
277
242
|
|
|
278
|
-
/**
|
|
279
|
-
|
|
280
|
-
|
|
243
|
+
/**
|
|
244
|
+
* 重做指定历史栈的下一次变更。语义与 {@link undo} 对称,详见其说明。
|
|
245
|
+
*
|
|
246
|
+
* @param stepType 历史类型
|
|
247
|
+
* @param id 目标栈 id(page 为 pageId,其余类型为对应资源 id),必填
|
|
248
|
+
*/
|
|
249
|
+
public redo(stepType: 'page', id: Id): StepValue | null;
|
|
250
|
+
public redo(stepType: 'codeBlock', id: Id): CodeBlockStepValue | null;
|
|
251
|
+
public redo(stepType: 'dataSource', id: Id): DataSourceStepValue | null;
|
|
252
|
+
public redo<T extends BaseStepValue>(stepType: string, id: Id): T | null;
|
|
253
|
+
public redo(stepType: HistoryStepType, id: Id): BaseStepValue | null {
|
|
254
|
+
if (!this.isValidStackId(id)) return null;
|
|
255
|
+
const undoRedo = this.state.steps[stepType]?.[id];
|
|
281
256
|
if (!undoRedo) return null;
|
|
282
257
|
const step = undoRedo.redo();
|
|
283
|
-
if (step) this.emit('
|
|
258
|
+
if (step) this.emit('change', step, stepType, id);
|
|
284
259
|
return step;
|
|
285
260
|
}
|
|
286
261
|
|
|
287
|
-
/**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
/** 撤销指定数据源的最近一次变更。 */
|
|
298
|
-
public undoDataSource(dataSourceId: Id): DataSourceStepValue | null {
|
|
299
|
-
const undoRedo = this.state.dataSourceState[dataSourceId];
|
|
300
|
-
if (!undoRedo) return null;
|
|
301
|
-
const step = undoRedo.undo();
|
|
302
|
-
if (step) this.emit('data-source-history-change', dataSourceId, step);
|
|
303
|
-
return step;
|
|
262
|
+
/**
|
|
263
|
+
* 是否可对指定历史栈撤销(cursor 高于 initial 基线底线)。
|
|
264
|
+
* @param stepType 历史类型
|
|
265
|
+
* @param id 目标栈 id;缺省 / 无效时返回 false
|
|
266
|
+
*/
|
|
267
|
+
public canUndo(stepType: HistoryStepType, id?: Id): boolean {
|
|
268
|
+
if (!this.isValidStackId(id)) return false;
|
|
269
|
+
const undoRedo = this.state.steps[stepType]?.[id];
|
|
270
|
+
return undoRedo ? undoRedo.getCursor() > undoFloor(undoRedo) : false;
|
|
304
271
|
}
|
|
305
272
|
|
|
306
|
-
/**
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return
|
|
273
|
+
/**
|
|
274
|
+
* 是否可对指定历史栈重做。
|
|
275
|
+
* @param stepType 历史类型
|
|
276
|
+
* @param id 目标栈 id;缺省 / 无效时返回 false
|
|
277
|
+
*/
|
|
278
|
+
public canRedo(stepType: HistoryStepType, id?: Id): boolean {
|
|
279
|
+
if (!this.isValidStackId(id)) return false;
|
|
280
|
+
return this.state.steps[stepType]?.[id]?.canRedo() ?? false;
|
|
313
281
|
}
|
|
314
282
|
|
|
315
|
-
/**
|
|
316
|
-
public
|
|
317
|
-
|
|
283
|
+
/** 读取指定页面历史栈当前游标所在的 step(cursor - 1);无则返回 null。 */
|
|
284
|
+
public getCurrentPageStep(pageId: Id): StepValue | null {
|
|
285
|
+
if (!this.isValidStackId(pageId)) return null;
|
|
286
|
+
return this.state.steps.page[pageId]?.getCurrentElement() ?? null;
|
|
318
287
|
}
|
|
319
288
|
|
|
320
|
-
/**
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
289
|
+
/**
|
|
290
|
+
* 用 `state` 替换指定页面栈当前游标所在的 step(并丢弃其后的重做尾部),游标不变。
|
|
291
|
+
* 用于「连续 set root 记录合并」等就地替换最新一条的场景;替换成功后派发 `change`。
|
|
292
|
+
*/
|
|
293
|
+
public replaceCurrentStep(stepType: HistoryStepType, state: StepValue, id: Id): StepValue | null {
|
|
294
|
+
if (!this.isValidStackId(id)) return null;
|
|
295
|
+
const undoRedo = getOrCreateStack(this.getStepBucket(stepType), id);
|
|
324
296
|
|
|
325
|
-
|
|
326
|
-
const undoRedo = this.getUndoRedo();
|
|
327
|
-
if (!undoRedo) return null;
|
|
328
|
-
// 不允许撤销越过初始基线(index 0 的 initial step)。
|
|
329
|
-
if (undoRedo.getCursor() <= undoFloor(undoRedo)) return null;
|
|
330
|
-
const state = undoRedo.undo();
|
|
331
|
-
this.emit('change', state);
|
|
332
|
-
return state;
|
|
333
|
-
}
|
|
297
|
+
this.fillStepMeta(state);
|
|
334
298
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (!undoRedo) return null;
|
|
338
|
-
const state = undoRedo.redo();
|
|
339
|
-
this.emit('change', state);
|
|
299
|
+
if (!undoRedo.replaceCurrentElement(state)) return null;
|
|
300
|
+
this.emit('change', state, stepType, id);
|
|
340
301
|
return state;
|
|
341
302
|
}
|
|
342
303
|
|
|
@@ -347,91 +308,53 @@ class History extends BaseService {
|
|
|
347
308
|
}
|
|
348
309
|
|
|
349
310
|
/**
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (marker) {
|
|
360
|
-
this.setPageMarker(targetPageId, {
|
|
361
|
-
name: marker.data?.name,
|
|
362
|
-
description: marker.historyDescription,
|
|
363
|
-
source: marker.source,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
if (`${targetPageId}` === `${this.state.pageId}`) {
|
|
367
|
-
this.setCanUndoRedo();
|
|
368
|
-
this.emit('clear-page', null);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* 清空数据源历史记录栈:传入 `dataSourceId` 仅清空该数据源,缺省清空全部数据源。
|
|
374
|
-
* 仅删除撤销/重做记录,不会改动数据源本身。
|
|
311
|
+
* 清空历史记录栈。统一入口,所有类型(page / codeBlock / dataSource / 扩展)行为一致:
|
|
312
|
+
* - 传入 `id`:仅清空 `stepType` 下该 id 对应的栈;
|
|
313
|
+
* - 缺省 `id`:清空 `stepType` 下的全部栈。
|
|
314
|
+
*
|
|
315
|
+
* 仅删除撤销/重做记录,不会改动 DSL / 代码块 / 数据源本身。清空时会**保留各栈原有的
|
|
316
|
+
* initial 基线**(文案 / 来源),无基线时清空成空栈。清空后派发 `clear`(签名 `(id, stepType)`)。
|
|
317
|
+
*
|
|
318
|
+
* @param stepType 历史类型
|
|
319
|
+
* @param id 目标栈 id;缺省表示清空该类型下全部栈
|
|
375
320
|
*/
|
|
376
|
-
public
|
|
377
|
-
|
|
378
|
-
|
|
321
|
+
public clear(stepType: HistoryStepType, id?: Id): void {
|
|
322
|
+
const bucket = this.state.steps[stepType];
|
|
323
|
+
if (!bucket) return;
|
|
324
|
+
|
|
325
|
+
if (this.isValidStackId(id)) {
|
|
326
|
+
this.clearStack(stepType, id);
|
|
327
|
+
} else if (id === undefined) {
|
|
328
|
+
Object.keys(bucket).forEach((stackId) => this.clearStack(stepType, stackId as Id));
|
|
379
329
|
} else {
|
|
380
|
-
|
|
330
|
+
return;
|
|
381
331
|
}
|
|
332
|
+
this.emit('clear', { id: id as Id, stepType });
|
|
382
333
|
}
|
|
383
334
|
|
|
384
335
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
336
|
+
* 标记历史记录为「已保存」(把对应栈当前游标所在的记录标为 `saved`)。统一入口:
|
|
337
|
+
* - 缺省 `id`:标记**全部类型、全部栈**(整份 DSL 落库场景),派发 `mark-saved` 且 `kind: 'all'`;
|
|
338
|
+
* - 传入 `id`:仅标记 `stepType` 下该 id 对应的栈,派发 `mark-saved` 且 `kind` 为 `stepType`。
|
|
339
|
+
*
|
|
340
|
+
* 同一栈内任意时刻最多只有一条记录为 `saved`;从 IndexedDB 恢复时游标会被定位到最近一条已保存记录之后。
|
|
341
|
+
*
|
|
342
|
+
* @param stepType 历史类型
|
|
343
|
+
* @param id 目标栈 id;缺省表示标记全部类型 / 全部栈
|
|
387
344
|
*/
|
|
388
|
-
public
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
345
|
+
public markSaved(stepType: HistoryStepType, id?: Id): void {
|
|
346
|
+
if (id === undefined) {
|
|
347
|
+
Object.values(this.state.steps).forEach((bucket) => Object.values(bucket).forEach(markStackSaved));
|
|
348
|
+
this.emit('mark-saved', { kind: 'all' });
|
|
349
|
+
return;
|
|
393
350
|
}
|
|
351
|
+
if (!this.isValidStackId(id)) return;
|
|
352
|
+
markStackSaved(this.state.steps[stepType]?.[id]);
|
|
353
|
+
this.emit('mark-saved', { kind: stepType, id });
|
|
394
354
|
}
|
|
395
355
|
|
|
396
356
|
/**
|
|
397
|
-
*
|
|
398
|
-
* 适用于「整体落库」场景;若只保存了其中一类,请改用更细粒度的
|
|
399
|
-
* {@link markPageSaved} / {@link markCodeBlockSaved} / {@link markDataSourceSaved}。
|
|
400
|
-
*/
|
|
401
|
-
public markSaved(): void {
|
|
402
|
-
Object.values(this.state.pageSteps).forEach(markStackSaved);
|
|
403
|
-
Object.values(this.state.codeBlockState).forEach(markStackSaved);
|
|
404
|
-
Object.values(this.state.dataSourceState).forEach(markStackSaved);
|
|
405
|
-
this.emit('mark-saved', { kind: 'all' });
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* 标记指定页面(缺省为当前活动页)的历史栈当前记录为已保存。
|
|
410
|
-
* 仅影响该页面自己的栈,不波及代码块 / 数据源 / 其它页面。
|
|
411
|
-
*/
|
|
412
|
-
public markPageSaved(pageId?: Id): void {
|
|
413
|
-
const targetPageId = pageId ?? this.state.pageId;
|
|
414
|
-
if (!targetPageId) return;
|
|
415
|
-
markStackSaved(this.state.pageSteps[targetPageId]);
|
|
416
|
-
this.emit('mark-saved', { kind: 'page', id: targetPageId });
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/** 标记指定代码块的历史栈当前记录为已保存,仅影响该代码块自己的栈。 */
|
|
420
|
-
public markCodeBlockSaved(codeBlockId: Id): void {
|
|
421
|
-
if (!codeBlockId) return;
|
|
422
|
-
markStackSaved(this.state.codeBlockState[codeBlockId]);
|
|
423
|
-
this.emit('mark-saved', { kind: 'code-block', id: codeBlockId });
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** 标记指定数据源的历史栈当前记录为已保存,仅影响该数据源自己的栈。 */
|
|
427
|
-
public markDataSourceSaved(dataSourceId: Id): void {
|
|
428
|
-
if (!dataSourceId) return;
|
|
429
|
-
markStackSaved(this.state.dataSourceState[dataSourceId]);
|
|
430
|
-
this.emit('mark-saved', { kind: 'data-source', id: dataSourceId });
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* 把当前内存中的全部历史栈(页面 / 代码块 / 数据源)序列化后写入本地 IndexedDB。
|
|
357
|
+
* 把当前内存中的全部历史栈(页面 / 代码块 / 数据源 / 扩展类型)序列化后写入本地 IndexedDB。
|
|
435
358
|
*
|
|
436
359
|
* - 每个 UndoRedo 栈连同其游标、容量一并保存,恢复后可继续 undo/redo;
|
|
437
360
|
* - `key` 用于区分不同活动页 / 项目(同一 store 下可保存多份快照),缺省为 `default`;
|
|
@@ -443,12 +366,14 @@ class History extends BaseService {
|
|
|
443
366
|
|
|
444
367
|
// serializeStacks 会在序列化各栈时只把每条 step 的 diff(可能含函数)序列化成字符串,其余字段原样保留,
|
|
445
368
|
// 因此整份快照可直接按对象写入 IndexedDB(结构化克隆),避免序列化整份快照的开销;读取时再用 parseDSL 还原 diff。
|
|
369
|
+
const steps: PersistedHistoryState['steps'] = { page: {}, codeBlock: {}, dataSource: {} };
|
|
370
|
+
Object.entries(this.state.steps).forEach(([stepType, bucket]) => {
|
|
371
|
+
steps[stepType] = serializeStacks(bucket);
|
|
372
|
+
});
|
|
373
|
+
|
|
446
374
|
const snapshot: PersistedHistoryState = {
|
|
447
375
|
version: PERSIST_VERSION,
|
|
448
|
-
|
|
449
|
-
pageSteps: serializeStacks(this.state.pageSteps),
|
|
450
|
-
codeBlockState: serializeStacks(this.state.codeBlockState),
|
|
451
|
-
dataSourceState: serializeStacks(this.state.dataSourceState),
|
|
376
|
+
steps,
|
|
452
377
|
savedAt: Date.now(),
|
|
453
378
|
};
|
|
454
379
|
|
|
@@ -461,7 +386,7 @@ class History extends BaseService {
|
|
|
461
386
|
* 从本地 IndexedDB 读取此前保存的历史快照并重建全部撤销/重做栈。
|
|
462
387
|
*
|
|
463
388
|
* - 读取到的每个栈都会经 {@link UndoRedo.fromSerialized} 还原(含游标),随后可直接 undo/redo;
|
|
464
|
-
* -
|
|
389
|
+
* - 会整体覆盖当前内存中的历史状态(活动页由 editorService 维护,不在此恢复);
|
|
465
390
|
* - 找不到对应记录时返回 null,且不改动当前状态;
|
|
466
391
|
* - 不支持 IndexedDB 的环境(如 SSR)会 reject。
|
|
467
392
|
*/
|
|
@@ -473,176 +398,168 @@ class History extends BaseService {
|
|
|
473
398
|
|
|
474
399
|
// 各 step 的 diff 以序列化字符串存储(含函数),由 deserializeStacks 逐条用 parseDSL 还原。
|
|
475
400
|
const parseDSL = getEditorConfig('parseDSL');
|
|
476
|
-
this.state.pageSteps = deserializeStacks(snapshot.pageSteps, parseDSL);
|
|
477
|
-
this.state.codeBlockState = deserializeStacks(snapshot.codeBlockState, parseDSL);
|
|
478
|
-
this.state.dataSourceState = deserializeStacks(snapshot.dataSourceState, parseDSL);
|
|
479
|
-
// initial 基线作为页面栈 index 0 的 step 随 pageSteps 一并还原,无需单独恢复。
|
|
480
|
-
this.state.pageId = snapshot.pageId;
|
|
481
401
|
|
|
482
|
-
|
|
402
|
+
const steps: HistoryState['steps'] = { page: {}, codeBlock: {}, dataSource: {} };
|
|
403
|
+
Object.entries(snapshot.steps).forEach(([stepType, bucket]) => {
|
|
404
|
+
steps[stepType] = deserializeStacks(bucket, parseDSL);
|
|
405
|
+
});
|
|
406
|
+
this.state.steps = steps;
|
|
407
|
+
// initial 基线作为各栈 index 0 的 step 随 steps 一并还原,无需单独恢复;活动页由 editorService 维护。
|
|
483
408
|
this.emit('restore-from-indexed-db', snapshot);
|
|
484
409
|
return snapshot;
|
|
485
410
|
}
|
|
486
411
|
|
|
487
412
|
/**
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
413
|
+
* 取出指定历史类型(页面 / 代码块 / 数据源 / 扩展类型)某个栈的平铺步骤列表(含 applied 标记)。
|
|
414
|
+
* 统一入口,替代旧的 `getPageStepList` / `getCodeBlockStepList` / `getDataSourceStepList`。
|
|
415
|
+
*
|
|
416
|
+
* 列表按时间正序,最早一步在最前面;`applied` 表示该步骤处于栈游标之前(已应用)。
|
|
417
|
+
* 供 revert / goto 等按 index 索引步骤的场景使用。通常 UI 应使用
|
|
418
|
+
* {@link getHistoryGroups} 取已合并分组的版本;本方法仅为兼容/调试保留。
|
|
419
|
+
*
|
|
420
|
+
* @param stepType 历史类型
|
|
421
|
+
* @param id 目标栈 id(page 为 pageId,其余类型为对应资源 id);缺省 / 无效时返回空数组
|
|
491
422
|
*/
|
|
492
|
-
public
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
423
|
+
public getStepList(stepType: 'page', id?: Id): HistoryStepEntry<StepValue>[];
|
|
424
|
+
public getStepList(stepType: 'codeBlock', id: Id): HistoryStepEntry<CodeBlockStepValue>[];
|
|
425
|
+
public getStepList(stepType: 'dataSource', id: Id): HistoryStepEntry<DataSourceStepValue>[];
|
|
426
|
+
public getStepList<T extends BaseStepValue>(stepType: HistoryStepType, id?: Id): HistoryStepEntry<T>[];
|
|
427
|
+
public getStepList(stepType: HistoryStepType, id?: Id): HistoryStepEntry<any>[] {
|
|
428
|
+
if (!this.isValidStackId(id)) return [];
|
|
429
|
+
const undoRedo = this.state.steps[stepType]?.[id];
|
|
496
430
|
if (!undoRedo) return [];
|
|
497
431
|
const list = undoRedo.getElementList();
|
|
498
432
|
const cursor = undoRedo.getCursor();
|
|
499
|
-
return list.map((step, index) => ({
|
|
500
|
-
step,
|
|
501
|
-
index,
|
|
502
|
-
applied: index < cursor,
|
|
503
|
-
}));
|
|
433
|
+
return list.map((step, index) => ({ step, index, applied: index < cursor }));
|
|
504
434
|
}
|
|
505
435
|
|
|
506
436
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
437
|
+
* 取出指定历史类型的历史分组(页面 / 代码块 / 数据源 / 扩展类型统一入口)。
|
|
438
|
+
*
|
|
439
|
+
* 把目标栈的步骤列表按"目标"做相邻合并(连续修改同一目标的 update 合并为一组,组内可展开查看每步;
|
|
440
|
+
* add / remove / 多实体 update 始终独立成组),并过滤掉 index 0 的 initial 基线(底部「初始」行由
|
|
441
|
+
* {@link getMarker} 驱动)。各类型行为完全一致,仅 `kind` 与 step 快照类型不同。
|
|
442
|
+
*
|
|
443
|
+
* 作用域:
|
|
444
|
+
* - 传入 `id`:仅取该 id 对应的单个栈(页面历史按活动页查看,传入 pageId);
|
|
445
|
+
* - 缺省 `id`:遍历该类型下全部栈并汇总(代码块 / 数据源按全部资源分桶展示)。
|
|
446
|
+
*
|
|
447
|
+
* @param stepType 历史类型,缺省 `page`
|
|
448
|
+
* @param id 目标栈 id;缺省表示遍历该类型下全部栈
|
|
511
449
|
*/
|
|
512
|
-
public
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
if (!
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
450
|
+
public getHistoryGroups(stepType: 'page', id?: Id): HistoryGroup<StepValue>[];
|
|
451
|
+
public getHistoryGroups(stepType: 'codeBlock', id?: Id): HistoryGroup<CodeBlockStepValue>[];
|
|
452
|
+
public getHistoryGroups(stepType: 'dataSource', id?: Id): HistoryGroup<DataSourceStepValue>[];
|
|
453
|
+
public getHistoryGroups<T extends BaseStepValue>(stepType: HistoryStepType, id?: Id): HistoryGroup<T>[];
|
|
454
|
+
public getHistoryGroups(stepType: HistoryStepType, id?: Id): HistoryGroup[] {
|
|
455
|
+
const bucket = this.state.steps[stepType];
|
|
456
|
+
if (!bucket) return [];
|
|
457
|
+
const kind = STEP_TYPE_KIND[stepType] ?? stepType;
|
|
458
|
+
const collect = (stackId: Id): HistoryGroup[] => {
|
|
459
|
+
const undoRedo = bucket[stackId];
|
|
460
|
+
if (!undoRedo) return [];
|
|
461
|
+
const list = undoRedo.getElementList();
|
|
462
|
+
if (!list.length) return [];
|
|
463
|
+
// initial 基线(index 0)不作为普通操作组展示,过滤掉;其余真实 step 的 index 保持不变,
|
|
464
|
+
// 以便面板 goto(index+1) / revert(index) 仍直接对应栈内位置。
|
|
465
|
+
return mergeSteps(kind, stackId, list, undoRedo.getCursor()).filter((group) => group.opType !== 'initial');
|
|
466
|
+
};
|
|
467
|
+
if (this.isValidStackId(id)) return collect(id);
|
|
468
|
+
return Object.keys(bucket).flatMap((stackId) => collect(stackId as Id));
|
|
523
469
|
}
|
|
524
470
|
|
|
525
471
|
/**
|
|
526
|
-
*
|
|
527
|
-
*
|
|
472
|
+
* 读取指定历史栈的当前游标(已应用步骤数量)。统一入口,替代旧的
|
|
473
|
+
* `getPageCursor` / `getCodeBlockCursor` / `getDataSourceCursor`。
|
|
474
|
+
* - `id` 缺省或非法、或对应栈不存在时返回 0;
|
|
475
|
+
* - `stepType` 支持 `page` / `codeBlock` / `dataSource` / 扩展类型。
|
|
528
476
|
*/
|
|
529
|
-
public
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (!undoRedo) return;
|
|
533
|
-
const list = undoRedo.getElementList();
|
|
534
|
-
if (!list.length) return;
|
|
535
|
-
const cursor = undoRedo.getCursor();
|
|
536
|
-
groups.push(...mergeStackSteps('code-block', id, list, cursor));
|
|
537
|
-
});
|
|
538
|
-
return groups;
|
|
477
|
+
public getCursor(stepType: HistoryStepType, id?: Id): number {
|
|
478
|
+
if (!this.isValidStackId(id)) return 0;
|
|
479
|
+
return this.state.steps[stepType]?.[id]?.getCursor() ?? 0;
|
|
539
480
|
}
|
|
540
481
|
|
|
541
482
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
483
|
+
* 按历史记录 uuid 在指定历史类型的栈中查找其所属 id 与索引,统一入口,替代旧的
|
|
484
|
+
* `getPageStepIndexByUuid` / `findCodeBlockStepLocationByUuid` / `findDataSourceStepLocationByUuid`。
|
|
485
|
+
*
|
|
486
|
+
* - 传入 `id`:仅在该 id 对应的单个栈中查找(如页面历史按活动页查看,传入 pageId);
|
|
487
|
+
* - 缺省 `id`:遍历该类型下全部栈查找(代码块 / 数据源等按全部资源分桶的场景)。
|
|
488
|
+
*
|
|
489
|
+
* 找不到时返回 null。供「按 uuid 回滚」等需要把 uuid 映射回 (id, index) 的场景使用。
|
|
490
|
+
*
|
|
491
|
+
* @param stepType 历史类型
|
|
492
|
+
* @param uuid 目标历史记录的 uuid
|
|
493
|
+
* @param id 目标栈 id;缺省表示遍历该类型下全部栈
|
|
544
494
|
*/
|
|
545
|
-
public
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}
|
|
495
|
+
public findStepLocationByUuid(stepType: HistoryStepType, uuid: string, id?: Id): { id: Id; index: number } | null {
|
|
496
|
+
if (!uuid) return null;
|
|
497
|
+
const bucket = this.state.steps[stepType];
|
|
498
|
+
if (!bucket) return null;
|
|
550
499
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
500
|
+
if (this.isValidStackId(id)) {
|
|
501
|
+
const index = this.getStepList(stepType, id).findIndex((entry) => entry.step.uuid === uuid);
|
|
502
|
+
return index >= 0 ? { id, index } : null;
|
|
503
|
+
}
|
|
555
504
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
505
|
+
for (const stackId of Object.keys(bucket)) {
|
|
506
|
+
const index = this.getStepList(stepType, stackId as Id).findIndex((entry) => entry.step.uuid === uuid);
|
|
507
|
+
if (index >= 0) return { id: stackId as Id, index };
|
|
508
|
+
}
|
|
509
|
+
return null;
|
|
559
510
|
}
|
|
560
511
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
*/
|
|
564
|
-
public getCodeBlockStepList(codeBlockId: Id): { step: CodeBlockStepValue; index: number; applied: boolean }[] {
|
|
565
|
-
const undoRedo = this.state.codeBlockState[codeBlockId];
|
|
566
|
-
if (!undoRedo) return [];
|
|
567
|
-
const list = undoRedo.getElementList();
|
|
568
|
-
const cursor = undoRedo.getCursor();
|
|
569
|
-
return list.map((step, index) => ({ step, index, applied: index < cursor }));
|
|
512
|
+
public usePlugin(options: SyncHookPlugin<SyncMethodName, History>): void {
|
|
513
|
+
super.usePlugin(options);
|
|
570
514
|
}
|
|
571
515
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
*/
|
|
575
|
-
public getDataSourceStepList(dataSourceId: Id): { step: DataSourceStepValue; index: number; applied: boolean }[] {
|
|
576
|
-
const undoRedo = this.state.dataSourceState[dataSourceId];
|
|
577
|
-
if (!undoRedo) return [];
|
|
578
|
-
const list = undoRedo.getElementList();
|
|
579
|
-
const cursor = undoRedo.getCursor();
|
|
580
|
-
return list.map((step, index) => ({ step, index, applied: index < cursor }));
|
|
516
|
+
public emit<Name extends keyof HistoryEvents, Param extends HistoryEvents[Name]>(eventName: Name, ...args: Param) {
|
|
517
|
+
return super.emit(eventName, ...args);
|
|
581
518
|
}
|
|
582
519
|
|
|
583
520
|
/**
|
|
584
|
-
*
|
|
585
|
-
*
|
|
521
|
+
* 取出指定历史类型的栈桶(`Record<id, UndoRedo>`);不存在时按需创建空桶,
|
|
522
|
+
* 从而支持通过 {@link registerStepType} 或首次 push 动态扩展历史类型。
|
|
586
523
|
*/
|
|
587
|
-
|
|
588
|
-
if (!
|
|
589
|
-
|
|
524
|
+
private getStepBucket(stepType: string): Record<Id, UndoRedo<any>> {
|
|
525
|
+
if (!this.state.steps[stepType]) {
|
|
526
|
+
this.state.steps[stepType] = {};
|
|
527
|
+
}
|
|
528
|
+
return this.state.steps[stepType];
|
|
590
529
|
}
|
|
591
530
|
|
|
592
531
|
/**
|
|
593
|
-
*
|
|
594
|
-
* 找不到时返回 null。
|
|
532
|
+
* 清空单个历史栈并按需重建其 initial 基线(保留原基线的文案 / 来源)。
|
|
595
533
|
*/
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
534
|
+
private clearStack(stepType: HistoryStepType, id: Id): void {
|
|
535
|
+
// 保留原 initial 基线的文案 / 来源(仅清空其后的真实操作记录),无基线时清空成空栈。
|
|
536
|
+
const marker = this.getMarker(stepType, id);
|
|
537
|
+
this.getStepBucket(stepType)[id] = new UndoRedo<any>();
|
|
538
|
+
if (marker) {
|
|
539
|
+
this.setMarker(stepType, id, {
|
|
540
|
+
name: marker.data?.name,
|
|
541
|
+
description: marker.historyDescription,
|
|
542
|
+
source: marker.source,
|
|
543
|
+
});
|
|
601
544
|
}
|
|
602
|
-
return null;
|
|
603
545
|
}
|
|
604
546
|
|
|
605
|
-
/**
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
public findDataSourceStepLocationByUuid(uuid: string): { id: Id; index: number } | null {
|
|
610
|
-
if (!uuid) return null;
|
|
611
|
-
for (const id of Object.keys(this.state.dataSourceState)) {
|
|
612
|
-
const index = this.getDataSourceStepList(id).findIndex((entry) => entry.step.uuid === uuid);
|
|
613
|
-
if (index >= 0) return { id, index };
|
|
614
|
-
}
|
|
615
|
-
return null;
|
|
547
|
+
/** 入栈前补全 step 的 uuid / timestamp(调用方未指定时)。 */
|
|
548
|
+
private fillStepMeta(step: BaseStepValue): void {
|
|
549
|
+
if (step.uuid === undefined) step.uuid = guid();
|
|
550
|
+
if (step.timestamp === undefined) step.timestamp = Date.now();
|
|
616
551
|
}
|
|
617
552
|
|
|
618
|
-
/**
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
public getDataSourceHistoryGroups(): StackHistoryGroup<DataSourceStepValue, 'data-source'>[] {
|
|
622
|
-
const groups: StackHistoryGroup<DataSourceStepValue, 'data-source'>[] = [];
|
|
623
|
-
Object.entries(this.state.dataSourceState).forEach(([id, undoRedo]) => {
|
|
624
|
-
if (!undoRedo) return;
|
|
625
|
-
const list = undoRedo.getElementList();
|
|
626
|
-
if (!list.length) return;
|
|
627
|
-
const cursor = undoRedo.getCursor();
|
|
628
|
-
groups.push(...mergeStackSteps('data-source', id, list, cursor));
|
|
629
|
-
});
|
|
630
|
-
return groups;
|
|
553
|
+
/** 校验「按 id 分栈」类型(codeBlock / dataSource / 扩展)的 id 是否有效。 */
|
|
554
|
+
private isValidStackId(id?: Id): id is Id {
|
|
555
|
+
return id !== undefined && id !== null && `${id}` !== '';
|
|
631
556
|
}
|
|
632
557
|
|
|
633
|
-
/**
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
*/
|
|
639
|
-
private getUndoRedo(pageId?: Id) {
|
|
640
|
-
const targetPageId = pageId ?? this.state.pageId;
|
|
641
|
-
if (!targetPageId) return null;
|
|
642
|
-
if (!this.state.pageSteps[targetPageId]) {
|
|
643
|
-
this.state.pageSteps[targetPageId] = new UndoRedo<StepValue>();
|
|
644
|
-
}
|
|
645
|
-
return this.state.pageSteps[targetPageId];
|
|
558
|
+
/** 清空全部历史栈内容(保留已注册的类型键,使扩展类型在 reset 后仍可用)。 */
|
|
559
|
+
private clearAllSteps(): void {
|
|
560
|
+
Object.keys(this.state.steps).forEach((stepType) => {
|
|
561
|
+
this.state.steps[stepType] = {};
|
|
562
|
+
});
|
|
646
563
|
}
|
|
647
564
|
|
|
648
565
|
/**
|
|
@@ -654,13 +571,6 @@ class History extends BaseService {
|
|
|
654
571
|
const resolvedAppId = appId ?? editorService.get('root')?.id;
|
|
655
572
|
return resolvedAppId ? `${dbName}-${resolvedAppId}` : dbName;
|
|
656
573
|
}
|
|
657
|
-
|
|
658
|
-
private setCanUndoRedo(): void {
|
|
659
|
-
const undoRedo = this.getUndoRedo();
|
|
660
|
-
this.state.canRedo = undoRedo?.canRedo() || false;
|
|
661
|
-
// 初始基线之上才可撤销:cursor 必须高于底线(有 initial 时为 1)。
|
|
662
|
-
this.state.canUndo = undoRedo ? undoRedo.getCursor() > undoFloor(undoRedo) : false;
|
|
663
|
-
}
|
|
664
574
|
}
|
|
665
575
|
|
|
666
576
|
export type HistoryService = History;
|