@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
@@ -1,46 +1,32 @@
1
1
  <template>
2
- <div v-if="!list.length" class="m-editor-history-list-empty">暂无操作记录</div>
3
- <TMagicScrollbar v-else max-height="360px">
4
- <ul class="m-editor-history-list-ul">
5
- <GroupRow
6
- v-for="group in list"
7
- :key="`pg-${group.steps[0]?.index}`"
8
- :group-key="`pg-${group.steps[0]?.index}`"
9
- :applied="group.applied"
10
- :merged="group.steps.length > 1"
11
- :op-type="group.opType"
12
- :desc="describePageGroup(group)"
13
- :source="groupSource(group)"
14
- :time="formatHistoryTime(groupTimestamp(group))"
15
- :time-title="formatHistoryFullTime(groupTimestamp(group))"
16
- :step-count="group.steps.length"
17
- :sub-steps="
18
- group.steps.map((s) => ({
19
- index: s.index,
20
- applied: s.applied,
21
- isCurrent: s.isCurrent,
22
- desc: describePageStep(s.step),
23
- diffable: isPageStepDiffable(s.step),
24
- revertable: s.applied && isPageStepRevertable(s.step),
25
- source: s.step.source,
26
- time: formatHistoryTime(s.step.timestamp),
27
- timeTitle: formatHistoryFullTime(s.step.timestamp),
28
- }))
29
- "
30
- :is-current="group.isCurrent"
31
- :expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
32
- @toggle="(key: string) => $emit('toggle', key)"
33
- @goto="(index: number) => $emit('goto', index)"
34
- @diff-step="(index: number) => $emit('diff-step', index)"
35
- @revert-step="(index: number) => $emit('revert-step', index)"
36
- />
37
- <!--
2
+ <div v-if="!list.length && !marker" class="m-editor-history-list-empty">暂无操作记录</div>
3
+ <template v-else>
4
+ <div v-if="list.length" class="m-editor-history-list-toolbar">
5
+ <span class="m-editor-history-list-clear" title="清空当前页面的历史记录" @click="$emit('clear')">清空</span>
6
+ </div>
7
+ <TMagicScrollbar max-height="360px">
8
+ <ul class="m-editor-history-list-ul">
9
+ <GroupRow
10
+ v-for="group in list"
11
+ :key="rowKey(group)"
12
+ :group="toRow(group)"
13
+ :expanded="isHistoryGroupExpanded(expanded, rowKey(group))"
14
+ :select-enabled="true"
15
+ @toggle="(key: string) => $emit('toggle', key)"
16
+ @goto="(index: number) => $emit('goto', index)"
17
+ @diff-step="(index: number) => $emit('diff-step', index)"
18
+ @revert-step="(index: number) => $emit('revert-step', index)"
19
+ @select="(index: number) => $emit('select', index)"
20
+ />
21
+ <!--
38
22
  初始状态项:永远位于列表底部(页面 tab 倒序展示,最底部=最早),
39
23
  作为"未修改"零点。当所有 group 都未 applied 时它即为当前位置。
24
+ 设置 root 时生成的「未修改的初始状态」标记(marker)会作为该行的文案与时间来源。
40
25
  -->
41
- <InitialRow :is-current="isInitial" @goto-initial="$emit('goto-initial')" />
42
- </ul>
43
- </TMagicScrollbar>
26
+ <InitialRow :is-current="isInitial" :marker="marker" @goto-initial="$emit('goto-initial')" />
27
+ </ul>
28
+ </TMagicScrollbar>
29
+ </template>
44
30
  </template>
45
31
 
46
32
  <script lang="ts" setup>
@@ -48,16 +34,15 @@ import { computed } from 'vue';
48
34
 
49
35
  import { TMagicScrollbar } from '@tmagic/design';
50
36
 
51
- import type { PageHistoryGroup, StepValue } from '@editor/type';
37
+ import type { HistoryRowDescriptor, PageHistoryGroup, StepValue } from '@editor/type';
52
38
 
