import { useState, useCallback, ReactNode } from 'react'; import { ReactFlow, applyNodeChanges, applyEdgeChanges, addEdge, Background, Controls, ReactFlowProvider, NodeTypes, NodeChange, EdgeChange, MarkerType, NodeMouseHandler, OnConnect, MiniMap, IsValidConnection, EdgeMouseHandler } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { AppNode, AppEdge, NodeData, EdgeData, NodeDataUpdate, EdgeDataUpdate } from './types/graph'; import {Settings, SettingsContext} from './types/settings' import CustomNode from './components/CustomNode'; import NodeEditor from './components/NodeEditor'; import EdgeEditor from './components/EdgeEditor' import SettingsEditor from './components/SettingsEditor' import Toggle from "./components/Toggle" import { generateWireGuardPrivateKey } from './utils/wireguardConfig'; import CryptoJS from 'crypto-js'; import './App.css'; import toast, { Toaster } from 'react-hot-toast'; import SaveConfig from './types/saveConfig' import { plainToInstance, instanceToPlain } from 'class-transformer'; import SaveLoadPanel from './components/SaveLoadPanel'; const initialNodes: AppNode[] = []; const initialEdges: AppEdge[] = []; const nodeTypes : NodeTypes = { custom: CustomNode, }; function generateNodeData(count: number) : NodeData | null { const privateKey = generateWireGuardPrivateKey(); const node : NodeData = { id: `n-${crypto.randomUUID()}`, label: `Node-${count + 1}`, privateKey: privateKey } return node } function FlowContent(): ReactNode { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); const [settings, setSettings] = useState(new Settings()); const [editingNode, setEditingNode] = useState(undefined); const [editingEdge, setEditingEdge] = useState(undefined); const [enableTwoWay, setEnableTwoWay] = useState(false); const [editSettings, setEditSettings] = useState(false); const onNodesChange = useCallback( (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)), []); const onEdgesChange = useCallback( (changes : EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)), []); const onConnect = useCallback( (params) => { const id = `e-${crypto.randomUUID()}` const newEdge : AppEdge = { ...params, id: id, animated: !enableTwoWay, markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed }, data : { id: id, isTwoWayEdge: enableTwoWay } } return setEdges((eds) => addEdge(newEdge, eds)); }, [enableTwoWay]); const onNodeClick = useCallback>( (_event, node) => setEditingNode(node.data), []); const onEdgeClick = useCallback>( (_event, edge) => setEditingEdge(edge.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 result = generateNodeData(nodes.length); if(!result) return; const newNode: AppNode = { id: result.id, position: { x: 0, y: 0 }, data: result, type: 'custom', selected: true }; setNodes((prev) => [...prev.map(node => ({ ...node, selected: false })), newNode]); }; const handleUpdateNode = (updatedData: NodeDataUpdate): void => { setNodes((prev) => prev.map((node) => { if (node.data.id === editingNode?.id) { return { ...node, data: {...node.data, ...updatedData} } } return node; }) ); }; const handleUpdateEdge = (updatedData: EdgeDataUpdate): void => { setEdges((prev) => prev.map((edge) => { if (edge.data && edge.data.id === editingEdge?.id) { edge.data = {...edge.data, ...updatedData} } return edge; }) ); }; const handleSaveConfig = async (pass?: string): Promise => { try { const saveConfig: SaveConfig = {settings: settings, nodes: [], edges: [], encrypted: false} nodes.forEach(node => { const nodeData = node.data; let privateKey: string = nodeData.privateKey; if(pass) { privateKey = CryptoJS.AES.encrypt(privateKey, pass).toString(); saveConfig.encrypted = true; } saveConfig.nodes.push({...node, data: {...nodeData, privateKey: privateKey}}); }); edges.forEach(edge => { saveConfig.edges.push(edge); }); const json = instanceToPlain(saveConfig); const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }); const lastNameKey = 'wg-last-filename'; const suggestedName = (localStorage.getItem(lastNameKey) || 'wg-config.json'); // Use File System Access API when available to show save file picker if ((window as any).showSaveFilePicker) { try { const handle = await (window as any).showSaveFilePicker({ suggestedName: suggestedName, types: [{ description: 'WireGuard Config', accept: { 'application/json': ['.json'] } }] }); const writable = await handle.createWritable(); await writable.write(blob); await writable.close(); // remember chosen name if available try { const name = handle.name || suggestedName; localStorage.setItem(lastNameKey, name); } catch (e) { localStorage.setItem(lastNameKey, suggestedName); } } catch (e) { // If user cancels or error, fallback to anchor download const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = suggestedName; a.click(); URL.revokeObjectURL(url); } } else { // Fallback: create an anchor and trigger download, using last used filename const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = suggestedName; a.click(); URL.revokeObjectURL(url); localStorage.setItem(lastNameKey, suggestedName); } } catch (e) { toast.error('保存失败: ' + e); } }; const handleLoadConfig = async (fileText: string, pass?: string): Promise => { try { const plainObject = JSON.parse(fileText); const saveConfig: SaveConfig = plainToInstance(SaveConfig, plainObject); if (saveConfig.encrypted) { if (!pass) { toast.error('需要密码以解密私钥'); return; } for(let node of saveConfig.nodes) { const privateKey = CryptoJS.AES.decrypt(node.data.privateKey, pass).toString(CryptoJS.enc.Utf8); if(!privateKey) { toast.error('密码错误'); return; } node.data.privateKey = privateKey; } } setSettings(saveConfig.settings); setNodes(saveConfig.nodes); setEdges(saveConfig.edges); } catch (e) { toast.error('加载失败: ' + e); } }; return (
{ if (n.type === 'input') return 'blue'; return '#eee'; }} maskColor="rgba(0, 0, 0, 0.1)" position="bottom-right" // 也可以是 top-right 等 />
图表操作
setEnableTwoWay(checked)} >双向连接
文件操作
{editingNode && ( setEditingNode(undefined)} settings={settings} /> )} {editingEdge && ( setEditingEdge(undefined)} /> )} {editSettings && ( {setSettings(settingsUpdate)}} onClose={() => setEditSettings(false)} /> )}
); } export default function App(): ReactNode { return ( ); }