写一个在线制作图编辑器

背景

由于最近项目要支持,图形编辑这么一个需求,希望可以自定义图标连接线等等功能,时间用紧迫,所以就网上查了一下,发现x6可以实现我们的需求,x6是AntV 旗下的图编辑引擎,还可以,但是也有很多bug,毕竟出来还没有一年,还需要打磨,但已经足够支持我们项目了。

预览地址

技术栈

  • react
  • typescript
  • react-dnd
  • x6
  • antd
├── config                     # 项目脚手架的配置文件
├── script                     # 项目配置文件
├── src                        # 源代码
│   ├── assets                 # 全局css,js,iamge等静态资源
│   ├── components             # 全局公用组件
│   ├── config                 # 图形的全局配置
│   ├── graph                  # 图形实例
│   ├── graphTemplateType      # 图形的模板
│   ├── hooks                  # 全局 hooks
│   ├── icons                  # 项目所有 svg icons
│   ├── interfaces             # 全局的ts文件类型
│   ├── core                   # 执行操作的代码
│   ├── layout                 # 布局
│   ├── utils                  # 公用的公用方法
│   ├── index.css              # 全局样式
│   ├── index.tsx              # 入口文件 加载组件 初始化等
│   └── react-app-env.d.ts     # react全局模块声明文件
├── .editorconfig              # 代码风格统一配置文件
├── .eslintrc.js               # eslint 配置项
├── .eslintignore              # eslint 忽略文件
├── .prettierrc                # prettierrc 配置项
├── .prettierignore            # prettierignore 忽略文件
├── tsconfig.json              # 项目全局ts配置文件
└── package.json               # package.json
复制代码

先创建Graph对象

graph文件下创建index.ts用来实例graph对象

import { Graph, FunctionExt, Shape } from '@antv/x6';
export default class FlowGraph {
  public static graph: Graph;
  public static init() {
    this.graph = new Graph({
      container: document.getElementById('container')!,
      width: 1000,
      height: 800,
      resizing: {
        enabled: true,
      },
      grid: {
        size: 10,
        visible: true,
        type: 'doubleMesh',
        args: [
          {
            color: '#cccccc',
            thickness: 1,
          },
          {
            color: '#5F95FF',
            thickness: 1,
            factor: 4,
          },
        ],
      },
      selecting: {
        enabled: true,
        multiple: true,
        rubberband: true,
        movable: true,
        showNodeSelectionBox: true,
        filter: ['groupNode'],
      },
      connecting: {
        anchor: 'center',
        connectionPoint: 'anchor',
        allowBlank: false,
        highlight: true,
        snap: true,
        createEdge() {
          return new Shape.Edge({
            attrs: {
              line: {
                stroke: '#5F95FF',
                strokeWidth: 1,
                targetMarker: {
                  name: 'classic',
                  size: 8,
                },
              },
            },
            router: {
              name: 'manhattan',
            },
            zIndex: 0,
          });
        },
        validateConnection({
          sourceView,
          targetView,
          sourceMagnet,
          targetMagnet,
        }) {
          if (sourceView === targetView) {
            return false;
          }
          if (!sourceMagnet) {
            return false;
          }
          if (!targetMagnet) {
            return false;
          }
          return true;
        },
      },
      highlighting: {
        magnetAvailable: {
          name: 'stroke',
          args: {
            padding: 4,
            attrs: {
              strokeWidth: 4,
              stroke: 'rgba(223,234,255)',
            },
          },
        },
      },
      snapline: true,
      history: true,
      clipboard: {
        enabled: true,
      },
      keyboard: {
        enabled: true,
      },
      embedding: {
        enabled: true,
        findParent({ node }) {
          const bbox = node.getBBox();
          return this.getNodes().filter((node) => {
            const data = node.getData<any>();
            if (data && data.parent) {
              const targetBBox = node.getBBox();
              return bbox.isIntersectWithRect(targetBBox);
            }
            return false;
          });
        },
      },
    });
    return this.graph;
  }
}
复制代码

创建自定义节点名

graph文件下创建registeredNode.ts用来创建自定义节点名

import { Graph, Dom } from '@antv/x6';
import { shapeName } from '@/config';
import { portsConfig } from '@/config/portsConfig';
export const FlowChartRect = Graph.registerNode(shapeName.flowChartRect, {
  inherit: 'rect',
  width: 80,
  height: 42,
  attrs: {
    body: {
      stroke: '#5F95FF',
      strokeWidth: 1,
      fill: 'rgba(95,149,255,0.05)',
    },
    fo: {
      refWidth: '100%',
      refHeight: '100%',
    },
    foBody: {
      xmlns: Dom.ns.xhtml,
      style: {
        width: '100%',
        height: '100%',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      },
    },
    'edit-text': {
      contenteditable: 'false',
      class: 'x6-edit-text',
      style: {
        width: '100%',
        textAlign: 'center',
        fontSize: 12,
        color: 'rgba(0,0,0,0.85)',
      },
    },
    text: {
      fontSize: 12,
      fill: 'rgba(0,0,0,0.85)',
      textWrap: {
        text: '',
        width: -10,
      },
    },
  },
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
    },
    {
      tagName: 'text',
      selector: 'text',
    },
    {
      tagName: 'foreignObject',
      selector: 'fo',
      children: [
        {
          ns: Dom.ns.xhtml,
          tagName: 'body',
          selector: 'foBody',
          children: [
            {
              tagName: 'div',
              selector: 'edit-text',
            },
          ],
        },
      ],
    },
  ],
  ports: portsConfig,
});
复制代码

