优化交互和外观
This commit is contained in:
parent
97dd6e6900
commit
6a45efffe9
18
src/App.css
18
src/App.css
@ -1,8 +1,10 @@
|
|||||||
#root {
|
#root {
|
||||||
max-width: 1280px;
|
width: 100vw;
|
||||||
margin: 0 auto;
|
height: 100vh;
|
||||||
padding: 2rem;
|
margin: 0;
|
||||||
text-align: center;
|
padding: 0;
|
||||||
|
max-width: none; /* 必须取消最大宽度限制 */
|
||||||
|
text-align: left; /* 取消居中对齐,否则 toolbar 里的文字可能会乱 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
@ -53,6 +55,7 @@
|
|||||||
|
|
||||||
.toolbar-group {
|
.toolbar-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -82,3 +85,10 @@
|
|||||||
transform: scale(0.95);
|
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,
|
ReactFlowProvider,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
OnEdgesChange,
|
EdgeChange,
|
||||||
MarkerType,
|
MarkerType,
|
||||||
|
NodeMouseHandler,
|
||||||
OnConnect,
|
OnConnect,
|
||||||
|
MiniMap,
|
||||||
|
IsValidConnection
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import { AppNode, AppEdge, NodeData } from './types/graph';
|
import { AppNode, AppEdge, NodeData } from './types/graph';
|
||||||
@ -21,23 +24,9 @@ import ConfigViewer from './components/ConfigViewer';
|
|||||||
import Toggle from "./components/Toggle"
|
import Toggle from "./components/Toggle"
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
const initialNodes: AppNode[] = [
|
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 initialEdges: AppEdge[] = [];
|
const initialEdges: AppEdge[] = [];
|
||||||
|
|
||||||
|
|
||||||
const nodeTypes : NodeTypes = {
|
const nodeTypes : NodeTypes = {
|
||||||
custom: CustomNode,
|
custom: CustomNode,
|
||||||
};
|
};
|
||||||
@ -47,31 +36,55 @@ function FlowContent(): ReactNode {
|
|||||||
const [edges, setEdges] = useState<AppEdge[]>(initialEdges);
|
const [edges, setEdges] = useState<AppEdge[]>(initialEdges);
|
||||||
const [editingNode, setEditingNode] = useState<NodeData | null>(null);
|
const [editingNode, setEditingNode] = useState<NodeData | null>(null);
|
||||||
const [showConfigViewer, setShowConfigViewer] = useState(false);
|
const [showConfigViewer, setShowConfigViewer] = useState(false);
|
||||||
|
const [enableTwoWay, setEnableTwoWay] = useState(false);
|
||||||
|
|
||||||
const onNodesChange = useCallback(
|
const onNodesChange = useCallback(
|
||||||
(changes: NodeChange<AppNode>[]) => setNodes((nds) => applyNodeChanges<AppNode>(changes, nds)),
|
(changes: NodeChange<AppNode>[]) => setNodes((nds) => applyNodeChanges<AppNode>(changes, nds)),
|
||||||
[],
|
[]);
|
||||||
);
|
|
||||||
|
|
||||||
const onEdgesChange = useCallback<OnEdgesChange>(
|
const onEdgesChange = useCallback(
|
||||||
(changes) => setEdges((edgesSnapshot) => applyEdgeChanges(changes, edgesSnapshot)),
|
(changes : EdgeChange<AppEdge>[]) => setEdges((eds) => applyEdgeChanges<AppEdge>(changes, eds)),
|
||||||
[],
|
[]);
|
||||||
);
|
|
||||||
|
|
||||||
const onConnect = useCallback<OnConnect>(
|
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) => {
|
const onNodeClick = useCallback<NodeMouseHandler<AppNode>>(
|
||||||
setEditingNode(node.data);
|
(_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 handleAddNode = (): void => {
|
||||||
const newNodeId = `n${Date.now()}`;
|
const newNodeId = `n${Date.now()}`;
|
||||||
const newNode: AppNode = {
|
const newNode: AppNode = {
|
||||||
id: newNodeId,
|
id: newNodeId,
|
||||||
position: { x: Math.random() * 500, y: Math.random() * 500 },
|
position: { x: 0, y: 0 },
|
||||||
data: {
|
data: {
|
||||||
label: `Node-${String.fromCharCode(65 + (nodes.length % 26))}`,
|
label: `Node-${String.fromCharCode(65 + (nodes.length % 26))}`,
|
||||||
ipAddress: `10.0.0.${nodes.length + 2}`,
|
ipAddress: `10.0.0.${nodes.length + 2}`,
|
||||||
@ -80,8 +93,9 @@ function FlowContent(): ReactNode {
|
|||||||
publicKey: '',
|
publicKey: '',
|
||||||
},
|
},
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
|
selected: true
|
||||||
};
|
};
|
||||||
setNodes((prev) => [...prev, newNode]);
|
setNodes((prev) => [...prev.map(node => ({ ...node, selected: false })), newNode]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateNode = (updatedData: NodeData): void => {
|
const handleUpdateNode = (updatedData: NodeData): void => {
|
||||||
@ -107,28 +121,36 @@ function FlowContent(): ReactNode {
|
|||||||
onNodeDoubleClick={onNodeClick}
|
onNodeDoubleClick={onNodeClick}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
deleteKeyCode={["Delete"]}
|
deleteKeyCode={["Delete"]}
|
||||||
defaultEdgeOptions={{
|
|
||||||
animated: true,
|
|
||||||
markerEnd: { type: MarkerType.ArrowClosed }
|
|
||||||
}}
|
|
||||||
fitView
|
fitView
|
||||||
|
isValidConnection={validateConnection}
|
||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
<Controls />
|
<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>
|
</ReactFlow>
|
||||||
|
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<div className="toolbar-group">
|
<div className="toolbar-group">
|
||||||
|
<Toggle
|
||||||
|
className="toolbar-item"
|
||||||
|
checked = {enableTwoWay}
|
||||||
|
onChange={checked => setEnableTwoWay(checked)} >双向连接</Toggle>
|
||||||
|
|
||||||
<button className="toolbar-btn" onClick={handleAddNode} title="添加新节点">
|
<button className="toolbar-btn" onClick={handleAddNode} title="添加新节点">
|
||||||
➕ 添加节点
|
➕ 添加节点
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="toolbar-btn"
|
<button className="toolbar-btn" onClick={() => setShowConfigViewer(true)} title="查看配置">
|
||||||
onClick={() => setShowConfigViewer(true)}
|
|
||||||
title="查看和下载WireGuard配置"
|
|
||||||
>
|
|
||||||
📋 配置
|
📋 配置
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export default function CustomNode({
|
|||||||
|
|
||||||
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => (
|
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => (
|
||||||
(["target", "source"] as const).map((type) => (
|
(["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>
|
</div>
|
||||||
|
|||||||
@ -3,19 +3,26 @@ import "./Toggle.css"
|
|||||||
|
|
||||||
interface ToggleProps {
|
interface ToggleProps {
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (checked: boolean) => void;
|
onChange?: (checked: boolean) => void;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Toggle : React.FC<ToggleProps> = ({
|
const Toggle : React.FC<ToggleProps> = ({
|
||||||
checked,
|
checked,
|
||||||
onChange,
|
onChange,
|
||||||
name
|
name,
|
||||||
|
children,
|
||||||
|
className = ""
|
||||||
}) => {
|
}) => {
|
||||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange?.(e.target.checked);
|
onChange?.(e.target.checked);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{children && <span>{children}</span>}
|
||||||
|
|
||||||
<label className="switch">
|
<label className="switch">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -25,6 +32,7 @@ const Toggle : React.FC<ToggleProps> = ({
|
|||||||
/>
|
/>
|
||||||
<span className="slider round"></span>
|
<span className="slider round"></span>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Node, Edge } from '@xyflow/react';
|
|||||||
|
|
||||||
export type AppNode = Node<NodeData>;
|
export type AppNode = Node<NodeData>;
|
||||||
|
|
||||||
export type AppEdge = Edge;
|
export type AppEdge = Edge<EdgeData>;
|
||||||
|
|
||||||
export type NodeData = {
|
export type NodeData = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -14,3 +14,7 @@ export type NodeData = {
|
|||||||
dnsServers?: string;
|
dnsServers?: string;
|
||||||
persistentKeepalive?: string;
|
persistentKeepalive?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EdgeData = {
|
||||||
|
isTwoWayEdge: boolean
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user