优化节点编辑的外观
This commit is contained in:
parent
0c5668838f
commit
6cf0af4528
38
src/App.tsx
38
src/App.tsx
@ -27,42 +27,20 @@ import './App.css';
|
|||||||
const initialNodes: AppNode[] = [];
|
const initialNodes: AppNode[] = [];
|
||||||
const initialEdges: AppEdge[] = [];
|
const initialEdges: AppEdge[] = [];
|
||||||
const initialSettings : Settings = {
|
const initialSettings : Settings = {
|
||||||
v4SubNetPrefix: [172, 29],
|
|
||||||
listenPort: 38894,
|
listenPort: 38894,
|
||||||
|
mtu: 1420,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function generateEdgeId() : string {
|
|
||||||
return `e-${crypto.randomUUID()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateNodeId() : string {
|
|
||||||
return `n-${crypto.randomUUID()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeTypes : NodeTypes = {
|
const nodeTypes : NodeTypes = {
|
||||||
custom: CustomNode,
|
custom: CustomNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getFirstAvailableId(ids: number[]) : number {
|
function generateNodeData(count: number) : NodeData | null {
|
||||||
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.filter(n => n.groupId == 0).map(n => n.hostId))
|
|
||||||
if(hostId > 255) return null
|
|
||||||
|
|
||||||
const privateKey = generateWireGuardPrivateKey();
|
const privateKey = generateWireGuardPrivateKey();
|
||||||
|
|
||||||
const node : NodeData = {
|
const node : NodeData = {
|
||||||
label: `Node-${hostId}`,
|
id: `n-${crypto.randomUUID()}`,
|
||||||
privateKey: privateKey,
|
label: `Node-${count + 1}`,
|
||||||
groupId: 0,
|
privateKey: privateKey
|
||||||
hostId: hostId,
|
|
||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@ -87,7 +65,7 @@ function FlowContent(): ReactNode {
|
|||||||
(params) => {
|
(params) => {
|
||||||
const newEdge : AppEdge = {
|
const newEdge : AppEdge = {
|
||||||
...params,
|
...params,
|
||||||
id: `e-${Date.now()}`,
|
id: `e-${crypto.randomUUID()}`,
|
||||||
animated: !enableTwoWay,
|
animated: !enableTwoWay,
|
||||||
markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed },
|
markerEnd: enableTwoWay ? undefined : { type: MarkerType.ArrowClosed },
|
||||||
data : {
|
data : {
|
||||||
@ -118,10 +96,10 @@ function FlowContent(): ReactNode {
|
|||||||
[edges]);
|
[edges]);
|
||||||
|
|
||||||
const handleAddNode = (): void => {
|
const handleAddNode = (): void => {
|
||||||
const result = generateNodeData(nodes.map(n => n.data));
|
const result = generateNodeData(nodes.length);
|
||||||
if(result == null) return;
|
if(result == null) return;
|
||||||
const newNode: AppNode = {
|
const newNode: AppNode = {
|
||||||
id: generateNodeId(),
|
id: result.id,
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
data: result,
|
data: result,
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
|
|||||||
66
src/components/Folder.css
Normal file
66
src/components/Folder.css
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
.folder-item {
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停微阴影 */
|
||||||
|
.folder-item:hover {
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
|
||||||
|
/* 核心修改:禁止选中标题文字 */
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-header:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 箭头旋转动画 */
|
||||||
|
.arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 核心动画逻辑 */
|
||||||
|
.folder-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
transition: grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-content.show {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-inner {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 16px; /* 默认隐藏时 padding 也是 0 */
|
||||||
|
transition: padding 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show .content-inner {
|
||||||
|
padding: 12px 16px; /* 展开后增加内边距 */
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
27
src/components/Folder.tsx
Normal file
27
src/components/Folder.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useState, ReactNode } from 'react';
|
||||||
|
import './Folder.css';
|
||||||
|
|
||||||
|
interface FolderProps {
|
||||||
|
title: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Folder({ title, children }: FolderProps): ReactNode {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="folder-item">
|
||||||
|
<div className="folder-header" onClick={() => setIsOpen(!isOpen)}>
|
||||||
|
<span>{title}</span>
|
||||||
|
{/* 使用同一个字符配合旋转动画,视觉更连贯 */}
|
||||||
|
<span className={`arrow ${isOpen ? 'show-arrow' : ''}`}>▼</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children && (
|
||||||
|
<div className={`folder-content ${isOpen ? 'show' : ''}`}>
|
||||||
|
<div className="content-inner">{children}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -56,11 +56,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error-box {
|
.error-box {
|
||||||
|
position: relative;
|
||||||
background: #fee;
|
background: #fee;
|
||||||
border: 1px solid #fcc;
|
border: 1px solid #fcc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
user-select: none; /* 禁止选中 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-close-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(255, 77, 79, 0.1); /* 悬停微红 */
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
||||||
|
20%, 80% { transform: translate3d(2px, 0, 0); }
|
||||||
|
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
||||||
|
40%, 60% { transform: translate3d(4px, 0, 0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
@ -118,41 +151,90 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 1. 基础通用样式 - 强化了投影和圆角 */
|
||||||
/* 1. 基础通用样式 */
|
|
||||||
.btn-interect, .btn-save, .btn-cancel {
|
.btn-interect, .btn-save, .btn-cancel {
|
||||||
padding: 8px 16px; /* 补充了间距,确保按钮有厚度 */
|
padding: 10px 20px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 6px; /* 稍微圆润一点,更现代 */
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.1s ease-in-out;
|
transition: all 0.2s ease; /* 稍微拉长过渡,显得更平滑 */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
/* 基础阴影 */
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2. 统一交互反馈:悬停变深,点下缩放 */
|
/* 2. 统一交互反馈:悬停增强阴影,移除缩放 */
|
||||||
.btn-interect:hover, .btn-save:hover, .btn-cancel:hover {
|
.btn-interect:hover, .btn-save:hover, .btn-cancel:hover {
|
||||||
filter: brightness(0.9); /* 自动变深,无需指定具体颜色 */
|
filter: brightness(1.05); /* 悬停微亮 */
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); /* 悬停时投影加深,产生“浮起”感 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移除 active 缩放,改为颜色反馈 */
|
||||||
.btn-interect:active, .btn-save:active, .btn-cancel:active {
|
.btn-interect:active, .btn-save:active, .btn-cancel:active {
|
||||||
transform: scale(0.96);
|
filter: brightness(0.95); /* 按下稍微变暗 */
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: none; /* 明确去掉缩放 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-save, .btn-cancel {
|
.btn-save, .btn-cancel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 3. 颜色方案优化 */
|
||||||
.btn-interect, .btn-save {
|
.btn-interect, .btn-save {
|
||||||
background: #1677ff;
|
background: #1677ff;
|
||||||
color: white;
|
color: white;
|
||||||
|
border: 1px solid #0958d9; /* 增加深色边框增强质感 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-cancel {
|
.btn-cancel {
|
||||||
background: #f5f5f5;
|
background: #ffffff; /* 改为白色底,配合阴影更好看 */
|
||||||
color: #333;
|
color: #4b5563;
|
||||||
|
border: 1px solid #e5e7eb; /* 浅灰色边框 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 补充:调整按钮组的间距,让阴影不被遮挡 */
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除所有按钮默认的 outline */
|
||||||
|
.btn-interect:focus,
|
||||||
|
.btn-save:focus,
|
||||||
|
.btn-cancel:focus,
|
||||||
|
.close-btn:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 蓝色按钮的焦点效果 (Save / Interect) */
|
||||||
|
.btn-interect:focus-visible,
|
||||||
|
.btn-save:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.3); /* 柔和的蓝色光晕 */
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 灰色按钮的焦点效果 (Cancel) */
|
||||||
|
.btn-cancel:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05); /* 极浅的灰色光晕 */
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关闭按钮的焦点效果 */
|
||||||
|
.close-btn:focus-visible {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@ -2,8 +2,8 @@ import { useState, ReactNode } from 'react';
|
|||||||
import { NodeData, Settings } from '../types/graph';
|
import { NodeData, Settings } from '../types/graph';
|
||||||
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
|
import { generateWireGuardPrivateKey } from '../utils/wireguardConfig'
|
||||||
import './NodeEditor.css';
|
import './NodeEditor.css';
|
||||||
|
import Folder from './Folder'
|
||||||
|
import { randomBytes } from 'tweetnacl';
|
||||||
|
|
||||||
interface NodeEditorProps {
|
interface NodeEditorProps {
|
||||||
node: NodeData;
|
node: NodeData;
|
||||||
@ -18,6 +18,7 @@ export default function NodeEditor({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onClose
|
onClose
|
||||||
}: NodeEditorProps): ReactNode {
|
}: NodeEditorProps): ReactNode {
|
||||||
|
|
||||||
const [formData, setFormData] = useState<NodeData>(node);
|
const [formData, setFormData] = useState<NodeData>(node);
|
||||||
const [errors, setErrors] = useState<string[]>([]);
|
const [errors, setErrors] = useState<string[]>([]);
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ export default function NodeEditor({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = (): void => {
|
const handleSave = (): void => {
|
||||||
|
setErrors(["123"])
|
||||||
// const validation = validateNodeConfig(formData);
|
// const validation = validateNodeConfig(formData);
|
||||||
// if (!validation.isValid) {
|
// if (!validation.isValid) {
|
||||||
// setErrors(validation.errors);
|
// setErrors(validation.errors);
|
||||||
@ -54,6 +56,12 @@ export default function NodeEditor({
|
|||||||
|
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<div className="error-box">
|
<div className="error-box">
|
||||||
|
<div
|
||||||
|
className="error-close-btn"
|
||||||
|
onClick={() => setErrors([])} // 点击清空错误数组
|
||||||
|
title="关闭提示"
|
||||||
|
>×</div>
|
||||||
|
|
||||||
{errors.map((error, idx) => (
|
{errors.map((error, idx) => (
|
||||||
<p key={idx} className="error-message">• {error}</p>
|
<p key={idx} className="error-message">• {error}</p>
|
||||||
))}
|
))}
|
||||||
@ -81,95 +89,100 @@ export default function NodeEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
{settings.ipv4Subnet && (
|
||||||
<label>主机ID</label>
|
<div className="form-group">
|
||||||
<input
|
<label>IPv4地址</label>
|
||||||
type="number"
|
<input
|
||||||
min="0"
|
type="text"
|
||||||
max="255"
|
value={formData.ipv4Address || ''}
|
||||||
step="1"
|
onChange={(e) => handleInputChange('ipv4Address', e.target.value)}
|
||||||
value={formData.hostId}
|
/>
|
||||||
onChange={(e) => handleInputChange('hostId', e.target.value)}
|
</div>
|
||||||
placeholder="同一子网ID下主机ID不能重复,不得超过255"
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
{settings.ipv6Subnet && (
|
||||||
<label>子网ID</label>
|
<div className="form-group">
|
||||||
<input
|
<label>IPv6地址</label>
|
||||||
type="number"
|
<input
|
||||||
min="0"
|
type="text"
|
||||||
max="255"
|
value={formData.ipv6Address || ''}
|
||||||
step="1"
|
onChange={(e) => handleInputChange('ipv6Address', e.target.value)}
|
||||||
value={formData.groupId}
|
/>
|
||||||
onChange={(e) => handleInputChange('groupId', e.target.value)}
|
</div>
|
||||||
placeholder="不得超过255"
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* host options */}
|
<Folder title='高级'>
|
||||||
|
<div className="form-group">
|
||||||
|
<label>子网黑名单</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.disallowIPs || ''}
|
||||||
|
onChange={(e) => handleInputChange('disallowIPs', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>PostUp (可选)</label>
|
<label>侦听端口</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="number"
|
||||||
value={formData.postUp || ''}
|
min="1024"
|
||||||
onChange={(e) => handleInputChange('postUp', e.target.value)}
|
max="49151"
|
||||||
/>
|
step="1"
|
||||||
</div>
|
value={formData.listenPort || ''}
|
||||||
|
onChange={(e) => handleInputChange('listenPort', e.target.value)}
|
||||||
|
placeholder={`默认值:${settings.listenPort}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>PostDown (可选)</label>
|
<label>mtu</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="number"
|
||||||
value={formData.postDown || ''}
|
min="1"
|
||||||
onChange={(e) => handleInputChange('postDown', e.target.value)}
|
step="1"
|
||||||
/>
|
value={formData.mtu || ''}
|
||||||
</div>
|
onChange={(e) => handleInputChange('mtu', e.target.value)}
|
||||||
|
placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>mtu (可选)</label>
|
<label>DNS服务器</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="text"
|
||||||
min="1"
|
value={formData.dnsServers || ''}
|
||||||
step="1"
|
onChange={(e) => handleInputChange('dnsServers', e.target.value)}
|
||||||
value={formData.mtu || ''}
|
placeholder="例如: 8.8.8.8,1.1.1.1"
|
||||||
onChange={(e) => handleInputChange('mtu', e.target.value)}
|
/>
|
||||||
placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
<div className="form-group">
|
||||||
|
<label>PostUp</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
value={formData.postUp || ''}
|
||||||
|
onChange={(e) => handleInputChange('postUp', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>侦听端口 (可选)</label>
|
<label>PostDown</label>
|
||||||
<input
|
<textarea
|
||||||
type="number"
|
rows={2}
|
||||||
min="1024"
|
value={formData.postDown || ''}
|
||||||
max="49151"
|
onChange={(e) => handleInputChange('postDown', e.target.value)}
|
||||||
step="1"
|
/>
|
||||||
value={formData.listenPort || ''}
|
</div>
|
||||||
onChange={(e) => handleInputChange('listenPort', e.target.value)}
|
|
||||||
placeholder={`默认值:${settings.listenPort}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>DNS服务器 (可选)</label>
|
<label>备注</label>
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
rows={4}
|
||||||
value={formData.dnsServers || ''}
|
value={formData.notes || ''}
|
||||||
onChange={(e) => handleInputChange('dnsServers', e.target.value)}
|
onChange={(e) => handleInputChange('notes', e.target.value)}
|
||||||
placeholder="例如: 8.8.8.8,1.1.1.1"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</Folder>
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label>备注 (可选)</label>
|
|
||||||
<textarea
|
|
||||||
rows={3}
|
|
||||||
value={formData.notes || ''}
|
|
||||||
onChange={(e) => handleInputChange('notes', e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="editor-actions">
|
<div className="editor-actions">
|
||||||
<button className="btn-save" onClick={handleSave}>保存</button>
|
<button className="btn-save" onClick={handleSave}>保存</button>
|
||||||
|
|||||||
@ -4,16 +4,8 @@ export type AppNode = Node<NodeData>;
|
|||||||
|
|
||||||
export type AppEdge = Edge<EdgeData>;
|
export type AppEdge = Edge<EdgeData>;
|
||||||
|
|
||||||
export class SubNetRouter {
|
|
||||||
private _nodes : Record<string, string | undefined> = {};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public subnet: string,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeData = {
|
export type NodeData = {
|
||||||
id: string;
|
readonly id: string;
|
||||||
label: string;
|
label: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
ipv4Address?: string;
|
ipv4Address?: string;
|
||||||
@ -28,18 +20,27 @@ export type NodeData = {
|
|||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EdgeData = {
|
||||||
|
isTwoWayEdge: boolean;
|
||||||
|
persistentKeepalive?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubNetRouter {
|
||||||
|
private _nodes : Record<string, string | undefined> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public subnet: string,
|
||||||
|
public readonly kind: 'ipv4' | 'ipv6'
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
listenPort: number;
|
listenPort: number;
|
||||||
mtu: number;
|
mtu: number;
|
||||||
|
|
||||||
ipv4Subnet?: string;
|
ipv4Subnet?: string;
|
||||||
ipv4SubnetRouter?: SubNetRouter;
|
ipv4SubNetRouter?: SubNetRouter;
|
||||||
|
|
||||||
ipv6Subnet?: string;
|
ipv6Subnet?: string;
|
||||||
ipv6SubnetRouter?: SubNetRouter;
|
ipv6SubnetRouter?: SubNetRouter;
|
||||||
}
|
|
||||||
|
|
||||||
export type EdgeData = {
|
|
||||||
isTwoWayEdge: boolean;
|
|
||||||
persistentKeepalive?: string;
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user