@tmagic/editor 1.8.0-beta.3 → 1.8.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +15 -1
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/es/hooks/use-code-block-edit.js +6 -3
- package/dist/es/hooks/use-data-source-edit.js +5 -2
- package/dist/es/hooks/use-stage.js +6 -3
- package/dist/es/index.js +6 -3
- package/dist/es/initService.js +1 -1
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +19 -51
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +22 -36
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +152 -93
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +36 -10
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +334 -208
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +28 -7
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +47 -57
- package/dist/es/layouts/history-list/composables.js +118 -25
- package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/code-block/useContentMenu.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/data-source/useContentMenu.js +1 -1
- package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +5 -5
- package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/es/services/codeBlock.js +105 -42
- package/dist/es/services/dataSource.js +85 -35
- package/dist/es/services/editor.js +317 -122
- package/dist/es/services/history.js +270 -204
- package/dist/es/services/keybinding.js +3 -3
- package/dist/es/services/ui.js +3 -0
- package/dist/es/style.css +65 -10
- package/dist/es/utils/content-menu.js +17 -9
- package/dist/es/utils/editor.js +35 -1
- package/dist/es/utils/history.js +223 -0
- package/dist/es/utils/indexed-db.js +86 -0
- package/dist/es/utils/undo-redo.js +60 -1
- package/dist/style.css +65 -10
- package/dist/tmagic-editor.umd.cjs +2047 -954
- package/package.json +7 -7
- package/src/components/CompareForm.vue +21 -1
- package/src/components/ToolButton.vue +2 -2
- package/src/hooks/use-code-block-edit.ts +4 -3
- package/src/hooks/use-data-source-edit.ts +2 -2
- package/src/hooks/use-stage.ts +3 -3
- package/src/index.ts +1 -0
- package/src/initService.ts +1 -1
- package/src/layouts/Framework.vue +1 -1
- package/src/layouts/NavMenu.vue +1 -1
- package/src/layouts/history-list/Bucket.vue +34 -67
- package/src/layouts/history-list/BucketTab.vue +46 -51
- package/src/layouts/history-list/GroupRow.vue +150 -98
- package/src/layouts/history-list/HistoryDiffDialog.vue +96 -69
- package/src/layouts/history-list/HistoryListPanel.vue +298 -111
- package/src/layouts/history-list/InitialRow.vue +25 -9
- package/src/layouts/history-list/PageTab.vue +58 -48
- package/src/layouts/history-list/composables.ts +210 -29
- package/src/layouts/page-bar/PageBar.vue +4 -2
- package/src/layouts/props-panel/PropsPanel.vue +5 -1
- package/src/layouts/sidebar/ComponentListPanel.vue +9 -5
- package/src/layouts/sidebar/Sidebar.vue +6 -1
- package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
- package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +1 -1
- package/src/layouts/sidebar/code-block/useContentMenu.ts +1 -1
- package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +1 -1
- package/src/layouts/sidebar/data-source/useContentMenu.ts +1 -1
- package/src/layouts/sidebar/layer/LayerMenu.vue +19 -11
- package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
- package/src/layouts/workspace/viewer/Stage.vue +1 -1
- package/src/layouts/workspace/viewer/ViewerMenu.vue +8 -8
- package/src/services/codeBlock.ts +129 -35
- package/src/services/dataSource.ts +105 -34
- package/src/services/editor.ts +470 -157
- package/src/services/history.ts +312 -203
- package/src/services/keybinding.ts +3 -3
- package/src/services/ui.ts +7 -0
- package/src/theme/history-list-panel.scss +87 -6
- package/src/theme/page-bar.scss +0 -4
- package/src/theme/props-panel.scss +3 -3
- package/src/type.ts +221 -61
- package/src/utils/content-menu.ts +18 -9
- package/src/utils/editor.ts +41 -1
- package/src/utils/history.ts +298 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/indexed-db.ts +122 -0
- package/src/utils/undo-redo.ts +88 -0
- package/types/index.d.ts +871 -308
|
@@ -1,44 +1,32 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="!list.length" class="m-editor-history-list-empty">暂无操作记录</div>
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
diffable: isPageStepDiffable(s.step),
|
|
23
|
-
revertable: s.applied,
|
|
24
|
-
time: formatHistoryTime(s.step.timestamp),
|
|
25
|
-
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
|
26
|
-
}))
|
|
27
|
-
"
|
|
28
|
-
:is-current="group.isCurrent"
|
|
29
|
-
:expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
|
|
30
|
-
@toggle="(key: string) => $emit('toggle', key)"
|
|
31
|
-
@goto="(index: number) => $emit('goto', index)"
|
|
32
|
-
@diff-step="(index: number) => $emit('diff-step', index)"
|
|
33
|
-
@revert-step="(index: number) => $emit('revert-step', index)"
|
|
34
|
-
/>
|
|
35
|
-
<!--
|
|
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
|
+
<!--
|
|
36
22
|
初始状态项:永远位于列表底部(页面 tab 倒序展示,最底部=最早),
|
|
37
23
|
作为"未修改"零点。当所有 group 都未 applied 时它即为当前位置。
|
|
24
|
+
设置 root 时生成的「未修改的初始状态」标记(marker)会作为该行的文案与时间来源。
|
|
38
25
|
-->
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
26
|
+
<InitialRow :is-current="isInitial" :marker="marker" @goto-initial="$emit('goto-initial')" />
|
|
27
|
+
</ul>
|
|
28
|
+
</TMagicScrollbar>
|
|
29
|
+
</template>
|
|
42
30
|
</template>
|
|
43
31
|
|
|
44
32
|
<script lang="ts" setup>
|
|
@@ -46,14 +34,15 @@ import { computed } from 'vue';
|
|
|
46
34
|
|
|
47
35
|
import { TMagicScrollbar } from '@tmagic/design';
|
|
48
36
|
|
|
49
|
-
import type { PageHistoryGroup, StepValue } from '@editor/type';
|
|
37
|
+
import type { HistoryRowDescriptor, PageHistoryGroup, StepValue } from '@editor/type';
|
|
50
38
|
|
|
39
|
+
import type { HistoryRowGroup } from './composables';
|
|
51
40
|
import {
|
|
52
41
|
describePageGroup,
|
|
53
42
|
describePageStep,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
isHistoryGroupExpanded,
|
|
44
|
+
isPageStepRevertable,
|
|
45
|
+
toRowGroup,
|
|
57
46
|
} from './composables';
|
|
58
47
|
import GroupRow from './GroupRow.vue';
|
|
59
48
|
import InitialRow from './InitialRow.vue';
|
|
@@ -66,11 +55,16 @@ const props = defineProps<{
|
|
|
66
55
|
/** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
|
|
67
56
|
list: PageHistoryGroup[];
|
|
68
57
|
/**
|
|
69
|
-
* 共享的折叠状态表(key ->
|
|
58
|
+
* 共享的折叠状态表(key -> 是否展开,缺省或 true 为展开、false 为收起),由顶层 panel 统一维护。
|
|
70
59
|
* 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
|
71
60
|
* 这样历史数据更新(新增 / 撤销重做导致列表顺序变化)后,已展开的分组状态仍能正确保持。
|
|
72
61
|
*/
|
|
73
62
|
expanded: Record<string, boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* 当前活动页的「加载/初始」基线记录(设置 root 时生成的 `opType: 'initial'` StepValue)。
|
|
65
|
+
* 提供时即使没有任何操作记录也会展示底部初始行,并用其文案 / 时间渲染。
|
|
66
|
+
*/
|
|
67
|
+
marker?: StepValue;
|
|
74
68
|
}>();
|
|
75
69
|
|
|
76
70
|
defineEmits<{
|
|
@@ -84,25 +78,41 @@ defineEmits<{
|
|
|
84
78
|
(_e: 'diff-step', _index: number): void;
|
|
85
79
|
/** 用户点击"回滚"按钮,携带目标 step 在栈中的索引,类 git revert。 */
|
|
86
80
|
(_e: 'revert-step', _index: number): void;
|
|
81
|
+
/** 用户点击记录行希望选中对应节点,携带目标 step 在栈中的索引。 */
|
|
82
|
+
(_e: 'select', _index: number): void;
|
|
83
|
+
/** 用户点击"清空"按钮,请求清空当前页面的历史记录(由上层弹窗二次确认后执行)。 */
|
|
84
|
+
(_e: 'clear'): void;
|
|
87
85
|
}>();
|
|
88
86
|
|
|
89
87
|
/**
|
|
90
88
|
* 当前 step 是否可查看差异:
|
|
91
89
|
* - 仅 update 操作;
|
|
92
|
-
* - 单节点更新(
|
|
90
|
+
* - 单节点更新(diff.length === 1),且 oldSchema / newSchema 都存在。
|
|
93
91
|
* 多节点更新难以选定单一对比目标,统一不展示差异入口。
|
|
94
92
|
*/
|
|
95
93
|
const isPageStepDiffable = (step: StepValue): boolean => {
|
|
96
94
|
if (step.opType !== 'update') return false;
|
|
97
|
-
const items = step.
|
|
95
|
+
const items = step.diff ?? [];
|
|
98
96
|
if (items.length !== 1) return false;
|
|
99
|
-
return Boolean(items[0]?.
|
|
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,
|
|
100
106
|
};
|
|
101
107
|
|
|
108
|
+
const rowKey = (group: PageHistoryGroup) => `pg-${group.steps[0]?.index}`;
|
|
109
|
+
|
|
110
|
+
const toRow = (group: PageHistoryGroup): HistoryRowGroup => toRowGroup(group, rowKey(group), descriptor);
|
|
111
|
+
|
|
102
112
|
/**
|
|
103
113
|
* 是否处于"初始状态"——即对应页面历史栈 cursor===0:
|
|
104
|
-
* 当 list 中所有 group 的 applied 都为 false
|
|
105
|
-
*
|
|
114
|
+
* 当 list 中所有 group 的 applied 都为 false 时即为该状态(空列表 `every` 返回 true,
|
|
115
|
+
* 即仅有 marker、无任何操作记录时也视为处于初始状态)。
|
|
106
116
|
*/
|
|
107
|
-
const isInitial = computed(() => props.list.
|
|
117
|
+
const isInitial = computed(() => props.list.every((g) => !g.applied));
|
|
108
118
|
</script>
|
|
@@ -4,15 +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,
|
|
12
|
+
HistoryOpSource,
|
|
11
13
|
HistoryOpType,
|
|
14
|
+
HistoryRowDescriptor,
|
|
12
15
|
PageHistoryGroup,
|
|
13
16
|
StepValue,
|
|
14
17
|
} from '@editor/type';
|
|
15
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
|
+
|
|
16
86
|
/**
|
|
17
87
|
* 历史记录面板共享逻辑:
|
|
18
88
|
* - 暴露三类历史的聚合数据(页面 / 数据源 / 代码块);
|
|
@@ -27,10 +97,11 @@ export const useHistoryList = () => {
|
|
|
27
97
|
/**
|
|
28
98
|
* 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。
|
|
29
99
|
* 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。
|
|
100
|
+
* 合并组默认展开;仅当值为 `false` 时表示收起。
|
|
30
101
|
*/
|
|
31
102
|
const expanded = reactive<Record<string, boolean>>({});
|
|
32
103
|
const toggleGroup = (key: string) => {
|
|
33
|
-
expanded[key] =
|
|
104
|
+
expanded[key] = expanded[key] === false;
|
|
34
105
|
};
|
|
35
106
|
|
|
36
107
|
const pageGroups = computed(() => historyService.getPageHistoryGroups());
|
|
@@ -107,7 +178,80 @@ export const opLabel = (op: HistoryOpType) => {
|
|
|
107
178
|
}
|
|
108
179
|
};
|
|
109
180
|
|
|
110
|
-
|
|
181
|
+
/** 内置操作途径的中文文案;自定义来源直接回显原值,未知 / 缺省返回空串(UI 据此不渲染)。 */
|
|
182
|
+
const HISTORY_SOURCE_LABELS: Record<string, string> = {
|
|
183
|
+
stage: '画布',
|
|
184
|
+
tree: '树面板',
|
|
185
|
+
'component-panel': '组件面板',
|
|
186
|
+
props: '配置面板',
|
|
187
|
+
code: '源码',
|
|
188
|
+
'root-code': 'DSL源码',
|
|
189
|
+
'stage-contextmenu': '画布菜单',
|
|
190
|
+
'tree-contextmenu': '树菜单',
|
|
191
|
+
toolbar: '工具栏',
|
|
192
|
+
shortcut: '快捷键',
|
|
193
|
+
rollback: '回滚',
|
|
194
|
+
api: '接口',
|
|
195
|
+
ai: 'AI',
|
|
196
|
+
initial: '初始值',
|
|
197
|
+
sync: '同步',
|
|
198
|
+
unknown: '未知',
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** 操作途径文案:用于历史面板展示「画布 / 树面板 / 配置面板…」标签。 */
|
|
202
|
+
export const sourceLabel = (source: HistoryOpSource = 'unknown'): string => {
|
|
203
|
+
return HISTORY_SOURCE_LABELS[source] ?? `${source}`;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** 取一组历史步骤里最后一步(最近一次)的操作途径,用于组头部展示。 */
|
|
207
|
+
export const groupSource = (group: { steps: { step: { source?: HistoryOpSource } }[] }): HistoryOpSource | undefined =>
|
|
208
|
+
group.steps[group.steps.length - 1]?.step.source;
|
|
209
|
+
|
|
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 }) =>
|
|
111
255
|
node?.name || node?.type || `${node?.id ?? ''}`;
|
|
112
256
|
|
|
113
257
|
/**
|
|
@@ -132,25 +276,25 @@ const pickLastDescription = (descs: (string | undefined)[]): string | undefined
|
|
|
132
276
|
export const describePageStep = (step: StepValue) => {
|
|
133
277
|
if (step.historyDescription) return step.historyDescription;
|
|
134
278
|
const { opType } = step;
|
|
279
|
+
const items = step.diff ?? [];
|
|
135
280
|
if (opType === 'add') {
|
|
136
|
-
const count =
|
|
137
|
-
const node =
|
|
281
|
+
const count = items.length;
|
|
282
|
+
const node = items[0]?.newSchema;
|
|
138
283
|
return `新增 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
|
|
139
284
|
}
|
|
140
285
|
if (opType === 'remove') {
|
|
141
|
-
const count =
|
|
142
|
-
const node =
|
|
286
|
+
const count = items.length;
|
|
287
|
+
const node = items[0]?.oldSchema;
|
|
143
288
|
return `删除 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
|
|
144
289
|
}
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
const { newNode, changeRecords } = updated[0];
|
|
290
|
+
if (!items.length) return '修改节点';
|
|
291
|
+
if (items.length === 1) {
|
|
292
|
+
const { newSchema, changeRecords } = items[0];
|
|
149
293
|
const propPath = changeRecords?.[0]?.propPath;
|
|
150
|
-
const target = labelWithId(nameOf(
|
|
294
|
+
const target = labelWithId(nameOf(newSchema), newSchema?.id);
|
|
151
295
|
return `修改 ${target}${propPath ? ` · ${propPath}` : ''}`;
|
|
152
296
|
}
|
|
153
|
-
return `修改 ${
|
|
297
|
+
return `修改 ${items.length} 个节点`;
|
|
154
298
|
};
|
|
155
299
|
|
|
156
300
|
/**
|
|
@@ -165,7 +309,7 @@ export const describePageGroup = (group: PageHistoryGroup) => {
|
|
|
165
309
|
if (group.steps.length === 1) return describePageStep(group.steps[0].step);
|
|
166
310
|
const paths = new Set<string>();
|
|
167
311
|
group.steps.forEach((s) => {
|
|
168
|
-
s.step.
|
|
312
|
+
s.step.diff?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
|
|
169
313
|
});
|
|
170
314
|
const pathList = Array.from(paths).slice(0, 3).join(', ');
|
|
171
315
|
const target = labelWithId(
|
|
@@ -177,12 +321,11 @@ export const describePageGroup = (group: PageHistoryGroup) => {
|
|
|
177
321
|
|
|
178
322
|
export const describeDataSourceStep = (step: DataSourceStepValue) => {
|
|
179
323
|
if (step.historyDescription) return step.historyDescription;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
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);
|
|
186
329
|
return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
|
|
187
330
|
};
|
|
188
331
|
|
|
@@ -192,22 +335,23 @@ export const describeDataSourceGroup = (group: DataSourceHistoryGroup) => {
|
|
|
192
335
|
if (group.steps.length === 1) return describeDataSourceStep(group.steps[0].step);
|
|
193
336
|
const paths = new Set<string>();
|
|
194
337
|
group.steps.forEach((s) => {
|
|
195
|
-
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));
|
|
196
339
|
});
|
|
197
340
|
const pathList = Array.from(paths).slice(0, 3).join(', ');
|
|
198
|
-
const rawTitle =
|
|
341
|
+
const rawTitle =
|
|
342
|
+
group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.title ||
|
|
343
|
+
group.steps[0].step.diff?.[0]?.oldSchema?.title;
|
|
199
344
|
const target = labelWithId(rawTitle, group.id);
|
|
200
345
|
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
|
201
346
|
};
|
|
202
347
|
|
|
203
348
|
export const describeCodeBlockStep = (step: CodeBlockStepValue) => {
|
|
204
349
|
if (step.historyDescription) return step.historyDescription;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
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);
|
|
211
355
|
return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
|
|
212
356
|
};
|
|
213
357
|
|
|
@@ -217,10 +361,47 @@ export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
|
|
|
217
361
|
if (group.steps.length === 1) return describeCodeBlockStep(group.steps[0].step);
|
|
218
362
|
const paths = new Set<string>();
|
|
219
363
|
group.steps.forEach((s) => {
|
|
220
|
-
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));
|
|
221
365
|
});
|
|
222
366
|
const pathList = Array.from(paths).slice(0, 3).join(', ');
|
|
223
|
-
const rawName =
|
|
367
|
+
const rawName =
|
|
368
|
+
group.steps[group.steps.length - 1].step.diff?.[0]?.newSchema?.name ||
|
|
369
|
+
group.steps[0].step.diff?.[0]?.oldSchema?.name;
|
|
224
370
|
const target = labelWithId(rawName, group.id);
|
|
225
371
|
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
|
226
372
|
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 页面 step 是否支持「回滚」(类 git revert):
|
|
376
|
+
* - 新增 / 删除:不依赖 changeRecords,反向应用即删除 / 加回,始终可回滚;
|
|
377
|
+
* - 更新:必须每个被更新节点都带有 changeRecords,才支持按 propPath 局部反向 patch。
|
|
378
|
+
* 缺失 changeRecords 的更新只能整节点替换,会冲掉该节点后续的无关变更,因此不支持回滚。
|
|
379
|
+
*/
|
|
380
|
+
export const isPageStepRevertable = (step: StepValue): boolean => {
|
|
381
|
+
if (step.opType !== 'update') return true;
|
|
382
|
+
const items = step.diff ?? [];
|
|
383
|
+
if (!items.length) return false;
|
|
384
|
+
return items.every((item) => Boolean(item.changeRecords?.length));
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
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 是否支持「回滚」:
|
|
400
|
+
* - 新增(无 oldSchema)/ 删除(无 newSchema):不依赖 changeRecords,始终可回滚;
|
|
401
|
+
* - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
|
402
|
+
*/
|
|
403
|
+
export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
|
|
404
|
+
const item = step.diff?.[0];
|
|
405
|
+
if (!item?.oldSchema || !item?.newSchema) return true;
|
|
406
|
+
return Boolean(item.changeRecords?.length);
|
|
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
|
|
|
@@ -151,7 +151,11 @@ const submit = async (v: MNode, eventData?: ContainerChangeEventData) => {
|
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
// 区分操作途径:表单字段编辑(MForm @change)会带上 eventData(含 changeRecords);
|
|
155
|
+
// 源码编辑器(CodeEditor @save → saveCode)保存时不带 eventData,据此标记为「源码编辑器」。
|
|
156
|
+
const historySource = eventData ? 'props' : 'code';
|
|
157
|
+
|
|
158
|
+
editorService.update(newValue, { changeRecords: eventData?.changeRecords, historySource });
|
|
155
159
|
} catch (e: any) {
|
|
156
160
|
emit('submit-error', e);
|
|
157
161
|
}
|
|
@@ -94,11 +94,15 @@ let clientX: number;
|
|
|
94
94
|
let clientY: number;
|
|
95
95
|
|
|
96
96
|
const appendComponent = ({ text, type, data = {} }: ComponentItem): void => {
|
|
97
|
-
editorService.add(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
editorService.add(
|
|
98
|
+
{
|
|
99
|
+
name: text,
|
|
100
|
+
type,
|
|
101
|
+
...data,
|
|
102
|
+
},
|
|
103
|
+
undefined,
|
|
104
|
+
{ historySource: 'component-panel' },
|
|
105
|
+
);
|
|
102
106
|
};
|
|
103
107
|
|
|
104
108
|
const dragstartHandler = ({ text, type, data = {} }: ComponentItem, e: DragEvent) => {
|
|
@@ -239,7 +239,12 @@ const unWatchEditorContentHeight = watch(
|
|
|
239
239
|
},
|
|
240
240
|
);
|
|
241
241
|
|
|
242
|
-
const activeTabName =
|
|
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> = {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
<Icon :icon="editable ? Edit : View" class="edit-icon" @click.stop="editCode(`${data.key}`)"></Icon>
|
|
25
25
|
</TMagicTooltip>
|
|
26
26
|
<TMagicTooltip v-if="data.type === 'code' && editable" effect="dark" content="删除" placement="bottom">
|
|
27
|
-
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}
|
|
27
|
+
<Icon :icon="Close" class="edit-icon" @click.stop="deleteCode(`${data.key}`, { historySource: 'tree' })"></Icon>
|
|
28
28
|
</TMagicTooltip>
|
|
29
29
|
<slot name="code-block-panel-tool" :id="data.key" :data="data"></slot>
|
|
30
30
|
</template>
|
|
@@ -44,7 +44,7 @@ import Tree from '@editor/components/Tree.vue';
|
|
|
44
44
|
import { useFilter } from '@editor/hooks/use-filter';
|
|
45
45
|
import { useNodeStatus } from '@editor/hooks/use-node-status';
|
|
46
46
|
import { useServices } from '@editor/hooks/use-services';
|
|
47
|
-
import { type CodeBlockListSlots, CodeDeleteErrorType, type TreeNodeData } from '@editor/type';
|
|
47
|
+
import { type CodeBlockListSlots, CodeDeleteErrorType, HistoryOpSource, type TreeNodeData } from '@editor/type';
|
|
48
48
|
|
|
49
49
|
defineSlots<CodeBlockListSlots>();
|
|
50
50
|
|
|
@@ -60,7 +60,7 @@ const props = defineProps<{
|
|
|
60
60
|
|
|
61
61
|
const emit = defineEmits<{
|
|
62
62
|
edit: [id: string];
|
|
63
|
-
remove: [id: string];
|
|
63
|
+
remove: [id: string, { historySource?: HistoryOpSource }];
|
|
64
64
|
'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
|
|
65
65
|
}>();
|
|
66
66
|
|
|
@@ -142,7 +142,7 @@ const editCode = (id: string) => {
|
|
|
142
142
|
emit('edit', id);
|
|
143
143
|
};
|
|
144
144
|
|
|
145
|
-
const deleteCode = async (id: string) => {
|
|
145
|
+
const deleteCode = async (id: string, { historySource }: { historySource?: HistoryOpSource } = {}) => {
|
|
146
146
|
const currentCode = codeList.value.find((codeItem) => codeItem.id === id);
|
|
147
147
|
const existBinds = Boolean(currentCode?.items?.length);
|
|
148
148
|
const undeleteableList = codeBlockService.getUndeletableList() || [];
|
|
@@ -154,7 +154,7 @@ const deleteCode = async (id: string) => {
|
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
// 无绑定关系,且不在不可删除列表中
|
|
157
|
-
emit('remove', id);
|
|
157
|
+
emit('remove', id, { historySource });
|
|
158
158
|
} else {
|
|
159
159
|
if (typeof props.customError === 'function') {
|
|
160
160
|
props.customError(id, existBinds ? CodeDeleteErrorType.BIND : CodeDeleteErrorType.UNDELETEABLE);
|
|
@@ -122,7 +122,7 @@ const {
|
|
|
122
122
|
menuData: contentMenuData,
|
|
123
123
|
contentMenuHideHandler,
|
|
124
124
|
} = useContentMenu((id: string) => {
|
|
125
|
-
codeBlockListRef.value?.deleteCode(id);
|
|
125
|
+
codeBlockListRef.value?.deleteCode(id, { historySource: 'tree-contextmenu' });
|
|
126
126
|
});
|
|
127
127
|
const menuData = computed<(MenuButton | MenuComponent)[]>(() => props.customContentMenu(contentMenuData, 'code-block'));
|
|
128
128
|
</script>
|
|
@@ -41,7 +41,7 @@ export const useContentMenu = (deleteCode: (id: string) => void) => {
|
|
|
41
41
|
|
|
42
42
|
const newCodeId = await codeBlockService.getUniqueId();
|
|
43
43
|
|
|
44
|
-
codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock));
|
|
44
|
+
codeBlockService.setCodeDslById(newCodeId, cloneDeep(codeBlock), { historySource: 'tree-contextmenu' });
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
{
|
|
@@ -129,7 +129,7 @@ const removeHandler = async (id: string) => {
|
|
|
129
129
|
type: 'warning',
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
dataSourceService.remove(id);
|
|
132
|
+
dataSourceService.remove(id, { historySource: 'tree-contextmenu' });
|
|
133
133
|
};
|
|
134
134
|
|
|
135
135
|
const dataSourceListRef = useTemplateRef<InstanceType<typeof DataSourceList>>('dataSourceList');
|