1. 官方文档
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;