@tmagic/editor 1.8.0-beta.0 → 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.
- package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +11 -2
- package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +18 -110
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +6 -0
- package/dist/es/components/CompareForm.js +5 -0
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +167 -0
- package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +19 -3
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +18 -2
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +37 -6
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +12 -3
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +3 -1
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +15 -7
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +14 -6
- package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +58 -11
- package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +11 -6
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +8 -2
- package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +3 -1
- package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +16 -3
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +14 -2
- package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +10 -2
- package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +8 -5
- package/dist/es/hooks/use-code-block-edit.js +2 -2
- package/dist/es/hooks/use-stage.js +15 -7
- package/dist/es/index.js +3 -1
- package/dist/es/initService.js +6 -6
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +4 -0
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +12 -7
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +8 -0
- package/dist/es/layouts/history-list/Bucket.js +5 -0
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +84 -0
- package/dist/es/layouts/history-list/CodeBlockTab.js +5 -0
- package/dist/es/layouts/history-list/CodeBlockTab.vue_vue_type_script_setup_true_lang.js +62 -0
- package/dist/es/layouts/history-list/DataSourceTab.js +5 -0
- package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +62 -0
- package/dist/es/layouts/history-list/GroupRow.js +5 -0
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +180 -0
- package/dist/es/layouts/history-list/HistoryDiffDialog.js +5 -0
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +195 -0
- package/dist/es/layouts/history-list/HistoryListPanel.js +5 -0
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +270 -0
- package/dist/es/layouts/history-list/InitialRow.js +5 -0
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +48 -0
- package/dist/es/layouts/history-list/PageTab.js +5 -0
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +99 -0
- package/dist/es/layouts/history-list/composables.js +154 -0
- package/dist/es/layouts/sidebar/layer/use-node-status.js +38 -4
- package/dist/es/services/BaseService.js +31 -68
- package/dist/es/services/codeBlock.js +205 -8
- package/dist/es/services/componentList.js +2 -2
- package/dist/es/services/dataSource.js +217 -8
- package/dist/es/services/dep.js +21 -9
- package/dist/es/services/editor.js +350 -160
- package/dist/es/services/events.js +2 -2
- package/dist/es/services/history.js +407 -12
- package/dist/es/services/keybinding.js +2 -2
- package/dist/es/services/props.js +8 -8
- package/dist/es/services/stageOverlay.js +2 -2
- package/dist/es/services/storage.js +2 -2
- package/dist/es/services/ui.js +2 -2
- package/dist/es/style.css +347 -1
- package/dist/es/utils/code-block.js +122 -0
- package/dist/es/utils/content-menu.js +4 -5
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/editor.js +3 -3
- package/dist/es/utils/undo-redo.js +20 -0
- package/dist/style.css +347 -1
- package/dist/tmagic-editor.umd.cjs +3273 -766
- package/package.json +7 -7
- package/src/Editor.vue +7 -0
- package/src/components/CodeBlockEditor.vue +20 -109
- package/src/components/CodeParams.vue +6 -0
- package/src/components/CompareForm.vue +225 -0
- package/src/editorProps.ts +2 -0
- package/src/fields/Code.vue +19 -3
- package/src/fields/CodeSelect.vue +17 -1
- package/src/fields/CodeSelectCol.vue +30 -2
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +7 -1
- package/src/fields/DataSourceFieldSelect/Index.vue +9 -2
- package/src/fields/DataSourceFields.vue +12 -3
- package/src/fields/DataSourceMethodSelect.vue +4 -1
- package/src/fields/DataSourceMethods.vue +14 -4
- package/src/fields/DataSourceMocks.vue +13 -3
- package/src/fields/DataSourceSelect.vue +4 -1
- package/src/fields/EventSelect.vue +57 -8
- package/src/fields/KeyValue.vue +17 -4
- package/src/fields/StyleSetter/Index.vue +6 -0
- package/src/fields/StyleSetter/components/Border.vue +15 -1
- package/src/fields/StyleSetter/components/Box.vue +2 -0
- package/src/fields/StyleSetter/pro/Background.vue +15 -1
- package/src/fields/StyleSetter/pro/Border.vue +24 -2
- package/src/fields/StyleSetter/pro/Font.vue +15 -1
- package/src/fields/StyleSetter/pro/Layout.vue +17 -1
- package/src/fields/StyleSetter/pro/Position.vue +15 -1
- package/src/fields/StyleSetter/pro/Transform.vue +15 -1
- package/src/fields/UISelect.vue +5 -2
- package/src/hooks/use-code-block-edit.ts +5 -2
- package/src/hooks/use-stage.ts +23 -7
- package/src/index.ts +1 -0
- package/src/layouts/CodeEditor.vue +15 -0
- package/src/layouts/Framework.vue +10 -5
- package/src/layouts/NavMenu.vue +9 -0
- package/src/layouts/history-list/Bucket.vue +99 -0
- package/src/layouts/history-list/CodeBlockTab.vue +61 -0
- package/src/layouts/history-list/DataSourceTab.vue +61 -0
- package/src/layouts/history-list/GroupRow.vue +207 -0
- package/src/layouts/history-list/HistoryDiffDialog.vue +212 -0
- package/src/layouts/history-list/HistoryListPanel.vue +271 -0
- package/src/layouts/history-list/InitialRow.vue +41 -0
- package/src/layouts/history-list/PageTab.vue +94 -0
- package/src/layouts/history-list/composables.ts +195 -0
- package/src/layouts/sidebar/layer/use-node-status.ts +64 -12
- package/src/services/BaseService.ts +35 -86
- package/src/services/codeBlock.ts +256 -5
- package/src/services/dataSource.ts +257 -6
- package/src/services/dep.ts +32 -9
- package/src/services/editor.ts +454 -171
- package/src/services/history.ts +452 -8
- package/src/services/props.ts +2 -2
- package/src/theme/history-list-panel.scss +423 -0
- package/src/theme/theme.scss +1 -0
- package/src/type.ts +179 -6
- package/src/utils/code-block.ts +150 -0
- package/src/utils/content-menu.ts +8 -5
- package/src/utils/dep/worker.ts +5 -3
- package/src/utils/editor.ts +4 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/undo-redo.ts +23 -0
- package/types/index.d.ts +2107 -810
- package/dist/es/utils/compose.js +0 -44
- package/dist/es/utils/editor-history.js +0 -100
- package/src/utils/compose.ts +0 -52
- package/src/utils/editor-history.ts +0 -138
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { computed, reactive } from 'vue';
|
|
2
|
+
|
|
3
|
+
import { useServices } from '@editor/hooks/use-services';
|
|
4
|
+
import type {
|
|
5
|
+
CodeBlockHistoryGroup,
|
|
6
|
+
CodeBlockStepValue,
|
|
7
|
+
DataSourceHistoryGroup,
|
|
8
|
+
DataSourceStepValue,
|
|
9
|
+
HistoryOpType,
|
|
10
|
+
PageHistoryGroup,
|
|
11
|
+
StepValue,
|
|
12
|
+
} from '@editor/type';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 历史记录面板共享逻辑:
|
|
16
|
+
* - 暴露三类历史的聚合数据(页面 / 数据源 / 代码块);
|
|
17
|
+
* - 提供折叠状态管理;
|
|
18
|
+
* - 提供操作描述文案生成器。
|
|
19
|
+
*
|
|
20
|
+
* 所有数据基于 historyService 的 reactive state 派生,自动跟随历史变化刷新。
|
|
21
|
+
*/
|
|
22
|
+
export const useHistoryList = () => {
|
|
23
|
+
const { historyService } = useServices();
|
|
24
|
+
|
|
25
|
+
/** 折叠状态:key 形如 `pg-${groupIdx}` / `ds-${id}-${groupIdx}` / `cb-${id}-${groupIdx}`。 */
|
|
26
|
+
const expanded = reactive<Record<string, boolean>>({});
|
|
27
|
+
const toggleGroup = (key: string) => {
|
|
28
|
+
expanded[key] = !expanded[key];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const pageGroups = computed(() => historyService.getPageHistoryGroups());
|
|
32
|
+
const dataSourceGroups = computed(() => historyService.getDataSourceHistoryGroups());
|
|
33
|
+
const codeBlockGroups = computed(() => historyService.getCodeBlockHistoryGroups());
|
|
34
|
+
|
|
35
|
+
/** 页面 tab 倒序展示(最新一组在最上面)。 */
|
|
36
|
+
const pageGroupsDisplay = computed(() => pageGroups.value.slice().reverse());
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 把按时间正序的 group 列表,再按 id 聚拢成 bucket(同 id 的所有分组放一起)。
|
|
40
|
+
* 每个 bucket 内部仍然按时间倒序展示(最近的操作最先看到)。
|
|
41
|
+
*/
|
|
42
|
+
const groupByTarget = <G extends { id: string | number }>(groups: G[]) => {
|
|
43
|
+
const map = new Map<string | number, G[]>();
|
|
44
|
+
groups.forEach((g) => {
|
|
45
|
+
const list = map.get(g.id) ?? [];
|
|
46
|
+
list.push(g);
|
|
47
|
+
map.set(g.id, list);
|
|
48
|
+
});
|
|
49
|
+
return Array.from(map.entries()).map(([id, gs]) => ({ id, groups: gs.slice().reverse() }));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const dataSourceGroupsByTarget = computed(() => groupByTarget(dataSourceGroups.value));
|
|
53
|
+
const codeBlockGroupsByTarget = computed(() => groupByTarget(codeBlockGroups.value));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
expanded,
|
|
57
|
+
toggleGroup,
|
|
58
|
+
pageGroups,
|
|
59
|
+
dataSourceGroups,
|
|
60
|
+
codeBlockGroups,
|
|
61
|
+
pageGroupsDisplay,
|
|
62
|
+
dataSourceGroupsByTarget,
|
|
63
|
+
codeBlockGroupsByTarget,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const opLabel = (op: HistoryOpType) => {
|
|
68
|
+
switch (op) {
|
|
69
|
+
case 'add':
|
|
70
|
+
return '新增';
|
|
71
|
+
case 'remove':
|
|
72
|
+
return '删除';
|
|
73
|
+
case 'update':
|
|
74
|
+
default:
|
|
75
|
+
return '修改';
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const nameOf = (node: { name?: string; id?: string | number; type?: string }) =>
|
|
80
|
+
node?.name || node?.type || `${node?.id ?? ''}`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 默认描述里展示「名称 (id: xxx)」,便于区分同名实体。
|
|
84
|
+
* - 当未传入 id,或 label 本身就是 id 字符串(即没有 name/type/title 可用)时,仅展示 label,避免出现「123 (id: 123)」。
|
|
85
|
+
*/
|
|
86
|
+
const labelWithId = (label: string | number | undefined, id: string | number | undefined): string => {
|
|
87
|
+
const labelStr = label === undefined || label === null ? '' : `${label}`;
|
|
88
|
+
if (id === undefined || id === null || id === '') return labelStr;
|
|
89
|
+
if (labelStr === '' || labelStr === `${id}`) return `${id}`;
|
|
90
|
+
return `${labelStr} (id: ${id})`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** 从一组可选 historyDescription 中取最后一条非空值;都为空时返回 undefined。 */
|
|
94
|
+
const pickLastDescription = (descs: (string | undefined)[]): string | undefined => {
|
|
95
|
+
for (let i = descs.length - 1; i >= 0; i--) {
|
|
96
|
+
if (descs[i]) return descs[i];
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const describePageStep = (step: StepValue) => {
|
|
102
|
+
if (step.historyDescription) return step.historyDescription;
|
|
103
|
+
const { opType } = step;
|
|
104
|
+
if (opType === 'add') {
|
|
105
|
+
const count = step.nodes?.length ?? 0;
|
|
106
|
+
const node = step.nodes?.[0];
|
|
107
|
+
return `新增 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
|
|
108
|
+
}
|
|
109
|
+
if (opType === 'remove') {
|
|
110
|
+
const count = step.removedItems?.length ?? 0;
|
|
111
|
+
const node = step.removedItems?.[0]?.node;
|
|
112
|
+
return `删除 ${count} 个节点${count === 1 && node ? `(${labelWithId(nameOf(node), node.id)})` : ''}`;
|
|
113
|
+
}
|
|
114
|
+
const updated = step.updatedItems ?? [];
|
|
115
|
+
if (!updated.length) return '修改节点';
|
|
116
|
+
if (updated.length === 1) {
|
|
117
|
+
const { newNode, changeRecords } = updated[0];
|
|
118
|
+
const propPath = changeRecords?.[0]?.propPath;
|
|
119
|
+
const target = labelWithId(nameOf(newNode), newNode?.id);
|
|
120
|
+
return `修改 ${target}${propPath ? ` · ${propPath}` : ''}`;
|
|
121
|
+
}
|
|
122
|
+
return `修改 ${updated.length} 个节点`;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 合并组的展示文案:
|
|
127
|
+
* - 若组内任一步显式提供了 historyDescription:取最后一条非空 historyDescription(最近一次的描述更准确);
|
|
128
|
+
* - 单步组:复用 describePageStep;
|
|
129
|
+
* - 多步组(连续修改同一节点):展示节点名 + 涉及的前几个 propPath。
|
|
130
|
+
*/
|
|
131
|
+
export const describePageGroup = (group: PageHistoryGroup) => {
|
|
132
|
+
const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
|
|
133
|
+
if (lastDesc) return lastDesc;
|
|
134
|
+
if (group.steps.length === 1) return describePageStep(group.steps[0].step);
|
|
135
|
+
const paths = new Set<string>();
|
|
136
|
+
group.steps.forEach((s) => {
|
|
137
|
+
s.step.updatedItems?.[0]?.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
|
|
138
|
+
});
|
|
139
|
+
const pathList = Array.from(paths).slice(0, 3).join(', ');
|
|
140
|
+
const target = labelWithId(
|
|
141
|
+
group.targetName ?? (group.targetId !== undefined ? `${group.targetId}` : '节点'),
|
|
142
|
+
group.targetId,
|
|
143
|
+
);
|
|
144
|
+
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const describeDataSourceStep = (step: DataSourceStepValue) => {
|
|
148
|
+
if (step.historyDescription) return step.historyDescription;
|
|
149
|
+
if (step.oldSchema === null && step.newSchema)
|
|
150
|
+
return `创建 ${labelWithId(step.newSchema.title, step.newSchema.id ?? step.id)}`;
|
|
151
|
+
if (step.newSchema === null && step.oldSchema)
|
|
152
|
+
return `删除 ${labelWithId(step.oldSchema.title, step.oldSchema.id ?? step.id)}`;
|
|
153
|
+
const propPath = step.changeRecords?.[0]?.propPath;
|
|
154
|
+
const title = labelWithId(step.newSchema?.title || step.oldSchema?.title, step.id);
|
|
155
|
+
return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const describeDataSourceGroup = (group: DataSourceHistoryGroup) => {
|
|
159
|
+
const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
|
|
160
|
+
if (lastDesc) return lastDesc;
|
|
161
|
+
if (group.steps.length === 1) return describeDataSourceStep(group.steps[0].step);
|
|
162
|
+
const paths = new Set<string>();
|
|
163
|
+
group.steps.forEach((s) => {
|
|
164
|
+
s.step.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
|
|
165
|
+
});
|
|
166
|
+
const pathList = Array.from(paths).slice(0, 3).join(', ');
|
|
167
|
+
const rawTitle = group.steps[group.steps.length - 1].step.newSchema?.title || group.steps[0].step.oldSchema?.title;
|
|
168
|
+
const target = labelWithId(rawTitle, group.id);
|
|
169
|
+
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const describeCodeBlockStep = (step: CodeBlockStepValue) => {
|
|
173
|
+
if (step.historyDescription) return step.historyDescription;
|
|
174
|
+
if (step.oldContent === null && step.newContent)
|
|
175
|
+
return `创建 ${labelWithId(step.newContent.name, step.newContent.id ?? step.id)}`;
|
|
176
|
+
if (step.newContent === null && step.oldContent)
|
|
177
|
+
return `删除 ${labelWithId(step.oldContent.name, step.oldContent.id ?? step.id)}`;
|
|
178
|
+
const propPath = step.changeRecords?.[0]?.propPath;
|
|
179
|
+
const title = labelWithId(step.newContent?.name || step.oldContent?.name, step.id);
|
|
180
|
+
return propPath ? `修改 ${title} · ${propPath}` : `修改 ${title}`;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
|
|
184
|
+
const lastDesc = pickLastDescription(group.steps.map((s) => s.step.historyDescription));
|
|
185
|
+
if (lastDesc) return lastDesc;
|
|
186
|
+
if (group.steps.length === 1) return describeCodeBlockStep(group.steps[0].step);
|
|
187
|
+
const paths = new Set<string>();
|
|
188
|
+
group.steps.forEach((s) => {
|
|
189
|
+
s.step.changeRecords?.forEach((r) => r.propPath && paths.add(r.propPath));
|
|
190
|
+
});
|
|
191
|
+
const pathList = Array.from(paths).slice(0, 3).join(', ');
|
|
192
|
+
const rawName = group.steps[group.steps.length - 1].step.newContent?.name || group.steps[0].step.oldContent?.name;
|
|
193
|
+
const target = labelWithId(rawName, group.id);
|
|
194
|
+
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
|
195
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
2
2
|
|
|
3
|
-
import type { Id, MNode, MPage, MPageFragment } from '@tmagic/core';
|
|
3
|
+
import type { Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/core';
|
|
4
4
|
import { getNodePath, isPage, isPageFragment, traverseNode } from '@tmagic/utils';
|
|
5
5
|
|
|
6
6
|
import type { LayerNodeStatus, Services } from '@editor/type';
|
|
@@ -9,12 +9,15 @@ import { updateStatus } from '@editor/utils/tree';
|
|
|
9
9
|
const createPageNodeStatus = (page: MPage | MPageFragment, initialLayerNodeStatus?: Map<Id, LayerNodeStatus>) => {
|
|
10
10
|
const map = new Map<Id, LayerNodeStatus>();
|
|
11
11
|
|
|
12
|
-
map.set(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
map.set(
|
|
13
|
+
page.id,
|
|
14
|
+
initialLayerNodeStatus?.get(page.id) || {
|
|
15
|
+
visible: true,
|
|
16
|
+
expand: true,
|
|
17
|
+
selected: true,
|
|
18
|
+
draggable: false,
|
|
19
|
+
},
|
|
20
|
+
);
|
|
18
21
|
|
|
19
22
|
page.items.forEach((node: MNode) =>
|
|
20
23
|
traverseNode<MNode>(node, (node) => {
|
|
@@ -45,22 +48,70 @@ export const useNodeStatus = ({ editorService }: Services) => {
|
|
|
45
48
|
page.value ? nodeStatusMaps.value.get(page.value.id) : new Map<Id, LayerNodeStatus>(),
|
|
46
49
|
);
|
|
47
50
|
|
|
48
|
-
//
|
|
51
|
+
// 切换页面 / 新增页面 / 整体替换 dsl 后 page 引用变化时,重新生成节点状态。
|
|
52
|
+
//
|
|
53
|
+
// 注意这里 watch 的是 page 引用而不是 page.id:
|
|
54
|
+
// 历史版本恢复 / 外部 modelValue 整体覆盖等场景,新旧 dsl 的 page.id 通常完全
|
|
55
|
+
// 一致,但 page 对象引用是新的、items 也是新的。仅监听 id 会漏掉这类「同 id
|
|
56
|
+
// 不同内容」的替换,导致 nodeStatusMaps 残留旧节点 status,组件树渲染滞留在
|
|
57
|
+
// 旧版本。监听引用可以覆盖普通切页(不同 id)和整体替换(同 id 新引用)两种
|
|
58
|
+
// 情况;同时配合下方 root-change 时清空缓存,避免拿到污染的 initial status。
|
|
49
59
|
watch(
|
|
50
|
-
|
|
51
|
-
(
|
|
52
|
-
if (!
|
|
60
|
+
page,
|
|
61
|
+
(newPage) => {
|
|
62
|
+
if (!newPage?.id) {
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
// 生成节点状态
|
|
57
|
-
nodeStatusMaps.value.set(
|
|
67
|
+
nodeStatusMaps.value.set(newPage.id, createPageNodeStatus(newPage, nodeStatusMaps.value.get(newPage.id)));
|
|
58
68
|
},
|
|
59
69
|
{
|
|
60
70
|
immediate: true,
|
|
61
71
|
},
|
|
62
72
|
);
|
|
63
73
|
|
|
74
|
+
/**
|
|
75
|
+
* root 整体被替换时(外部 modelValue 变化、历史版本恢复、套件编辑模式进入/退出等):
|
|
76
|
+
* - 仅 watch page 引用还不够,因为 root-change 同步触发时 page 还是旧引用,
|
|
77
|
+
* 等 initService 的异步 IIFE 跑完 editorService.select(...) 后 page 才会
|
|
78
|
+
* 被替换为新 dsl 中的 page;此时上面的 page watch 才会触发重建。
|
|
79
|
+
* - 但若直接同步清空 nodeStatusMaps,会让 nodeStatusMap (computed) 立刻变
|
|
80
|
+
* undefined。上层 LayerPanel 用 `v-if="page && nodeStatusMap"` 渲染组件树,
|
|
81
|
+
* 会瞬间销毁整个面板;若紧接着的异步 select 链路(套件退出等场景)发生
|
|
82
|
+
* 竞态、page 引用未变 / 解析失败,watch(page) 不触发重建,组件树就再也回
|
|
83
|
+
* 不来。
|
|
84
|
+
* - 此外「污染」问题本质来自 createPageNodeStatus 用旧 status 作为新节点
|
|
85
|
+
* initial 值:只要新 root 的 page 是新引用,watch(page) 会触发重建,重建
|
|
86
|
+
* 时基于新 page.items 生成的 map 只会包含新节点 id;旧节点 id 即便残留在
|
|
87
|
+
* initialLayerNodeStatus 中也不会被写入新 map。真正的风险只有「同一 page
|
|
88
|
+
* id 下,新旧 dsl 都存在同一节点 id 但其实是不同节点」这种极端情况——这
|
|
89
|
+
* 在常规业务中不会发生(id 是 uuid)。
|
|
90
|
+
*
|
|
91
|
+
* 综合:root-change 时仅清理「在新 root 中已不存在的 page id」对应缓存,
|
|
92
|
+
* 保留仍然有效的 page status 不动;既避免 v-if 闪断,也不会保留无关 page 的
|
|
93
|
+
* 死缓存。同 page id 的重建交给下方 watch(page) 触发。
|
|
94
|
+
*/
|
|
95
|
+
const rootChangeHandler = (value: MApp | null) => {
|
|
96
|
+
if (!value) {
|
|
97
|
+
nodeStatusMaps.value = new Map();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const validPageIds = new Set<Id>();
|
|
102
|
+
(value.items || []).forEach((p) => {
|
|
103
|
+
if (p?.id !== undefined) validPageIds.add(p.id);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
for (const cachedPageId of Array.from(nodeStatusMaps.value.keys())) {
|
|
107
|
+
if (!validPageIds.has(cachedPageId)) {
|
|
108
|
+
nodeStatusMaps.value.delete(cachedPageId);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
editorService.on('root-change', rootChangeHandler);
|
|
114
|
+
|
|
64
115
|
// 选中状态变化,更新节点状态
|
|
65
116
|
watch(
|
|
66
117
|
nodes,
|
|
@@ -111,6 +162,7 @@ export const useNodeStatus = ({ editorService }: Services) => {
|
|
|
111
162
|
editorService.on('remove', removeHandler);
|
|
112
163
|
|
|
113
164
|
onBeforeUnmount(() => {
|
|
165
|
+
editorService.off('root-change', rootChangeHandler);
|
|
114
166
|
editorService.off('remove', removeHandler);
|
|
115
167
|
editorService.off('add', addHandler);
|
|
116
168
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-misused-promises */
|
|
2
1
|
/*
|
|
3
2
|
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
|
4
3
|
*
|
|
@@ -19,45 +18,32 @@
|
|
|
19
18
|
|
|
20
19
|
import { EventEmitter } from 'events';
|
|
21
20
|
|
|
22
|
-
import { compose } from '@editor/utils/compose';
|
|
23
|
-
|
|
24
21
|
const methodName = (prefix: string, name: string) => `${prefix}${name[0].toUpperCase()}${name.substring(1)}`;
|
|
25
22
|
|
|
26
23
|
const isError = (error: any): boolean => Object.prototype.toString.call(error) === '[object Error]';
|
|
27
24
|
|
|
28
|
-
const doAction = (
|
|
29
|
-
args
|
|
30
|
-
scope: any,
|
|
31
|
-
sourceMethod: any,
|
|
32
|
-
beforeMethodName: string,
|
|
33
|
-
afterMethodName: string,
|
|
34
|
-
fn: (args: any[], next?: Function | undefined) => void,
|
|
35
|
-
) => {
|
|
36
|
-
try {
|
|
37
|
-
let beforeArgs = args;
|
|
25
|
+
const doAction = (args: any[], scope: any, sourceMethod: any, beforeMethodName: string, afterMethodName: string) => {
|
|
26
|
+
let beforeArgs = args;
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
28
|
+
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
|
|
29
|
+
beforeArgs = beforeMethod(...beforeArgs) || [];
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
if (isError(beforeArgs)) throw beforeArgs;
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
33
|
+
if (!Array.isArray(beforeArgs)) {
|
|
34
|
+
beforeArgs = [beforeArgs];
|
|
47
35
|
}
|
|
36
|
+
}
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
52
|
-
returnValue = afterMethod(returnValue, ...beforeArgs);
|
|
38
|
+
let returnValue: any = sourceMethod.apply(scope, beforeArgs);
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
41
|
+
returnValue = afterMethod(returnValue, ...beforeArgs);
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
} catch (error) {
|
|
59
|
-
throw error;
|
|
43
|
+
if (isError(returnValue)) throw returnValue;
|
|
60
44
|
}
|
|
45
|
+
|
|
46
|
+
return returnValue;
|
|
61
47
|
};
|
|
62
48
|
|
|
63
49
|
const doAsyncAction = async (
|
|
@@ -66,40 +52,34 @@ const doAsyncAction = async (
|
|
|
66
52
|
sourceMethod: any,
|
|
67
53
|
beforeMethodName: string,
|
|
68
54
|
afterMethodName: string,
|
|
69
|
-
fn: (args: any[], next?: Function | undefined) => Promise<void> | void,
|
|
70
55
|
) => {
|
|
71
|
-
|
|
72
|
-
let beforeArgs = args;
|
|
56
|
+
let beforeArgs = args;
|
|
73
57
|
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
for (const beforeMethod of scope.pluginOptionsList[beforeMethodName]) {
|
|
59
|
+
beforeArgs = (await beforeMethod(...beforeArgs)) || [];
|
|
76
60
|
|
|
77
|
-
|
|
61
|
+
if (isError(beforeArgs)) throw beforeArgs;
|
|
78
62
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
63
|
+
if (!Array.isArray(beforeArgs)) {
|
|
64
|
+
beforeArgs = [beforeArgs];
|
|
82
65
|
}
|
|
66
|
+
}
|
|
83
67
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
87
|
-
returnValue = await afterMethod(returnValue, ...beforeArgs);
|
|
68
|
+
let returnValue: any = await sourceMethod.apply(scope, beforeArgs);
|
|
88
69
|
|
|
89
|
-
|
|
90
|
-
|
|
70
|
+
for (const afterMethod of scope.pluginOptionsList[afterMethodName]) {
|
|
71
|
+
returnValue = await afterMethod(returnValue, ...beforeArgs);
|
|
91
72
|
|
|
92
|
-
|
|
93
|
-
} catch (error) {
|
|
94
|
-
throw error;
|
|
73
|
+
if (isError(returnValue)) throw returnValue;
|
|
95
74
|
}
|
|
75
|
+
|
|
76
|
+
return returnValue;
|
|
96
77
|
};
|
|
97
78
|
|
|
98
79
|
/**
|
|
99
|
-
*
|
|
100
|
-
* 方法1:
|
|
80
|
+
* 对Class进行扩展
|
|
101
81
|
* 给Class中的每个方法都添加before after两个钩子
|
|
102
|
-
* 给Class添加一个usePlugin方法,
|
|
82
|
+
* 给Class添加一个usePlugin方法,usePlugin方法可以传入一个包含before或者after方法的对象
|
|
103
83
|
*
|
|
104
84
|
* 例如:
|
|
105
85
|
* Class EditorService extends BaseService {
|
|
@@ -124,27 +104,9 @@ const doAsyncAction = async (
|
|
|
124
104
|
*
|
|
125
105
|
* 调用时的参数会透传到before方法的参数中, 然后before的return 会作为原方法的参数和after的参数,after第一个参数则是原方法的return值;
|
|
126
106
|
* 如需终止后续方法调用可以return new Error();
|
|
127
|
-
*
|
|
128
|
-
* 方法2:
|
|
129
|
-
* 给Class中的每个方法都添加中间件
|
|
130
|
-
* 给Class添加一个use方法,use方法可以传入一个包含源对象方法名作为key值的对象
|
|
131
|
-
*
|
|
132
|
-
* 例如:
|
|
133
|
-
* Class EditorService extends BaseService {
|
|
134
|
-
* constructor() {
|
|
135
|
-
* super([ { name: 'add', isAsync: true },]);
|
|
136
|
-
* }
|
|
137
|
-
* add(value) { return result; }
|
|
138
|
-
* };
|
|
139
|
-
*
|
|
140
|
-
* const editorService = new EditorService();
|
|
141
|
-
* editorService.use({
|
|
142
|
-
* add(value, next) { console.log(value); next() },
|
|
143
|
-
* });
|
|
144
107
|
*/
|
|
145
|
-
|
|
108
|
+
class BaseService extends EventEmitter {
|
|
146
109
|
private pluginOptionsList: Record<string, Function[]> = {};
|
|
147
|
-
private middleware: Record<string, Function[]> = {};
|
|
148
110
|
private taskList: (() => Promise<void>)[] = [];
|
|
149
111
|
private doingTask = false;
|
|
150
112
|
|
|
@@ -161,14 +123,12 @@ export default class extends EventEmitter {
|
|
|
161
123
|
|
|
162
124
|
this.pluginOptionsList[beforeMethodName] = [];
|
|
163
125
|
this.pluginOptionsList[afterMethodName] = [];
|
|
164
|
-
this.middleware[propertyName] = [];
|
|
165
126
|
|
|
166
|
-
const fn = compose(this.middleware[propertyName], isAsync);
|
|
167
127
|
Object.defineProperty(scope, propertyName, {
|
|
168
128
|
value: isAsync
|
|
169
129
|
? async (...args: any[]) => {
|
|
170
130
|
if (!serialMethods.includes(propertyName)) {
|
|
171
|
-
return doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName
|
|
131
|
+
return doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName);
|
|
172
132
|
}
|
|
173
133
|
|
|
174
134
|
// 由于async await,所以会出现函数执行到await时让出线程,导致执行顺序出错,例如调用了select(1) -> update -> select(2),这个时候就有可能出现update了2;
|
|
@@ -176,7 +136,7 @@ export default class extends EventEmitter {
|
|
|
176
136
|
const promise = new Promise<any>((resolve, reject) => {
|
|
177
137
|
this.taskList.push(async () => {
|
|
178
138
|
try {
|
|
179
|
-
const value = await doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName
|
|
139
|
+
const value = await doAsyncAction(args, scope, sourceMethod, beforeMethodName, afterMethodName);
|
|
180
140
|
resolve(value);
|
|
181
141
|
} catch (e) {
|
|
182
142
|
reject(e);
|
|
@@ -190,20 +150,11 @@ export default class extends EventEmitter {
|
|
|
190
150
|
|
|
191
151
|
return promise;
|
|
192
152
|
}
|
|
193
|
-
: (...args: any[]) => doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName
|
|
153
|
+
: (...args: any[]) => doAction(args, scope, sourceMethod, beforeMethodName, afterMethodName),
|
|
194
154
|
});
|
|
195
155
|
});
|
|
196
156
|
}
|
|
197
157
|
|
|
198
|
-
/**
|
|
199
|
-
* @deprecated 请使用usePlugin代替
|
|
200
|
-
*/
|
|
201
|
-
public use(options: Record<string, Function>) {
|
|
202
|
-
for (const [methodName, method] of Object.entries(options)) {
|
|
203
|
-
if (typeof method === 'function') this.middleware[methodName].push(method);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
158
|
public usePlugin(options: Record<string, Function>) {
|
|
208
159
|
for (const [methodName, method] of Object.entries(options)) {
|
|
209
160
|
if (typeof method === 'function' && !this.pluginOptionsList[methodName].includes(method)) {
|
|
@@ -224,10 +175,6 @@ export default class extends EventEmitter {
|
|
|
224
175
|
for (const key of Object.keys(this.pluginOptionsList)) {
|
|
225
176
|
this.pluginOptionsList[key] = [];
|
|
226
177
|
}
|
|
227
|
-
|
|
228
|
-
for (const key of Object.keys(this.middleware)) {
|
|
229
|
-
this.middleware[key] = [];
|
|
230
|
-
}
|
|
231
178
|
}
|
|
232
179
|
|
|
233
180
|
private async doTask() {
|
|
@@ -240,3 +187,5 @@ export default class extends EventEmitter {
|
|
|
240
187
|
this.doingTask = false;
|
|
241
188
|
}
|
|
242
189
|
}
|
|
190
|
+
|
|
191
|
+
export default BaseService;
|