@tmagic/editor 1.8.0-beta.1 → 1.8.0-beta.2

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 (119) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +11 -2
  2. package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +18 -110
  3. package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +6 -0
  4. package/dist/es/components/CompareForm.js +5 -0
  5. package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +167 -0
  6. package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +19 -3
  7. package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +18 -2
  8. package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +37 -6
  9. package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  10. package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +2 -2
  11. package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +14 -6
  12. package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +3 -1
  13. package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +15 -7
  14. package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +14 -6
  15. package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +3 -1
  16. package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +58 -11
  17. package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +11 -6
  18. package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +8 -5
  19. package/dist/es/hooks/use-code-block-edit.js +2 -2
  20. package/dist/es/hooks/use-stage.js +15 -7
  21. package/dist/es/index.js +3 -1
  22. package/dist/es/initService.js +6 -6
  23. package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +4 -0
  24. package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +12 -7
  25. package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +8 -0
  26. package/dist/es/layouts/history-list/Bucket.js +5 -0
  27. package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +84 -0
  28. package/dist/es/layouts/history-list/CodeBlockTab.js +5 -0
  29. package/dist/es/layouts/history-list/CodeBlockTab.vue_vue_type_script_setup_true_lang.js +62 -0
  30. package/dist/es/layouts/history-list/DataSourceTab.js +5 -0
  31. package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +62 -0
  32. package/dist/es/layouts/history-list/GroupRow.js +5 -0
  33. package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +180 -0
  34. package/dist/es/layouts/history-list/HistoryDiffDialog.js +5 -0
  35. package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +195 -0
  36. package/dist/es/layouts/history-list/HistoryListPanel.js +5 -0
  37. package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +270 -0
  38. package/dist/es/layouts/history-list/InitialRow.js +5 -0
  39. package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +48 -0
  40. package/dist/es/layouts/history-list/PageTab.js +5 -0
  41. package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +99 -0
  42. package/dist/es/layouts/history-list/composables.js +154 -0
  43. package/dist/es/layouts/sidebar/layer/use-node-status.js +1 -1
  44. package/dist/es/services/BaseService.js +31 -68
  45. package/dist/es/services/codeBlock.js +205 -8
  46. package/dist/es/services/componentList.js +2 -2
  47. package/dist/es/services/dataSource.js +217 -8
  48. package/dist/es/services/dep.js +21 -9
  49. package/dist/es/services/editor.js +350 -160
  50. package/dist/es/services/events.js +2 -2
  51. package/dist/es/services/history.js +407 -12
  52. package/dist/es/services/keybinding.js +2 -2
  53. package/dist/es/services/props.js +8 -8
  54. package/dist/es/services/stageOverlay.js +2 -2
  55. package/dist/es/services/storage.js +2 -2
  56. package/dist/es/services/ui.js +2 -2
  57. package/dist/es/style.css +347 -1
  58. package/dist/es/utils/code-block.js +122 -0
  59. package/dist/es/utils/content-menu.js +4 -5
  60. package/dist/es/utils/dep/worker.js +1 -1
  61. package/dist/es/utils/editor.js +2 -2
  62. package/dist/es/utils/undo-redo.js +20 -0
  63. package/dist/style.css +347 -1
  64. package/dist/tmagic-editor.umd.cjs +3165 -774
  65. package/package.json +7 -7
  66. package/src/Editor.vue +7 -0
  67. package/src/components/CodeBlockEditor.vue +20 -109
  68. package/src/components/CodeParams.vue +6 -0
  69. package/src/components/CompareForm.vue +225 -0
  70. package/src/editorProps.ts +2 -0
  71. package/src/fields/Code.vue +19 -3
  72. package/src/fields/CodeSelect.vue +17 -1
  73. package/src/fields/CodeSelectCol.vue +30 -2
  74. package/src/fields/DataSourceFieldSelect/FieldSelect.vue +7 -1
  75. package/src/fields/DataSourceFieldSelect/Index.vue +2 -2
  76. package/src/fields/DataSourceFields.vue +12 -3
  77. package/src/fields/DataSourceMethodSelect.vue +4 -1
  78. package/src/fields/DataSourceMethods.vue +14 -4
  79. package/src/fields/DataSourceMocks.vue +13 -3
  80. package/src/fields/DataSourceSelect.vue +4 -1
  81. package/src/fields/EventSelect.vue +57 -8
  82. package/src/fields/KeyValue.vue +17 -4
  83. package/src/fields/UISelect.vue +5 -2
  84. package/src/hooks/use-code-block-edit.ts +5 -2
  85. package/src/hooks/use-stage.ts +23 -7
  86. package/src/index.ts +1 -0
  87. package/src/layouts/CodeEditor.vue +15 -0
  88. package/src/layouts/Framework.vue +10 -5
  89. package/src/layouts/NavMenu.vue +9 -0
  90. package/src/layouts/history-list/Bucket.vue +99 -0
  91. package/src/layouts/history-list/CodeBlockTab.vue +61 -0
  92. package/src/layouts/history-list/DataSourceTab.vue +61 -0
  93. package/src/layouts/history-list/GroupRow.vue +207 -0
  94. package/src/layouts/history-list/HistoryDiffDialog.vue +212 -0
  95. package/src/layouts/history-list/HistoryListPanel.vue +271 -0
  96. package/src/layouts/history-list/InitialRow.vue +41 -0
  97. package/src/layouts/history-list/PageTab.vue +94 -0
  98. package/src/layouts/history-list/composables.ts +195 -0
  99. package/src/layouts/sidebar/layer/use-node-status.ts +9 -6
  100. package/src/services/BaseService.ts +35 -86
  101. package/src/services/codeBlock.ts +256 -5
  102. package/src/services/dataSource.ts +257 -6
  103. package/src/services/dep.ts +32 -9
  104. package/src/services/editor.ts +454 -171
  105. package/src/services/history.ts +452 -8
  106. package/src/services/props.ts +2 -2
  107. package/src/theme/history-list-panel.scss +423 -0
  108. package/src/theme/theme.scss +1 -0
  109. package/src/type.ts +179 -6
  110. package/src/utils/code-block.ts +150 -0
  111. package/src/utils/content-menu.ts +8 -5
  112. package/src/utils/dep/worker.ts +5 -3
  113. package/src/utils/index.ts +1 -0
  114. package/src/utils/undo-redo.ts +23 -0
  115. package/types/index.d.ts +2105 -810
  116. package/dist/es/utils/compose.js +0 -44
  117. package/dist/es/utils/editor-history.js +0 -100
  118. package/src/utils/compose.ts +0 -52
  119. package/src/utils/editor-history.ts +0 -138
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div class="m-editor-data-source-fields">
3
- <MagicTable :data="model[name]" :columns="fieldColumns" :border="true"></MagicTable>
3
+ <MagicTable :data="model[name]" :columns="displayColumns" :border="true"></MagicTable>
4
4
 
5
- <div class="m-editor-data-source-fields-footer">
5
+ <div v-if="!isCompare" class="m-editor-data-source-fields-footer">
6
6
  <TMagicButton size="small" :disabled="disabled" plain @click="newFromJsonHandler()">快速添加</TMagicButton>
7
7
  <TMagicButton size="small" type="primary" :disabled="disabled" plain @click="newHandler()">添加</TMagicButton>
8
8
  </div>
@@ -47,7 +47,7 @@
47
47
  </template>
48
48
 
49
49
  <script setup lang="ts">
50
- import { inject, Ref, ref } from 'vue';
50
+ import { computed, inject, Ref, ref } from 'vue';
51
51
 
52
52
  import type { DataSchema } from '@tmagic/core';
53
53
  import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
@@ -84,6 +84,10 @@ const emit = defineEmits<{
84
84
  }>();
85
85
 
86
86
  const { uiService } = useServices();
87
+ const mForm = inject<FormState | undefined>('mForm');
88
+
89
+ /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
90
+ const isCompare = computed(() => Boolean(mForm?.isCompare));
87
91
 
88
92
  const fieldValues = ref<Record<string, any>>({});
89
93
  const fieldTitle = ref('');
@@ -176,6 +180,11 @@ const fieldColumns: ColumnConfig[] = [
176
180
  },
177
181
  ];
178
182
 
