@tmagic/editor 1.8.0-beta.5 → 1.8.0-beta.7

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.
Files changed (46) hide show
  1. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +4 -1
  2. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +8 -2
  3. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +25 -8
  4. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +6 -4
  5. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +25 -12
  6. package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +27 -5
  7. package/dist/es/index.js +2 -2
  8. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +10 -14
  9. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +12 -4
  10. package/dist/es/layouts/history-list/composables.js +28 -77
  11. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +2 -2
  12. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +16 -3
  13. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +23 -1
  14. package/dist/es/services/codeBlock.js +30 -19
  15. package/dist/es/services/dataSource.js +29 -21
  16. package/dist/es/services/editor.js +52 -33
  17. package/dist/es/services/history.js +10 -15
  18. package/dist/es/style.css +4 -1
  19. package/dist/es/utils/data-source/index.js +2 -0
  20. package/dist/es/utils/editor.js +68 -48
  21. package/dist/es/utils/history.js +42 -33
  22. package/dist/style.css +4 -1
  23. package/dist/tmagic-editor.umd.cjs +412 -295
  24. package/package.json +7 -7
  25. package/src/fields/CodeSelectCol.vue +7 -1
  26. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +16 -2
  27. package/src/fields/DataSourceFields.vue +37 -8
  28. package/src/fields/DataSourceMethodSelect.vue +9 -4
  29. package/src/fields/DataSourceMethods.vue +42 -21
  30. package/src/fields/StyleSetter/components/Border.vue +15 -6
  31. package/src/layouts/history-list/HistoryListPanel.vue +12 -25
  32. package/src/layouts/history-list/InitialRow.vue +3 -0
  33. package/src/layouts/history-list/composables.ts +34 -94
  34. package/src/layouts/sidebar/Sidebar.vue +2 -2
  35. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +31 -2
  36. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +26 -1
  37. package/src/services/codeBlock.ts +28 -22
  38. package/src/services/dataSource.ts +29 -24
  39. package/src/services/editor.ts +41 -37
  40. package/src/services/history.ts +19 -25
  41. package/src/theme/style-setter/border.scss +4 -1
  42. package/src/type.ts +38 -26
  43. package/src/utils/data-source/index.ts +2 -0
  44. package/src/utils/editor.ts +125 -59
  45. package/src/utils/history.ts +48 -45
  46. package/types/index.d.ts +109 -112
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.5",
2
+ "version": "1.8.0-beta.7",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -58,11 +58,11 @@
58
58
  "moveable": "^0.53.0",
59
59
  "serialize-javascript": "^7.0.0",
60
60
  "sortablejs": "^1.15.6",
61
- "@tmagic/form": "1.8.0-beta.5",
62
- "@tmagic/table": "1.8.0-beta.5",
63
- "@tmagic/utils": "1.8.0-beta.5",
64
- "@tmagic/design": "1.8.0-beta.5",
65
- "@tmagic/stage": "1.8.0-beta.5"
61
+ "@tmagic/form": "1.8.0-beta.7",
62
+ "@tmagic/design": "1.8.0-beta.7",
63
+ "@tmagic/table": "1.8.0-beta.7",
64
+ "@tmagic/utils": "1.8.0-beta.7",
65
+ "@tmagic/stage": "1.8.0-beta.7"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/events": "^3.0.3",
@@ -76,7 +76,7 @@
76
76
  "type-fest": "^5.2.0",
77
77
  "typescript": "^6.0.3",
78
78
  "vue": "^3.5.34",
79
- "@tmagic/core": "1.8.0-beta.5"
79
+ "@tmagic/core": "1.8.0-beta.7"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -107,10 +107,12 @@ const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues
107
107
 
108
108
  const notEditable = computed(() => filterFunction(mForm, props.config.notEditable, props));
109
109
 
