react-flow实现dag工作流

1. 官方文档

Introduction to React Flow

2.效果

3. 代码

index.jsx

import { useState, useCallback, useEffect } from 'react';
import ReactFlow, {
	Controls,
	Background,
	applyNodeChanges,
	applyEdgeChanges,
	addEdge,
	ReactFlowProvider,
	useReactFlow
} from 'reactflow';
import 'reactflow/dist/style.css';
import { TaskNode } from './taskNode';
import { Button, Space, message } from 'antd';
import { saveMonthTaskInfo, getMonthTaskInfo } from '@/api/modules/month_task_dag';
import AddNode from './addNodeForm';
import { store } from "@/redux";

// const initialNodes = [
// 	{
// 		id: 'qb_value_cal',
// 		data: { label: 'XXXX', task_status: 'success' },
// 		position: { x: 196.99037808813034, y: -16.15861389212995 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'platform_acct_price',
// 		data: { label: 'xxxx', task_status: 'failure' },
// 		position: { x: 9.004772768299915, y: 71.58667201646344 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 't_syn_settlement',
// 		data: { label: 'xxxx', task_status: 'failure' },
// 		position: { x: 198.74746953729812, y: 150.0741352155024 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'midas_sett',
// 		data: { label: 'xxxx', task_status: 'running' },
// 		position: { x: -145.96731433103253, y: -13.672818469718123 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'midas_sett_consume',
// 		data: { label: 'xxxx', task_status: 'failure' },
// 		position: { x: -123.70028149684317, y: 184.60354995136538 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'dyb_value_cal',
// 		data: { label: 'xxxx', task_status: 'running' },
// 		position: { x: 16.548987140345645, y: 268.97166003745264 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'prepay_dyb',
// 		data: { label: 'xxxxx', task_status: 'failure' },
// 		position: { x: 114.87245269459243, y: 393.62735077521256 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'prepay_consume',
// 		data: { label: 'xxxxx', task_status: 'failure' },
// 		position: { x: 114.87245269459243, y: 393.62735077521256 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'midas_dyb_price',
// 		data: { label: 'xxxxxxx', task_status: 'failure' },
// 		position: { x: 114.87245269459243, y: 393.62735077521256 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'midas_split',
// 		data: { label: 'xxxxxx', task_status: 'running' },
// 		position: { x: -107.1137248521369, y: 391.20277175226386 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'qb_value_adjust',
// 		data: { label: 'xxxxxxx', task_status: 'failure' },
// 		position: { x: 14.93930727104339, y: 499.4122832380474 },
// 		type: 'taskNode'
// 	},
// 	{
// 		id: 'midas_bill',
// 		data: { label: 'xxxxxxx', task_status: 'failure' },
// 		position: { x: 14.93930727104339, y: 499.4122832380474 },
// 		type: 'taskNode'
// 	}
// ];
// const initialEdges = [
// 	{
// 		id: "reactflow__edge-dyb_value_calb-p", 
// 		source: "dyb_value_cal", 
// 		sourcehAndle: "b", 
// 		target: "prepay_dyb", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-midas_dyb_priceb", 
// 		source: "midas_dyb_price", 
// 		sourcehAndle: "b", 
// 		target: "midas_split", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-midas_settb-plat", 
// 		source: "midas_sett", 
// 		sourcehAndle: "b", 
// 		target: "platform_acct_price", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-midas_sett_consu", 
// 		source: "midas_sett_consume", 
// 		sourcehAndle: "b", 
// 		target: "midas_dyb_price", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-platform_acct_pr", 
// 		source: "platform_acct_price", 
// 		sourcehAndle: "b", 
// 		target: "dyb_value_cal", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-platform_acct_priceb-midas_sett_consumea", 
// 		source: "platform_acct_price", 
// 		sourcehAndle: "b", 
// 		target: "midas_sett_consume", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-prepay_dybb-prep", 
// 		source: "prepay_dyb", 
// 		sourcehAndle: "b", 
// 		target: "prepay_consume", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-qb_value_calb-qb", 
// 		source: "qb_value_cal", 
// 		sourcehAndle: "b", 
// 		target: "qb_value_adjust", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-qb_value_calb-t_", 
// 		source: "qb_value_cal", 
// 		sourcehAndle: "b", 
// 		target: "t_syn_settlement", 
// 		targethAndle: "a"
// 	}, 
// 	{
// 		id: "reactflow__edge-t_syn_settlement", 
// 		source: "t_syn_settlement", 
// 		sourcehAndle: "b", 
// 		target: "dyb_value_cal", 
// 		targethAndle: "a"
// 	}
// ];
const nodeTypes = { taskNode: TaskNode };
const username = store.getState().global.userName
function Flow() {
	const [nodes, setNodes] = useState([]);
	const [edges, setEdges] = useState([]);
	const handleGetNodesInfo  = async () => {
		const {data} = await getMonthTaskInfo({"module_view":"toc_revenue"})
		console.log(data);
		setNodes(data["initialNodes"]);
		setEdges(data["initialEdges"]);
	};
	// 初始化是获取节点和边信息并渲染
	useEffect(() => {
		handleGetNodesInfo();
	}, []);
	// 获取flow实例
	const reactFlowInstance = useReactFlow();
	// 保存节点和边信息
  const handleSaveNodes  = async () => {
		console.log(reactFlowInstance.getNodes())
		console.log(reactFlowInstance.getEdges())
		const {data} = await saveMonthTaskInfo({"initialNodes":reactFlowInstance.getNodes(), "initialEdges":reactFlowInstance.getEdges(), "module_view":"toc_revenue"})
		console.log(data)
		if (data=="success"){
			message.success(data);
		}else{
			message.error(data);
		}
  };
	// 添加节点
	const handleAddNode = (newNode) => {
		// setNodes([...nodes, newNode]); // 受控组件的添加方式
		reactFlowInstance.addNodes(newNode) // 非受控组件的添加方式
	};
	const onNodesChange = useCallback(  
	// 这段代码使用了React的useCallback hook来创建一个函数onNodesChange,
	// 其中用到了applyNodeChanges函数和setNodes函数。onNodesChange函数接收一个名为changes的参数,
	// 这个参数表示节点的变化。applyNodeChanges函数根据这个变化来更新节点信息,并返回新的节点信息。
	// setNodes函数则用来更新组件的状态,将新的节点信息更新到组件中。
	// 最后,上述代码中的useCallback hook的第二个参数是一个依赖数组,表示只有当setNodes函数发生变化时,
	// 才需要重新创建onNodesChange函数。这样可以避免不必要的重复渲染。
	// applyNodeChanges
	// 描述:返回具有应用更改的节点数组
	// 类型:(changes: NodeChange[], nodes: Node[]) => Node[]
		(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
		[]
	);
	const onEdgesChange = useCallback((changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),[]);

	const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);
	return (
		<>
			<Space direction="vertical">
				<Space wrap>
					<AddNode username={username} handleAddNode={handleAddNode}></AddNode>
					<Button size="large" onClick={handleSaveNodes} >保存</Button>
				</Space>
			</Space>
			<div style={{ height: '100%' }}>
				<ReactFlow
					nodes={nodes}
					onNodesChange={onNodesChange}
					edges={edges}
					onEdgesChange={onEdgesChange}
					onConnect={onConnect}
					nodeTypes={nodeTypes}
				>
					<Background />
					<Controls />
				</ReactFlow>
			</div>
		</>
	);
}
export default function () {
  return (
    <ReactFlowProvider>
      <Flow />
    </ReactFlowProvider>
  );
}

