From ee269917f8acd7d32a124bc090ba38192e2c8d92 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Wed, 29 May 2024 19:32:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(dep,editor,data-source,schema):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=BC=96=E8=BE=91=E5=99=A8=E4=B8=AD=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E6=94=B6=E9=9B=86=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- packages/data-source/src/utils.ts | 2 +- packages/dep/src/Target.ts | 37 +- packages/dep/src/Watcher.ts | 123 ++- packages/dep/src/types.ts | 14 +- packages/dep/src/utils.ts | 20 +- packages/dep/tests/Watch.spec.ts | 1 + packages/editor/src/editorProps.ts | 7 - packages/editor/src/initService.ts | 52 +- .../sidebar/code-block/CodeBlockList.vue | 50 +- .../sidebar/data-source/DataSourceList.vue | 22 +- packages/editor/src/services/codeBlock.ts | 30 +- packages/editor/src/services/dataSource.ts | 26 +- packages/editor/src/services/dep.ts | 82 +- packages/editor/src/services/editor.ts | 43 +- packages/editor/src/services/props.ts | 23 +- packages/editor/src/utils/idle-task.ts | 72 ++ packages/schema/src/index.ts | 1 + packages/ui-react/package.json | 2 +- playground/package.json | 2 +- playground/src/configs/componentGroupList.ts | 4 +- playground/src/pages/Editor.vue | 22 +- pnpm-lock.yaml | 798 +++++++++++------- runtime/react/package.json | 2 +- runtime/tmagic-form/package.json | 2 +- runtime/vue-runtime-help/package.json | 2 +- runtime/vue3/package.json | 2 +- 27 files changed, 953 insertions(+), 490 deletions(-) create mode 100644 packages/editor/src/utils/idle-task.ts diff --git a/package.json b/package.json index ef2803ce..10a3050d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/data-source/src/utils.ts b/packages/data-source/src/utils.ts index 9343e6de..64e2292e 100644 --- a/packages/data-source/src/utils.ts +++ b/packages/data-source/src/utils.ts @@ -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) { diff --git a/packages/dep/src/Target.ts b/packages/dep/src/Target.ts index 5c3e2462..863dc4ed 100644 --- a/packages/dep/src/Target.ts +++ b/packages/dep/src/Target.ts @@ -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; +} + /** * 需要收集依赖的目标 * 例如:一个代码块可以为一个目标 @@ -49,20 +56,20 @@ export default class Target { /** * 更新依赖 - * @param node 节点配置 + * @param option 节点配置 * @param key 哪个key配置了这个目标的id */ - public updateDep(node: Record, 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, 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, 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)); } diff --git a/packages/dep/src/Watcher.ts b/packages/dep/src/Watcher.ts index 7e3a79bd..d0319028 100644 --- a/packages/dep/src/Watcher.ts +++ b/packages/dep/src/Watcher.ts @@ -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[], 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[]) { - 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[]) { 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, 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, 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); + }); + } + } } diff --git a/packages/dep/src/types.ts b/packages/dep/src/types.ts index ba5b5f5e..e5e34579 100644 --- a/packages/dep/src/types.ts +++ b/packages/dep/src/types.ts @@ -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; diff --git a/packages/dep/src/utils.ts b/packages/dep/src/utils.ts index c334db70..fccfa230 100644 --- a/packages/dep/src/utils.ts +++ b/packages/dep/src/utils.ts @@ -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); + }); + }); +}; diff --git a/packages/dep/tests/Watch.spec.ts b/packages/dep/tests/Watch.spec.ts index 03088a77..a0c8a554 100644 --- a/packages/dep/tests/Watch.spec.ts +++ b/packages/dep/tests/Watch.spec.ts @@ -223,6 +223,7 @@ describe('Watcher', () => { ], }, ], + {}, true, ); diff --git a/packages/editor/src/editorProps.ts b/packages/editor/src/editorProps.ts index 4c5fec11..bbaf9709 100644 --- a/packages/editor/src/editorProps.ts +++ b/packages/editor/src/editorProps.ts @@ -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; /** 禁止多选 */ diff --git a/packages/editor/src/initService.ts b/packages/editor/src/initService.ts index 9912ac3b..19fddd3b 100644 --- a/packages/editor/src/initService.ts +++ b/packages/editor/src/initService.ts @@ -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); diff --git a/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue b/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue index afb9be96..ec55f26e 100644 --- a/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue +++ b/packages/editor/src/layouts/sidebar/code-block/CodeBlockList.vue @@ -58,26 +58,44 @@ const { codeBlockService, depService, editorService } = services || {}; // 代码块列表 const codeList = computed(() => - 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; diff --git a/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue b/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue index dac96438..8dd4d60a 100644 --- a/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue +++ b/packages/editor/src/layouts/sidebar/data-source/DataSourceList.vue @@ -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/node、method、cond,是分开存储,这里将其合并展示 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 || [], }; }), ); diff --git a/packages/editor/src/services/codeBlock.ts b/packages/editor/src/services/codeBlock.ts index 6f922a68..05c9c7ee 100644 --- a/packages/editor/src/services/codeBlock.ts +++ b/packages/editor/src/services/codeBlock.ts @@ -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; diff --git a/packages/editor/src/services/dataSource.ts b/packages/editor/src/services/dataSource.ts index 95dfecd9..989e2e37 100644 --- a/packages/editor/src/services/dataSource.ts +++ b/packages/editor/src/services/dataSource.ts @@ -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; diff --git a/packages/editor/src/services/dep.ts b/packages/editor/src/services/dep.ts index 87331c60..6fee3597 100644 --- a/packages/editor/src/services/dep.ts +++ b/packages/editor/src/services/dep.ts @@ -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( + eventName: Name, + listener: (...args: Param) => void | Promise, + ) { + return super.on(eventName, listener as any); + } + + public once( + eventName: Name, + listener: (...args: Param) => void | Promise, + ) { + return super.once(eventName, listener as any); + } + + public emit(eventName: Name, ...args: Param) { + return super.emit(eventName, ...args); + } } export type DepService = Dep; diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index 2ef0fb57..aeca36e1 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -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 { + public async paste( + position: PastePosition = {}, + collectorOptions?: CustomTargetOptions, + ): Promise { 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); } diff --git a/packages/editor/src/services/props.ts b/packages/editor/src/services/props.ts index 86674f5c..227c76d5 100644 --- a/packages/editor/src/services/props.ts +++ b/packages/editor/src/services/props.ts @@ -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); } }); } diff --git a/packages/editor/src/utils/idle-task.ts b/packages/editor/src/utils/idle-task.ts new file mode 100644 index 00000000..b4f8733b --- /dev/null +++ b/packages/editor/src/utils/idle-task.ts @@ -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 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( + eventName: Name, + listener: (...args: Param) => void | Promise, + ) { + return super.on(eventName, listener as any); + } + + public once( + eventName: Name, + listener: (...args: Param) => void | Promise, + ) { + return super.once(eventName, listener as any); + } + + public emit(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'); + } + } +} diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index acba1a8c..2064d4b9 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -275,6 +275,7 @@ export interface DepData { /** 组件名称 */ name: string; keys: (string | number)[]; + data?: Record; }; } diff --git a/packages/ui-react/package.json b/packages/ui-react/package.json index b85e34e9..4f269514 100644 --- a/packages/ui-react/package.json +++ b/packages/ui-react/package.json @@ -29,6 +29,6 @@ "devDependencies": { "@types/react": "^17.0.37", "@types/react-dom": "^17.0.11", - "typescript": "^5.4.2" + "typescript": "^5.4.5" } } diff --git a/playground/package.json b/playground/package.json index ebe693f8..ec69402f 100644 --- a/playground/package.json +++ b/playground/package.json @@ -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" diff --git a/playground/src/configs/componentGroupList.ts b/playground/src/configs/componentGroupList.ts index 1b21f53e..792e866e 100644 --- a/playground/src/configs/componentGroupList.ts +++ b/playground/src/configs/componentGroupList.ts @@ -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[]; diff --git a/playground/src/pages/Editor.vue b/playground/src/pages/Editor.vue index fcdd4280..0925b704 100644 --- a/playground/src/pages/Editor.vue +++ b/playground/src/pages/Editor.vue @@ -1,6 +1,6 @@ - +