记一次react项目里的流程和工单管理。
流程图的绘制用的是@antv/x6
工单模板用的是 fr-generator和form-render
每步流程可以选择要填写的模板,选择允许输入的人,只有允许的用户才可以点击下一步
我记得是参考一个vue项目做的,但是项目地址忘了哎
写得不是很好,见谅,这算是半年前写的东西了,好多忘了,没什么精力去缩减
- 流程
- 工单
流程图布局样式
# 布局
<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>);
}
# ConfigNode 点
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, 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…
ggeditor会比较简单且适合,但是无文档,还好有人统计整合了一套
其他可用
一些字段备注
# 点
表单字段都在 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
当选人员时,审核人有两个以上时,会签和主动接单可用,且互斥