Compare commits
No commits in common. "a9cbcd7823bc2b2854426f7f819f2fecee52355c" and "9e79166186bfbe4d35c6cdf73ffc2192050dd602" have entirely different histories.
a9cbcd7823
...
9e79166186
1
TODO.md
1
TODO.md
@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
- [ ] 实现子网路由功能,并验证有效
|
- [ ] 实现子网路由功能,并验证有效
|
||||||
|
|
||||||
- [ ] 使用Immer优化对象和数组state的更新
|
|
||||||
- [ ] 实现配置保存和加载功能
|
- [ ] 实现配置保存和加载功能
|
||||||
- [ ] 实现加密功能(完全加密和只加密私钥)
|
- [ ] 实现加密功能(完全加密和只加密私钥)
|
||||||
- [ ] 添加测试用例
|
- [ ] 添加测试用例
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
max-width: 240px; /* 限制节点不要太长 */
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -39,12 +38,24 @@
|
|||||||
.node-label {
|
.node-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 12px; /* smaller title font */
|
font-size: 14px;
|
||||||
flex: 0 0 100px; /* 固定宽度 100px,给按钮留更多空间 */
|
flex: 1;
|
||||||
min-width: 0; /* allow truncation inside flex */
|
word-break: break-word;
|
||||||
overflow: hidden;
|
}
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
.edit-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-info {
|
.node-info {
|
||||||
@ -52,37 +63,6 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gen-btn {
|
|
||||||
background: #1677ff;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 1px 6px; /* 更小的按钮内边距 */
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 10px; /* 更小的文字 */
|
|
||||||
line-height: 1;
|
|
||||||
height: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
opacity: 0.95;
|
|
||||||
transition: transform 120ms ease, box-shadow 120ms ease, background-color 120ms ease, opacity 120ms ease;
|
|
||||||
outline: none; /* remove default focus outline */
|
|
||||||
}
|
|
||||||
|
|
||||||
.gen-btn:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.gen-btn:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gen-btn:active {
|
|
||||||
transform: translateY(1px) scale(0.995);
|
|
||||||
background-color: #0f62d6; /* slightly darker on press */
|
|
||||||
box-shadow: inset 0 1px 2px rgba(0,0,0,0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@ -7,15 +7,10 @@ export default function CustomNode({
|
|||||||
data,
|
data,
|
||||||
selected
|
selected
|
||||||
}: NodeProps<AppNode>): ReactNode {
|
}: NodeProps<AppNode>): ReactNode {
|
||||||
const handleGenerate = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
<span className="node-label">{data.label}</span>
|
||||||
<button className="gen-btn" onClick={handleGenerate} onDoubleClick={e => e.stopPropagation()}>生成配置</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => (
|
{[Position.Top, Position.Bottom, Position.Right, Position.Left].map((position) => (
|
||||||
|
|||||||
@ -12,10 +12,16 @@ interface NodeEditorProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
|
|
||||||
|
class Validation {
|
||||||
|
constructor(
|
||||||
|
public readonly isValid: boolean,
|
||||||
|
public readonly errors: string[],
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Validate(updateData : NodeDataUpdate, settings : Settings) : Validation {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const {ipv4Address, ipv6Address, mtu, listenPort} = updateData;
|
|
||||||
const {ipv4Subnet, ipv6Subnet} = settings;
|
|
||||||
|
|
||||||
if(!updateData.label) {
|
if(!updateData.label) {
|
||||||
errors.push("Label不能是空");
|
errors.push("Label不能是空");
|
||||||
@ -25,6 +31,8 @@ function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
|
|||||||
errors.push("privateKey不能是空");
|
errors.push("privateKey不能是空");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ipv4Subnet = settings.ipv4Subnet;
|
||||||
|
const ipv4Address = updateData.ipv4Address;
|
||||||
if(ipv4Subnet) {
|
if(ipv4Subnet) {
|
||||||
const cidr = IPNetwork.parse(ipv4Subnet);
|
const cidr = IPNetwork.parse(ipv4Subnet);
|
||||||
if(!ipv4Address) {
|
if(!ipv4Address) {
|
||||||
@ -34,6 +42,8 @@ function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ipv6Subnet = settings.ipv6Subnet;
|
||||||
|
const ipv6Address = updateData.ipv6Address;
|
||||||
if(ipv6Subnet) {
|
if(ipv6Subnet) {
|
||||||
const cidr = IPNetwork.parse(ipv6Subnet);
|
const cidr = IPNetwork.parse(ipv6Subnet);
|
||||||
if(!ipv6Address) {
|
if(!ipv6Address) {
|
||||||
@ -43,23 +53,8 @@ function Validate(updateData : NodeDataUpdate, settings : Settings) : string[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(listenPort !== undefined) {
|
const isValid : boolean = errors.length === 0;
|
||||||
if(isNaN(listenPort)) {
|
return new Validation(isValid, errors);
|
||||||
errors.push("监听端口不是数字");
|
|
||||||
} else if(listenPort < 30000 || listenPort > 49151) {
|
|
||||||
errors.push("监听端口不在范围内:[30000, 49151]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mtu !== undefined) {
|
|
||||||
if(isNaN(mtu)) {
|
|
||||||
errors.push("mtu不是数字");
|
|
||||||
} else if(mtu < 1200) {
|
|
||||||
errors.push("mtu过小(小于1200)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NodeEditor({
|
export default function NodeEditor({
|
||||||
@ -99,11 +94,12 @@ export default function NodeEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const validation = Validate(updateData, settings);
|
const validation = Validate(updateData, settings);
|
||||||
setErrors(validation);
|
if(!validation.isValid) {
|
||||||
if(validation.length > 0) {
|
setErrors(validation.errors);
|
||||||
return ;
|
return ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setErrors([]);
|
||||||
onUpdate(updateData);
|
onUpdate(updateData);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@ -179,23 +175,26 @@ export default function NodeEditor({
|
|||||||
<Folder title='高级'>
|
<Folder title='高级'>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>子网黑名单</label>
|
<label>子网黑名单</label>
|
||||||
<textarea
|
<input
|
||||||
rows={2}
|
type="text"
|
||||||
value={disallowIPs || ''}
|
value={disallowIPs || ''}
|
||||||
onChange={e => setDisallowIPs(e.target.value)}
|
onChange={e => setDisallowIPs(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>监听端口</label>
|
<label>侦听端口</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="30000"
|
min="30000"
|
||||||
max="49151"
|
max="49151"
|
||||||
step="1"
|
step="1"
|
||||||
value={listenPort || ""}
|
value={listenPort || ''}
|
||||||
onChange={e => setListenPort(e.target.valueAsNumber)}
|
onChange={e => {
|
||||||
placeholder='范围:[30000, 49151]'
|
const value = e.target.valueAsNumber;
|
||||||
|
setListenPort(isNaN(value) ? undefined : value);
|
||||||
|
}}
|
||||||
|
placeholder={`默认值:${settings.listenPort}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -206,8 +205,11 @@ export default function NodeEditor({
|
|||||||
min="1200"
|
min="1200"
|
||||||
step="1"
|
step="1"
|
||||||
value={mtu || ''}
|
value={mtu || ''}
|
||||||
onChange={e => setmtu(e.target.valueAsNumber)}
|
onChange={e => {
|
||||||
placeholder='需要大于1200'
|
const value = e.target.valueAsNumber;
|
||||||
|
setmtu(isNaN(value) ? undefined : value);
|
||||||
|
}}
|
||||||
|
placeholder={settings.mtu ? `默认值:${settings.mtu}` : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -9,44 +9,6 @@ interface SettingEditorProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Validate(updateData: Settings) : string[] {
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
const {ipv4Subnet, ipv6Subnet, mtu, listenPort} = updateData;
|
|
||||||
|
|
||||||
if(ipv4Subnet) {
|
|
||||||
const result = IPNetwork.parse(ipv4Subnet)
|
|
||||||
if(!result.isValid) {
|
|
||||||
errors.push("IPv4子网:" + (result.error ?? "ipv4子网不合法"))
|
|
||||||
} else if(result.version != 'IPv4') {
|
|
||||||
errors.push("IPv4子网:" + "非IPv4 CIDR");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ipv6Subnet) {
|
|
||||||
const result = IPNetwork.parse(ipv6Subnet)
|
|
||||||
if(!result.isValid) {
|
|
||||||
errors.push("IPv6子网:" + (result.error ?? "子网不合法"));
|
|
||||||
} else if(result.version != 'IPv6') {
|
|
||||||
errors.push("IPv6子网:" + "非IPv6 CIDR");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isNaN(listenPort)) {
|
|
||||||
errors.push("监听端口不是数字");
|
|
||||||
} else if(listenPort < 30000 || listenPort > 49151) {
|
|
||||||
errors.push("监听端口不在范围内:[30000, 49151]");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isNaN(mtu)) {
|
|
||||||
errors.push("mtu不是数字");
|
|
||||||
} else if(mtu < 1200) {
|
|
||||||
errors.push("mtu过小(小于1200)");
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsEditor({
|
export default function SettingsEditor({
|
||||||
settings,
|
settings,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
@ -60,20 +22,35 @@ export default function SettingsEditor({
|
|||||||
const [ipv6Subnet, setIpv6Subnet] = useState(settings.ipv6Subnet)
|
const [ipv6Subnet, setIpv6Subnet] = useState(settings.ipv6Subnet)
|
||||||
|
|
||||||
const handleSave = (): void => {
|
const handleSave = (): void => {
|
||||||
const updateData = {
|
const errorInfo : string[] = [];
|
||||||
|
if(ipv4Subnet) {
|
||||||
|
const result = IPNetwork.parse(ipv4Subnet)
|
||||||
|
if(!result.isValid) {
|
||||||
|
errorInfo.push("IPv4子网:" + (result.error ?? "ipv4子网不合法"))
|
||||||
|
} else if(result.version != 'IPv4') {
|
||||||
|
errorInfo.push("IPv4子网:" + "非IPv4 CIDR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(ipv6Subnet) {
|
||||||
|
const result = IPNetwork.parse(ipv6Subnet)
|
||||||
|
if(!result.isValid) {
|
||||||
|
errorInfo.push("IPv6子网:" + (result.error ?? "子网不合法"));
|
||||||
|
} else if(result.version != 'IPv6') {
|
||||||
|
errorInfo.push("IPv6子网:" + "非IPv6 CIDR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(errorInfo.length > 0) {
|
||||||
|
setErrors(errorInfo);
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors([]);
|
||||||
|
onUpdate({
|
||||||
listenPort: listenPort,
|
listenPort: listenPort,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
ipv4Subnet: ipv4Subnet,
|
ipv4Subnet: ipv4Subnet,
|
||||||
ipv6Subnet: ipv6Subnet
|
ipv6Subnet: ipv6Subnet
|
||||||
};
|
});
|
||||||
|
|
||||||
const validation = Validate(updateData);
|
|
||||||
setErrors(validation);
|
|
||||||
if(validation.length > 0) {
|
|
||||||
return ;
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdate(updateData);
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,7 +77,7 @@ export default function SettingsEditor({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label>监听端口</label>
|
<label>侦听端口</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="30000"
|
min="30000"
|
||||||
@ -118,7 +95,10 @@ export default function SettingsEditor({
|
|||||||
min="1200"
|
min="1200"
|
||||||
step="1"
|
step="1"
|
||||||
value={mtu || ''}
|
value={mtu || ''}
|
||||||
onChange={e => setmtu(e.target.valueAsNumber)}
|
onChange={e => {
|
||||||
|
const value = e.target.valueAsNumber;
|
||||||
|
setmtu(isNaN(value) ? 1420 : value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Node, Edge } from '@xyflow/react';
|
import { Node, Edge } from '@xyflow/react';
|
||||||
import { createContext } from 'react';
|
|
||||||
|
|
||||||
export type AppNode = Node<NodeData>;
|
export type AppNode = Node<NodeData>;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user