taskNode.jsx

/* eslint-disable react/prop-types */
import React, { useState } from 'react'
import { Button, Modal, Form, Input, Popover, Table, message } from 'antd'
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined, PlayCircleOutlined } from '@ant-design/icons';
import { getMonthTaskInfo, getHistoryTaskInfo, runMonthTask } from '@/api/modules/month_task_dag';
import { store } from "@/redux";
import { getFifteenAgoData } from '@/utils/date_utils';
import { Handle, Position, useReactFlow } from 'reactflow';
const username = store.getState().global.userName

const columns = [
	{
		title: '任务ID',
		dataIndex: 'task_type',
		key: 'task_type',
	},
	{
		title: '任务名称',
		dataIndex: 'task_type_name',
		key: 'task_type_name',
	},
	{
		title: '任务状态',
		dataIndex: 'task_status',
		key: 'task_status',
	},
	{
		title: '运行者',
		dataIndex: 'user',
		key: 'user',
	},
	{
		title: '数据月份',
		dataIndex: 'data_month',
		key: 'data_month',
	},
	{
		title: '创建时间',
		dataIndex: 'create_time',
		key: 'create_time',
	},
	{
		title: '完成时间',
		dataIndex: 'finish_time',
		key: 'finish_time',
	},
	{
		title: '修改时间',
		dataIndex: 'modify_time',
		key: 'modify_time',
	}
];

