优化交互和外观

This commit is contained in:
limil 2026-01-30 12:13:09 +08:00
parent 97dd6e6900
commit 6a45efffe9
5 changed files with 102 additions and 58 deletions

View File

@ -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;
}

View File

@ -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<AppEdge[]>(initialEdges);
const [editingNode, setEditingNode] = useState<NodeData | null>(null);
const [showConfigViewer, setShowConfigViewer] = useState(false);
const [enableTwoWay, setEnableTwoWay] = useState(false);
const onNodesChange = useCallback(
(changes: NodeChange<AppNode>[]) => setNodes((nds) => applyNodeChanges<AppNode>(changes, nds)),
[],
);
[]);
const onEdgesChange = useCallback<OnEdgesChange>(
(changes) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)),
[],
);
const onEdgesChange = useCallback(
(changes : EdgeChange<AppEdge>[]) => setEdges((eds) => applyEdgeChanges<AppEdge>(changes, eds)),
[]);
const onConnect = useCallback<OnConnect>(
(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<AppEdge>(newEdge, eds));
},
[enableTwoWay]);
const onNodeClick = useCallback((_event: React.MouseEvent, node: AppNode) => {
setEditingNode(node.data);
}, []);
const onNodeClick = useCallback<NodeMouseHandler<AppNode>>(
(_event, node) => setEditingNode(node.data),
[]);
const validateConnection = useCallback<IsValidConnection>(
(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}
>
<Background />
<Controls />
<MiniMap
nodeColor={(n) => {
if (n.type === 'input') return 'blue';
return '#eee';
}}
maskColor="rgba(0, 0, 0, 0.1)"
position="bottom-right" // 也可以是 top-right 等
/>
</ReactFlow>
<div className="toolbar">
<div className="toolbar-group">
<Toggle
className="toolbar-item"
checked = {enableTwoWay}
onChange={checked => setEnableTwoWay(checked)} ></Toggle>
<button className="toolbar-btn" onClick={handleAddNode} title="添加新节点">
</button>
<button
className="toolbar-btn"
onClick={() => setShowConfigViewer(true)}
title="查看和下载WireGuard配置"
>
<button className="toolbar-btn" onClick={() => setShowConfigViewer(true)} title="查看配置">
📋
</button>
</div>
</div>

View File

@ -30,7 +30,7 @@ export default function CustomNode({
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => (
(["target", "source"] as const).map((type) => (
<Handle type={type} position={position} id={position} key={position} className="node-handle"/>
<Handle type={type} position={position} id={position} key={`${type}-${position}`} className="node-handle"/>
))
))}
</div>

View File

@ -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<ToggleProps> = ({
checked,
onChange,
name
name,
children,
className = ""
}) => {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange?.(e.target.checked);
};
return (
<label className="switch">
<input
type="checkbox"
name={name}
checked={checked}
onChange={handleChange}
/>
<span className="slider round"></span>
</label>
<div className={className}>
{children && <span>{children}</span>}
<label className="switch">
<input
type="checkbox"
name={name}
checked={checked}
onChange={handleChange}
/>
<span className="slider round"></span>
</label>
</div>
)
}

View File

@ -2,7 +2,7 @@ import { Node, Edge } from '@xyflow/react';
export type AppNode = Node<NodeData>;
export type AppEdge = Edge;
export type AppEdge = Edge<EdgeData>;
export type NodeData = {
label: string;
@ -13,4 +13,8 @@ export type NodeData = {
endpoint?: string;
dnsServers?: string;
persistentKeepalive?: string;
}
export type EdgeData = {
isTwoWayEdge: boolean
}