背景
由于最近项目要支持,图形编辑这么一个需求,希望可以自定义图标
,连接线
等等功能,时间用紧迫,所以就网上查了一下,发现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;
};
复制代码
温馨提示:以上代码不全,只是个思路,具体请参看该项目的源码,地址
总结
ant-simple-pro简洁,美观,快速上手,支持3大框架,vue3,react,angular。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END