diff --git a/package-lock.json b/package-lock.json index d429869..4e6c0f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,14 @@ "version": "0.0.0", "dependencies": { "@xyflow/react": "^12.10.0", + "base64-js": "^1.5.1", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "tweetnacl": "^1.0.3" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/base64-js": "^1.3.2", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", @@ -1411,6 +1414,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/base64-js": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.3.2.tgz", + "integrity": "sha512-Q2Xn2/vQHRGLRXhQ5+BSLwhHkR3JVflxVKywH0Q6fVoAiUE8fFYL2pE5/l2ZiOiBDfA8qUqRnSxln4G/NFz1Sg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", @@ -1619,6 +1629,25 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/baseline-browser-mapping": { "version": "2.9.18", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", @@ -2918,6 +2947,11 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index d25ab40..875e198 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,14 @@ }, "dependencies": { "@xyflow/react": "^12.10.0", + "base64-js": "^1.5.1", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "tweetnacl": "^1.0.3" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/base64-js": "^1.3.2", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", diff --git a/src/App.tsx b/src/App.tsx index d09de87..7f0edd8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,24 +17,62 @@ import { IsValidConnection } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { AppNode, AppEdge, NodeData } from './types/graph'; +import { AppNode, AppEdge, NodeData, Settings } from './types/graph'; import CustomNode from './components/CustomNode'; import NodeEditor from './components/NodeEditor'; import Toggle from "./components/Toggle" +import { generateWireGuardPrivateKey } from './utils/wireguardConfig'; import './App.css'; const initialNodes: AppNode[] = []; const initialEdges: AppEdge[] = []; +const initialSettings : Settings = { + v4SubNetPrefix: [172, 29], + listenPort: 38894, +}; + +export function generateEdgeId() : string { + return `e-${crypto.randomUUID()}`; +} + +export function generateNodeId() : string { + return `n-${crypto.randomUUID()}`; +} const nodeTypes : NodeTypes = { custom: CustomNode, }; +function getFirstAvailableId(ids: number[]) : number { + const idSet = new Set(ids); + let id = 1; + while (idSet.has(id)) { + id++; + } + return id; +} + +function generateNodeData(nodes : NodeData[]) : NodeData | null { + const hostId = getFirstAvailableId(nodes.map(n => n.hostId)) + if(hostId > 255) return null + + const privateKey = generateWireGuardPrivateKey(); + + const node : NodeData = { + label: `Node-${hostId}`, + privateKey: privateKey, + groupId: 0, + hostId: hostId, + } + return node +} + function FlowContent(): ReactNode { const [nodes, setNodes] = useState(initialNodes); const [edges, setEdges] = useState(initialEdges); + const [settings, setSettings] = useState(initialSettings); + const [editingNode, setEditingNode] = useState(null); - const [showConfigViewer, setShowConfigViewer] = useState(false); const [enableTwoWay, setEnableTwoWay] = useState(false); const onNodesChange = useCallback( @@ -80,17 +118,12 @@ function FlowContent(): ReactNode { [edges]); const handleAddNode = (): void => { - const newNodeId = `n${Date.now()}`; + const result = generateNodeData(nodes.map(n => n.data)); + if(result == null) return; const newNode: AppNode = { - id: newNodeId, + id: generateNodeId(), position: { x: 0, y: 0 }, - data: { - label: `Node-${String.fromCharCode(65 + (nodes.length % 26))}`, - ipAddress: `10.0.0.${nodes.length + 2}`, - listenPort: `${51820 + nodes.length}`, - privateKey: '', - publicKey: '', - }, + data: result, type: 'custom', selected: true }; @@ -146,7 +179,7 @@ function FlowContent(): ReactNode { ➕ 添加节点 - @@ -158,16 +191,9 @@ function FlowContent(): ReactNode { node={editingNode} onUpdate={handleUpdateNode} onClose={() => setEditingNode(null)} + settings={settings} /> )} - - {/* {showConfigViewer && ( - setShowConfigViewer(false)} - /> - )} */} ); } diff --git a/src/components/CustomNode.tsx b/src/components/CustomNode.tsx index bdd99ad..d3db6bc 100644 --- a/src/components/CustomNode.tsx +++ b/src/components/CustomNode.tsx @@ -14,18 +14,6 @@ export default function CustomNode({
- {data.ipAddress && ( -
- IP: - {data.ipAddress} -
- )} - {data.listenPort && ( -
- Port: - {data.listenPort} -
- )}
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => ( diff --git a/src/components/NodeEditor.css b/src/components/NodeEditor.css index 4fcec41..c2d7e74 100644 --- a/src/components/NodeEditor.css +++ b/src/components/NodeEditor.css @@ -70,6 +70,8 @@ } .form-group { + display: flex; + flex-direction: column; margin-bottom: 15px; } @@ -83,7 +85,7 @@ .form-group input, .form-group textarea { - width: 100%; + flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; @@ -111,33 +113,46 @@ border-top: 1px solid #eee; } -.btn-save, -.btn-cancel { - flex: 1; - padding: 10px; +.item-group { + display: flex; + gap: 10px; +} + + +/* 1. 基础通用样式 */ +.btn-interect, .btn-save, .btn-cancel { + padding: 8px 16px; /* 补充了间距,确保按钮有厚度 */ border: none; border-radius: 4px; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.3s ease; + transition: all 0.1s ease-in-out; + display: flex; + align-items: center; + justify-content: center; } -.btn-save { +/* 2. 统一交互反馈:悬停变深,点下缩放 */ +.btn-interect:hover, .btn-save:hover, .btn-cancel:hover { + filter: brightness(0.9); /* 自动变深,无需指定具体颜色 */ + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.btn-interect:active, .btn-save:active, .btn-cancel:active { + transform: scale(0.96); +} + +.btn-save, .btn-cancel { + flex: 1; +} + +.btn-interect, .btn-save { background: #1677ff; color: white; } -.btn-save:hover { - background: #0b66d4; - box-shadow: 0 2px 8px rgba(22, 119, 255, 0.3); -} - .btn-cancel { background: #f5f5f5; color: #333; -} - -.btn-cancel:hover { - background: #e6e6e6; -} +} \ No newline at end of file diff --git a/src/components/NodeEditor.tsx b/src/components/NodeEditor.tsx index 3ddefb2..494a3ca 100644 --- a/src/components/NodeEditor.tsx +++ b/src/components/NodeEditor.tsx @@ -1,16 +1,19 @@ import { useState, ReactNode } from 'react'; -import { validateNodeConfig } from '../utils/wireguardConfig'; -import { NodeData } from '../types/graph'; +import { NodeData, Settings } from '../types/graph'; import './NodeEditor.css'; + + interface NodeEditorProps { node: NodeData; + settings: Settings; onUpdate: (data: NodeData) => void; onClose: () => void; } export default function NodeEditor({ - node, + node, + settings, onUpdate, onClose }: NodeEditorProps): ReactNode { @@ -25,15 +28,15 @@ export default function NodeEditor({ }; const handleSave = (): void => { - const validation = validateNodeConfig(formData); - if (!validation.isValid) { - setErrors(validation.errors); - return; - } + // const validation = validateNodeConfig(formData); + // if (!validation.isValid) { + // setErrors(validation.errors); + // return; + // } - setErrors([]); - onUpdate(formData); - onClose(); + // setErrors([]); + // onUpdate(formData); + // onClose(); }; return ( @@ -63,13 +66,16 @@ export default function NodeEditor({
- - handleInputChange('ipAddress', e.target.value)} - placeholder="例如: 10.0.0.1" - /> + +
+ handleInputChange('privateKey', e.target.value)} + readOnly + /> + +
+
@@ -82,36 +88,6 @@ export default function NodeEditor({ />
-
- -