@tmagic/editor 1.8.0-beta.2 → 1.8.0-beta.3
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 +9 -0
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +32 -28
- package/dist/es/editorProps.js +2 -0
- package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +2 -5
- package/dist/es/hooks/use-stage.js +2 -1
- package/dist/es/index.js +3 -1
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +2 -5
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +33 -14
- package/dist/es/layouts/history-list/BucketTab.js +5 -0
- package/dist/es/layouts/history-list/{CodeBlockTab.vue_vue_type_script_setup_true_lang.js → BucketTab.vue_vue_type_script_setup_true_lang.js} +27 -16
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +71 -53
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +78 -28
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +136 -66
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +15 -9
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +12 -6
- package/dist/es/layouts/history-list/composables.js +20 -2
- package/dist/es/services/history.js +5 -2
- package/dist/es/style.css +44 -12
- package/dist/style.css +44 -12
- package/dist/tmagic-editor.umd.cjs +479 -326
- package/package.json +7 -7
- package/src/Editor.vue +8 -0
- package/src/components/CompareForm.vue +32 -19
- package/src/editorProps.ts +7 -0
- package/src/fields/CodeLink.vue +2 -5
- package/src/hooks/use-stage.ts +1 -0
- package/src/index.ts +2 -0
- package/src/layouts/CodeEditor.vue +2 -5
- package/src/layouts/history-list/Bucket.vue +53 -28
- package/src/layouts/history-list/BucketTab.vue +77 -0
- package/src/layouts/history-list/GroupRow.vue +95 -60
- package/src/layouts/history-list/HistoryDiffDialog.vue +51 -32
- package/src/layouts/history-list/HistoryListPanel.vue +161 -52
- package/src/layouts/history-list/InitialRow.vue +17 -6
- package/src/layouts/history-list/PageTab.vue +20 -6
- package/src/layouts/history-list/composables.ts +32 -1
- package/src/services/history.ts +3 -0
- package/src/theme/history-list-panel.scss +52 -13
- package/src/type.ts +90 -0
- package/types/index.d.ts +295 -135
- package/dist/es/layouts/history-list/CodeBlockTab.js +0 -5
- package/dist/es/layouts/history-list/DataSourceTab.js +0 -5
- package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +0 -62
- package/src/layouts/history-list/CodeBlockTab.vue +0 -61
- package/src/layouts/history-list/DataSourceTab.vue +0 -61
|
@@ -32,12 +32,18 @@
|
|
|
32
32
|
</component>
|
|
33
33
|
|
|
34
34
|
<component
|
|
35
|
+
v-if="!disabledDataSource"
|
|
35
36
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
|
36
37
|
v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
|
|
37
38
|
>
|
|
38
|
-
<
|
|
39
|
+
<BucketTab
|
|
40
|
+
title="数据源"
|
|
41
|
+
prefix="ds"
|
|
39
42
|
:buckets="dataSourceGroupsByTarget"
|
|
40
43
|
:expanded="expanded"
|
|
44
|
+
:describe-group="describeDataSourceGroup"
|
|
45
|
+
:describe-step="describeDataSourceStep"
|
|
46
|
+
:is-step-diffable="isDataSourceStepDiffable"
|
|
41
47
|
@toggle="toggleGroup"
|
|
42
48
|
@goto="onDataSourceGoto"
|
|
43
49
|
@goto-initial="onDataSourceGotoInitial"
|
|
@@ -47,12 +53,18 @@
|
|
|
47
53
|
</component>
|
|
48
54
|
|
|
49
55
|
<component
|
|
56
|
+
v-if="!disabledCodeBlock"
|
|
50
57
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
|
51
58
|
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
|
|
52
59
|
>
|
|
53
|
-
<
|
|
60
|
+
<BucketTab
|
|
61
|
+
title="代码块"
|
|
62
|
+
prefix="cb"
|
|
54
63
|
:buckets="codeBlockGroupsByTarget"
|
|
55
64
|
:expanded="expanded"
|
|
65
|
+
:describe-group="describeCodeBlockGroup"
|
|
66
|
+
:describe-step="describeCodeBlockStep"
|
|
67
|
+
:is-step-diffable="isCodeBlockStepDiffable"
|
|
56
68
|
@toggle="toggleGroup"
|
|
57
69
|
@goto="onCodeBlockGoto"
|
|
58
70
|
@goto-initial="onCodeBlockGotoInitial"
|
|
@@ -60,6 +72,15 @@
|
|
|
60
72
|
@revert-step="onCodeBlockRevert"
|
|
61
73
|
/>
|
|
62
74
|
</component>
|
|
75
|
+
|
|
76
|
+
<component
|
|
77
|
+
v-for="tab in extraTabs"
|
|
78
|
+
:key="tab.name"
|
|
79
|
+
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
|
80
|
+
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab) }) || {}"
|
|
81
|
+
>
|
|
82
|
+
<component :is="tab.component" v-bind="tab.props || {}" v-on="tab.listeners || {}" />
|
|
83
|
+
</component>
|
|
63
84
|
</TMagicTabs>
|
|
64
85
|
</div>
|
|
65
86
|
|
|
@@ -74,7 +95,12 @@
|
|
|
74
95
|
</template>
|
|
75
96
|
</TMagicPopover>
|
|
76
97
|
|
|
77
|
-
<HistoryDiffDialog
|
|
98
|
+
<HistoryDiffDialog
|
|
99
|
+
ref="diffDialog"
|
|
100
|
+
:extend-state="extendFormState"
|
|
101
|
+
:on-confirm="onConfirmRevert"
|
|
102
|
+
@close="onDiffDialogClose"
|
|
103
|
+
/>
|
|
78
104
|
</template>
|
|
79
105
|
|
|
80
106
|
<script lang="ts" setup>
|
|
@@ -95,10 +121,11 @@
|
|
|
95
121
|
* 此外每条 step 上提供"查看差异"入口(仅在前后值都存在的 update 步骤显示),
|
|
96
122
|
* 点击后弹出 HistoryDiffDialog,使用 CompareForm 组件以表单形式展示新旧值差异。
|
|
97
123
|
*
|
|
98
|
-
* 各 tab 的内容拆分为独立的 SFC
|
|
124
|
+
* 各 tab 的内容拆分为独立的 SFC:页面用 PageTab,数据源 / 代码块复用通用的 BucketTab
|
|
125
|
+
* (通过 title / prefix / describe* / isStepDiffable 注入差异)。
|
|
99
126
|
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
|
100
127
|
*/
|
|
101
|
-
import { inject, markRaw, ref, useTemplateRef } from 'vue';
|
|
128
|
+
import { computed, inject, markRaw, ref, shallowRef, useTemplateRef, watch } from 'vue';
|
|
102
129
|
import { Clock, Close } from '@element-plus/icons-vue';
|
|
103
130
|
|
|
104
131
|
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
|
@@ -106,10 +133,16 @@ import type { FormState } from '@tmagic/form';
|
|
|
106
133
|
|
|
107
134
|
import MIcon from '@editor/components/Icon.vue';
|
|
108
135
|
import { useServices } from '@editor/hooks/use-services';
|
|
136
|
+
import type { CodeBlockStepValue, DataSourceStepValue, DiffDialogPayload, HistoryListExtraTab } from '@editor/type';
|
|
109
137
|
|
|
110
|
-
import
|
|
111
|
-
import {
|
|
112
|
-
|
|
138
|
+
import BucketTab from './BucketTab.vue';
|
|
139
|
+
import {
|
|
140
|
+
describeCodeBlockGroup,
|
|
141
|
+
describeCodeBlockStep,
|
|
142
|
+
describeDataSourceGroup,
|
|
143
|
+
describeDataSourceStep,
|
|
144
|
+
useHistoryList,
|
|
145
|
+
} from './composables';
|
|
113
146
|
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
|
114
147
|
import PageTab from './PageTab.vue';
|
|
115
148
|
|
|
@@ -119,14 +152,36 @@ defineOptions({
|
|
|
119
152
|
|
|
120
153
|
const ClockIcon = markRaw(Clock);
|
|
121
154
|
const CloseIcon = markRaw(Close);
|
|
122
|
-
const activeTab = ref<
|
|
155
|
+
const activeTab = ref<string>('page');
|
|
123
156
|
|
|
124
157
|
/** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */
|
|
125
158
|
const visible = ref(false);
|
|
126
159
|
|
|
127
160
|
const tabPaneComponent = getDesignConfig('components')?.tabPane;
|
|
128
161
|
|
|
129
|
-
|
|
162
|
+
/**
|
|
163
|
+
* 业务方自定义的扩展 tab,由 Editor 顶层通过 `historyListExtraTabs` 注入。
|
|
164
|
+
* 追加在内置「页面 / 数据源 / 代码块」三个 tab 之后,未提供时为空数组。
|
|
165
|
+
*/
|
|
166
|
+
const extraTabs = inject<HistoryListExtraTab[]>('historyListExtraTabs', []);
|
|
167
|
+
|
|
168
|
+
/** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
|
|
169
|
+
const resolveTabLabel = (tab: HistoryListExtraTab) => (typeof tab.label === 'function' ? tab.label() : tab.label);
|
|
170
|
+
|
|
171
|
+
const { editorService, dataSourceService, codeBlockService, historyService, propsService } = useServices();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用,
|
|
175
|
+
* 禁用后对应的历史记录 tab 不再展示。若当前激活的 tab 恰好被禁用,则回退到「页面」tab。
|
|
176
|
+
*/
|
|
177
|
+
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
|
|
178
|
+
const disabledCodeBlock = computed(() => propsService.getDisabledCodeBlock());
|
|
179
|
+
|
|
180
|
+
watch([disabledDataSource, disabledCodeBlock], ([dsDisabled, cbDisabled]) => {
|
|
181
|
+
if ((activeTab.value === 'data-source' && dsDisabled) || (activeTab.value === 'code-block' && cbDisabled)) {
|
|
182
|
+
activeTab.value = 'page';
|
|
183
|
+
}
|
|
184
|
+
});
|
|
130
185
|
|
|
131
186
|
/**
|
|
132
187
|
* 通过 inject 拿到 Editor 顶层注入的 `extendFormState`,转交给 HistoryDiffDialog
|
|
@@ -149,6 +204,12 @@ const {
|
|
|
149
204
|
codeBlockGroupsByTarget,
|
|
150
205
|
} = useHistoryList();
|
|
151
206
|
|
|
207
|
+
/** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
|
|
208
|
+
const isDataSourceStepDiffable = (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema);
|
|
209
|
+
|
|
210
|
+
/** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
|
|
211
|
+
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent);
|
|
212
|
+
|
|
152
213
|
/** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
|
|
153
214
|
const indexToCursor = (index: number) => index + 1;
|
|
154
215
|
|
|
@@ -180,40 +241,25 @@ const onCodeBlockGotoInitial = (id: string | number) => {
|
|
|
180
241
|
codeBlockService.goto(id, 0);
|
|
181
242
|
};
|
|
182
243
|
|
|
183
|
-
/**
|
|
184
|
-
* 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
|
|
185
|
-
* 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
|
|
186
|
-
*/
|
|
187
|
-
const onPageRevert = (index: number) => {
|
|
188
|
-
editorService.revertPageStep(index);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const onDataSourceRevert = (id: string | number, index: number) => {
|
|
192
|
-
dataSourceService.revert(id, index);
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const onCodeBlockRevert = (id: string | number, index: number) => {
|
|
196
|
-
codeBlockService.revert(id, index);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
244
|
const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
|
|
200
245
|
|
|
201
246
|
/**
|
|
202
|
-
*
|
|
247
|
+
* 构造页面 step 的差异弹窗入参:仅 update 单节点修改可对比,传入旧/新节点。
|
|
203
248
|
* 节点类型 `type` 优先取 newNode.type,再回退 oldNode.type。
|
|
204
249
|
* `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
|
|
250
|
+
* 无可对比内容(如多节点 / add / remove)时返回 null。
|
|
205
251
|
*/
|
|
206
|
-
const
|
|
252
|
+
const buildPageDiffPayload = (index: number): DiffDialogPayload | null => {
|
|
207
253
|
const groups = historyService.getPageHistoryGroups();
|
|
208
254
|
for (const group of groups) {
|
|
209
255
|
const entry = group.steps.find((s) => s.index === index);
|
|
210
256
|
if (!entry) continue;
|
|
211
257
|
const item = entry.step.updatedItems?.[0];
|
|
212
|
-
if (!item?.oldNode || !item?.newNode) return;
|
|
258
|
+
if (!item?.oldNode || !item?.newNode) return null;
|
|
213
259
|
const type = (item.newNode.type as string) || (item.oldNode.type as string) || '';
|
|
214
260
|
const nodeId = item.newNode.id ?? item.oldNode.id;
|
|
215
261
|
const currentNode = nodeId !== undefined ? editorService.getNodeById(nodeId) : null;
|
|
216
|
-
|
|
262
|
+
return {
|
|
217
263
|
category: 'node',
|
|
218
264
|
type,
|
|
219
265
|
lastValue: item.oldNode as Record<string, any>,
|
|
@@ -221,21 +267,35 @@ const onPageDiff = (index: number) => {
|
|
|
221
267
|
currentValue: (currentNode as Record<string, any>) || null,
|
|
222
268
|
targetLabel: (item.newNode.name as string) || (item.oldNode.name as string) || type,
|
|
223
269
|
id: nodeId,
|
|
224
|
-
}
|
|
225
|
-
return;
|
|
270
|
+
};
|
|
226
271
|
}
|
|
272
|
+
return null;
|
|
227
273
|
};
|
|
228
274
|
|
|
229
|
-
|
|
230
|
-
|
|
275
|
+
/**
|
|
276
|
+
* 在指定分组列表中按 id / index 查找命中的 step,命中后交由 build 构造差异弹窗入参。
|
|
277
|
+
* 用于统一数据源、代码块两类历史的查找逻辑。
|
|
278
|
+
*/
|
|
279
|
+
const findGroupStep = <G extends { id: string | number; steps: { index: number; step: any }[] }>(
|
|
280
|
+
groups: G[],
|
|
281
|
+
id: string | number,
|
|
282
|
+
index: number,
|
|
283
|
+
build: (_step: G['steps'][number]['step']) => DiffDialogPayload | null,
|
|
284
|
+
): DiffDialogPayload | null => {
|
|
231
285
|
for (const group of groups) {
|
|
232
286
|
if (group.id !== id) continue;
|
|
233
287
|
const entry = group.steps.find((s) => s.index === index);
|
|
234
288
|
if (!entry) continue;
|
|
235
|
-
|
|
236
|
-
|
|
289
|
+
return build(entry.step);
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const buildDataSourceDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
|
295
|
+
findGroupStep(historyService.getDataSourceHistoryGroups(), id, index, ({ oldSchema, newSchema }) => {
|
|
296
|
+
if (!oldSchema || !newSchema) return null;
|
|
237
297
|
const currentSchema = dataSourceService.getDataSourceById(`${id}`);
|
|
238
|
-
|
|
298
|
+
return {
|
|
239
299
|
category: 'data-source',
|
|
240
300
|
type: newSchema.type || oldSchema.type || 'base',
|
|
241
301
|
lastValue: oldSchema as Record<string, any>,
|
|
@@ -243,29 +303,78 @@ const onDataSourceDiff = (id: string | number, index: number) => {
|
|
|
243
303
|
currentValue: (currentSchema as Record<string, any>) || null,
|
|
244
304
|
targetLabel: newSchema.title || oldSchema.title || `${id}`,
|
|
245
305
|
id,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
};
|
|
306
|
+
};
|
|
307
|
+
});
|
|
250
308
|
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (group.id !== id) continue;
|
|
255
|
-
const entry = group.steps.find((s) => s.index === index);
|
|
256
|
-
if (!entry) continue;
|
|
257
|
-
const { oldContent, newContent } = entry.step;
|
|
258
|
-
if (!oldContent || !newContent) return;
|
|
309
|
+
const buildCodeBlockDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
|
310
|
+
findGroupStep(historyService.getCodeBlockHistoryGroups(), id, index, ({ oldContent, newContent }) => {
|
|
311
|
+
if (!oldContent || !newContent) return null;
|
|
259
312
|
const currentContent = codeBlockService.getCodeContentById(id);
|
|
260
|
-
|
|
313
|
+
return {
|
|
261
314
|
category: 'code-block',
|
|
262
315
|
lastValue: oldContent as Record<string, any>,
|
|
263
316
|
value: newContent as Record<string, any>,
|
|
264
317
|
currentValue: (currentContent as Record<string, any>) || null,
|
|
265
318
|
targetLabel: newContent.name || oldContent.name || `${id}`,
|
|
266
319
|
id,
|
|
267
|
-
}
|
|
268
|
-
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const onPageDiff = (index: number) => {
|
|
324
|
+
const payload = buildPageDiffPayload(index);
|
|
325
|
+
if (payload) diffDialogRef.value?.open(payload);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const onDataSourceDiff = (id: string | number, index: number) => {
|
|
329
|
+
const payload = buildDataSourceDiffPayload(id, index);
|
|
330
|
+
if (payload) diffDialogRef.value?.open(payload);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const onCodeBlockDiff = (id: string | number, index: number) => {
|
|
334
|
+
const payload = buildCodeBlockDiffPayload(id, index);
|
|
335
|
+
if (payload) diffDialogRef.value?.open(payload);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const onConfirmRevert = shallowRef();
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
|
|
342
|
+
* 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
|
|
343
|
+
*
|
|
344
|
+
* 交互:先弹出该步骤的差异弹窗供用户确认,点击「确定回滚」后再真正执行回滚;
|
|
345
|
+
* 对没有可对比内容的步骤(如 add / remove / 多节点更新)则直接回滚。
|
|
346
|
+
*/
|
|
347
|
+
const onPageRevert = (index: number) => {
|
|
348
|
+
const payload = buildPageDiffPayload(index);
|
|
349
|
+
onConfirmRevert.value = () => editorService.revertPageStep(index);
|
|
350
|
+
if (payload) {
|
|
351
|
+
diffDialogRef.value?.open({ ...payload });
|
|
352
|
+
} else {
|
|
353
|
+
onConfirmRevert.value();
|
|
269
354
|
}
|
|
270
355
|
};
|
|
356
|
+
|
|
357
|
+
const onDataSourceRevert = (id: string | number, index: number) => {
|
|
358
|
+
const payload = buildDataSourceDiffPayload(id, index);
|
|
359
|
+
onConfirmRevert.value = () => dataSourceService.revert(id, index);
|
|
360
|
+
if (payload) {
|
|
361
|
+
diffDialogRef.value?.open({ ...payload });
|
|
362
|
+
} else {
|
|
363
|
+
onConfirmRevert.value();
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const onCodeBlockRevert = (id: string | number, index: number) => {
|
|
368
|
+
const payload = buildCodeBlockDiffPayload(id, index);
|
|
369
|
+
onConfirmRevert.value = () => codeBlockService.revert(id, index);
|
|
370
|
+
if (payload) {
|
|
371
|
+
diffDialogRef.value?.open({ ...payload });
|
|
372
|
+
} else {
|
|
373
|
+
onConfirmRevert.value();
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const onDiffDialogClose = () => {
|
|
378
|
+
onConfirmRevert.value = undefined;
|
|
379
|
+
};
|
|
271
380
|
</script>
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
class="m-editor-history-list-item m-editor-history-list-initial"
|
|
4
4
|
:class="{ 'is-current': isCurrent, 'is-clickable': !isCurrent }"
|
|
5
5
|
:title="isCurrent ? '当前已回到未修改的初始状态' : '点击回到未修改的初始状态'"
|
|
6
|
-
@click="onClick"
|
|
7
6
|
>
|
|
8
7
|
<span class="m-editor-history-list-item-index" title="历史步骤编号 #0(未修改的初始状态)">#0</span>
|
|
9
8
|
<span class="m-editor-history-list-item-op op-initial">初始</span>
|
|
10
9
|
<span class="m-editor-history-list-item-desc">未修改的初始状态</span>
|
|
11
|
-
<span
|
|
10
|
+
<span
|
|
11
|
+
v-if="gotoEnabled && !isCurrent"
|
|
12
|
+
class="m-editor-history-list-item-goto"
|
|
13
|
+
title="回到该记录"
|
|
14
|
+
@click.stop="onClick"
|
|
15
|
+
>回到</span
|
|
16
|
+
>
|
|
12
17
|
</li>
|
|
13
18
|
</template>
|
|
14
19
|
|
|
@@ -24,10 +29,16 @@ defineOptions({
|
|
|
24
29
|
name: 'MEditorHistoryListInitialRow',
|
|
25
30
|
});
|
|
26
31
|
|
|
27
|
-
const props =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const props = withDefaults(
|
|
33
|
+
defineProps<{
|
|
34
|
+
/** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */
|
|
35
|
+
isCurrent: boolean;
|
|
36
|
+
gotoEnabled?: boolean;
|
|
37
|
+
}>(),
|
|
38
|
+
{
|
|
39
|
+
gotoEnabled: true,
|
|
40
|
+
},
|
|
41
|
+
);
|
|
31
42
|
|
|
32
43
|
const emit = defineEmits<{
|
|
33
44
|
/** 点击非当前的初始项时触发,由上层调用对应 service 的 goto 把 cursor 移到 0。 */
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
<TMagicScrollbar v-else max-height="360px">
|
|
4
4
|
<ul class="m-editor-history-list-ul">
|
|
5
5
|
<GroupRow
|
|
6
|
-
v-for="
|
|
7
|
-
:key="`pg-${
|
|
8
|
-
:group-key="`pg-${
|
|
6
|
+
v-for="group in list"
|
|
7
|
+
:key="`pg-${group.steps[0]?.index}`"
|
|
8
|
+
:group-key="`pg-${group.steps[0]?.index}`"
|
|
9
9
|
:applied="group.applied"
|
|
10
10
|
:merged="group.steps.length > 1"
|
|
11
11
|
:op-type="group.opType"
|
|
12
12
|
:desc="describePageGroup(group)"
|
|
13
|
+
:time="formatHistoryTime(groupTimestamp(group))"
|
|
14
|
+
:time-title="formatHistoryFullTime(groupTimestamp(group))"
|
|
13
15
|
:step-count="group.steps.length"
|
|
14
16
|
:sub-steps="
|
|
15
17
|
group.steps.map((s) => ({
|
|
@@ -19,10 +21,12 @@
|
|
|
19
21
|
desc: describePageStep(s.step),
|
|
20
22
|
diffable: isPageStepDiffable(s.step),
|
|
21
23
|
revertable: s.applied,
|
|
24
|
+
time: formatHistoryTime(s.step.timestamp),
|
|
25
|
+
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
|
22
26
|
}))
|
|
23
27
|
"
|
|
24
28
|
:is-current="group.isCurrent"
|
|
25
|
-
:expanded="!!expanded[`pg-${
|
|
29
|
+
:expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
|
|
26
30
|
@toggle="(key: string) => $emit('toggle', key)"
|
|
27
31
|
@goto="(index: number) => $emit('goto', index)"
|
|
28
32
|
@diff-step="(index: number) => $emit('diff-step', index)"
|
|
@@ -44,7 +48,13 @@ import { TMagicScrollbar } from '@tmagic/design';
|
|
|
44
48
|
|
|
45
49
|
import type { PageHistoryGroup, StepValue } from '@editor/type';
|
|
46
50
|
|
|
47
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
describePageGroup,
|
|
53
|
+
describePageStep,
|
|
54
|
+
formatHistoryFullTime,
|
|
55
|
+
formatHistoryTime,
|
|
56
|
+
groupTimestamp,
|
|
57
|
+
} from './composables';
|
|
48
58
|
import GroupRow from './GroupRow.vue';
|
|
49
59
|
import InitialRow from './InitialRow.vue';
|
|
50
60
|
|
|
@@ -55,7 +65,11 @@ defineOptions({
|
|
|
55
65
|
const props = defineProps<{
|
|
56
66
|
/** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
|
|
57
67
|
list: PageHistoryGroup[];
|
|
58
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
|
|
70
|
+
* 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
|
71
|
+
* 这样历史数据更新(新增 / 撤销重做导致列表顺序变化)后,已展开的分组状态仍能正确保持。
|
|
72
|
+
*/
|
|
59
73
|
expanded: Record<string, boolean>;
|
|
60
74
|
}>();
|
|
61
75
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { computed, reactive } from 'vue';
|
|
2
2
|
|
|
3
|
+
import { datetimeFormatter } from '@tmagic/form';
|
|
4
|
+
|
|
3
5
|
import { useServices } from '@editor/hooks/use-services';
|
|
4
6
|
import type {
|
|
5
7
|
CodeBlockHistoryGroup,
|
|
@@ -22,7 +24,10 @@ import type {
|
|
|
22
24
|
export const useHistoryList = () => {
|
|
23
25
|
const { historyService } = useServices();
|
|
24
26
|
|
|
25
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。
|
|
29
|
+
* 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。
|
|
30
|
+
*/
|
|
26
31
|
const expanded = reactive<Record<string, boolean>>({});
|
|
27
32
|
const toggleGroup = (key: string) => {
|
|
28
33
|
expanded[key] = !expanded[key];
|
|
@@ -64,6 +69,32 @@ export const useHistoryList = () => {
|
|
|
64
69
|
};
|
|
65
70
|
};
|
|
66
71
|
|
|
72
|
+
/**
|
|
73
|
+
* 历史面板的时间展示:
|
|
74
|
+
* - 当天的记录只显示 `HH:mm:ss`;
|
|
75
|
+
* - 跨天的记录显示 `MM-DD HH:mm:ss`。
|
|
76
|
+
* 无时间戳(旧数据 / 未写入)时返回空串,UI 据此不渲染时间。
|
|
77
|
+
*/
|
|
78
|
+
export const formatHistoryTime = (timestamp?: number): string => {
|
|
79
|
+
if (!timestamp) return '';
|
|
80
|
+
const isToday =
|
|
81
|
+
datetimeFormatter(new Date(timestamp), '', 'YYYY-MM-DD') ===
|
|
82
|
+
(datetimeFormatter(new Date(), '', 'YYYY-MM-DD') as string);
|
|
83
|
+
return `${
|
|
84
|
+
isToday
|
|
85
|
+
? datetimeFormatter(new Date(timestamp), '', 'HH:mm:ss')
|
|
86
|
+
: datetimeFormatter(new Date(timestamp), '', 'MM-DD HH:mm:ss')
|
|
87
|
+
}`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** 完整时间(含年份与秒),用于 title 悬浮提示。无时间戳时返回空串。 */
|
|
91
|
+
export const formatHistoryFullTime = (timestamp?: number): string =>
|
|
92
|
+
timestamp ? `${datetimeFormatter(new Date(timestamp), '', 'YYYY-MM-DD HH:mm:ss')}` : '';
|
|
93
|
+
|
|
94
|
+
/** 取一组历史步骤里最后一步(最近一次)的时间戳,用于组头部展示。 */
|
|
95
|
+
export const groupTimestamp = (group: { steps: { step: { timestamp?: number } }[] }): number | undefined =>
|
|
96
|
+
group.steps[group.steps.length - 1]?.step.timestamp;
|
|
97
|
+
|
|
67
98
|
export const opLabel = (op: HistoryOpType) => {
|
|
68
99
|
switch (op) {
|
|
69
100
|
case 'add':
|
package/src/services/history.ts
CHANGED
|
@@ -254,6 +254,7 @@ class History extends BaseService {
|
|
|
254
254
|
public push(state: StepValue, pageId?: Id): StepValue | null {
|
|
255
255
|
const undoRedo = this.getUndoRedo(pageId);
|
|
256
256
|
if (!undoRedo) return null;
|
|
257
|
+
if (state.timestamp === undefined) state.timestamp = Date.now();
|
|
257
258
|
undoRedo.pushElement(state);
|
|
258
259
|
// 仅当推入的是当前活动页时才需要刷新 canUndo/canRedo —— 其它页栈对当前 UI 状态没影响。
|
|
259
260
|
if (pageId === undefined || `${pageId}` === `${this.state.pageId}`) {
|
|
@@ -289,6 +290,7 @@ class History extends BaseService {
|
|
|
289
290
|
newContent: payload.newContent ? cloneDeep(payload.newContent) : null,
|
|
290
291
|
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
|
|
291
292
|
historyDescription: payload.historyDescription,
|
|
293
|
+
timestamp: Date.now(),
|
|
292
294
|
};
|
|
293
295
|
|
|
294
296
|
this.getCodeBlockUndoRedo(codeBlockId).pushElement(step);
|
|
@@ -318,6 +320,7 @@ class History extends BaseService {
|
|
|
318
320
|
newSchema: payload.newSchema ? cloneDeep(payload.newSchema) : null,
|
|
319
321
|
changeRecords: payload.changeRecords?.length ? cloneDeep(payload.changeRecords) : undefined,
|
|
320
322
|
historyDescription: payload.historyDescription,
|
|
323
|
+
timestamp: Date.now(),
|
|
321
324
|
};
|
|
322
325
|
|
|
323
326
|
this.getDataSourceUndoRedo(dataSourceId).pushElement(step);
|
|
@@ -125,19 +125,19 @@
|
|
|
125
125
|
&.is-merged {
|
|
126
126
|
margin: 4px 0;
|
|
127
127
|
padding: 4px 8px 6px;
|
|
128
|
-
background-color: rgba(
|
|
129
|
-
border: 1px solid rgba(
|
|
130
|
-
border-left: 3px solid #
|
|
128
|
+
background-color: rgba(47, 84, 235, 0.06);
|
|
129
|
+
border: 1px solid rgba(47, 84, 235, 0.18);
|
|
130
|
+
border-left: 3px solid #2f54eb;
|
|
131
131
|
border-radius: 4px;
|
|
132
132
|
|
|
133
133
|
// 卡片本体已经有背景色,hover 状态以更深的同色提示交互
|
|
134
134
|
&:hover {
|
|
135
|
-
background-color: rgba(
|
|
135
|
+
background-color: rgba(47, 84, 235, 0.1);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
.m-editor-history-list-group-head {
|
|
139
139
|
font-weight: 600;
|
|
140
|
-
color: #
|
|
140
|
+
color: #1d39c4;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// 已撤销态:整张卡片去色
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
margin: 6px 0 0 6px;
|
|
170
170
|
padding: 0;
|
|
171
171
|
list-style: none;
|
|
172
|
-
border-left: 1px dashed rgba(
|
|
172
|
+
border-left: 1px dashed rgba(47, 84, 235, 0.45);
|
|
173
173
|
|
|
174
174
|
li {
|
|
175
175
|
display: flex;
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
cursor: pointer;
|
|
186
186
|
|
|
187
187
|
&:hover {
|
|
188
|
-
background-color: rgba(
|
|
188
|
+
background-color: rgba(47, 84, 235, 0.1);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -222,6 +222,16 @@
|
|
|
222
222
|
white-space: nowrap;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// 操作时间:弱化展示,紧贴在描述之后、各操作按钮之前。
|
|
226
|
+
.m-editor-history-list-item-time {
|
|
227
|
+
flex: 0 0 auto;
|
|
228
|
+
color: #a8abb2;
|
|
229
|
+
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
230
|
+
font-size: 11px;
|
|
231
|
+
font-weight: 400; // 防止被合并组头部的粗体继承
|
|
232
|
+
white-space: nowrap;
|
|
233
|
+
}
|
|
234
|
+
|
|
225
235
|
.m-editor-history-list-item-op {
|
|
226
236
|
flex: 0 0 auto;
|
|
227
237
|
padding: 0 6px;
|
|
@@ -240,7 +250,7 @@
|
|
|
240
250
|
}
|
|
241
251
|
|
|
242
252
|
&.op-update {
|
|
243
|
-
background-color: #
|
|
253
|
+
background-color: #e6a23c;
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
&.op-initial {
|
|
@@ -279,7 +289,7 @@
|
|
|
279
289
|
font-size: 10px;
|
|
280
290
|
line-height: 16px;
|
|
281
291
|
color: #fff;
|
|
282
|
-
background-color: #
|
|
292
|
+
background-color: #2f54eb;
|
|
283
293
|
font-weight: 500;
|
|
284
294
|
letter-spacing: 0.2px;
|
|
285
295
|
}
|
|
@@ -300,21 +310,39 @@
|
|
|
300
310
|
}
|
|
301
311
|
}
|
|
302
312
|
|
|
313
|
+
// 「跳转」按钮:将历史游标移动到该 step,替代原先点击整行跳转的交互。
|
|
314
|
+
// 使用与组卡片一致的紫色色系,与「查看差异」「回滚」区分开。
|
|
315
|
+
.m-editor-history-list-item-goto {
|
|
316
|
+
flex: 0 0 auto;
|
|
317
|
+
padding: 0 6px;
|
|
318
|
+
border-radius: 2px;
|
|
319
|
+
font-size: 10px;
|
|
320
|
+
line-height: 16px;
|
|
321
|
+
color: #606266;
|
|
322
|
+
background-color: rgba(96, 98, 102, 0.1);
|
|
323
|
+
cursor: pointer;
|
|
324
|
+
user-select: none;
|
|
325
|
+
|
|
326
|
+
&:hover {
|
|
327
|
+
background-color: rgba(96, 98, 102, 0.18);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
303
331
|
// 「回滚」按钮:类 git revert,把目标 step 反向应用一次作为新提交。
|
|
304
|
-
//
|
|
332
|
+
// 使用红色色调,强调其为"破坏性/可逆操作",与「查看差异」「跳转」区分开。
|
|
305
333
|
.m-editor-history-list-item-revert {
|
|
306
334
|
flex: 0 0 auto;
|
|
307
335
|
padding: 0 6px;
|
|
308
336
|
border-radius: 2px;
|
|
309
337
|
font-size: 10px;
|
|
310
338
|
line-height: 16px;
|
|
311
|
-
color: #
|
|
312
|
-
background-color: rgba(
|
|
339
|
+
color: #f56c6c;
|
|
340
|
+
background-color: rgba(245, 108, 108, 0.12);
|
|
313
341
|
cursor: pointer;
|
|
314
342
|
user-select: none;
|
|
315
343
|
|
|
316
344
|
&:hover {
|
|
317
|
-
background-color: rgba(
|
|
345
|
+
background-color: rgba(245, 108, 108, 0.25);
|
|
318
346
|
}
|
|
319
347
|
}
|
|
320
348
|
|
|
@@ -369,6 +397,17 @@
|
|
|
369
397
|
flex-direction: column;
|
|
370
398
|
}
|
|
371
399
|
|
|
400
|
+
.m-editor-history-diff-dialog-notice {
|
|
401
|
+
margin-bottom: 8px;
|
|
402
|
+
padding: 8px 12px;
|
|
403
|
+
background-color: #fdf6ec;
|
|
404
|
+
border: 1px solid #faecd8;
|
|
405
|
+
border-radius: 4px;
|
|
406
|
+
color: #e6a23c;
|
|
407
|
+
font-size: 13px;
|
|
408
|
+
line-height: 1.5;
|
|
409
|
+
}
|
|
410
|
+
|
|
372
411
|
.m-editor-history-diff-dialog-header {
|
|
373
412
|
display: flex;
|
|
374
413
|
align-items: center;
|