183
+ /** 对比模式下移除「操作」列(编辑/删除按钮),仅保留只读列。 */
184
+ const displayColumns = computed<ColumnConfig[]>(() =>
185
+ isCompare.value ? fieldColumns.filter((col) => !col.actions) : fieldColumns,
186
+ );
187
+
179
188
  const dataSourceFieldsConfig: FormConfig = [
180
189
  { name: 'index', type: 'hidden', filter: 'number', defaultValue: -1 },
181
190
  {
@@ -13,7 +13,7 @@
13
13
  ></MCascader>
14
14
 
15
15
  <TMagicTooltip
16
- v-if="model[name] && isCustomMethod && hasDataSourceSidePanel"
16
+ v-if="model[name] && isCustomMethod && hasDataSourceSidePanel && !isCompare"
17
17
  :content="notEditable ? '查看' : '编辑'"
18
18
  >
19
19
  <TMagicButton class="m-fields-select-action-button" :size="size" @click="editCodeHandler">
@@ -81,6 +81,9 @@ const hasDataSourceSidePanel = computed(() =>
81
81
 
82
82
  const notEditable = computed(() => filterFunction(mForm, props.config.notEditable, props));
83
83
 
84
+ /** 对比模式下隐藏查看/编辑操作按钮,仅保留只读展示。 */
85
+ const isCompare = computed(() => Boolean(mForm?.isCompare));
86
+
84
87
  const dataSources = computed(() => dataSourceService.get('dataSources'));
85
88
 
86
89
  const isCustomMethod = computed(() => {
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div class="m-editor-data-source-methods">
3
- <MagicTable :data="model[name]" :columns="methodColumns" :border="true"></MagicTable>
3
+ <MagicTable :data="model[name]" :columns="displayColumns" :border="true"></MagicTable>
4
4
 
5
- <div class="m-editor-data-source-methods-footer">
5
+ <div v-if="!isCompare" class="m-editor-data-source-methods-footer">
6
6
  <TMagicButton size="small" type="primary" :disabled="disabled" plain @click="createCodeHandler"
7
7
  >添加</TMagicButton
8
8
  >
@@ -21,12 +21,12 @@
21
21
  </template>
22
22
 
23
23
  <script setup lang="ts">
24
- import { nextTick, ref, useTemplateRef } from 'vue';
24
+ import { computed, inject, nextTick, ref, useTemplateRef } from 'vue';
25
25
  import { cloneDeep } from 'lodash-es';
26
26
 
27
27
  import type { CodeBlockContent } from '@tmagic/core';
28
28
  import { TMagicButton, tMagicMessageBox } from '@tmagic/design';
29
- import type { ContainerChangeEventData, DataSourceMethodsConfig, FieldProps } from '@tmagic/form';
29
+ import type { ContainerChangeEventData, DataSourceMethodsConfig, FieldProps, FormState } from '@tmagic/form';
30
30
  import { type ColumnConfig, MagicTable } from '@tmagic/table';
31
31
 
32
32
  import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
@@ -42,6 +42,11 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodsConfig>>(), {
42
42
 
43
43
  const emit = defineEmits(['change']);
44
44
 
45
+ const mForm = inject<FormState | undefined>('mForm');
46
+
47
+ /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
48
+ const isCompare = computed(() => Boolean(mForm?.isCompare));
49
+
45
50
  const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
46
51
  const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>('codeBlockEditor');
47
52
 
@@ -107,6 +112,11 @@ const methodColumns: ColumnConfig[] = [
107
112
  },
108
113
  ];
109
114
 
115
+ /** 对比模式下移除「操作」列(编辑/删除按钮),仅保留只读列。 */
116
+ const displayColumns = computed<ColumnConfig[]>(() =>
117
+ isCompare.value ? methodColumns.filter((col) => !col.actions) : methodColumns,
118
+ );
119
+
110
120
  const createCodeHandler = () => {
111
121
  codeConfig.value = {
112
122
  name: '',
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div class="m-editor-data-source-fields">
3
- <MagicTable :data="model[name]" :columns="columns"></MagicTable>
3
+ <MagicTable :data="model[name]" :columns="displayColumns"></MagicTable>
4
4
 
5
- <div class="m-editor-data-source-fields-footer">
5
+ <div v-if="!isCompare" class="m-editor-data-source-fields-footer">
6
6
  <TMagicButton size="small" type="primary" :disabled="disabled" plain @click="newHandler()">添加</TMagicButton>
7
7
  </div>
8
8
 
@@ -28,7 +28,7 @@
28
28
  </template>
29
29
 
30
30
  <script setup lang="ts">
31
- import { inject, Ref, ref } from 'vue';
31
+ import { computed, inject, Ref, ref } from 'vue';
32
32
 
33
33
  import type { MockSchema } from '@tmagic/core';
34
34
  import { TMagicButton, tMagicMessageBox, TMagicSwitch } from '@tmagic/design';
@@ -54,6 +54,11 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMocksConfig>>(), {
54
54
  const emit = defineEmits(['change']);
55
55
 
56
56
  const { uiService } = useServices();
57
+ const mForm = inject<FormState | undefined>('mForm');
58
+
59
+ /** 对比模式下隐藏新增/编辑/删除等操作按钮,仅保留只读展示。 */
60
+ const isCompare = computed(() => Boolean(mForm?.isCompare));
61
+
57
62
  const width = defineModel<number>('width', { default: 670 });
58
63
 
59
64
  const drawerTitle = ref('');
@@ -202,6 +207,11 @@ const columns: ColumnConfig[] = [
202
207
  },
203
208
  ];
204
209
 
210
+ /** 对比模式下移除「操作」列(编辑/删除按钮),仅保留只读列。 */
211
+ const displayColumns = computed<ColumnConfig[]>(() =>
212
+ isCompare.value ? columns.filter((col) => !col.actions) : columns,
213
+ );
214
+
205
215
  const newHandler = () => {
206
216
  const isFirstRow = props.model[props.name].length === 0;
207
217
  formValues.value = {
@@ -11,7 +11,7 @@
11
11
  @change="changeHandler"
12
12
  ></MSelect>
13
13
 
14
- <TMagicTooltip v-if="model[name] && hasDataSourceSidePanel" :content="notEditable ? '查看' : '编辑'">
14
+ <TMagicTooltip v-if="model[name] && hasDataSourceSidePanel && !isCompare" :content="notEditable ? '查看' : '编辑'">
15
15
  <TMagicButton class="m-fields-select-action-button" :size="size" @click="editHandler"
16
16
  ><MIcon :icon="!notEditable ? Edit : View"></MIcon
17
17
  ></TMagicButton>
@@ -56,6 +56,9 @@ const dataSources = computed(() => dataSourceService.get('dataSources'));
56
56
 
57
57
  const notEditable = computed(() => filterFunction(mForm, props.config.notEditable, props));
58
58
 
59
+ /** 对比模式下隐藏查看/编辑操作按钮,仅保留只读展示。 */
60
+ const isCompare = computed(() => Boolean(mForm?.isCompare));
61
+
59
62
  const hasDataSourceSidePanel = computed(() =>
60
63
  uiService.get('sideBarItems').find((item) => item.$key === SideItemKey.DATA_SOURCE),
61
64
  );
@@ -6,22 +6,32 @@
6
6
  :size="size"
7
7
  :disabled="disabled"
8
8
  :model="model"
9
+ :last-values="lastValues"
10
+ :is-compare="isCompareMode"
9
11
  :config="tableConfig"
10
12
  @change="onChangeHandler"
11
13
  ></MTable>
12
14
 
13
15
  <div v-else class="fullWidth">
14
- <TMagicButton class="create-button" type="primary" :size="size" :disabled="disabled" @click="addEvent()"
16
+ <TMagicButton
17
+ v-if="!isCompareMode"
18
+ class="create-button"
19
+ type="primary"
20
+ :size="size"
21
+ :disabled="disabled"
22
+ @click="addEvent()"
15
23
  >添加事件</TMagicButton
16
24
  >
17
25
  <MPanel
18
- v-for="(cardItem, index) in model[name]"
19
- :key="index"
26
+ v-for="entry in displayList"
27
+ :key="entry.index"
20
28
  :disabled="disabled"
21
29
  :size="size"
22
- :prop="`${prop}.${index}`"
30
+ :prop="`${prop}.${entry.index}`"
23
31
  :config="actionsConfig"
24
- :model="cardItem"
32
+ :model="entry.cardItem"
33
+ :last-values="entry.lastCardItem"
34
+ :is-compare="isCompareMode"
25
35
  :label-width="config.labelWidth || '100px'"
26
36
  @change="onChangeHandler"
27
37
  >
@@ -29,19 +39,22 @@
29
39
  <MFormContainer
30
40
  class="fullWidth"
31
41
  :config="eventNameConfig"
32
- :model="cardItem"
42
+ :model="entry.cardItem"
43
+ :last-values="entry.lastCardItem"
44
+ :is-compare="isCompareMode"
33
45
  :disabled="disabled"
34
46
  :size="size"
35
- :prop="`${prop}.${index}`"
47
+ :prop="`${prop}.${entry.index}`"
36
48
  @change="eventNameChangeHandler"
37
49
  ></MFormContainer>
38
50
  <TMagicButton
51
+ v-if="!isCompareMode"
39
52
  style="color: #f56c6c"
40
53
  link
41
54
  :icon="Delete"
42
55
  :disabled="disabled"
43
56
  :size="size"
44
- @click="removeEvent(Number(index))"
57
+ @click="removeEvent(Number(entry.index))"
45
58
  ></TMagicButton>
46
59
  </template>
47
60
  </MPanel>
@@ -374,6 +387,42 @@ const isOldVersion = computed(() => {
374
387
  return !has(props.model[props.name][0], 'actions');
375
388
  });
376
389
 
390
+ /**
391
+ * 对比模式判定:
392
+ *
393
+ * event-select 内部由「事件列表 + 嵌套子表单」组成,属于复合字段。父级 `MFormContainer` 已将其
394
+ * 归入「自接管对比字段」(见 Container.vue 的 `SELF_DIFF_FIELD_TYPES`),即对比时只渲染一次本组件,
395
+ * 并把当前值 `model` 与历史值 `lastValues` 一并传入,由本组件把 `is-compare`/`lastValues` 透传给
396
+ * 内部的 MPanel / MFormContainer,逐项(事件名、动作)展示前后差异。
397
+ *
398
+ * 仅当存在历史值时才启用对比,避免 lastValues 缺失时退化为「全部新增」的空对比。
399
+ */
400
+ const isCompareMode = computed(() => Boolean(props.isCompare && props.lastValues));
401
+
402
+ /**
403
+ * 待渲染的事件卡片列表。
404
+ *
405
+ * - 非对比模式:直接映射当前事件列表,`lastCardItem` 为空;
406
+ * - 对比模式:按索引对齐当前值与历史值,取两者长度的最大值,使得「新增」(仅当前有)与
407
+ * 「删除」(仅历史有)的事件都能被渲染出来;缺失的一侧用空对象兜底,从而让子级正确高亮差异。
408
+ */
409
+ const displayList = computed<{ cardItem: any; lastCardItem: any; index: number }[]>(() => {
410
+ const current = props.model[props.name] || [];
411
+
412
+ if (!isCompareMode.value) {
413
+ return current.map((cardItem: any, index: number) => ({ cardItem, lastCardItem: undefined, index }));
414
+ }
415
+
416
+ const last = props.lastValues?.[props.name] || [];
417
+ const length = Math.max(current.length, last.length);
418
+
419
+ return Array.from({ length }, (_, index) => ({
420
+ cardItem: current[index] ?? {},
421
+ lastCardItem: last[index] ?? {},
422
+ index,
423
+ }));
424
+ });
425
+
377
426
  // 添加事件
378
427
  const addEvent = () => {
379
428
  const defaultEvent = {
@@ -19,6 +19,7 @@
19
19
  ></TMagicInput>
20
20
 
21
21
  <TMagicButton
22
+ v-if="!isCompare"
22
23
  class="m-fields-key-value-delete"
23
24
  type="danger"
24
25
  :size="size"
@@ -30,7 +31,14 @@
30
31
  ></TMagicButton>
31
32
  </div>
32
33
 
33
- <TMagicButton type="primary" :size="size" :disabled="disabled" plain :icon="Plus" @click="addHandler"
34
+ <TMagicButton
35
+ v-if="!isCompare"
36
+ type="primary"
37
+ :size="size"
38
+ :disabled="disabled"
39
+ plain
40
+ :icon="Plus"
41
+ @click="addHandler"
34
42
  >添加</TMagicButton
35
43
  >
36
44
  </div>
@@ -52,7 +60,7 @@
52
60
  ></MagicCodeEditor>
53
61
 
54
62
  <TMagicButton
55
- v-if="config.advanced"
63
+ v-if="config.advanced && !isCompare"
56
64
  size="default"
57
65
  :disabled="disabled"
58
66
  link
@@ -63,11 +71,11 @@
63
71
  </template>
64
72
 
65
73
  <script setup lang="ts">
66
- import { ref, watchEffect } from 'vue';
74
+ import { computed, inject, ref, watchEffect } from 'vue';
67
75
  import { Delete, Plus } from '@element-plus/icons-vue';
68
76
 
69
77
  import { TMagicButton, TMagicInput } from '@tmagic/design';
70
- import type { FieldProps, KeyValueConfig } from '@tmagic/form';
78
+ import type { FieldProps, FormState, KeyValueConfig } from '@tmagic/form';
71
79
 
72
80
  import CodeIcon from '@editor/icons/CodeIcon.vue';
73
81
  import MagicCodeEditor from '@editor/layouts/CodeEditor.vue';
@@ -84,6 +92,11 @@ const emit = defineEmits<{
84
92
  change: [value: Record<string, any>];
85
93
  }>();
86
94
 
95
+ const mForm = inject<FormState | undefined>('mForm');
96
+
97
+ /** 对比模式下隐藏增删/代码切换等操作按钮,仅保留只读展示。 */
98
+ const isCompare = computed(() => Boolean(mForm?.isCompare));
99
+
87
100
  const records = ref<[string, string][]>([]);
88
101
  const showCode = ref(false);
89
102
 
@@ -6,7 +6,7 @@
6
6
  </div>
7
7
  <div class="m-fields-ui-select" v-else style="display: flex">
8
8
  <template v-if="val">
9
- <TMagicTooltip content="清除" placement="top">
9
+ <TMagicTooltip v-if="!isCompare" content="清除" placement="top">
10
10
  <TMagicButton
11
11
  style="padding: 0"
12
12
  type="danger"
@@ -32,7 +32,7 @@
32
32
  </TMagicTooltip>
33
33
  </template>
34
34
 
35
- <TMagicTooltip v-else content="点击此处选择" placement="top">
35
+ <TMagicTooltip v-else-if="!isCompare" content="点击此处选择" placement="top">
36
36
  <TMagicButton link style="padding: 0; margin: 0" :disabled="disabled" :size="size" @click="startSelect"
37
37
  >点击此处选择</TMagicButton
38
38
  >
@@ -67,6 +67,9 @@ const mForm = inject<FormState>('mForm');
67
67
  const val = computed(() => props.model[props.name]);
68
68
  const uiSelectMode = ref(false);
69
69
 
70
+ /** 对比模式下隐藏清除/选择等操作按钮,仅保留只读展示。 */
71
+ const isCompare = computed(() => Boolean(mForm?.isCompare));
72
+
70
73
  const cancelHandler = () => {
71
74
  uiService.set('uiSelectMode', false);
72
75
  uiSelectMode.value = false;
@@ -3,6 +3,7 @@ import { cloneDeep } from 'lodash-es';
3
3
 
4
4
  import type { CodeBlockContent } from '@tmagic/core';
5
5
  import { tMagicMessage } from '@tmagic/design';
6
+ import type { ContainerChangeEventData } from '@tmagic/form';
6
7
 
7
8
  import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
8
9
  import type { Services } from '@editor/type';
@@ -61,10 +62,12 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
61
62
  codeBlockService.deleteCodeDslByIds([key]);
62
63
  };
63
64
 
64
- const submitCodeBlockHandler = async (values: CodeBlockContent) => {
65
+ const submitCodeBlockHandler = async (values: CodeBlockContent, eventData?: ContainerChangeEventData) => {
65
66
  if (!codeId.value) return;
66
67
 
67
- await codeBlockService.setCodeDslById(codeId.value, values);
68
+ await codeBlockService.setCodeDslById(codeId.value, values, {
69
+ changeRecords: eventData?.changeRecords,
70
+ });
68
71
 
69
72
  codeBlockEditorRef.value?.hide();
70
73
  };
@@ -97,23 +97,39 @@ export const useStage = (stageOptions: StageOptions) => {
97
97
 
98
98
  stage.on('update', (ev: UpdateEventData) => {
99
99
  if (ev.parentEl) {
100
- for (const data of ev.data) {
101
- const id = getIdFromEl()(data.el);
102
- const pId = getIdFromEl()(ev.parentEl);
103
- id && pId && editorService.moveToContainer({ id, style: data.style }, pId);
100
+ // 拖动多选元素到一个新容器:整批合成一次 moveToContainer,只产生一条历史记录
101
+ const pId = getIdFromEl()(ev.parentEl);
102
+ if (!pId) return;
103
+ const configs = ev.data
104
+ .map((data) => {
105
+ const id = getIdFromEl()(data.el);
106
+ if (!id) return null;
107
+ const cfg: MNode = { id, style: data.style };
108
+ return cfg;
109
+ })
110
+ .filter((cfg): cfg is MNode => Boolean(cfg));
111
+ if (configs.length > 0) {
112
+ editorService.moveToContainer(configs, pId);
104
113
  }
105
114
  return;
106
115
  }
107
116
 
108
- // 为每个元素单独更新,确保 changeRecords 与对应的元素关联
117
+ // 多选拖动 / 多选缩放:所有元素整批走一次 update,避免历史栈被切成 N 条
118
+ // changeRecordList 与 configs 同序,每个节点保留自己的 records;
119
+ // 不能把多个节点的 records 合并到同一个数组里,否则 doUpdate / nodeUpdateHandler 会把别的节点的 propPath 当成自己的。
120
+ const configs: MNode[] = [];
121
+ const changeRecordList: ReturnType<typeof buildChangeRecords>[] = [];
109
122
  ev.data.forEach((data) => {
110
123
  const id = getIdFromEl()(data.el);
111
124
  if (!id) return;
112
125
 
113
126
  const { style = {} } = data;
114
-
115
- editorService.update({ id, style }, { changeRecords: buildChangeRecords(style, 'style') });
127
+ configs.push({ id, style });
128
+ changeRecordList.push(buildChangeRecords(style, 'style'));
116
129
  });
130
+ if (configs.length === 0) return;
131
+
132
+ editorService.update(configs, { changeRecordList });
117
133
  });
118
134
 
119
135
  stage.on('sort', (ev: SortEventData) => {
package/src/index.ts CHANGED
@@ -70,6 +70,7 @@ export { default as LayoutContainer } from './components/SplitView.vue';
70
70
  export { default as SplitView } from './components/SplitView.vue';
71
71
  export { default as Resizer } from './components/Resizer.vue';
72
72
  export { default as CodeBlockEditor } from './components/CodeBlockEditor.vue';
73
+ export { default as CompareForm } from './components/CompareForm.vue';
73
74
  export { default as FloatingBox } from './components/FloatingBox.vue';
74
75
  export { default as Tree } from './components/Tree.vue';
75
76
  export { default as TreeNode } from './components/TreeNode.vue';
@@ -330,6 +330,21 @@ watch(
330
330
  },
331
331
  );
332
332
 
333
+ // diff 模式下,对比的"当前值"(modifiedValues)也可能在外部变化(例如 lastValues 不变、当前 model 变了),
334
+ // 此时同样需要重新设置编辑器值,否则右侧编辑器内容会停留在初始化时的快照。
335
+ watch(
336
+ () => props.modifiedValues,
337
+ (v, preV) => {
338
+ if (props.type !== 'diff') return;
339
+ if (v !== preV) {
340
+ setEditorValue(props.initValues, props.modifiedValues);
341
+ }
342
+ },
343
+ {
344
+ deep: true,
345
+ },
346
+ );
347
+
333
348
  watch(
334
349
  () => props.options,
335
350
  (v) => {
@@ -23,15 +23,15 @@
23
23
  left-class="m-editor-framework-left"
24
24
  center-class="m-editor-framework-center"
25
25
  right-class="m-editor-framework-right"
26
- :left="columnWidth.left"
26
+ :left="hideSidebar ? undefined : columnWidth.left"
27
27
  :right="columnWidth.right"
28
- :min-left="MIN_LEFT_COLUMN_WIDTH"
28
+ :min-left="hideSidebar ? 0 : MIN_LEFT_COLUMN_WIDTH"
29
29
  :min-right="MIN_RIGHT_COLUMN_WIDTH"
30
30
  :min-center="MIN_CENTER_COLUMN_WIDTH"
31
31
  :width="frameworkRect.width"
32
32
  @change="columnWidthChange"
33
33
  >
34
- <template #left>
34
+ <template v-if="!hideSidebar" #left>
35
35
  <slot name="sidebar"></slot>
36
36
  </template>
37
37
 
@@ -94,10 +94,12 @@ defineOptions({
94
94
  name: 'MEditorFramework',
95
95
  });
96
96
 
97
- defineProps<{
97
+ const props = defineProps<{
98
98
  disabledPageFragment: boolean;
99
99
  pageBarSortOptions?: PageBarSortOptions;
100
100
  pageFilterFunction?: (_page: MPage | MPageFragment, _keyword: string) => boolean;
101
+ /** 是否隐藏左侧面板 */
102
+ hideSidebar?: boolean;
101
103
  }>();
102
104
 
103
105
  const codeOptions = inject('codeOptions', {});
@@ -132,7 +134,10 @@ watch(
132
134
  );
133
135
 
134
136
  const columnWidthChange = (columnW: GetColumnWidth) => {
135
- storageService.setItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, columnW.left, { protocol: Protocol.NUMBER });
137
+ // 隐藏左侧面板时 left 恒为 0,不覆盖已持久化的左栏宽度,避免重新展示时丢失宽度
138
+ if (!props.hideSidebar) {
139
+ storageService.setItem(LEFT_COLUMN_WIDTH_STORAGE_KEY, columnW.left, { protocol: Protocol.NUMBER });
140
+ }
136
141
  storageService.setItem(RIGHT_COLUMN_WIDTH_STORAGE_KEY, columnW.right, { protocol: Protocol.NUMBER });
137
142
 
138
143
  uiService.set('columnWidth', columnW);
@@ -19,6 +19,7 @@ import { NodeType } from '@tmagic/core';
19
19
  import { useServices } from '@editor/hooks/use-services';
20
20
  import { ColumnLayout, MenuBarData, MenuButton, MenuComponent, MenuItem } from '@editor/type';
21
21
 
22
+ import HistoryListPanel from './history-list/HistoryListPanel.vue';
22
23
  import NavMenuColumn from './NavMenuColumn.vue';
23
24
 
24
25
  defineOptions({
@@ -103,6 +104,14 @@ const getConfig = (item: MenuItem): (MenuButton | MenuComponent)[] => {
103
104
  handler: () => editorService.redo(),
104
105
  });
105
106
  break;
107
+ case 'history-list':
108
+ // 历史记录面板:以 component 形式挂入,自带 popover;点击 nav 上的图标弹出。
109
+ config.push({
110
+ type: 'component',
111
+ className: 'history-list',
112
+ component: markRaw(HistoryListPanel),
113
+ });
114
+ break;
106
115
  case 'zoom-in':
107
116
  config.push({
108
117
  type: 'button',
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div class="m-editor-history-list-bucket">
3
+ <div class="m-editor-history-list-bucket-title">
4
+ <span>{{ title }}</span>
5
+ <code>{{ String(bucketId) }}</code>
6
+ <span class="m-editor-history-list-bucket-count">{{ groups.length }} 组</span>
7
+ </div>
8
+
9
+ <ul class="m-editor-history-list-ul">
10
+ <GroupRow
11
+ v-for="(group, gIdx) in groups"
12
+ :key="`${prefix}-${bucketId}-${gIdx}`"
13
+ :group-key="`${prefix}-${bucketId}-${gIdx}`"
14
+ :applied="group.applied"
15
+ :merged="group.steps.length > 1"
16
+ :op-type="group.opType"
17
+ :desc="describeGroup(group)"
18
+ :step-count="group.steps.length"
19
+ :sub-steps="
20
+ group.steps.map((s: any) => ({
21
+ index: s.index,
22
+ applied: s.applied,
23
+ isCurrent: s.isCurrent,
24
+ desc: describeStep(s.step),
25
+ diffable: isStepDiffable ? isStepDiffable(s.step) : false,
26
+ revertable: s.applied,
27
+ }))
28
+ "
29
+ :is-current="group.isCurrent"
30
+ :expanded="!!expanded[`${prefix}-${bucketId}-${gIdx}`]"
31
+ @toggle="(key: string) => $emit('toggle', key)"
32
+ @goto="(index: number) => $emit('goto', bucketId, index)"
33
+ @diff-step="(index: number) => $emit('diff-step', bucketId, index)"
34
+ @revert-step="(index: number) => $emit('revert-step', bucketId, index)"
35
+ />
36
+ <!--
37
+ 初始状态项:永远位于该 bucket 列表底部(同样按倒序展示,最底部 = 最早状态)。
38
+ 当 bucket 内所有 group 都未 applied 时即为当前位置。
39
+ -->
40
+ <InitialRow :is-current="isInitial" @goto-initial="$emit('goto-initial', bucketId)" />
41
+ </ul>
42
+ </div>
43
+ </template>
44
+
45
+ <script lang="ts" setup>
46
+ import { computed } from 'vue';
47
+
48
+ import type { HistoryOpType } from '@editor/type';
49
+
50
+ import GroupRow from './GroupRow.vue';
51
+ import InitialRow from './InitialRow.vue';
52
+
53
+ defineOptions({
54
+ name: 'MEditorHistoryListBucket',
55
+ });
56
+
57
+ const props = defineProps<{
58
+ /** Bucket 标题,例如 "数据源" / "代码块",渲染在 bucket 头部。 */
59
+ title: string;
60
+ /** 当前 bucket 对应的目标 id(dataSource.id 或 codeBlock.id),同时用于组装子项的 key。 */
61
+ bucketId: string | number;
62
+ /** 子项 key 的命名空间前缀:`ds` 表示数据源,`cb` 表示代码块。与上层折叠状态 key 保持一致。 */
63
+ prefix: 'ds' | 'cb';
64
+ /** 当前 bucket 下的所有历史分组,按时间倒序展示(最近的操作在前)。 */
65
+ groups: {
66
+ applied: boolean;
67
+ isCurrent?: boolean;
68
+ opType: HistoryOpType;
69
+ steps: { index: number; applied: boolean; isCurrent?: boolean; step: any }[];
70
+ }[];
71
+ /** 组级描述文案生成器,接收一个 group,返回展示文本。由父组件按业务类型注入。 */
72
+ describeGroup: (_group: any) => string;
73
+ /** 单步描述文案生成器,接收一个 step,返回展示文本。用于合并组展开后的子步列表。 */
74
+ describeStep: (_step: any) => string;
75
+ /** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
76
+ isStepDiffable?: (_step: any) => boolean;
77
+ /** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
78
+ expanded: Record<string, boolean>;
79
+ }>();
80
+
81
+ defineEmits<{
82
+ /** 透传子组件 GroupRow 的 toggle,由上层 panel 更新 expanded。 */
83
+ (_e: 'toggle', _key: string): void;
84
+ /**
85
+ * 透传子组件 GroupRow 的 goto,并附带当前 bucket 对应的 id(dataSourceId / codeBlockId),
86
+ * 上层据此调用对应 service.goto(id, targetCursor)。
87
+ */
88
+ (_e: 'goto', _bucketId: string | number, _index: number): void;
89
+ /** 用户点击初始项希望该 bucket 回到未修改状态;携带 bucketId 用于上层路由到正确的 service。 */
90
+ (_e: 'goto-initial', _bucketId: string | number): void;
91
+ /** 用户点击"查看差异",携带 bucketId 与 step 索引。 */
92
+ (_e: 'diff-step', _bucketId: string | number, _index: number): void;
93
+ /** 用户点击"回滚"按钮,携带 bucketId 与 step 索引,类 git revert。 */
94
+ (_e: 'revert-step', _bucketId: string | number, _index: number): void;
95
+ }>();
96
+
97
+ /** 该 bucket 是否处于初始状态(栈 cursor=0),等价于全部 group 都未 applied。 */
98
+ const isInitial = computed(() => props.groups.length > 0 && props.groups.every((g) => !g.applied));
99
+ </script>