赞
踩
上一篇我们介绍了开源的无代码开发平台Brick, 在无代码开发中比较常见的一个功能是工作流设计器,工作流可以做流程审批设计,数据操作设计等。我们基于antv的x6
封装一个通用的能力,剩下的只需扩充节点类型就可以。
我们看上面的演示功能,主要有三个核心模块。
export const useWorkflowInit = (container: React.RefObject<HTMLDivElement>) => { const currGraphRef = useRef<Graph>(); const update = useUpdate(); useEffect(() => { const graph = new Graph({ container: container.current, autoResize: true, //自动变更画布大小 grid: true, panning: false, //可以拖拽画布 // mousewheel: true, //滚轮缩放画布 background: { }, connecting: {}, onEdgeLabelRendered: (args: any) => { const { selectors, edge } = args; const content = selectors.foContent as HTMLDivElement; if (content) { ReactDOM.render(<EdgeLabel edge={edge} />, content); } }, }); setWorkflowElement(container.current); }, [container.current]); return { graph: currGraphRef.current, }; };
export const Workflow: FC<IWorkflowProps> = ({ style, className, data }) => {
const { graph } = useWorkflowInit(containerRef);
return (
<div style={style} className={classNameStr}>
<div ref={containerRef} className={s.container} id="container"></div>
</div>
);
};
export class WorkflowAppProcessor { self: WorkflowAppProcessor; graphProcessor: GraphProcessor; workflowElement: HTMLElement | null; /** * node 节点相关内容 */ nodeModule: TNodeModuleMap; //工作留数据 workflowData: Observable<IWorkflowEntity>; activeNode: Observable<IWorkflowNodeData | null>; constructor() { this.self = this; this.activeNode = observable(null); this.workflowData = observable({} as IWorkflowEntity); this.workflowElement = null; this.graphProcessor = createGraphProcessor().processor; this.nodeModule = getNodeModule(); } setWorkflowElement = (element: HTMLDivElement) => { this.workflowElement = element; }; /** * 设置workflow数据 * @param data */ setWorkflowData = (data: IWorkflowEntity) => { this.workflowData.set(data); }; /** * 根据类型获取节点的默认数据 * @param nodeType * @param defaultNodeData */ _getDefaultNodeData = (nodeType: TNodeType, defaultNodeData?: Partial<IWorkflowNodeData>) => { }; addNodeData = (nodeType: TNodeType, defaultNodeData?: Partial<IWorkflowNodeData>) => { }; /** * 修改node data * @param nodeData */ updateNodeData = (nodeData: IWorkflowNodeData) => { ... }; .... }
我们看到节点类型会有很多,我们可以按照约定去注册使用节点。
看下面的截图,为每个类型分别创建了三个文件。
entry.ts
入口文件Setting.tsx
节点设置器AddData.tsx ...
节点显示组件entry.ts
入口文件功能export class AddDataNode extends BaseNode { // 获取节点显示组件 static getNodeElement = (): TLazyFunctionComponent => { return React.lazy(() => import('./AddData')); }; // 获取节点设计组件 static getSettingPanel = (): TLazyFunctionComponent => { return React.lazy(() => import('./Setting')); }; // 元数据信息 static getMetadata = (): ISettingPanelMetaData => { return { name: '新增数据', type: ENodeType.AddData, icon: React.createElement(PlusOutlined), }; }; // 默认配置数据 static getDefaultConfigData = () => { return {} as IAddDataNodeConfig; }; }
看到 AddDataNode
继承了BaseNode
,我们可以在BaseNode
去做实现约束。在入口文件中的getNodeElement
和getSettingPanel
方法中使用了React.lazy
,可以帮我们实现异步加载这些组件。
Setting.tsx
节点设置器在Setting中封装一个SettingFormItem
,是对antd的FormItem做了封装,提供设置的变更
const Setting: FC<ISettingComponentProps<ENodeType.AddData>> = (props) => { const { nodeData } = props; return ( <div> <SettingFormItem title={'选择表单'} formItemProps={{ name: ['tableId'], }} > <AppTableCaseCadeSelect /> </SettingFormItem> </div> ); }; export default Setting;
const TableEvent: FC<INodeComponentProps<ENodeType.TableEvent>> = (props) => {
const { nodeData } = props;
const triggerEvent = nodeData?.config?.triggerEvent;
....
return <div>{text}</div>;
};
export default TableEvent;
import * as nodes from '../components/nodes'; export const getNodeModule = () => { const result: TNodeModuleMap = {} as TNodeModuleMap; Object.values(nodes).forEach((item) => { const metaData = item.getMetadata(); const nodeType = metaData.type; const nodeModuleValue: INodeModuleValue = { nodeComponent: item.getNodeElement(), settingComponent: item.getSettingPanel(), metaData, defaultNodeConfigData: item.getDefaultConfigData?.() || {}, }; result[nodeType] = nodeModuleValue; }); return result; };
主要从头全局状态中获取当前选中的node节点,根据节点的node类型去做对应的Setting展示。集成了antd的form组件,从而点确定的时候去更新全局需要存储的信息。
export interface ISettingContainerProps {} export const SettingContainer: FC<ISettingContainerProps> = memo((props) => { const [activeNode, clearActiveNode, nodeModule, updateNodeData, nodeMap] = useWorkflowAppSelector( (s) => [s.activeNode, s.clearActiveNode, s.nodeModule, s.updateNodeData, s.workflowData.nodeMap] ); const nodeId = activeNode?.id!; const [form] = Form.useForm(); useEffect(() => { const values = nodeMap?.[nodeId] || {}; form.setFieldsValue(values); }, [nodeId]); const onClose = useMemoizedFn(() => { clearActiveNode(); }); /** * 更新widget */ const onOk = async () => { try { const values = await form.validateFields(); updateNodeData({ ...values, id: nodeId }); onClose(); } catch (error: any) { const errMessage = error?.errorFields?.[0]?.errors?.[0]; message.error(errMessage); return; } }; const SettingComponent = nodeModule?.[activeNode?.type!]?.settingComponent; const Footer = () => { return ( <div className={s.footer}> <Space> <Button onClick={onClose}>取消</Button> <Button type={'primary'} onClick={onOk}> 确定 </Button> </Space> </div> ); }; return ( <Drawer title={activeNode?.name || '设置'} placement="right" bodyStyle={{ padding: '24px 0', }} width={600} onClose={onClose} open={Boolean(activeNode)} footer={<Footer />} > <Form form={form}> <Suspense fallback={<div>Loading...</div>}> {SettingComponent && <SettingComponent nodeData={activeNode!} />} </Suspense> </Form> </Drawer> ); });
主要也是通过当前激活的节点,去做对应的容器渲染。
export const NodeContainer = ({ node }: { node: Node }) => { const NodeComponent = nodeModule?.[nodeType]?.nodeComponent; return ( <div onClick={onNodeClick} > {!isEnd && ( <div className={s.content}> <div className={s.left}>{NodeComponent && <NodeComponent nodeData={currNode!} />}</div> </div> )} </div> ); };
通过以上的设置,后续我们扩充节点只需要添加这三个文件就可以了。
画布内的节点,连线等操作,我们封装一个通用的类来管理。
export class GraphProcessor extends BaseProcessor { // graph实例,不是 graph: Graph | null; constructor() { super(); this.graph = null; this.init(); } private init = async () => { this.listeners(); }; /** * 设置graph实例 * @param graph */ setGraph = (graph: Graph) => { this.graph = graph; // @ts-ignore window._graph = graph; }; /** * 添加node节点 * @param nodeType * @param data */ addNode = (nodeType: TNodeType, data: Node.Metadata) => { data.data = { ...data.data, type: nodeType, }; if (!data.id) { data.id = uuid(); } return this.graph?.addNode({ ...DEFAULT_NODE_ATTR, ...data }); }; addEdge = (source: string, target: string) => { return this.graph?.addEdge({ source, target, attrs: { line: { stroke: '#8f8f8f', strokeWidth: 1, }, }, defaultLabel: { markup: Markup.getForeignObjectMarkup(), attrs: { fo: { width: 18, height: 18, x: -9, y: 0, // y: -(NODE_GAP / 2), }, }, }, label: { // attrs: { // text: { // text: "s1" // } // }, position: { distance: -35, // distance: -(NODE_GAP / 2) }, }, router: { name: 'orth', args: { padding: { bottom: 10, }, }, }, }); }; addNodeByEdge = ({ nodeType, data, edge, isRedraw = true, }: { nodeType: TNodeType; data?: Node.Metadata; edge: Edge; isRedraw?: boolean; }) => { const sourceId = edge.getSourceCellId(); const targetId = edge.getTargetCellId(); this.addNode(nodeType, data!); // 当前线删除 edge.remove(); this.addEdge(sourceId, data!.id!); this.addEdge(data!.id!, targetId); if (isRedraw) { this.redraw(); } }; /** * 重新绘制实图 */ redraw = () => { const nodes = this.graph?.getNodes(); const edges = this.graph?.getEdges(); const graphArea = this.graph?.getGraphArea(); // 画布宽度 const graphWidth = graphArea?.width || 0; // 画布高度 const graphHeight = graphArea?.height || 0; const connections = edges?.map((f) => ({ sourceId: f.getSourceCellId(), targetId: f.getTargetCellId(), })) || []; // 1. 通过edges找出层级关系 const treeLevelData = convertToLevelTree(connections); treeLevelData.forEach((currLevelData, level) => { // 当前级别数量 const currLevenLength = currLevelData.length; // 当前级别node总宽度 const nodeSumWidth = NODE_WIDTH * currLevenLength + NODE_GAP * (currLevenLength - 1); const currBeginX = (graphWidth - nodeSumWidth) / 2; currLevelData.forEach((nodeId, index) => { const currNode = this.graph?.getCellById(nodeId) as Node; if (currNode) { // const { width, height } = currNode.getSize();; currNode?.setPosition({ x: currBeginX + (NODE_WIDTH + NODE_GAP) * index, y: NODE_GAP + (NODE_HEIGHT + NODE_GAP) * level, }); } }); }); // 2. 根据层级关系重新渲染 }; /** * 删除node节点 * @param node */ removeNode = (node: Node) => { this.graph?.removeNode(node); }; /** * 获取workflow数据 */ getData = () => { return this.graph?.toJSON(); }; }
其中的一些细节就不做详细介绍了,有需要的可以直接看代码。
建立了一个微信交流群,请添加微信号brickmaster1
,备注brick
,我会拉你进群
整个工作流的代码在 b-workflow,有需要的,可以直接去查看
大家觉得有帮助,请在github帮忙star一下。
如果你觉得该文章不错,不妨
1、点赞,让更多的人也能看到这篇内容
2、关注我,让我们成为长期关系
3、关注公众号「前端有话说」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。