@tmagic/editor 1.8.0-beta.4 → 1.8.0-beta.5
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/components/CompareForm.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/es/index.js +6 -3
- package/dist/es/initService.js +1 -1
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -55
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -39
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +149 -103
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +32 -8
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +333 -211
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +28 -7
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -60
- package/dist/es/layouts/history-list/composables.js +73 -32
- package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/services/codeBlock.js +85 -37
- package/dist/es/services/dataSource.js +62 -26
- package/dist/es/services/editor.js +243 -100
- package/dist/es/services/history.js +270 -206
- package/dist/es/services/ui.js +3 -0
- package/dist/es/style.css +49 -6
- package/dist/es/utils/editor.js +35 -1
- package/dist/es/utils/history.js +223 -0
- package/dist/es/utils/indexed-db.js +86 -0
- package/dist/es/utils/undo-redo.js +60 -1
- package/dist/style.css +49 -6
- package/dist/tmagic-editor.umd.cjs +1799 -905
- package/package.json +7 -7
- package/src/components/CompareForm.vue +3 -1
- package/src/components/ToolButton.vue +2 -2
- package/src/index.ts +1 -0
- package/src/initService.ts +1 -1
- package/src/layouts/Framework.vue +1 -1
- package/src/layouts/history-list/Bucket.vue +34 -71
- package/src/layouts/history-list/BucketTab.vue +46 -54
- package/src/layouts/history-list/GroupRow.vue +146 -111
- package/src/layouts/history-list/HistoryDiffDialog.vue +94 -68
- package/src/layouts/history-list/HistoryListPanel.vue +296 -113
- package/src/layouts/history-list/InitialRow.vue +25 -9
- package/src/layouts/history-list/PageTab.vue +57 -51
- package/src/layouts/history-list/composables.ts +157 -36
- package/src/layouts/page-bar/PageBar.vue +4 -2
- package/src/layouts/sidebar/Sidebar.vue +6 -1
- package/src/services/codeBlock.ts +107 -37
- package/src/services/dataSource.ts +89 -40
- package/src/services/editor.ts +370 -134
- package/src/services/history.ts +305 -203
- package/src/services/ui.ts +7 -0
- package/src/theme/history-list-panel.scss +72 -5
- package/src/theme/page-bar.scss +0 -4
- package/src/type.ts +167 -63
- package/src/utils/editor.ts +41 -1
- package/src/utils/history.ts +298 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/indexed-db.ts +122 -0
- package/src/utils/undo-redo.ts +88 -0
- package/types/index.d.ts +783 -291
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
popper-class="m-editor-history-list-popover"
|
|
4
4
|
placement="bottom"
|
|
5
5
|
trigger="click"
|
|
6
|
-
:visible="visible"
|
|
6
|
+
v-model:visible="visible"
|
|
7
7
|
:width="660"
|
|
8
8
|
>
|
|
9
9
|
<div class="m-editor-history-list">
|
|
@@ -23,11 +23,14 @@
|
|
|
23
23
|
<PageTab
|
|
24
24
|
:list="pageGroupsDisplay"
|
|
25
25
|
:expanded="expanded"
|
|
26
|
+
:marker="pageMarker"
|
|
26
27
|
@toggle="toggleGroup"
|
|
27
28
|
@goto="onPageGoto"
|
|
28
29
|
@goto-initial="onPageGotoInitial"
|
|
29
30
|
@diff-step="onPageDiff"
|
|
30
31
|
@revert-step="onPageRevert"
|
|
32
|
+
@select="onPageSelect"
|
|
33
|
+
@clear="onPageClear"
|
|
31
34
|
/>
|
|
32
35
|
</component>
|
|
33
36
|
|
|
@@ -37,19 +40,15 @@
|
|
|
37
40
|
v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
|
|
38
41
|
>
|
|
39
42
|
<BucketTab
|
|
40
|
-
|
|
41
|
-
prefix="ds"
|
|
43
|
+
:config="dataSourceConfig"
|
|
42
44
|
:buckets="dataSourceGroupsByTarget"
|
|
43
45
|
:expanded="expanded"
|
|
44
|
-
:describe-group="describeDataSourceGroup"
|
|
45
|
-
:describe-step="describeDataSourceStep"
|
|
46
|
-
:is-step-diffable="isDataSourceStepDiffable"
|
|
47
|
-
:is-step-revertable="isDataSourceStepRevertable"
|
|
48
46
|
@toggle="toggleGroup"
|
|
49
47
|
@goto="onDataSourceGoto"
|
|
50
48
|
@goto-initial="onDataSourceGotoInitial"
|
|
51
49
|
@diff-step="onDataSourceDiff"
|
|
52
50
|
@revert-step="onDataSourceRevert"
|
|
51
|
+
@clear="onDataSourceClear"
|
|
53
52
|
/>
|
|
54
53
|
</component>
|
|
55
54
|
|
|
@@ -59,19 +58,15 @@
|
|
|
59
58
|
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
|
|
60
59
|
>
|
|
61
60
|
<BucketTab
|
|
62
|
-
|
|
63
|
-
prefix="cb"
|
|
61
|
+
:config="codeBlockConfig"
|
|
64
62
|
:buckets="codeBlockGroupsByTarget"
|
|
65
63
|
:expanded="expanded"
|
|
66
|
-
:describe-group="describeCodeBlockGroup"
|
|
67
|
-
:describe-step="describeCodeBlockStep"
|
|
68
|
-
:is-step-diffable="isCodeBlockStepDiffable"
|
|
69
|
-
:is-step-revertable="isCodeBlockStepRevertable"
|
|
70
64
|
@toggle="toggleGroup"
|
|
71
65
|
@goto="onCodeBlockGoto"
|
|
72
66
|
@goto-initial="onCodeBlockGotoInitial"
|
|
73
67
|
@diff-step="onCodeBlockDiff"
|
|
74
68
|
@revert-step="onCodeBlockRevert"
|
|
69
|
+
@clear="onCodeBlockClear"
|
|
75
70
|
/>
|
|
76
71
|
</component>
|
|
77
72
|
|
|
@@ -97,12 +92,8 @@
|
|
|
97
92
|
</template>
|
|
98
93
|
</TMagicPopover>
|
|
99
94
|
|
|
100
|
-
<HistoryDiffDialog
|
|
101
|
-
|
|
102
|
-
:extend-state="extendFormState"
|
|
103
|
-
:on-confirm="onConfirmRevert"
|
|
104
|
-
@close="onDiffDialogClose"
|
|
105
|
-
/>
|
|
95
|
+
<HistoryDiffDialog ref="diffDialog" :extend-state="extendFormState" />
|
|
96
|
+
<HistoryDiffDialog ref="confirmDialog" :is-confirm="true" :extend-state="extendFormState" />
|
|
106
97
|
</template>
|
|
107
98
|
|
|
108
99
|
<script lang="ts" setup>
|
|
@@ -127,15 +118,29 @@
|
|
|
127
118
|
* (通过 title / prefix / describe* / isStepDiffable 注入差异)。
|
|
128
119
|
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
|
129
120
|
*/
|
|
130
|
-
import { computed, inject, markRaw, ref,
|
|
121
|
+
import { computed, inject, markRaw, ref, useTemplateRef, watch } from 'vue';
|
|
131
122
|
import { Clock, Close } from '@element-plus/icons-vue';
|
|
132
123
|
|
|
133
|
-
import {
|
|
124
|
+
import {
|
|
125
|
+
getDesignConfig,
|
|
126
|
+
TMagicButton,
|
|
127
|
+
tMagicMessage,
|
|
128
|
+
tMagicMessageBox,
|
|
129
|
+
TMagicPopover,
|
|
130
|
+
TMagicTabs,
|
|
131
|
+
TMagicTooltip,
|
|
132
|
+
} from '@tmagic/design';
|
|
134
133
|
import type { FormState } from '@tmagic/form';
|
|
135
134
|
|
|
136
135
|
import MIcon from '@editor/components/Icon.vue';
|
|
137
136
|
import { useServices } from '@editor/hooks/use-services';
|
|
138
|
-
import type {
|
|
137
|
+
import type {
|
|
138
|
+
CodeBlockStepValue,
|
|
139
|
+
DataSourceStepValue,
|
|
140
|
+
DiffDialogPayload,
|
|
141
|
+
HistoryBucketConfig,
|
|
142
|
+
HistoryListExtraTab,
|
|
143
|
+
} from '@editor/type';
|
|
139
144
|
|
|
140
145
|
import BucketTab from './BucketTab.vue';
|
|
141
146
|
import {
|
|
@@ -158,7 +163,10 @@ const ClockIcon = markRaw(Clock);
|
|
|
158
163
|
const CloseIcon = markRaw(Close);
|
|
159
164
|
const activeTab = ref<string>('page');
|
|
160
165
|
|
|
161
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。
|
|
168
|
+
* 点击面板以外区域的自动收起由 TMagicPopover 通过 v-model:visible 回写完成。
|
|
169
|
+
*/
|
|
162
170
|
const visible = ref(false);
|
|
163
171
|
|
|
164
172
|
const tabPaneComponent = getDesignConfig('components')?.tabPane;
|
|
@@ -172,7 +180,8 @@ const extraTabs = inject<HistoryListExtraTab[]>('historyListExtraTabs', []);
|
|
|
172
180
|
/** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
|
|
173
181
|
const resolveTabLabel = (tab: HistoryListExtraTab) => (typeof tab.label === 'function' ? tab.label() : tab.label);
|
|
174
182
|
|
|
175
|
-
const { editorService, dataSourceService, codeBlockService, historyService, propsService } =
|
|
183
|
+
const { editorService, dataSourceService, codeBlockService, historyService, propsService, stageOverlayService } =
|
|
184
|
+
useServices();
|
|
176
185
|
|
|
177
186
|
/**
|
|
178
187
|
* 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用,
|
|
@@ -208,11 +217,41 @@ const {
|
|
|
208
217
|
codeBlockGroupsByTarget,
|
|
209
218
|
} = useHistoryList();
|
|
210
219
|
|
|
220
|
+
/**
|
|
221
|
+
* 当前活动页的「加载/初始」标记记录(设置 root 时生成),透传给 PageTab 的底部初始行展示。
|
|
222
|
+
* 基于 historyService 的 reactive state 派生,活动页切换或标记写入后自动刷新。
|
|
223
|
+
*/
|
|
224
|
+
const pageMarker = computed(() => historyService.getPageMarker());
|
|
225
|
+
|
|
211
226
|
/** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
|
|
212
|
-
const isDataSourceStepDiffable = (step: DataSourceStepValue) =>
|
|
227
|
+
const isDataSourceStepDiffable = (step: DataSourceStepValue) =>
|
|
228
|
+
Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
|
|
213
229
|
|
|
214
230
|
/** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
|
|
215
|
-
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) =>
|
|
231
|
+
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) =>
|
|
232
|
+
Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 数据源 / 代码块两类 bucket 历史的整体渲染配置:把 title / prefix 与各自的描述、
|
|
236
|
+
* 可差异、可回滚判定收敛为单一对象整体注入 BucketTab,组件内部按需读取。
|
|
237
|
+
*/
|
|
238
|
+
const dataSourceConfig: HistoryBucketConfig<DataSourceStepValue> = {
|
|
239
|
+
title: '数据源',
|
|
240
|
+
prefix: 'ds',
|
|
241
|
+
describeGroup: describeDataSourceGroup,
|
|
242
|
+
describeStep: describeDataSourceStep,
|
|
243
|
+
isStepDiffable: isDataSourceStepDiffable,
|
|
244
|
+
isStepRevertable: isDataSourceStepRevertable,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const codeBlockConfig: HistoryBucketConfig<CodeBlockStepValue> = {
|
|
248
|
+
title: '代码块',
|
|
249
|
+
prefix: 'cb',
|
|
250
|
+
describeGroup: describeCodeBlockGroup,
|
|
251
|
+
describeStep: describeCodeBlockStep,
|
|
252
|
+
isStepDiffable: isCodeBlockStepDiffable,
|
|
253
|
+
isStepRevertable: isCodeBlockStepRevertable,
|
|
254
|
+
};
|
|
216
255
|
|
|
217
256
|
/** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
|
|
218
257
|
const indexToCursor = (index: number) => index + 1;
|
|
@@ -221,6 +260,29 @@ const onPageGoto = (index: number) => {
|
|
|
221
260
|
editorService.gotoPageStep(indexToCursor(index));
|
|
222
261
|
};
|
|
223
262
|
|
|
263
|
+
/**
|
|
264
|
+
* 点击页面历史记录行:选中该记录对应的画布节点。
|
|
265
|
+
* - 从目标 step 的 diff 中取节点 id(优先 newSchema,回退 oldSchema),按出现顺序找到第一个当前仍存在的节点;
|
|
266
|
+
* - 与图层树点击选中一致:editorService.select + 画布 / overlay 画布 select 三者联动;
|
|
267
|
+
* - 该 step 涉及的节点都已不存在(如删除记录、被撤销的新增)时给出提示,不做选中。
|
|
268
|
+
*/
|
|
269
|
+
const onPageSelect = async (index: number) => {
|
|
270
|
+
const step = historyService.getPageStepList()[index]?.step;
|
|
271
|
+
if (!step) return;
|
|
272
|
+
const targetId = (step.diff ?? [])
|
|
273
|
+
.map((item) => item.newSchema?.id ?? item.oldSchema?.id)
|
|
274
|
+
.find((id) => id !== undefined && id !== null && editorService.getNodeById(id, false));
|
|
275
|
+
if (targetId === undefined || targetId === null) {
|
|
276
|
+
tMagicMessage.warning('该记录对应的节点已不存在,无法选中');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const node = editorService.getNodeById(targetId, false);
|
|
280
|
+
if (!node) return;
|
|
281
|
+
await editorService.select(node);
|
|
282
|
+
editorService.get('stage')?.select(targetId);
|
|
283
|
+
stageOverlayService.get('stage')?.select(targetId);
|
|
284
|
+
};
|
|
285
|
+
|
|
224
286
|
const onDataSourceGoto = (id: string | number, index: number) => {
|
|
225
287
|
dataSourceService.goto(id, indexToCursor(index));
|
|
226
288
|
};
|
|
@@ -246,83 +308,90 @@ const onCodeBlockGotoInitial = (id: string | number) => {
|
|
|
246
308
|
};
|
|
247
309
|
|
|
248
310
|
const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
|
|
311
|
+
const confirmDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('confirmDialog');
|
|
249
312
|
|
|
250
313
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
|
|
254
|
-
* 无可对比内容(如多节点 / add / remove)时返回 null。
|
|
314
|
+
* 三类历史(页面 / 数据源 / 代码块)差异弹窗入参的构造差异,收敛为一份配置:
|
|
315
|
+
* 仅「分组来源、当前值读取、类型 / 展示名提取」不同,定位 step、校验前后值、组装 payload 的流程共用。
|
|
255
316
|
*/
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
type,
|
|
269
|
-
lastValue: item.oldNode as Record<string, any>,
|
|
270
|
-
value: item.newNode as Record<string, any>,
|
|
271
|
-
currentValue: (currentNode as Record<string, any>) || null,
|
|
272
|
-
targetLabel: (item.newNode.name as string) || (item.oldNode.name as string) || type,
|
|
273
|
-
id: nodeId,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
return null;
|
|
277
|
-
};
|
|
317
|
+
interface DiffPayloadSource {
|
|
318
|
+
/** 表单类别:节点 / 数据源 / 代码块。 */
|
|
319
|
+
category: DiffDialogPayload['category'];
|
|
320
|
+
/** 该类别按时间正序的历史分组列表(含已撤销)。 */
|
|
321
|
+
groups: () => { id?: string | number; steps: { index: number; step: { diff?: any[] } }[] }[];
|
|
322
|
+
/** 读取目标当前实际值,用于「与当前对比」;不存在时返回空即禁用对比。 */
|
|
323
|
+
getCurrent: (_id: string | number) => Record<string, any> | null | undefined;
|
|
324
|
+
/** 由新/旧快照提取展示名(含各自的兜底,如节点回退 type、数据源 / 代码块回退 id)。 */
|
|
325
|
+
resolveLabel: (_newSchema: Record<string, any>, _oldSchema: Record<string, any>, _id: string | number) => string;
|
|
326
|
+
/** 由新/旧快照提取类型;代码块无 type 字段则不传。 */
|
|
327
|
+
resolveType?: (_newSchema: Record<string, any>, _oldSchema: Record<string, any>) => string;
|
|
328
|
+
}
|
|
278
329
|
|
|
279
330
|
/**
|
|
280
|
-
*
|
|
281
|
-
*
|
|
331
|
+
* 构造差异弹窗入参:仅 update(前后值都存在)可对比。
|
|
332
|
+
* - 页面(无 id):在全部分组中按 index 定位 step,目标 id 取自快照;
|
|
333
|
+
* - 数据源 / 代码块(带 id):先匹配分组 id 再按 index 定位。
|
|
334
|
+
* 无可对比内容(多节点 / add / remove)或定位不到时返回 null。
|
|
282
335
|
*/
|
|
283
|
-
const
|
|
284
|
-
groups
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
return
|
|
336
|
+
const buildDiffPayload = (source: DiffPayloadSource, index: number, id?: string | number): DiffDialogPayload | null => {
|
|
337
|
+
for (const group of source.groups()) {
|
|
338
|
+
if (id !== undefined && group.id !== id) continue;
|
|
339
|
+
const step = group.steps.find((s) => s.index === index)?.step;
|
|
340
|
+
if (!step) continue;
|
|
341
|
+
const oldSchema = step.diff?.[0]?.oldSchema as Record<string, any> | undefined;
|
|
342
|
+
const newSchema = step.diff?.[0]?.newSchema as Record<string, any> | undefined;
|
|
343
|
+
if (!oldSchema || !newSchema) return null;
|
|
344
|
+
const targetId = id ?? newSchema.id ?? oldSchema.id;
|
|
345
|
+
const type = source.resolveType?.(newSchema, oldSchema);
|
|
346
|
+
return {
|
|
347
|
+
category: source.category,
|
|
348
|
+
...(type !== undefined ? { type } : {}),
|
|
349
|
+
lastValue: oldSchema,
|
|
350
|
+
value: newSchema,
|
|
351
|
+
currentValue: (targetId !== undefined ? source.getCurrent(targetId) : null) || null,
|
|
352
|
+
targetLabel: source.resolveLabel(newSchema, oldSchema, targetId),
|
|
353
|
+
id: targetId,
|
|
354
|
+
};
|
|
294
355
|
}
|
|
295
356
|
return null;
|
|
296
357
|
};
|
|
297
358
|
|
|
359
|
+
const buildPageDiffPayload = (index: number): DiffDialogPayload | null =>
|
|
360
|
+
buildDiffPayload(
|
|
361
|
+
{
|
|
362
|
+
category: 'node',
|
|
363
|
+
groups: () => historyService.getPageHistoryGroups(),
|
|
364
|
+
getCurrent: (id) => editorService.getNodeById(id) as Record<string, any> | null,
|
|
365
|
+
resolveType: (n, o) => n.type || o.type || '',
|
|
366
|
+
resolveLabel: (n, o) => n.name || o.name || n.type || o.type || '',
|
|
367
|
+
},
|
|
368
|
+
index,
|
|
369
|
+
);
|
|
370
|
+
|
|
298
371
|
const buildDataSourceDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const currentSchema = dataSourceService.getDataSourceById(`${id}`);
|
|
302
|
-
return {
|
|
372
|
+
buildDiffPayload(
|
|
373
|
+
{
|
|
303
374
|
category: 'data-source',
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
375
|
+
groups: () => historyService.getDataSourceHistoryGroups(),
|
|
376
|
+
getCurrent: (id) => dataSourceService.getDataSourceById(`${id}`) as Record<string, any> | null,
|
|
377
|
+
resolveType: (n, o) => n.type || o.type || 'base',
|
|
378
|
+
resolveLabel: (n, o, id) => n.title || o.title || `${id}`,
|
|
379
|
+
},
|
|
380
|
+
index,
|
|
381
|
+
id,
|
|
382
|
+
);
|
|
312
383
|
|
|
313
384
|
const buildCodeBlockDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const currentContent = codeBlockService.getCodeContentById(id);
|
|
317
|
-
return {
|
|
385
|
+
buildDiffPayload(
|
|
386
|
+
{
|
|
318
387
|
category: 'code-block',
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
388
|
+
groups: () => historyService.getCodeBlockHistoryGroups(),
|
|
389
|
+
getCurrent: (id) => codeBlockService.getCodeContentById(id) as Record<string, any> | null,
|
|
390
|
+
resolveLabel: (n, o, id) => n.name || o.name || `${id}`,
|
|
391
|
+
},
|
|
392
|
+
index,
|
|
393
|
+
id,
|
|
394
|
+
);
|
|
326
395
|
|
|
327
396
|
const onPageDiff = (index: number) => {
|
|
328
397
|
const payload = buildPageDiffPayload(index);
|
|
@@ -339,46 +408,160 @@ const onCodeBlockDiff = (id: string | number, index: number) => {
|
|
|
339
408
|
if (payload) diffDialogRef.value?.open(payload);
|
|
340
409
|
};
|
|
341
410
|
|
|
342
|
-
const onConfirmRevert = shallowRef();
|
|
343
|
-
|
|
344
411
|
/**
|
|
345
|
-
*
|
|
412
|
+
* 「回滚」统一入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
|
|
346
413
|
* 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
|
|
347
414
|
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
415
|
+
* 交互:
|
|
416
|
+
* - 可差异对比的步骤(单节点 / 单实体 update):弹出差异弹窗供用户确认,点「确定回滚」再执行;
|
|
417
|
+
* - 无法对比的步骤(add / remove / 多节点更新,payload 为 null):弹出普通二次确认框,确认后执行。
|
|
418
|
+
*
|
|
419
|
+
* 页面 / 数据源 / 代码块三类回滚仅「差异入参构造」与「实际 revert 调用」不同,
|
|
420
|
+
* 由调用方分别传入 payload 与 revert,公共的弹窗 / 确认流程在此收敛。
|
|
421
|
+
*/
|
|
422
|
+
const runRevert = (payload: DiffDialogPayload | null): Promise<boolean> => {
|
|
423
|
+
if (payload && confirmDialogRef.value) {
|
|
424
|
+
return confirmDialogRef.value.confirm(payload);
|
|
425
|
+
}
|
|
426
|
+
return confirmRevert();
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 回滚前置校验:若该历史步骤回滚所依赖的目标数据已被删除,则无法回滚。
|
|
431
|
+
* - update(把旧值写回):被修改的目标必须仍存在;
|
|
432
|
+
* - 页面 remove(还原被删节点):被删节点的原父容器必须仍存在,否则无处插回;
|
|
433
|
+
* add(回滚即删除)即使目标已不在,也已达成「删除」目的,不视为失败。
|
|
434
|
+
*
|
|
435
|
+
* 命中时弹出「回滚失败」提示并返回 true,调用方据此中止本次回滚。
|
|
350
436
|
*/
|
|
437
|
+
const isPageRevertTargetMissing = (index: number): boolean => {
|
|
438
|
+
const step = historyService.getPageStepList()[index]?.step;
|
|
439
|
+
if (!step) return false;
|
|
440
|
+
if (step.opType === 'update') {
|
|
441
|
+
return (step.diff ?? []).some((item) => {
|
|
442
|
+
const id = item.newSchema?.id ?? item.oldSchema?.id;
|
|
443
|
+
return id !== undefined && !editorService.getNodeById(id, false);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (step.opType === 'remove') {
|
|
447
|
+
return (step.diff ?? []).some(
|
|
448
|
+
(item) => item.parentId !== undefined && !editorService.getNodeById(item.parentId, false),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
/** 数据源 update 步骤回滚时,对应数据源必须仍存在(已删除则无处写回旧值)。 */
|
|
455
|
+
const isDataSourceRevertTargetMissing = (id: string | number, index: number): boolean => {
|
|
456
|
+
const step = historyService.getDataSourceStepList(id)[index]?.step;
|
|
457
|
+
return Boolean(step && step.opType === 'update' && !dataSourceService.getDataSourceById(`${id}`));
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/** 代码块 update 步骤回滚时,对应代码块必须仍存在(已删除则无处写回旧值)。 */
|
|
461
|
+
const isCodeBlockRevertTargetMissing = (id: string | number, index: number): boolean => {
|
|
462
|
+
const step = historyService.getCodeBlockStepList(id)[index]?.step;
|
|
463
|
+
return Boolean(step && step.opType === 'update' && !codeBlockService.getCodeContentById(id));
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
/** 目标数据已被删除、无法回滚时的统一提示。 */
|
|
467
|
+
const showRevertTargetMissing = () => {
|
|
468
|
+
tMagicMessage.error('回滚失败:该记录对应的数据已被删除');
|
|
469
|
+
};
|
|
470
|
+
|
|
351
471
|
const onPageRevert = (index: number) => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
diffDialogRef.value?.open({ ...payload });
|
|
356
|
-
} else {
|
|
357
|
-
onConfirmRevert.value();
|
|
472
|
+
if (isPageRevertTargetMissing(index)) {
|
|
473
|
+
showRevertTargetMissing();
|
|
474
|
+
return Promise.resolve(null);
|
|
358
475
|
}
|
|
476
|
+
return runRevert(buildPageDiffPayload(index)).then((result) => (result ? editorService.revertPageStep(index) : null));
|
|
359
477
|
};
|
|
360
478
|
|
|
361
479
|
const onDataSourceRevert = (id: string | number, index: number) => {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
diffDialogRef.value?.open({ ...payload });
|
|
366
|
-
} else {
|
|
367
|
-
onConfirmRevert.value();
|
|
480
|
+
if (isDataSourceRevertTargetMissing(id, index)) {
|
|
481
|
+
showRevertTargetMissing();
|
|
482
|
+
return Promise.resolve(null);
|
|
368
483
|
}
|
|
484
|
+
return runRevert(buildDataSourceDiffPayload(id, index)).then((result) =>
|
|
485
|
+
result ? dataSourceService.revert(id, index) : null,
|
|
486
|
+
);
|
|
369
487
|
};
|
|
370
488
|
|
|
371
489
|
const onCodeBlockRevert = (id: string | number, index: number) => {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
diffDialogRef.value?.open({ ...payload });
|
|
376
|
-
} else {
|
|
377
|
-
onConfirmRevert.value();
|
|
490
|
+
if (isCodeBlockRevertTargetMissing(id, index)) {
|
|
491
|
+
showRevertTargetMissing();
|
|
492
|
+
return Promise.resolve(null);
|
|
378
493
|
}
|
|
494
|
+
return runRevert(buildCodeBlockDiffPayload(id, index)).then((result) =>
|
|
495
|
+
result ? codeBlockService.revert(id, index) : null,
|
|
496
|
+
);
|
|
379
497
|
};
|
|
380
498
|
|
|
381
|
-
|
|
382
|
-
|
|
499
|
+
/**
|
|
500
|
+
* 「回滚」二次确认:新增 / 删除 / 多节点更新等无法做差异对比的步骤,
|
|
501
|
+
* 不弹差异弹窗,改用一个普通确认框替代「确定回滚」按钮,避免点击后无任何提示直接执行。
|
|
502
|
+
* 用户取消时返回 false,调用方据此中止回滚。
|
|
503
|
+
*/
|
|
504
|
+
const confirmRevert = (): Promise<boolean> =>
|
|
505
|
+
confirmDialog(
|
|
506
|
+
'确定回滚该步骤吗?回滚会将该操作作为一条新记录反向应用(新增将被删除、删除将被还原),不影响后续历史记录。',
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* 通用二次确认弹窗:清空历史 / 无法差异对比的回滚等会改变状态的操作,先弹出确认框,
|
|
511
|
+
* 用户点击「确定」返回 true,取消(confirm reject)时返回 false 并静默忽略。
|
|
512
|
+
*/
|
|
513
|
+
const confirmDialog = async (message: string): Promise<boolean> => {
|
|
514
|
+
try {
|
|
515
|
+
await tMagicMessageBox.confirm(message, '提示', {
|
|
516
|
+
confirmButtonText: '确定',
|
|
517
|
+
cancelButtonText: '取消',
|
|
518
|
+
type: 'warning',
|
|
519
|
+
});
|
|
520
|
+
return true;
|
|
521
|
+
// eslint-disable-next-line no-unused-vars
|
|
522
|
+
} catch (e) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 把内存中(已清空对应类别后的)历史状态重新写回 IndexedDB,
|
|
529
|
+
* 使本地持久化的那份与内存保持一致——即「连同本地保存的一并删除」。
|
|
530
|
+
* 不支持 IndexedDB 或写入失败时静默忽略(内存清空已生效)。
|
|
531
|
+
*/
|
|
532
|
+
const syncIndexedDB = async () => {
|
|
533
|
+
try {
|
|
534
|
+
await historyService.saveToIndexedDB();
|
|
535
|
+
// eslint-disable-next-line no-unused-vars
|
|
536
|
+
} catch (e) {
|
|
537
|
+
// ignore: 内存清空已生效,本地同步失败不阻塞交互
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const onPageClear = async () => {
|
|
542
|
+
if (
|
|
543
|
+
await confirmDialog('确定清空当前页面的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')
|
|
544
|
+
) {
|
|
545
|
+
historyService.clearPage();
|
|
546
|
+
await syncIndexedDB();
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const onDataSourceClear = async () => {
|
|
551
|
+
if (
|
|
552
|
+
await confirmDialog('确定清空数据源的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')
|
|
553
|
+
) {
|
|
554
|
+
historyService.clearDataSource();
|
|
555
|
+
await syncIndexedDB();
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const onCodeBlockClear = async () => {
|
|
560
|
+
if (
|
|
561
|
+
await confirmDialog('确定清空代码块的历史记录吗?清空后将无法撤销/重做之前的操作,本地保存的记录也会一并删除。')
|
|
562
|
+
) {
|
|
563
|
+
historyService.clearCodeBlock();
|
|
564
|
+
await syncIndexedDB();
|
|
565
|
+
}
|
|
383
566
|
};
|
|
384
567
|
</script>
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
<li
|
|
3
3
|
class="m-editor-history-list-item m-editor-history-list-initial"
|
|
4
4
|
:class="{ 'is-current': isCurrent, 'is-clickable': !isCurrent }"
|
|
5
|
-
:title="
|
|
5
|
+
:title="rowTitle"
|
|
6
6
|
>
|
|
7
7
|
<span class="m-editor-history-list-item-index" title="历史步骤编号 #0(未修改的初始状态)">#0</span>
|
|
8
8
|
<span class="m-editor-history-list-item-op op-initial">初始</span>
|
|
9
|
-
<span class="m-editor-history-list-item-desc"
|
|
10
|
-
<span
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@click.stop="onClick"
|
|
15
|
-
>回到</span
|
|
16
|
-
>
|
|
9
|
+
<span class="m-editor-history-list-item-desc">{{ desc }}</span>
|
|
10
|
+
<span v-if="gotoEnabled && !isCurrent" class="m-editor-history-list-item-actions">
|
|
11
|
+
<span class="m-editor-history-list-item-goto" title="回到该记录" @click.stop="onClick">回到</span>
|
|
12
|
+
</span>
|
|
13
|
+
<span v-if="time" class="m-editor-history-list-item-time" :title="timeTitle">{{ time }}</span>
|
|
17
14
|
</li>
|
|
18
15
|
</template>
|
|
19
16
|
|
|
@@ -22,9 +19,17 @@
|
|
|
22
19
|
* 「初始状态」记录行:渲染于历史列表底部,作为整个栈的"零点"。
|
|
23
20
|
* - 点击该行会把对应栈撤销到 cursor === 0(即没有任何已应用步骤),等同于回到所有修改之前。
|
|
24
21
|
* - 当对应栈本身已处于 cursor === 0 时(isCurrent=true),用户已在初始状态,点击不再触发动作。
|
|
22
|
+
* - 当上层传入 `marker`(设置 root 时为该页生成的「未修改的初始状态」标记)时,
|
|
23
|
+
* 用标记的文案与时间渲染本行;标记不进入撤销/重做栈,仅作为该页基线展示。
|
|
25
24
|
*
|
|
26
25
|
* 该行不是真实 step,仅作为 UI 入口;上层负责把"点击"翻译为 `service.goto*(0)`。
|
|
27
26
|
*/
|
|
27
|
+
import { computed } from 'vue';
|
|
28
|
+
|
|
29
|
+
import type { StepValue } from '@editor/type';
|
|
30
|
+
|
|
31
|
+
import { formatHistoryFullTime, formatHistoryTime } from './composables';
|
|
32
|
+
|
|
28
33
|
defineOptions({
|
|
29
34
|
name: 'MEditorHistoryListInitialRow',
|
|
30
35
|
});
|
|
@@ -34,12 +39,23 @@ const props = withDefaults(
|
|
|
34
39
|
/** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */
|
|
35
40
|
isCurrent: boolean;
|
|
36
41
|
gotoEnabled?: boolean;
|
|
42
|
+
/** 该页面的「加载/初始」基线记录(设置 root 时生成的 `opType: 'initial'` StepValue);提供时用其文案与时间展示。 */
|
|
43
|
+
marker?: StepValue;
|
|
37
44
|
}>(),
|
|
38
45
|
{
|
|
39
46
|
gotoEnabled: true,
|
|
47
|
+
marker: undefined,
|
|
40
48
|
},
|
|
41
49
|
);
|
|
42
50
|
|
|
51
|
+
const desc = computed(() => props.marker?.historyDescription || '未修改的初始状态');
|
|
52
|
+
const time = computed(() => formatHistoryTime(props.marker?.timestamp));
|
|
53
|
+
const timeTitle = computed(() => formatHistoryFullTime(props.marker?.timestamp));
|
|
54
|
+
const rowTitle = computed(() => {
|
|
55
|
+
const base = props.marker?.historyDescription || '未修改的初始状态';
|
|
56
|
+
return props.isCurrent ? `当前已回到${base}` : `点击回到${base}`;
|
|
57
|
+
});
|
|
58
|
+
|
|
43
59
|
const emit = defineEmits<{
|
|
44
60
|
/** 点击非当前的初始项时触发,由上层调用对应 service 的 goto 把 cursor 移到 0。 */
|
|
45
61
|
(_e: 'goto-initial'): void;
|