@tmagic/editor 1.8.0-beta.19 → 1.8.0-beta.20
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/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/sidebar/layer/use-click.js +3 -3
- package/dist/es/layouts/sidebar/layer/use-node-status.js +2 -2
- package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +7 -7
- package/dist/es/services/dep.js +82 -26
- package/dist/es/services/editor.js +36 -34
- package/dist/es/services/history.js +1 -1
- package/dist/es/services/keybinding.js +3 -3
- package/dist/es/utils/content-menu.js +2 -2
- package/dist/es/utils/dep/collect-worker-client.js +114 -0
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/operator.js +2 -2
- package/dist/tmagic-editor.umd.cjs +241 -72
- package/package.json +7 -7
- package/src/layouts/sidebar/layer/LayerMenu.vue +2 -2
- package/src/layouts/sidebar/layer/use-click.ts +3 -3
- package/src/layouts/sidebar/layer/use-node-status.ts +2 -2
- package/src/layouts/workspace/viewer/ViewerMenu.vue +7 -7
- package/src/services/dep.ts +160 -31
- package/src/services/editor.ts +52 -43
- package/src/services/history.ts +1 -1
- package/src/services/keybinding.ts +3 -3
- package/src/utils/content-menu.ts +2 -2
- package/src/utils/dep/collect-worker-client.ts +149 -0
- package/src/utils/dep/worker.ts +113 -9
- package/src/utils/operator.ts +2 -2
- package/types/index.d.ts +24 -2
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.8.0-beta.
|
|
2
|
+
"version": "1.8.0-beta.20",
|
|
3
3
|
"name": "@tmagic/editor",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"moveable": "^0.53.0",
|
|
59
59
|
"serialize-javascript": "^7.0.0",
|
|
60
60
|
"sortablejs": "^1.15.6",
|
|
61
|
-
"@tmagic/design": "1.8.0-beta.
|
|
62
|
-
"@tmagic/
|
|
63
|
-
"@tmagic/stage": "1.8.0-beta.
|
|
64
|
-
"@tmagic/
|
|
65
|
-
"@tmagic/
|
|
61
|
+
"@tmagic/design": "1.8.0-beta.20",
|
|
62
|
+
"@tmagic/form": "1.8.0-beta.20",
|
|
63
|
+
"@tmagic/stage": "1.8.0-beta.20",
|
|
64
|
+
"@tmagic/table": "1.8.0-beta.20",
|
|
65
|
+
"@tmagic/utils": "1.8.0-beta.20"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/events": "^3.0.3",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"type-fest": "^5.2.0",
|
|
77
77
|
"typescript": "^6.0.3",
|
|
78
78
|
"vue": "^3.5.40",
|
|
79
|
-
"@tmagic/core": "1.8.0-beta.
|
|
79
|
+
"@tmagic/core": "1.8.0-beta.20"
|
|
80
80
|
},
|
|
81
81
|
"peerDependenciesMeta": {
|
|
82
82
|
"typescript": {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { computed, markRaw, useTemplateRef } from 'vue';
|
|
7
7
|
import { Files, Plus } from '@element-plus/icons-vue';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { isPageOrFragment } from '@tmagic/utils';
|
|
10
10
|
|
|
11
11
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
|
12
12
|
import { useServices } from '@editor/hooks/use-services';
|
|
@@ -101,7 +101,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
|
101
101
|
type: 'button',
|
|
102
102
|
text: '全部折叠',
|
|
103
103
|
icon: FolderMinusIcon,
|
|
104
|
-
display: () =>
|
|
104
|
+
display: () => isPageOrFragment(node.value),
|
|
105
105
|
handler: () => {
|
|
106
106
|
emit('collapse-all');
|
|
107
107
|
},
|
|
@@ -2,7 +2,7 @@ import { computed, type ComputedRef, nextTick, type Ref, type ShallowRef } from
|
|
|
2
2
|
import { throttle } from 'lodash-es';
|
|
3
3
|
|
|
4
4
|
import { Id, MNode } from '@tmagic/core';
|
|
5
|
-
import { getElById,
|
|
5
|
+
import { getElById, isPageOrFragment } from '@tmagic/utils';
|
|
6
6
|
|
|
7
7
|
import type { LayerNodeStatus, Services, TreeNodeData } from '@editor/type';
|
|
8
8
|
import { UI_SELECT_MODE_EVENT_NAME } from '@editor/utils/const';
|
|
@@ -36,7 +36,7 @@ export const useClick = (
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
const multiSelect = async (data: MNode) => {
|
|
39
|
-
if (
|
|
39
|
+
if (isPageOrFragment(data)) {
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -50,7 +50,7 @@ export const useClick = (
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
if (
|
|
53
|
+
if (isPageOrFragment(node)) {
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
2
2
|
|
|
3
3
|
import type { Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/core';
|
|
4
|
-
import { getNodePath,
|
|
4
|
+
import { getNodePath, isPageOrFragment, traverseNode } from '@tmagic/utils';
|
|
5
5
|
|
|
6
6
|
import type { LayerNodeStatus, Services } from '@editor/type';
|
|
7
7
|
import { updateStatus } from '@editor/utils/tree';
|
|
@@ -136,7 +136,7 @@ export const useNodeStatus = ({ editorService }: Services) => {
|
|
|
136
136
|
|
|
137
137
|
const addHandler = (newNodes: MNode[]) => {
|
|
138
138
|
newNodes.forEach((node) => {
|
|
139
|
-
if (
|
|
139
|
+
if (isPageOrFragment(node)) return;
|
|
140
140
|
|
|
141
141
|
traverseNode(node, (node: MNode) => {
|
|
142
142
|
nodeStatusMap.value?.set(node.id, {
|
|
@@ -7,7 +7,7 @@ import { computed, markRaw, ref, useTemplateRef, watch } from 'vue';
|
|
|
7
7
|
import { Bottom, Top } from '@element-plus/icons-vue';
|
|
8
8
|
|
|
9
9
|
import { NodeType } from '@tmagic/core';
|
|
10
|
-
import {
|
|
10
|
+
import { isPageOrFragment } from '@tmagic/utils';
|
|
11
11
|
|
|
12
12
|
import ContentMenu from '@editor/components/ContentMenu.vue';
|
|
13
13
|
import { useServices } from '@editor/hooks/use-services';
|
|
@@ -59,14 +59,14 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
|
59
59
|
direction: 'horizontal',
|
|
60
60
|
display: () => {
|
|
61
61
|
if (!node.value) return false;
|
|
62
|
-
return !
|
|
62
|
+
return !isPageOrFragment(node.value);
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
type: 'button',
|
|
67
67
|
text: '上移一层',
|
|
68
68
|
icon: markRaw(Top),
|
|
69
|
-
display: () => !
|
|
69
|
+
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
|
70
70
|
handler: () => {
|
|
71
71
|
editorService.moveLayer(1, { historySource: 'stage-contextmenu' });
|
|
72
72
|
},
|
|
@@ -75,7 +75,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
|
75
75
|
type: 'button',
|
|
76
76
|
text: '下移一层',
|
|
77
77
|
icon: markRaw(Bottom),
|
|
78
|
-
display: () => !
|
|
78
|
+
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
|
79
79
|
handler: () => {
|
|
80
80
|
editorService.moveLayer(-1, { historySource: 'stage-contextmenu' });
|
|
81
81
|
},
|
|
@@ -84,7 +84,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
|
84
84
|
type: 'button',
|
|
85
85
|
text: '置顶',
|
|
86
86
|
icon: markRaw(Top),
|
|
87
|
-
display: () => !
|
|
87
|
+
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
|
88
88
|
handler: () => {
|
|
89
89
|
editorService.moveLayer(LayerOffset.TOP, { historySource: 'stage-contextmenu' });
|
|
90
90
|
},
|
|
@@ -93,7 +93,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
|
93
93
|
type: 'button',
|
|
94
94
|
text: '置底',
|
|
95
95
|
icon: markRaw(Bottom),
|
|
96
|
-
display: () => !
|
|
96
|
+
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
|
97
97
|
handler: () => {
|
|
98
98
|
editorService.moveLayer(LayerOffset.BOTTOM, { historySource: 'stage-contextmenu' });
|
|
99
99
|
},
|
|
@@ -102,7 +102,7 @@ const menuData = computed<(MenuButton | MenuComponent)[]>(() =>
|
|
|
102
102
|
{
|
|
103
103
|
type: 'divider',
|
|
104
104
|
direction: 'horizontal',
|
|
105
|
-
display: () => !
|
|
105
|
+
display: () => !isPageOrFragment(node.value) && !props.isMultiSelect,
|
|
106
106
|
},
|
|
107
107
|
useDeleteMenu('stage-contextmenu'),
|
|
108
108
|
{
|
package/src/services/dep.ts
CHANGED
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { reactive, shallowReactive } from 'vue';
|
|
19
19
|
import { throttle } from 'lodash-es';
|
|
20
|
-
import serialize from 'serialize-javascript';
|
|
21
20
|
|
|
22
|
-
import type { DepData, DepExtendedData, Id, MApp, MNode, Target
|
|
21
|
+
import type { DepData, DepExtendedData, Id, MApp, MNode, Target } from '@tmagic/core';
|
|
23
22
|
import { DepTargetType, traverseTarget, Watcher } from '@tmagic/core';
|
|
24
23
|
import { isPage } from '@tmagic/utils';
|
|
25
24
|
|
|
25
|
+
import { CollectWorkerClient } from '@editor/utils/dep/collect-worker-client';
|
|
26
26
|
import { IdleTask } from '@editor/utils/dep/idle-task';
|
|
27
|
-
import
|
|
27
|
+
import type { DepsData } from '@editor/utils/dep/worker';
|
|
28
28
|
|
|
29
29
|
import BaseService from './BaseService';
|
|
30
30
|
|
|
@@ -54,6 +54,7 @@ interface CollectBatch {
|
|
|
54
54
|
dsPending: number;
|
|
55
55
|
collectedEmitted: boolean;
|
|
56
56
|
dsSettled: boolean;
|
|
57
|
+
aborted: boolean;
|
|
57
58
|
resolve: (completed: boolean) => void;
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -63,7 +64,9 @@ class Dep extends BaseService {
|
|
|
63
64
|
taskLength: 0,
|
|
64
65
|
});
|
|
65
66
|
|
|
66
|
-
private idleTask = new IdleTask<
|
|
67
|
+
private idleTask = new IdleTask<void>();
|
|
68
|
+
|
|
69
|
+
private collectWorker = new CollectWorkerClient();
|
|
67
70
|
|
|
68
71
|
private watcher = new Watcher({ initialTargets: reactive({}) });
|
|
69
72
|
|
|
@@ -153,6 +156,14 @@ class Dep extends BaseService {
|
|
|
153
156
|
this.emit('ds-collected', nodes, deep);
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
/**
|
|
160
|
+
* 空闲收集依赖
|
|
161
|
+
*
|
|
162
|
+
* 遍历匹配是整个收集过程中最耗时的部分(要深度遍历节点树并对每个属性做 target 匹配),
|
|
163
|
+
* 节点/数据源多时在主线程执行会长时间卡死页面,因此优先交给 worker 执行,主线程只负责把结果写回 target。
|
|
164
|
+
* 自定义 target 的 isTarget 是无法跨线程传递的闭包函数(没有 descriptor),仍走主线程空闲队列;
|
|
165
|
+
* worker 不可用(如 SSR)或执行失败时也会回退到主线程空闲队列。
|
|
166
|
+
*/
|
|
156
167
|
public async collectIdle(nodes: MNode[], depExtendedData: DepExtendedData = {}, deep = false, type?: DepTargetType) {
|
|
157
168
|
if (this.waitingWorker) {
|
|
158
169
|
await this.waitingWorker;
|
|
@@ -165,12 +176,23 @@ class Dep extends BaseService {
|
|
|
165
176
|
dsPending: 0,
|
|
166
177
|
collectedEmitted: false,
|
|
167
178
|
dsSettled: false,
|
|
179
|
+
aborted: false,
|
|
168
180
|
resolve: () => {},
|
|
169
181
|
};
|
|
170
182
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
183
|
+
const workerTargets: Target[] = [];
|
|
184
|
+
|
|
185
|
+
for (const target of this.watcher.getCollectableTargets(type)) {
|
|
186
|
+
if (target.descriptor && this.collectWorker.isSupported) {
|
|
187
|
+
workerTargets.push(target);
|
|
188
|
+
} else {
|
|
189
|
+
this.enqueueTasks(nodes, target, depExtendedData, deep, batch);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (workerTargets.length && nodes.length) {
|
|
194
|
+
this.collectByCollectWorker(nodes, workerTargets, depExtendedData, deep, batch);
|
|
195
|
+
}
|
|
174
196
|
|
|
175
197
|
// 没有命中任何 target,无需收集,直接完成
|
|
176
198
|
if (batch.pending === 0) {
|
|
@@ -197,18 +219,9 @@ class Dep extends BaseService {
|
|
|
197
219
|
this.waitingWorker = promise;
|
|
198
220
|
this.resolveWaitingWorker = waitingResolve;
|
|
199
221
|
|
|
200
|
-
return
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
dsl: serialize(dsl),
|
|
204
|
-
});
|
|
205
|
-
worker.onmessage = (e) => {
|
|
206
|
-
resolve(e.data);
|
|
207
|
-
};
|
|
208
|
-
worker.onerror = () => {
|
|
209
|
-
resolve({});
|
|
210
|
-
};
|
|
211
|
-
}).then((depsData) => {
|
|
222
|
+
return this.collectWorker.collectDsl(dsl).then((deps) => {
|
|
223
|
+
const depsData: DepsData = deps || {};
|
|
224
|
+
|
|
212
225
|
if (generation !== this.workerGeneration) {
|
|
213
226
|
waitingResolve();
|
|
214
227
|
return depsData;
|
|
@@ -245,8 +258,12 @@ class Dep extends BaseService {
|
|
|
245
258
|
// 先删除原有依赖,重新收集
|
|
246
259
|
if (isPage(node)) {
|
|
247
260
|
this.removePageDep(target, depExtendedData);
|
|
248
|
-
} else {
|
|
261
|
+
} else if (deep) {
|
|
262
|
+
// deep 收集会覆盖整棵子树,删除也要同步清掉子孙旧依赖
|
|
249
263
|
this.watcher.removeTargetDep(target, node);
|
|
264
|
+
} else {
|
|
265
|
+
// 非 deep 只动当前节点,避免误清子节点上其他尚未重收的依赖
|
|
266
|
+
target.removeDep(node.id);
|
|
250
267
|
}
|
|
251
268
|
|
|
252
269
|
this.watcher.collectItem(node, target, depExtendedData, deep);
|
|
@@ -271,6 +288,8 @@ class Dep extends BaseService {
|
|
|
271
288
|
public clearIdleTasks() {
|
|
272
289
|
this.abortActiveBatches();
|
|
273
290
|
this.idleTask.clearTasks();
|
|
291
|
+
// 丢掉 worker 在途任务,避免 abort 后的长任务继续占着常驻 worker
|
|
292
|
+
this.collectWorker.abort();
|
|
274
293
|
}
|
|
275
294
|
|
|
276
295
|
public on<Name extends keyof DepEvents, Param extends DepEvents[Name]>(
|
|
@@ -294,6 +313,7 @@ class Dep extends BaseService {
|
|
|
294
313
|
this.resolveWaitingWorker = undefined;
|
|
295
314
|
this.waitingWorker = undefined;
|
|
296
315
|
this.idleTask.clearTasks();
|
|
316
|
+
this.collectWorker.abort();
|
|
297
317
|
|
|
298
318
|
for (const type of Object.keys(this.watcher.getTargetsList())) {
|
|
299
319
|
this.removeTargets(type);
|
|
@@ -309,6 +329,7 @@ class Dep extends BaseService {
|
|
|
309
329
|
this.reset();
|
|
310
330
|
this.removeAllPlugins();
|
|
311
331
|
this.idleTask.removeAllListeners();
|
|
332
|
+
this.collectWorker.terminate();
|
|
312
333
|
}
|
|
313
334
|
|
|
314
335
|
public emit<Name extends keyof DepEvents, Param extends DepEvents[Name]>(eventName: Name, ...args: Param) {
|
|
@@ -328,6 +349,110 @@ class Dep extends BaseService {
|
|
|
328
349
|
}
|
|
329
350
|
}
|
|
330
351
|
|
|
352
|
+
/**
|
|
353
|
+
* 把遍历匹配交给 worker,结果回来后按 target 分片写回主线程
|
|
354
|
+
*/
|
|
355
|
+
private collectByCollectWorker(
|
|
356
|
+
nodes: MNode[],
|
|
357
|
+
targets: Target[],
|
|
358
|
+
depExtendedData: DepExtendedData,
|
|
359
|
+
deep: boolean,
|
|
360
|
+
batch: CollectBatch,
|
|
361
|
+
) {
|
|
362
|
+
const hasDataSource = targets.some((target) => target.type === DepTargetType.DATA_SOURCE);
|
|
363
|
+
|
|
364
|
+
// 先占位,避免 worker 返回前批次被误判为已完成(提前 resolve、collecting 提前复位)
|
|
365
|
+
this.beginBatchTask(batch, hasDataSource);
|
|
366
|
+
|
|
367
|
+
this.collectWorker
|
|
368
|
+
.collect({
|
|
369
|
+
nodes,
|
|
370
|
+
targets: targets.map((target) => target.descriptor!),
|
|
371
|
+
depExtendedData,
|
|
372
|
+
deep,
|
|
373
|
+
})
|
|
374
|
+
.then((result) => {
|
|
375
|
+
if (batch.aborted) return;
|
|
376
|
+
|
|
377
|
+
for (const target of targets) {
|
|
378
|
+
if (result) {
|
|
379
|
+
// 写回是纯字典操作,但数据量大时同样会占用主线程,因此按 target 拆成空闲任务
|
|
380
|
+
this.enqueueApplyTask(target, result.deps[target.type]?.[target.id] || {}, result.nodeIds, batch, {
|
|
381
|
+
nodes,
|
|
382
|
+
depExtendedData,
|
|
383
|
+
deep,
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
this.enqueueTasks(nodes, target, depExtendedData, deep, batch);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
.finally(() => {
|
|
391
|
+
// 释放占位:必须在新任务入队之后,否则批次会在中途被判定为完成
|
|
392
|
+
this.onBatchTaskDone(batch, hasDataSource);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 用 worker 的收集结果替换 target 上这批节点的依赖
|
|
398
|
+
*/
|
|
399
|
+
private enqueueApplyTask(
|
|
400
|
+
target: Target,
|
|
401
|
+
deps: DepData,
|
|
402
|
+
nodeIds: Id[],
|
|
403
|
+
batch: CollectBatch,
|
|
404
|
+
{ nodes, depExtendedData, deep }: { nodes: MNode[]; depExtendedData: DepExtendedData; deep: boolean },
|
|
405
|
+
) {
|
|
406
|
+
const isDataSource = target.type === DepTargetType.DATA_SOURCE;
|
|
407
|
+
|
|
408
|
+
this.beginBatchTask(batch, isDataSource);
|
|
409
|
+
|
|
410
|
+
this.idleTask.enqueueTask(
|
|
411
|
+
() => {
|
|
412
|
+
try {
|
|
413
|
+
// target 可能在 worker 收集期间被移除或重建(如修改数据源字段)
|
|
414
|
+
const current = this.watcher.getTarget(target.id, target.type);
|
|
415
|
+
if (current !== target) {
|
|
416
|
+
// 已被同 id 的新 target 替换时,用当前 target 在主线程补收,避免依赖空窗
|
|
417
|
+
if (current) {
|
|
418
|
+
this.enqueueTasks(nodes, current, depExtendedData, deep, batch);
|
|
419
|
+
}
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 页面按 pageId 匹配删除,可清掉页面内已被删除节点的残留依赖
|
|
424
|
+
if (nodes.some((node) => isPage(node))) {
|
|
425
|
+
this.removePageDep(target, depExtendedData);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
for (const id of nodeIds) {
|
|
429
|
+
target.removeDep(id);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
for (const [id, dep] of Object.entries(deps)) {
|
|
433
|
+
target.deps[id] = dep;
|
|
434
|
+
}
|
|
435
|
+
} finally {
|
|
436
|
+
this.onBatchTaskDone(batch, isDataSource);
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
undefined,
|
|
440
|
+
isDataSource,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private enqueueTasks(
|
|
445
|
+
nodes: MNode[],
|
|
446
|
+
target: Target,
|
|
447
|
+
depExtendedData: DepExtendedData,
|
|
448
|
+
deep: boolean,
|
|
449
|
+
batch: CollectBatch,
|
|
450
|
+
) {
|
|
451
|
+
for (const node of nodes) {
|
|
452
|
+
this.enqueueTask(node, target, depExtendedData, deep, batch);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
331
456
|
private enqueueTask(
|
|
332
457
|
node: MNode,
|
|
333
458
|
target: Target,
|
|
@@ -337,24 +462,17 @@ class Dep extends BaseService {
|
|
|
337
462
|
) {
|
|
338
463
|
const isDataSource = target.type === DepTargetType.DATA_SOURCE;
|
|
339
464
|
|
|
340
|
-
batch
|
|
341
|
-
if (isDataSource) {
|
|
342
|
-
batch.dsPending += 1;
|
|
343
|
-
}
|
|
465
|
+
this.beginBatchTask(batch, isDataSource);
|
|
344
466
|
|
|
345
467
|
this.idleTask.enqueueTask(
|
|
346
|
-
(
|
|
468
|
+
() => {
|
|
347
469
|
try {
|
|
348
|
-
this.collectNode(node, target, depExtendedData
|
|
470
|
+
this.collectNode(node, target, depExtendedData);
|
|
349
471
|
} finally {
|
|
350
472
|
this.onBatchTaskDone(batch, isDataSource);
|
|
351
473
|
}
|
|
352
474
|
},
|
|
353
|
-
|
|
354
|
-
node,
|
|
355
|
-
deep: false,
|
|
356
|
-
target,
|
|
357
|
-
},
|
|
475
|
+
undefined,
|
|
358
476
|
isDataSource,
|
|
359
477
|
);
|
|
360
478
|
|
|
@@ -365,7 +483,16 @@ class Dep extends BaseService {
|
|
|
365
483
|
}
|
|
366
484
|
}
|
|
367
485
|
|
|
486
|
+
private beginBatchTask(batch: CollectBatch, isDataSource: boolean) {
|
|
487
|
+
batch.pending += 1;
|
|
488
|
+
if (isDataSource) {
|
|
489
|
+
batch.dsPending += 1;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
368
493
|
private onBatchTaskDone(batch: CollectBatch, isDataSource: boolean) {
|
|
494
|
+
if (batch.aborted) return;
|
|
495
|
+
|
|
369
496
|
if (isDataSource) {
|
|
370
497
|
batch.dsPending -= 1;
|
|
371
498
|
// 数据源依赖收集完先 resolve,让 stage 尽快更新,其余依赖继续在后台收集
|
|
@@ -412,6 +539,8 @@ class Dep extends BaseService {
|
|
|
412
539
|
this.activeBatches.clear();
|
|
413
540
|
|
|
414
541
|
for (const batch of batches) {
|
|
542
|
+
// 标记中断:worker 结果可能在之后才返回,此时不能再写回依赖,也不能再结算批次
|
|
543
|
+
batch.aborted = true;
|
|
415
544
|
batch.resolve(false);
|
|
416
545
|
}
|
|
417
546
|
|
package/src/services/editor.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { reactive, toRaw } from 'vue';
|
|
19
|
+
import { nextTick, reactive, toRaw } from 'vue';
|
|
20
20
|
import { cloneDeep, isEmpty, isEqual, isObject, mergeWith, uniq } from 'lodash-es';
|
|
21
21
|
|
|
22
22
|
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
guid,
|
|
31
31
|
isPage,
|
|
32
32
|
isPageFragment,
|
|
33
|
+
isPageOrFragment,
|
|
33
34
|
setValueByKeyPath,
|
|
34
35
|
traverseNode,
|
|
35
36
|
} from '@tmagic/utils';
|
|
@@ -272,7 +273,7 @@ class Editor extends BaseService {
|
|
|
272
273
|
public isOnDifferentPage(node: MNode): boolean {
|
|
273
274
|
const currentPageId = this.get('page')?.id;
|
|
274
275
|
if (currentPageId === undefined || currentPageId === null) return false;
|
|
275
|
-
if (
|
|
276
|
+
if (isPageOrFragment(node)) {
|
|
276
277
|
return `${node.id}` !== `${currentPageId}`;
|
|
277
278
|
}
|
|
278
279
|
const nodePage = this.getNodeInfo(node.id, false).page;
|
|
@@ -413,11 +414,11 @@ class Editor extends BaseService {
|
|
|
413
414
|
|
|
414
415
|
if (!curNode) throw new Error('当前选中节点为空');
|
|
415
416
|
|
|
416
|
-
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !(
|
|
417
|
+
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !isPageOrFragment(node)) {
|
|
417
418
|
throw new Error('app下不能添加组件');
|
|
418
419
|
}
|
|
419
420
|
|
|
420
|
-
if (parent.id !== curNode.id && !(
|
|
421
|
+
if (parent.id !== curNode.id && !isPageOrFragment(node)) {
|
|
421
422
|
const index = parent.items.indexOf(curNode);
|
|
422
423
|
parent.items?.splice(index + 1, 0, node);
|
|
423
424
|
} else {
|
|
@@ -487,7 +488,7 @@ class Editor extends BaseService {
|
|
|
487
488
|
const newNodes = await Promise.all(
|
|
488
489
|
addNodes.map((node) => {
|
|
489
490
|
const root = this.get('root');
|
|
490
|
-
if ((
|
|
491
|
+
if (isPageOrFragment(node) && root) {
|
|
491
492
|
return this.doAdd(node, root);
|
|
492
493
|
}
|
|
493
494
|
const parentNode = parent ?? getAddParent(node);
|
|
@@ -523,7 +524,7 @@ class Editor extends BaseService {
|
|
|
523
524
|
}
|
|
524
525
|
}
|
|
525
526
|
|
|
526
|
-
if (!(
|
|
527
|
+
if (!isPageOrFragment(newNodes[0])) {
|
|
527
528
|
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
|
528
529
|
if (!doNotPushHistory) {
|
|
529
530
|
const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
|
|
@@ -554,8 +555,8 @@ class Editor extends BaseService {
|
|
|
554
555
|
doNotPushHistory,
|
|
555
556
|
});
|
|
556
557
|
|
|
557
|
-
// 页面 / 页面片新增不入历史栈(见上方
|
|
558
|
-
const addedPages = newNodes.filter((node) =>
|
|
558
|
+
// 页面 / 页面片新增不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
|
559
|
+
const addedPages = newNodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
|
559
560
|
if (addedPages.length) {
|
|
560
561
|
historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
|
|
561
562
|
}
|
|
@@ -578,12 +579,14 @@ class Editor extends BaseService {
|
|
|
578
579
|
|
|
579
580
|
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
|
|
580
581
|
|
|
581
|
-
parent.items?.splice(index, 1);
|
|
582
582
|
const stage = this.get('stage');
|
|
583
|
-
|
|
583
|
+
const currentPage = this.get('page');
|
|
584
|
+
const isDeletingCurrentPage = isPageOrFragment(node) && !!currentPage && `${currentPage.id}` === `${node.id}`;
|
|
585
|
+
|
|
586
|
+
parent.items?.splice(index, 1);
|
|
584
587
|
|
|
585
588
|
// 始终清理已删除节点在 state 中的残留引用:
|
|
586
|
-
// - 即使后续会调用
|
|
589
|
+
// - 即使后续会调用 select 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点
|
|
587
590
|
const selectedNodes = this.get('nodes');
|
|
588
591
|
const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`);
|
|
589
592
|
if (removedSelectedIndex !== -1) {
|
|
@@ -591,38 +594,44 @@ class Editor extends BaseService {
|
|
|
591
594
|
nextSelected.splice(removedSelectedIndex, 1);
|
|
592
595
|
this.set('nodes', nextSelected);
|
|
593
596
|
}
|
|
594
|
-
if (isPage(node) || isPageFragment(node)) {
|
|
595
|
-
const currentPage = this.get('page');
|
|
596
|
-
if (currentPage && `${currentPage.id}` === `${node.id}`) {
|
|
597
|
-
this.set('page', null);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const selectDefault = async (pages: MNode[]) => {
|
|
602
|
-
if (pages[0]) {
|
|
603
|
-
await this.select(pages[0]);
|
|
604
|
-
stage?.select(pages[0].id);
|
|
605
|
-
} else {
|
|
606
|
-
this.selectRoot();
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
597
|
|
|
598
|
+
const removeData = { id: node.id, parentId: parent.id, root: cloneDeep(root) };
|
|
610
599
|
const rootItems = root.items || [];
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
600
|
+
// 删非当前页时画布不应该变动,所以只有删的是当前页才重新选中
|
|
601
|
+
const shouldReselect = isDeletingCurrentPage && !doNotSelect && !doNotSwitchPage;
|
|
602
|
+
// 还有剩余页面才能切过去,否则退回选中 root
|
|
603
|
+
const shouldSwitchPage = shouldReselect && rootItems.length > 0;
|
|
604
|
+
|
|
605
|
+
if (isPageOrFragment(node)) {
|
|
606
|
+
if (isPage(node)) {
|
|
607
|
+
this.state.pageLength -= 1;
|
|
608
|
+
} else {
|
|
609
|
+
this.state.pageFragmentLength -= 1;
|
|
618
610
|
}
|
|
619
|
-
} else if (isPageFragment(node)) {
|
|
620
|
-
this.state.pageFragmentLength -= 1;
|
|
621
611
|
|
|
622
|
-
if (
|
|
623
|
-
|
|
612
|
+
if (shouldSwitchPage) {
|
|
613
|
+
// runtime 收到页面删除时会销毁当前渲染的 page 实例,必须先把画布切到剩余页面再通知删除,
|
|
614
|
+
// 否则画布会被清空,且要等下一次 updatePageId 才能恢复。
|
|
615
|
+
// nextTick 用于等 Stage 的 page watch 把新页面 id 同步给 runtime。
|
|
616
|
+
await this.select(rootItems[0]);
|
|
617
|
+
stage?.select(rootItems[0].id);
|
|
618
|
+
await nextTick();
|
|
619
|
+
stage?.remove(removeData);
|
|
620
|
+
} else {
|
|
621
|
+
// page 置空会让 Workspace 卸载 Stage 并销毁 renderer,删除通知必须在卸载前发出,
|
|
622
|
+
// 且不能 await:runtime 未 ready 时 getRuntime 的监听会随 destroy 一起被移除,永远不会 resolve
|
|
623
|
+
stage?.remove(removeData);
|
|
624
|
+
|
|
625
|
+
if (shouldReselect) {
|
|
626
|
+
// 页面已全部删完
|
|
627
|
+
this.selectRoot();
|
|
628
|
+
} else if (isDeletingCurrentPage) {
|
|
629
|
+
this.set('page', null);
|
|
630
|
+
}
|
|
624
631
|
}
|
|
625
632
|
} else {
|
|
633
|
+
stage?.remove(removeData);
|
|
634
|
+
|
|
626
635
|
if (!doNotSelect) {
|
|
627
636
|
await this.select(parent);
|
|
628
637
|
stage?.select(parent.id);
|
|
@@ -664,7 +673,7 @@ class Editor extends BaseService {
|
|
|
664
673
|
|
|
665
674
|
const removedItems: StepDiffItem<MNode>[] = [];
|
|
666
675
|
let pageForOp: { name: string; id: Id } | null = null;
|
|
667
|
-
if (!(
|
|
676
|
+
if (!isPageOrFragment(nodes[0])) {
|
|
668
677
|
for (const n of nodes) {
|
|
669
678
|
const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
|
|
670
679
|
if (parent && curNode) {
|
|
@@ -702,8 +711,8 @@ class Editor extends BaseService {
|
|
|
702
711
|
this.emit('remove', nodes);
|
|
703
712
|
this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
|
|
704
713
|
|
|
705
|
-
// 页面 / 页面片删除不入历史栈(见上方
|
|
706
|
-
const removedPages = nodes.filter((node) =>
|
|
714
|
+
// 页面 / 页面片删除不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
|
715
|
+
const removedPages = nodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
|
707
716
|
if (removedPages.length) {
|
|
708
717
|
historyService.notifyPageStructureChange({ add: [], remove: removedPages });
|
|
709
718
|
}
|
|
@@ -764,7 +773,7 @@ class Editor extends BaseService {
|
|
|
764
773
|
}
|
|
765
774
|
|
|
766
775
|
// 只有被更新节点正好是当前选中页面时才同步 state.page,避免「更新非当前页」误将编辑器切到该页
|
|
767
|
-
if (
|
|
776
|
+
if (isPageOrFragment(newConfig)) {
|
|
768
777
|
const currentPage = this.get('page');
|
|
769
778
|
if (currentPage && `${currentPage.id}` === `${newConfig.id}`) {
|
|
770
779
|
this.set('page', newConfig as MPage | MPageFragment);
|
|
@@ -1139,7 +1148,7 @@ class Editor extends BaseService {
|
|
|
1139
1148
|
}: DslOpOptions = {},
|
|
1140
1149
|
): Promise<MNode | MNode[]> {
|
|
1141
1150
|
const isBatch = Array.isArray(config);
|
|
1142
|
-
const configs = (isBatch ? config : [config]).filter((item) => !(
|
|
1151
|
+
const configs = (isBatch ? config : [config]).filter((item) => !isPageOrFragment(item));
|
|
1143
1152
|
|
|
1144
1153
|
if (configs.length === 0) {
|
|
1145
1154
|
throw new Error('没有可移动的节点');
|
|
@@ -1760,7 +1769,7 @@ class Editor extends BaseService {
|
|
|
1760
1769
|
private getPageOfNode(id: Id): MPage | MPageFragment | null {
|
|
1761
1770
|
const { node, page } = this.getNodeInfo(id, false);
|
|
1762
1771
|
if (page) return page;
|
|
1763
|
-
if (node && (
|
|
1772
|
+
if (node && isPageOrFragment(node)) return node as MPage | MPageFragment;
|
|
1764
1773
|
return null;
|
|
1765
1774
|
}
|
|
1766
1775
|
|
package/src/services/history.ts
CHANGED
|
@@ -357,7 +357,7 @@ class History extends BaseService {
|
|
|
357
357
|
* 派发「页面 / 页面片结构变更」事件(`page-structure-change`)。
|
|
358
358
|
*
|
|
359
359
|
* 常规 `editorService.add` / `remove` 页面节点不会写入 `page` 历史栈(见 editor.add / remove 中
|
|
360
|
-
* 对
|
|
360
|
+
* 对 isPageOrFragment 的分支),因此不会产生任何 historyService 事件。该方法用于在这些
|
|
361
361
|
* 场景(以及 setRoot 整体替换 DSL 增删页面)下,向外统一通知页面结构的增删变化,供业务方感知。
|
|
362
362
|
*
|
|
363
363
|
* 一次操作涉及多个页面时,调用方应把本次增删的页面**合并为一个 change 一次性传入**,
|