feat: 创建节点

This commit is contained in:
李如威 2025-09-15 18:00:02 +08:00
parent f42a2583c1
commit 9ab6726e17
7 changed files with 328 additions and 35 deletions

View File

@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@ant-design/pro-components": "^2.8.10",
"@xyflow/react": "^12.8.4",
"antd": "^5.27.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@ -1909,6 +1910,49 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
},
"node_modules/@types/d3-drag": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-selection": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="
},
"node_modules/@types/d3-transition": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
"dependencies": {
"@types/d3-selection": "*"
}
},
"node_modules/@types/d3-zoom": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
"dependencies": {
"@types/d3-interpolate": "*",
"@types/d3-selection": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1925,7 +1969,7 @@
"version": "19.1.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz",
"integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -1974,6 +2018,36 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@xyflow/react": {
"version": "12.8.4",
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.4.tgz",
"integrity": "sha512-bqUu4T5QSHiCFPkoH+b+LROKwQJdLvcjhGbNW9c1dLafCBRjmH1IYz0zPE+lRDXCtQ9kRyFxz3tG19+8VORJ1w==",
"dependencies": {
"@xyflow/system": "0.0.68",
"classcat": "^5.0.3",
"zustand": "^4.4.0"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
}
},
"node_modules/@xyflow/system": {
"version": "0.0.68",
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.68.tgz",
"integrity": "sha512-QDG2wxIG4qX+uF8yzm1ULVZrcXX3MxPBoxv7O52FWsX87qIImOqifUhfa/TwsvLdzn7ic2DDBH1uI8TKbdNTYA==",
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@ -2199,6 +2273,11 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@ -2277,6 +2356,102 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/dayjs": {
"version": "1.11.14",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.14.tgz",
@ -4212,6 +4387,33 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.5.7",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
"dependencies": {
"use-sync-external-store": "^1.2.2"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
}
}
}

View File

@ -11,6 +11,7 @@
},
"dependencies": {
"@ant-design/pro-components": "^2.8.10",
"@xyflow/react": "^12.8.4",
"antd": "^5.27.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",

View File

@ -0,0 +1,8 @@
.flow-detail-right .node {
cursor: pointer;
border-radius: 8px;
margin-bottom: 8px;
padding: 16px 8px;
box-sizing: border-box;
text-align: center;
}

View File

@ -0,0 +1,107 @@
import { Button, Card, Col, Row, Typography } from "antd"
import { useNavigate } from "react-router-dom";
import { useCallback } from 'react';
import {
ReactFlow,
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Position,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
const COLORS = {
input: "#89A8B2",
git: "#B3C8CF",
cmd: "#E5E1DA",
status: "#F1F0E8"
}
const initialNodes = [
{
id: '1',
position: { x: 0, y: 0 },
data: { label: '开始' },
sourcePosition: 'right',
targetPosition: 'left',
type: 'input',
style: {
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 navigate = useNavigate();
const backButton = <Button onClick={() => navigate("/flow")}>返回</Button>
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const onBeforeDelete = async (params) => {
if (params?.nodes?.find(e => e.type === "input")) {
return false;
}
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 RightCard = ({ onCreate}) => {
const nodes = [{ title: 'Git' }, {title: "Cmd"}, {title: "Status"}];
return <Card title="节点" className="flow-detail-right">
{nodes?.map((e, i) =>
<div
className="node"
style={{ backgroundColor: COLORS[e.title.toLowerCase()] }}
onClick={() => {
onCreate && onCreate(e.title.toLowerCase());
}}
>
<Typography.Text>
{e.title}
</Typography.Text>
</div>
)}
</Card>
}
export default FlowDetail;

View File

@ -4,11 +4,14 @@ import { useAuth } from "../../context/AuthContext";
import { LoadingOutlined } from "@ant-design/icons";
import { useEffect, useRef, useState } from "react";
import { tableList, flowRun, flowStop } from "../../api/flow";
import { useNavigate } from "react-router-dom";
import './Detail.css';
export default () => {
const { user, logout } = useAuth();
const [status, setStatus] = useState({});
const eventSourceRef = useRef(null);
const navigate = useNavigate();
const needReload = useRef(false);
const pageInit = useRef(false);
const actionRef = useRef(null);
@ -80,7 +83,7 @@ export default () => {
dataIndex: 'last_status',
title: '最后执行结果',
valueType: 'select',
fieldProps: {
fieldProps: {
options: [
{ value: 'RUNNING', "label": "执行中" },
{ value: 'SUCCESS', "label": "成功" },
@ -106,7 +109,9 @@ export default () => {
<Button type="link" danger style={{ padding: '0 0', border: 'none' }}>暂停</Button>
</Popconfirm>
),
user?.level === 0 && <Button key='2' type="link" style={{ padding: '0 0', border: 'none' }}>编辑</Button>,
user?.level === 0 && <Button key='2' type="link" style={{ padding: '0 0', border: 'none' }} onClick={() => {
navigate("/flow/detail/" + record.uuid);
}}>编辑</Button>,
user?.level === 0 && <Button key='3' type="link" danger style={{ padding: '0 0', border: 'none' }}>删除</Button>,
]
}
@ -179,36 +184,4 @@ export default () => {
</Card>
)
}
const RunningStatus = ({record, status}) => {
const [open, setOpen] = useState(false);
const current = record?.content?.findIndex(e => status?.[e.uuid]);
console.log(status, current);
return <>
<a onClick={() => setOpen(true)}>
<Space>
<LoadingOutlined />
执行中
</Space>
</a>
<Modal
title={record?.title || `#${record?.id} 执行状态`}
closable={false}
maskClosable={false}
open={open}
onCancel={() => setOpen(false)}
footer={[<Button key="cancel" onClick={() => setOpen(false)}>关闭</Button>]}
>
<Steps
progressDot
current={current}
direction="vertical"
items={record?.content}
/>
</Modal>
</>
}

View File

@ -6,6 +6,7 @@ import Login from "./pages/Login";
import User from "./pages/User";
import Profile from "./pages/User/Profile";
import Flow from "./pages/Flow";
import FlowDetail from "./pages/Flow/Detail";
import LayoutPage from "./pages/Layout";
@ -28,6 +29,7 @@ const routes = [
element: <LayoutPage />,
children: [
{ path: "/flow", element: <Flow /> },
{ path: "/flow/detail/:work_uuid", element: <FlowDetail /> },
{ path: "/user", element: <User /> },
{ path: "/user/detail", element: <Profile /> }
]