// const historydata = [
// 	{
// 		task_id: '任务ID',
// 		task_name: '任务名称',
// 		task_status: '任务状态',
// 		user: '运行者',
// 		create_time:'创建时间',
// 		finish_time:'完成时间',
// 		modify_time:'修改时间'
// 	}
// ];
// eslint-disable-next-line react/prop-types
export function TaskNode({ data, isConnectable }) {
	const [isModalVisible, setIsModalVisible] = useState(false);  // 表单
	const [isPopoverVisible, setPopoverVisible] = useState(false);  //菜单
	const [isTableVisible, setTableVisible] = useState(false);  // 表格
	const [historyRunData, setHistoryRunData] = useState(false);  // 表格

	const reactFlowInstance = useReactFlow();

	const handleVisibleChange = (isPopoverVisible) => {
		setPopoverVisible(isPopoverVisible);
	};
  const getHistoryInfo  = async () => {
		console.log(data)
		const {data: historydata} = await getHistoryTaskInfo({"task_type":data.task_type})
		setHistoryRunData(historydata)
  };
	const handleMenuClick = () => {
		getHistoryInfo();
		setPopoverVisible(false);
		setTableVisible(true);
	};
	const menu = (
		<a key="record" onClick={handleMenuClick}>
			运行记录
		</a>
	);

	const fifteenAgo = getFifteenAgoData()

	const handleFormSubmit = (values) => {
		const runTask  = async () => {
			const {data: runResult} = await runMonthTask({"task_type":data.task_type,"task_type_name":data.task_type_name, "data_month":values.data_month, "username":values.username})
			console.log(runResult);
			message.success(runResult);
		};
		// 提交任务后刷新任务状态
		console.log(values, data.task_type, data.task_type_name);
		runTask();
		setIsModalVisible(false);
		const handleGetNodesInfo  = async () => {
			const {data} = await getMonthTaskInfo({"module_view":"toc_revenue"})
			console.log(data);
		};
		handleGetNodesInfo();
		reactFlowInstance.addNodes[data["initialNodes"]];
	};
	const handleClick = () => {
    setIsModalVisible(true);
  };

	const getStatusIcon = () => {
    switch (data.task_status) {
			case 'no_run':
				return <PlayCircleOutlined />
      case 'success':
        return <CheckCircleOutlined />;
      case 'failed':
        return <CloseCircleOutlined />;
      case 'running':
        return <LoadingOutlined />;
      default:
        return null;
    }
  };
	return (
		<>
			<Popover
				content={menu}  // 弹出一个菜单项
				trigger="hover"
				visible={isPopoverVisible}
				onVisibleChange={handleVisibleChange}
				placement="rightTop"
				title="编辑选项"
			>
				<div style={{ display: 'flex', alignItems: 'center', padding: '10px', borderRadius: '10px', backgroundColor: '#f5f5f5' }} 
				onClick={handleClick}
				>
					<div style={{ marginRight: '10px', borderRadius: '50%', overflow: 'hidden' }}>
						{getStatusIcon(data.task_status)}
					</div>
					<div>{data.task_type_name}</div>
					<Handle type="target" position={Position.Top} id="a" isConnectable={isConnectable} />
					<Handle type="source" position={Position.Bottom} id="b" isConnectable={isConnectable} />
				</div>
			</Popover>
			{/* ===========================================运行记录=========================================== */}
			<Modal
				title="运行记录"
				visible={isTableVisible}
				onCancel={() => setTableVisible(false)}
				footer={null}
				width={'100vh'} // 设置Modal的宽度
			>
				<div style={{ overflowY: 'auto', maxHeight: '70vh' }}> {/* 设置带有垂直滚动条的容器 */}
					<Table columns={columns} dataSource={historyRunData} rowKey="create_time" pagination={{ pageSize: 5 }}/>
				</div>
			</Modal>
			{/* ===========================================表单=========================================== */}
			<Modal
				title={data.task_type_name}
				visible={isModalVisible}
				onCancel={() => setIsModalVisible(false)}
				footer={null}
			>
				<Form onFinish={handleFormSubmit} initialValues={{ data_month:fifteenAgo.substring(0,7), username: username}}>
					<Form.Item
						label="数据月份"
						name="data_month"
						rules={[{ required: true, message: 'YYYY-MM' }]}
					>
						<Input />
					</Form.Item>
					<Form.Item
						label="执行人"
						name="username"
						rules={[{ required: true }]}
					>
						<Input disabled/>
					</Form.Item>
					<Form.Item>
						<Button htmlType="submit">运行</Button>
					</Form.Item>
				</Form>
			</Modal>
		</>
	);
}

