feat: 编辑UI
This commit is contained in:
parent
9ab6726e17
commit
25fa8a043a
|
|
@ -384,6 +384,16 @@ def create_app():
|
||||||
res.append(obj)
|
res.append(obj)
|
||||||
return TableResponse(data=res, total=len(res))
|
return TableResponse(data=res, total=len(res))
|
||||||
|
|
||||||
|
@app.get("/api/flows/detail", tags=["Flow"], description="任务详情", response_model=BaseResponse)
|
||||||
|
async def flows_detail(work_uuid: str, user: DB.User = Depends(get_current_user)):
|
||||||
|
work = await DB.Work.filter(uuid=work_uuid).first()
|
||||||
|
if not work:
|
||||||
|
return BaseResponse(success=False, message="not found")
|
||||||
|
work_content = await DB.WorkContent.filter(work_id=work.id).first()
|
||||||
|
res = work.json()
|
||||||
|
res["content"] = work_content.json()
|
||||||
|
return BaseResponse(success=True, data=res)
|
||||||
|
|
||||||
@app.get("/api/flows/status", tags=["Flow"], description="获取任务状态")
|
@app.get("/api/flows/status", tags=["Flow"], description="获取任务状态")
|
||||||
async def flows_status(token: str = ""):
|
async def flows_status(token: str = ""):
|
||||||
# 如果出现长时间链接被中断,可以设置 nginx 的 proxy_read_timeout
|
# 如果出现长时间链接被中断,可以设置 nginx 的 proxy_read_timeout
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@ export const flowStop = async (id, data = {}) => {
|
||||||
return request("/api/stop?work_uuid=" + id, "POST", data)
|
return request("/api/stop?work_uuid=" + id, "POST", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const flowDetail = async (id) => {
|
||||||
|
return request("/api/flows/detail?work_uuid=" + id, "GET")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Card, Col, Row, Typography } from "antd"
|
import { Button, Card, Col, message, Row, Typography } from "antd"
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
|
|
@ -10,8 +10,12 @@ import {
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
addEdge,
|
addEdge,
|
||||||
Position,
|
Position,
|
||||||
|
useReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import { flowDetail } from "../../api/flow";
|
||||||
|
import EditNode from "./components/EditNode";
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
input: "#89A8B2",
|
input: "#89A8B2",
|
||||||
|
|
@ -32,23 +36,18 @@ const initialNodes = [
|
||||||
backgroundColor: COLORS["input"],
|
backgroundColor: COLORS["input"],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
position: { x: 200, y: 0 },
|
|
||||||
data: { label: '2' },
|
|
||||||
sourcePosition: 'right',
|
|
||||||
targetPosition: 'left',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
|
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const FlowDetail = () => {
|
const FlowDetail = () => {
|
||||||
|
const { work_uuid } = useParams()
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const backButton = <Button onClick={() => navigate("/flow")}>返回</Button>
|
const backButton = <Button onClick={() => navigate("/flow")}>返回</Button>
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
|
const [record, setRecord] = useState({});
|
||||||
|
const editRef = useRef(null);
|
||||||
|
const controlRef = useRef(null);
|
||||||
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||||
const onBeforeDelete = async (params) => {
|
const onBeforeDelete = async (params) => {
|
||||||
if (params?.nodes?.find(e => e.type === "input")) {
|
if (params?.nodes?.find(e => e.type === "input")) {
|
||||||
|
|
@ -56,7 +55,82 @@ const FlowDetail = () => {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return <Row gutter={[16, 0]}>
|
const onAdd = (type) => {
|
||||||
|
setNodes([
|
||||||
|
...nodes,
|
||||||
|
{
|
||||||
|
id: new Date().getTime() + '_' + nodes.length,
|
||||||
|
position: { x: nodes.length * 5, y: 0 },
|
||||||
|
data: { label: '2' },
|
||||||
|
sourcePosition: 'right',
|
||||||
|
targetPosition: 'left',
|
||||||
|
style: { backgroundColor: COLORS[type] },
|
||||||
|
type: type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
const onNodeClick = (_, record) => {
|
||||||
|
if (record?.type === 'input') return;
|
||||||
|
editRef?.current?.open(record);
|
||||||
|
}
|
||||||
|
const loadDetail = async () => {
|
||||||
|
try {
|
||||||
|
const res = await flowDetail(work_uuid);
|
||||||
|
const { data: { content: { content: infoList } } } = res;
|
||||||
|
let startX = 0;
|
||||||
|
let startIndex = 1;
|
||||||
|
if (res?.success) {
|
||||||
|
setRecord(res.data);
|
||||||
|
const tempEdges = [];
|
||||||
|
const tempNodes = [{
|
||||||
|
id: '1',
|
||||||
|
position: { x: startX, y: 0 },
|
||||||
|
sourcePosition: 'right',
|
||||||
|
type: 'input',
|
||||||
|
style: { backgroundColor: 'input' },
|
||||||
|
data: { label: '开始' },
|
||||||
|
}]
|
||||||
|
infoList.forEach((e, i) => {
|
||||||
|
tempNodes.push({
|
||||||
|
id: e.uuid,
|
||||||
|
position: { x: startX + 200, y: 0 },
|
||||||
|
sourcePosition: 'right',
|
||||||
|
targetPosition: 'left',
|
||||||
|
type: e.type,
|
||||||
|
style: { backgroundColor: COLORS[e.type] },
|
||||||
|
data: { label: e.title },
|
||||||
|
detail: e
|
||||||
|
})
|
||||||
|
|
||||||
|
const sourceNode = tempNodes[startIndex - 1];
|
||||||
|
const targetNode = tempNodes[startIndex];
|
||||||
|
tempEdges.push({
|
||||||
|
id: `${sourceNode.id}-${targetNode.id}`,
|
||||||
|
source: sourceNode.id,
|
||||||
|
target: targetNode.id
|
||||||
|
})
|
||||||
|
|
||||||
|
startX += 200;
|
||||||
|
startIndex += 1;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
setNodes(tempNodes);
|
||||||
|
setEdges(tempEdges);
|
||||||
|
} else {
|
||||||
|
message.error(res.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadDetail();
|
||||||
|
}, [work_uuid])
|
||||||
|
|
||||||
|
return <ReactFlowProvider>
|
||||||
|
<Row gutter={[16, 0]}>
|
||||||
<Col span={18} >
|
<Col span={18} >
|
||||||
<Card title="工作" extra={backButton}>
|
<Card title="工作" extra={backButton}>
|
||||||
<div style={{ height: 'calc(100vh - 220px)', width: '100%' }}>
|
<div style={{ height: 'calc(100vh - 220px)', width: '100%' }}>
|
||||||
|
|
@ -67,22 +141,25 @@ const FlowDetail = () => {
|
||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
fitView
|
onNodeDoubleClick={onNodeClick}
|
||||||
defaultViewport={{ zoom: 0.1, x: 0, y: 0}}
|
|
||||||
style={{ background: '#f2f3f5' }}
|
style={{ background: '#f2f3f5' }}
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
|
fitView
|
||||||
>
|
>
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background />
|
<Background />
|
||||||
|
<ControlButtons onLoad={obj => (controlRef.current = obj)} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<RightCard />
|
<RightCard onCreate={onAdd} />
|
||||||
|
<EditNode ref={editRef} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
</ReactFlowProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
const RightCard = ({ onCreate }) => {
|
const RightCard = ({ onCreate }) => {
|
||||||
|
|
@ -90,6 +167,7 @@ const RightCard = ({ onCreate}) => {
|
||||||
return <Card title="节点" className="flow-detail-right">
|
return <Card title="节点" className="flow-detail-right">
|
||||||
{nodes?.map((e, i) =>
|
{nodes?.map((e, i) =>
|
||||||
<div
|
<div
|
||||||
|
key={e.title}
|
||||||
className="node"
|
className="node"
|
||||||
style={{ backgroundColor: COLORS[e.title.toLowerCase()] }}
|
style={{ backgroundColor: COLORS[e.title.toLowerCase()] }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -104,4 +182,12 @@ const RightCard = ({ onCreate}) => {
|
||||||
</Card>
|
</Card>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ControlButtons = ({ onLoad }) => {
|
||||||
|
const reactFlow = useReactFlow();
|
||||||
|
useEffect(() => {
|
||||||
|
reactFlow && onLoad(reactFlow)
|
||||||
|
}, [reactFlow])
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
export default FlowDetail;
|
export default FlowDetail;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { DrawerForm, ModalForm, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
|
||||||
|
import { Button, Flex, Space } from "antd";
|
||||||
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const EditNode = forwardRef(({ onFinish }, ref) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const [record, setRecord] = useState(null);
|
||||||
|
const [clients, setClients] = useState([{ label: "Local", value: 'local'}]);
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: (data) => {
|
||||||
|
setOpen(true);
|
||||||
|
setRecord(data);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (record && formRef.current) {
|
||||||
|
formRef.current.setFieldsValue(record.detail);
|
||||||
|
}
|
||||||
|
}, [record, formRef.current]);
|
||||||
|
|
||||||
|
return <DrawerForm
|
||||||
|
title="编辑"
|
||||||
|
trigger={<></>}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={e => setOpen(e)}
|
||||||
|
formRef={formRef}
|
||||||
|
width={600}
|
||||||
|
drawerProps={{ maskClosable: false, destroyOnHidden: true}}
|
||||||
|
>
|
||||||
|
<ProFormText name='uuid' disabled label='UUID' />
|
||||||
|
<Flex align="center">
|
||||||
|
<ProFormSelect width={480} label="目标" rules={[{ required: true }]} name="client" options={clients} />
|
||||||
|
<Button style={{ marginLeft: 6, marginTop: 6}}>添加</Button>
|
||||||
|
</Flex>
|
||||||
|
<ProFormText name='title' rules={[{ required: true}]} label='标题' fieldProps={{ maxLength: 20}} />
|
||||||
|
<ProFormTextArea fieldProps={{ maxLength: 50}} name='description' label='描述' />
|
||||||
|
|
||||||
|
</DrawerForm>
|
||||||
|
})
|
||||||
|
|
||||||
|
export default EditNode;
|
||||||
Loading…
Reference in New Issue