效果图如下:
目前只做了绘制部分,绘制方式也比较简单,点击工具栏中需要绘制的图形,在画布上左键点击将会绘制一个图形出来,工具栏选中第一个,再点击其他图像,长按鼠标左键可以移动,删除使用键盘delete键,目前没做批量框选(懒得写了,按照点击选中的思路可以自己实现),工具栏最后一个画线,需要鼠标长按,起点与终点在图形上,路径为自动生成,不能自定义调整,但可以通过拖动节点改变路径。
节点结构如下:
type Node = {
key: number;//标识
type: 1 | 2 | 3 | 4 | 5 | 6;//1开始,2结束,3任务节点,4决策节点,5子流程节点,6连接线
name?: string;//名称
x: number;//坐标x
y: number;//坐标y
radius?: number;//半径
width?: number;//宽
heigth?: number;//高
isCheck: boolean;//是否选中
startNode?: number;//开始节点--作连接线时使用
endNode?: number;//结束节点--作连接线时使用
//连接线线路
ponits?: { start: { x: number, y: number }, center?: { x: number, y: number }[], end: { x: number, y: number }, arrow?: { x: number, y: number }[] };
endPoint?: { x: number, y: number };//结束点--作连接线未完成画虚线时使用
}
如需要传给后端需要调整格式(目前传不了,因为没做节点绑定人),后端的部分后面再做了,有空了再去优化,比如工具栏图标,UI什么的,完整代码如下
index.tsx
import { BranchesOutlined, DragOutlined, } from '@ant-design/icons';
import { Button, Col, Form, Row, Tooltip } from 'antd';
import styles from './index.less'
import React, { MouseEvent, useEffect, useRef, useState } from 'react';
type Node = {
key: number;//标识
type: 1 | 2 | 3 | 4 | 5 | 6;//1开始,2结束,3任务节点,4决策节点,5子流程节点,6连接线
name?: string;//名称
x: number;//坐标x
y: number;//坐标y
radius?: number;//半径
width?: number;//宽
heigth?: number;//高
isCheck: boolean;//是否选中
startNode?: number;//开始节点--作连接线时使用
endNode?: number;//结束节点--作连接线时使用
//连接线线路
ponits?: { start: { x: number, y: number }, center?: { x: number, y: number }[], end: { x: number, y: number }, arrow?: { x: number, y: number }[] };
endPoint?: { x: number, y: number };//结束点--作连接线未完成画虚线时使用
}
const Test: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [nodes, setNodes] = useState<Record<number, Node | undefined>>({});
const [checkButton, setCheckButton] = useState<number>();
const [checkNode, setCheckNode] = useState<number>(0);
const [currentConnetLine, setCurrentConnetLine] = useState<number>(0);
const [mouseDown, setMouseDown] = useState<boolean>(false);
/**
* 检查鼠标是否点击到线段
* @param click 鼠标坐标
* @param line 线段起始结束点
* @param width 线段宽度
*/
const checkClickLine = (click: { x: number, y: number }, line: { start: { x: number, y: number }, end: { x: number, y: number } }, width: number): boolean => {
const { x: startX, y: startY } = line.start;
const { x: endX, y: endY } = line.end;
// 计算线段的方向向量
const dx = endX - startX;
const dy = endY - startY;
// 计算线段长度
const length = Math.sqrt(dx * dx + dy * dy);
// 如果线段长度为0,则直接比较点是否相同
if (length === 0) {
const distance = Math.sqrt(Math.pow(click.x - startX, 2) + Math.pow(click.y - startY, 2));
return distance <= width / 2;
}
// 计算点击点到线段所在直线的垂直距离
const u = ((click.x - startX) * dx + (click.y - startY) * dy) / (length * length);
// 检查点击点是否在线段上
if (u < 0 || u > 1) {
return false;
}
// 计算最近点
const xClosest = startX + u * dx;
const yClosest = startY + u * dy;
// 计算点击点到最近点的距离
const distance = Math.sqrt(Math.pow(click.x - xClosest, 2) + Math.pow(click.y - yClosest, 2));
// 检查距离是否小于线段宽度的一半
return distance <= width / 2;
}
const checkNodeSelect = (node: Node, e: MouseEvent): boolean => {
const canvas = canvasRef.current;
if (canvas) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
switch (node.type) {
case 1:
case 2:
if (node.radius) {
const distance = Math.sqrt(Math.pow(x - node.x, 2) + Math.pow(y - node.y, 2));
return distance <= node.radius
}
break;
case 3:
case 4:
if (node.heigth && node.width) {
const leftTop = { x: node.x - node.width / 2, y: node.y - node.heigth / 2 };
const rightBottom = { x: node.x + node.width / 2, y: node.y + node.heigth / 2 };
return x >= leftTop.x && x <= rightBottom.x && y >= leftTop.y && y <= rightBottom.y;
}
break;
case 5:
if (node.radius) {
const halfSize = node.radius / 2 - 3;
const dx = Math.abs(x - node.x);
const dy = Math.abs(y - node.y);
return dx * dx + dy * dy <= halfSize * halfSize;;
}
break
case 6:
if (node.ponits) {
let check = false;
const ponits = node.ponits;
if (ponits.center) {
for (let i = 0, l = ponits.center.length; i < l; i++) {
if (!check) {
if (i == 0) {
check = checkClickLine({ x, y }, { start: ponits.start, end: ponits.center[i] }, 5);
if (!check) {
check = checkClickLine({ x, y }, { start: ponits.center[i], end: ponits.center[i + 1] }, 5);
}
}
else if (i == l - 1) {
check = checkClickLine({ x, y }, { start: ponits.center[i], end: ponits.end }, 5);
} else {
check = checkClickLine({ x, y }, { start: ponits.center[i], end: ponits.center[i + 1] }, 5);
}
}
}
} else {
check = checkClickLine({ x, y }, { start: ponits.start, end: ponits.end }, 5);
}
return check;
}
break;
}
}
return false
}
const [form] = Form.useForm();
/**
* 获取节点的上下左右几个端点
* @param node
* @param direction 上下左右|1234
*/
const getNodePonit = (node: Node, direction: 1 | 2 | 3 | 4): { x: number, y: number } => {
switch (node.type) {
case 1: case 2:
switch (direction) {
case 1:
return { x: node.x, y: node.y - (node.radius ? node.radius : 0) };
case 2:
return { x: node.x, y: node.y + (node.radius ? node.radius : 0) };
case 3:
return { x: node.x - (node.radius ? node.radius : 0), y: node.y };
case 4:
return { x: node.x + (node.radius ? node.radius : 0), y: node.y };
}
case 5:
switch (direction) {
case 1:
return { x: node.x, y: node.y - (node.radius ? node.radius : 0) / 2 };
case 2:
return { x: node.x, y: node.y + (node.radius ? node.radius : 0) / 2 };
case 3:
return { x: node.x - (node.radius ? node.radius : 0) / 2, y: node.y };
case 4:
return { x: node.x + (node.radius ? node.radius : 0) / 2, y: node.y };
}
case 3: case 4:
switch (direction) {
case 1: