Files
KLALB-dashboard/src/pages/settings.tsx
T
SerinaNya 89474b9e91 feat(dashboard)!: add routing table and split endpoint settings
- Add #/routing-table with one-second polling and route filters
- Separate auto-connect targets from local external endpoints
- Remove the obsolete interfaces navigation module

BREAKING CHANGE: #/interfaces is removed.
2026-08-27 23:19:39 +08:00

1298 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import {
Settings,
Server,
Network,
Cpu,
Save,
RotateCcw,
AlertCircle,
Dice5,
Plus,
Trash2,
Layers,
} from 'lucide-react'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import { Slider } from '@/components/ui/slider'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/components/ui/tabs'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from '@/components/ui/field'
import { toast } from '@/components/ui/toast'
import { useKlalbConfig } from '@/hooks/use-klalb-config'
import type { KLALBControllerConfig } from '@/types/api'
const LANGUAGE_ITEMS = [
{ value: 'ZH_CN', label: '简体中文 (ZH_CN)' },
{ value: 'EN_US', label: 'English (EN_US)' },
]
const CONGESTION_ITEMS = [
{ value: 'BBR', label: 'BBR (高吞吐低延迟,推荐)' },
{ value: 'Vegas2', label: 'Vegas2 (基于延迟的平滑算法)' },
{ value: 'DCTCP', label: 'DCTCP (数据中心低时延拥塞控制)' },
]
const PERFORMANCE_ITEMS = [
{ value: 'multifill', label: '多核按顺序填充 (multifill - 推荐通用)' },
{ value: 'multiscatter', label: '多核均匀打散 (multiscatter - 适合多路NUMA)' },
{ value: 'singlecore', label: '单核节能优先 (singlecore - 低功耗/云机)' },
]
// Random IPv6 generator compatible with KLALB format
function generateRandomIPv6(): string {
const hex = () => Math.floor(Math.random() * 0x10000).toString(16)
return `2486:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}:${hex()}`
}
// Helper to safely extract address string whether it's a string or an object with host/port
function toAddrString(item: unknown): string {
if (!item) return ''
if (typeof item === 'string') return item
if (typeof item === 'object' && item !== null) {
const obj = item as Record<string, unknown>
if (obj.host && obj.port) {
const proto = obj.protocol ? `${obj.protocol}://` : ''
return `${proto}${obj.host}:${obj.port}`
}
if (obj.name && typeof obj.name === 'string') return obj.name
if (obj.address && typeof obj.address === 'string') return obj.address
}
return String(item)
}
function SettingsForm({
initialConfig,
onSave,
onReset,
isSaving,
}: {
initialConfig: KLALBControllerConfig
onSave: (config: KLALBControllerConfig) => Promise<void>
onReset: () => void
isSaving: boolean
}) {
const [form, setForm] = useState<KLALBControllerConfig>(() => ({
...initialConfig,
TCPListen: toAddrString(initialConfig.TCPListen),
UDPListen: toAddrString(initialConfig.UDPListen),
webListen:
initialConfig.webListen ??
(initialConfig.webPort ? `http://0.0.0.0:${initialConfig.webPort}` : 'http://0.0.0.0:4665'),
}))
const [tunEnabled, setTunEnabled] = useState<boolean>(() =>
initialConfig.enableTUN ??
(initialConfig.TUNName !== null &&
initialConfig.TUNName !== undefined &&
initialConfig.TUNName.trim() !== '')
)
const [autoConnections, setAutoConnections] = useState<string[]>(() =>
(
Array.isArray(initialConfig.autoConnections)
? initialConfig.autoConnections
: Array.isArray(initialConfig.ConnectLineTable)
? initialConfig.ConnectLineTable
: []
).map(toAddrString)
)
const [externalEndpoints, setExternalEndpoints] = useState<string[]>(() =>
(
Array.isArray(initialConfig.externalEndpoints)
? initialConfig.externalEndpoints
: Array.isArray(initialConfig.openConnections)
? initialConfig.openConnections
: Array.isArray(initialConfig.LineTable)
? initialConfig.LineTable
: []
).map(toAddrString)
)
const [ntpServers, setNtpServers] = useState<string[]>(() =>
(
Array.isArray(initialConfig.ntpServers)
? initialConfig.ntpServers
: Array.isArray(initialConfig.ntpServerTable)
? initialConfig.ntpServerTable
: []
).map(toAddrString)
)
const [dnsList, setDnsList] = useState<string[]>(() =>
(Array.isArray(initialConfig.DNS) ? initialConfig.DNS : []).map(toAddrString)
)
const [extraRoutes, setExtraRoutes] = useState<string[]>(() =>
(Array.isArray(initialConfig.ExtraRoutes) ? initialConfig.ExtraRoutes : []).map(toAddrString)
)
const [interfaceExcepts, setInterfaceExcepts] = useState<string[]>(() =>
(
Array.isArray(initialConfig.NetworkInterfaceExcepts)
? initialConfig.NetworkInterfaceExcepts
: []
).map(toAddrString)
)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const updatedAutoConnections = autoConnections
.filter((address) => address.trim() !== '')
.map((address) => address.trim())
const updatedExternalEndpoints = externalEndpoints
.filter((address) => address.trim() !== '')
.map((address) => address.trim())
// 保存时同时携带新键名与历史别名,兼容新旧后端
const denyQuery =
form.denyExternalEndpointQuery ?? form.denyConnectionQuery ?? form.denyLineTableQuery ?? false
const denyBroadcast =
form.denyExternalEndpointBroadcast ??
form.denyConnectionBroadcast ??
form.denyLineTableBroadcast ??
false
const webListen = form.webListen?.trim() || 'http://0.0.0.0:4665'
let webPort = 4665
try {
const parsedPort = new URL(webListen).port
if (parsedPort) webPort = parseInt(parsedPort, 10)
} catch {
// Java backend validates the canonical listen address.
}
const updatedConfig: Record<string, unknown> = {
...form,
webListen,
webPort,
externalEndpoints: updatedExternalEndpoints,
openConnections: updatedExternalEndpoints,
autoConnections: updatedAutoConnections,
LineTable: updatedExternalEndpoints,
ConnectLineTable: updatedAutoConnections,
ntpServers: ntpServers.filter((s) => s.trim() !== ''),
ntpServerTable: ntpServers.filter((s) => s.trim() !== ''),
DNS: dnsList.filter((s) => s.trim() !== ''),
ExtraRoutes: extraRoutes.filter((s) => s.trim() !== ''),
NetworkInterfaceExcepts: interfaceExcepts.filter((s) => s.trim() !== ''),
enableTUN: tunEnabled,
TUNName: form.TUNName?.trim() || 'KLALB_SRv6',
denyExternalEndpointQuery: denyQuery,
denyExternalEndpointBroadcast: denyBroadcast,
denyConnectionQuery: denyQuery,
denyConnectionBroadcast: denyBroadcast,
Type: 'KLALBController',
}
await onSave(updatedConfig as KLALBControllerConfig)
}
// --- Generic Dynamic List Handlers ---
const addListItem = (setter: React.Dispatch<React.SetStateAction<string[]>>) => {
setter((prev) => [...prev, ''])
}
const updateListItem = (
index: number,
value: string,
setter: React.Dispatch<React.SetStateAction<string[]>>
) => {
setter((prev) => {
const next = [...prev]
next[index] = value
return next
})
}
const removeListItem = (
index: number,
setter: React.Dispatch<React.SetStateAction<string[]>>
) => {
setter((prev) => prev.filter((_, i) => i !== index))
}
return (
<form onSubmit={handleSubmit} className="flex flex-1 flex-col gap-6 p-6">
{/* Header Banner */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-xl font-bold tracking-tight">系统配置</h1>
<p className="text-xs text-muted-foreground">
管理 KLALB SRv6 路由控制核心、网络寻址、连接与性能调度参数
</p>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={onReset}
disabled={isSaving}
>
<RotateCcw data-icon="inline-start" />
重置
</Button>
<Button type="submit" size="sm" disabled={isSaving}>
<Save data-icon="inline-start" />
{isSaving ? '正在保存...' : '保存配置'}
</Button>
</div>
</div>
{/* Main Tabs Container */}
<Tabs defaultValue="general" className="flex flex-col gap-6">
<TabsList className="w-full justify-start border-b rounded-none bg-transparent p-0 h-auto">
<TabsTrigger
value="general"
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
>
<Server className="size-4 mr-2" />
常规与设备
</TabsTrigger>
<TabsTrigger
value="network"
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
>
<Network className="size-4 mr-2" />
网络与寻址
</TabsTrigger>
<TabsTrigger
value="lists"
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
>
<Layers className="size-4 mr-2" />
连接与同步列表
</TabsTrigger>
<TabsTrigger
value="performance"
className="rounded-none border-b-2 border-transparent py-2.5 px-4 font-medium data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:shadow-none"
>
<Cpu className="size-4 mr-2" />
性能与流控
</TabsTrigger>
</TabsList>
{/* Tab 1: 常规与设备 (General) */}
<TabsContent value="general" className="flex flex-col gap-6 m-0">
<Card className="shadow-2xs">
<CardHeader>
<CardTitle className="text-base">节点基本身份</CardTitle>
<CardDescription>
设置该节点在 KLALB 全网拓扑中呈现的设备标识与语言偏好
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Field>
<FieldLabel htmlFor="deviceName">设备名称</FieldLabel>
<Input
id="deviceName"
placeholder="如: Edge-Router-01"
value={form.DeviceName || ''}
onChange={(e) =>
setForm((prev) => ({ ...prev, DeviceName: e.target.value }))
}
/>
<FieldDescription>全网路由广播与拓扑图中显示的节点名称</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="language">系统语言</FieldLabel>
<Select
items={LANGUAGE_ITEMS}
value={form.language || 'ZH_CN'}
onValueChange={(val) =>
setForm((prev) => ({ ...prev, language: val || undefined }))
}
>
<SelectTrigger id="language">
<SelectValue placeholder="选择语言" />
</SelectTrigger>
<SelectContent>
{LANGUAGE_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldDescription>本地界面与日志消息的默认语言</FieldDescription>
</Field>
<Field className="md:col-span-2">
<FieldLabel htmlFor="deviceDescription">设备描述</FieldLabel>
<Textarea
id="deviceDescription"
placeholder={'如: 机房 A 区多线聚合网关\n支持多行输入'}
className="min-h-20"
value={form.DeviceDescription || ''}
onChange={(e) =>
setForm((prev) => ({ ...prev, DeviceDescription: e.target.value }))
}
/>
<FieldDescription>本地备注说明信息,支持多行,不参与网络路由决策</FieldDescription>
</Field>
</FieldGroup>
</CardContent>
</Card>
<Card className="shadow-2xs">
<CardHeader>
<CardTitle className="text-base">服务与虚拟网卡</CardTitle>
<CardDescription>
配置 TUN 虚拟网络适配器及 Web API 服务的运行参数
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Field
orientation="horizontal"
className="flex items-center justify-between rounded-lg border p-4 md:col-span-2"
>
<FieldContent>
<FieldLabel htmlFor="switch-tun-enable">
启用 TUN 虚拟网卡
</FieldLabel>
<FieldDescription>
开启后创建系统级虚拟网卡,将虚拟 IPv6/SRv6 流量注入系统内核协议栈
</FieldDescription>
</FieldContent>
<Switch
id="switch-tun-enable"
checked={tunEnabled}
onCheckedChange={(checked) => {
setTunEnabled(checked)
if (
checked &&
(!form.TUNName || form.TUNName.trim() === '')
) {
setForm((prev) => ({ ...prev, TUNName: 'KLALB_SRv6' }))
}
}}
/>
</Field>
{!tunEnabled && (
<div className="md:col-span-2">
<Alert className="border-amber-200 bg-amber-50 text-amber-900 shadow-2xs dark:border-amber-900 dark:bg-amber-950 dark:text-amber-50">
<AlertCircle className="size-4 text-amber-600 dark:text-amber-400" />
<AlertTitle className="font-semibold">
TUN 虚拟网卡已禁用
</AlertTitle>
<AlertDescription className="text-amber-800 dark:text-amber-200">
系统将以无 TUN 适配器模式运行,仅支持链路转发与端口桥接,本端虚拟 IPv6 地址将无法被操作系统内核直接路由访问。
</AlertDescription>
</Alert>
</div>
)}
{tunEnabled && (
<Field className="md:col-span-2">
<FieldLabel htmlFor="tunName">
TUN 虚拟网卡设备名
</FieldLabel>
<Input
id="tunName"
placeholder="默认: KLALB_SRv6"
value={form.TUNName || ''}
onChange={(e) =>
setForm((prev) => ({
...prev,
TUNName: e.target.value,
}))
}
/>
<FieldDescription>
创建的虚拟网卡适配器名称(如 Windows 上的 Wintun 适配器名,默认 KLALB_SRv6
</FieldDescription>
</Field>
)}
<Field>
<FieldLabel htmlFor="webListen">Web API 监听地址</FieldLabel>
<Input
id="webListen"
type="text"
placeholder="http://0.0.0.0:4665"
value={form.webListen ?? 'http://0.0.0.0:4665'}
onChange={(e) =>
setForm((prev) => ({
...prev,
webListen: e.target.value,
}))
}
/>
<FieldDescription>
Web API 与前端仪表盘托管监听地址(默认 http://0.0.0.0:4665
</FieldDescription>
</Field>
<Field
orientation="horizontal"
className="flex items-center justify-between rounded-lg border p-4"
>
<FieldContent>
<FieldLabel htmlFor="switch-nogui"> GUI 模式</FieldLabel>
<FieldDescription>
启动时跳过 Java Swing 图形窗口,仅通过控制台与 Web Dashboard 运行
</FieldDescription>
</FieldContent>
<Switch
id="switch-nogui"
checked={form.nogui ?? false}
onCheckedChange={(checked) =>
setForm((prev) => ({ ...prev, nogui: checked }))
}
/>
</Field>
</FieldGroup>
</CardContent>
</Card>
</TabsContent>
{/* Tab 2: 网络与寻址 (Network) */}
<TabsContent value="network" className="m-0 flex flex-col gap-6">
<Card className="shadow-2xs">
<CardHeader>
<CardTitle className="text-base">SRv6 虚拟网络寻址</CardTitle>
<CardDescription>
配置本节点在虚拟 SRv6 网络中的专属 IPv6 虚拟地址 (SID) AS 自治域号
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Field className="md:col-span-2">
<FieldLabel htmlFor="virtualAddress">
本节点虚拟 IPv6 地址
</FieldLabel>
<div className="flex items-center gap-2">
<Input
id="virtualAddress"
className="font-mono"
placeholder="如: 2486:1:0:0:0:0:0:8889"
value={form.VirtualAddress || ''}
onChange={(e) =>
setForm((prev) => ({
...prev,
VirtualAddress: e.target.value,
}))
}
/>
<Button
type="button"
variant="outline"
onClick={() =>
setForm((prev) => ({
...prev,
VirtualAddress: generateRandomIPv6(),
}))
}
title="随机生成私有 SRv6 专用地址"
>
<Dice5 data-icon="inline-start" />
随机生成
</Button>
</div>
<FieldDescription>
本端 SRv6 报文路由目的地址(/128 主机路由)。
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="virtualASN">
虚拟自治系统编号
</FieldLabel>
<Input
id="virtualASN"
type="number"
placeholder="如: 2142606939373348329"
value={form.VirtualASN ?? ''}
onChange={(e) =>
setForm((prev) => ({
...prev,
VirtualASN: e.target.value
? parseInt(e.target.value)
: undefined,
}))
}
/>
<FieldDescription>
虚拟网络 BGP/SRv6 AS 编号
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="virtualSocketName">
虚拟套接字协议
</FieldLabel>
<Input
id="virtualSocketName"
placeholder="kltp"
value={form.VirtualSocketName || 'kltp'}
onChange={(e) =>
setForm((prev) => ({
...prev,
VirtualSocketName: e.target.value,
}))
}
/>
<FieldDescription>
用于虚拟数据包传输的可靠协议栈名称(默认 kltp
</FieldDescription>
</Field>
</FieldGroup>
</CardContent>
</Card>
<Card className="shadow-2xs">
<CardHeader>
<CardTitle className="text-base">本地监听端点</CardTitle>
<CardDescription>
配置节点在物理网络上用于接收其他节点连接的 TCP / UDP 监听 URI
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Field>
<FieldLabel htmlFor="tcpListen">TCP 监听地址</FieldLabel>
<Input
id="tcpListen"
className="font-mono text-xs"
placeholder="tcp://0.0.0.0:4565"
value={form.TCPListen || ''}
onChange={(e) =>
setForm((prev) => ({
...prev,
TCPListen: e.target.value,
}))
}
/>
<FieldDescription>
接收外部 TCP 链路握手的监听 URI
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="udpListen">UDP 监听地址</FieldLabel>
<Input
id="udpListen"
className="font-mono text-xs"
placeholder="udp://0.0.0.0:4572"
value={form.UDPListen || ''}
onChange={(e) =>
setForm((prev) => ({
...prev,
UDPListen: e.target.value,
}))
}
/>
<FieldDescription>
接收外部 UDP/KLTP 链路报文的监听 URI
</FieldDescription>
</Field>
</FieldGroup>
</CardContent>
</Card>
</TabsContent>
{/* Tab 3: 连接与同步列表 (Lists) */}
<TabsContent value="lists" className="m-0 flex flex-col gap-6">
{/* 1. Automatic Connection Endpoints */}
<Card className="shadow-2xs">
<CardHeader className="flex flex-row items-center justify-between pb-3">
<div>
<CardTitle className="text-base">与以下端点自动建立连接</CardTitle>
<CardDescription>本节点启动时主动连接的远端端点</CardDescription>
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addListItem(setAutoConnections)}
>
<Plus data-icon="inline-start" />
添加端点
</Button>
</CardHeader>
<CardContent>
<FieldGroup className="gap-2">
{autoConnections.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
未配置自动连接端点
</div>
) : (
autoConnections.map((endpoint, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
className="flex-1 font-mono text-xs"
placeholder="tcp://kne01.yoyo250.fun:4565 或 udp://..."
value={endpoint}
onChange={(e) =>
updateListItem(idx, e.target.value, setAutoConnections)
}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-destructive hover:text-destructive"
onClick={() => removeListItem(idx, setAutoConnections)}
title="删除该端点"
>
<Trash2 className="size-3.5" />
</Button>
</div>
))
)}
</FieldGroup>
</CardContent>
</Card>
{/* 2. Published External Endpoints */}
<Card className="shadow-2xs">
<CardHeader className="flex flex-row items-center justify-between pb-3">
<div>
<CardTitle className="text-base">本机外部端点</CardTitle>
<CardDescription>
本节点对外发布且可被其他节点访问的端点,不是远端连接目标
</CardDescription>
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addListItem(setExternalEndpoints)}
>
<Plus data-icon="inline-start" />
添加端点
</Button>
</CardHeader>
<CardContent>
<FieldGroup className="gap-2">
{externalEndpoints.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
未配置本机外部端点
</div>
) : (
externalEndpoints.map((endpoint, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
className="flex-1 font-mono text-xs"
placeholder="tcp://本机公网地址:4565 或 udp://..."
value={endpoint}
onChange={(e) =>
updateListItem(idx, e.target.value, setExternalEndpoints)
}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-destructive hover:text-destructive"
onClick={() => removeListItem(idx, setExternalEndpoints)}
title="删除该端点"
>
<Trash2 className="size-3.5" />
</Button>
</div>
))
)}
</FieldGroup>
</CardContent>
</Card>
{/* 3. NTP Server List */}
<Card className="shadow-2xs">
<CardHeader className="flex flex-row items-center justify-between pb-3">
<div>
<CardTitle className="text-base">
NTP 授时服务器列表
</CardTitle>
<CardDescription>
高精度分布式时钟同步参考源列表 ( ntp://ntp1.aliyun.com)
</CardDescription>
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addListItem(setNtpServers)}
>
<Plus data-icon="inline-start" />
添加 NTP
</Button>
</CardHeader>
<CardContent>
<FieldGroup className="gap-2">
{ntpServers.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
未配置 NTP 服务器,系统将使用本机系统时间
</div>
) : (
ntpServers.map((server, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
className="flex-1 font-mono text-xs"
placeholder="ntp://ntp1.aliyun.com"
value={server}
onChange={(e) =>
updateListItem(idx, e.target.value, setNtpServers)
}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-destructive hover:text-destructive"
onClick={() => removeListItem(idx, setNtpServers)}
>
<Trash2 className="size-3.5" />
</Button>
</div>
))
)}
</FieldGroup>
</CardContent>
</Card>
{/* 3. DNS, Extra Routes & Network Interface Exceptions in 3 Columns / Grid */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{/* DNS List */}
<Card className="shadow-2xs">
<CardHeader className="flex flex-row items-center justify-between pb-3">
<div>
<CardTitle className="text-base">
DNS 服务器列表
</CardTitle>
<CardDescription>
虚拟网络递归 DNS 解析服务器
</CardDescription>
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addListItem(setDnsList)}
>
<Plus data-icon="inline-start" />
添加
</Button>
</CardHeader>
<CardContent>
<FieldGroup className="gap-2">
{dnsList.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
未配置专用 DNS 服务器
</div>
) : (
dnsList.map((dns, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
className="flex-1 font-mono text-xs"
placeholder="2486:1::8888 或 8.8.8.8"
value={dns}
onChange={(e) =>
updateListItem(idx, e.target.value, setDnsList)
}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-destructive hover:text-destructive"
onClick={() => removeListItem(idx, setDnsList)}
>
<Trash2 className="size-3.5" />
</Button>
</div>
))
)}
</FieldGroup>
</CardContent>
</Card>
{/* Extra Routes */}
<Card className="shadow-2xs">
<CardHeader className="flex flex-row items-center justify-between pb-3">
<div>
<CardTitle className="text-base">
额外路由列表
</CardTitle>
<CardDescription>
本地控制器附加静态路由规则
</CardDescription>
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addListItem(setExtraRoutes)}
>
<Plus data-icon="inline-start" />
添加
</Button>
</CardHeader>
<CardContent>
<FieldGroup className="gap-2">
{extraRoutes.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
未配置额外静态路由
</div>
) : (
extraRoutes.map((route, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
className="flex-1 font-mono text-xs"
placeholder="如: 2486:2::/64"
value={route}
onChange={(e) =>
updateListItem(idx, e.target.value, setExtraRoutes)
}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-destructive hover:text-destructive"
onClick={() => removeListItem(idx, setExtraRoutes)}
>
<Trash2 className="size-3.5" />
</Button>
</div>
))
)}
</FieldGroup>
</CardContent>
</Card>
{/* Network Interface Exceptions */}
<Card className="shadow-2xs">
<CardHeader className="flex flex-row items-center justify-between pb-3">
<div>
<CardTitle className="text-base">
网卡排除列表
</CardTitle>
<CardDescription>
指定不绑定的物理网卡设备名
</CardDescription>
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={() => addListItem(setInterfaceExcepts)}
>
<Plus data-icon="inline-start" />
添加
</Button>
</CardHeader>
<CardContent>
<FieldGroup className="gap-2">
{interfaceExcepts.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
未配置排除项,系统将使用全部可用物理接口
</div>
) : (
interfaceExcepts.map((iface, idx) => (
<div key={idx} className="flex items-center gap-2">
<Input
className="flex-1 font-mono text-xs"
placeholder="如: eth0 或 WLAN"
value={iface}
onChange={(e) =>
updateListItem(
idx,
e.target.value,
setInterfaceExcepts
)
}
/>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-destructive hover:text-destructive"
onClick={() =>
removeListItem(idx, setInterfaceExcepts)
}
>
<Trash2 className="size-3.5" />
</Button>
</div>
))
)}
</FieldGroup>
</CardContent>
</Card>
</div>
{/* Security & Broadcast Flags */}
<Card className="shadow-2xs">
<CardHeader>
<CardTitle className="text-base">安全与广播开关</CardTitle>
<CardDescription>
控制连接信息的外部探测查询与全网广播行为
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Field
orientation="horizontal"
className="flex items-center justify-between rounded-lg border p-3"
>
<FieldContent>
<FieldLabel htmlFor="switch-deny-query">禁用地址查询</FieldLabel>
<FieldDescription>
禁止远端节点主动探测查询本机的连接信息
</FieldDescription>
</FieldContent>
<Switch
id="switch-deny-query"
checked={
form.denyExternalEndpointQuery ??
form.denyConnectionQuery ??
form.denyLineTableQuery ??
false
}
onCheckedChange={(checked) =>
setForm((prev) => ({
...prev,
denyExternalEndpointQuery: checked,
denyConnectionQuery: checked,
denyLineTableQuery: checked,
}))
}
/>
</Field>
<Field
orientation="horizontal"
className="flex items-center justify-between rounded-lg border p-3"
>
<FieldContent>
<FieldLabel htmlFor="switch-deny-broadcast">禁用地址广播</FieldLabel>
<FieldDescription>
禁止向全网周期性广播本机的连接信息
</FieldDescription>
</FieldContent>
<Switch
id="switch-deny-broadcast"
checked={
form.denyExternalEndpointBroadcast ??
form.denyConnectionBroadcast ??
form.denyLineTableBroadcast ??
false
}
onCheckedChange={(checked) =>
setForm((prev) => ({
...prev,
denyExternalEndpointBroadcast: checked,
denyConnectionBroadcast: checked,
denyLineTableBroadcast: checked,
}))
}
/>
</Field>
</FieldGroup>
</CardContent>
</Card>
</TabsContent>
{/* Tab 4: 性能与流控 (Performance) */}
<TabsContent value="performance" className="m-0 flex flex-col gap-6">
<Card className="shadow-2xs">
<CardHeader>
<CardTitle className="text-base">算法与调度策略</CardTitle>
<CardDescription>
设置传输层拥塞控制算法及多核 CPU 负载均衡策略
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-3">
<Field>
<FieldLabel htmlFor="congestion">拥塞控制算法</FieldLabel>
<Select
items={CONGESTION_ITEMS}
value={form.congestionAlgorithm || 'BBR'}
onValueChange={(val) =>
setForm((prev) => ({
...prev,
congestionAlgorithm: val || undefined,
}))
}
>
<SelectTrigger id="congestion">
<SelectValue placeholder="选择算法" />
</SelectTrigger>
<SelectContent>
{CONGESTION_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldDescription>
WAN 链路报文传输的流量与拥塞控制引擎
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="perfStrategy">
性能调度策略
</FieldLabel>
<Select
items={PERFORMANCE_ITEMS}
value={form.performanceStrategy || 'multifill'}
onValueChange={(val) =>
setForm((prev) => ({
...prev,
performanceStrategy: val || undefined,
}))
}
>
<SelectTrigger id="perfStrategy">
<SelectValue placeholder="选择策略" />
</SelectTrigger>
<SelectContent>
{PERFORMANCE_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldDescription>
多核 CPU 上的转发线程负载分配模型
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="linkConns">
单链路连接流数
</FieldLabel>
<Input
id="linkConns"
type="number"
min={1}
max={10}
value={form.linkConnectionsCount ?? 1}
onChange={(e) =>
setForm((prev) => ({
...prev,
linkConnectionsCount: parseInt(e.target.value) || 1,
}))
}
/>
<FieldDescription>
单个远端物理节点建立的底层并发 TCP 流数 (1~10)
</FieldDescription>
</Field>
</FieldGroup>
</CardContent>
</Card>
<Card className="shadow-2xs">
<CardHeader>
<CardTitle className="text-base">
网络时延门限与微调参数
</CardTitle>
<CardDescription>
调整动态突发流量阈值及 Nagle 延迟合并包参数
</CardDescription>
</CardHeader>
<CardContent>
<FieldGroup className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Field>
<div className="flex items-center justify-between">
<FieldLabel htmlFor="burstLimit">
突发限制倍率
</FieldLabel>
<span className="font-mono text-xs font-semibold">
{(form.burstLimit ?? 1.5).toFixed(2)}x
</span>
</div>
<Slider
id="burstLimit"
min={1}
max={2}
step={0.05}
value={[form.burstLimit ?? 1.5]}
onValueChange={(vals) => {
const v = Array.isArray(vals) ? vals[0] : vals
setForm((prev) => ({
...prev,
burstLimit: typeof v === 'number' ? v : 1.5,
}))
}}
/>
<FieldDescription>
允许瞬时突发带宽相对于平稳速率的倍数上限 (1.0~2.0)
</FieldDescription>
</Field>
<Field>
<div className="flex items-center justify-between">
<FieldLabel htmlFor="delayUpper">
时延门限上限
</FieldLabel>
<span className="font-mono text-xs font-semibold">
{(form.delayUpperBound ?? 1.2).toFixed(2)}x
</span>
</div>
<Slider
id="delayUpper"
min={1}
max={3}
step={0.05}
value={[form.delayUpperBound ?? 1.2]}
onValueChange={(vals) => {
const v = Array.isArray(vals) ? vals[0] : vals
setForm((prev) => ({
...prev,
delayUpperBound: typeof v === 'number' ? v : 1.2,
}))
}}
/>
<FieldDescription>
判定链路发生拥塞的时延相对膨胀上限倍率
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="nagleDelay">
传输粘包等待时间
</FieldLabel>
<Input
id="nagleDelay"
type="number"
min={0}
placeholder="1000000"
value={form.nagleDelayTime ?? 1000000}
onChange={(e) =>
setForm((prev) => ({
...prev,
nagleDelayTime: parseInt(e.target.value) || 0,
}))
}
/>
<FieldDescription>
小包合并发送等待超时 (单位: 纳秒 ns1000000ns = 1ms)
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="linkNagleDelay">
链路粘包等待时间
</FieldLabel>
<Input
id="linkNagleDelay"
type="number"
min={0}
placeholder="0"
value={form.linkNagleDelayTime ?? 0}
onChange={(e) =>
setForm((prev) => ({
...prev,
linkNagleDelayTime: parseInt(e.target.value) || 0,
}))
}
/>
<FieldDescription>
物理链路发送层的聚合超时 (单位: 纳秒 ns0 为禁用)
</FieldDescription>
</Field>
</FieldGroup>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</form>
)
}
export function SettingsPage() {
const {
config,
isLoading,
isSaving,
fetchConfig,
saveConfig,
} = useKlalbConfig()
const [versionKey, setVersionKey] = useState(0)
const handleReset = () => {
fetchConfig()
setVersionKey((prev) => prev + 1)
}
const handleSave = async (updatedConfig: KLALBControllerConfig) => {
const res = await saveConfig(updatedConfig)
if (res.success) {
toast.add({
title: '配置已保存',
type: 'success',
})
setVersionKey((prev) => prev + 1)
} else {
toast.add({
title: '保存配置失败',
description: res.message || '请检查配置参数或后端服务状态',
type: 'error',
})
}
}
if (isLoading || !config) {
return (
<div className="flex flex-1 items-center justify-center p-12">
<div className="flex flex-col items-center gap-2 text-muted-foreground">
<Settings className="size-8 animate-spin text-primary" />
<span className="text-sm">正在载入系统配置...</span>
</div>
</div>
)
}
return (
<SettingsForm
key={`${versionKey}-${config.VirtualAddress ?? ''}`}
initialConfig={config}
onSave={handleSave}
onReset={handleReset}
isSaving={isSaving}
/>
)
}