优化交互和外观
This commit is contained in:
parent
97dd6e6900
commit
6a45efffe9
18
src/App.css
18
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;
|
||||
}
|
||||
100
src/App.tsx
100
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<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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -14,3 +14,7 @@ export type NodeData = {
|
||||
dnsServers?: string;
|
||||
persistentKeepalive?: string;
|
||||
}
|
||||
|
||||
export type EdgeData = {
|
||||
isTwoWayEdge: boolean
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user