feat(dep,editor,data-source,schema): 优化编辑器中依赖收集性能

This commit is contained in:
roymondchen 2024-05-29 19:32:16 +08:00
parent cc8ec39dad
commit ee269917f8
27 changed files with 953 additions and 490 deletions

View File

@ -74,7 +74,7 @@
"semver": "^7.3.7",
"serialize-javascript": "^6.0.0",
"shx": "^0.3.4",
"typescript": "^5.4.2",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vitepress": "1.2.2",
"vitest": "^1.6.0",

View File

@ -154,7 +154,7 @@ export const compliedIteratorItems = (itemData: any, items: MNode[], dsId: strin
}),
);
watcher.collect(items, true);
watcher.collect(items, {}, true);
const { deps } = watcher.getTarget(dsId);
if (!Object.keys(deps).length) {

View File

@ -2,6 +2,13 @@ import type { DepData } from '@tmagic/schema';
import { DepTargetType, type IsTarget, type TargetOptions } from './types';
export interface DepUpdateOptions {
id: string | number;
name: string;
key: string | number;
data: Record<string, any>;
}
/**
*
*
@ -49,20 +56,20 @@ export default class Target {
/**
*
* @param node
* @param option
* @param key key配置了这个目标的id
*/
public updateDep(node: Record<string | number, any>, key: string | number) {
const dep = this.deps[node.id] || {
name: node.name,
public updateDep({ id, name, key, data }: DepUpdateOptions) {
const dep = this.deps[id] || {
name,
keys: [],
};
if (node.name) {
dep.name = node.name;
}
dep.name = name;
this.deps[node.id] = dep;
dep.data = data;
this.deps[id] = dep;
if (dep.keys.indexOf(key) === -1) {
dep.keys.push(key);
@ -75,15 +82,15 @@ export default class Target {
* @param key key需要移除key
* @returns void
*/
public removeDep(node?: Record<string | number, any>, key?: string | number) {
if (!node) {
public removeDep(id?: string | number, key?: string | number) {
if (typeof id === 'undefined') {
Object.keys(this.deps).forEach((depKey) => {
delete this.deps[depKey];
});
return;
}
const dep = this.deps[node.id];
const dep = this.deps[id];
if (!dep) return;
@ -92,10 +99,10 @@ export default class Target {
dep.keys.splice(index, 1);
if (dep.keys.length === 0) {
delete this.deps[node.id];
delete this.deps[id];
}
} else {
delete this.deps[node.id];
delete this.deps[id];
}
}
@ -105,8 +112,8 @@ export default class Target {
* @param key key
* @returns boolean
*/
public hasDep(node: Record<string | number, any>, key: string | number) {
const dep = this.deps[node.id];
public hasDep(id: string | number, key: string | number) {
const dep = this.deps[id];
return Boolean(dep?.keys.find((d) => d === key));
}

View File

@ -1,15 +1,23 @@
import { isObject } from '@tmagic/utils';
import type Target from './Target';
import { DepTargetType, type TargetList } from './types';
import { type DepExtendedData, DepTargetType, type TargetList, TargetNode } from './types';
import { traverseTarget } from './utils';
export default class Watcher {
private targetsList: TargetList = {};
private childrenProp = 'items';
private idProp = 'id';
private nameProp = 'name';
constructor(options?: { initialTargets?: TargetList }) {
constructor(options?: { initialTargets?: TargetList; childrenProp?: string }) {
if (options?.initialTargets) {
this.targetsList = options.initialTargets;
}
if (options?.childrenProp) {
this.childrenProp = options.childrenProp;
}
}
public getTargetsList() {
@ -106,58 +114,58 @@ export default class Watcher {
* @param deep
* @param type
*/
public collect(nodes: Record<string | number, any>[], deep = false, type?: DepTargetType) {
Object.values(this.targetsList).forEach((targets) => {
Object.values(targets).forEach((target) => {
if ((!type && !target.isCollectByDefault) || (type && target.type !== type)) return;
nodes.forEach((node) => {
// 先删除原有依赖,重新收集
target.removeDep(node);
this.collectItem(node, target, deep);
});
});
public collect(nodes: TargetNode[], depExtendedData: DepExtendedData = {}, deep = false, type?: DepTargetType) {
this.collectByCallback(nodes, type, ({ node, target }) => {
this.removeTargetDep(target, node);
this.collectItem(node, target, depExtendedData, deep);
});
}
public collectByCallback(
nodes: TargetNode[],
type: DepTargetType | undefined,
cb: (data: { node: TargetNode; target: Target }) => void,
) {
traverseTarget(
this.targetsList,
(target) => {
if (!type && !target.isCollectByDefault) {
return;
}
nodes.forEach((node) => {
cb({ node, target });
});
},
type,
);
}
/**
*
* @param nodes
*/
public clear(nodes?: Record<string | number, any>[]) {
const clearedItemsNodeIds: (string | number)[] = [];
Object.values(this.targetsList).forEach((targets) => {
Object.values(targets).forEach((target) => {
if (nodes) {
nodes.forEach((node) => {
target.removeDep(node);
public clear(nodes?: TargetNode[], type?: DepTargetType) {
let { targetsList } = this;
if (Array.isArray(node.items) && node.items.length && !clearedItemsNodeIds.includes(node.id)) {
clearedItemsNodeIds.push(node.id);
this.clear(node.items);
}
});
} else {
target.removeDep();
}
});
});
}
if (type) {
targetsList = {
[type]: this.getTargets(type),
};
}
/**
*
* @param type
* @param nodes
*/
public clearByType(type: DepTargetType, nodes?: Record<string | number, any>[]) {
const clearedItemsNodeIds: (string | number)[] = [];
const targetList = this.getTargets(type);
Object.values(targetList).forEach((target) => {
traverseTarget(targetsList, (target) => {
if (nodes) {
nodes.forEach((node) => {
target.removeDep(node);
if (Array.isArray(node.items) && node.items.length && !clearedItemsNodeIds.includes(node.id)) {
clearedItemsNodeIds.push(node.id);
this.clear(node.items);
target.removeDep(node[this.idProp]);
if (
Array.isArray(node[this.childrenProp]) &&
node[this.childrenProp].length &&
!clearedItemsNodeIds.includes(node[this.idProp])
) {
clearedItemsNodeIds.push(node[this.idProp]);
this.clear(node[this.childrenProp]);
}
});
} else {
@ -166,14 +174,28 @@ export default class Watcher {
});
}
private collectItem(node: Record<string | number, any>, target: Target, deep = false) {
/**
*
* @param type
* @param nodes
*/
public clearByType(type: DepTargetType, nodes?: TargetNode[]) {
this.clear(nodes, type);
}
public collectItem(node: TargetNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
const collectTarget = (config: Record<string | number, any>, prop = '') => {
const doCollect = (key: string, value: any) => {
const keyIsItems = key === 'items';
const keyIsItems = key === this.childrenProp;
const fullKey = prop ? `${prop}.${key}` : key;
if (target.isTarget(fullKey, value)) {
target.updateDep(node, fullKey);
target.updateDep({
id: node[this.idProp],
name: `${node[this.nameProp] || node[this.idProp]}`,
data: depExtendedData,
key: fullKey,
});
} else if (!keyIsItems && Array.isArray(value)) {
value.forEach((item, index) => {
if (isObject(item)) {
@ -186,7 +208,7 @@ export default class Watcher {
if (keyIsItems && deep && Array.isArray(value)) {
value.forEach((child) => {
this.collectItem(child, target, deep);
this.collectItem(child, target, depExtendedData, deep);
});
}
};
@ -199,4 +221,13 @@ export default class Watcher {
collectTarget(node);
}
public removeTargetDep(target: Target, node: TargetNode, key?: string | number) {
target.removeDep(node[this.idProp], key);
if (typeof key === 'undefined' && Array.isArray(node[this.childrenProp]) && node[this.childrenProp].length) {
node[this.childrenProp].forEach((item: TargetNode) => {
this.removeTargetDep(target, item, key);
});
}
}
}

View File

@ -13,12 +13,6 @@ export enum DepTargetType {
DATA_SOURCE_METHOD = 'data-source-method',
/** 数据源条件 */
DATA_SOURCE_COND = 'data-source-cond',
/** 复制组件时关联的组件 */
RELATED_COMP_WHEN_COPY = 'related-comp-when-copy',
/** 复制组件时关联的代码块 */
RELATED_CODE_WHEN_COPY = 'related-code-when-copy',
/** 复制组件时关联的数据源 */
RELATED_DS_WHEN_COPY = 'related-ds-when-copy',
}
export type IsTarget = (key: string | number, value: any) => boolean;
@ -47,3 +41,11 @@ export interface TargetList {
[targetId: string | number]: Target;
};
}
export interface TargetNode {
readonly id: string | number;
readonly name?: string;
readonly [key: string | number]: any;
}
export type DepExtendedData = Record<string, any>;

View File

@ -10,7 +10,7 @@ import {
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils';
import Target from './Target';
import { CustomTargetOptions, DepTargetType } from './types';
import { DepTargetType, type TargetList } from './types';
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) =>
new Target({
@ -32,13 +32,6 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initi
},
});
export const createRelatedTargetForCopy = (options: CustomTargetOptions, type: DepTargetType) =>
new Target({
id: type,
type,
...options,
});
/**
* ['array'] ['array', '0'] ['array', '0', 'a'] false
* ['array', 'a'] true
@ -199,3 +192,14 @@ export const createDataSourceMethodTarget = (ds: DataSourceSchema, initialDeps:
return Boolean(ds?.methods?.find((method) => method.name === value[1]));
},
});
export const traverseTarget = (targetsList: TargetList, cb: (target: Target) => void, type?: DepTargetType) => {
Object.values(targetsList).forEach((targets) => {
Object.values(targets).forEach((target) => {
if (type && target.type !== type) {
return;
}
cb(target);
});
});
};

View File

@ -223,6 +223,7 @@ describe('Watcher', () => {
],
},
],
{},
true,
);

View File

@ -1,5 +1,4 @@
import type { EventOption } from '@tmagic/core';
import type { CustomTargetOptions } from '@tmagic/dep';
import type { FormConfig, FormState } from '@tmagic/form';
import type { DataSourceSchema, Id, MApp, MNode } from '@tmagic/schema';
import StageCore, {
@ -70,12 +69,6 @@ export interface EditorProps {
codeOptions?: { [key: string]: any };
/** 禁用鼠标左键按下时就开始拖拽,需要先选中再可以拖拽 */
disabledDragStart?: boolean;
/** 自定义依赖收集器,复制组件时会将关联组件一并复制 */
collectorOptions?: CustomTargetOptions;
/** 自定义依赖收集器,复制组件时会将关联代码块一并复制 */
collectorOptionsForCode?: CustomTargetOptions;
/** 自定义依赖收集器,复制组件时会将关联数据源一并复制 */
collectorOptionsForDataSource?: CustomTargetOptions;
/** 标尺配置 */
guidesOptions?: Partial<GuidesOptions>;
/** 禁止多选 */

View File

@ -7,12 +7,11 @@ import {
createDataSourceCondTarget,
createDataSourceMethodTarget,
createDataSourceTarget,
createRelatedTargetForCopy,
DepTargetType,
Target,
} from '@tmagic/dep';
import type { CodeBlockContent, DataSourceSchema, Id, MApp, MNode, MPage, MPageFragment } from '@tmagic/schema';
import { getNodes } from '@tmagic/utils';
import { getNodes, isPage } from '@tmagic/utils';
import PropsPanel from './layouts/PropsPanel.vue';
import { EditorProps } from './editorProps';
@ -268,17 +267,12 @@ export const initServiceEvents = (
}
};
const depUpdateHandler = (node: MNode) => {
updateNodeWhenDataSourceChange([node]);
};
const collectedHandler = (nodes: MNode[]) => {
updateNodeWhenDataSourceChange(nodes);
};
depService.on('add-target', targetAddHandler);
depService.on('remove-target', targetRemoveHandler);
depService.on('dep-update', depUpdateHandler);
depService.on('collected', collectedHandler);
const initDataSourceDepTarget = (ds: DataSourceSchema) => {
@ -307,7 +301,9 @@ export const initServiceEvents = (
});
if (Array.isArray(value.items)) {
depService.collect(value.items, true);
value.items.forEach((page) => {
depService.collectIdle([page], { pageId: page.id }, true);
});
} else {
depService.clear();
delete value.dataSourceDeps;
@ -334,14 +330,28 @@ export const initServiceEvents = (
}
};
const collectIdle = (nodes: MNode[], deep: boolean) => {
nodes.forEach((node) => {
let pageId: Id | undefined;
if (isPage(node)) {
pageId = node.id;
} else {
const info = editorService.getNodeInfo(node.id);
pageId = info.page?.id;
}
depService.collectIdle([node], { pageId }, deep);
});
};
// 新增节点,收集依赖
const nodeAddHandler = (nodes: MNode[]) => {
depService.collect(nodes);
collectIdle(nodes, true);
};
// 节点更新,收集依赖
const nodeUpdateHandler = (nodes: MNode[]) => {
depService.collect(nodes);
collectIdle(nodes, false);
};
// 节点删除,清除对齐的依赖收集
@ -351,7 +361,7 @@ export const initServiceEvents = (
// 由于历史记录变化是更新整个page所以历史记录变化时需要重新收集依赖
const historyChangeHandler = (page: MPage | MPageFragment) => {
depService.collect([page], true);
depService.collectIdle([page], { pageId: page.id }, true);
};
editorService.on('history-change', historyChangeHandler);
@ -386,7 +396,9 @@ export const initServiceEvents = (
removeDataSourceTarget(config.id);
initDataSourceDepTarget(config);
depService.collect(root?.items || [], true);
(root?.items || []).forEach((page) => {
depService.collectIdle([page], { pageId: page.id }, true);
});
const targets = depService.getTargets(DepTargetType.DATA_SOURCE);
@ -410,25 +422,9 @@ export const initServiceEvents = (
dataSourceService.on('update', dataSourceUpdateHandler);
dataSourceService.on('remove', dataSourceRemoveHandler);
// 初始化复制组件相关的依赖收集器
if (props.collectorOptions && !depService.hasTarget(DepTargetType.RELATED_COMP_WHEN_COPY)) {
depService.addTarget(createRelatedTargetForCopy(props.collectorOptions, DepTargetType.RELATED_COMP_WHEN_COPY));
}
if (props.collectorOptionsForCode && !depService.hasTarget(DepTargetType.RELATED_CODE_WHEN_COPY)) {
depService.addTarget(
createRelatedTargetForCopy(props.collectorOptionsForCode, DepTargetType.RELATED_CODE_WHEN_COPY),
);
}
if (props.collectorOptionsForDataSource && !depService.hasTarget(DepTargetType.RELATED_DS_WHEN_COPY)) {
depService.addTarget(
createRelatedTargetForCopy(props.collectorOptionsForDataSource, DepTargetType.RELATED_DS_WHEN_COPY),
);
}
onBeforeUnmount(() => {
depService.off('add-target', targetAddHandler);
depService.off('remove-target', targetRemoveHandler);
depService.off('dep-update', depUpdateHandler);
depService.off('collected', collectedHandler);
editorService.off('history-change', historyChangeHandler);

View File

@ -58,26 +58,44 @@ const { codeBlockService, depService, editorService } = services || {};
//
const codeList = computed<TreeNodeData[]>(() =>
Object.values(depService?.getTargets(DepTargetType.CODE_BLOCK) || {}).map((target) => {
Object.entries(codeBlockService?.getCodeDsl() || {}).map(([codeId, code]) => {
const target = depService?.getTarget(codeId, DepTargetType.CODE_BLOCK);
//
const pageList: TreeNodeData[] =
editorService?.get('root')?.items.map((page) => ({
name: page.devconfig?.tabName || page.name,
type: 'node',
id: `${codeId}_${page.id}`,
key: page.id,
items: [],
})) || [];
//
const compNodes: TreeNodeData[] = Object.entries(target.deps).map(([id, dep]) => ({
name: dep.name,
type: 'node',
id: `${target.id}_${id}`,
key: id,
items: dep.keys.map((key) => {
const data: TreeNodeData = { name: `${key}`, id: `${target.id}_${id}_${key}`, type: 'key' };
return data;
}),
}));
if (target) {
Object.entries(target.deps).forEach(([id, dep]) => {
const page = pageList.find((page) => page.key === dep.data?.pageId);
page?.items?.push({
name: dep.name,
type: 'node',
id: `${page.id}_${id}`,
key: id,
items: dep.keys.map((key) => {
const data: TreeNodeData = { name: `${key}`, id: `${target.id}_${id}_${key}`, type: 'key' };
return data;
}),
});
});
}
const data: TreeNodeData = {
id: target.id,
key: target.id,
name: target.name,
id: codeId,
key: codeId,
name: code.name,
type: 'code',
codeBlockContent: codeBlockService?.getCodeContentById(target.id),
items: compNodes,
codeBlockContent: codeBlockService?.getCodeContentById(codeId),
//
items: pageList.length > 1 ? pageList.filter((page) => page.items?.length) : pageList[0]?.items || [],
};
return data;

View File

@ -81,16 +81,19 @@ const getNodeTreeConfig = (id: string, dep: DepData[string], type?: string, pare
* @param deps 依赖
* @param type 依赖类型
*/
const mergeChildren = (dsId: Id, items: any[], deps: DepData, type?: string) => {
const mergeChildren = (dsId: Id, pageItems: any[], deps: DepData, type?: string) => {
Object.entries(deps).forEach(([id, dep]) => {
//
const page = pageItems.find((page) => page.key === dep.data?.pageId);
//
const nodeItem = items.find((item) => item.key === id);
const nodeItem = page?.items.find((item: any) => item.key === id);
// key
if (nodeItem) {
nodeItem.items = nodeItem.items.concat(getKeyTreeConfig(dep, type, nodeItem.key));
} else {
//
items.push(getNodeTreeConfig(id, dep, type, dsId));
page?.items.push(getNodeTreeConfig(id, dep, type, page.id));
}
});
};
@ -101,7 +104,15 @@ const list = computed(() =>
const dsMethodDeps = dsMethodDep.value[ds.id]?.deps || {};
const dsCondDeps = dsCondDep.value[ds.id]?.deps || {};
const items: any[] = [];
const items =
editorService?.get('root')?.items.map((page) => ({
name: page.devconfig?.tabName || page.name,
type: 'node',
id: `${ds.id}_${page.id}`,
key: page.id,
items: [],
})) || [];
// key/nodemethodcond
mergeChildren(ds.id, items, dsDeps);
mergeChildren(ds.id, items, dsMethodDeps, 'method');
@ -112,7 +123,8 @@ const list = computed(() =>
key: ds.id,
name: ds.title,
type: 'ds',
items,
//
items: items.length > 1 ? items.filter((page) => page.items.length) : items[0]?.items || [],
};
}),
);

View File

@ -20,11 +20,10 @@ import { reactive } from 'vue';
import { cloneDeep, get, keys, pick } from 'lodash-es';
import type { Writable } from 'type-fest';
import { DepTargetType } from '@tmagic/dep';
import { type CustomTargetOptions, Target, Watcher } from '@tmagic/dep';
import type { ColumnConfig } from '@tmagic/form';
import type { CodeBlockContent, CodeBlockDSL, Id, MNode } from '@tmagic/schema';
import depService from '@editor/services/dep';
import editorService from '@editor/services/editor';
import storageService, { Protocol } from '@editor/services/storage';
import type { AsyncHookPlugin, CodeState } from '@editor/type';
@ -258,17 +257,26 @@ class CodeBlock extends BaseService {
* @param config
* @returns
*/
public copyWithRelated(config: MNode | MNode[]): void {
public copyWithRelated(config: MNode | MNode[], collectorOptions?: CustomTargetOptions): void {
const copyNodes: MNode[] = Array.isArray(config) ? config : [config];
// 关联的代码块也一并复制
depService.clearByType(DepTargetType.RELATED_CODE_WHEN_COPY);
depService.collect(copyNodes, true, DepTargetType.RELATED_CODE_WHEN_COPY);
const customTarget = depService.getTarget(
DepTargetType.RELATED_CODE_WHEN_COPY,
DepTargetType.RELATED_CODE_WHEN_COPY,
);
const copyData: CodeBlockDSL = {};
if (customTarget) {
if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
const customTarget = new Target({
id: 'related-code-when-copy',
...collectorOptions,
});
const coperWatcher = new Watcher();
coperWatcher.addTarget(customTarget);
coperWatcher.collect(
copyNodes.map((node) => ({ id: `${node.id}`, name: `${node.name || node.id}` })),
{},
true,
);
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
const node = editorService.getNodeById(nodeId);
if (!node) return;

View File

@ -3,12 +3,11 @@ import { cloneDeep, get } from 'lodash-es';
import { Writable } from 'type-fest';
import type { EventOption } from '@tmagic/core';
import { DepTargetType } from '@tmagic/dep';
import { type CustomTargetOptions, Target, Watcher } from '@tmagic/dep';
import type { FormConfig } from '@tmagic/form';
import type { DataSourceSchema, Id, MNode } from '@tmagic/schema';
import { guid, toLine } from '@tmagic/utils';
import depService from '@editor/services/dep';
import editorService from '@editor/services/editor';
import storageService, { Protocol } from '@editor/services/storage';
import type { DatasourceTypeOption, SyncHookPlugin } from '@editor/type';
@ -163,13 +162,26 @@ class DataSource extends BaseService {
* @param config
* @returns
*/
public copyWithRelated(config: MNode | MNode[]): void {
public copyWithRelated(config: MNode | MNode[], collectorOptions?: CustomTargetOptions): void {
const copyNodes: MNode[] = Array.isArray(config) ? config : [config];
depService.clearByType(DepTargetType.RELATED_DS_WHEN_COPY);
depService.collect(copyNodes, true, DepTargetType.RELATED_DS_WHEN_COPY);
const customTarget = depService.getTarget(DepTargetType.RELATED_DS_WHEN_COPY, DepTargetType.RELATED_DS_WHEN_COPY);
const copyData: DataSourceSchema[] = [];
if (customTarget) {
if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
const customTarget = new Target({
id: 'related-ds-when-copy',
...collectorOptions,
});
const coperWatcher = new Watcher();
coperWatcher.addTarget(customTarget);
coperWatcher.collect(
copyNodes.map((node) => ({ id: `${node.id}`, name: `${node.name || node.id}` })),
{},
true,
);
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
const node = editorService.getNodeById(nodeId);
if (!node) return;

View File

@ -17,18 +17,35 @@
*/
import { reactive } from 'vue';
import { DepTargetType, type Target, Watcher } from '@tmagic/dep';
import { type DepExtendedData, DepTargetType, type Target, type TargetNode, Watcher } from '@tmagic/dep';
import type { Id, MNode } from '@tmagic/schema';
import { isPage } from '@tmagic/utils';
import { IdleTask } from '@editor/utils/idle-task';
import BaseService from './BaseService';
export interface DepEvents {
'add-target': [target: Target];
'remove-target': [id: string | number];
collected: [nodes: MNode[], deep: boolean];
}
const idleTask = new IdleTask<{ node: TargetNode; deep: boolean; target: Target }>();
class Dep extends BaseService {
private watcher = new Watcher({ initialTargets: reactive({}) });
public removeTargets(type: string = DepTargetType.DEFAULT) {
this.watcher.removeTargets(type);
this.emit('remove-target');
const targets = this.watcher.getTargets(type);
if (!targets) return;
for (const target of Object.values(targets)) {
this.emit('remove-target', target.id);
}
}
public getTargets(type: string = DepTargetType.DEFAULT) {
@ -46,18 +63,55 @@ class Dep extends BaseService {
public removeTarget(id: Id, type: string = DepTargetType.DEFAULT) {
this.watcher.removeTarget(id, type);
this.emit('remove-target');
this.emit('remove-target', id);
}
public clearTargets() {
this.watcher.clearTargets();
}
public collect(nodes: MNode[], deep = false, type?: DepTargetType) {
this.watcher.collect(nodes, deep, type);
public collect(nodes: MNode[], depExtendedData: DepExtendedData = {}, deep = false, type?: DepTargetType) {
this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
this.collectNode(node, target, depExtendedData, deep);
});
this.emit('collected', nodes, deep);
}
public collectIdle(nodes: MNode[], depExtendedData: DepExtendedData = {}, deep = false, type?: DepTargetType) {
this.watcher.collectByCallback(nodes, type, ({ node, target }) => {
idleTask.enqueueTask(
({ node, deep, target }) => {
this.collectNode(node, target, depExtendedData, deep);
},
{
node,
deep,
target,
},
);
});
idleTask.once('finish', () => {
this.emit('collected', nodes, deep);
});
}
collectNode(node: MNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
// 先删除原有依赖,重新收集
if (isPage(node)) {
Object.entries(target.deps).forEach(([depKey, dep]) => {
if (dep.data?.pageId && dep.data.pageId === depExtendedData.pageId) {
delete target.deps[depKey];
}
});
} else {
this.watcher.removeTargetDep(target, node);
}
this.watcher.collectItem(node, target, depExtendedData, deep);
}
public clear(nodes?: MNode[]) {
return this.watcher.clear(nodes);
}
@ -73,6 +127,24 @@ class Dep extends BaseService {
public hasSpecifiedTypeTarget(type: string = DepTargetType.DEFAULT): boolean {
return this.watcher.hasSpecifiedTypeTarget(type);
}
public on<Name extends keyof DepEvents, Param extends DepEvents[Name]>(
eventName: Name,
listener: (...args: Param) => void | Promise<void>,
) {
return super.on(eventName, listener as any);
}
public once<Name extends keyof DepEvents, Param extends DepEvents[Name]>(
eventName: Name,
listener: (...args: Param) => void | Promise<void>,
) {
return super.once(eventName, listener as any);
}
public emit<Name extends keyof DepEvents, Param extends DepEvents[Name]>(eventName: Name, ...args: Param) {
return super.emit(eventName, ...args);
}
}
export type DepService = Dep;

View File

@ -20,14 +20,13 @@ import { reactive, toRaw } from 'vue';
import { cloneDeep, get, isObject, mergeWith, uniq } from 'lodash-es';
import { Writable } from 'type-fest';
import { DepTargetType } from '@tmagic/dep';
import { type CustomTargetOptions, Target, Watcher } from '@tmagic/dep';
import type { Id, MApp, MComponent, MContainer, MNode, MPage, MPageFragment } from '@tmagic/schema';
import { NodeType } from '@tmagic/schema';
import { calcValueByFontsize, getNodePath, isNumber, isPage, isPageFragment, isPop } from '@tmagic/utils';
import BaseService from '@editor/services//BaseService';
import propsService from '@editor/services//props';
import depService from '@editor/services/dep';
import historyService from '@editor/services/history';
import storageService, { Protocol } from '@editor/services/storage';
import type {
@ -661,16 +660,26 @@ class Editor extends BaseService {
* @param config
* @returns
*/
public copyWithRelated(config: MNode | MNode[]): void {
public copyWithRelated(config: MNode | MNode[], collectorOptions?: CustomTargetOptions): void {
const copyNodes: MNode[] = Array.isArray(config) ? config : [config];
// 关联的组件也一并复制
depService.clearByType(DepTargetType.RELATED_COMP_WHEN_COPY);
depService.collect(copyNodes, true, DepTargetType.RELATED_COMP_WHEN_COPY);
const customTarget = depService.getTarget(
DepTargetType.RELATED_COMP_WHEN_COPY,
DepTargetType.RELATED_COMP_WHEN_COPY,
);
if (customTarget) {
// 初始化复制组件相关的依赖收集器
if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
const customTarget = new Target({
id: 'related-comp-when-copy',
...collectorOptions,
});
const coperWatcher = new Watcher();
coperWatcher.addTarget(customTarget);
coperWatcher.collect(
copyNodes.map((node) => ({ id: `${node.id}`, name: `${node.name || node.id}` })),
{},
true,
);
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
const node = this.getNodeById(nodeId);
if (!node) return;
@ -686,6 +695,7 @@ class Editor extends BaseService {
});
});
}
storageService.setItem(COPY_STORAGE_KEY, copyNodes, {
protocol: Protocol.OBJECT,
});
@ -696,7 +706,10 @@ class Editor extends BaseService {
* @param position
* @returns
*/
public async paste(position: PastePosition = {}): Promise<MNode | MNode[] | void> {
public async paste(
position: PastePosition = {},
collectorOptions?: CustomTargetOptions,
): Promise<MNode | MNode[] | void> {
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
if (!Array.isArray(config)) return;
@ -711,7 +724,11 @@ class Editor extends BaseService {
}
}
const pasteConfigs = await this.doPaste(config, position);
propsService.replaceRelateId(config, pasteConfigs);
if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
}
return this.add(pasteConfigs, parent);
}

View File

@ -20,12 +20,11 @@ import { reactive } from 'vue';
import { cloneDeep, mergeWith } from 'lodash-es';
import { Writable } from 'type-fest';
import { DepTargetType } from '@tmagic/dep';
import { type CustomTargetOptions, Target, Watcher } from '@tmagic/dep';
import type { FormConfig } from '@tmagic/form';
import type { Id, MComponent, MNode } from '@tmagic/schema';
import { getNodePath, getValueByKeyPath, guid, setValueByKeyPath, toLine } from '@tmagic/utils';
import depService from '@editor/services/dep';
import editorService from '@editor/services/editor';
import type { AsyncHookPlugin, PropsState, SyncHookPlugin } from '@editor/type';
import { fillConfig } from '@editor/utils/props';
@ -196,13 +195,21 @@ class Props extends BaseService {
* @param originConfigs
* @param targetConfigs
*/
public replaceRelateId(originConfigs: MNode[], targetConfigs: MNode[]) {
public replaceRelateId(originConfigs: MNode[], targetConfigs: MNode[], collectorOptions: CustomTargetOptions) {
const relateIdMap = this.getRelateIdMap();
if (Object.keys(relateIdMap).length === 0) return;
depService.clearByType(DepTargetType.RELATED_COMP_WHEN_COPY);
depService.collect(originConfigs, true, DepTargetType.RELATED_COMP_WHEN_COPY);
const target = depService.getTarget(DepTargetType.RELATED_COMP_WHEN_COPY, DepTargetType.RELATED_COMP_WHEN_COPY);
if (!target) return;
const target = new Target({
id: 'related-comp-when-copy',
...collectorOptions,
});
const coperWatcher = new Watcher();
coperWatcher.addTarget(target);
coperWatcher.collect(originConfigs);
originConfigs.forEach((config: MNode) => {
const newId = relateIdMap[config.id];
const path = getNodePath(newId, targetConfigs);
@ -219,7 +226,7 @@ class Props extends BaseService {
// 递归items
if (config.items && Array.isArray(config.items)) {
this.replaceRelateId(config.items, targetConfigs);
this.replaceRelateId(config.items, targetConfigs, collectorOptions);
}
});
}

View File

@ -0,0 +1,72 @@
import { EventEmitter } from 'events';
export interface IdleTaskEvents {
finish: [];
}
globalThis.requestIdleCallback =
globalThis.requestIdleCallback ||
function (cb) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
export class IdleTask<T = any> extends EventEmitter {
private taskList: {
handler: (data: T) => void;
data: T;
}[] = [];
private taskHandle: number | null = null;
public enqueueTask(taskHandler: (data: T) => void, taskData: T) {
this.taskList.push({
handler: taskHandler,
data: taskData,
});
if (!this.taskHandle) {
this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 10000 });
}
}
public on<Name extends keyof IdleTaskEvents, Param extends IdleTaskEvents[Name]>(
eventName: Name,
listener: (...args: Param) => void | Promise<void>,
) {
return super.on(eventName, listener as any);
}
public once<Name extends keyof IdleTaskEvents, Param extends IdleTaskEvents[Name]>(
eventName: Name,
listener: (...args: Param) => void | Promise<void>,
) {
return super.once(eventName, listener as any);
}
public emit<Name extends keyof IdleTaskEvents, Param extends IdleTaskEvents[Name]>(eventName: Name, ...args: Param) {
return super.emit(eventName, ...args);
}
private runTaskQueue(deadline: IdleDeadline) {
while ((deadline.timeRemaining() > 15 || deadline.didTimeout) && this.taskList.length) {
const task = this.taskList.shift();
task!.handler(task!.data);
}
if (this.taskList.length) {
this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 10000 });
} else {
this.taskHandle = 0;
this.emit('finish');
}
}
}

View File

@ -275,6 +275,7 @@ export interface DepData {
/** 组件名称 */
name: string;
keys: (string | number)[];
data?: Record<string, any>;
};
}

View File

@ -29,6 +29,6 @@
"devDependencies": {
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"typescript": "^5.4.2"
"typescript": "^5.4.5"
}
}

View File

@ -39,7 +39,7 @@
"@vue/compiler-sfc": "^3.4.27",
"sass": "^1.77.0",
"terser": "^5.31.0",
"typescript": "^5.4.2",
"typescript": "^5.4.5",
"unplugin-auto-import": "^0.12.0",
"unplugin-vue-components": "^0.22.11",
"vite": "^5.2.11"

View File

@ -1,5 +1,7 @@
import { Files, FolderOpened, Grid, PictureFilled, SwitchButton, Ticket, Tickets } from '@element-plus/icons-vue';
import type { ComponentGroup } from '@tmagic/editor';
export default [
{
title: '示例容器',
@ -90,4 +92,4 @@ export default [
},
],
},
];
] as ComponentGroup[];

View File

@ -1,6 +1,6 @@
<template>
<div class="editor-app">
<m-editor
<TMagicEditor
v-model="value"
ref="editor"
:menu="menu"
@ -17,7 +17,6 @@
:moveable-options="moveableOptions"
:auto-scroll-into-view="true"
:stage-rect="stageRect"
:collector-options="collectorOptions"
:layerContentMenu="contentMenuData"
:stageContentMenu="contentMenuData"
@props-submit-error="propsSubmitErrorHandler"
@ -25,7 +24,7 @@
<template #workspace-content>
<DeviceGroup ref="deviceGroup" v-model="stageRect"></DeviceGroup>
</template>
</m-editor>
</TMagicEditor>
<TMagicDialog v-model="previewVisible" destroy-on-close class="pre-viewer" title="预览" :width="stageRect?.width">
<iframe
@ -51,13 +50,12 @@ import { TMagicDialog, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
import {
ContentMenu,
COPY_STORAGE_KEY,
DatasourceTypeOption,
DepTargetType,
type DatasourceTypeOption,
editorService,
MenuBarData,
MenuButton,
MoveableOptions,
Services,
type MenuBarData,
type MenuButton,
type MoveableOptions,
type Services,
TMagicEditor,
} from '@tmagic/editor';
import type { MContainer, MNode } from '@tmagic/schema';
@ -136,16 +134,14 @@ const usePasteMenu = (menu?: Ref<InstanceType<typeof ContentMenu> | undefined>):
},
});
const contentMenuData = computed(() => [
const contentMenuData = computed<MenuButton[]>(() => [
{
type: 'button',
text: '复制(带关联信息)',
icon: markRaw(CopyDocument),
display: (services: Services) =>
services?.depService?.hasSpecifiedTypeTarget(DepTargetType.RELATED_COMP_WHEN_COPY) || false,
handler: (services: Services) => {
const nodes = services?.editorService?.get('nodes');
nodes && services?.editorService?.copyWithRelated(cloneDeep(nodes));
nodes && services?.editorService?.copyWithRelated(cloneDeep(nodes), collectorOptions);
nodes && services?.codeBlockService?.copyWithRelated(cloneDeep(nodes));
nodes && services?.dataSourceService?.copyWithRelated(cloneDeep(nodes));
},

798
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@
"@types/react-dom": "^17.0.11",
"@vitejs/plugin-legacy": "^5.4.0",
"@vitejs/plugin-react-refresh": "^1.3.1",
"typescript": "^5.4.2",
"typescript": "^5.4.5",
"vite": "^5.2.11"
}
}

View File

@ -46,7 +46,7 @@
"@vitejs/plugin-vue": "^5.0.4",
"@vue/compiler-sfc": "^3.4.27",
"rimraf": "^3.0.2",
"typescript": "^5.4.2",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vue-tsc": "^2.0.16"
}

View File

@ -45,6 +45,6 @@
"devDependencies": {
"@types/node": "^18.19.0",
"rimraf": "^3.0.2",
"typescript": "^5.4.2"
"typescript": "^5.4.5"
}
}

View File

@ -39,7 +39,7 @@
"rollup-plugin-external-globals": "^0.10.0",
"sass": "^1.77.0",
"terser": "^5.31.0",
"typescript": "^5.4.2",
"typescript": "^5.4.5",
"vite": "^5.2.11"
}
}