使用上面自定义节点,来注册,在graph文件下创建base文件夹,改文件夹专门用来存放基础的自定义节点,在/graph/base下新建index.ts

import { shapeName } from '@/config';
import { Dom } from '@antv/x6';
import { portsConfig } from '@/config/portsConfig';
export const roundedRectangle = {
  shape: shapeName.flowChartRect,
  attrs: {
    body: {
      rx: 24,
      ry: 24,
    },
    text: {
      textWrap: {
        text: '',
      },
    },
  },
};
export const rectangle = {
  shape: shapeName.flowChartRect,
  attrs: {
    text: {
      textWrap: {
        text: '',
      },
    },
  },
};
复制代码

操作节点

在core文件下创建dragTarget.ts,dropTarget.ts这两个文件,一个拖拽目标,一个拖拽目的地

dragTarget.ts,用来渲染要拖拽的目标元素集合列表图形

import React, { memo, FC } from 'react';
import { useDrag } from 'react-dnd';
import { tempalteType } from '@/graphTemplateType';
import { overHiddleText } from '@/utils';
import style from './index.module.scss';
import SvgCompent from '@/components/svgIcon';
const DragTarget: FC<{
  itemValue: tempalteType;
}> = memo(function DragTarget({ itemValue }) {
  const [, drager] = useDrag({
    type: 'Box',
    item: itemValue,
  });

  return (
    <a ref={drager} className={style.templateRender} title={itemValue.title}>
      <SvgCompent iconClass={itemValue.type} fontSize="60px" />
      <p>{overHiddleText(itemValue.title, 7)}</p>
    </a>
  );
});

export default DragTarget;

复制代码

dropTarget.ts,用来渲染实例化图形元素

import React, { memo, useState, useRef, useEffect } from 'react';
import { useDrop } from 'react-dnd';
import style from './index.module.scss';
import { Drawer } from 'antd';
import { useOnResize, useKeydown } from '@/hooks';
import FlowGraph from '@/graph';
import { formatGroupInfoToNodeMeta } from '@/utils/formatGroupInfoToNodeMeta';
import { tempalteType } from '@/graphTemplateType';
import ConfigPanel from '@/core/ConfigPanel';
import { UnorderedListOutlined } from '@ant-design/icons';
import '@/graph/registeredNode';
import '@/graph/reactRegisteredNode';

const closeStyle: React.CSSProperties = {
  right: '0px',
};

const DropTarget = memo(function DropTarget(props) {
  const [visible, setVisible] = useState<boolean>(true);
  const [isRender, setIsRender] = useState<boolean>(false);
  const { width, height } = useOnResize();
  const onClose = () => setVisible(false);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const keyDown = useKeydown([isRender]);
  const [collectProps, droper] = useDrop({
    accept: 'Box',
    collect: (minoter) => ({
      isOver: minoter.isOver(),
      canDrop: minoter.canDrop(),
      item: minoter.getItem(),
    }),
    drop: (item: tempalteType, monitor) => {
      // 拖拽组件当前offset
      const currentMouseOffset = monitor.getClientOffset();
      // 拖拽组件初始拖拽时offset
      const sourceMouseOffset = monitor.getInitialClientOffset();
      const sourceElementOffset = monitor.getInitialSourceClientOffset();
      const diffX = sourceMouseOffset!.x - sourceElementOffset!.x;
      const diffY = sourceMouseOffset!.y - sourceElementOffset!.y;
      const x = currentMouseOffset!.x - diffX;
      const y = currentMouseOffset!.y - diffY;
      // 将实际的x,y这样的坐标转换画布本地坐标
      const point = FlowGraph.graph.clientToLocal(x, y);
      const createNodeData = formatGroupInfoToNodeMeta(item, point);
      FlowGraph.graph.addNode(createNodeData);
    },
  });

  useEffect(() => {
    const graph = FlowGraph.init();
    if (graph) {
      setIsRender(true);
    }
  }, []);

  useEffect(() => {
    if (FlowGraph.isGraphReady()) {
      FlowGraph.graph.resize(width - 300, height);
    }
  }, [width, height]);
  return (
    <div className={style.warp}>
      <div
        ref={(ele) => {
          containerRef.current = ele;
          droper(ele);
        }}
        className={style.dropTarget}
        id="container"
      ></div>
      <Drawer
        placement="right"
        mask={false}
        onClose={onClose}
        visible={visible}
        width={300}
      >
        <div className={style.config}>{isRender && <ConfigPanel />}</div>
      </Drawer>
      <div
        className={style.close}
        style={!visible ? closeStyle : undefined}
        onClick={() => setVisible(true)}
      >
        <UnorderedListOutlined />
      </div>
    </div>
  );
});

export default DropTarget;

复制代码

formatGroupInfoToNodeMeta函数

export const formatGroupInfoToNodeMeta = <T = tempalteType>(
  dropItem: T,
  point: { x: number; y: number },
) => {
  const { category, type } = dropItem as unknown as tempalteType;
  const { x, y } = point;
  let createNode = { x, y, data: dropItem };
  switch (category) {
    case 'base':
      createNode = Object.assign(
        {},
        createNode,
        filterNode(baseGraphNodeList, type),
      );
      break;
    default:
      break;
  }
  return createNode;
};

复制代码

1CWUJOThkq5GznK.gif

温馨提示:以上代码不全,只是个思路,具体请参看该项目的源码,地址

总结

预览地址

项目代码地址

ant-simple-pro简洁,美观,快速上手,支持3大框架,vue3,react,angular。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享