addNodeForm.jsx

import { PlusOutlined } from '@ant-design/icons';
import {
  ModalForm,
  ProFormText
} from '@ant-design/pro-components';
import { Button, message } from 'antd';
const AddNodeForm = (props:any) => {
	const initialValues = {
    mail_to: ''
  };
  return (
    <ModalForm<{
      task_type: string;
      task_type_name: string;
			system_module: string;
			settlement_mode: string;
			mail_to: string;
			creater: string;
    }
		>
      title="新增节点"
			initialValues={initialValues}
      trigger={
        <Button type="primary" size="large">
          <PlusOutlined />
          新增节点
        </Button>
      }
      autoFocusFirstInput
      modalProps={{
        onCancel: () => message.info("取消成功"),
      }}
      submitTimeout={2000}
      onFinish={
				async (values) => {
					const newNode = 	{
						id: values.task_type,
						data: { task_type:values.task_type, task_type_name: values.task_type_name, 
							system_module:values.system_module, settlement_mode:values.settlement_mode, 
							mail_to: values.mail_to, creater:values.creater,
							task_status: 'no_run'},
						position: { x: Math.random() * 500, y: Math.random() * 500 },
						type: 'taskNode'
					}
					props.handleAddNode(newNode)
					message.success('提交成功');
					return true;
      }}
    > 
			<ProFormText width="md" name="task_type" label="任务ID" placeholder="任务用于唯一标识的英文名" />
			<ProFormText width="md" name="task_type_name" label="任务描述" placeholder="用于节点名称显示" />
			<ProFormText width="md" name="system_module" label="归属的系统模块" placeholder="归属的系统模块" />
			<ProFormText width="md" name="settlement_mode" label="归属的结算模式" placeholder="mobile/non_mobile/operator等" />
			<ProFormText width="md" name="mail_to" label="邮件人" placeholder="邮件人" />
			<ProFormText width="md" name="creater" label="创建人" disabled initialValue={props.username} />
    </ModalForm>
  );
};

export default AddNodeForm;

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值