@tmagic/editor 1.8.0-beta.4 → 1.8.0-beta.6

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 (76) hide show
  1. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +1 -1
  2. package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
  3. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +4 -1
  4. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +8 -2
  5. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +25 -8
  6. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +6 -4
  7. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +25 -12
  8. package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +27 -5
  9. package/dist/es/index.js +6 -3
  10. package/dist/es/initService.js +1 -1
  11. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
  12. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -55
  13. package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -39
  14. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +149 -103
  15. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +32 -8
  16. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +335 -213
  17. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +36 -7
  18. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -60
  19. package/dist/es/layouts/history-list/composables.js +73 -32
  20. package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
  21. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
  22. package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +16 -3
  23. package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +23 -1
  24. package/dist/es/services/codeBlock.js +96 -37
  25. package/dist/es/services/dataSource.js +70 -26
  26. package/dist/es/services/editor.js +266 -104
  27. package/dist/es/services/history.js +271 -212
  28. package/dist/es/services/ui.js +3 -0
  29. package/dist/es/style.css +53 -7
  30. package/dist/es/utils/data-source/index.js +2 -0
  31. package/dist/es/utils/editor.js +103 -49
  32. package/dist/es/utils/history.js +232 -0
  33. package/dist/es/utils/indexed-db.js +86 -0
  34. package/dist/es/utils/undo-redo.js +60 -1
  35. package/dist/style.css +53 -7
  36. package/dist/tmagic-editor.umd.cjs +2064 -1000
  37. package/package.json +7 -7
  38. package/src/components/CompareForm.vue +3 -1
  39. package/src/components/ToolButton.vue +2 -2
  40. package/src/fields/CodeSelectCol.vue +7 -1
  41. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +16 -2
  42. package/src/fields/DataSourceFields.vue +37 -8
  43. package/src/fields/DataSourceMethodSelect.vue +9 -4
  44. package/src/fields/DataSourceMethods.vue +42 -21
  45. package/src/fields/StyleSetter/components/Border.vue +15 -6
  46. package/src/index.ts +1 -0
  47. package/src/initService.ts +1 -1
  48. package/src/layouts/Framework.vue +1 -1
  49. package/src/layouts/history-list/Bucket.vue +34 -71
  50. package/src/layouts/history-list/BucketTab.vue +46 -54
  51. package/src/layouts/history-list/GroupRow.vue +146 -111
  52. package/src/layouts/history-list/HistoryDiffDialog.vue +94 -68
  53. package/src/layouts/history-list/HistoryListPanel.vue +298 -115
  54. package/src/layouts/history-list/InitialRow.vue +28 -9
  55. package/src/layouts/history-list/PageTab.vue +57 -51
  56. package/src/layouts/history-list/composables.ts +157 -36
  57. package/src/layouts/page-bar/PageBar.vue +4 -2
  58. package/src/layouts/sidebar/Sidebar.vue +6 -1
  59. package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +30 -2
  60. package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +26 -1
  61. package/src/services/codeBlock.ts +113 -37
  62. package/src/services/dataSource.ts +94 -40
  63. package/src/services/editor.ts +376 -136
  64. package/src/services/history.ts +306 -209
  65. package/src/services/ui.ts +7 -0
  66. package/src/theme/history-list-panel.scss +72 -5
  67. package/src/theme/page-bar.scss +0 -4
  68. package/src/theme/style-setter/border.scss +4 -1
  69. package/src/type.ts +183 -64
  70. package/src/utils/data-source/index.ts +2 -0
  71. package/src/utils/editor.ts +166 -60
  72. package/src/utils/history.ts +308 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/indexed-db.ts +122 -0
  75. package/src/utils/undo-redo.ts +88 -0
  76. package/types/index.d.ts +813 -302
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.8.0-beta.4",
2
+ "version": "1.8.0-beta.6",
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/design": "1.8.0-beta.4",
62
- "@tmagic/form": "1.8.0-beta.4",
63
- "@tmagic/stage": "1.8.0-beta.4",
64
- "@tmagic/table": "1.8.0-beta.4",
65
- "@tmagic/utils": "1.8.0-beta.4"
61
+ "@tmagic/design": "1.8.0-beta.6",
62
+ "@tmagic/stage": "1.8.0-beta.6",
63
+ "@tmagic/form": "1.8.0-beta.6",
64
+ "@tmagic/table": "1.8.0-beta.6",
65
+ "@tmagic/utils": "1.8.0-beta.6"
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.4"
79
+ "@tmagic/core": "1.8.0-beta.6"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
@@ -180,7 +180,9 @@ const defaultLoadConfig = async (): Promise<FormConfig> => {
180
180
  if (!props.type) {
181
181
  return [];
182
182
  }
183
- return removeStyleDisplayConfig(await propsService.getPropsConfig(props.type));
183
+ return removeStyleDisplayConfig(
184
+ await propsService.getPropsConfig(props.type, { node: props.value as unknown as MNode }),
185
+ );
184
186
  }
185
187
  case 'data-source': {
186
188
  return dataSourceService.getFormConfig(props.type || 'base');
@@ -13,7 +13,7 @@
13
13
 
14
14
  <template v-else-if="data.type === 'button'">
15
15
  <TMagicTooltip v-if="data.tooltip" effect="dark" placement="bottom-start" :content="data.tooltip">
16
- <TMagicButton size="small" link :disabled="disabled">
16
+ <TMagicButton size="small" link :disabled="disabled" v-bind="data.buttonProps || {}">
17
17
  <template #icon v-if="data.icon">
18
18
  <MIcon :icon="data.icon"></MIcon>
19
19
  </template>
@@ -21,7 +21,7 @@
21
21
  </TMagicButton>
22
22
  </TMagicTooltip>
23
23
 
24
- <TMagicButton v-else size="small" link :disabled="disabled" :title="data.text">
24
+ <TMagicButton v-else size="small" link :disabled="disabled" :title="data.text" v-bind="data.buttonProps || {}">
25
25
  <template #icon v-if="data.icon">
26
26
  <MIcon :icon="data.icon"></MIcon>
27
27
  </template>
@@ -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, {
package/src/index.ts CHANGED
@@ -72,6 +72,7 @@ export { default as Resizer } from './components/Resizer.vue';
72
72
  export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
73
73
  export { default as CompareForm } from './components/CompareForm.vue';
74
74
  export { default as HistoryListBucket } from './layouts/history-list/Bucket.vue';
75
+ export { default as HistoryListBucketTab } from './layouts/history-list/BucketTab.vue';
75
76
  export { default as HistoryDiffDialog } from './layouts/history-list/HistoryDiffDialog.vue';
76
77
  export { default as FloatingBox } from './components/FloatingBox.vue';
77
78
  export { default as Tree } from './components/Tree.vue';
@@ -55,7 +55,7 @@ export const initServiceState = (
55
55
  watch(
56
56
  () => props.modelValue,
57
57
  (modelValue) => {
58
- editorService.set('root', modelValue || null);
58
+ editorService.set('root', modelValue || null, { historySource: 'initial' });
59
59
  },
60
60
  {
61
61
  immediate: true,
@@ -168,7 +168,7 @@ onBeforeUnmount(() => {
168
168
  const saveCode = (value: string) => {
169
169
  try {
170
170
  const parseDSL = getEditorConfig('parseDSL');
171
- editorService.set('root', parseDSL(value));
171
+ editorService.set('root', parseDSL(value), { historySource: 'root-code' });
172
172
  } catch (e: any) {
173
173
  console.error(e);
174
174
  }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="m-editor-history-list-bucket">
3
3
  <div class="m-editor-history-list-bucket-title">
4
- <span>{{ title }}</span>
4
+ <span>{{ config.title }}</span>
5
5
  <code>{{ String(bucketId) }}</code>
6
6
  <span class="m-editor-history-list-bucket-count">{{ groups.length }} 组</span>
7
7
  </div>
@@ -9,32 +9,10 @@
9
9
  <ul class="m-editor-history-list-ul">
10
10
  <GroupRow
11
11
  v-for="group in groups"
12
- :key="`${prefix}-${bucketId}-${group.steps[0]?.index}`"
13
- :group-key="`${prefix}-${bucketId}-${group.steps[0]?.index}`"
14
- :applied="group.applied"
15
- :merged="group.steps.length > 1"
16
- :op-type="group.opType"
17
- :desc="describeGroup(group)"
18
- :source="groupSource(group)"
19
- :time="formatHistoryTime(groupTimestamp(group))"
20
- :time-title="formatHistoryFullTime(groupTimestamp(group))"
21
- :step-count="group.steps.length"
22
- :sub-steps="
23
- group.steps.map((s: any) => ({
24
- index: s.index,
25
- applied: s.applied,
26
- isCurrent: s.isCurrent,
27
- desc: describeStep(s.step),
28
- diffable: isStepDiffable ? isStepDiffable(s.step) : false,
29
- revertable: s.applied && (isStepRevertable ? isStepRevertable(s.step) : true),
30
- source: s.step.source,
31
- time: formatHistoryTime(s.step.timestamp),
32
- timeTitle: formatHistoryFullTime(s.step.timestamp),
33
- }))
34
- "
35
- :is-current="group.isCurrent"
36
- :expanded="!!expanded[`${prefix}-${bucketId}-${group.steps[0]?.index}`]"
37
- :goto-enabled="gotoEnabled"
12
+ :key="rowKey(group)"
13
+ :group="toRow(group)"
14
+ :expanded="isHistoryGroupExpanded(expanded, rowKey(group))"
15
+ :goto-enabled="config.gotoEnabled"
38
16
  @toggle="(key: string) => $emit('toggle', key)"
39
17
  @goto="(index: number) => $emit('goto', bucketId, index)"
40
18
  @diff-step="(index: number) => $emit('diff-step', bucketId, index)"
@@ -43,24 +21,25 @@
43
21
  <!--
44
22
  初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。
45
23
  当 bucket 内所有 group 都未 applied 时即为当前位置。
46
- showInitial=false 时不展示(用于没有"撤销到初始状态"语义的自定义历史,如业务模块历史)。
24
+ config.showInitial=false 时不展示(用于没有"撤销到初始状态"语义的自定义历史,如业务模块历史)。
47
25
  -->
48
26
  <InitialRow
49
- v-if="showInitial !== false"
27
+ v-if="config.showInitial !== false"
50
28
  :is-current="isInitial"
51
- :goto-enabled="gotoEnabled"
29
+ :goto-enabled="config.gotoEnabled"
52
30
  @goto-initial="$emit('goto-initial', bucketId)"
53
31
  />
54
32
  </ul>
55
33
  </div>
56
34
  </template>
57
35
 
58
- <script lang="ts" setup>
36
+ <script lang="ts" setup generic="T extends BaseStepValue = BaseStepValue">
59
37
  import { computed } from 'vue';
60
38
 
61
- import type { HistoryOpType } from '@editor/type';
39
+ import type { BaseStepValue, HistoryBucketConfig } from '@editor/type';
62
40
 
63
- import { formatHistoryFullTime, formatHistoryTime, groupSource, groupTimestamp } from './composables';
41
+ import type { HistoryBucketGroup, HistoryRowGroup } from './composables';
42
+ import { isHistoryGroupExpanded, toRowGroup } from './composables';
64
43
  import GroupRow from './GroupRow.vue';
65
44
  import InitialRow from './InitialRow.vue';
66
45
 
@@ -68,44 +47,19 @@ defineOptions({
68
47
  name: 'MEditorHistoryListBucket',
69
48
  });
70
49
 
71
- const props = withDefaults(
72
- defineProps<{
73
- /** Bucket 标题,例如 "数据源" / "代码块",渲染在 bucket 头部。 */
74
- title: string;
75
- /** 当前 bucket 对应的目标 id(dataSource.id 或 codeBlock.id),同时用于组装子项的 key。 */
76
- bucketId: string | number;
77
- /**
78
- * 子项 key 的命名空间前缀:内置 `ds` 表示数据源,`cb` 表示代码块;
79
- * 业务方复用 Bucket 时可传入自定义前缀(如 `mod`)。与上层折叠状态 key 保持一致。
80
- */
81
- prefix: string;
82
- /** 是否展示底部「回到初始状态」入口,默认 true。无 undo cursor 语义的自定义历史可传 false 关闭。 */
83
- showInitial?: boolean;
84
- /** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
85
- groups: {
86
- applied: boolean;
87
- isCurrent?: boolean;
88
- opType: HistoryOpType;
89
- steps: { index: number; applied: boolean; isCurrent?: boolean; step: any }[];
90
- }[];
91
- /** 组级描述文案生成器,接收一个 group,返回展示文本。由父组件按业务类型注入。 */
92
- describeGroup: (_group: any) => string;
93
- /** 单步描述文案生成器,接收一个 step,返回展示文本。用于合并组展开后的子步列表。 */
94
- describeStep: (_step: any) => string;
95
- /** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
96
- isStepDiffable?: (_step: any) => boolean;
97
- /** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。由父组件按业务类型注入;不传则已应用即可回滚。 */
98
- isStepRevertable?: (_step: any) => boolean;
99
- /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
100
- expanded: Record<string, boolean>;
101
- /** 是否支持「跳转到该记录」(goto)。默认 true。 */
102
- gotoEnabled?: boolean;
103
- }>(),
104
- {
105
- showInitial: true,
106
- gotoEnabled: true,
107
- },
108
- );
50
+ const props = defineProps<{
51
+ /**
52
+ * 该类历史的整体渲染配置(title / prefix / describe* / isStep* / showInitial / gotoEnabled)。
53
+ * 由父组件按业务类型注入,组件内部按需读取,避免逐项透传多个 props。
54
+ */
55
+ config: HistoryBucketConfig<T>;
56
+ /** 当前 bucket 对应的目标 id(dataSource.id 或 codeBlock.id),同时用于组装子项的 key。 */
57
+ bucketId: string | number;
58
+ /** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
59
+ groups: HistoryBucketGroup<T>[];
60
+ /** 共享的折叠状态表(key -> 是否展开,缺省或 true 为展开、false 为收起),由顶层 panel 统一维护以便跨 tab 复用。 */
61
+ expanded: Record<string, boolean>;
62
+ }>();
109
63
 
110
64
  defineEmits<{
111
65
  /** 透传子组件 GroupRow 的 toggle,由上层 panel 更新 expanded。 */
@@ -123,6 +77,15 @@ defineEmits<{
123
77
  (_e: 'revert-step', _bucketId: string | number, _index: number): void;
124
78
  }>();
125
79
 
80
+ /**
81
+ * 子项 / 折叠状态 key:`${prefix}-${bucketId}-${组内首步 index}`。
82
+ * 以稳定的 step 索引(而非展示位置)标识分组,历史数据更新后已展开的分组状态仍能正确保持。
83
+ */
84
+ const rowKey = (group: HistoryBucketGroup<T>) => `${props.config.prefix}-${props.bucketId}-${group.steps[0]?.index}`;
85
+
86
+ /** 把原始分组派生为 GroupRow 直接消费的视图模型。 */
87
+ const toRow = (group: HistoryBucketGroup<T>): HistoryRowGroup => toRowGroup(group, rowKey(group), props.config);
88
+
126
89
  /** 该 bucket 是否处于初始状态(栈 cursor=0),等价于全部 group 都未 applied。 */
127
90
  const isInitial = computed(() => props.groups.length > 0 && props.groups.every((g) => !g.applied));
128
91
  </script>