From 78067519c3cf2e5f1309b51bd7e8f107752f8137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=A6=82=E5=A8=81?= Date: Wed, 17 Sep 2025 10:11:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/main.py | 16 +++-- .../src/pages/Client/components/Create.jsx | 24 ++++++- frontend/src/pages/Client/components/Edit.jsx | 28 ++++++++ frontend/src/pages/Client/index.jsx | 27 +++++--- frontend/src/pages/Flow/Detail.jsx | 56 +++++++++++----- .../src/pages/Flow/components/CustomNode.css | 15 +++++ .../src/pages/Flow/components/CustomNode.jsx | 17 +++++ .../src/pages/Flow/components/EditNode.jsx | 64 +++++++++++++------ frontend/src/pages/Flow/index.jsx | 4 -- 9 files changed, 193 insertions(+), 58 deletions(-) create mode 100644 frontend/src/pages/Client/components/Edit.jsx create mode 100644 frontend/src/pages/Flow/components/CustomNode.css create mode 100644 frontend/src/pages/Flow/components/CustomNode.jsx diff --git a/backend/app/main.py b/backend/app/main.py index 66b0c53..c3d9bbe 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -386,6 +386,10 @@ def create_app(): res.append(obj) return TableResponse(data=res, total=len(res)) + @app.put("/api/flows", tags=["Flow"], description="任务修改", response_model=BaseResponse) + async def update_flow(work_uuid: int, body: ClientRequest, user: DB.User = Depends(get_current_user)): + return BaseResponse() + @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() @@ -422,17 +426,17 @@ def create_app(): @app.post("/api/clients", tags=["Client"], description="终端创建", response_model=BaseResponse) async def create_client(body: ClientRequest, user: DB.User = Depends(get_current_user)): - await DB.Client(**body, user_id=user.id).save() + await DB.Client(**body.model_dump(), user_id=user.id).save() return BaseResponse() @app.post("/api/clients", tags=["Client"], description="终端创建", response_model=BaseResponse) async def create_client(body: ClientRequest, user: DB.User = Depends(get_current_user)): - await DB.Client(**body, user_id=user.id).save() + await DB.Client(**body.model_dump, user_id=user.id).save() return BaseResponse() @app.put("/api/clients", tags=["Client"], description="终端修改", response_model=BaseResponse) - async def update_client(client_id:int, body: ClientRequest, user: DB.User = Depends(get_current_user)): - obj = await DB.Client.filter(id=client_id).first() + async def update_client(id:int, body: ClientRequest, user: DB.User = Depends(get_current_user)): + obj = await DB.Client.filter(id=id).first() if obj: obj.update_from_dict(body.model_dump()) obj.update_time = datetime.datetime.now() @@ -440,8 +444,8 @@ def create_app(): return BaseResponse() @app.delete("/api/clients", tags=["Client"], description="终端删除", response_model=BaseResponse) - async def del_client(client_id: int, user: DB.User = Depends(get_current_user)): - await DB.Client.filter(id=client_id).delete() + async def del_client(id: int, user: DB.User = Depends(get_current_user)): + await DB.Client.filter(id=id).delete() return BaseResponse() return app diff --git a/frontend/src/pages/Client/components/Create.jsx b/frontend/src/pages/Client/components/Create.jsx index f76468e..1840186 100644 --- a/frontend/src/pages/Client/components/Create.jsx +++ b/frontend/src/pages/Client/components/Create.jsx @@ -1,11 +1,29 @@ -import { ModalForm, ProForm } from "@ant-design/pro-components"; -import { Button } from "antd"; +import { ModalForm, ProFormDigit, ProFormText } from "@ant-design/pro-components"; +import { Button, message } from "antd"; import { PlusOutlined } from "@ant-design/icons"; +import { create } from "../../../api/client"; + +const required = [{ required: true}] const Create = ({ onFinish }) => { return } type='primary'>创建} - > + onFinish={async params => { + await create(params); + message.success("创建成功"); + onFinish && onFinish(); + return true; + }} + > + + + + + + } export default Create; \ No newline at end of file diff --git a/frontend/src/pages/Client/components/Edit.jsx b/frontend/src/pages/Client/components/Edit.jsx new file mode 100644 index 0000000..b5b5f17 --- /dev/null +++ b/frontend/src/pages/Client/components/Edit.jsx @@ -0,0 +1,28 @@ +import { ModalForm, ProFormDigit, ProFormText } from "@ant-design/pro-components"; +import { Button, message } from "antd"; +import { update } from "../../../api/client"; + +const required = [{ required: true}] + +const Edit = ({ data, onFinish }) => { + return 编辑} + onFinish={async params => { + await update(data.id, params); + message.success("编辑成功"); + onFinish && onFinish(); + return true; + }} + > + + + + + + +} + +export default Edit; \ No newline at end of file diff --git a/frontend/src/pages/Client/index.jsx b/frontend/src/pages/Client/index.jsx index 532d5c1..440fbe3 100644 --- a/frontend/src/pages/Client/index.jsx +++ b/frontend/src/pages/Client/index.jsx @@ -1,9 +1,10 @@ import { ProTable } from "@ant-design/pro-components" -import { Card } from "antd" +import { Button, Card, message, Popconfirm } from "antd" import { useRef } from "react" import { useNavigate } from "react-router-dom" -import { tableList } from "../../api/client" +import { del, tableList } from "../../api/client" import Create from "./components/Create" +import Edit from "./components/Edit" const ClientPage = () => { const navigate = useNavigate(); @@ -12,6 +13,11 @@ const ClientPage = () => { const res = await tableList(); return res; } + const onDelete = async (record) => { + const res = await del(record.id); + message.success("删除成功"); + actionRef?.current?.reload(); + } const columns = [ { title: '#ID', @@ -27,27 +33,28 @@ const ClientPage = () => { }, { title: '创建时间', - dataIndex: "update_time", + dataIndex: "create_time", + render: (_, record) => new Date(record?.create_time * 1000).toLocaleString() }, { title: '操作', valueType: 'option', key: 'option', - with: 100, + width: 100, fixed: 'right', render: (_, record) => [ - , - , + actionRef?.current?.reload()} />, + await onDelete(record)}> + + + ] } ] - return + return actionRef?.current?.reload()} />}> } actionRef={actionRef} rowKey='id' search={false} diff --git a/frontend/src/pages/Flow/Detail.jsx b/frontend/src/pages/Flow/Detail.jsx index 9d53374..478dcca 100644 --- a/frontend/src/pages/Flow/Detail.jsx +++ b/frontend/src/pages/Flow/Detail.jsx @@ -16,28 +16,40 @@ import { import '@xyflow/react/dist/style.css'; import { flowDetail } from "../../api/flow"; import EditNode from "./components/EditNode"; +import CustomNode from "./components/CustomNode"; const COLORS = { - input: "#89A8B2", + first: "#89A8B2", git: "#B3C8CF", cmd: "#E5E1DA", status: "#F1F0E8" } +const POSITION = { + first: ['right'], + git: ['right', 'left'], + cmd: ['right', 'left'], + status: ['left'], +} + const initialNodes = [ { id: '1', position: { x: 0, y: 0 }, - data: { label: '开始' }, + data: { label: '开始', style: { backgroundColor: COLORS['first'] }, position: POSITION['first'] }, sourcePosition: 'right', targetPosition: 'left', - type: 'input', - style: { - backgroundColor: COLORS["input"], - } + type: 'first', }, ]; -const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }]; +const initialEdges = []; + +const nodeTypes = { + first: CustomNode, + git: CustomNode, + cmd: CustomNode, + status: CustomNode, +} const FlowDetail = () => { const { work_uuid } = useParams() @@ -55,22 +67,32 @@ const FlowDetail = () => { } return true; } + const onEdit = ({ id, ...detail }) => { + const temp = [...nodes]; + temp.forEach(e => { + if (e.id == id) { + e.detail = detail; + e.data = { ...e.data, label: detail.title } + } + }) + controlRef?.current?.updateNode(id); + setNodes(temp); + } const onAdd = (type) => { setNodes([ ...nodes, { - id: new Date().getTime() + '_' + nodes.length, + id: 'temp_' + new Date().getTime() + '_' + nodes.length, position: { x: nodes.length * 5, y: 0 }, - data: { label: '2' }, + data: { label: type, style: { backgroundColor: COLORS[type] }, position: POSITION[type] }, sourcePosition: 'right', targetPosition: 'left', - style: { backgroundColor: COLORS[type] }, type: type, }, ]); } const onNodeClick = (_, record) => { - if (record?.type === 'input') return; + if (record?.type === 'first') return; editRef?.current?.open(record); } const loadDetail = async () => { @@ -86,9 +108,8 @@ const FlowDetail = () => { id: '1', position: { x: startX, y: 0 }, sourcePosition: 'right', - type: 'input', - style: { backgroundColor: 'input' }, - data: { label: '开始' }, + type: 'first', + data: { label: '开始', style: { backgroundColor: COLORS['first'] }, position: POSITION['first'] }, }] infoList.forEach((e, i) => { tempNodes.push({ @@ -97,8 +118,7 @@ const FlowDetail = () => { sourcePosition: 'right', targetPosition: 'left', type: e.type, - style: { backgroundColor: COLORS[e.type] }, - data: { label: e.title }, + data: { label: e.title, style: { backgroundColor: COLORS[e.type] }, position: POSITION[e.type] }, detail: e }) @@ -144,6 +164,8 @@ const FlowDetail = () => { onNodeDoubleClick={onNodeClick} style={{ background: '#f2f3f5' }} position={Position.Right} + nodeTypes={nodeTypes} + defaultViewport={{ x: 0, y: 0, zoom: 1 }} fitView > @@ -156,7 +178,7 @@ const FlowDetail = () => { - + diff --git a/frontend/src/pages/Flow/components/CustomNode.css b/frontend/src/pages/Flow/components/CustomNode.css new file mode 100644 index 0000000..4d9eb52 --- /dev/null +++ b/frontend/src/pages/Flow/components/CustomNode.css @@ -0,0 +1,15 @@ +.custom-node { + height: 40.5px; + width: 150px; + border: 1px solid #eee; + padding: 5px; + border-radius: 5px; + background: white; + display: flex; + align-items: center; + justify-content: center; +} + +.custom-node:hover { + border: 1px solid lightgray; +} \ No newline at end of file diff --git a/frontend/src/pages/Flow/components/CustomNode.jsx b/frontend/src/pages/Flow/components/CustomNode.jsx new file mode 100644 index 0000000..7c3a60a --- /dev/null +++ b/frontend/src/pages/Flow/components/CustomNode.jsx @@ -0,0 +1,17 @@ +import React, { memo } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import './CustomNode.css'; + +const CustomNode = memo(({ data: { style, position, ...data}, isConnectable }) => { + return ( +
+
+ {data.label} +
+ {position?.includes('left') && } + {position?.includes('right') && } +
+ ); +}) + +export default CustomNode; diff --git a/frontend/src/pages/Flow/components/EditNode.jsx b/frontend/src/pages/Flow/components/EditNode.jsx index d16a968..5bf544b 100644 --- a/frontend/src/pages/Flow/components/EditNode.jsx +++ b/frontend/src/pages/Flow/components/EditNode.jsx @@ -1,43 +1,71 @@ -import { DrawerForm, ModalForm, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components"; -import { Button, Flex, Space } from "antd"; +import { DrawerForm, ModalForm, ProFormDigit, ProFormGroup, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components"; +import { Button, Divider, Flex, Space, Typography } from "antd"; import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; +import { tableList } from "../../../api/client"; 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'}]); + const [clients, setClients] = useState([{ label: "Local", value: 'local' }]); + const onRequest = async () => { + const res = await tableList(); + return [{label: "[LOCAL]", value: 0},...res?.data?.map(e => ({ label: e.name, value: e.id }))]; + } + const onSubmit = async (params) => { + onFinish && onFinish({ ...params, id: record?.id}) + return true; + } useImperativeHandle(ref, () => ({ open: (data) => { setOpen(true); - setRecord(data); + setTimeout(() => setRecord(data), 100); } })) - useEffect(() => { - if (record && formRef.current) { + if (record && formRef.current && open) { formRef.current.setFieldsValue(record.detail); } - }, [record, formRef.current]); + }, [record, open, formRef.current]); - return } open={open} onOpenChange={e => setOpen(e)} formRef={formRef} - width={600} - drawerProps={{ maskClosable: false, destroyOnHidden: true}} + onFinish={onSubmit} + modalProps={{ maskClosable: false, destroyOnHidden: true }} > - - - - - - - + 基础信息 + + + + + + + {record?.type === 'git' && <> + Git + + + + + + + + + + } + {record?.type === 'cmd' && <> + 命令 + + } + {record?.type === 'status' && <> + 健康监测 + + } - + }) export default EditNode; \ No newline at end of file diff --git a/frontend/src/pages/Flow/index.jsx b/frontend/src/pages/Flow/index.jsx index d9cad9f..13a6bdb 100644 --- a/frontend/src/pages/Flow/index.jsx +++ b/frontend/src/pages/Flow/index.jsx @@ -129,7 +129,6 @@ export default () => { } // 状态检测 useEffect(() => { - if (!pageInit.current) return; if (!user?.token) return; if (eventSourceRef.current) { eventSourceRef.current.close(); @@ -167,9 +166,6 @@ export default () => { }; }, [user]); - useEffect(() => { - pageInit.current = true; - }, []) return (