110
- const hasCodeBlockSidePanel = computed(() =>
110
+ const codeBlockSidePanel = computed(() =>
111
111
  (uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.CODE_BLOCK),
112
112
  );
113
113
 
114
+ const hasCodeBlockSidePanel = computed(() => codeBlockSidePanel.value);
115
+
114
116
  /**
115
117
  * 根据代码块id获取代码块参数配置
116
118
  * @param codeId 代码块ID
@@ -191,6 +193,10 @@ const onParamsChangeHandler = (value: any, eventData: ContainerChangeEventData)
191
193
  };
192
194
 
193
195
  const editCode = (id: string) => {
196
+ const sideBarItem = codeBlockSidePanel.value;
197
+ if (sideBarItem) {
198
+ uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.CODE_BLOCK);
199
+ }
194
200
  eventBus?.emit('edit-code', id);
195
201
  };
196
202
  </script>
@@ -211,11 +211,25 @@ const onChangeHandler = (v: string[] = []) => {
211
211
  emit('change', v);
212
212
  };
213
213
 
214
- const hasDataSourceSidePanel = computed(() =>
214
+ const dataSourceSidePanel = computed(() =>
215
215
  uiService.get('sideBarItems').find((item) => item.$key === SideItemKey.DATA_SOURCE),
216
216
  );
217
217
 
218
+ const hasDataSourceSidePanel = computed(() => dataSourceSidePanel.value);
219
+
218
220
  const editHandler = (id: string) => {
219
- eventBus?.emit('edit-data-source', removeDataSourceFieldPrefix(id));
221
+ const sideBarItem = dataSourceSidePanel.value;
222
+ if (sideBarItem) {
223
+ uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.DATA_SOURCE);
224
+ }
225
+
226
+ const dataSourceId = removeDataSourceFieldPrefix(id);
227
+ const fieldPath = selectFieldsId.value;
228
+
229
+ if (fieldPath.length) {
230
+ eventBus?.emit('edit-data-source-field', dataSourceId, [...fieldPath]);
231
+ } else {
232
+ eventBus?.emit('edit-data-source', dataSourceId);
233
+ }
220
234
  };
221
235
  </script>
@@ -47,7 +47,7 @@
47
47
  </template>
48
48
 
49
49
  <script setup lang="ts">
50
- import { computed, inject, Ref, ref } from 'vue';
50
+ import { computed, type ComputedRef, inject, onMounted, provide, Ref, ref } from 'vue';
51
51
 
52
52
  import type { DataSchema } from '@tmagic/core';
53
53
  import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
@@ -101,6 +101,16 @@ const newHandler = () => {
101
101
  addDialogVisible.value = true;
102
102
  };
103
103
 
104
+ const editField = (row: Record<string, any>, index: number) => {
105
+ fieldValues.value = {
106
+ ...row,
107
+ index,
108
+ };
109
+ fieldTitle.value = `编辑${row.title}`;
110
+ calcBoxPosition();
111
+ addDialogVisible.value = true;
112
+ };
113
+
104
114
  const fieldChange = ({ index, ...value }: Record<string, any>, data: ContainerChangeEventData) => {
105
115
  addDialogVisible.value = false;
106
116
 
@@ -158,13 +168,7 @@ const fieldColumns: ColumnConfig[] = [
158
168
  {
159
169
  text: '编辑',
160
170
  handler: (row: Record<string, any>, index: number) => {
161
- fieldValues.value = {
162
- ...row,
163
- index,
164
- };
165
- fieldTitle.value = `编辑${row.title}`;
166
- calcBoxPosition();
167
- addDialogVisible.value = true;
171
+ editField(row, index);
168
172
  },
169
173
  },
170
174
  {
@@ -354,4 +358,29 @@ const { height: editorHeight } = useEditorContentHeight();
354
358
 
355
359
  const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
356
360
  const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
361
+
362
+ /**
363
+ * 由 DataSourceConfigPanel 注入:打开数据源详情后需要直接打开的字段路径(字段名数组)。
364
+ * 当前层消费 path[0],并把剩余路径下发给嵌套字段,实现逐层打开。
365
+ */
366
+ const editingFieldPath = inject<ComputedRef<string[]>>(
367
+ 'editingDataSourceFieldPath',
368
+ computed(() => []),
369
+ );
370
+
371
+ provide(
372
+ 'editingDataSourceFieldPath',
373
+ computed(() => editingFieldPath.value.slice(1)),
374
+ );
375
+
376
+ onMounted(() => {
377
+ const path = editingFieldPath.value;
378
+ if (!path.length) return;
379
+
380
+ const fields: Record<string, any>[] = props.model[props.name] || [];
381
+ const index = fields.findIndex((field) => field.name === path[0]);
382
+ if (index === -1) return;
383
+
384
+ editField(fields[index], index);
385
+ });
357
386
  </script>
@@ -13,7 +13,7 @@
13
13
  ></MCascader>
14
14
 
15
15
  <TMagicTooltip
16
- v-if="model[name] && isCustomMethod && hasDataSourceSidePanel && !isCompare"
16
+ v-if="model[name] && isCustomMethod && dataSourceSidePanel && !isCompare"
17
17
  :content="notEditable ? '查看' : '编辑'"
18
18
  >
19
19
  <TMagicButton class="m-fields-select-action-button" :size="size" @click="editCodeHandler">
@@ -75,7 +75,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodSelectConfig>>
75
75
  disabled: false,
76
76
  });
