From faabf1bb3ad77b6b09498bb7512e3f2d1a7f0a76 Mon Sep 17 00:00:00 2001 From: parisma Date: Mon, 27 May 2024 17:44:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor,dep):=20=E6=94=AF=E6=8C=81=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E7=BB=84=E4=BB=B6=E6=97=B6=E5=B0=86=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E7=9A=84=E6=95=B0=E6=8D=AE=E6=BA=90=E4=B8=80?= =?UTF-8?q?=E5=B9=B6=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/dep/src/types.ts | 4 + packages/dep/src/utils.ts | 6 +- packages/editor/src/editorProps.ts | 6 +- .../editor/src/hooks/use-code-block-edit.ts | 2 - packages/editor/src/initService.ts | 14 ++- packages/editor/src/services/codeBlock.ts | 86 +++++++++++++++++-- packages/editor/src/services/dataSource.ts | 57 +++++++++++- packages/editor/src/services/props.ts | 15 ++-- packages/editor/src/utils/editor.ts | 2 + playground/package.json | 2 + playground/src/pages/Editor.vue | 75 +++++++++++++++- pnpm-lock.yaml | 6 ++ 12 files changed, 245 insertions(+), 30 deletions(-) diff --git a/packages/dep/src/types.ts b/packages/dep/src/types.ts index a6f8d30f..324d2be0 100644 --- a/packages/dep/src/types.ts +++ b/packages/dep/src/types.ts @@ -15,6 +15,10 @@ export enum DepTargetType { 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; diff --git a/packages/dep/src/utils.ts b/packages/dep/src/utils.ts index 584c71b4..c334db70 100644 --- a/packages/dep/src/utils.ts +++ b/packages/dep/src/utils.ts @@ -32,10 +32,10 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initi }, }); -export const createRelatedCompTarget = (options: CustomTargetOptions) => +export const createRelatedTargetForCopy = (options: CustomTargetOptions, type: DepTargetType) => new Target({ - id: DepTargetType.RELATED_COMP_WHEN_COPY, - type: DepTargetType.RELATED_COMP_WHEN_COPY, + id: type, + type, ...options, }); diff --git a/packages/editor/src/editorProps.ts b/packages/editor/src/editorProps.ts index b5103502..4c5fec11 100644 --- a/packages/editor/src/editorProps.ts +++ b/packages/editor/src/editorProps.ts @@ -70,8 +70,12 @@ export interface EditorProps { codeOptions?: { [key: string]: any }; /** 禁用鼠标左键按下时就开始拖拽,需要先选中再可以拖拽 */ disabledDragStart?: boolean; - /** 自定义依赖收集器,复制组件时会将关联依赖一并复制 */ + /** 自定义依赖收集器,复制组件时会将关联组件一并复制 */ collectorOptions?: CustomTargetOptions; + /** 自定义依赖收集器,复制组件时会将关联代码块一并复制 */ + collectorOptionsForCode?: CustomTargetOptions; + /** 自定义依赖收集器,复制组件时会将关联数据源一并复制 */ + collectorOptionsForDataSource?: CustomTargetOptions; /** 标尺配置 */ guidesOptions?: Partial; /** 禁止多选 */ diff --git a/packages/editor/src/hooks/use-code-block-edit.ts b/packages/editor/src/hooks/use-code-block-edit.ts index 3b5b88c5..f71a6e19 100644 --- a/packages/editor/src/hooks/use-code-block-edit.ts +++ b/packages/editor/src/hooks/use-code-block-edit.ts @@ -67,8 +67,6 @@ export const useCodeBlockEdit = (codeBlockService?: CodeBlockService) => { await codeBlockService?.setCodeDslById(codeId.value, values); - tMagicMessage.success('代码块保存成功'); - codeBlockEditor.value?.hide(); }; diff --git a/packages/editor/src/initService.ts b/packages/editor/src/initService.ts index 9f6734c2..9912ac3b 100644 --- a/packages/editor/src/initService.ts +++ b/packages/editor/src/initService.ts @@ -7,7 +7,7 @@ import { createDataSourceCondTarget, createDataSourceMethodTarget, createDataSourceTarget, - createRelatedCompTarget, + createRelatedTargetForCopy, DepTargetType, Target, } from '@tmagic/dep'; @@ -412,7 +412,17 @@ export const initServiceEvents = ( // 初始化复制组件相关的依赖收集器 if (props.collectorOptions && !depService.hasTarget(DepTargetType.RELATED_COMP_WHEN_COPY)) { - depService.addTarget(createRelatedCompTarget(props.collectorOptions)); + 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(() => { diff --git a/packages/editor/src/services/codeBlock.ts b/packages/editor/src/services/codeBlock.ts index 76aeb7af..6f922a68 100644 --- a/packages/editor/src/services/codeBlock.ts +++ b/packages/editor/src/services/codeBlock.ts @@ -17,21 +17,26 @@ */ import { reactive } from 'vue'; -import { keys, pick } from 'lodash-es'; +import { cloneDeep, get, keys, pick } from 'lodash-es'; import type { Writable } from 'type-fest'; +import { DepTargetType } from '@tmagic/dep'; import type { ColumnConfig } from '@tmagic/form'; -import type { CodeBlockContent, CodeBlockDSL, Id } from '@tmagic/schema'; +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'; import { CODE_DRAFT_STORAGE_KEY } from '@editor/type'; import { getConfig } from '@editor/utils/config'; +import { COPY_CODE_STORAGE_KEY } from '@editor/utils/editor'; import BaseService from './BaseService'; const canUsePluginMethods = { async: ['setCodeDslById', 'setEditStatus', 'setCombineIds', 'setUndeleteableList', 'deleteCodeDslByIds'] as const, - sync: [], + sync: ['setCodeDslByIdSync'], }; type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>; @@ -46,7 +51,10 @@ class CodeBlock extends BaseService { }); constructor() { - super(canUsePluginMethods.async.map((methodName) => ({ name: methodName, isAsync: true }))); + super([ + ...canUsePluginMethods.async.map((methodName) => ({ name: methodName, isAsync: true })), + ...canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false })), + ]); } /** @@ -88,20 +96,32 @@ class CodeBlock extends BaseService { * @returns {void} */ public async setCodeDslById(id: Id, codeConfig: Partial): Promise { + this.setCodeDslByIdSync(id, codeConfig, true); + } + + /** + * 为了兼容历史原因 + * 设置代码块ID和代码内容到源dsl + * @param {Id} id 代码块id + * @param {CodeBlockContent} codeConfig 代码块内容配置信息 + * @param {boolean} force 是否强制写入,默认true + * @returns {void} + */ + public setCodeDslByIdSync(id: Id, codeConfig: Partial, force = true): void { const codeDsl = this.getCodeDsl(); if (!codeDsl) { throw new Error('dsl中没有codeBlocks'); } + if (codeDsl[id] && !force) return; - const codeConfigProcessed = codeConfig; - if (codeConfig.content) { + const codeConfigProcessed = cloneDeep(codeConfig); + if (codeConfigProcessed.content) { // 在保存的时候转换代码内容 const parseDSL = getConfig('parseDSL'); - if (typeof codeConfig.content === 'string') { - codeConfig.content = parseDSL<(...args: any[]) => any>(codeConfig.content); + if (typeof codeConfigProcessed.content === 'string') { + codeConfigProcessed.content = parseDSL<(...args: any[]) => any>(codeConfigProcessed.content); } - codeConfigProcessed.content = codeConfig.content; } const existContent = codeDsl[id] || {}; @@ -233,6 +253,54 @@ class CodeBlock extends BaseService { return await this.getUniqueId(); } + /** + * 复制时会带上组件关联的代码块 + * @param config 组件节点配置 + * @returns + */ + public copyWithRelated(config: MNode | MNode[]): 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) { + Object.keys(customTarget.deps).forEach((nodeId: Id) => { + const node = editorService.getNodeById(nodeId); + if (!node) return; + customTarget!.deps[nodeId].keys.forEach((key) => { + const relateCodeId = get(node, key); + const isExist = Object.keys(copyData).find((codeId: Id) => codeId === relateCodeId); + if (!isExist) { + const relateCode = this.getCodeContentById(relateCodeId); + if (relateCode) { + copyData[relateCodeId] = relateCode; + } + } + }); + }); + } + storageService.setItem(COPY_CODE_STORAGE_KEY, copyData, { + protocol: Protocol.OBJECT, + }); + } + + /** + * 粘贴代码块 + * @returns + */ + public paste() { + const codeDsl: CodeBlockDSL = storageService.getItem(COPY_CODE_STORAGE_KEY); + Object.keys(codeDsl).forEach((codeId: Id) => { + // 不覆盖同样id的代码块 + this.setCodeDslByIdSync(codeId, codeDsl[codeId], false); + }); + } + public resetState() { this.state.codeDsl = null; this.state.editable = true; diff --git a/packages/editor/src/services/dataSource.ts b/packages/editor/src/services/dataSource.ts index e4bd55bc..95dfecd9 100644 --- a/packages/editor/src/services/dataSource.ts +++ b/packages/editor/src/services/dataSource.ts @@ -1,14 +1,19 @@ import { reactive } from 'vue'; -import { cloneDeep } from 'lodash-es'; +import { cloneDeep, get } from 'lodash-es'; import { Writable } from 'type-fest'; import type { EventOption } from '@tmagic/core'; +import { DepTargetType } from '@tmagic/dep'; import type { FormConfig } from '@tmagic/form'; -import type { DataSourceSchema } from '@tmagic/schema'; +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'; import { getFormConfig, getFormValue } from '@editor/utils/data-source'; +import { COPY_DS_STORAGE_KEY } from '@editor/utils/editor'; import BaseService from './BaseService'; @@ -102,7 +107,7 @@ class DataSource extends BaseService { public add(config: DataSourceSchema) { const newConfig = { ...config, - id: this.createId(), + id: config.id && !this.getDataSourceById(config.id) ? config.id : this.createId(), }; this.get('dataSources').push(newConfig); @@ -116,7 +121,6 @@ class DataSource extends BaseService { const dataSources = this.get('dataSources'); const index = dataSources.findIndex((ds) => ds.id === config.id); - dataSources[index] = cloneDeep(config); this.emit('update', config); @@ -153,6 +157,51 @@ class DataSource extends BaseService { public usePlugin(options: SyncHookPlugin): void { super.usePlugin(options); } + + /** + * 复制时会带上组件关联的数据源 + * @param config 组件节点配置 + * @returns + */ + public copyWithRelated(config: MNode | MNode[]): 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) { + Object.keys(customTarget.deps).forEach((nodeId: Id) => { + const node = editorService.getNodeById(nodeId); + if (!node) return; + customTarget!.deps[nodeId].keys.forEach((key) => { + const [relateDsId] = get(node, key); + const isExist = copyData.find((dsItem) => dsItem.id === relateDsId); + if (!isExist) { + const relateDs = this.getDataSourceById(relateDsId); + if (relateDs) { + copyData.push(relateDs); + } + } + }); + }); + } + storageService.setItem(COPY_DS_STORAGE_KEY, copyData, { + protocol: Protocol.OBJECT, + }); + } + /** + * 粘贴数据源 + * @returns + */ + public paste() { + const dataSource: DataSourceSchema[] = storageService.getItem(COPY_DS_STORAGE_KEY); + dataSource.forEach((item: DataSourceSchema) => { + // 不覆盖同样id的数据源 + if (!this.getDataSourceById(item.id)) { + this.add(item); + } + }); + } } export type DataSourceService = DataSource; diff --git a/packages/editor/src/services/props.ts b/packages/editor/src/services/props.ts index 455d5299..86674f5c 100644 --- a/packages/editor/src/services/props.ts +++ b/packages/editor/src/services/props.ts @@ -23,7 +23,7 @@ import { Writable } from 'type-fest'; import { DepTargetType } from '@tmagic/dep'; import type { FormConfig } from '@tmagic/form'; import type { Id, MComponent, MNode } from '@tmagic/schema'; -import { getValueByKeyPath, guid, setValueByKeyPath, toLine } from '@tmagic/utils'; +import { getNodePath, getValueByKeyPath, guid, setValueByKeyPath, toLine } from '@tmagic/utils'; import depService from '@editor/services/dep'; import editorService from '@editor/services/editor'; @@ -199,13 +199,14 @@ class Props extends BaseService { public replaceRelateId(originConfigs: MNode[], targetConfigs: MNode[]) { 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; - originConfigs.forEach((config: MNode) => { const newId = relateIdMap[config.id]; - const targetConfig = targetConfigs.find((targetConfig) => targetConfig.id === newId); + const path = getNodePath(newId, targetConfigs); + const targetConfig = path[path.length - 1]; if (!targetConfig) return; @@ -213,9 +214,13 @@ class Props extends BaseService { const relateOriginId = getValueByKeyPath(fullKey, config); const relateTargetId = relateIdMap[relateOriginId]; if (!relateTargetId) return; - setValueByKeyPath(fullKey, relateTargetId, targetConfig); }); + + // 递归items + if (config.items && Array.isArray(config.items)) { + this.replaceRelateId(config.items, targetConfigs); + } }); } diff --git a/packages/editor/src/utils/editor.ts b/packages/editor/src/utils/editor.ts index 2d482fe9..68763f1a 100644 --- a/packages/editor/src/utils/editor.ts +++ b/packages/editor/src/utils/editor.ts @@ -25,6 +25,8 @@ import { calcValueByFontsize, getNodePath, isNumber, isPage, isPageFragment, isP import { Layout } from '@editor/type'; export const COPY_STORAGE_KEY = '$MagicEditorCopyData'; +export const COPY_CODE_STORAGE_KEY = '$MagicEditorCopyCode'; +export const COPY_DS_STORAGE_KEY = '$MagicEditorCopyDataSource'; /** * 获取所有页面配置 diff --git a/playground/package.json b/playground/package.json index 23aa210d..817c8d78 100644 --- a/playground/package.json +++ b/playground/package.json @@ -23,12 +23,14 @@ "@tmagic/stage": "1.4.5", "@tmagic/utils": "1.4.5", "element-plus": "^2.6.1", + "lodash-es": "^4.17.21", "monaco-editor": "^0.48.0", "serialize-javascript": "^6.0.0", "vue": "^3.4.27", "vue-router": "^4.0.10" }, "devDependencies": { + "@types/lodash-es": "^4.17.4", "@types/node": "^18.19.0", "@types/serialize-javascript": "^5.0.1", "@vitejs/plugin-legacy": "^5.4.0", diff --git a/playground/src/pages/Editor.vue b/playground/src/pages/Editor.vue index c3a19b10..fcdd4280 100644 --- a/playground/src/pages/Editor.vue +++ b/playground/src/pages/Editor.vue @@ -17,6 +17,9 @@ :moveable-options="moveableOptions" :auto-scroll-into-view="true" :stage-rect="stageRect" + :collector-options="collectorOptions" + :layerContentMenu="contentMenuData" + :stageContentMenu="contentMenuData" @props-submit-error="propsSubmitErrorHandler" >