From 456692ff8ab2959519c6c858cd0098b06188560c Mon Sep 17 00:00:00 2001 From: parisma Date: Mon, 11 Dec 2023 19:31:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E6=9C=89=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E7=9A=84=E5=86=85=E5=AE=B9=E6=97=B6=E5=B1=95=E7=A4=BA=E7=B2=98?= =?UTF-8?q?=E8=B4=B4=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/dep/src/Target.ts | 5 ++ packages/dep/src/Watcher.ts | 15 ++++- packages/dep/src/types.ts | 12 ++++ packages/dep/src/utils.ts | 9 ++- packages/editor/src/editorProps.ts | 3 + packages/editor/src/initService.ts | 20 +++--- .../layouts/workspace/viewer/ViewerMenu.vue | 5 -- packages/editor/src/services/dep.ts | 8 ++- packages/editor/src/services/editor.ts | 48 ++++++++++++-- packages/editor/src/services/props.ts | 63 ++++++++++++++++++- packages/editor/src/type.ts | 1 + packages/editor/src/utils/content-menu.ts | 9 ++- packages/editor/src/utils/operator.ts | 1 - 13 files changed, 167 insertions(+), 32 deletions(-) diff --git a/packages/dep/src/Target.ts b/packages/dep/src/Target.ts index 96ba3c14..5c3e2462 100644 --- a/packages/dep/src/Target.ts +++ b/packages/dep/src/Target.ts @@ -29,11 +29,16 @@ export default class Target { * 实例:{ 'node_id': { name: 'node_name', keys: [ created, mounted ] } } */ public deps: DepData = {}; + /** + * 是否默认收集,默认为true,当值为false时需要传入type参数给collect方法才会被收集 + */ + public isCollectByDefault?: boolean; constructor(options: TargetOptions) { this.isTarget = options.isTarget; this.id = options.id; this.name = options.name; + this.isCollectByDefault = options.isCollectByDefault ?? true; if (options.type) { this.type = options.type; } diff --git a/packages/dep/src/Watcher.ts b/packages/dep/src/Watcher.ts index b2e79b45..c6a9d2d4 100644 --- a/packages/dep/src/Watcher.ts +++ b/packages/dep/src/Watcher.ts @@ -53,6 +53,15 @@ export default class Watcher { return Boolean(this.getTarget(id, type)); } + /** + * 判断是否存在指定类型的target + * @param type target type + * @returns boolean + */ + public hasSpecifiedTypeTarget(type: string = DepTargetType.DEFAULT): boolean { + return Object.keys(this.getTargets(type)).length > 0; + } + /** * 删除指定id的target * @param id target id @@ -95,10 +104,12 @@ export default class Watcher { * 收集依赖 * @param nodes 需要收集的节点 * @param deep 是否需要收集子节点 + * @param type 强制收集指定类型的依赖 */ - public collect(nodes: Record[], deep = false) { + 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); @@ -143,7 +154,7 @@ export default class Watcher { } else if (!keyIsItems && Array.isArray(value)) { value.forEach((item, index) => { if (isObject(item)) { - collectTarget(item, `${fullKey}.${index}`); + collectTarget(item, `${fullKey}[${index}]`); } }); } else if (isObject(value)) { diff --git a/packages/dep/src/types.ts b/packages/dep/src/types.ts index 6a01d276..a6f8d30f 100644 --- a/packages/dep/src/types.ts +++ b/packages/dep/src/types.ts @@ -13,6 +13,8 @@ export enum DepTargetType { DATA_SOURCE_METHOD = 'data-source-method', /** 数据源条件 */ DATA_SOURCE_COND = 'data-source-cond', + /** 复制组件时关联的组件 */ + RELATED_COMP_WHEN_COPY = 'related-comp-when-copy', } export type IsTarget = (key: string | number, value: any) => boolean; @@ -24,6 +26,16 @@ export interface TargetOptions { type?: string; name?: string; initialDeps?: DepData; + /** 是否默认收集,默认为true,当值为false时需要传入type参数给collect方法才会被收集 */ + isCollectByDefault?: boolean; +} + +export interface CustomTargetOptions { + isTarget: IsTarget; + name?: string; + initialDeps?: DepData; + /** 是否默认收集,默认为true,当值为false时需要传入type参数给collect方法才会被收集 */ + isCollectByDefault?: boolean; } export interface TargetList { diff --git a/packages/dep/src/utils.ts b/packages/dep/src/utils.ts index 1ee5071a..aeb45307 100644 --- a/packages/dep/src/utils.ts +++ b/packages/dep/src/utils.ts @@ -9,7 +9,7 @@ import { import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils'; import Target from './Target'; -import { DepTargetType } from './types'; +import { CustomTargetOptions, DepTargetType } from './types'; export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) => new Target({ @@ -31,6 +31,13 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initi }, }); +export const createRelatedCompTarget = (options: CustomTargetOptions) => + new Target({ + id: DepTargetType.RELATED_COMP_WHEN_COPY, + type: DepTargetType.RELATED_COMP_WHEN_COPY, + ...options, + }); + export const createDataSourceTarget = (ds: DataSourceSchema, initialDeps: DepData = {}) => new Target({ type: DepTargetType.DATA_SOURCE, diff --git a/packages/editor/src/editorProps.ts b/packages/editor/src/editorProps.ts index 67b098ef..f6011626 100644 --- a/packages/editor/src/editorProps.ts +++ b/packages/editor/src/editorProps.ts @@ -1,4 +1,5 @@ import type { EventOption } from '@tmagic/core'; +import { CustomTargetOptions } from '@tmagic/dep'; import type { FormConfig, FormState } from '@tmagic/form'; import type { DataSourceSchema, Id, MApp, MNode } from '@tmagic/schema'; import StageCore, { @@ -65,6 +66,8 @@ export interface EditorProps { updateDragEl?: UpdateDragEl; disabledDragStart?: boolean; extendFormState?: (state: FormState) => Record | Promise>; + /** 自定义依赖收集器,复制组件时会将关联依赖一并复制 */ + collectorOptions?: CustomTargetOptions; } export const defaultEditorProps = { diff --git a/packages/editor/src/initService.ts b/packages/editor/src/initService.ts index 3bad8886..bb4308df 100644 --- a/packages/editor/src/initService.ts +++ b/packages/editor/src/initService.ts @@ -2,13 +2,14 @@ import { onUnmounted, reactive, toRaw, watch } from 'vue'; import { cloneDeep } from 'lodash-es'; import type { EventOption } from '@tmagic/core'; -import type { Target } from '@tmagic/dep'; import { createCodeBlockTarget, createDataSourceCondTarget, createDataSourceMethodTarget, createDataSourceTarget, + createRelatedCompTarget, DepTargetType, + Target, } from '@tmagic/dep'; import type { CodeBlockContent, DataSourceSchema, Id, MApp, MNode, MPage } from '@tmagic/schema'; import { getNodes } from '@tmagic/utils'; @@ -186,7 +187,7 @@ export const initServiceEvents = ( return stage?.renderer.runtime?.getApp?.(); }; - const updateDataSoucreSchema = () => { + const updateDataSourceSchema = () => { const root = editorService.get('root'); if (root?.dataSources) { @@ -194,7 +195,7 @@ export const initServiceEvents = ( } }; - const upateNodeWhenDataSourceChange = (nodes: MNode[]) => { + const updateNodeWhenDataSourceChange = (nodes: MNode[]) => { const root = editorService.get('root'); const stage = editorService.get('stage'); @@ -210,7 +211,7 @@ export const initServiceEvents = ( app.dsl.dataSources = root.dataSources; } - updateDataSoucreSchema(); + updateDataSourceSchema(); nodes.forEach((node) => { const deps = Object.values(root.dataSourceDeps || {}); @@ -258,11 +259,11 @@ export const initServiceEvents = ( }; const depUpdateHandler = (node: MNode) => { - upateNodeWhenDataSourceChange([node]); + updateNodeWhenDataSourceChange([node]); }; const collectedHandler = (nodes: MNode[]) => { - upateNodeWhenDataSourceChange(nodes); + updateNodeWhenDataSourceChange(nodes); }; depService.on('add-target', targetAddHandler); @@ -381,7 +382,7 @@ export const initServiceEvents = ( const nodes = getNodes(Object.keys(targets[config.id].deps), root?.items); - upateNodeWhenDataSourceChange(nodes); + updateNodeWhenDataSourceChange(nodes); }; const removeDataSourceTarget = (id: string) => { @@ -399,6 +400,11 @@ export const initServiceEvents = ( dataSourceService.on('update', dataSourceUpdateHandler); dataSourceService.on('remove', dataSourceRemoveHandler); + // 初始化复制组件相关的依赖收集器 + if (props.collectorOptions && !depService.hasTarget(DepTargetType.RELATED_COMP_WHEN_COPY)) { + depService.addTarget(createRelatedCompTarget(props.collectorOptions)); + } + onUnmounted(() => { depService.off('add-target', targetAddHandler); depService.off('remove-target', targetRemoveHandler); diff --git a/packages/editor/src/layouts/workspace/viewer/ViewerMenu.vue b/packages/editor/src/layouts/workspace/viewer/ViewerMenu.vue index e72919d9..83b926f6 100644 --- a/packages/editor/src/layouts/workspace/viewer/ViewerMenu.vue +++ b/packages/editor/src/layouts/workspace/viewer/ViewerMenu.vue @@ -11,10 +11,8 @@ import { isPage } from '@tmagic/utils'; import ContentMenu from '@editor/components/ContentMenu.vue'; import CenterIcon from '@editor/icons/CenterIcon.vue'; -import storageService from '@editor/services/storage'; import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type'; import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu'; -import { COPY_STORAGE_KEY } from '@editor/utils/editor'; defineOptions({ name: 'MEditorViewerMenu', @@ -28,7 +26,6 @@ const props = withDefaults( const services = inject('services'); const editorService = services?.editorService; const menu = ref>(); -const canPaste = ref(false); const canCenter = ref(false); const node = computed(() => editorService?.get('node')); @@ -129,8 +126,6 @@ watch( const show = (e: MouseEvent) => { menu.value?.show(e); - const data = storageService.getItem(COPY_STORAGE_KEY); - canPaste.value = data !== 'undefined' && !!data; }; defineExpose({ show }); diff --git a/packages/editor/src/services/dep.ts b/packages/editor/src/services/dep.ts index ea2cc45f..4591d152 100644 --- a/packages/editor/src/services/dep.ts +++ b/packages/editor/src/services/dep.ts @@ -53,8 +53,8 @@ class Dep extends BaseService { this.watcher.clearTargets(); } - public collect(nodes: MNode[], deep = false) { - this.watcher.collect(nodes, deep); + public collect(nodes: MNode[], deep = false, type?: DepTargetType) { + this.watcher.collect(nodes, deep, type); this.emit('collected', nodes, deep); } @@ -65,6 +65,10 @@ class Dep extends BaseService { public hasTarget(id: Id, type: string = DepTargetType.DEFAULT) { return this.watcher.hasTarget(id, type); } + + public hasSpecifiedTypeTarget(type: string = DepTargetType.DEFAULT): boolean { + return this.watcher.hasSpecifiedTypeTarget(type); + } } export type DepService = Dep; diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index 5f508fdf..7620a8cb 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -17,13 +17,15 @@ */ import { reactive, toRaw } from 'vue'; -import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es'; +import { cloneDeep, get, isObject, mergeWith, uniq } from 'lodash-es'; +import { DepTargetType } from '@tmagic/dep'; import type { Id, MApp, MComponent, MContainer, MNode, MPage } from '@tmagic/schema'; import { NodeType } from '@tmagic/schema'; import StageCore from '@tmagic/stage'; import { getNodePath, isNumber, isPage, isPop } from '@tmagic/utils'; +import depService from '@editor/services/dep'; import historyService from '@editor/services/history'; import storageService, { Protocol } from '@editor/services/storage'; import type { AddMNode, EditorNodeInfo, PastePosition, StepValue, StoreState, StoreStateKey } from '@editor/type'; @@ -593,9 +595,9 @@ class Editor extends BaseService { } /** - * 将组将节点配置转化成string,然后存储到localStorage中 + * 将组件节点配置存储到localStorage中 * @param config 组件节点配置 - * @returns 组件节点配置 + * @returns */ public copy(config: MNode | MNode[]): void { storageService.setItem(COPY_STORAGE_KEY, Array.isArray(config) ? config : [config], { @@ -603,6 +605,41 @@ class Editor extends BaseService { }); } + /** + * 复制时会带上组件关联的依赖 + * @param config 组件节点配置 + * @returns + */ + public copyWithRelated(config: MNode | MNode[]): void { + const copyNodes: MNode[] = Array.isArray(config) ? config : [config]; + // 关联的组件也一并复制 + depService.getTarget(DepTargetType.RELATED_COMP_WHEN_COPY, DepTargetType.RELATED_COMP_WHEN_COPY)?.removeDep(); + 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) { + Object.keys(customTarget.deps).forEach((nodeId: Id) => { + const node = this.getNodeById(nodeId); + if (!node) return; + customTarget!.deps[nodeId].keys.forEach((key) => { + const relateNodeId = get(node, key); + const isExist = copyNodes.find((node) => node.id === relateNodeId); + if (!isExist) { + const relateNode = this.getNodeById(relateNodeId); + if (relateNode) { + copyNodes.push(relateNode); + } + } + }); + }); + } + storageService.setItem(COPY_STORAGE_KEY, copyNodes, { + protocol: Protocol.OBJECT, + }); + } + /** * 从localStorage中获取节点,然后添加到当前容器中 * @param position 粘贴的坐标 @@ -610,7 +647,6 @@ class Editor extends BaseService { */ public async paste(position: PastePosition = {}): Promise { const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY); - if (!Array.isArray(config)) return; const node = this.get('node'); @@ -623,13 +659,13 @@ class Editor extends BaseService { parent = this.get('page'); } } - const pasteConfigs = await this.doPaste(config, position); - + propsService.replaceRelateId(config, pasteConfigs); return this.add(pasteConfigs, parent); } public async doPaste(config: MNode[], position: PastePosition = {}): Promise { + propsService.clearRelateId(); const pasteConfigs = await beforePaste(position, cloneDeep(config)); return pasteConfigs; } diff --git a/packages/editor/src/services/props.ts b/packages/editor/src/services/props.ts index 87d8d896..8d4ede08 100644 --- a/packages/editor/src/services/props.ts +++ b/packages/editor/src/services/props.ts @@ -17,12 +17,15 @@ */ import { reactive } from 'vue'; -import { cloneDeep, mergeWith } from 'lodash-es'; +import { cloneDeep, get, mergeWith, set } from 'lodash-es'; +import { DepTargetType } from '@tmagic/dep'; import type { FormConfig } from '@tmagic/form'; -import type { MComponent, MNode } from '@tmagic/schema'; +import type { Id, MComponent, MNode } from '@tmagic/schema'; import { guid, toLine } from '@tmagic/utils'; +import depService from '@editor/services/dep'; +import editorService from '@editor/services/editor'; import type { PropsState } from '@editor/type'; import { fillConfig } from '@editor/utils/props'; @@ -32,6 +35,7 @@ class Props extends BaseService { private state = reactive({ propsConfigMap: {}, propsValueMap: {}, + relateIdMap: {}, }); constructor() { @@ -135,11 +139,16 @@ class Props extends BaseService { /** * 将组件与组件的子元素配置中的id都设置成一个新的ID + * 如果没有相同ID则保持不变 * @param {Object} config 组件配置 */ /* eslint no-param-reassign: ["error", { "props": false }] */ public async setNewItemId(config: MNode) { - config.id = await this.createId(config.type || 'component'); + if (editorService.getNodeById(config.id)) { + const newId = await this.createId(config.type || 'component'); + this.setRelateId(config.id, newId); + config.id = newId; + } if (config.items && Array.isArray(config.items)) { for (const item of config.items) { @@ -176,11 +185,59 @@ class Props extends BaseService { this.state.propsValueMap = {}; } + /** + * 替换关联ID + * @param originConfigs 原组件配置 + * @param targetConfigs 待替换的组件配置 + */ + public replaceRelateId(originConfigs: MNode[], targetConfigs: MNode[]) { + const relateIdMap = this.getRelateIdMap(); + if (Object.keys(relateIdMap).length === 0) return; + const target = depService.getTarget(DepTargetType.RELATED_COMP_WHEN_COPY, DepTargetType.RELATED_COMP_WHEN_COPY); + if (!target) return; + originConfigs.forEach((config: MNode) => { + const newId = relateIdMap[config.id]; + const targetConfig = targetConfigs.find((targetConfig) => targetConfig.id === newId); + if (!targetConfig) return; + target.deps[config.id]?.keys?.forEach((fullKey) => { + const relateOriginId = get(config, fullKey); + const relateTargetId = relateIdMap[relateOriginId]; + if (!relateTargetId) return; + set(targetConfig, fullKey, relateTargetId); + }); + }); + } + + /** + * 清除setNewItemId前后映射关系 + */ + public clearRelateId() { + this.state.relateIdMap = {}; + } + public destroy() { this.resetState(); this.removeAllListeners(); this.removeAllPlugins(); } + + /** + * 获取setNewItemId前后映射关系 + * @param oldId 原组件ID + * @returns 新旧ID映射 + */ + private getRelateIdMap() { + return this.state.relateIdMap; + } + + /** + * 记录setNewItemId前后映射关系 + * @param oldId 原组件ID + * @param newId 分配的新ID + */ + private setRelateId(oldId: Id, newId: Id) { + this.state.relateIdMap[oldId] = newId; + } } export type PropsService = Props; diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index ca0bcd02..051cd2ac 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -148,6 +148,7 @@ export type StoreStateKey = keyof StoreState; export interface PropsState { propsConfigMap: Record; propsValueMap: Record>; + relateIdMap: Record; } export interface ComponentGroupState { diff --git a/packages/editor/src/utils/content-menu.ts b/packages/editor/src/utils/content-menu.ts index 7b2fa80d..2ae38154 100644 --- a/packages/editor/src/utils/content-menu.ts +++ b/packages/editor/src/utils/content-menu.ts @@ -1,4 +1,4 @@ -import { computed, markRaw, Ref, ref } from 'vue'; +import { computed, markRaw, Ref } from 'vue'; import { CopyDocument, Delete, DocumentCopy } from '@element-plus/icons-vue'; import { Id, MContainer, NodeType } from '@tmagic/schema'; @@ -7,6 +7,8 @@ import { isPage } from '@tmagic/utils'; import ContentMenu from '@editor/components/ContentMenu.vue'; import type { MenuButton, Services } from '@editor/type'; +import { COPY_STORAGE_KEY } from './editor'; + export const useDeleteMenu = (): MenuButton => ({ type: 'button', text: '删除', @@ -21,8 +23,6 @@ export const useDeleteMenu = (): MenuButton => ({ }, }); -const canPaste = ref(false); - export const useCopyMenu = (): MenuButton => ({ type: 'button', text: '复制', @@ -30,7 +30,6 @@ export const useCopyMenu = (): MenuButton => ({ handler: (services) => { const nodes = services?.editorService?.get('nodes'); nodes && services?.editorService?.copy(nodes); - canPaste.value = true; }, }); @@ -38,7 +37,7 @@ export const usePasteMenu = (menu?: Ref | undef type: 'button', text: '粘贴', icon: markRaw(DocumentCopy), - display: () => canPaste.value, + display: (services) => !!services?.storageService?.getItem(COPY_STORAGE_KEY), handler: (services) => { const nodes = services?.editorService?.get('nodes'); if (!nodes || nodes.length === 0) return; diff --git a/packages/editor/src/utils/operator.ts b/packages/editor/src/utils/operator.ts index a2152d90..d255f79e 100644 --- a/packages/editor/src/utils/operator.ts +++ b/packages/editor/src/utils/operator.ts @@ -40,7 +40,6 @@ export const beforePaste = async (position: PastePosition, config: MNode[]): Pro if (pastePosition.top && configItem.style?.top) { pastePosition.top = configItem.style?.top - referenceTop + pastePosition.top; } - const pasteConfig = await propsService.setNewItemId(configItem); if (pasteConfig.style) {