mirror of
				https://github.com/Tencent/tmagic-editor.git
				synced 2025-11-04 18:52:18 +08:00 
			
		
		
		
	fix(editor): 有复制的内容时展示粘贴菜单
This commit is contained in:
		
							parent
							
								
									fc89d4cbe8
								
							
						
					
					
						commit
						456692ff8a
					
				@ -29,11 +29,16 @@ export default class Target {
 | 
				
			|||||||
   * 实例:{ 'node_id': { name: 'node_name', keys: [ created, mounted ] } }
 | 
					   * 实例:{ 'node_id': { name: 'node_name', keys: [ created, mounted ] } }
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public deps: DepData = {};
 | 
					  public deps: DepData = {};
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * 是否默认收集,默认为true,当值为false时需要传入type参数给collect方法才会被收集
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  public isCollectByDefault?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(options: TargetOptions) {
 | 
					  constructor(options: TargetOptions) {
 | 
				
			||||||
    this.isTarget = options.isTarget;
 | 
					    this.isTarget = options.isTarget;
 | 
				
			||||||
    this.id = options.id;
 | 
					    this.id = options.id;
 | 
				
			||||||
    this.name = options.name;
 | 
					    this.name = options.name;
 | 
				
			||||||
 | 
					    this.isCollectByDefault = options.isCollectByDefault ?? true;
 | 
				
			||||||
    if (options.type) {
 | 
					    if (options.type) {
 | 
				
			||||||
      this.type = options.type;
 | 
					      this.type = options.type;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -53,6 +53,15 @@ export default class Watcher {
 | 
				
			|||||||
    return Boolean(this.getTarget(id, type));
 | 
					    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
 | 
					   * 删除指定id的target
 | 
				
			||||||
   * @param id target id
 | 
					   * @param id target id
 | 
				
			||||||
@ -95,10 +104,12 @@ export default class Watcher {
 | 
				
			|||||||
   * 收集依赖
 | 
					   * 收集依赖
 | 
				
			||||||
   * @param nodes 需要收集的节点
 | 
					   * @param nodes 需要收集的节点
 | 
				
			||||||
   * @param deep 是否需要收集子节点
 | 
					   * @param deep 是否需要收集子节点
 | 
				
			||||||
 | 
					   * @param type 强制收集指定类型的依赖
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public collect(nodes: Record<string | number, any>[], deep = false) {
 | 
					  public collect(nodes: Record<string | number, any>[], deep = false, type?: DepTargetType) {
 | 
				
			||||||
    Object.values(this.targetsList).forEach((targets) => {
 | 
					    Object.values(this.targetsList).forEach((targets) => {
 | 
				
			||||||
      Object.values(targets).forEach((target) => {
 | 
					      Object.values(targets).forEach((target) => {
 | 
				
			||||||
 | 
					        if ((!type && !target.isCollectByDefault) || (type && target.type !== type)) return;
 | 
				
			||||||
        nodes.forEach((node) => {
 | 
					        nodes.forEach((node) => {
 | 
				
			||||||
          // 先删除原有依赖,重新收集
 | 
					          // 先删除原有依赖,重新收集
 | 
				
			||||||
          target.removeDep(node);
 | 
					          target.removeDep(node);
 | 
				
			||||||
@ -143,7 +154,7 @@ export default class Watcher {
 | 
				
			|||||||
        } else if (!keyIsItems && Array.isArray(value)) {
 | 
					        } else if (!keyIsItems && Array.isArray(value)) {
 | 
				
			||||||
          value.forEach((item, index) => {
 | 
					          value.forEach((item, index) => {
 | 
				
			||||||
            if (isObject(item)) {
 | 
					            if (isObject(item)) {
 | 
				
			||||||
              collectTarget(item, `${fullKey}.${index}`);
 | 
					              collectTarget(item, `${fullKey}[${index}]`);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        } else if (isObject(value)) {
 | 
					        } else if (isObject(value)) {
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,8 @@ export enum DepTargetType {
 | 
				
			|||||||
  DATA_SOURCE_METHOD = 'data-source-method',
 | 
					  DATA_SOURCE_METHOD = 'data-source-method',
 | 
				
			||||||
  /** 数据源条件 */
 | 
					  /** 数据源条件 */
 | 
				
			||||||
  DATA_SOURCE_COND = 'data-source-cond',
 | 
					  DATA_SOURCE_COND = 'data-source-cond',
 | 
				
			||||||
 | 
					  /** 复制组件时关联的组件 */
 | 
				
			||||||
 | 
					  RELATED_COMP_WHEN_COPY = 'related-comp-when-copy',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IsTarget = (key: string | number, value: any) => boolean;
 | 
					export type IsTarget = (key: string | number, value: any) => boolean;
 | 
				
			||||||
@ -24,6 +26,16 @@ export interface TargetOptions {
 | 
				
			|||||||
  type?: string;
 | 
					  type?: string;
 | 
				
			||||||
  name?: string;
 | 
					  name?: string;
 | 
				
			||||||
  initialDeps?: DepData;
 | 
					  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 {
 | 
					export interface TargetList {
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,7 @@ import {
 | 
				
			|||||||
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils';
 | 
					import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from '@tmagic/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Target from './Target';
 | 
					import Target from './Target';
 | 
				
			||||||
import { DepTargetType } from './types';
 | 
					import { CustomTargetOptions, DepTargetType } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) =>
 | 
					export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) =>
 | 
				
			||||||
  new Target({
 | 
					  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 = {}) =>
 | 
					export const createDataSourceTarget = (ds: DataSourceSchema, initialDeps: DepData = {}) =>
 | 
				
			||||||
  new Target({
 | 
					  new Target({
 | 
				
			||||||
    type: DepTargetType.DATA_SOURCE,
 | 
					    type: DepTargetType.DATA_SOURCE,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import type { EventOption } from '@tmagic/core';
 | 
					import type { EventOption } from '@tmagic/core';
 | 
				
			||||||
 | 
					import { CustomTargetOptions } from '@tmagic/dep';
 | 
				
			||||||
import type { FormConfig, FormState } from '@tmagic/form';
 | 
					import type { FormConfig, FormState } from '@tmagic/form';
 | 
				
			||||||
import type { DataSourceSchema, Id, MApp, MNode } from '@tmagic/schema';
 | 
					import type { DataSourceSchema, Id, MApp, MNode } from '@tmagic/schema';
 | 
				
			||||||
import StageCore, {
 | 
					import StageCore, {
 | 
				
			||||||
@ -65,6 +66,8 @@ export interface EditorProps {
 | 
				
			|||||||
  updateDragEl?: UpdateDragEl;
 | 
					  updateDragEl?: UpdateDragEl;
 | 
				
			||||||
  disabledDragStart?: boolean;
 | 
					  disabledDragStart?: boolean;
 | 
				
			||||||
  extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
 | 
					  extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
 | 
				
			||||||
 | 
					  /** 自定义依赖收集器,复制组件时会将关联依赖一并复制 */
 | 
				
			||||||
 | 
					  collectorOptions?: CustomTargetOptions;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const defaultEditorProps = {
 | 
					export const defaultEditorProps = {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,13 +2,14 @@ import { onUnmounted, reactive, toRaw, watch } from 'vue';
 | 
				
			|||||||
import { cloneDeep } from 'lodash-es';
 | 
					import { cloneDeep } from 'lodash-es';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import type { EventOption } from '@tmagic/core';
 | 
					import type { EventOption } from '@tmagic/core';
 | 
				
			||||||
import type { Target } from '@tmagic/dep';
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  createCodeBlockTarget,
 | 
					  createCodeBlockTarget,
 | 
				
			||||||
  createDataSourceCondTarget,
 | 
					  createDataSourceCondTarget,
 | 
				
			||||||
  createDataSourceMethodTarget,
 | 
					  createDataSourceMethodTarget,
 | 
				
			||||||
  createDataSourceTarget,
 | 
					  createDataSourceTarget,
 | 
				
			||||||
 | 
					  createRelatedCompTarget,
 | 
				
			||||||
  DepTargetType,
 | 
					  DepTargetType,
 | 
				
			||||||
 | 
					  Target,
 | 
				
			||||||
} from '@tmagic/dep';
 | 
					} from '@tmagic/dep';
 | 
				
			||||||
import type { CodeBlockContent, DataSourceSchema, Id, MApp, MNode, MPage } from '@tmagic/schema';
 | 
					import type { CodeBlockContent, DataSourceSchema, Id, MApp, MNode, MPage } from '@tmagic/schema';
 | 
				
			||||||
import { getNodes } from '@tmagic/utils';
 | 
					import { getNodes } from '@tmagic/utils';
 | 
				
			||||||
@ -186,7 +187,7 @@ export const initServiceEvents = (
 | 
				
			|||||||
    return stage?.renderer.runtime?.getApp?.();
 | 
					    return stage?.renderer.runtime?.getApp?.();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateDataSoucreSchema = () => {
 | 
					  const updateDataSourceSchema = () => {
 | 
				
			||||||
    const root = editorService.get('root');
 | 
					    const root = editorService.get('root');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (root?.dataSources) {
 | 
					    if (root?.dataSources) {
 | 
				
			||||||
@ -194,7 +195,7 @@ export const initServiceEvents = (
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const upateNodeWhenDataSourceChange = (nodes: MNode[]) => {
 | 
					  const updateNodeWhenDataSourceChange = (nodes: MNode[]) => {
 | 
				
			||||||
    const root = editorService.get('root');
 | 
					    const root = editorService.get('root');
 | 
				
			||||||
    const stage = editorService.get('stage');
 | 
					    const stage = editorService.get('stage');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -210,7 +211,7 @@ export const initServiceEvents = (
 | 
				
			|||||||
      app.dsl.dataSources = root.dataSources;
 | 
					      app.dsl.dataSources = root.dataSources;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    updateDataSoucreSchema();
 | 
					    updateDataSourceSchema();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    nodes.forEach((node) => {
 | 
					    nodes.forEach((node) => {
 | 
				
			||||||
      const deps = Object.values(root.dataSourceDeps || {});
 | 
					      const deps = Object.values(root.dataSourceDeps || {});
 | 
				
			||||||
@ -258,11 +259,11 @@ export const initServiceEvents = (
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const depUpdateHandler = (node: MNode) => {
 | 
					  const depUpdateHandler = (node: MNode) => {
 | 
				
			||||||
    upateNodeWhenDataSourceChange([node]);
 | 
					    updateNodeWhenDataSourceChange([node]);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const collectedHandler = (nodes: MNode[]) => {
 | 
					  const collectedHandler = (nodes: MNode[]) => {
 | 
				
			||||||
    upateNodeWhenDataSourceChange(nodes);
 | 
					    updateNodeWhenDataSourceChange(nodes);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  depService.on('add-target', targetAddHandler);
 | 
					  depService.on('add-target', targetAddHandler);
 | 
				
			||||||
@ -381,7 +382,7 @@ export const initServiceEvents = (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const nodes = getNodes(Object.keys(targets[config.id].deps), root?.items);
 | 
					    const nodes = getNodes(Object.keys(targets[config.id].deps), root?.items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    upateNodeWhenDataSourceChange(nodes);
 | 
					    updateNodeWhenDataSourceChange(nodes);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const removeDataSourceTarget = (id: string) => {
 | 
					  const removeDataSourceTarget = (id: string) => {
 | 
				
			||||||
@ -399,6 +400,11 @@ export const initServiceEvents = (
 | 
				
			|||||||
  dataSourceService.on('update', dataSourceUpdateHandler);
 | 
					  dataSourceService.on('update', dataSourceUpdateHandler);
 | 
				
			||||||
  dataSourceService.on('remove', dataSourceRemoveHandler);
 | 
					  dataSourceService.on('remove', dataSourceRemoveHandler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 初始化复制组件相关的依赖收集器
 | 
				
			||||||
 | 
					  if (props.collectorOptions && !depService.hasTarget(DepTargetType.RELATED_COMP_WHEN_COPY)) {
 | 
				
			||||||
 | 
					    depService.addTarget(createRelatedCompTarget(props.collectorOptions));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onUnmounted(() => {
 | 
					  onUnmounted(() => {
 | 
				
			||||||
    depService.off('add-target', targetAddHandler);
 | 
					    depService.off('add-target', targetAddHandler);
 | 
				
			||||||
    depService.off('remove-target', targetRemoveHandler);
 | 
					    depService.off('remove-target', targetRemoveHandler);
 | 
				
			||||||
 | 
				
			|||||||
@ -11,10 +11,8 @@ import { isPage } from '@tmagic/utils';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
					import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
				
			||||||
import CenterIcon from '@editor/icons/CenterIcon.vue';
 | 
					import CenterIcon from '@editor/icons/CenterIcon.vue';
 | 
				
			||||||
import storageService from '@editor/services/storage';
 | 
					 | 
				
			||||||
import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
					import { LayerOffset, Layout, MenuButton, MenuComponent, Services } from '@editor/type';
 | 
				
			||||||
import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
 | 
					import { useCopyMenu, useDeleteMenu, useMoveToMenu, usePasteMenu } from '@editor/utils/content-menu';
 | 
				
			||||||
import { COPY_STORAGE_KEY } from '@editor/utils/editor';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineOptions({
 | 
					defineOptions({
 | 
				
			||||||
  name: 'MEditorViewerMenu',
 | 
					  name: 'MEditorViewerMenu',
 | 
				
			||||||
@ -28,7 +26,6 @@ const props = withDefaults(
 | 
				
			|||||||
const services = inject<Services>('services');
 | 
					const services = inject<Services>('services');
 | 
				
			||||||
const editorService = services?.editorService;
 | 
					const editorService = services?.editorService;
 | 
				
			||||||
const menu = ref<InstanceType<typeof ContentMenu>>();
 | 
					const menu = ref<InstanceType<typeof ContentMenu>>();
 | 
				
			||||||
const canPaste = ref(false);
 | 
					 | 
				
			||||||
const canCenter = ref(false);
 | 
					const canCenter = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const node = computed(() => editorService?.get('node'));
 | 
					const node = computed(() => editorService?.get('node'));
 | 
				
			||||||
@ -129,8 +126,6 @@ watch(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const show = (e: MouseEvent) => {
 | 
					const show = (e: MouseEvent) => {
 | 
				
			||||||
  menu.value?.show(e);
 | 
					  menu.value?.show(e);
 | 
				
			||||||
  const data = storageService.getItem(COPY_STORAGE_KEY);
 | 
					 | 
				
			||||||
  canPaste.value = data !== 'undefined' && !!data;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineExpose({ show });
 | 
					defineExpose({ show });
 | 
				
			||||||
 | 
				
			|||||||
@ -53,8 +53,8 @@ class Dep extends BaseService {
 | 
				
			|||||||
    this.watcher.clearTargets();
 | 
					    this.watcher.clearTargets();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public collect(nodes: MNode[], deep = false) {
 | 
					  public collect(nodes: MNode[], deep = false, type?: DepTargetType) {
 | 
				
			||||||
    this.watcher.collect(nodes, deep);
 | 
					    this.watcher.collect(nodes, deep, type);
 | 
				
			||||||
    this.emit('collected', nodes, deep);
 | 
					    this.emit('collected', nodes, deep);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,6 +65,10 @@ class Dep extends BaseService {
 | 
				
			|||||||
  public hasTarget(id: Id, type: string = DepTargetType.DEFAULT) {
 | 
					  public hasTarget(id: Id, type: string = DepTargetType.DEFAULT) {
 | 
				
			||||||
    return this.watcher.hasTarget(id, type);
 | 
					    return this.watcher.hasTarget(id, type);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public hasSpecifiedTypeTarget(type: string = DepTargetType.DEFAULT): boolean {
 | 
				
			||||||
 | 
					    return this.watcher.hasSpecifiedTypeTarget(type);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type DepService = Dep;
 | 
					export type DepService = Dep;
 | 
				
			||||||
 | 
				
			|||||||
@ -17,13 +17,15 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { reactive, toRaw } from 'vue';
 | 
					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 type { Id, MApp, MComponent, MContainer, MNode, MPage } from '@tmagic/schema';
 | 
				
			||||||
import { NodeType } from '@tmagic/schema';
 | 
					import { NodeType } from '@tmagic/schema';
 | 
				
			||||||
import StageCore from '@tmagic/stage';
 | 
					import StageCore from '@tmagic/stage';
 | 
				
			||||||
import { getNodePath, isNumber, isPage, isPop } from '@tmagic/utils';
 | 
					import { getNodePath, isNumber, isPage, isPop } from '@tmagic/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import depService from '@editor/services/dep';
 | 
				
			||||||
import historyService from '@editor/services/history';
 | 
					import historyService from '@editor/services/history';
 | 
				
			||||||
import storageService, { Protocol } from '@editor/services/storage';
 | 
					import storageService, { Protocol } from '@editor/services/storage';
 | 
				
			||||||
import type { AddMNode, EditorNodeInfo, PastePosition, StepValue, StoreState, StoreStateKey } from '@editor/type';
 | 
					import type { AddMNode, EditorNodeInfo, PastePosition, StepValue, StoreState, StoreStateKey } from '@editor/type';
 | 
				
			||||||
@ -593,9 +595,9 @@ class Editor extends BaseService {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * 将组将节点配置转化成string,然后存储到localStorage中
 | 
					   * 将组件节点配置存储到localStorage中
 | 
				
			||||||
   * @param config 组件节点配置
 | 
					   * @param config 组件节点配置
 | 
				
			||||||
   * @returns 组件节点配置
 | 
					   * @returns
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public copy(config: MNode | MNode[]): void {
 | 
					  public copy(config: MNode | MNode[]): void {
 | 
				
			||||||
    storageService.setItem(COPY_STORAGE_KEY, Array.isArray(config) ? config : [config], {
 | 
					    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中获取节点,然后添加到当前容器中
 | 
					   * 从localStorage中获取节点,然后添加到当前容器中
 | 
				
			||||||
   * @param position 粘贴的坐标
 | 
					   * @param position 粘贴的坐标
 | 
				
			||||||
@ -610,7 +647,6 @@ class Editor extends BaseService {
 | 
				
			|||||||
   */
 | 
					   */
 | 
				
			||||||
  public async paste(position: PastePosition = {}): Promise<MNode | MNode[] | void> {
 | 
					  public async paste(position: PastePosition = {}): Promise<MNode | MNode[] | void> {
 | 
				
			||||||
    const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
 | 
					    const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!Array.isArray(config)) return;
 | 
					    if (!Array.isArray(config)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const node = this.get('node');
 | 
					    const node = this.get('node');
 | 
				
			||||||
@ -623,13 +659,13 @@ class Editor extends BaseService {
 | 
				
			|||||||
        parent = this.get('page');
 | 
					        parent = this.get('page');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const pasteConfigs = await this.doPaste(config, position);
 | 
					    const pasteConfigs = await this.doPaste(config, position);
 | 
				
			||||||
 | 
					    propsService.replaceRelateId(config, pasteConfigs);
 | 
				
			||||||
    return this.add(pasteConfigs, parent);
 | 
					    return this.add(pasteConfigs, parent);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
 | 
					  public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
 | 
				
			||||||
 | 
					    propsService.clearRelateId();
 | 
				
			||||||
    const pasteConfigs = await beforePaste(position, cloneDeep(config));
 | 
					    const pasteConfigs = await beforePaste(position, cloneDeep(config));
 | 
				
			||||||
    return pasteConfigs;
 | 
					    return pasteConfigs;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -17,12 +17,15 @@
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { reactive } from 'vue';
 | 
					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 { 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 { guid, toLine } from '@tmagic/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import depService from '@editor/services/dep';
 | 
				
			||||||
 | 
					import editorService from '@editor/services/editor';
 | 
				
			||||||
import type { PropsState } from '@editor/type';
 | 
					import type { PropsState } from '@editor/type';
 | 
				
			||||||
import { fillConfig } from '@editor/utils/props';
 | 
					import { fillConfig } from '@editor/utils/props';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -32,6 +35,7 @@ class Props extends BaseService {
 | 
				
			|||||||
  private state = reactive<PropsState>({
 | 
					  private state = reactive<PropsState>({
 | 
				
			||||||
    propsConfigMap: {},
 | 
					    propsConfigMap: {},
 | 
				
			||||||
    propsValueMap: {},
 | 
					    propsValueMap: {},
 | 
				
			||||||
 | 
					    relateIdMap: {},
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
@ -135,11 +139,16 @@ class Props extends BaseService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * 将组件与组件的子元素配置中的id都设置成一个新的ID
 | 
					   * 将组件与组件的子元素配置中的id都设置成一个新的ID
 | 
				
			||||||
 | 
					   * 如果没有相同ID则保持不变
 | 
				
			||||||
   * @param {Object} config 组件配置
 | 
					   * @param {Object} config 组件配置
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  /* eslint no-param-reassign: ["error", { "props": false }] */
 | 
					  /* eslint no-param-reassign: ["error", { "props": false }] */
 | 
				
			||||||
  public async setNewItemId(config: MNode) {
 | 
					  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)) {
 | 
					    if (config.items && Array.isArray(config.items)) {
 | 
				
			||||||
      for (const item of config.items) {
 | 
					      for (const item of config.items) {
 | 
				
			||||||
@ -176,11 +185,59 @@ class Props extends BaseService {
 | 
				
			|||||||
    this.state.propsValueMap = {};
 | 
					    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() {
 | 
					  public destroy() {
 | 
				
			||||||
    this.resetState();
 | 
					    this.resetState();
 | 
				
			||||||
    this.removeAllListeners();
 | 
					    this.removeAllListeners();
 | 
				
			||||||
    this.removeAllPlugins();
 | 
					    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;
 | 
					export type PropsService = Props;
 | 
				
			||||||
 | 
				
			|||||||
@ -148,6 +148,7 @@ export type StoreStateKey = keyof StoreState;
 | 
				
			|||||||
export interface PropsState {
 | 
					export interface PropsState {
 | 
				
			||||||
  propsConfigMap: Record<string, FormConfig>;
 | 
					  propsConfigMap: Record<string, FormConfig>;
 | 
				
			||||||
  propsValueMap: Record<string, Partial<MNode>>;
 | 
					  propsValueMap: Record<string, Partial<MNode>>;
 | 
				
			||||||
 | 
					  relateIdMap: Record<Id, Id>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ComponentGroupState {
 | 
					export interface ComponentGroupState {
 | 
				
			||||||
 | 
				
			|||||||
@ -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 { CopyDocument, Delete, DocumentCopy } from '@element-plus/icons-vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Id, MContainer, NodeType } from '@tmagic/schema';
 | 
					import { Id, MContainer, NodeType } from '@tmagic/schema';
 | 
				
			||||||
@ -7,6 +7,8 @@ import { isPage } from '@tmagic/utils';
 | 
				
			|||||||
import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
					import ContentMenu from '@editor/components/ContentMenu.vue';
 | 
				
			||||||
import type { MenuButton, Services } from '@editor/type';
 | 
					import type { MenuButton, Services } from '@editor/type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { COPY_STORAGE_KEY } from './editor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useDeleteMenu = (): MenuButton => ({
 | 
					export const useDeleteMenu = (): MenuButton => ({
 | 
				
			||||||
  type: 'button',
 | 
					  type: 'button',
 | 
				
			||||||
  text: '删除',
 | 
					  text: '删除',
 | 
				
			||||||
@ -21,8 +23,6 @@ export const useDeleteMenu = (): MenuButton => ({
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const canPaste = ref(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const useCopyMenu = (): MenuButton => ({
 | 
					export const useCopyMenu = (): MenuButton => ({
 | 
				
			||||||
  type: 'button',
 | 
					  type: 'button',
 | 
				
			||||||
  text: '复制',
 | 
					  text: '复制',
 | 
				
			||||||
@ -30,7 +30,6 @@ export const useCopyMenu = (): MenuButton => ({
 | 
				
			|||||||
  handler: (services) => {
 | 
					  handler: (services) => {
 | 
				
			||||||
    const nodes = services?.editorService?.get('nodes');
 | 
					    const nodes = services?.editorService?.get('nodes');
 | 
				
			||||||
    nodes && services?.editorService?.copy(nodes);
 | 
					    nodes && services?.editorService?.copy(nodes);
 | 
				
			||||||
    canPaste.value = true;
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -38,7 +37,7 @@ export const usePasteMenu = (menu?: Ref<InstanceType<typeof ContentMenu> | undef
 | 
				
			|||||||
  type: 'button',
 | 
					  type: 'button',
 | 
				
			||||||
  text: '粘贴',
 | 
					  text: '粘贴',
 | 
				
			||||||
  icon: markRaw(DocumentCopy),
 | 
					  icon: markRaw(DocumentCopy),
 | 
				
			||||||
  display: () => canPaste.value,
 | 
					  display: (services) => !!services?.storageService?.getItem(COPY_STORAGE_KEY),
 | 
				
			||||||
  handler: (services) => {
 | 
					  handler: (services) => {
 | 
				
			||||||
    const nodes = services?.editorService?.get('nodes');
 | 
					    const nodes = services?.editorService?.get('nodes');
 | 
				
			||||||
    if (!nodes || nodes.length === 0) return;
 | 
					    if (!nodes || nodes.length === 0) return;
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,6 @@ export const beforePaste = async (position: PastePosition, config: MNode[]): Pro
 | 
				
			|||||||
      if (pastePosition.top && configItem.style?.top) {
 | 
					      if (pastePosition.top && configItem.style?.top) {
 | 
				
			||||||
        pastePosition.top = configItem.style?.top - referenceTop + pastePosition.top;
 | 
					        pastePosition.top = configItem.style?.top - referenceTop + pastePosition.top;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const pasteConfig = await propsService.setNewItemId(configItem);
 | 
					      const pasteConfig = await propsService.setNewItemId(configItem);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (pasteConfig.style) {
 | 
					      if (pasteConfig.style) {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user