feat: 编辑UI

This commit is contained in:
李如威 2025-09-16 11:34:09 +08:00
parent 9ab6726e17
commit 25fa8a043a
6 changed files with 188 additions and 42 deletions

View File

@ -384,6 +384,16 @@ def create_app():
res.append(obj)
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="获取任务状态")
async def flows_status(token: str = ""):
# 如果出现长时间链接被中断,可以设置 nginx 的 proxy_read_timeout

View File

@ -12,6 +12,12 @@ export const flowStop = async (id, data = {}) => {
return request("/api/stop?work_uuid=" + id, "POST", data)
}
export const flowDetail = async (id) => {
return request("/api/flows/detail?work_uuid=" + id, "GET")
}

View File

@ -5,4 +5,5 @@
padding: 16px 8px;
box-sizing: border-box;
text-align: center;
}
}

View File

@ -1,6 +1,6 @@
import { Button, Card, Col, Row, Typography } from "antd"
import { useNavigate } from "react-router-dom";
import { useCallback } from 'react';
import { Button, Card, Col, message, Row, Typography } from "antd"
import { useNavigate, useParams } from "react-router-dom";
import { useCallback, useEffect, useRef, useState } from 'react';
import {
ReactFlow,
MiniMap,
@ -10,8 +10,12 @@ import {
useEdgesState,
addEdge,
Position,
useReactFlow,
ReactFlowProvider,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { flowDetail } from "../../api/flow";
import EditNode from "./components/EditNode";
const COLORS = {
input: "#89A8B2",
@ -32,23 +36,18 @@ const initialNodes = [
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 FlowDetail = () => {
const { work_uuid } = useParams()
const navigate = useNavigate();
const backButton = <Button onClick={() => navigate("/flow")}>返回</Button>
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
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 onBeforeDelete = async (params) => {
if (params?.nodes?.find(e => e.type === "input")) {
@ -56,40 +55,119 @@ const FlowDetail = () => {
}
return true;
}
return <Row gutter={[16, 0]}>
<Col span={18} >
<Card title="工作" extra={backButton}>
<div style={{ height: 'calc(100vh - 220px)', width: '100%' }}>
<ReactFlow
nodes={nodes}
onBeforeDelete={onBeforeDelete}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
defaultViewport={{ zoom: 0.1, x: 0, y: 0}}
style={{ background: '#f2f3f5' }}
position={Position.Right}
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
</div>
</Card>
</Col>
<Col span={6}>
<RightCard />
</Col>
</Row>
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} >
<Card title="工作" extra={backButton}>
<div style={{ height: 'calc(100vh - 220px)', width: '100%' }}>
<ReactFlow
nodes={nodes}
onBeforeDelete={onBeforeDelete}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDoubleClick={onNodeClick}
style={{ background: '#f2f3f5' }}
position={Position.Right}
fitView
>
<MiniMap />
<Controls />
<Background />
<ControlButtons onLoad={obj => (controlRef.current = obj)} />
</ReactFlow>
</div>
</Card>
</Col>
<Col span={6}>
<RightCard onCreate={onAdd} />
<EditNode ref={editRef} />
</Col>
</Row>
</ReactFlowProvider>
}
const RightCard = ({ onCreate}) => {
const nodes = [{ title: 'Git' }, {title: "Cmd"}, {title: "Status"}];
const RightCard = ({ onCreate }) => {
const nodes = [{ title: 'Git' }, { title: "Cmd" }, { title: "Status" }];
return <Card title="节点" className="flow-detail-right">
{nodes?.map((e, i) =>
<div
key={e.title}
className="node"
style={{ backgroundColor: COLORS[e.title.toLowerCase()] }}
onClick={() => {
@ -104,4 +182,12 @@ const RightCard = ({ onCreate}) => {
</Card>
}
const ControlButtons = ({ onLoad }) => {
const reactFlow = useReactFlow();
useEffect(() => {
reactFlow && onLoad(reactFlow)
}, [reactFlow])
return <></>
}
export default FlowDetail;

View File

@ -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;