@tmagic/editor 1.8.0-beta.2 → 1.8.0-beta.4
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 +46 -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-code-block-edit.js +6 -3
- package/dist/es/hooks/use-data-source-edit.js +5 -2
- package/dist/es/hooks/use-stage.js +8 -4
- 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/NavMenu.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 +37 -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} +30 -16
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +91 -60
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +82 -30
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +140 -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 +15 -6
- package/dist/es/layouts/history-list/composables.js +72 -2
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
- package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +5 -5
- package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/es/services/codeBlock.js +25 -10
- package/dist/es/services/dataSource.js +24 -10
- package/dist/es/services/editor.js +98 -46
- package/dist/es/services/history.js +7 -2
- package/dist/es/services/keybinding.js +3 -3
- package/dist/es/style.css +60 -16
- package/dist/es/utils/content-menu.js +17 -9
- package/dist/style.css +60 -16
- package/dist/tmagic-editor.umd.cjs +795 -443
- package/package.json +7 -7
- package/src/Editor.vue +8 -0
- package/src/components/CompareForm.vue +50 -19
- package/src/editorProps.ts +7 -0
- package/src/fields/CodeLink.vue +2 -5
- package/src/hooks/use-code-block-edit.ts +4 -3
- package/src/hooks/use-data-source-edit.ts +2 -2
- package/src/hooks/use-stage.ts +4 -3
- package/src/index.ts +2 -0
- package/src/layouts/CodeEditor.vue +2 -5
- package/src/layouts/NavMenu.vue +1 -1
- package/src/layouts/history-list/Bucket.vue +58 -29
- package/src/layouts/history-list/BucketTab.vue +80 -0
- package/src/layouts/history-list/GroupRow.vue +110 -58
- package/src/layouts/history-list/HistoryDiffDialog.vue +53 -33
- package/src/layouts/history-list/HistoryListPanel.vue +165 -52
- package/src/layouts/history-list/InitialRow.vue +17 -6
- package/src/layouts/history-list/PageTab.vue +25 -7
- package/src/layouts/history-list/composables.ts +92 -1
- package/src/layouts/props-panel/PropsPanel.vue +5 -1
- package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
- package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
- package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
- package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
- package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +1 -1
- package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
- package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
- package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
- package/src/layouts/workspace/viewer/Stage.vue +1 -1
- package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
- package/src/services/codeBlock.ts +33 -9
- package/src/services/dataSource.ts +30 -8
- package/src/services/editor.ts +111 -34
- package/src/services/history.ts +10 -0
- package/src/services/keybinding.ts +3 -3
- package/src/theme/history-list-panel.scss +67 -14
- package/src/theme/props-panel.scss +3 -3
- package/src/type.ts +146 -0
- package/src/utils/content-menu.ts +18 -9
- package/types/index.d.ts +387 -156
- 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,19 @@
|
|
|
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"
|
|
47
|
+
:is-step-revertable="isDataSourceStepRevertable"
|
|
41
48
|
@toggle="toggleGroup"
|
|
42
49
|
@goto="onDataSourceGoto"
|
|
43
50
|
@goto-initial="onDataSourceGotoInitial"
|
|
@@ -47,12 +54,19 @@
|
|
|
47
54
|
</component>
|
|
48
55
|
|
|
49
56
|
<component
|
|
57
|
+
v-if="!disabledCodeBlock"
|
|
50
58
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
|
51
59
|
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
|
|
52
60
|
>
|
|
53
|
-
<
|
|
61
|
+
<BucketTab
|
|
62
|
+
title="代码块"
|
|
63
|
+
prefix="cb"
|
|
54
64
|
:buckets="codeBlockGroupsByTarget"
|
|
55
65
|
:expanded="expanded"
|
|
66
|
+
:describe-group="describeCodeBlockGroup"
|
|
67
|
+
:describe-step="describeCodeBlockStep"
|
|
68
|
+
:is-step-diffable="isCodeBlockStepDiffable"
|
|
69
|
+
:is-step-revertable="isCodeBlockStepRevertable"
|
|
56
70
|
@toggle="toggleGroup"
|
|
57
71
|
@goto="onCodeBlockGoto"
|
|
58
72
|
@goto-initial="onCodeBlockGotoInitial"
|
|
@@ -60,6 +74,15 @@
|
|
|
60
74
|
@revert-step="onCodeBlockRevert"
|
|
61
75
|
/>
|
|
62
76
|
</component>
|
|
77
|
+
|
|
78
|
+
<component
|
|
79
|
+
v-for="tab in extraTabs"
|
|
80
|
+
:key="tab.name"
|
|
81
|
+
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
|
82
|
+
v-bind="tabPaneComponent?.props({ name: tab.name, label: resolveTabLabel(tab) }) || {}"
|
|
83
|
+
>
|
|
84
|
+
<component :is="tab.component" v-bind="tab.props || {}" v-on="tab.listeners || {}" />
|
|
85
|
+
</component>
|
|
63
86
|
</TMagicTabs>
|
|
64
87
|
</div>
|
|
65
88
|
|
|
@@ -74,7 +97,12 @@
|
|
|
74
97
|
</template>
|
|
75
98
|
</TMagicPopover>
|
|
76
99
|
|
|
77
|
-
<HistoryDiffDialog
|
|
100
|
+
<HistoryDiffDialog
|
|
101
|
+
ref="diffDialog"
|
|
102
|
+
:extend-state="extendFormState"
|
|
103
|
+
:on-confirm="onConfirmRevert"
|
|
104
|
+
@close="onDiffDialogClose"
|
|
105
|
+
/>
|
|
78
106
|
</template>
|
|
79
107
|
|
|
80
108
|
<script lang="ts" setup>
|
|
@@ -95,10 +123,11 @@
|
|
|
95
123
|
* 此外每条 step 上提供"查看差异"入口(仅在前后值都存在的 update 步骤显示),
|
|
96
124
|
* 点击后弹出 HistoryDiffDialog,使用 CompareForm 组件以表单形式展示新旧值差异。
|
|
97
125
|
*
|
|
98
|
-
* 各 tab 的内容拆分为独立的 SFC
|
|
126
|
+
* 各 tab 的内容拆分为独立的 SFC:页面用 PageTab,数据源 / 代码块复用通用的 BucketTab
|
|
127
|
+
* (通过 title / prefix / describe* / isStepDiffable 注入差异)。
|
|
99
128
|
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
|
100
129
|
*/
|
|
101
|
-
import { inject, markRaw, ref, useTemplateRef } from 'vue';
|
|
130
|
+
import { computed, inject, markRaw, ref, shallowRef, useTemplateRef, watch } from 'vue';
|
|
102
131
|
import { Clock, Close } from '@element-plus/icons-vue';
|
|
103
132
|
|
|
104
133
|
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
|
@@ -106,10 +135,18 @@ import type { FormState } from '@tmagic/form';
|
|
|
106
135
|
|
|
107
136
|
import MIcon from '@editor/components/Icon.vue';
|
|
108
137
|
import { useServices } from '@editor/hooks/use-services';
|
|
138
|
+
import type { CodeBlockStepValue, DataSourceStepValue, DiffDialogPayload, HistoryListExtraTab } from '@editor/type';
|
|
109
139
|
|
|
110
|
-
import
|
|
111
|
-
import {
|
|
112
|
-
|
|
140
|
+
import BucketTab from './BucketTab.vue';
|
|
141
|
+
import {
|
|
142
|
+
describeCodeBlockGroup,
|
|
143
|
+
describeCodeBlockStep,
|
|
144
|
+
describeDataSourceGroup,
|
|
145
|
+
describeDataSourceStep,
|
|
146
|
+
isCodeBlockStepRevertable,
|
|
147
|
+
isDataSourceStepRevertable,
|
|
148
|
+
useHistoryList,
|
|
149
|
+
} from './composables';
|
|
113
150
|
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
|
114
151
|
import PageTab from './PageTab.vue';
|
|
115
152
|
|
|
@@ -119,14 +156,36 @@ defineOptions({
|
|
|
119
156
|
|
|
120
157
|
const ClockIcon = markRaw(Clock);
|
|
121
158
|
const CloseIcon = markRaw(Close);
|
|
122
|
-
const activeTab = ref<
|
|
159
|
+
const activeTab = ref<string>('page');
|
|
123
160
|
|
|
124
161
|
/** 面板显隐受控:reference 图标点击切换,右上角关闭按钮置为 false。 */
|
|
125
162
|
const visible = ref(false);
|
|
126
163
|
|
|
127
164
|
const tabPaneComponent = getDesignConfig('components')?.tabPane;
|
|
128
165
|
|
|
129
|
-
|
|
166
|
+
/**
|
|
167
|
+
* 业务方自定义的扩展 tab,由 Editor 顶层通过 `historyListExtraTabs` 注入。
|
|
168
|
+
* 追加在内置「页面 / 数据源 / 代码块」三个 tab 之后,未提供时为空数组。
|
|
169
|
+
*/
|
|
170
|
+
const extraTabs = inject<HistoryListExtraTab[]>('historyListExtraTabs', []);
|
|
171
|
+
|
|
172
|
+
/** label 支持字符串或函数,函数形式便于展示动态数量等内容。 */
|
|
173
|
+
const resolveTabLabel = (tab: HistoryListExtraTab) => (typeof tab.label === 'function' ? tab.label() : tab.label);
|
|
174
|
+
|
|
175
|
+
const { editorService, dataSourceService, codeBlockService, historyService, propsService } = useServices();
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 数据源 / 代码块功能可被业务方通过 `disabledDataSource` / `disabledCodeBlock` 禁用,
|
|
179
|
+
* 禁用后对应的历史记录 tab 不再展示。若当前激活的 tab 恰好被禁用,则回退到「页面」tab。
|
|
180
|
+
*/
|
|
181
|
+
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
|
|
182
|
+
const disabledCodeBlock = computed(() => propsService.getDisabledCodeBlock());
|
|
183
|
+
|
|
184
|
+
watch([disabledDataSource, disabledCodeBlock], ([dsDisabled, cbDisabled]) => {
|
|
185
|
+
if ((activeTab.value === 'data-source' && dsDisabled) || (activeTab.value === 'code-block' && cbDisabled)) {
|
|
186
|
+
activeTab.value = 'page';
|
|
187
|
+
}
|
|
188
|
+
});
|
|
130
189
|
|
|
131
190
|
/**
|
|
132
191
|
* 通过 inject 拿到 Editor 顶层注入的 `extendFormState`,转交给 HistoryDiffDialog
|
|
@@ -149,6 +208,12 @@ const {
|
|
|
149
208
|
codeBlockGroupsByTarget,
|
|
150
209
|
} = useHistoryList();
|
|
151
210
|
|
|
211
|
+
/** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
|
|
212
|
+
const isDataSourceStepDiffable = (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema);
|
|
213
|
+
|
|
214
|
+
/** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
|
|
215
|
+
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent);
|
|
216
|
+
|
|
152
217
|
/** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
|
|
153
218
|
const indexToCursor = (index: number) => index + 1;
|
|
154
219
|
|
|
@@ -180,40 +245,25 @@ const onCodeBlockGotoInitial = (id: string | number) => {
|
|
|
180
245
|
codeBlockService.goto(id, 0);
|
|
181
246
|
};
|
|
182
247
|
|
|
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
248
|
const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
|
|
200
249
|
|
|
201
250
|
/**
|
|
202
|
-
*
|
|
251
|
+
* 构造页面 step 的差异弹窗入参:仅 update 单节点修改可对比,传入旧/新节点。
|
|
203
252
|
* 节点类型 `type` 优先取 newNode.type,再回退 oldNode.type。
|
|
204
253
|
* `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
|
|
254
|
+
* 无可对比内容(如多节点 / add / remove)时返回 null。
|
|
205
255
|
*/
|
|
206
|
-
const
|
|
256
|
+
const buildPageDiffPayload = (index: number): DiffDialogPayload | null => {
|
|
207
257
|
const groups = historyService.getPageHistoryGroups();
|
|
208
258
|
for (const group of groups) {
|
|
209
259
|
const entry = group.steps.find((s) => s.index === index);
|
|
210
260
|
if (!entry) continue;
|
|
211
261
|
const item = entry.step.updatedItems?.[0];
|
|
212
|
-
if (!item?.oldNode || !item?.newNode) return;
|
|
262
|
+
if (!item?.oldNode || !item?.newNode) return null;
|
|
213
263
|
const type = (item.newNode.type as string) || (item.oldNode.type as string) || '';
|
|
214
264
|
const nodeId = item.newNode.id ?? item.oldNode.id;
|
|
215
265
|
const currentNode = nodeId !== undefined ? editorService.getNodeById(nodeId) : null;
|
|
216
|
-
|
|
266
|
+
return {
|
|
217
267
|
category: 'node',
|
|
218
268
|
type,
|
|
219
269
|
lastValue: item.oldNode as Record<string, any>,
|
|
@@ -221,21 +271,35 @@ const onPageDiff = (index: number) => {
|
|
|
221
271
|
currentValue: (currentNode as Record<string, any>) || null,
|
|
222
272
|
targetLabel: (item.newNode.name as string) || (item.oldNode.name as string) || type,
|
|
223
273
|
id: nodeId,
|
|
224
|
-
}
|
|
225
|
-
return;
|
|
274
|
+
};
|
|
226
275
|
}
|
|
276
|
+
return null;
|
|
227
277
|
};
|
|
228
278
|
|
|
229
|
-
|
|
230
|
-
|
|
279
|
+
/**
|
|
280
|
+
* 在指定分组列表中按 id / index 查找命中的 step,命中后交由 build 构造差异弹窗入参。
|
|
281
|
+
* 用于统一数据源、代码块两类历史的查找逻辑。
|
|
282
|
+
*/
|
|
283
|
+
const findGroupStep = <G extends { id: string | number; steps: { index: number; step: any }[] }>(
|
|
284
|
+
groups: G[],
|
|
285
|
+
id: string | number,
|
|
286
|
+
index: number,
|
|
287
|
+
build: (_step: G['steps'][number]['step']) => DiffDialogPayload | null,
|
|
288
|
+
): DiffDialogPayload | null => {
|
|
231
289
|
for (const group of groups) {
|
|
232
290
|
if (group.id !== id) continue;
|
|
233
291
|
const entry = group.steps.find((s) => s.index === index);
|
|
234
292
|
if (!entry) continue;
|
|
235
|
-
|
|
236
|
-
|
|
293
|
+
return build(entry.step);
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const buildDataSourceDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
|
299
|
+
findGroupStep(historyService.getDataSourceHistoryGroups(), id, index, ({ oldSchema, newSchema }) => {
|
|
300
|
+
if (!oldSchema || !newSchema) return null;
|
|
237
301
|
const currentSchema = dataSourceService.getDataSourceById(`${id}`);
|
|
238
|
-
|
|
302
|
+
return {
|
|
239
303
|
category: 'data-source',
|
|
240
304
|
type: newSchema.type || oldSchema.type || 'base',
|
|
241
305
|
lastValue: oldSchema as Record<string, any>,
|
|
@@ -243,29 +307,78 @@ const onDataSourceDiff = (id: string | number, index: number) => {
|
|
|
243
307
|
currentValue: (currentSchema as Record<string, any>) || null,
|
|
244
308
|
targetLabel: newSchema.title || oldSchema.title || `${id}`,
|
|
245
309
|
id,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
};
|
|
310
|
+
};
|
|
311
|
+
});
|
|
250
312
|
|
|
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;
|
|
313
|
+
const buildCodeBlockDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
|
314
|
+
findGroupStep(historyService.getCodeBlockHistoryGroups(), id, index, ({ oldContent, newContent }) => {
|
|
315
|
+
if (!oldContent || !newContent) return null;
|
|
259
316
|
const currentContent = codeBlockService.getCodeContentById(id);
|
|
260
|
-
|
|
317
|
+
return {
|
|
261
318
|
category: 'code-block',
|
|
262
319
|
lastValue: oldContent as Record<string, any>,
|
|
263
320
|
value: newContent as Record<string, any>,
|
|
264
321
|
currentValue: (currentContent as Record<string, any>) || null,
|
|
265
322
|
targetLabel: newContent.name || oldContent.name || `${id}`,
|
|
266
323
|
id,
|
|
267
|
-
}
|
|
268
|
-
|
|
324
|
+
};
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const onPageDiff = (index: number) => {
|
|
328
|
+
const payload = buildPageDiffPayload(index);
|
|
329
|
+
if (payload) diffDialogRef.value?.open(payload);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const onDataSourceDiff = (id: string | number, index: number) => {
|
|
333
|
+
const payload = buildDataSourceDiffPayload(id, index);
|
|
334
|
+
if (payload) diffDialogRef.value?.open(payload);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const onCodeBlockDiff = (id: string | number, index: number) => {
|
|
338
|
+
const payload = buildCodeBlockDiffPayload(id, index);
|
|
339
|
+
if (payload) diffDialogRef.value?.open(payload);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const onConfirmRevert = shallowRef();
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
|
|
346
|
+
* 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
|
|
347
|
+
*
|
|
348
|
+
* 交互:先弹出该步骤的差异弹窗供用户确认,点击「确定回滚」后再真正执行回滚;
|
|
349
|
+
* 对没有可对比内容的步骤(如 add / remove / 多节点更新)则直接回滚。
|
|
350
|
+
*/
|
|
351
|
+
const onPageRevert = (index: number) => {
|
|
352
|
+
const payload = buildPageDiffPayload(index);
|
|
353
|
+
onConfirmRevert.value = () => editorService.revertPageStep(index);
|
|
354
|
+
if (payload) {
|
|
355
|
+
diffDialogRef.value?.open({ ...payload });
|
|
356
|
+
} else {
|
|
357
|
+
onConfirmRevert.value();
|
|
269
358
|
}
|
|
270
359
|
};
|
|
360
|
+
|
|
361
|
+
const onDataSourceRevert = (id: string | number, index: number) => {
|
|
362
|
+
const payload = buildDataSourceDiffPayload(id, index);
|
|
363
|
+
onConfirmRevert.value = () => dataSourceService.revert(id, index);
|
|
364
|
+
if (payload) {
|
|
365
|
+
diffDialogRef.value?.open({ ...payload });
|
|
366
|
+
} else {
|
|
367
|
+
onConfirmRevert.value();
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const onCodeBlockRevert = (id: string | number, index: number) => {
|
|
372
|
+
const payload = buildCodeBlockDiffPayload(id, index);
|
|
373
|
+
onConfirmRevert.value = () => codeBlockService.revert(id, index);
|
|
374
|
+
if (payload) {
|
|
375
|
+
diffDialogRef.value?.open({ ...payload });
|
|
376
|
+
} else {
|
|
377
|
+
onConfirmRevert.value();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const onDiffDialogClose = () => {
|
|
382
|
+
onConfirmRevert.value = undefined;
|
|
383
|
+
};
|
|
271
384
|
</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,16 @@
|
|
|
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
|
+
:source="groupSource(group)"
|
|
14
|
+
:time="formatHistoryTime(groupTimestamp(group))"
|
|
15
|
+
:time-title="formatHistoryFullTime(groupTimestamp(group))"
|
|
13
16
|
:step-count="group.steps.length"
|
|
14
17
|
:sub-steps="
|
|
15
18
|
group.steps.map((s) => ({
|
|
@@ -18,11 +21,14 @@
|
|
|
18
21
|
isCurrent: s.isCurrent,
|
|
19
22
|
desc: describePageStep(s.step),
|
|
20
23
|
diffable: isPageStepDiffable(s.step),
|
|
21
|
-
revertable: s.applied,
|
|
24
|
+
revertable: s.applied && isPageStepRevertable(s.step),
|
|
25
|
+
source: s.step.source,
|
|
26
|
+
time: formatHistoryTime(s.step.timestamp),
|
|
27
|
+
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
|
22
28
|
}))
|
|
23
29
|
"
|
|
24
30
|
:is-current="group.isCurrent"
|
|
25
|
-
:expanded="!!expanded[`pg-${
|
|
31
|
+
:expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
|
|
26
32
|
@toggle="(key: string) => $emit('toggle', key)"
|
|
27
33
|
@goto="(index: number) => $emit('goto', index)"
|
|
28
34
|
@diff-step="(index: number) => $emit('diff-step', index)"
|
|
@@ -44,7 +50,15 @@ import { TMagicScrollbar } from '@tmagic/design';
|
|
|
44
50
|
|
|
45
51
|
import type { PageHistoryGroup, StepValue } from '@editor/type';
|
|
46
52
|
|
|
47
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
describePageGroup,
|
|
55
|
+
describePageStep,
|
|
56
|
+
formatHistoryFullTime,
|
|
57
|
+
formatHistoryTime,
|
|
58
|
+
groupSource,
|
|
59
|
+
groupTimestamp,
|
|
60
|
+
isPageStepRevertable,
|
|
61
|
+
} from './composables';
|
|
48
62
|
import GroupRow from './GroupRow.vue';
|
|
49
63
|
import InitialRow from './InitialRow.vue';
|
|
50
64
|
|
|
@@ -55,7 +69,11 @@ defineOptions({
|
|
|
55
69
|
const props = defineProps<{
|
|
56
70
|
/** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
|
|
57
71
|
list: PageHistoryGroup[];
|
|
58
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
|
|
74
|
+
* 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
|
75
|
+
* 这样历史数据更新(新增 / 撤销重做导致列表顺序变化)后,已展开的分组状态仍能正确保持。
|
|
76
|
+
*/
|
|
59
77
|
expanded: Record<string, boolean>;
|
|
60
78
|
}>();
|
|
61
79
|
|
|
@@ -1,11 +1,14 @@
|
|
|
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,
|
|
6
8
|
CodeBlockStepValue,
|
|
7
9
|
DataSourceHistoryGroup,
|
|
8
10
|
DataSourceStepValue,
|
|
11
|
+
HistoryOpSource,
|
|
9
12
|
HistoryOpType,
|
|
10
13
|
PageHistoryGroup,
|
|
11
14
|
StepValue,
|
|
@@ -22,7 +25,10 @@ import type {
|
|
|
22
25
|
export const useHistoryList = () => {
|
|
23
26
|
const { historyService } = useServices();
|
|
24
27
|
|
|
25
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。
|
|
30
|
+
* 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。
|
|
31
|
+
*/
|
|
26
32
|
const expanded = reactive<Record<string, boolean>>({});
|
|
27
33
|
const toggleGroup = (key: string) => {
|
|
28
34
|
expanded[key] = !expanded[key];
|
|
@@ -64,6 +70,32 @@ export const useHistoryList = () => {
|
|
|
64
70
|
};
|
|
65
71
|
};
|
|
66
72
|
|
|
73
|
+
/**
|
|
74
|
+
* 历史面板的时间展示:
|
|
75
|
+
* - 当天的记录只显示 `HH:mm:ss`;
|
|
76
|
+
* - 跨天的记录显示 `MM-DD HH:mm:ss`。
|
|
77
|
+
* 无时间戳(旧数据 / 未写入)时返回空串,UI 据此不渲染时间。
|
|
78
|
+
*/
|
|
79
|
+
export const formatHistoryTime = (timestamp?: number): string => {
|
|
80
|
+
if (!timestamp) return '';
|
|
81
|
+
const isToday =
|
|
82
|
+
datetimeFormatter(new Date(timestamp), '', 'YYYY-MM-DD') ===
|
|
83
|
+
(datetimeFormatter(new Date(), '', 'YYYY-MM-DD') as string);
|
|
84
|
+
return `${
|
|
85
|
+
isToday
|
|
86
|
+
? datetimeFormatter(new Date(timestamp), '', 'HH:mm:ss')
|
|
87
|
+
: datetimeFormatter(new Date(timestamp), '', 'MM-DD HH:mm:ss')
|
|
88
|
+
}`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** 完整时间(含年份与秒),用于 title 悬浮提示。无时间戳时返回空串。 */
|
|
92
|
+
export const formatHistoryFullTime = (timestamp?: number): string =>
|
|
93
|
+
timestamp ? `${datetimeFormatter(new Date(timestamp), '', 'YYYY-MM-DD HH:mm:ss')}` : '';
|
|
94
|
+
|
|
95
|
+
/** 取一组历史步骤里最后一步(最近一次)的时间戳,用于组头部展示。 */
|
|
96
|
+
export const groupTimestamp = (group: { steps: { step: { timestamp?: number } }[] }): number | undefined =>
|
|
97
|
+
group.steps[group.steps.length - 1]?.step.timestamp;
|
|
98
|
+
|
|
67
99
|
export const opLabel = (op: HistoryOpType) => {
|
|
68
100
|
switch (op) {
|
|
69
101
|
case 'add':
|
|
@@ -76,6 +108,32 @@ export const opLabel = (op: HistoryOpType) => {
|
|
|
76
108
|
}
|
|
77
109
|
};
|
|
78
110
|
|
|
111
|
+
/** 内置操作途径的中文文案;自定义来源直接回显原值,未知 / 缺省返回空串(UI 据此不渲染)。 */
|
|
112
|
+
const HISTORY_SOURCE_LABELS: Record<string, string> = {
|
|
113
|
+
stage: '画布',
|
|
114
|
+
tree: '树面板',
|
|
115
|
+
'component-panel': '组件面板',
|
|
116
|
+
props: '配置面板',
|
|
117
|
+
code: '源码',
|
|
118
|
+
'stage-contextmenu': '画布菜单',
|
|
119
|
+
'tree-contextmenu': '树菜单',
|
|
120
|
+
toolbar: '工具栏',
|
|
121
|
+
shortcut: '快捷键',
|
|
122
|
+
rollback: '回滚',
|
|
123
|
+
api: '接口',
|
|
124
|
+
ai: 'AI',
|
|
125
|
+
unknown: '未知',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** 操作途径文案:用于历史面板展示「画布 / 树面板 / 配置面板…」标签。 */
|
|
129
|
+
export const sourceLabel = (source: HistoryOpSource = 'unknown'): string => {
|
|
130
|
+
return HISTORY_SOURCE_LABELS[source] ?? `${source}`;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** 取一组历史步骤里最后一步(最近一次)的操作途径,用于组头部展示。 */
|
|
134
|
+
export const groupSource = (group: { steps: { step: { source?: HistoryOpSource } }[] }): HistoryOpSource | undefined =>
|
|
135
|
+
group.steps[group.steps.length - 1]?.step.source;
|
|
136
|
+
|
|
79
137
|
const nameOf = (node: { name?: string; id?: string | number; type?: string }) =>
|
|
80
138
|
node?.name || node?.type || `${node?.id ?? ''}`;
|
|
81
139
|
|
|
@@ -193,3 +251,36 @@ export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
|
|
|
193
251
|
const target = labelWithId(rawName, group.id);
|
|
194
252
|
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
|
195
253
|
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 页面 step 是否支持「回滚」(类 git revert):
|
|
257
|
+
* - 新增 / 删除:不依赖 changeRecords,反向应用即删除 / 加回,始终可回滚;
|
|
258
|
+
* - 更新:必须每个被更新节点都带有 changeRecords,才支持按 propPath 局部反向 patch。
|
|
259
|
+
* 缺失 changeRecords 的更新只能整节点替换,会冲掉该节点后续的无关变更,因此不支持回滚。
|
|
260
|
+
*/
|
|
261
|
+
export const isPageStepRevertable = (step: StepValue): boolean => {
|
|
262
|
+
if (step.opType !== 'update') return true;
|
|
263
|
+
const items = step.updatedItems ?? [];
|
|
264
|
+
if (!items.length) return false;
|
|
265
|
+
return items.every((item) => Boolean(item.changeRecords?.length));
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 数据源 step 是否支持「回滚」:
|
|
270
|
+
* - 新增(oldSchema=null)/ 删除(newSchema=null):不依赖 changeRecords,始终可回滚;
|
|
271
|
+
* - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
|
272
|
+
*/
|
|
273
|
+
export const isDataSourceStepRevertable = (step: DataSourceStepValue): boolean => {
|
|
274
|
+
if (step.oldSchema === null || step.newSchema === null) return true;
|
|
275
|
+
return Boolean(step.changeRecords?.length);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 代码块 step 是否支持「回滚」:
|
|
280
|
+
* - 新增(oldContent=null)/ 删除(newContent=null):不依赖 changeRecords,始终可回滚;
|
|
281
|
+
* - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
|
282
|
+
*/
|
|
283
|
+
export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
|
|
284
|
+
if (step.oldContent === null || step.newContent === null) return true;
|
|
285
|
+
return Boolean(step.changeRecords?.length);
|
|
286
|
+
};
|
|
@@ -151,7 +151,11 @@ const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
|
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
// 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
|
|
155
|
+
// 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
|
|
156
|
+
const historySource = eventData ? 'props' : 'code';
|
|
157
|
+
|
|
158
|
+
editorService.update(newValue, { changeRecords: eventData?.changeRecords, historySource });
|
|
155
159
|
} catch (e: any) {
|
|
156
160
|
emit('submit-error', e);
|
|
157
161
|
}
|
|
@@ -94,11 +94,15 @@ let clientX: number;
|
|
|
94
94
|
let clientY: number;
|
|
95
95
|
|
|
96
96
|
const appendComponent = ({ text, type, data = {} }: ComponentItem): void => {
|
|
97
|
-
editorService.add(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
editorService.add(
|
|
98
|
+
{
|
|
99
|
+
name: text,
|
|
100
|
+
type,
|
|
101
|
+
...data,
|
|
102
|
+
},
|
|
103
|
+
undefined,
|
|
104
|
+
{ historySource: 'component-panel' },
|
|
105
|
+
);
|
|
102
106
|
};
|
|
103
107
|
|
|
104
108
|
const dragstartHandler = ({ text, type, data = {} }: ComponentItem, e: DragEvent) => {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editCode(`${data.key}`)"></Icon>
|
|
25
25
|
</TMagicTooltip>
|
|
26
26
|
<TMagicTooltip v-if="data.type === 'code' && editable" effect="dark" content="删除" placement="bottom">
|
|
27
|
-
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}
|
|
27
|
+
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}`, { historySource: 'tree' })"></Icon>
|
|
28
28
|
</TMagicTooltip>
|
|
29
29
|
<slot name="code-block-panel-tool" :id="data.key" :data="data"></slot>
|
|
30
30
|
</template>
|
|
@@ -44,7 +44,7 @@ import Tree from '@editor/components/Tree.vue';
|
|
|
44
44
|
import { useFilter } from '@editor/hooks/use-filter';
|
|
45
45
|
import { useNodeStatus } from '@editor/hooks/use-node-status';
|
|
46
46
|
import { useServices } from '@editor/hooks/use-services';
|
|
47
|
-
import { type CodeBlockListSlots, CodeDeleteErrorType, type TreeNodeData } from '@editor/type';
|
|
47
|
+
import { type CodeBlockListSlots, CodeDeleteErrorType, HistoryOpSource, type TreeNodeData } from '@editor/type';
|
|
48
48
|
|
|
49
49
|
defineSlots<CodeBlockListSlots>();
|
|
50
50
|
|
|
@@ -60,7 +60,7 @@ const props = defineProps<{
|
|
|
60
60
|
|
|
61
61
|
const emit = defineEmits<{
|
|
62
62
|
edit: [id: string];
|
|
63
|
-
remove: [id: string];
|
|
63
|
+
remove: [id: string, { historySource?: HistoryOpSource }];
|
|
64
64
|
'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
|
|
65
65
|
}>();
|
|
66
66
|
|
|
@@ -142,7 +142,7 @@ const editCode = (id: string) => {
|
|
|
142
142
|
emit('edit', id);
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
-
const deleteCode = async (id: string) => {
|
|
145
|
+
const deleteCode = async (id: string, { historySource }: { historySource?: HistoryOpSource } = {}) => {
|
|
146
146
|
const currentCode = codeList.value.find((codeItem) => codeItem.id === id);
|
|
147
147
|
const existBinds = Boolean(currentCode?.items?.length);
|
|
148
148
|
const undeleteableList = codeBlockService.getUndeletableList() || [];
|
|
@@ -154,7 +154,7 @@ const deleteCode = async (id: string) => {
|
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
// 无绑定关系,且不在不可删除列表中
|
|
157
|
-
emit('remove', id);
|
|
157
|
+
emit('remove', id, { historySource });
|
|
158
158
|
} else {
|
|
159
159
|
if (typeof props.customError === 'function') {
|
|
160
160
|
props.customError(id, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
|
|
@@ -122,7 +122,7 @@ const {
|
|
|
122
122
|
menuData: contentMenuData,
|
|
123
123
|
contentMenuHideHandler,
|
|
124
124
|
} = useContentMenu((id: string) => {
|
|
125
|
-
codeBlockListRef.value?.deleteCode(id);
|
|
125
|
+
codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
|
|
126
126
|
});
|
|
127
127
|
const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block'));
|
|
128
128
|
</script>
|
|
@@ -41,7 +41,7 @@ export const useContentMenu = (deleteCode: (id: string) => void) => {
|
|
|
41
41
|
|
|
42
42
|
const newCodeId = await codeBlockService.getUniqueId();
|
|
43
43
|
|
|
44
|
-
codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock));
|
|
44
|
+
codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock), { historySource: 'tree-contextmenu' });
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
{
|