记一次react项目の流程和工单管理

709 阅读4分钟

记一次react项目里的流程和工单管理。

流程图的绘制用的是@antv/x6

工单模板用的是 fr-generatorform-render

每步流程可以选择要填写的模板,选择允许输入的人,只有允许的用户才可以点击下一步

我记得是参考一个vue项目做的,但是项目地址忘了哎

写得不是很好,见谅,这算是半年前写的东西了,好多忘了,没什么精力去缩减

  • 流程

image-20220409204123147.png

  • 工单

image-20220409220313410.png

流程图布局样式

# 布局
<Modal
 <div width="100%">
     //整一块
            <div className={"processWrap"}>
                //上面的工具栏
              <div className={"toolbar"}>{isReady && <ToolBar />}</div>
			//一整块,分成三块,左,中,右
              <div className={"content"}>
                  
                <div id="stencil" className={"shapes"}></div>
                <div id="container" className="x6-graph" />
                <div className={"config1"}>
                  {isReady && (
                    <ConfigPanel
                      users={users}
                      roles={roles}
                      templates={selectTemplates}
                      taskListData={taskListData}
                    />
                  )}
                </div>
              </div>
            </div>
          </div>
#样式
.processWrap {
    width: 100%;
    height: 100%;
    .toolbar {
      display: flex;
      align-items: center;
      height: 40px;
      background-color: #fafbfc;
      border-bottom: 1px solid #dfe3e8;
    }
    .content {
      display: flex;
      height: calc(100% - 105px);
      position: relative;
      .shapes {
        position: relative;
        width: 127px;
        height: 600px;
        border-right: 1px solid #dfe3e8;
      }
      .config1 {
        box-sizing: border-box;
        width: 290px;
        height: 600px;
        overflow: auto;
        padding: 0 24px;
        border-left: 1px solid rgba(0, 0, 0, 0.08);
        
        .ant-row .result{
          background: #eee;
          color: #333333;
          padding: 3px 4px;
          border-radius: 5px;
          display: inline-block;
          font-size: 12px;
          margin-left: 4px;
          line-height: 1.25;
          margin-top: 4px;
        }
      }
 
    }
  }
  .config1::-webkit-scrollbar{
    width: 0;
  }
  .configNode{
    .ant-form-item{
      margin-bottom: 6px;
    }
    .ant-form-item-label > label{
      height: 6px;
    }
    .ant-form-vertical .ant-form-item-label, .ant-col-24.ant-form-item-label, .ant-col-xl-24.ant-form-item-label{
      padding:0
    }
  }
  .pd-b8{
    padding-bottom: 8px;
  }

  

  .x6-widget-stencil {
    background-color: #fff;
    .x6-widget-stencil-title {
      background-color: #fff;
    }
  }
  
  // resizing
  .x6-widget-dnd{
    z-index: 5000;
  }
  .x6-widget-transform {
    margin: -1px 0 0 -1px;
    padding: 0px;
    border: 1px solid #239edd;
    > div {
      border: 1px solid #239edd;
    }
    > div:hover {
      background-color: #3dafe4;
    }
    .x6-widget-transform-active-handle {
      background-color: #3dafe4;
    }
  }
  .x6-widget-transform-resize {
    border-radius: 0;
  }
  
  // selection
  .x6-widget-selection-inner {
    border: 1px solid #239edd;
  }
  .x6-widget-selection-box {
    opacity: 0;
  }
  
  // snapline
  .x6-widget-snapline-vertical {
    border-right-color: #239edd;
  }
  .x6-widget-snapline-horizontal {
    border-bottom-color: #239edd;
  }
  
  // minimap
  .x6-widget-minimap-viewport,
  .x6-widget-minimap-viewport-zoom {
    border: 2px solid #3371dd;
  }
  
  .processModal{
    .dnd-container::-webkit-scrollbar{
      width: 0;
    }
  }
  .fr-wrapper{
    min-height: 0px!important;
  }
  .createOrder{
   
    .workflow-classify-title {
      border-left: 3px solid rgb(64, 158, 255);
      padding-left: 5px;
      font-size: 16px;

    }
    .process-div-body {
      width: 16%;
      display: inline-block;
      margin-left: 0;
      margin-top: 12px;
      padding-left: 5px;
      text-align: left;
      margin-bottom:15px;
      border: 1px solid #dcdfe6;
      cursor: pointer;
      border-radius: 5px;
     
    }
    .process-div-body:hover{
      border: 1px solid black;
    }
    .process-div-title {
      font-size: 15px;
      margin-top: 6px;
      color: #606266;
      height: 18px;
      line-height: 18px;
    }
  
    .process-div-remarks {
      color: #999999;
      margin-top: 6px;
      font-size: 12px;
    }
  
    .ellipsis {
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }

  .ant-select-item-option-content2 .ant-select-item-option-content{
    white-space:normal;
    text-overflow:inherit
  }

工具栏toolbar代码

import React, { useEffect, useState } from 'react';
import { Toolbar } from '@antv/x6-react-components';
import FlowGraph from '../../Graph';
import { ClearOutlined, UndoOutlined, RedoOutlined, ZoomInOutlined, ZoomOutOutlined, } from '@ant-design/icons';
import '@antv/x6-react-components/es/toolbar/style/index.css';
const Item = Toolbar.Item;
const Group = Toolbar.Group;
export default function () {
  const [canUndo, setCanUndo] = useState(false);
  const [canRedo, setCanRedo] = useState(false);
  const [zoom, setZoom] = useState(1);
  useEffect(() => {
    const { graph } = FlowGraph;
    // history
    const { history } = graph;
    setCanUndo(history.canUndo());
    setCanRedo(history.canRedo());
    history.on('change', () => {
      setCanUndo(history.canUndo());
      setCanRedo(history.canRedo());
    });
    // zoom
    setZoom(graph.zoom());
    graph.on('scale', () => {
      setZoom(graph.zoom());
    });
  }, []);
  const handleClick = (name) => {
    const { graph } = FlowGraph;
    switch (name) {
      case 'undo':
        graph.history.undo();
        break;
      case 'redo':
        graph.history.redo();
        break;
      case 'delete':
        graph.clearCells();
        break;
      case 'zoomIn':
        graph.zoom(0.1);
        break;
      case 'zoomOut':
        graph.zoom(-0.1);
        break;
      default:
        break;
    }
  };
  return (<div>
    <Toolbar hoverEffect={true} size="small" onClick={handleClick}>
      <Group>
        <Item name="delete" icon={<ClearOutlined />} tooltip="Clear (Cmd + D, Ctrl + D)" />
      </Group>
      <Group>
        <Item name="zoomIn" tooltip="ZoomIn (Cmd + 1, Ctrl + 1)" icon={<ZoomInOutlined />} disabled={zoom > 1.5} />
        <Item name="zoomOut" tooltip="ZoomOut (Cmd + 2, Ctrl + 2)" icon={<ZoomOutOutlined />} disabled={zoom < 0.5} />
        <span style={{ lineHeight: '28px', fontSize: 12, marginRight: 4 }}>
          {`${(zoom * 100).toFixed(0)}%`}
        </span>
      </Group>
      <Group>
        <Item name="undo" tooltip="Undo (Cmd + Z, Ctrl + Z)" icon={<UndoOutlined />} disabled={!canUndo} />
        <Item name="redo" tooltip="Redo (Cmd + Shift + Z, Ctrl + Y)" icon={<RedoOutlined />} disabled={!canRedo} />
      </Group>
    </Toolbar>
  </div>);
}

左侧图形shape.js

import { Graph } from '@antv/x6'

const ports = {
  groups: {
    top: {
      position: 'top',
      attrs: {
        circle: {
          r: 4,
          magnet: true,
          stroke: '#D06269',
          strokeWidth: 1,
          fill: '#fff',
          style: {
            visibility: 'hidden',
          },
        },
      },
    },
    right: {
      position: 'right',
      attrs: {
        circle: {
          r: 4,
          magnet: true,
          stroke: '#D06269',
          strokeWidth: 1,
          fill: '#fff',
          style: {
            visibility: 'hidden',
          },
        },
      },
    },
    bottom: {
      position: 'bottom',
      attrs: {
        circle: {
          r: 4,
          magnet: true,
          stroke: '#D06269',
          strokeWidth: 1,
          fill: '#fff',
          style: {
            visibility: 'hidden',
          },
        },
      },
    },
    left: {
      position: 'left',
      attrs: {
        circle: {
          r: 4,
          magnet: true,
          stroke: '#D06269',
          strokeWidth: 1,
          fill: '#fff',
          style: {
            visibility: 'hidden',
          },
        },
      },
    },
  },
  items: [
    {
      group: 'top',
    },
    {
      group: 'right',
    },
    {
      group: 'bottom',
    },
    {
      group: 'left',
    },
  ],
}

Graph.registerNode('custom-rect', {
  inherit: 'rect',
  width: 60,
  height: 30,
  attrs: {
    body: {
      strokeWidth: 1,
    },
    // label: {
    //   text: 'name',
    // },
  },
  ports: { ...ports },
})

Graph.registerNode('custom-polygon', {
  inherit: 'polygon',
  width: 60,
  height: 30,
  attrs: {
    body: {
      strokeWidth: 1,
    },

  },
  ports: { ...ports },
})

Graph.registerNode('custom-circle', {
  inherit: 'circle',
  width: 48,
  height: 48,
  attrs: {
    body: {
      strokeWidth: 1,
    },
  },
  ports: { ...ports },
})

中间流程图graph

获取id为container和stencil去渲染

import { Graph, Addon, Shape } from "@antv/x6";
import "./shape";

export default class FlowGraph {
  static graph;
  static stencil;

  static init() {
    this.graph = new Graph({
      container: document.getElementById("container"),
      width: 1000,
      height: 800,
      grid: {
        size: 10,
        visible: true,
        type: "doubleMesh",
        args: [
          {
            color: "#E7E8EA",
            thickness: 1,
          },
          {
            color: "#CBCED3",
            thickness: 1,
            factor: 5,
          },
        ],
      },
      panning: {
        enabled: true,
        eventTypes: ["leftMouseDown", "rightMouseDown", "mouseWheel"],
        modifiers: "ctrl",
      },
      mousewheel: {
        enabled: true,
        zoomAtMousePosition: true,
        modifiers: "ctrl",
        minScale: 0.5,
        maxScale: 3,
      },
      connecting: {
        router: "manhattan",
        connector: {
          name: "rounded",
          args: {
            radius: 8,
          },
        },
        anchor: "center",
        connectionPoint: "anchor",
        allowBlank: false,
        snap: {
          radius: 20,
        },
        createEdge() {
          return new Shape.Edge({
            attrs: {
              line: {
                stroke: "#000",
                strokeWidth: 1,
                targetMarker: {
                  name: "block",
                  width: 12,
                  height: 8,
                },
              },
              label: {
                clazz: "flow",
              },
            },
            zIndex: 0,
          });
        },
        validateConnection({ targetMagnet }) {
          return !!targetMagnet;
        },
      },
      highlighting: {
        magnetAdsorbed: {
          name: "stroke",
          args: {
            attrs: {
              fill: "#D06269",
              stroke: "#D06269",
            },
          },
        },
      },
      resizing: true,
      rotating: true,
      selecting: {
        enabled: true,
        rubberband: true,
        showNodeSelectionBox: true,
      },
      snapline: true,
      keyboard: true,
      history: true,
      minimap: {
        enabled: true,
        container: document.getElementById("minimap"),
        width: 198,
        height: 198,
        padding: 10,
      },
      clipboard: true,
    });
    this.initStencil();
    this.initShape();
    this.initEvent();
    this.initKeyboard();
    return this.graph;
  }

  static initStencil() {
    this.stencil = new Addon.Stencil({
      title: "节点",
      target: this.graph,
      stencilGraphWidth: 107,
      stencilGraphHeight: 600,
      layoutOptions: {
        columns: 1,
        columnWidth: 68,
        rowHeight: 50,
        marginY: 40,
      },
      getDropNode(node) {
        const size = node.size();
        return node.clone().size(size.width * 1.5, size.height * 1.5);
      },
    });
    const stencilContainer = document.querySelector("#stencil");
    if (stencilContainer) {
      stencilContainer.appendChild(this.stencil.container);
    }
  }

  static initShape() {
    const { graph } = this;
    const r1 = graph.createNode({
      shape: "custom-circle",
      attrs: {
        label: {
          text: "起点",
          clazz: "start",
          task: [],
          readonlyTpls: [],
          hideTpls: [],
          cc: [],
        },
      },
    });
    const r2 = graph.createNode({
      shape: "custom-rect",
      attrs: {
        label: {
          text: "审核节点",
          clazz: "userTask",
          task: [],
          assignType: "",
          assignValue: [],
          isCounterSign: false,
          fullHandle: false,
          activeOrder: false,
          writeTpls: [],
          hideTpls: [],
          cc: [],
        },
      },
    });
    const r3 = graph.createNode({
      shape: "custom-rect",
      // label: 'second',
      attrs: {
        body: {
          rx: 4,
          ry: 4,
        },
        label: {
          text: "处理节点",
          clazz: "receiveTask",
          task: [],
          assignType: "",
          assignValue: [],
          isCounterSign: false,
          fullHandle: false,
          activeOrder: false,
          writeTpls: [],
          hideTpls: [],
          cc: [],
        },
      },
    });
    const r4 = graph.createNode({
      shape: "custom-circle",
      attrs: {
        label: {
          text: "终点",
          clazz: "end",
          task: [],
          hideTpls: [],
        },
      },
    });
    const r5 = graph.createNode({
      shape: "custom-polygon",
      // label: 'third',
      attrs: {
        body: {
          refPoints: "0,10 10,0 20,10 10,20",
          fill: "#2ECC71",
        },
        label: {
          text: "并行",
          clazz: "parallelGateway",
        },
      },
    });
    const r6 = graph.createNode({
      shape: "custom-polygon",
      attrs: {
        // body: {
        //   refPoints: '10,0 40,0 30,20 0,20',
        // },
        body: {
          refPoints: "0,10 10,0 20,10 10,20",
          fill: "#D24646",
        },
        label: {
          text: "排他",
          clazz: "exclusiveGateway",
        },
      },
    });

    this.stencil.load([r1, r2, r3, r4, r5, r6]);
  }

  static showPorts(ports, show) {
    for (let i = 0, len = ports.length; i < len; i = i + 1) {
      ports[i].style.visibility = show ? "visible" : "hidden";
    }
  }

  static initEvent() {
    const { graph } = this;
    const container = document.getElementById("container");

    graph.on("node:mouseenter", () => {
      const ports = container.querySelectorAll(".x6-port-body");
      this.showPorts(ports, true);
    });
    graph.on("node:mouseleave", () => {
      const ports = container.querySelectorAll(".x6-port-body");
      this.showPorts(ports, false);
    });
  }

  static initKeyboard() {
    const { graph } = this;
    // copy cut paste
    graph.bindKey(["meta+c", "ctrl+c"], () => {
      const cells = graph.getSelectedCells();
      if (cells.length) {
        graph.copy(cells);
      }
      return false;
    });
    graph.bindKey(["meta+x", "ctrl+x"], () => {
      const cells = graph.getSelectedCells();
      if (cells.length) {
        graph.cut(cells);
      }
      return false;
    });
    graph.bindKey(["meta+v", "ctrl+v"], () => {
      if (!graph.isClipboardEmpty()) {
        const cells = graph.paste({ offset: 32 });
        graph.cleanSelection();
        graph.select(cells);
      }
      return false;
    });

    //undo redo
    graph.bindKey(["meta+z", "ctrl+z"], () => {
      if (graph.history.canUndo()) {
        graph.history.undo();
      }
      return false;
    });
    graph.bindKey(["meta+shift+z", "ctrl+shift+z"], () => {
      if (graph.history.canRedo()) {
        graph.history.redo();
      }
      return false;
    });

    // select all
    graph.bindKey(["meta+a", "ctrl+a"], () => {
      const nodes = graph.getNodes();
      if (nodes) {
        graph.select(nodes);
      }
    });

    //delete
    graph.bindKey("backspace", () => {
      const cells = graph.getSelectedCells();
      if (cells.length) {
        graph.removeCells(cells);
      }
    });

    // zoom
    graph.bindKey(["ctrl+1", "meta+1"], () => {
      const zoom = graph.zoom();
      if (zoom < 1.5) {
        graph.zoom(0.1);
      }
    });
    graph.bindKey(["ctrl+2", "meta+2"], () => {
      const zoom = graph.zoom();
      if (zoom > 0.5) {
        graph.zoom(-0.1);
      }
    });
  }
}

流程图初始化渲染

import FlowGraph from "./components/process/Graph";

	await value = this.getProcessInitData();
	//获取数据,打开弹窗(我项目的流程图是放在modal里的),
	this.setState({ open: true }, () => {
	//这里之所以加定时器去打开,是因为有时候打开会白屏报错,发现加定时器就ok了
      this.timer = setTimeout(() => {
        this.graph = FlowGraph.init();
        this.graph.resize(1402, 600);
        this.setState({ isReady: true });
        if (value) {
          this.graph.fromJSON(value);
        }
      }, 200);
    });

右侧流程相关编辑

# index.js  	分为点,线,网格三块点击相应有的不同的信息。
import React, { useEffect, useState } from "react";
import ConfigGrid from "./ConfigGrid";
import ConfigNode from "./ConfigNode";
import ConfigEdge from "./ConfigEdge";
import FlowGraph from "../../Graph";
import { useGridAttr } from "./global";

export var CONFIG_TYPE;

CONFIG_TYPE = {
  GRID: "GRID",
  NODE: "NODE",
  EDGE: "EDGE",
};
export default function (props) {
  const { users, roles, templates, taskListData, isProcessor } = props;
  const [type, setType] = useState(CONFIG_TYPE.GRID);
  const [id, setId] = useState("");
  const { gridAttrs, setGridAttr } = useGridAttr();

  useEffect(() => {
    const { graph } = FlowGraph;
    graph.on("blank:click", () => {
      setType(CONFIG_TYPE.GRID);
    });
    graph.on("cell:click", ({ cell }) => {
      setType(cell.isNode() ? CONFIG_TYPE.NODE : CONFIG_TYPE.EDGE);
      setId(cell.id);
    });
  }, []);

  return (
    <div className={"config"}>
      {type === CONFIG_TYPE.GRID && (
        <ConfigGrid attrs={gridAttrs} setAttr={setGridAttr} />
      )}
      {type === CONFIG_TYPE.NODE && (
        <ConfigNode
          id={id}
          users={users}
          roles={roles}
          templates={templates}
          taskListData={taskListData}
        />
      )}
      {type === CONFIG_TYPE.EDGE  && <ConfigEdge id={id} />}
    </div>
  );
}

# global.js 样式默认值
import { useState } from 'react';
export function useGridAttr() {
    const [gridAttrs, setGridAttrs] = useState({
        type: 'mesh',
        size: 10,
        color: '#e5e5e5',
        thickness: 1,
        colorSecond: '#d0d0d0',
        thicknessSecond: 1,
        factor: 4,
        bgColor: 'transparent',
        showImage: false,
        repeat: 'watermark',
        angle: 30,
        position: 'center',
        bgSize: JSON.stringify({ width: 150, height: 150 }),
        opacity: 0.1,
    });
    const setGridAttr = (key, value) => {
        setGridAttrs((prev) => (Object.assign(Object.assign({}, prev), { [key]: value })));
    };
    return {
        gridAttrs,
        setGridAttr,
    };
}
# ConfigEdge 线
import React, { useEffect, useState, useRef } from 'react';
import { Tabs, Row, Col, Input, Slider, Select,Switch,Form } from 'antd';
import FlowGraph from '../../../Graph';
const { TabPane } = Tabs;
export default function (props) {
    const { id } = props;
    const [attrs, setAttrs] = useState({
        stroke: '#5F95FF',
        strokeWidth: 1,
        connector: 'normal',
        text: '',
        sort: '',
        flowProperties:'',
        isExecuteTask:false,
        conditionExpression:''
    });
    const cellRef = useRef();
    const formRef = useRef();
    useEffect(() => {
        if (id) {
            const { graph } = FlowGraph;
            const cell = graph.getCellById(id);
            if (!cell || !cell.isEdge()) {
                return;
            }
            cellRef.current = cell;
            const connector = cell.getConnector() || {
                name: 'normal',
            };
            setAttr('stroke', cell.attr('line/stroke'));
            setAttr('strokeWidth', cell.attr('line/strokeWidth'));
            setAttr('connector', connector.name);
            setAttr('text', cell.attr('label/text'));
            setAttr('sort', cell.attr('label/sort'));
            setAttr('flowProperties', cell.attr('label/flowProperties'));
            setAttr('isExecuteTask', cell.attr('label/isExecuteTask'));
            setAttr('conditionExpression', cell.attr('label/conditionExpression'));
            formRef.current.setFieldsValue({text:cell.attr('label/text'),sort:cell.attr('label/sort'),flowProperties:cell.attr('label/flowProperties')})
        }
    }, [id]);
    const setAttr = (key, val) => {
      setAttrs((prev) => ({
        ...prev,
        [key]: val,
      }));
    };
    const onStrokeChange = (e) => {
        const val = e.target.value;
        setAttr('stroke', val);
        cellRef.current.attr('line/stroke', val);
    };
    const onStrokeWidthChange = (val) => {
        setAttr('strokeWidth', val);
        cellRef.current.attr('line/strokeWidth', val);
    };
    const onConnectorChange = (val) => {
        setAttr('connector', val);
        const cell = cellRef.current;
        cell.setConnector(val);
    };
    const onConditionExpression = (val)=>{
      let value = val.target.value
        setAttr('conditionExpression', value);
        cellRef.current.attr('label/conditionExpression', value);
    }
    const onIsExecuteTask = (val)=>{
      let value = val
        setAttr('isExecuteTask', value);
        cellRef.current.attr('label/isExecuteTask', value);
    }

    const onLabelChange = (x, y) => {
      // console.log(x, y)
      Object.keys(x).forEach(i => {
        if (i === 'text')cellRef.current.setLabels(x[i]);
        cellRef.current.attr(`label/${i}`, x[i]);
        setAttr(i, x[i]);
      })
    }

    return (<Tabs defaultActiveKey="1" >
      <TabPane tab="线条" key="1" className='configNode'>
      <Form
        // name="basic"
        // labelCol={{ span: 8 }}
        // wrapperCol={{ span: 16 }}
        layout='vertical'
        ref={formRef}
        labelAlign='left'
        size='small'
        // initialValues={{ text: attrs.text }}
        // onFinish={onFinish}
        // onFinishFailed={onFinishFailed}
        onValuesChange={onLabelChange}
      >
           <Form.Item
          label="名称"
          name="text"
          rules={[{ required: true, message: '必填项' }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="顺序"
          name="sort"
          rules={[{ required: true, message: '必填项' }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="属性"
          name="flowProperties"
          rules={[{ required: true, message: '必填项' }]}
        >
             <Select style={{ width: '100%' }} value={attrs.flowProperties} >
              <Select.Option value="1">同意</Select.Option>
              <Select.Option value="0">拒绝</Select.Option>
              <Select.Option value="2">其他</Select.Option>
            </Select>
        </Form.Item>
      </Form>
        <Row align="middle" className='pd-b8'>
          <Col span={14}>是否执行任务</Col>
          <Col span={8}>
            <Switch checkedChildren="是" unCheckedChildren="否" onChange={onIsExecuteTask} checked={attrs.isExecuteTask}></Switch>
          </Col>
        </Row>
        <Row align="middle" className='pd-b8'>
          <Col span={24}>条件表达式</Col>
          <Col span={24}>
            <Input.TextArea onChange={onConditionExpression} value={attrs.conditionExpression}/>
          </Col>
        </Row>
        <Row align="middle">
          <Col span={8}>宽度</Col>
          <Col span={14}>
            <Slider min={1} max={5} step={1} value={attrs.strokeWidth} onChange={onStrokeWidthChange}/>
          </Col>
          <Col span={2}>
            <div className="result">{attrs.strokeWidth}</div>
          </Col>
        </Row>
        <Row align="middle" className='pd-b8'>
          <Col span={8}>颜色</Col>
          <Col span={14}>
            <Input type="color" value={attrs.stroke} style={{ width: '100%' }} onChange={onStrokeChange}/>
          </Col>
        </Row>
        <Row align="middle">
          <Col span={8}>形状</Col>
          <Col span={14}>
            <Select style={{ width: '100%' }} value={attrs.connector} onChange={onConnectorChange}>
              <Select.Option value="normal">Normal</Select.Option>
              <Select.Option value="smooth">Smooth</Select.Option>
              <Select.Option value="rounded">Rounded</Select.Option>
              <Select.Option value="jumpover">Jumpover</Select.Option>
            </Select>
          </Col>
        </Row>
      </TabPane>
    </Tabs>);
}
# ConfigGrid 网格
import React, { useEffect } from 'react'
import { Tabs, Row, Col, Select, Slider, Input, Checkbox } from 'antd'
import FlowGraph from '../../../Graph'

const { TabPane } = Tabs

const GRID_TYPE = {
  DOT: 'dot',
  FIXED_DOT: 'fixedDot',
  MESH: 'mesh',
  DOUBLE_MESH: 'doubleMesh',
}

const REPEAT_TYPE = {
  NO_REPEAT: 'no-repeat',
  REPEAT: 'repeat',
  REPEAT_X: 'repeat-x',
  REPEAT_Y: 'repeat-y',
  ROUND: 'round',
  SPACE: 'space',
  FLIPX: 'flipX',
  FLIPY: 'flipY',
  FLIPXY: 'flipXY',
  WATERMARK: 'watermark',
}


const tryToJSON = (val) => {
  try {
    return JSON.parse(val);
  }
  catch (error) {
    return val;
  }
};
export default function (props) {
  const { attrs, setAttr } = props;
  // console.log('attrs', attrs)
  useEffect(() => {
    let options;
    if (attrs.type === 'doubleMesh') {
      options = {
        type: attrs.type,
        args: [
          {
            color: attrs.color,
            thickness: attrs.thickness,
          },
          {
            color: attrs.colorSecond,
            thickness: attrs.thicknessSecond,
            factor: attrs.factor,
          },
        ],
      };
    }
    else {
      options = {
        type: attrs.type,
        args: [
          {
            color: attrs.color,
            thickness: attrs.thickness,
          },
        ],
      };
    }
    // console.log('options', options)
    const { graph } = FlowGraph;
    graph.drawGrid(options);
  }, [
    attrs.type,
    attrs.color,
    attrs.thickness,
    attrs.thicknessSecond,
    attrs.colorSecond,
    attrs.factor,
  ]);
  useEffect(() => {
    const { graph } = FlowGraph;
    graph.setGridSize(attrs.size);
  }, [attrs.size]);
  useEffect(() => {
    const options = {
      color: attrs.bgColor,
      image: attrs.showImage
        ? 'https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*o-MuTpQaj7EAAAAAAAAAAABkARQnAQ'
        : undefined,
      repeat: attrs.repeat,
      angle: attrs.angle,
      size: tryToJSON(attrs.bgSize),
      position: tryToJSON(attrs.position),
      opacity: attrs.opacity,
    };
    const { graph } = FlowGraph;
    graph.drawBackground(options);
  }, [
    attrs.bgColor,
    attrs.showImage,
    attrs.repeat,
    attrs.angle,
    attrs.bgSize,
    attrs.position,
    attrs.opacity,
  ]);
  return (<Tabs defaultActiveKey="1">
    <TabPane tab="网格" key="1">
      <Row align="middle">
        <Col span={10}>Grid Type</Col>
        <Col span={12}>
          <Select style={{ width: '100%' }} value={attrs.type} onChange={(val) => setAttr('type', val)}>
            <Select.Option value={GRID_TYPE.DOT}>Dot</Select.Option>
            <Select.Option value={GRID_TYPE.FIXED_DOT}>
              Fixed Dot
            </Select.Option>
            <Select.Option value={GRID_TYPE.MESH}>Mesh</Select.Option>
            <Select.Option value={GRID_TYPE.DOUBLE_MESH}>
              Double Mesh
            </Select.Option>
          </Select>
        </Col>
      </Row>
      <Row align="middle">
        <Col span={10}>Grid Size</Col>
        <Col span={10}>
          <Slider min={1} max={20} step={1} value={attrs.size} onChange={(val) => setAttr('size', val)} />
        </Col>
        <Col span={2}>
          <div className="result">{attrs.size}</div>
        </Col>
      </Row>
      {attrs.type === 'doubleMesh' ? (<React.Fragment>
        <Row align="middle">
          <Col span={10}>Primary Color</Col>
          <Col span={12}>
            <Input type="color" value={attrs.color} style={{ width: '100%' }} onChange={(e) => setAttr('color', e.target.value)} />
          </Col>
        </Row>
        <Row align="middle">
          <Col span={10}>Primary Thickness</Col>
          <Col span={10}>
            <Slider min={0.5} max={10} step={0.5} value={attrs.thickness} onChange={(val) => setAttr('thickness', val)} />
          </Col>
          <Col span={2}>
            <div className="result">{attrs.thickness.toFixed(1)}</div>
          </Col>
        </Row>
        <Row align="middle">
          <Col span={10}>Secondary Color</Col>
          <Col span={12}>
            <Input type="color" value={attrs.colorSecond} style={{ width: '100%' }} onChange={(e) => setAttr('colorSecond', e.target.value)} />
          </Col>
        </Row>
        <Row align="middle">
          <Col span={10}>Secondary Thickness</Col>
          <Col span={10}>
            <Slider min={0.5} max={10} step={0.5} value={attrs.thicknessSecond} onChange={(val) => setAttr('thicknessSecond', val)} />
          </Col>
          <Col span={2}>
            <div className="result">{attrs.thicknessSecond.toFixed(1)}</div>
          </Col>
        </Row>
        <Row align="middle">
          <Col span={10}>Scale Factor</Col>
          <Col span={10}>
            <Slider min={1} max={10} step={1} value={attrs.factor} onChange={(val) => setAttr('factor', val)} />
          </Col>
          <Col span={2}>
            <div className="result">{attrs.factor}</div>
          </Col>
        </Row>
      </React.Fragment>) : (<React.Fragment>
        <Row align="middle">
          <Col span={10}>Grid Color</Col>
          <Col span={12}>
            <Input type="color" value={attrs.color} style={{ width: '100%' }} onChange={(e) => setAttr('color', e.target.value)} />
          </Col>
        </Row>
        <Row align="middle">
          <Col span={10}>Thickness</Col>
          <Col span={10}>
            <Slider min={0.5} max={10} step={0.5} value={attrs.thickness} onChange={(val) => setAttr('thickness', val)} />
          </Col>
          <Col span={1}>
            <div className="result">{attrs.thickness.toFixed(1)}</div>
          </Col>
        </Row>
      </React.Fragment>)}
    </TabPane>
    <TabPane tab="背景" key="2">
      <Row align="middle">
        <Col span={6}>Color</Col>
        <Col span={14}>
          <Input type="color" value={attrs.bgColor} style={{ width: '100%' }} onChange={(e) => setAttr('bgColor', e.target.value)} />
        </Col>
      </Row>
  
    </TabPane>
  </Tabs>);
}
# ConfigNodeimport React, { useEffect, useState, useRef } from "react";
import { Tabs, Row, Col, Input, Slider, Select, Switch, Form } from "antd";
import FlowGraph from "../../../Graph";
const { TabPane } = Tabs;
export default function (props) {
  const { id, users, roles, templates, taskListData } = props;
  const [attrs, setAttrs] = useState({
    stroke: "#5F95FF",
    strokeWidth: 1,
    fill: "rgba(95,149,255,0.05)",
    fontSize: 12,
    color: "rgba(0,0,0,0.85)",
    text: "",
    sort: "",
    task: [],
    readonlyTpls: [],
    hideTpls: [],
    cc: [],
    clazz: "",
    assignType: "",
    assignValue: [],
    isCounterSign: false,
    fullHandle: false,
    activeOrder: false,
    writeTpls: [],
  });
  const cellRef = useRef();
  const formRef = useRef();
  // 处理、审核节点
  const customRect = ["userTask", "receiveTask"];
  // 网关
  const gateway = ["parallelGateway", "exclusiveGateway"];
  useEffect(() => {
    if (id) {
      const { graph } = FlowGraph;
      const cell = graph.getCellById(id);
      if (!cell || !cell.isNode()) {
        return;
      }
      cellRef.current = cell;
      let attrs = {
          //包括自定义的节点属性,这里初始化
        stroke: cell.attr("body/stroke"),
        strokeWidth: cell.attr("body/strokeWidth"),
        fill: cell.attr("body/fill"),
        fontSize: cell.attr("text/fontSize"),
        color: cell.attr("text/fill"),
        text: cell.attr("label/text"),
        sort: cell.attr("label/sort"),
        task: cell.attr("label/task"),
        readonlyTpls: cell.attr("label/readonlyTpls"),
        hideTpls: cell.attr("label/hideTpls"),
        cc: cell.attr("label/cc"),
        clazz: cell.attr("label/clazz"),
        assignType: cell.attr("label/assignType"),
        assignValue: cell.attr("label/assignValue"),
        isCounterSign: cell.attr("label/isCounterSign"),
        fullHandle: cell.attr("label/fullHandle"),
        activeOrder: cell.attr("label/activeOrder"),
        writeTpls: cell.attr("label/writeTpls"),
        host: cell.attr("label/host"),
        port: cell.attr("label/port"),
        username: cell.attr("label/username"),
        pwd: cell.attr("label/pwd"),
        exchange: cell.attr("label/exchange"),
        queue: cell.attr("label/queue"),
        rabbitmq_body: cell.attr("label/rabbitmq_body"),
        rabbitmq_body_type: cell.attr("label/rabbitmq_body_type"),
      };
      // console.log("assignValue", cell.attr("label/assignValue"));
      setAttrs(attrs);
   
      formRef.current.setFieldsValue(attrs);
    }
  }, [id]);

  const setAttr = (key, val) => {
    setAttrs((prev) => {
      return {
        ...prev,
        [key]: val,
      };
    });
  };

  const onStrokeChange = (e) => {
    const val = e.target.value;
    setAttr("stroke", val);
    cellRef.current.attr("body/stroke", val);
  };
  const onStrokeWidthChange = (val) => {
    setAttr("strokeWidth", val);
    cellRef.current.attr("body/strokeWidth", val);
  };
  const onFillChange = (e) => {
    const val = e.target.value;
    setAttr("fill", val);
    cellRef.current.attr("body/fill", val);
  };

  const onFontSizeChange = (val) => {
    setAttr("fontSize", val);
    cellRef.current.attr("text/fontSize", val);
  };

  const onColorChange = (e) => {
    const val = e.target.value;
    setAttr("color", val);
    cellRef.current.attr("text/fill", val);
  };

  const onLabelChange = (label, e) => {
    const val = e?.target?.value || e;
    setAttr(label, val);
    cellRef.current.attr(`label/${label}`, val);
  };

  const onValuesChange = (x, y) => {
    Object.keys(x).forEach((i) => {
      console.log(i, x[i]);
      // 指派类型改变的时候,值清空
      // if (i === "assignType" && x[i]) {
      //   console.log("改变类型");
      //   cellRef.current.attrs.label["assignValue"] = [];
      //   setAttr("assignValue", []);
      // }
      // 之所以设值,是为了这是必填项
      if (i === "assignType" && x[i] === "machine") {
        cellRef.current.attrs.label["assignValue"] = [0];
        // setAttr(i, x[i]);
      }
      // cellRef.current.attr(`label/${i}`, x[i]);
      cellRef.current.attrs.label[i] = x[i];
      setAttr(i, x[i]);
    });
  };

  return (
    <Tabs defaultActiveKey="1">
      <TabPane tab="节点" key="1" className="configNode">
        <Form

          layout="vertical"
          ref={formRef}
          labelAlign="left"
          size="small"

          onValuesChange={onValuesChange}
        >
          <Form.Item
            label="名称"
            name="text"
            rules={[{ required: true, message: "必填项" }]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            label="顺序"
            name="sort"
            rules={[{ required: true, message: "必填项" }]}
          >
            <Input />
          </Form.Item>
          {gateway.includes(attrs.clazz) ? null : (
            <Form.Item label="之后任务" name="task">
              <Select mode="multiple" allowClear showArrow>
                {taskListData.map((i) => {
                  return (
                    <Select.Option value={i.id} key={i.id}>
                      {i.name}
                    </Select.Option>
                  );
                })}
              </Select>
            </Form.Item>
          )}

          
            
                  <Form.Item
                    label="queue"
                    name="queue"
                    rules={[{ required: true, message: "必填项" }]}
                  >
                    <Input />
                  </Form.Item>
                。。。因为太长,删了很多
          )}
        </Form>
   

        <Row align="middle">
          <Col span={8}>边框色</Col>
          <Col span={14}>
            <Input
              type="color"
              value={attrs.stroke}
              style={{ width: "100%" }}
              onChange={onStrokeChange}
            />
          </Col>
        </Row>
        <Row align="middle">
          <Col span={8}>边框宽</Col>
          <Col span={12}>
            <Slider
              min={1}
              max={5}
              step={1}
              value={attrs.strokeWidth}
              onChange={onStrokeWidthChange}
            />
          </Col>
          <Col span={2}>
            <div className="result">{attrs.strokeWidth}</div>
          </Col>
        </Row>
        <Row align="middle">
          <Col span={8}>背景色</Col>
          <Col span={14}>
            <Input
              type="color"
              value={attrs.fill}
              style={{ width: "100%" }}
              onChange={onFillChange}
            />
          </Col>
        </Row>
      </TabPane>
      <TabPane tab="文本" key="2">
        <Row align="middle">
          <Col span={8}>尺寸</Col>
          <Col span={12}>
            <Slider
              min={8}
              max={16}
              step={1}
              value={attrs.fontSize}
              onChange={onFontSizeChange}
            />
          </Col>
          <Col span={2}>
            <div className="result">{attrs.fontSize}</div>
          </Col>
        </Row>
        <Row align="middle">
          <Col span={8}>颜色</Col>
          <Col span={14}>
            <Input
              type="color"
              value={attrs.color}
              style={{ width: "100%" }}
              onChange={onColorChange}
            />
          </Col>
        </Row>
      </TabPane>
    </Tabs>
  );
}

模板管理

每步骤要填写的表单,用的是fr-generator编辑和form-render渲染

工单渲染

<Modal
        title="处理工单"
        width="100%">
        //步骤
        <Steps current={activeIndex} progressDot>
          ...
        </Steps>
        // 一些信息
         <Card title="公共信息" style={{ margin: "15px 0" }}>
          <Row>
            <Col span={10} offset={2}>
              <span className="bold-text">标题:</span>
            </Col>
            <Col span={10} offset={2}>
              <span className="bold-text">优先级:</span>
            </Col>
          </Row>
        </Card>
    	// 表单
     <Card title="表单信息" style={{ marginBottom: 15 }}>
                    <FormRender
                      key={index}	

收集的一些流程图绘制插件

projectstorm.gitbooks.io/react-diagr…

antv-g6.gitee.io/zh/examples…

github.com/havardh/wor…

github.com/antvis/X6

github.com/alibaba/but…

ggeditor.com/zh-CN/guide…

segmentfault.com/q/101000002…

ggeditor会比较简单且适合,但是无文档,还好有人统计整合了一套

www.yuque.com/blueju/gg-e…

www.yuque.com/antv/g6/ffz…

其他可用

github.com/alibaba/but…

github.com/antvis/X6

一些字段备注

# 点
表单字段都在 attrs.label 中
cc:抄送邮件
readonlyTpls:只读模板
hideTpls:隐藏模板
writeTpls:可写模板
task:之后任务
sort:顺序
label:标题
size:尺寸,数组
shape:"start-node",类型
clazz:同上 "receiveTask""userTask"
x:坐标
y:坐标

assignType: "person" 指派类型 人员、角色、部门、变量
assignValue: Array(1) 审批人/角色等

起点:任务、只读、隐藏、抄送
结束:任务、隐藏
审核:任务、可写、隐藏、抄送  指派类型、审批人
处理:任务、可写、隐藏、抄送  指派类型、审批人
网关:只要标题、顺序

线:	clazz: "flow"
属性 flowProperties ‘0’-2,是否执行任务 isExecuteTask ,条件表达式 conditionExpression
flowProperties:
value="1">同意	
value="0">拒绝
value="2">其他

会签:isCounterSign
全员处理:fullHandle
主动接单:activeOrder

当选人员时,审核人有两个以上时,会签和主动接单可用,且互斥