39
+ import type { HistoryRowGroup } from './composables';
53
40
  import {
54
41
  describePageGroup,
55
42
  describePageStep,
56
- formatHistoryFullTime,
57
- formatHistoryTime,
58
- groupSource,
59
- groupTimestamp,
43
+ isHistoryGroupExpanded,
60
44
  isPageStepRevertable,
45
+ toRowGroup,
61
46
  } from './composables';
62
47
  import GroupRow from './GroupRow.vue';
63
48
  import InitialRow from './InitialRow.vue';
@@ -70,11 +55,16 @@ const props = defineProps<{
70
55
  /** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
71
56
  list: PageHistoryGroup[];
72
57
  /**
73
- * 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
58
+ * 共享的折叠状态表(key -> 是否展开,缺省或 true 为展开、false 为收起),由顶层 panel 统一维护。
74
59
  * 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
75
60
  * 这样历史数据更新(新增 / 撤销重做导致列表顺序变化)后,已展开的分组状态仍能正确保持。
76
61
  */
77
62
  expanded: Record<string, boolean>;
63
+ /**
64
+ * 当前活动页的「加载/初始」基线记录(设置 root 时生成的 `opType: 'initial'` StepValue)。
65
+ * 提供时即使没有任何操作记录也会展示底部初始行,并用其文案 / 时间渲染。
66
+ */
67
+ marker?: StepValue;
78
68
  }>();
79
69
 
80
70
  defineEmits<{
@@ -88,25 +78,41 @@ defineEmits<{
88
78
  (_e: 'diff-step', _index: number): void;
89
79
  /** 用户点击"回滚"按钮,携带目标 step 在栈中的索引,类 git revert。 */
90
80
  (_e: 'revert-step', _index: number): void;
81
+ /** 用户点击记录行希望选中对应节点,携带目标 step 在栈中的索引。 */
82
+ (_e: 'select', _index: number): void;
83
+ /** 用户点击"清空"按钮,请求清空当前页面的历史记录(由上层弹窗二次确认后执行)。 */
84
+ (_e: 'clear'): void;
91
85
  }>();
92
86
 
93
87
  /**
94
88
  * 当前 step 是否可查看差异:
95
89
  * - 仅 update 操作;
96
- * - 单节点更新(updatedItems.length === 1),且 oldNode / newNode 都存在。
90
+ * - 单节点更新(diff.length === 1),且 oldSchema / newSchema 都存在。
97
91
  * 多节点更新难以选定单一对比目标,统一不展示差异入口。
98
92
  */
99
93
  const isPageStepDiffable = (step: StepValue): boolean => {
100
94
  if (step.opType !== 'update') return false;
101
- const items = step.updatedItems ?? [];
95
+ const items = step.diff ?? [];
102
96
  if (items.length !== 1) return false;
103
- return Boolean(items[0]?.oldNode && items[0]?.newNode);
97
+ return Boolean(items[0]?.oldSchema && items[0]?.newSchema);
98
+ };
99
+
100
+ /** 页面历史的描述 / 可操作性判定集合,注入给统一的 `toRowGroup`。 */
101
+ const descriptor: HistoryRowDescriptor<StepValue> = {
102
+ describeGroup: describePageGroup,
103
+ describeStep: describePageStep,
104
+ isStepDiffable: isPageStepDiffable,
105
+ isStepRevertable: isPageStepRevertable,
104
106
  };
105
107
 
108
+ const rowKey = (group: PageHistoryGroup) => `pg-${group.steps[0]?.index}`;
109
+
110
+ const toRow = (group: PageHistoryGroup): HistoryRowGroup => toRowGroup(group, rowKey(group), descriptor);
111
+
106
112
  /**
107
113
  * 是否处于"初始状态"——即对应页面历史栈 cursor===0:
108
- * 当 list 中所有 group 的 applied 都为 false 时即为该状态。
109
- * 没有任何 group 的情况由外层"暂无操作记录"分支兜底,本计算可以不考虑。
114
+ * 当 list 中所有 group 的 applied 都为 false 时即为该状态(空列表 `every` 返回 true,
115
+ * 即仅有 marker、无任何操作记录时也视为处于初始状态)。
110
116
  */
111
- const isInitial = computed(() => props.list.length > 0 && props.list.every((g) => !g.applied));
117
+ const isInitial = computed(() => props.list.every((g) => !g.applied));
112
118
  </script>
@@ -4,16 +4,85 @@ import { datetimeFormatter } from '@tmagic/form';
4
4
 
5
5
  import { useServices } from '@editor/hooks/use-services';
6
6
  import type {
7
+ BaseStepValue,
7
8
  CodeBlockHistoryGroup,
8
9
  CodeBlockStepValue,
9
10
  DataSourceHistoryGroup,
10
11
  DataSourceStepValue,
11
12
  HistoryOpSource,
12
13
  HistoryOpType,
14
+ HistoryRowDescriptor,
13
15
  PageHistoryGroup,
14
16
  StepValue,
15
17
  } from '@editor/type';
16
18
 
19
+ /**
20
+ * 通用 bucket 分组(数据源 / 代码块及业务自定义历史)在面板中的展示结构。
21
+ * 由 Bucket / BucketTab 复用,step 类型通过泛型 T 收窄(约束为 {@link BaseStepValue})。
22
+ */
23
+ export interface HistoryBucketGroup<T extends BaseStepValue = BaseStepValue> {
24
+ /** 组内最后一步是否已应用 */
25
+ applied: boolean;
26
+ /** 是否为当前所在的分组 */
27
+ isCurrent?: boolean;
28
+ /** 该分组的操作类型 */
29
+ opType: HistoryOpType;
30
+ /** 组内所有步骤 */
31
+ steps: { index: number; applied: boolean; isCurrent?: boolean; step: T }[];
32
+ }
33
+
34
+ /** GroupRow 渲染所需的单个子步视图模型(已由 {@link toRowGroup} 预先派生,组件内部不再触碰原始 step)。 */
35
+ export interface HistoryRowStep {
36
+ /** 该子步在所属栈中的稳定索引。 */
37
+ index: number;
38
+ /** 是否已应用(false 表示已被 undo,UI 灰态)。 */
39
+ applied: boolean;
40
+ /** 是否为当前所在步骤。 */
41
+ isCurrent?: boolean;
42
+ /** 是否为最近一次保存的记录。 */
43
+ saved?: boolean;
44
+ /** 子步描述文案。 */
45
+ desc: string;
46
+ /** 是否可查看差异。 */
47
+ diffable?: boolean;
48
+ /** 是否可回滚。 */
49
+ revertable?: boolean;
50
+ /** 操作途径。 */
51
+ source?: HistoryOpSource;
52
+ /** 时间文案。 */
53
+ time?: string;
54
+ /** 时间的完整 title 提示。 */
55
+ timeTitle?: string;
56
+ }
57
+
58
+ /**
59
+ * GroupRow 渲染所需的整组视图模型(由 {@link toRowGroup} 统一派生)。
60
+ * 把原先 GroupRow 上十多个扁平 props 收敛为单一对象,header 信息与子步列表一并携带。
61
+ */
62
+ export interface HistoryRowGroup {
63
+ /** 分组的稳定 key,作为 toggle 事件 payload 与折叠状态的索引。 */
64
+ key: string;
65
+ /** 组内最后一步是否已应用。 */
66
+ applied: boolean;
67
+ /** 是否为当前所在分组。 */
68
+ isCurrent: boolean;
69
+ /** 操作类型,用于徽标颜色与文案。 */
70
+ opType: HistoryOpType;
71
+ /** 组整体描述文案。 */
72
+ desc: string;
73
+ /** 组的操作途径(取组内最近一步)。 */
74
+ source?: HistoryOpSource;
75
+ /** 组头部时间文案(取组内最近一步)。 */
76
+ time?: string;
77
+ /** 组头部时间的完整 title 提示。 */
78
+ timeTitle?: string;
79
+ /** 子步列表(时间正序);其长度即合并步数,length > 1 即为合并组。 */
80
+ subSteps: HistoryRowStep[];
81
+ }
82
+
83
+ /** 合并组默认展开;仅当 expanded[key] === false 时为收起。 */
84
+ export const isHistoryGroupExpanded = (expanded: Record<string, boolean>, key: string) => expanded[key] !== false;
85
+
17
86
  /**
18
87
  * 历史记录面板共享逻辑:
19
88
  * - 暴露三类历史的聚合数据(页面 / 数据源 / 代码块);
@@ -28,10 +97,11 @@ export const useHistoryList = () => {
28
97
  /**
29
98
  * 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。
30
99
  * 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。
100
+ * 合并组默认展开;仅当值为 `false` 时表示收起。
31
101
  */
32
102
  const expanded = reactive<Record<string, boolean>>({});
33
103
  const toggleGroup = (key: string) => {
34
- expanded[key] = !expanded[key];
104
+ expanded[key] = expanded[key] === false;
35
105
  };
36
106
 
37
107
  const pageGroups = computed(() => historyService.getPageHistoryGroups());
@@ -115,6 +185,7 @@ const HISTORY_SOURCE_LABELS: Record<string, string> = {
115
185
  'component-panel': '组件面板',
116
186
  props: '配置面板',
117
187
  code: '源码',
188
+ 'root-code': 'DSL源码',
118
189
  'stage-contextmenu': '画布菜单',
119
190
  'tree-contextmenu': '树菜单',
120
191
  toolbar: '工具栏',
@@ -122,6 +193,8 @@ const HISTORY_SOURCE_LABELS: Record<string, string> = {
122
193
  rollback: '回滚',
123
194
  api: '接口',
124
195
  ai: 'AI',
196
+ initial: '初始值',
197
+ sync: '同步',
125
198
  unknown: '未知',
126
199
  };
127
200
 
@@ -134,7 +207,51 @@ export const sourceLabel = (source: HistoryOpSource = 'unknown'): string => {
134
207
  export const groupSource = (group: { steps: { step: { source?: HistoryOpSource } }[] }): HistoryOpSource | undefined =>
135
208
  group.steps[group.steps.length - 1]?.step.source;
136
209
 
137
- const nameOf = (node: { name?: string; id?: string | number; type?: string }) =>
210
+ /** {@link toRowGroup} 接受的最小分组结构,PageHistoryGroup HistoryBucketGroup 均满足。 */
211
+ interface RowGroupInput<T extends BaseStepValue = BaseStepValue> {
212
+ applied: boolean;
213
+ isCurrent?: boolean;
214
+ opType: HistoryOpType;
215
+ steps: { index: number; applied: boolean; isCurrent?: boolean; step: T }[];
216
+ }
217
+
218
+ /**
219
+ * 把一个历史分组(页面 / bucket)派生为 GroupRow 直接消费的视图模型 {@link HistoryRowGroup}。
220
+ * 统一了原先 PageTab / Bucket 各自内联的 sub-steps 映射逻辑:描述、可差异、可回滚、时间、途径
221
+ * 全部在此一次性算好,组件层只负责渲染。
222
+ */
223
+ export const toRowGroup = <T extends BaseStepValue = BaseStepValue>(
224
+ group: RowGroupInput<T>,
225
+ key: string,
226
+ descriptor: HistoryRowDescriptor<T>,
227
+ ): HistoryRowGroup => {
228
+ const { describeGroup, describeStep, isStepDiffable, isStepRevertable } = descriptor;
229
+ const timestamp = groupTimestamp(group);
230
+ return {
231
+ key,
232
+ applied: group.applied,
233
+ isCurrent: Boolean(group.isCurrent),
234
+ opType: group.opType,
235
+ desc: describeGroup(group),
236
+ source: groupSource(group),
237
+ time: formatHistoryTime(timestamp),
238
+ timeTitle: formatHistoryFullTime(timestamp),
239
+ subSteps: group.steps.map((s) => ({
240
+ index: s.index,
241
+ applied: s.applied,
242
+ isCurrent: s.isCurrent,
243
+ saved: s.step.saved,
244
+ desc: describeStep(s.step),
245
+ diffable: isStepDiffable ? isStepDiffable(s.step) : false,
246
+ revertable: s.applied && (isStepRevertable ? isStepRevertable(s.step) : true),
247
+ source: s.step.source,
248
+ time: formatHistoryTime(s.step.timestamp),
249
+ timeTitle: formatHistoryFullTime(s.step.timestamp),
250
+ })),
251
+ };
252
+ };
253
+
254
+ const nameOf = (node?: { name?: string; id?: string | number; type?: string }) =>
138
255
  node?.name || node?.type || `${node?.id ?? ''}`;
139
256
 
140
257
  /**
@@ -159,25 +276,25 @@ const pickLastDescription = (descs: (string | undefined)[]): string | undefined
159
276
  export const describePageStep = (step: StepValue) => {
160
277
  if (step.historyDescription) return step.historyDescription;
161
278
  const { opType } = step;
279
+ const items = step.diff ?? [];
162
280
  if (opType === 'add') {
163
- const count = step.nodes?.length ?? 0;
164
- const node = step.nodes?.[0];
281
+ const count = items.length;
282
+ const node = items[0]?.newSchema;
165
283
  return `新增 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
166
284
  }
167
285
  if (opType === 'remove') {
168
- const count = step.removedItems?.length ?? 0;
169
- const node = step.removedItems?.[0]?.node;
286
+ const count = items.length;
287
+ const node = items[0]?.oldSchema;
170
288
  return `删除 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
171
289
  }
172
- const updated = step.updatedItems ?? [];
173
- if (!updated.length) return '修改节点';
174
- if (updated.length === 1) {
175
- const { newNode, changeRecords } = updated[0];
290
+ if (!items.length) return '修改节点';
291
+ if (items.length === 1) {
292
+ const { newSchema, changeRecords } = items[0];
176
293
  const propPath = changeRecords?.[0]?.propPath;
177
- const target = labelWithId(nameOf(newNode), newNode?.id);
294
+ const target = labelWithId(nameOf(newSchema), newSchema?.id);
178
295
  return `修改 ${target}${propPath ? ` · ${propPath}` : ''}`;
179
296
  }
180
- return `修改 ${updated.length} 个节点`;
297
+ return `修改 ${items.length} 个节点`;
181
298
  };
182
299
 
183
300
  /**
@@ -192,7 +309,7 @@ export const describePageGroup = (group: PageHistoryGroup) => {
192
309
  if (group.steps.length === 1) return describePageStep(group.steps[0].step);
193
310
  const paths = new Set<string>();
194
311
  group.steps.forEach((s) => {
195
- s.step.updatedItems?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
312
+ s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
196
313
  });
197
314
  const pathList = Array.from(paths).slice(0, 3).join(', ');
198
315
  const target = labelWithId(
@@ -204,12 +321,11 @@ export const describePageGroup = (group: PageHistoryGroup) => {
204
321
 
205
322
  export const describeDataSourceStep = (step: DataSourceStepValue) => {
206
323
  if (step.historyDescription) return step.historyDescription;
207
- if (step.oldSchema === null && step.newSchema)
208
- return `创建 ${labelWithId(step.newSchema.title, step.newSchema.id ?? step.id)}`;
209
- if (step.newSchema === null && step.oldSchema)
210
- return `删除 ${labelWithId(step.oldSchema.title, step.oldSchema.id ?? step.id)}`;
211
- const propPath = step.changeRecords?.[0]?.propPath;
212
- const title = labelWithId(step.newSchema?.title || step.oldSchema?.title, step.id);
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);
213
329
  return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
214
330
  };
215
331
 
@@ -219,22 +335,23 @@ export const describeDataSourceGroup = (group: DataSourceHistoryGroup) => {
219
335
  if (group.steps.length === 1) return describeDataSourceStep(group.steps[0].step);
220
336
  const paths = new Set<string>();
221
337
  group.steps.forEach((s) => {
222
- s.step.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
338
+ s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
223
339
  });
224
340
  const pathList = Array.from(paths).slice(0, 3).join(', ');
225
- const rawTitle = group.steps[group.steps.length - 1].step.newSchema?.title || group.steps[0].step.oldSchema?.title;
341
+ const rawTitle =
342
+ group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.title ||
343
+ group.steps[0].step.diff?.[0]?.oldSchema?.title;
226
344
  const target = labelWithId(rawTitle, group.id);
227
345
  return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
228
346
  };
229
347
 
230
348
  export const describeCodeBlockStep = (step: CodeBlockStepValue) => {
231
349
  if (step.historyDescription) return step.historyDescription;
232
- if (step.oldContent === null && step.newContent)
233
- return `创建 ${labelWithId(step.newContent.name, step.newContent.id ?? step.id)}`;
234
- if (step.newContent === null && step.oldContent)
235
- return `删除 ${labelWithId(step.oldContent.name, step.oldContent.id ?? step.id)}`;
236
- const propPath = step.changeRecords?.[0]?.propPath;
237
- const title = labelWithId(step.newContent?.name || step.oldContent?.name, step.id);
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);
238
355
  return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
239
356
  };
240
357
 
@@ -244,10 +361,12 @@ export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
244
361
  if (group.steps.length === 1) return describeCodeBlockStep(group.steps[0].step);
245
362
  const paths = new Set<string>();
246
363
  group.steps.forEach((s) => {
247
- s.step.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
364
+ s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
248
365
  });
249
366
  const pathList = Array.from(paths).slice(0, 3).join(', ');
250
- const rawName = group.steps[group.steps.length - 1].step.newContent?.name || group.steps[0].step.oldContent?.name;
367
+ const rawName =
368
+ group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.name ||
369
+ group.steps[0].step.diff?.[0]?.oldSchema?.name;
251
370
  const target = labelWithId(rawName, group.id);
252
371
  return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
253
372
  };
@@ -260,27 +379,29 @@ export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
260
379
  */
261
380
  export const isPageStepRevertable = (step: StepValue): boolean => {
262
381
  if (step.opType !== 'update') return true;
263
- const items = step.updatedItems ?? [];
382
+ const items = step.diff ?? [];
264
383
  if (!items.length) return false;
265
384
  return items.every((item) => Boolean(item.changeRecords?.length));
266
385
  };
267
386
 
268
387
  /**
269
388
  * 数据源 step 是否支持「回滚」:
270
- * - 新增(oldSchema=null)/ 删除(newSchema=null):不依赖 changeRecords,始终可回滚;
389
+ * - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
271
390
  * - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
272
391
  */
273
392
  export const isDataSourceStepRevertable = (step: DataSourceStepValue): boolean => {
274
- if (step.oldSchema === null || step.newSchema === null) return true;
275
- return Boolean(step.changeRecords?.length);
393
+ const item = step.diff?.[0];
394
+ if (!item?.oldSchema || !item?.newSchema) return true;
395
+ return Boolean(item.changeRecords?.length);
276
396
  };
277
397
 
278
398
  /**
279
399
  * 代码块 step 是否支持「回滚」:
280
- * - 新增(oldContent=null)/ 删除(newContent=null):不依赖 changeRecords,始终可回滚;
400
+ * - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
281
401
  * - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
282
402
  */
283
403
  export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
284
- if (step.oldContent === null || step.newContent === null) return true;
285
- return Boolean(step.changeRecords?.length);
404
+ const item = step.diff?.[0];
405
+ if (!item?.oldSchema || !item?.newSchema) return true;
406
+ return Boolean(item.changeRecords?.length);
286
407
  };
@@ -51,6 +51,7 @@
51
51
  type: 'button',
52
52
  text: '删除',
53
53
  icon: Delete,
54
+ buttonProps: { type: 'danger' },
54
55
  handler: () => remove(item),
55
56
  }"
56
57
  ></ToolButton>
@@ -72,7 +73,7 @@ import { computed, ref, useTemplateRef, watch } from 'vue';
72
73
  import { CaretBottom, Delete, DocumentCopy } from '@element-plus/icons-vue';
73
74
 
74
75
  import { type Id, type MPage, type MPageFragment, NodeType } from '@tmagic/core';
75
- import { TMagicIcon, TMagicPopover } from '@tmagic/design';
76
+ import { TMagicIcon, tMagicMessageBox, TMagicPopover } from '@tmagic/design';
76
77
 
77
78
  import ToolButton from '@editor/components/ToolButton.vue';
78
79
  import { useServices } from '@editor/hooks/use-services';
@@ -140,7 +141,8 @@ const copy = (node: MPage | MPageFragment) => {
140
141
  });
141
142
  };
142
143
 
143
- const remove = (node: MPage | MPageFragment) => {
144
+ const remove = async (node: MPage | MPageFragment) => {
145
+ await tMagicMessageBox.confirm('确定删除该页面吗?');
144
146
  editorService.remove(node);
145
147
  };
146
148
 
@@ -239,7 +239,12 @@ const unWatchEditorContentHeight = watch(
239
239
  },
240
240
  );
241
241
 
242
- const activeTabName = ref(props.data?.status);
242
+ const activeTabName = computed<string>({
243
+ get: () => uiService.get('sideBarActiveTabName'),
244
+ set: (value) => uiService.set('sideBarActiveTabName', value),
245
+ });
246
+
247
+ uiService.set('sideBarActiveTabName', props.data?.status || '');
243
248
 
244
249
  const getItemConfig = (data: SideItem): SideComponent => {
245
250
  const map: Record<string, SideComponent> = {
@@ -22,7 +22,7 @@
22
22
  </template>
23
23
 
24
24
  <script setup lang="ts">
25
- import { inject, nextTick, Ref, ref, watch, watchEffect } from 'vue';
25
+ import { computed, inject, nextTick, provide, Ref, ref, watch, watchEffect } from 'vue';
26
26
 
27
27
  import type { DataSourceSchema } from '@tmagic/core';
28
28
  import { tMagicMessage } from '@tmagic/design';
@@ -41,6 +41,10 @@ const props = defineProps<{
41
41
  title?: string;
42
42
  values: any;
43
43
  disabled: boolean;
44
+ /** 打开后需要直接定位并打开的方法名,传入时默认激活「方法定义」tab */
45
+ editMethodName?: string;
46
+ /** 打开后需要直接定位并打开的字段路径,传入时默认激活「数据定义」tab */
47
+ editFieldPath?: string[];
44
48
  }>();
45
49
 
46
50
  const boxVisible = defineModel<boolean>('visible', { default: false });
@@ -62,9 +66,33 @@ const { height: editorHeight } = useEditorContentHeight();
62
66
  const parentFloating = inject<Ref<HTMLDivElement | null>>('parentFloating', ref(null));
63
67
  const { boxPosition, calcBoxPosition } = useNextFloatBoxPosition(uiService, parentFloating);
64
68
 
69
+ /** 供「方法定义」tab 内的字段消费,用于打开数据源详情后自动打开指定方法 */
70
+ provide(
71
+ 'editingDataSourceMethodName',
72
+ computed(() => props.editMethodName),
73
+ );
74
+
75
+ /** 供「数据定义」tab 内的字段消费,用于打开数据源详情后自动打开指定字段 */
76
+ provide(
77
+ 'editingDataSourceFieldPath',
78
+ computed(() => props.editFieldPath || []),
79
+ );
80
+
65
81
  watchEffect(() => {
66
82
  initValues.value = props.values;
67
- dataSourceConfig.value = dataSourceService.getFormConfig(initValues.value.type);
83
+ const config = dataSourceService.getFormConfig(initValues.value.type);
84
+
85
+ // 传入方法名/字段路径时,将外层 tab 容器默认激活到对应 tab(status: methods / fields)
86
+ let activeTab = '';
87
+ if (props.editMethodName) {
88
+ activeTab = 'methods';
89
+ } else if (props.editFieldPath?.length) {
90
+ activeTab = 'fields';
91
+ }
92
+
93
+ dataSourceConfig.value = activeTab
94
+ ? config.map((item) => ((item as { type?: string }).type === 'tab' ? { ...item, active: activeTab } : item))
95
+ : config;
68
96
  });
69
97
 
70
98
  const submitHandler = (values: any, data: ContainerChangeEventData) => {
@@ -29,6 +29,8 @@
29
29
  :disabled="!editable"
30
30
  :values="dataSourceValues"
31
31
  :title="dialogTitle"
32
+ :edit-method-name="editMethodName"
33
+ :edit-field-path="editFieldPath"
32
34
  @submit="submitDataSourceHandler"
33
35
  @close="editDialogCloseHandler"
34
36
  ></DataSourceConfigPanel>
@@ -45,7 +47,7 @@
45
47
  </template>
46
48
 
47
49
  <script setup lang="ts">
48
- import { computed, inject, useTemplateRef, watch } from 'vue';
50
+ import { computed, inject, ref, useTemplateRef, watch } from 'vue';
49
51
  import { mergeWith } from 'lodash-es';
50
52
 
51
53
  import { tMagicMessageBox, TMagicScrollbar } from '@tmagic/design';
@@ -79,7 +81,16 @@ const { dataSourceService } = useServices();
79
81
  const { editDialog, dataSourceValues, dialogTitle, editable, editHandler, submitDataSourceHandler } =
80
82
  useDataSourceEdit(dataSourceService);
81
83
 
84
+ /** 打开数据源详情时需要直接定位并打开的方法名,为空则正常展示「数据定义」tab */
85
+ const editMethodName = ref('');
86
+
87
+ /** 打开数据源详情时需要直接定位并打开的字段路径,为空则不自动打开字段配置 */
88
+ const editFieldPath = ref<string[]>([]);
89
+
82
90
  const editDialogCloseHandler = () => {
91
+ editMethodName.value = '';
92
+ editFieldPath.value = [];
93
+
83
94
  if (dataSourceListRef.value) {
84
95
  for (const [, status] of dataSourceListRef.value.nodeStatusMap.entries()) {
85
96
  status.selected = false;
@@ -139,6 +150,20 @@ const filterTextChangeHandler = (val: string) => {
139
150
  };
140
151
 
141
152
  eventBus?.on('edit-data-source', (id: string) => {
153
+ editMethodName.value = '';
154
+ editFieldPath.value = [];
155
+ editHandler(id);
156
+ });
157
+
158
+ eventBus?.on('edit-data-source-method', (id: string, methodName: string) => {
159
+ editMethodName.value = methodName;
160
+ editFieldPath.value = [];
161
+ editHandler(id);
162
+ });
163
+
164
+ eventBus?.on('edit-data-source-field', (id: string, fieldPath: string[]) => {
165
+ editMethodName.value = '';
166
+ editFieldPath.value = fieldPath;
142
167
  editHandler(id);
143
168
  });
144
169