77
77
 
78
- const hasDataSourceSidePanel = computed(() =>
78
+ const dataSourceSidePanel = computed(() =>
79
79
  (uiService.get('sideBarItems') || []).find((item) => item.$key === SideItemKey.DATA_SOURCE),
80
80
  );
81
81
 
@@ -208,12 +208,17 @@ const onParamsChangeHandler = (value: any, eventData: ContainerChangeEventData)
208
208
  };
209
209
 
210
210
  const editCodeHandler = () => {
211
- const [id] = props.model[props.name];
211
+ const [id, methodName] = props.model[props.name];
212
212
 
213
213
  const dataSource = dataSourceService.getDataSourceById(id);
214
214
 
215
215
  if (!dataSource) return;
216
216
 
217
- eventBus?.emit('edit-data-source', id);
217
+ const sideBarItem = dataSourceSidePanel.value;
218
+ if (sideBarItem) {
219
+ uiService.set('sideBarActiveTabName', sideBarItem.text || sideBarItem.$key || SideItemKey.DATA_SOURCE);
220
+ }
221
+
222
+ eventBus?.emit('edit-data-source-method', id, methodName);
218
223
  };
219
224
  </script>
@@ -21,7 +21,7 @@
21
21
  </template>
22
22
 
23
23
  <script setup lang="ts">
24
- import { computed, inject, nextTick, ref, useTemplateRef } from 'vue';
24
+ import { computed, type ComputedRef, inject, nextTick, onMounted, ref, useTemplateRef } from 'vue';
25
25
  import { cloneDeep } from 'lodash-es';
26
26
 
27
27
  import type { CodeBlockContent } from '@tmagic/core';
