前端使用canvas绘制简单工作流-react

效果图如下:

目前只做了绘制部分,绘制方式也比较简单,点击工具栏中需要绘制的图形,在画布上左键点击将会绘制一个图形出来,工具栏选中第一个,再点击其他图像,长按鼠标左键可以移动,删除使用键盘delete键,目前没做批量框选(懒得写了,按照点击选中的思路可以自己实现),工具栏最后一个画线,需要鼠标长按,起点与终点在图形上,路径为自动生成,不能自定义调整,但可以通过拖动节点改变路径。

节点结构如下:

TypeScript 复制代码
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

TypeScript 复制代码
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:
                        return { x: node.x, y: node.y - (node.heigth ? node.heigth / 2 : 0) };
                    case 2:
                        return { x: node.x, y: node.y + (node.heigth ? node.heigth / 2 : 0) };
                    case 3:
                        return { x: node.x - (node.width ? node.width / 2 : 0), y: node.y };
                    case 4:
                        return { x: node.x + (node.width ? node.width / 2 : 0), y: node.y };
                }
        }
        return { x: 0, y: 0 };
    }

    /**
    * 获取箭头的三个点坐标
    * @param node 顶点
    * @param type 1,箭头向上,2箭头向下,3,箭头向左,4箭头向右
    */
    const getArrow = (node: { x: number, y: number }, type: 1 | 2 | 3 | 4): { x: number, y: number }[] => {
        const width = 8;
        const height = 14;
        switch (type) {
            case 1:
                return [{ x: node.x, y: node.y }, { x: node.x - width / 2, y: node.y + height }, { x: node.x + width / 2, y: node.y + height }]
            case 2:
                return [{ x: node.x, y: node.y }, { x: node.x - width / 2, y: node.y - height }, { x: node.x + width / 2, y: node.y - height }]
            case 3:
                return [{ x: node.x, y: node.y }, { x: node.x + height, y: node.y - width / 2 }, { x: node.x + height, y: node.y + width / 2 }]
            case 4:
                return [{ x: node.x, y: node.y }, { x: node.x - height, y: node.y - width / 2 }, { x: node.x - height, y: node.y + width / 2 }]
        }
    }

    /**
     * 根据连接的两点计算线路点
     * @param startNode 
     * @param endNode 
     * @returns 
     */
    const getConnetPoints = (startNode: Node, endNode: Node): { start: { x: number, y: number }, center?: { x: number, y: number }[], end: { x: number, y: number }, arrow?: { x: number, y: number }[] } => {
        if (startNode.x === endNode.x) {

            if (startNode.y > endNode.y) {
                //终点在正上方
                const end = getNodePonit(endNode, 2);
                return { start: getNodePonit(startNode, 1), end, arrow: getArrow(end, 1) }
            } else {
                const end = getNodePonit(endNode, 1);
                return { start: getNodePonit(startNode, 2), end, arrow: getArrow(end, 2) }
            }
        } else if (startNode.y === endNode.y) {
            if (startNode.x > endNode.x) {
                //终点在正左方
                const end = getNodePonit(endNode, 4);
                return { start: getNodePonit(startNode, 3), end, arrow: getArrow(end, 3) }
            } else {
                const end = getNodePonit(endNode, 3);
                return { start: getNodePonit(startNode, 4), end, arrow: getArrow(end, 4) }
            }
        } else if (startNode.x > endNode.x) {
            if (startNode.y > endNode.y) {
                if ((startNode.y - endNode.y) > (startNode.x - endNode.x)) {
                    const start = getNodePonit(startNode, 1);
                    const end = getNodePonit(endNode, 2);
                    const centerY = start.y - (start.y - end.y) / 2;
                    const center = [{ x: start.x, y: centerY }, { x: end.x, y: centerY }]
                    const arrow = getArrow(end, 1)
                    return { start, center, end, arrow }
                } else {
                    const start = getNodePonit(startNode, 3);
                    const end = getNodePonit(endNode, 4);
                    const centerX = start.x - (start.x - end.x) / 2;
                    const center = [{ x: centerX, y: start.y }, { x: centerX, y: end.y }]
                    const arrow = getArrow(end, 3)
                    return { start, center, end, arrow }
                }
            } else {
                if ((endNode.y - startNode.y) > (startNode.x - endNode.x)) {
                    const start = getNodePonit(startNode, 2);
                    const end = getNodePonit(endNode, 1);
                    const centerY = end.y - (end.y - start.y) / 2;
                    const center = [{ x: start.x, y: centerY }, { x: end.x, y: centerY }]
                    const arrow = getArrow(end, 2)
                    return { start, center, end, arrow }
                } else {
                    const start = getNodePonit(startNode, 3);
                    const end = getNodePonit(endNode, 4);
                    const centerX = start.x - (start.x - end.x) / 2;
                    const center = [{ x: centerX, y: start.y }, { x: centerX, y: end.y }]
                    const arrow = getArrow(end, 3)
                    return { start, center, end, arrow }
                }
            }
        } else {
            if (startNode.y > endNode.y) {
                if ((startNode.y - endNode.y) > (endNode.x - startNode.x)) {
                    const start = getNodePonit(startNode, 1);
                    const end = getNodePonit(endNode, 2);
                    const centerY = start.y - (start.y - end.y) / 2;
                    const center = [{ x: start.x, y: centerY }, { x: end.x, y: centerY }]
                    const arrow = getArrow(end, 1)
                    return { start, center, end, arrow }
                } else {
                    const start = getNodePonit(startNode, 4);
                    const end = getNodePonit(endNode, 3);
                    const centerX = end.x - (end.x - start.x) / 2;
                    const center = [{ x: centerX, y: start.y }, { x: centerX, y: end.y }]
                    const arrow = getArrow(end, 4)
                    return { start, center, end, arrow }
                }
            } else {
                if ((endNode.y - startNode.y) > (endNode.x - startNode.x)) {
                    const start = getNodePonit(startNode, 2);
                    const end = getNodePonit(endNode, 1);
                    const centerY = end.y - (end.y - start.y) / 2;
                    const center = [{ x: start.x, y: centerY }, { x: end.x, y: centerY }]
                    const arrow = getArrow(end, 2)
                    return { start, center, end, arrow }
                } else {
                    const start = getNodePonit(startNode, 4);
                    const end = getNodePonit(endNode, 3);
                    const centerX = end.x - (end.x - start.x) / 2;
                    const center = [{ x: centerX, y: start.y }, { x: centerX, y: end.y }]
                    const arrow = getArrow(end, 4)
                    return { start, center, end, arrow }
                }
            }
        }
    }

    useEffect(() => {
        if (canvasRef.current && nodes && Object.values(nodes).length > 0) {
            const ctx = canvasRef.current.getContext('2d');
            if (ctx) {
                ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
                Object.values(nodes).map(node => {
                    if (node) {
                        switch (node.type) {
                            case 1:
                            case 2:
                                if (node.radius) {
                                    ctx.beginPath();
                                    ctx.arc(node.x, node.y, node.radius, 0, 2 * Math.PI);
                                    ctx.fillStyle = node.type === 1 ? '#90EE90' : '#FFA07A';
                                    ctx.fill();
                                    ctx.lineWidth = 0.5;
                                    ctx.strokeStyle = 'black';
                                    ctx.stroke();
                                    // 添加文字  
                                    ctx.fillStyle = '#000000'; // 文字颜色  
                                    ctx.font = '12px Arial'; // 文字样式  
                                    ctx.textAlign = 'center'; // 文字对齐方式  
                                    ctx.fillText(node.type === 1 ? '开始' : '结束', node.x, node.y + 2);
                                    ctx.stroke();
                                    if (node.isCheck) {
                                        // 设定正方形的位置和尺寸
                                        const halfSize = node.radius + 2;
                                        // 绘制正方形的虚线边框  
                                        ctx.setLineDash([5, 5]); // 设置虚线样式,5个单位实线,5个单位间隔  
                                        ctx.beginPath();
                                        ctx.rect(node.x - halfSize, node.y - halfSize, halfSize * 2, halfSize * 2);
                                        ctx.strokeStyle = 'blue'; // 边框颜色  
                                        ctx.stroke();
                                        //绘制完虚线后重置线型为实线  
                                        ctx.setLineDash([]);
                                    }
                                }
                                break;
                            case 3:
                            case 4:
                                if (node.heigth && node.width) {
                                    ctx.beginPath();
                                    const radius = 20; // 圆角的大小 
                                    const x = node.x - node.width / 2;
                                    const y = node.y - node.heigth / 2;
                                    ctx.strokeStyle = node.type == 3 ? '#FF0000' : '#ADD8E6';
                                    // 设置边框宽度  
                                    ctx.lineWidth = 0.5;
                                    ctx.moveTo(x + radius, y);
                                    ctx.arcTo(x + node.width, y, x + node.width, y + node.heigth, radius);
                                    ctx.arcTo(x + node.width, y + node.heigth, x, y + node.heigth, radius);
                                    ctx.arcTo(x, y + node.heigth, x, y, radius);
                                    ctx.arcTo(x, y, x + radius, y, radius);
                                    ctx.stroke();
                                    ctx.fillStyle = 'white'; // 矩形颜色  
                                    ctx.fill();
                                    // 添加文字  
                                    ctx.fillStyle = '#000000'; // 文字颜色  
                                    ctx.font = '12px Arial'; // 文字样式  
                                    ctx.textAlign = 'left'; // 文字对齐方式  
                                    const text = node.name ? node.name : '节点' + node.key;
                                    if (text.length < 8) {
                                        ctx.fillText(text, x + (5 * (8 - text.length)), y + 27);
                                    } else if (text.length < 15) {
                                        ctx.fillText(text.slice(0, 7), x + 8, y + 20);
                                        const twoLine = text.slice(7, text.length);
                                        ctx.fillText(twoLine, x + 8 + (5.7 * (7 - twoLine.length)), y + 38);
                                    } else {
                                        ctx.fillText(text.slice(0, 7), x + 8, y + 20);
                                        const twoLine = text.slice(7, 13) + '...';
                                        ctx.fillText(twoLine, x + 8, y + 38);
                                    }
                                    ctx.stroke();
                                    if (node.isCheck) {
                                        ctx.setLineDash([5, 5]);
                                        ctx.beginPath();
                                        ctx.rect(node.x - node.width / 2 - 2, node.y - node.heigth / 2 - 2, node.width + 4, node.width / 2 + 4);
                                        ctx.strokeStyle = 'blue';
                                        ctx.stroke();
                                        ctx.setLineDash([]);
                                    }
                                }
                                break;
                            case 5:
                                if (node.radius) {
                                    ctx.lineWidth = 0.5;
                                    ctx.strokeStyle = 'black';

                                    // 开始绘制菱形路径  
                                    ctx.beginPath();
                                    ctx.moveTo(node.x, node.y - node.radius / 2);
                                    ctx.lineTo(node.x + node.radius / 2, node.y);
                                    ctx.lineTo(node.x, node.y + node.radius / 2);
                                    ctx.lineTo(node.x - node.radius / 2, node.y);
                                    ctx.closePath();
                                    // 绘制边框  
                                    ctx.stroke();
                                    // 设置填充样式并填充颜色  
                                    ctx.fillStyle = 'white';
                                    ctx.fill();

                                    // 设置填充样式并填充颜色  
                                    ctx.beginPath();
                                    ctx.fillStyle = 'black';
                                    ctx.fill();
                                    ctx.font = '18px Arial';
                                    ctx.textAlign = 'center';
                                    ctx.textBaseline = 'middle';
                                    ctx.fillText('if', node.x, node.y + 2);
                                    ctx.stroke();
                                    if (node.isCheck) {
                                        const halfSize = node.radius / 2 + 2;
                                        ctx.setLineDash([5, 5]);
                                        ctx.beginPath();
                                        ctx.rect(node.x - halfSize, node.y - halfSize, halfSize * 2, halfSize * 2);
                                        ctx.strokeStyle = 'blue';
                                        ctx.stroke();
                                        ctx.setLineDash([]);
                                    }
                                }
                                break
                            case 6:
                                if (node.endPoint) {
                                    // 设置虚线样式  
                                    ctx.setLineDash([5, 5]); // 第一个数字是实线部分长度,第二个数字是间隔长度  
                                    ctx.lineDashOffset = 0; // 虚线偏移量  
                                    ctx.lineWidth = 2; // 线条宽度  
                                    const x2 = node.x;
                                    const y2 = node.y;
                                    const x1 = node.endPoint.x;
                                    const y1 = node.endPoint.y;
                                    const dx = x2 - x1;
                                    const dy = y2 - y1;
                                    const distanceP1P2 = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
                                    const ratio = 20 / distanceP1P2;
                                    const p3 = { x: x1 + (x2 - x1) * ratio, y: y1 + (y2 - y1) * ratio };
                                    const unitX = dx / distanceP1P2;
                                    const unitY = dy / distanceP1P2;
                                    const nx = -dy / distanceP1P2;
                                    const ny = dx / distanceP1P2;
                                    const p4 = { x: p3.x + nx * 5, y: p3.y + ny * 5 };
                                    const p5 = { x: p3.x - nx * 5, y: p3.y - ny * 5 };
                                    const p6 = { x: x1 + unitX * 5, y: y1 + unitY * 5 };

                                    // 绘制箭头主体(直线)  
                                    ctx.beginPath();
                                    ctx.moveTo(node.x, node.y);
                                    ctx.lineTo(p6.x, p6.y);
                                    ctx.stroke();


                                    // 计算顶点p1到中垂线起点p2的向量  

                                    ctx.setLineDash([]);
                                    ctx.beginPath();

                                    ctx.moveTo(node.endPoint.x, node.endPoint.y);
                                    ctx.lineTo(p4.x, p4.y);
                                    ctx.lineTo(p5.x, p5.y);
                                    ctx.closePath();
                                    ctx.fillStyle = 'black'; // 箭头颜色  
                                    ctx.fill();
                                    // 重置虚线样式,如果之后还需要绘制实线或其他虚线样式  

                                } else {
                                    if (node.ponits) {
                                        console.log(node.isCheck)
                                        const ponits = node.ponits;
                                        ctx.lineWidth = 2; // 线条宽度  
                                        ctx.strokeStyle = node.isCheck ? 'red' : 'black',
                                            ctx.beginPath();
                                        ctx.moveTo(ponits.start.x, ponits.start.y);
                                        if (ponits.center) {
                                            ponits.center.map(n => {
                                                ctx.lineTo(n.x, n.y);
                                            })
                                        }
                                        ctx.lineTo(ponits.end.x, ponits.end.y);
                                        ctx.stroke();
                                        if (ponits.arrow) {
                                            ctx.beginPath();
                                            ctx.lineWidth = 2;
                                            ctx.moveTo(ponits.arrow[0].x, ponits.arrow[0].y);
                                            ctx.lineTo(ponits.arrow[1].x, ponits.arrow[1].y);
                                            ctx.lineTo(ponits.arrow[2].x, ponits.arrow[2].y);
                                            ctx.closePath();
                                            ctx.fillStyle = node.isCheck ? 'red' : 'black', // 箭头颜色  
                                                ctx.fill();
                                        }
                                    }
                                }
                                break;
                        }
                    }
                })
            }
        }


    }, [nodes])



    const onMouseDown = (e: MouseEvent) => {
        const canvas = canvasRef.current;
        if (e.button === 0) {
            setMouseDown(true)
        } else {
            setMouseDown(false)
        }
        if (canvas) {
            const rect = canvas.getBoundingClientRect();
            const key = Object.values(nodes).length + 1;
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            switch (checkButton) {
                case 0:
                    if (e.button === 0) {
                        let check = false;
                        Object.values(nodes).map(node => {
                            if (node) {
                                if (!check && checkNodeSelect(node, e)) {
                                    setCheckNode(node.key);
                                    node.isCheck = true;
                                    check = true;
                                } else {
                                    node.isCheck = false;
                                }
                            }
                        })
                        if (!check) {
                            setCheckNode(0)
                        }
                        setNodes({ ...nodes })

                    } else {
                        setCheckNode(0);
                    }
                    break
                case 1:
                case 2:
                    if (e.button === 0) {
                        nodes[key] = { key, name: checkButton === 1 ? '开始' : '结束', type: checkButton, x, y, radius: 20, isCheck: false };
                        setNodes({ ...nodes })
                    }
                    break
                case 3:
                case 4:
                    if (e.button === 0) {
                        nodes[key] = { key, name: checkButton === 3 ? '任务节点' : '子流程节点', type: checkButton, x, y, isCheck: false, width: 100, heigth: 50 };
                        setNodes({ ...nodes })
                    }
                    break
                case 5:
                    nodes[key] = { key, type: checkButton, name: '决策节点', x, y, isCheck: false, radius: 40 };
                    setNodes({ ...nodes })
                    break
                case 6:
                    const currentConnetLine: Node = { key, type: checkButton, name: '连接线', x, y, isCheck: false }
                    setCurrentConnetLine(key)
                    let check = false;
                    Object.values(nodes).map(node => {
                        if (node) {
                            if (!check && checkNodeSelect(node, e)) {
                                currentConnetLine.startNode = node.key
                                check = true;
                            }
                        }
                    })
                    nodes[key] = currentConnetLine;
                    setNodes({ ...nodes })
                    break;
            }
        }
    };

    const onMouseMove = (e: MouseEvent) => {
        const canvas = canvasRef.current;
        if (canvas && mouseDown) {
            const rect = canvas.getBoundingClientRect();
            switch (checkButton) {
                case 0:
                    const node = nodes[checkNode];
                    if (node) {
                        node.x = e.clientX - rect.left;
                        node.y = e.clientY - rect.top;
                        //重新计算连接线
                        Object.values(nodes).map(node => {
                            if (node?.type === 6 && node.startNode && node.endNode) {
                                const startNode = nodes[node.startNode];
                                const endNode = nodes[node.endNode];
                                if (startNode && endNode) {
                                    node.ponits = getConnetPoints(startNode, endNode)
                                }
                            }
                        });
                        setNodes({ ...nodes })
                    }
                    break;
                case 6:
                    const lineNode = nodes[currentConnetLine];
                    if (lineNode) {
                        lineNode.endPoint = { x: e.clientX - rect.left, y: e.clientY - rect.top }
                        setNodes({ ...nodes })
                    }
                    break;

            }

        }
    };

    const handleKeyDown = (event: any) => {
        if (event.key === 'Delete' && checkNode) {
            nodes[checkNode] = undefined
            setNodes({ ...nodes })
        }
    };

    useEffect(() => {
        document.addEventListener('keydown', handleKeyDown);
        return () => {
            document.removeEventListener('keydown', handleKeyDown);
        };
    }, [checkNode]);

    const onMouseUp = (e: MouseEvent) => {
        if (e.button === 0) {
            switch (checkButton) {
                case 6:
                    const canvas = canvasRef.current;
                    const connetLine = nodes[currentConnetLine];
                    if (canvas && connetLine) {
                        if (!connetLine.startNode) {
                            nodes[currentConnetLine] = undefined;
                            setCurrentConnetLine(0)
                            setNodes({ ...nodes })
                            return;
                        }
                        const rect = canvas.getBoundingClientRect();
                        const x = e.clientX - rect.left;
                        const y = e.clientY - rect.top;
                        if (x === connetLine.x && y === connetLine.y) {
                            nodes[currentConnetLine] = undefined;
                            setCurrentConnetLine(0)
                        } else {
                            let check = false;
                            Object.values(nodes).map(node => {
                                if (node) {
                                    if (!check && checkNodeSelect(node, e) && connetLine.startNode !== node.key) {
                                        connetLine.endNode = node.key;
                                        check = true;
                                    }
                                }
                            })
                            if (!check) {
                                nodes[currentConnetLine] = undefined;
                            } else {
                                connetLine.endPoint = undefined;
                                if (connetLine.endNode) {
                                    const startNode = nodes[connetLine.startNode];
                                    const endNode = nodes[connetLine.endNode];
                                    if (startNode && endNode) {
                                        connetLine.ponits = getConnetPoints(startNode, endNode)
                                        nodes[currentConnetLine] = connetLine;
                                    }
                                }
                                setCurrentConnetLine(0)
                            }
                            setNodes({ ...nodes })
                        }
                        break;
                    }
                    break;
            }
            setMouseDown(false)
        }
    };

    const changeButton = (btn: number) => {
        setCheckButton(btn);
        switch (btn) {
            case 1: case 2: case 3: case 4: case 5:
                setCheckNode(0);
                Object.values(nodes).map(node => {
                    if (node)
                        node.isCheck = false;
                })
                setNodes({ ...nodes })
                break;
        }

    }
    const typeStr = (type: number) => {
        switch (type) {
            case 1: return '开始节点';
            case 2: return '结束节点';
            case 3: return '任务节点';
            case 4: return '子流程节点';
            case 5: return '决策节点';
            case 6: return '连接线';
        }
    }
    return (<Row gutter={24}>
        <Col span={6}>

            <table className={styles.canvasTable}>
                <thead>
                    <tr>
                        <th colSpan={2} >
                            <p style={{ paddingTop: 10 }}>
                                绘制工具
                            </p>
                        </th>
                    </tr>
                </thead>
                <tbody >
                    <tr>
                        <td style={{ backgroundColor: checkButton === 0 ? '#948f8f' : '' }} onClick={() => { changeButton(0) }} >
                            <Tooltip title='选择'>
                                <Button onClick={() => { changeButton(0) }} icon={<DragOutlined />} />
                            </Tooltip>
                        </td>
                        <td style={{ backgroundColor: checkButton === 1 ? '#948f8f' : '' }} onClick={() => { changeButton(1) }} >
                            <Tooltip title='开始节点'>
                                <Button onClick={() => { changeButton(1) }} shape="circle" style={{ backgroundColor: '#90EE90' }}><p style={{ fontSize: '11px', paddingTop: '10px' }}>开始</p></Button>
                            </Tooltip>
                        </td>
                    </tr>
                    <tr>
                        <td style={{ backgroundColor: checkButton === 2 ? '#948f8f' : '' }} onClick={() => { changeButton(2) }}>
                            <Tooltip title='结束节点'>
                                <Button onClick={() => { changeButton(2) }} shape="circle" style={{ backgroundColor: '#FFA07A' }} ><p style={{ fontSize: '11px', paddingTop: '10px' }}>结束</p></Button>
                            </Tooltip>
                        </td>
                        <td style={{ backgroundColor: checkButton === 5 ? '#948f8f' : '' }} onClick={() => { changeButton(5) }}>
                            <Tooltip title='决策节点'>
                                <div className={styles.rotatedSquare} onClick={() => { changeButton(5) }}>if</div>
                            </Tooltip>
                        </td>
                    </tr>
                    <tr>
                        <td style={{ backgroundColor: checkButton === 3 ? '#948f8f' : '' }} onClick={() => { changeButton(3) }}>
                            <Tooltip title='任务节点'>
                                <Button onClick={() => { changeButton(3) }} shape="round" style={{ border: '1px solid #FF0000', width: '50px' }}>
                                    <p style={{ fontSize: '11px', paddingTop: '10px' }}>任务</p>
                                </Button>
                            </Tooltip>
                        </td>
                        <td style={{ backgroundColor: checkButton === 4 ? '#948f8f' : '' }} onClick={() => { changeButton(4) }}>
                            <Tooltip title='子流程节点' >
                                <Button onClick={() => { changeButton(4) }} shape="round" style={{ border: '1px solid #ADD8E6', width: '50px' }}>
                                    <p style={{ fontSize: '11px', paddingTop: '10px' }}>子流程</p>
                                </Button>
                            </Tooltip>
                        </td>
                    </tr>
                    <tr>
                        <td style={{ backgroundColor: checkButton === 6 ? '#948f8f' : '' }} onClick={() => { changeButton(6) }} >
                            <Tooltip title='连接节点'>
                                <Button onClick={() => { changeButton(6) }} icon={<BranchesOutlined />} />
                            </Tooltip>
                        </td>
                        <td>
                        </td>
                    </tr>
                </tbody>
            </table>
            <table className={styles.canvasTable}>
                <thead>
                    <tr>
                        <th colSpan={2} >
                            <p style={{ paddingTop: 10 }}>
                                节点属性
                            </p>
                        </th>
                    </tr>
                </thead>
                <tbody >
                    <tr>
                        <td style={{ width: '80px' }}>节点名称</td>
                        <td >
                            {(checkNode !== 0 && nodes[checkNode]) && (<div>{nodes[checkNode].name}</div>)}
                        </td>
                    </tr>
                    <tr>
                        <td >节点类型</td>
                        <td >
                            {(checkNode !== 0 && nodes[checkNode]) && (<div>{typeStr(nodes[checkNode].type)}</div>)}
                        </td>
                    </tr>
                </tbody>
            </table>
        </Col>
        <Col span={18}>
            <canvas onMouseDown={onMouseDown}
                onMouseMove={onMouseMove}
                onMouseUp={onMouseUp}
                ref={canvasRef}
                width='700px'
                height='400px'
                style={{ border: '1px solid #000' }}

            />
        </Col>
    </Row>
    )
}
export default Test;

index.less:

css 复制代码
.rotatedSquare {  
  background-color: rgb(216, 218, 223);  
  padding-top: 2px;
  margin-top: 2px;
  margin-left: 30px;
  cursor: pointer;
  height: 26px;
  width: 30px;
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); 
}  

.canvasTable{
  border: 1px solid #000;
  width: 100%;
}
.canvasTable th{
  text-align: center;
  align-items: center;
}
.canvasTable td{
  padding: 5px;
  text-align: center;
  align-items: center;
  border: 1px solid #000
}
相关推荐
别拿曾经看以后~1 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
川石课堂软件测试1 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
problc2 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter