优化节点外观
This commit is contained in:
parent
070b197171
commit
653b20ed61
58
src/App.tsx
58
src/App.tsx
@ -18,7 +18,7 @@ import {
|
|||||||
EdgeMouseHandler
|
EdgeMouseHandler
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import { AppNode, AppEdge, NodeData, EdgeData, NodeDataUpdate, EdgeDataUpdate, Settings } from './types/graph';
|
import { AppNode, AppEdge, NodeData, EdgeData, NodeDataUpdate, EdgeDataUpdate, Settings, initialSettings, SettingsContext } from './types/graph';
|
||||||
import CustomNode from './components/CustomNode';
|
import CustomNode from './components/CustomNode';
|
||||||
import NodeEditor from './components/NodeEditor';
|
import NodeEditor from './components/NodeEditor';
|
||||||
import EdgeEditor from './components/EdgeEditor'
|
import EdgeEditor from './components/EdgeEditor'
|
||||||
@ -29,10 +29,6 @@ import './App.css';
|
|||||||
|
|
||||||
const initialNodes: AppNode[] = [];
|
const initialNodes: AppNode[] = [];
|
||||||
const initialEdges: AppEdge[] = [];
|
const initialEdges: AppEdge[] = [];
|
||||||
const initialSettings : Settings = {
|
|
||||||
listenPort: 38894,
|
|
||||||
mtu: 1420,
|
|
||||||
};
|
|
||||||
|
|
||||||
const nodeTypes : NodeTypes = {
|
const nodeTypes : NodeTypes = {
|
||||||
custom: CustomNode,
|
custom: CustomNode,
|
||||||
@ -147,31 +143,33 @@ function FlowContent(): ReactNode {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100vw', height: '100vh' }}>
|
<div style={{ width: '100vw', height: '100vh' }}>
|
||||||
<ReactFlow
|
<SettingsContext value={settings}>
|
||||||
nodes={nodes}
|
<ReactFlow
|
||||||
edges={edges}
|
nodes={nodes}
|
||||||
onNodesChange={onNodesChange}
|
edges={edges}
|
||||||
onEdgesChange={onEdgesChange}
|
onNodesChange={onNodesChange}
|
||||||
onConnect={onConnect}
|
onEdgesChange={onEdgesChange}
|
||||||
onNodeDoubleClick={onNodeClick}
|
onConnect={onConnect}
|
||||||
onEdgeDoubleClick={onEdgeClick}
|
onNodeDoubleClick={onNodeClick}
|
||||||
nodeTypes={nodeTypes}
|
onEdgeDoubleClick={onEdgeClick}
|
||||||
deleteKeyCode={["Delete"]}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
deleteKeyCode={["Delete"]}
|
||||||
isValidConnection={validateConnection}
|
fitView
|
||||||
>
|
isValidConnection={validateConnection}
|
||||||
<Background />
|
>
|
||||||
<Controls />
|
<Background />
|
||||||
<MiniMap
|
<Controls />
|
||||||
nodeColor={(n) => {
|
<MiniMap
|
||||||
if (n.type === 'input') return 'blue';
|
nodeColor={(n) => {
|
||||||
return '#eee';
|
if (n.type === 'input') return 'blue';
|
||||||
}}
|
return '#eee';
|
||||||
maskColor="rgba(0, 0, 0, 0.1)"
|
}}
|
||||||
position="bottom-right" // 也可以是 top-right 等
|
maskColor="rgba(0, 0, 0, 0.1)"
|
||||||
/>
|
position="bottom-right" // 也可以是 top-right 等
|
||||||
</ReactFlow>
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</SettingsContext>
|
||||||
|
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<div className="toolbar-group">
|
<div className="toolbar-group">
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
background: white;
|
background: white;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-width: 240px; /* 限制节点不要太长 */
|
max-width: 300px; /* 限制节点不要太长 */
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
@ -30,26 +30,43 @@
|
|||||||
|
|
||||||
.node-header {
|
.node-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
align-items: center;
|
margin-bottom: 4px;
|
||||||
margin-bottom: 8px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-label {
|
.node-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 12px; /* smaller title font */
|
font-size: 12px; /* smaller title font */
|
||||||
flex: 0 0 100px; /* 固定宽度 100px,给按钮留更多空间 */
|
flex: 0 0 120px; /* 固定宽度 100px,给按钮留更多空间 */
|
||||||
min-width: 0; /* allow truncation inside flex */
|
min-width: 0; /* allow truncation inside flex */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-info {
|
.node-info {
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
color: #666;
|
border-bottom: 1px solid #eee;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .label {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gen-btn {
|
.gen-btn {
|
||||||
@ -59,10 +76,11 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1px 6px; /* 更小的按钮内边距 */
|
padding: 1px 6px; /* 更小的按钮内边距 */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 10px; /* 更小的文字 */
|
font-size: 8px; /* 更小的文字 */
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
flex: 1;
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease, opacity 120ms ease;
|
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease, opacity 120ms ease;
|
||||||
outline: none; /* remove default focus outline */
|
outline: none; /* remove default focus outline */
|
||||||
@ -72,7 +90,6 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gen-btn:focus {
|
.gen-btn:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -81,20 +98,4 @@
|
|||||||
transform: translateY(1px) scale(0.995);
|
transform: translateY(1px) scale(0.995);
|
||||||
background-color: #0f62d6; /* slightly darker on press */
|
background-color: #0f62d6; /* slightly darker on press */
|
||||||
box-shadow: inset 0 1px 2px rgba(0,0,0,0.12);
|
box-shadow: inset 0 1px 2px rgba(0,0,0,0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item .label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item .value {
|
|
||||||
color: #333;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode, useContext } from 'react';
|
||||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||||
import { AppNode } from '../types/graph';
|
import { AppNode, SettingsContext } from '../types/graph';
|
||||||
import './CustomNode.css';
|
import './CustomNode.css';
|
||||||
|
|
||||||
export default function CustomNode({
|
export default function CustomNode({
|
||||||
@ -10,11 +10,38 @@ export default function CustomNode({
|
|||||||
const handleGenerate = (e: React.MouseEvent) => {
|
const handleGenerate = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const settings = useContext(SettingsContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`custom-node ${selected ? 'selected' : ''}`}>
|
<div className={`custom-node ${selected ? 'selected' : ''}`}>
|
||||||
<div className="node-header">
|
<div className="node-header">
|
||||||
<span className="node-label" title={data.label}>{data.label}</span>
|
<label className="node-label">{data.label}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='node-info'>
|
||||||
|
{settings.ipv4Subnet && (
|
||||||
|
<div className="info-item">
|
||||||
|
<span className="label">IPv4地址:</span>
|
||||||
|
<span className="value">{data.ipv4Address || "未设置"}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{settings.ipv6Subnet && (
|
||||||
|
<div className="info-item">
|
||||||
|
<span className="label">IPv6地址:</span>
|
||||||
|
<span className="value">{data.ipv6Address || "未设置"}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(!settings.ipv4Subnet && !settings.ipv6Subnet) && (
|
||||||
|
<div className="info-item">
|
||||||
|
<span className="label">未设置任何子网,请在设置中设置</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="node-actions">
|
||||||
<button className="gen-btn" onClick={handleGenerate} onDoubleClick={e => e.stopPropagation()}>生成配置</button>
|
<button className="gen-btn" onClick={handleGenerate} onDoubleClick={e => e.stopPropagation()}>生成配置</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -160,10 +160,6 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease; /* 稍微拉长过渡,显得更平滑 */
|
transition: all 0.2s ease; /* 稍微拉长过渡,显得更平滑 */
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
/* 基础阴影 */
|
/* 基础阴影 */
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,18 +27,14 @@ function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
|
|||||||
|
|
||||||
if(ipv4Subnet) {
|
if(ipv4Subnet) {
|
||||||
const cidr = IPNetwork.parse(ipv4Subnet);
|
const cidr = IPNetwork.parse(ipv4Subnet);
|
||||||
if(!ipv4Address) {
|
if(ipv4Address && !cidr.contains(IPNetwork.parse(`${ipv4Address}/32`))) {
|
||||||
errors.push("需要设置IPv4地址");
|
|
||||||
} else if(!cidr.contains(IPNetwork.parse(`${ipv4Address}/32`))) {
|
|
||||||
errors.push("IPv4不在子网范围中");
|
errors.push("IPv4不在子网范围中");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(ipv6Subnet) {
|
if(ipv6Subnet) {
|
||||||
const cidr = IPNetwork.parse(ipv6Subnet);
|
const cidr = IPNetwork.parse(ipv6Subnet);
|
||||||
if(!ipv6Address) {
|
if(ipv6Address && !cidr.contains(IPNetwork.parse(`${ipv6Address}/128`))) {
|
||||||
errors.push("需要设置IPv6地址");
|
|
||||||
} else if(!cidr.contains(IPNetwork.parse(`${ipv6Address}/128`))) {
|
|
||||||
errors.push("IPv6不在子网范围中");
|
errors.push("IPv6不在子网范围中");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ function Validate(updateData: Settings) : string[] {
|
|||||||
if(ipv4Subnet) {
|
if(ipv4Subnet) {
|
||||||
const result = IPNetwork.parse(ipv4Subnet)
|
const result = IPNetwork.parse(ipv4Subnet)
|
||||||
if(!result.isValid) {
|
if(!result.isValid) {
|
||||||
errors.push("IPv4子网:" + (result.error ?? "ipv4子网不合法"))
|
errors.push("IPv4子网:" + (result.error || "ipv4子网不合法"))
|
||||||
} else if(result.version != 'IPv4') {
|
} else if(result.version != 'IPv4') {
|
||||||
errors.push("IPv4子网:" + "非IPv4 CIDR");
|
errors.push("IPv4子网:" + "非IPv4 CIDR");
|
||||||
}
|
}
|
||||||
@ -26,7 +26,7 @@ function Validate(updateData: Settings) : string[] {
|
|||||||
if(ipv6Subnet) {
|
if(ipv6Subnet) {
|
||||||
const result = IPNetwork.parse(ipv6Subnet)
|
const result = IPNetwork.parse(ipv6Subnet)
|
||||||
if(!result.isValid) {
|
if(!result.isValid) {
|
||||||
errors.push("IPv6子网:" + (result.error ?? "子网不合法"));
|
errors.push("IPv6子网:" + (result.error || "子网不合法"));
|
||||||
} else if(result.version != 'IPv6') {
|
} else if(result.version != 'IPv6') {
|
||||||
errors.push("IPv6子网:" + "非IPv6 CIDR");
|
errors.push("IPv6子网:" + "非IPv6 CIDR");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,4 +38,11 @@ export interface Settings {
|
|||||||
|
|
||||||
ipv4Subnet?: string;
|
ipv4Subnet?: string;
|
||||||
ipv6Subnet?: string;
|
ipv6Subnet?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const initialSettings : Settings = {
|
||||||
|
listenPort: 38894,
|
||||||
|
mtu: 1420,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsContext = createContext<Settings>(initialSettings);
|
||||||
Loading…
x
Reference in New Issue
Block a user