@@ -52,6 +52,29 @@ const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>(
52
52
 
53
53
  let editIndex = -1;
54
54
 
55
+ const editMethod = (method: CodeBlockContent, index: number) => {
56
+ let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
57
+
58
+ if (method.content) {
59
+ if (typeof method.content !== 'string') {
60
+ codeContent = method.content.toString();
61
+ } else {
62
+ codeContent = method.content;
63
+ }
64
+ }
65
+
66
+ codeConfig.value = {
67
+ ...cloneDeep(method),
68
+ content: codeContent,
69
+ };
70
+
71
+ editIndex = index;
72
+
73
+ nextTick(() => {
74
+ codeBlockEditorRef.value?.show();
75
+ });
76
+ };
77
+
55
78
  const methodColumns: ColumnConfig[] = [
56
79
  {
57
80
  label: '名称',
@@ -77,26 +100,7 @@ const methodColumns: ColumnConfig[] = [
77
100
  {
78
101
  text: '编辑',
79
102
  handler: (method: CodeBlockContent, index: number) => {
80
- let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
81
-
82
- if (method.content) {
83
- if (typeof method.content !== 'string') {
84
- codeContent = method.content.toString();
85
- } else {
86
- codeContent = method.content;
87
- }
88
- }
89
-
90
- codeConfig.value = {
91
- ...cloneDeep(method),
92
- content: codeContent,
93
- };
94
-
95
- editIndex = index;
96
-
97
- nextTick(() => {
98
- codeBlockEditorRef.value?.show();
99
- });
103
+ editMethod(method, index);
100
104
  },
101
105
  },
102
106
  {
@@ -158,4 +162,21 @@ const submitCodeHandler = (value: CodeBlockContent, data: ContainerChangeEventDa
158
162
 
159
163
  codeBlockEditorRef.value?.hide();
160
164
  };
165
+
166
+ /** 由 DataSourceConfigPanel 注入:打开数据源详情后需要直接打开的方法名 */
167
+ const editingMethodName = inject<ComputedRef<string | undefined>>(
168
+ 'editingDataSourceMethodName',
169
+ computed(() => ''),
170
+ );
171
+
172
+ onMounted(() => {
173
+ const methodName = editingMethodName.value;
174
+ if (!methodName) return;
175
+
176
+ const methods: CodeBlockContent[] = props.model[props.name] || [];
177
+ const index = methods.findIndex((method) => method.name === methodName);
178
+ if (index === -1) return;
179
+
180
+ editMethod(methods[index], index);
181
+ });
161
182
  </script>
@@ -4,27 +4,31 @@
4
4
  <div class="border-icon-container-row">
5
5
  <div
6
6
  class="border-icon border-icon-top"
7
- :class="{ active: direction === 'Top' }"
7
+ :class="{ active: direction === 'Top', configured: isConfigured('Top') }"
8
8
  @click="selectDirection('Top')"
9
9
  ></div>
10
10
  </div>
11
11
  <div class="border-icon-container-row">
12
12
  <div
13
13
  class="border-icon border-icon-left"
14
- :class="{ active: direction === 'Left' }"
14
+ :class="{ active: direction === 'Left', configured: isConfigured('Left') }"
15
15
  @click="selectDirection('Left')"
16
16
  ></div>
17
- <div class="border-icon" :class="{ active: direction === '' }" @click="selectDirection()"></div>
17
+ <div
18
+ class="border-icon"
19
+ :class="{ active: direction === '', configured: isConfigured('') }"
20
+ @click="selectDirection()"
21
+ ></div>
18
22
  <div
19
23
  class="border-icon border-icon-right"
20
- :class="{ active: direction === 'Right' }"
24
+ :class="{ active: direction === 'Right', configured: isConfigured('Right') }"
21
25
  @click="selectDirection('Right')"
22
26
  ></div>
23
27
  </div>
24
28
  <div class="border-icon-container-row">
25
29
  <div
26
30
  class="border-icon border-icon-bottom"
27
- :class="{ active: direction === 'Bottom' }"
31
+ :class="{ active: direction === 'Bottom', configured: isConfigured('Bottom') }"
28
32
  @click="selectDirection('Bottom')"
29
33
  ></div>
30
34
  </div>
@@ -98,7 +102,7 @@ const emit = defineEmits<{
98
102
  addDiffCount: [];
99
103
  }>();
100
104
 
101
- withDefaults(
105
+ const props = withDefaults(
102
106
  defineProps<{
103
107
  model: FormValue;
104
108
  lastValues?: FormValue;
@@ -109,6 +113,11 @@ withDefaults(
109
113
  {},
110
114
  );
111
115
 
116
+ const hasValue = (value: unknown) => value !== undefined && value !== null && value !== '';
117
+
118
+ const isConfigured = (d: string) =>
119
+ ['Width', 'Color', 'Style'].some((key) => hasValue(props.model?.[`border${d}${key}`]));
120
+
112
121
  const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
113
122
  eventData.changeRecords?.forEach((record) => {
114
123
  emit('change', record.value, {
@@ -100,8 +100,8 @@
100
100
  /**
101
101
  * 历史记录面板:在顶部 NavMenu 上点击图标打开 popover,分三个 tab:
102
102
  * - 页面:当前活动页面的历史栈,连续修改同一节点的多步会被合并成一组
103
- * - 数据源:以 dataSource.id 分组,每组内部相邻的连续 update 自动合并
104
- * - 代码块:同上,按 codeBlock.id 分组并合并相邻 update
103
+ * - 数据源:以 dataSource.id 分桶,每条操作记录独立展示
104
+ * - 代码块:同上,按 codeBlock.id 分桶,每条操作记录独立展示
105
105
  *
106
106
  * 数据通过 historyService 暴露的聚合 API 读取,UI 仅用于只读展示,
107
107
  * 同时支持点击任意一条记录跳转至该状态:
@@ -135,6 +135,7 @@ import type { FormState } from '@tmagic/form';
135
135
  import MIcon from '@editor/components/Icon.vue';
136
136
  import { useServices } from '@editor/hooks/use-services';
137
137
  import type {
138
+ BaseStepValue,
138
139
  CodeBlockStepValue,
139
140
  DataSourceStepValue,
140
141
  DiffDialogPayload,
@@ -143,15 +144,7 @@ import type {
143
144
  } from '@editor/type';
144
145
 
145
146
  import BucketTab from './BucketTab.vue';
146
- import {
147
- describeCodeBlockGroup,
148
- describeCodeBlockStep,
149
- describeDataSourceGroup,
150
- describeDataSourceStep,
151
- isCodeBlockStepRevertable,
152
- isDataSourceStepRevertable,
153
- useHistoryList,
154
- } from './composables';
147
+ import { describeStep, isSingleDiffStepRevertable, useHistoryList } from './composables';
155
148
  import HistoryDiffDialog from './HistoryDiffDialog.vue';
156
149
  import PageTab from './PageTab.vue';
157
150
 
@@ -223,34 +216,28 @@ const {
223
216
  */
224
217
  const pageMarker = computed(() => historyService.getPageMarker());
225
218
 
226
- /** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
227
- const isDataSourceStepDiffable = (step: DataSourceStepValue) =>
228
- Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
229
-
230
219
  /** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
231
- const isCodeBlockStepDiffable = (step: CodeBlockStepValue) =>
232
- Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
220
+ const isStepDiffable = (step: BaseStepValue) => Boolean(step.diff?.[0]?.oldSchema && step.diff?.[0]?.newSchema);
233
221
 
234
222
  /**
235
223
  * 数据源 / 代码块两类 bucket 历史的整体渲染配置:把 title / prefix 与各自的描述、
236
224
  * 可差异、可回滚判定收敛为单一对象整体注入 BucketTab,组件内部按需读取。
237
225
  */
226
+ // 数据源/代码块不做相邻合并,每组恒为单步,省略 describeGroup,由 toRowGroup 回退到 describeStep。
238
227
  const dataSourceConfig: HistoryBucketConfig<DataSourceStepValue> = {
239
228
  title: '数据源',
240
229
  prefix: 'ds',
241
- describeGroup: describeDataSourceGroup,
242
- describeStep: describeDataSourceStep,
243
- isStepDiffable: isDataSourceStepDiffable,
244
- isStepRevertable: isDataSourceStepRevertable,
230
+ describeStep: (step: DataSourceStepValue): string => describeStep(step, (schema) => schema?.title, '数据源'),
231
+ isStepDiffable,
232
+ isStepRevertable: isSingleDiffStepRevertable,
245
233
  };
246
234
 
247
235
  const codeBlockConfig: HistoryBucketConfig<CodeBlockStepValue> = {
248
236
  title: '代码块',
249
237
  prefix: 'cb',
250
- describeGroup: describeCodeBlockGroup,
251
- describeStep: describeCodeBlockStep,
252
- isStepDiffable: isCodeBlockStepDiffable,
253
- isStepRevertable: isCodeBlockStepRevertable,
238
+ describeStep: (step: CodeBlockStepValue): string => describeStep(step, (content) => content?.name, '代码块'),
239
+ isStepDiffable,
240
+ isStepRevertable: isSingleDiffStepRevertable,
254
241
  };
255
242
 
256
243
  /** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
@@ -7,6 +7,7 @@
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
9
  <span class="m-editor-history-list-item-desc">{{ desc }}</span>
10
+ <span v-if="saved" class="m-editor-history-list-item-saved" title="该记录为最近一次保存的状态">已保存</span>
10
11
  <span v-if="gotoEnabled && !isCurrent" class="m-editor-history-list-item-actions">
11
12
  <span class="m-editor-history-list-item-goto" title="回到该记录" @click.stop="onClick">回到</span>
12
13
  </span>
@@ -49,6 +50,8 @@ const props = withDefaults(
49
50
  );
50
51
 
51
52
  const desc = computed(() => props.marker?.historyDescription || '未修改的初始状态');
53
+ /** 基线(初始状态)是否为最近一次保存点:仅页面栈的 `initial` 基线 step 会被 markSaved 标记。 */
54
+ const saved = computed(() => Boolean(props.marker?.saved));
52
55
  const time = computed(() => formatHistoryTime(props.marker?.timestamp));
53
56
  const timeTitle = computed(() => formatHistoryFullTime(props.marker?.timestamp));
54
57
  const rowTitle = computed(() => {
@@ -5,10 +5,6 @@ import { datetimeFormatter } from '@tmagic/form';
5
5
  import { useServices } from '@editor/hooks/use-services';
6
6
  import type {
7
7
  BaseStepValue,
8
- CodeBlockHistoryGroup,
9
- CodeBlockStepValue,
10
- DataSourceHistoryGroup,
11
- DataSourceStepValue,
12
8
  HistoryOpSource,
13
9
  HistoryOpType,
14
10
  HistoryRowDescriptor,
@@ -227,12 +223,14 @@ export const toRowGroup = <T extends BaseStepValue = BaseStepValue>(
227
223
  ): HistoryRowGroup => {
228
224
  const { describeGroup, describeStep, isStepDiffable, isStepRevertable } = descriptor;
229
225
  const timestamp = groupTimestamp(group);
226
+ // 无 describeGroup 时回退到组内最后一步的 describeStep:数据源/代码块不做相邻合并,每组恒为单步,二者等价。
227
+ const lastStep = group.steps[group.steps.length - 1]?.step;
230
228
  return {
231
229
  key,
232
230
  applied: group.applied,
233
231
  isCurrent: Boolean(group.isCurrent),
234
232
  opType: group.opType,
235
- desc: describeGroup(group),
233
+ desc: describeGroup ? describeGroup(group) : describeStep(lastStep),
236
234
  source: groupSource(group),
237
235
  time: formatHistoryTime(timestamp),
238
236
  timeTitle: formatHistoryFullTime(timestamp),
@@ -273,30 +271,43 @@ const pickLastDescription = (descs: (string | undefined)[]): string | undefined
273
271
  return undefined;
274
272
  };
275
273
 
276
- export const describePageStep = (step: StepValue) => {
274
+ /**
275
+ * 页面 / 数据源 / 代码块三类历史共用的单步描述核心。
276
+ * 各类型只在「取展示名」与「实体单位名」上有差异,通过参数注入,文案模板完全一致:
277
+ * - 新增 / 删除:单实体展示「label」,多实体(仅页面可能出现)退化为「N 个X」;
278
+ * - 修改:展示「label · propPath」,无 diff 时兜底「X」,多实体退化为「N 个X」。
279
+ * 操作类型(新增 / 删除 / 修改)已由列表行的 op 徽标单独展示,故描述文案不再重复动词。
280
+ * 展示 id 统一取 schema.id;调用方显式传入的 historyDescription 永远优先。
281
+ */
282
+ export const describeStep = <T>(
283
+ step: BaseStepValue<T>,
284
+ getLabel: (_schema?: T) => string | number | undefined,
285
+ unit: string,
286
+ ): string => {
277
287
  if (step.historyDescription) return step.historyDescription;
278
- const { opType } = step;
279
288
  const items = step.diff ?? [];
280
- if (opType === 'add') {
281
- const count = items.length;
289
+ const label = (schema?: T) => labelWithId(getLabel(schema), (schema as { id?: string | number } | undefined)?.id);
290
+
291
+ if (step.opType === 'add') {
282
292
  const node = items[0]?.newSchema;
283
- return `新增 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
293
+ return items.length === 1 && node ? label(node) : `${items.length} 个${unit}`;
284
294
  }
285
- if (opType === 'remove') {
286
- const count = items.length;
295
+ if (step.opType === 'remove') {
287
296
  const node = items[0]?.oldSchema;
288
- return `删除 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
297
+ return items.length === 1 && node ? label(node) : `${items.length} 个${unit}`;
289
298
  }
290
- if (!items.length) return '修改节点';
299
+ if (!items.length) return unit;
291
300
  if (items.length === 1) {
292
- const { newSchema, changeRecords } = items[0];
293
- const propPath = changeRecords?.[0]?.propPath;
294
- const target = labelWithId(nameOf(newSchema), newSchema?.id);
295
- return `修改 ${target}${propPath ? ` · ${propPath}` : ''}`;
301
+ const { newSchema, oldSchema, changeRecords } = items[0];
302
+ const propPath = changeRecords?.map((changeRecord) => changeRecord.propPath).join(',');
303
+ const target = label(newSchema ?? oldSchema);
304
+ return propPath ? `${target} · ${propPath}` : target;
296
305
  }
297
- return `修改 ${items.length} 个节点`;
306
+ return `${items.length} 个${unit}`;
298
307
  };
299
308
 
309
+ export const describePageStep = (step: StepValue): string => describeStep(step, (node) => nameOf(node), '节点');
310
+
300
311
  /**
301
312
  * 合并组的展示文案:
302
313
  * - 若组内任一步显式提供了 historyDescription:取最后一条非空 historyDescription(最近一次的描述更准确);
@@ -307,68 +318,8 @@ export const describePageGroup = (group: PageHistoryGroup) => {
307
318
  const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
308
319
  if (lastDesc) return lastDesc;
309
320
  if (group.steps.length === 1) return describePageStep(group.steps[0].step);
310
- const paths = new Set<string>();
311
- group.steps.forEach((s) => {
312
- s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
313
- });
314
- const pathList = Array.from(paths).slice(0, 3).join(', ');
315
- const target = labelWithId(
316
- group.targetName ?? (group.targetId !== undefined ? `${group.targetId}` : '节点'),
317
- group.targetId,
318
- );
319
- return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
320
- };
321
321
 
322
- export const describeDataSourceStep = (step: DataSourceStepValue) => {
323
- if (step.historyDescription) return step.historyDescription;
324
- const { oldSchema: oldSchema, newSchema: newSchema, changeRecords } = step.diff?.[0] ?? {};
325
- if (!oldSchema && newSchema) return `创建 ${labelWithId(newSchema.title, newSchema.id ?? step.id)}`;
326
- if (!newSchema && oldSchema) return `删除 ${labelWithId(oldSchema.title, oldSchema.id ?? step.id)}`;
327
- const propPath = changeRecords?.[0]?.propPath;
328
- const title = labelWithId(newSchema?.title || oldSchema?.title, step.id);
329
- return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
330
- };
331
-
332
- export const describeDataSourceGroup = (group: DataSourceHistoryGroup) => {
333
- const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
334
- if (lastDesc) return lastDesc;
335
- if (group.steps.length === 1) return describeDataSourceStep(group.steps[0].step);
336
- const paths = new Set<string>();
337
- group.steps.forEach((s) => {
338
- s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
339
- });
340
- const pathList = Array.from(paths).slice(0, 3).join(', ');
341
- const rawTitle =
342
- group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.title ||
343
- group.steps[0].step.diff?.[0]?.oldSchema?.title;
344
- const target = labelWithId(rawTitle, group.id);
345
- return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
346
- };
347
-
348
- export const describeCodeBlockStep = (step: CodeBlockStepValue) => {
349
- if (step.historyDescription) return step.historyDescription;
350
- const { oldSchema: oldContent, newSchema: newContent, changeRecords } = step.diff?.[0] ?? {};
351
- if (!oldContent && newContent) return `创建 ${labelWithId(newContent.name, newContent.id ?? step.id)}`;
352
- if (!newContent && oldContent) return `删除 ${labelWithId(oldContent.name, oldContent.id ?? step.id)}`;
353
- const propPath = changeRecords?.[0]?.propPath;
354
- const title = labelWithId(newContent?.name || oldContent?.name, step.id);
355
- return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
356
- };
357
-
358
- export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
359
- const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
360
- if (lastDesc) return lastDesc;
361
- if (group.steps.length === 1) return describeCodeBlockStep(group.steps[0].step);
362
- const paths = new Set<string>();
363
- group.steps.forEach((s) => {
364
- s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
365
- });
366
- const pathList = Array.from(paths).slice(0, 3).join(', ');
367
- const rawName =
368
- group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.name ||
369
- group.steps[0].step.diff?.[0]?.oldSchema?.name;
370
- const target = labelWithId(rawName, group.id);
371
- return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
322
+ return labelWithId(group.targetName ?? (group.targetId !== undefined ? `${group.targetId}` : '节点'), group.targetId);
372
323
  };
373
324
 
374
325
  /**
@@ -385,22 +336,11 @@ export const isPageStepRevertable = (step: StepValue): boolean => {
385
336
  };
386
337
 
387
338
  /**
388
- * 数据源 step 是否支持「回滚」:
389
- * - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
390
- * - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
391
- */
392
- export const isDataSourceStepRevertable = (step: DataSourceStepValue): boolean => {
393
- const item = step.diff?.[0];
394
- if (!item?.oldSchema || !item?.newSchema) return true;
395
- return Boolean(item.changeRecords?.length);
396
- };
397
-
398
- /**
399
- * 代码块 step 是否支持「回滚」:
339
+ * diff 项历史(数据源 / 代码块)是否支持「回滚」:
400
340
  * - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
401
- * - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
341
+ * - 更新(前后内容都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
402
342
  */
403
- export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
343
+ export const isSingleDiffStepRevertable = (step: BaseStepValue): boolean => {
404
344
  const item = step.diff?.[0];
405
345
  if (!item?.oldSchema || !item?.newSchema) return true;
406
346
  return Boolean(item.changeRecords?.length);
@@ -257,7 +257,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
257
257
  slots: {},
258
258
  },
259
259
  layer: {
260
- $key: 'layer',
260
+ $key: SideItemKey.LAYER,
261
261
  type: 'component',
262
262
  icon: List,
263
263
  text: '已选组件',
@@ -277,7 +277,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
277
277
  slots: {},
278
278
  },
279
279
  [SideItemKey.CODE_BLOCK]: {
280
- $key: 'code-block',
280
+ $key: SideItemKey.CODE_BLOCK,
281
281
  type: 'component',
282
282
  icon: EditPen,
283
283
  text: '代码编辑',