From 6a45efffe992f305e9e4ccf4dd0fb48f53899e61 Mon Sep 17 00:00:00 2001 From: limil Date: Fri, 30 Jan 2026 12:13:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=A4=E4=BA=92=E5=92=8C?= =?UTF-8?q?=E5=A4=96=E8=A7=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.css | 18 ++++-- src/App.tsx | 100 +++++++++++++++++++++------------- src/components/CustomNode.tsx | 2 +- src/components/Toggle.tsx | 34 +++++++----- src/types/graph.ts | 6 +- 5 files changed, 102 insertions(+), 58 deletions(-) diff --git a/src/App.css b/src/App.css index a2020d9..3cab1cd 100644 --- a/src/App.css +++ b/src/App.css @@ -1,8 +1,10 @@ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; + max-width: none; /* 必须取消最大宽度限制 */ + text-align: left; /* 取消居中对齐,否则 toolbar 里的文字可能会乱 */ } .logo { @@ -53,6 +55,7 @@ .toolbar-group { display: flex; + flex-direction: column; gap: 8px; background: white; padding: 10px; @@ -82,3 +85,10 @@ transform: scale(0.95); } +.toolbar-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: #333; +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 8de9e2e..f048db5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,9 +9,12 @@ import { ReactFlowProvider, NodeTypes, NodeChange, - OnEdgesChange, + EdgeChange, MarkerType, + NodeMouseHandler, OnConnect, + MiniMap, + IsValidConnection } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { AppNode, AppEdge, NodeData } from './types/graph'; @@ -21,23 +24,9 @@ import ConfigViewer from './components/ConfigViewer'; import Toggle from "./components/Toggle" import './App.css'; -const initialNodes: AppNode[] = [ - { - id: 'n1', - position: { x: 250, y: 50 }, - data: { - label: 'Node-A', - ipAddress: '10.0.0.1', - listenPort: '51820', - privateKey: '', - publicKey: '', - }, - type: 'custom', - }, -]; +const initialNodes: AppNode[] = []; const initialEdges: AppEdge[] = []; - const nodeTypes : NodeTypes = { custom: CustomNode, }; @@ -47,31 +36,55 @@ function FlowContent(): ReactNode { const [edges, setEdges] = useState(initialEdges); const [editingNode, setEditingNode] = useState(null); const [showConfigViewer, setShowConfigViewer] = useState(false); + const [enableTwoWay, setEnableTwoWay] = useState(false); const onNodesChange = useCallback( (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)), - [], - ); + []); - const onEdgesChange = useCallback( - (changes) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)), - [], - ); + const onEdgesChange = useCallback( + (changes : EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)), + []); const onConnect = useCallback( - (params) => setEdges((edgesSnapshot) => addEdge(params, edgesSnapshot)), - [], - ); + (params) => { + const newEdge : AppEdge = { + ...params, + id: `e-${Date.now()}`, + animated: !enableTwoWay, + markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed }, + data : { + isTwoWayEdge: enableTwoWay + } + } + return setEdges((eds) => addEdge(newEdge, eds)); + }, + [enableTwoWay]); - const onNodeClick = useCallback((_event: React.MouseEvent, node: AppNode) => { - setEditingNode(node.data); - }, []); + const onNodeClick = useCallback>( + (_event, node) => setEditingNode(node.data), + []); + + const validateConnection = useCallback( + (connection) => { + if (connection.source === connection.target) { + return false; + } + const isDuplicate = edges.some( + (edge) => ( + (edge.source === connection.source && edge.target === connection.target) || + (edge.source === connection.target && edge.target === connection.source) + ) + ); + return !isDuplicate; + }, + [edges]); const handleAddNode = (): void => { const newNodeId = `n${Date.now()}`; const newNode: AppNode = { id: newNodeId, - position: { x: Math.random() * 500, y: Math.random() * 500 }, + position: { x: 0, y: 0 }, data: { label: `Node-${String.fromCharCode(65 + (nodes.length % 26))}`, ipAddress: `10.0.0.${nodes.length + 2}`, @@ -80,8 +93,9 @@ function FlowContent(): ReactNode { publicKey: '', }, type: 'custom', + selected: true }; - setNodes((prev) => [...prev, newNode]); + setNodes((prev) => [...prev.map(node => ({ ...node, selected: false })), newNode]); }; const handleUpdateNode = (updatedData: NodeData): void => { @@ -107,28 +121,36 @@ function FlowContent(): ReactNode { onNodeDoubleClick={onNodeClick} nodeTypes={nodeTypes} deleteKeyCode={["Delete"]} - defaultEdgeOptions={{ - animated: true, - markerEnd: { type: MarkerType.ArrowClosed } - }} fitView + isValidConnection={validateConnection} > + { + if (n.type === 'input') return 'blue'; + return '#eee'; + }} + maskColor="rgba(0, 0, 0, 0.1)" + position="bottom-right" // 也可以是 top-right 等 + />
+ setEnableTwoWay(checked)} >双向连接 + - +
diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index 5b63e93..bdd99ad 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -30,7 +30,7 @@ export default function CustomNode({ {[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => ( (["target", "source"] as const).map((type) => ( - + )) ))} diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index 280ef13..221dd90 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -2,29 +2,37 @@ import { ChangeEvent } from 'react' import "./Toggle.css" interface ToggleProps { - checked: boolean; - onChange: (checked: boolean) => void; - name?: string; + checked: boolean; + onChange?: (checked: boolean) => void; + name?: string; + children?: React.ReactNode; + className?: string, } const Toggle : React.FC = ({ checked, onChange, - name + name, + children, + className = "" }) => { const handleChange = (e: ChangeEvent) => { onChange?.(e.target.checked); }; return ( - +
+ {children && {children}} + + +
) } diff --git a/src/types/graph.ts b/src/types/graph.ts index 6fa8909..4aa095e 100644 --- a/src/types/graph.ts +++ b/src/types/graph.ts @@ -2,7 +2,7 @@ import { Node, Edge } from '@xyflow/react'; export type AppNode = Node; -export type AppEdge = Edge; +export type AppEdge = Edge; export type NodeData = { label: string; @@ -13,4 +13,8 @@ export type NodeData = { endpoint?: string; dnsServers?: string; persistentKeepalive?: string; +} + +export type EdgeData = { + isTwoWayEdge: boolean } \ No newline at end of file