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.
This commit is contained in:
2026-08-27 23:19:39 +08:00
parent ad94d2c274
commit 89474b9e91
7 changed files with 450 additions and 162 deletions
+109 -139
View File
@@ -88,55 +88,6 @@ function toAddrString(item: unknown): string {
return String(item)
}
interface ConnectionItem {
id: string
address: string
autoConnect: boolean
}
function parseConnectionsFromConfig(config: KLALBControllerConfig | null): ConnectionItem[] {
if (!config) return []
const lineTable = (
Array.isArray(config.externalEndpoints)
? config.externalEndpoints
: Array.isArray(config.openConnections)
? config.openConnections
: Array.isArray(config.LineTable)
? config.LineTable
: []
).map(toAddrString)
const connectTable = (
Array.isArray(config.autoConnections)
? config.autoConnections
: Array.isArray(config.ConnectLineTable)
? config.ConnectLineTable
: []
).map(toAddrString)
const items: ConnectionItem[] = []
// openConnections (autoConnect: false)
lineTable.forEach((addr, index) => {
if (addr && addr.trim() !== '') {
items.push({
id: `open-${index}-${addr}`,
address: addr,
autoConnect: false,
})
}
})
// autoConnections (autoConnect: true)
connectTable.forEach((addr, index) => {
if (addr && addr.trim() !== '') {
items.push({
id: `auto-${index}-${addr}`,
address: addr,
autoConnect: true,
})
}
})
return items
}
function SettingsForm({
initialConfig,
onSave,
@@ -163,8 +114,26 @@ function SettingsForm({
initialConfig.TUNName.trim() !== '')
)
const [connections, setConnections] = useState<ConnectionItem[]>(() =>
parseConnectionsFromConfig(initialConfig)
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[]>(() =>
@@ -196,14 +165,12 @@ function SettingsForm({
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const cleanConnections = connections.filter((c) => c.address.trim() !== '')
// 互斥分离:选中自动连接的进 autoConnections,未选中的进 externalEndpoints
const updatedExternalEndpoints = cleanConnections
.filter((c) => !c.autoConnect)
.map((c) => c.address.trim())
const updatedAutoConnections = cleanConnections
.filter((c) => c.autoConnect)
.map((c) => c.address.trim())
const updatedAutoConnections = autoConnections
.filter((address) => address.trim() !== '')
.map((address) => address.trim())
const updatedExternalEndpoints = externalEndpoints
.filter((address) => address.trim() !== '')
.map((address) => address.trim())
// 保存时同时携带新键名与历史别名,兼容新旧后端
const denyQuery =
@@ -248,34 +215,6 @@ function SettingsForm({
await onSave(updatedConfig as KLALBControllerConfig)
}
// --- Dynamic Connection Items Handlers ---
const addConnection = () => {
setConnections((prev) => [
...prev,
{
id: `conn-${Date.now()}`,
address: '',
autoConnect: true,
},
])
}
const updateConnection = (
index: number,
field: 'address' | 'autoConnect',
value: string | boolean
) => {
setConnections((prev) => {
const next = [...prev]
next[index] = { ...next[index], [field]: value }
return next
})
}
const removeConnection = (index: number) => {
setConnections((prev) => prev.filter((_, i) => i !== index))
}
// --- Generic Dynamic List Handlers ---
const addListItem = (setter: React.Dispatch<React.SetStateAction<string[]>>) => {
setter((prev) => [...prev, ''])
@@ -688,72 +627,50 @@ function SettingsForm({
{/* Tab 3: 连接与同步列表 (Lists) */}
<TabsContent value="lists" className="m-0 flex flex-col gap-6">
{/* 1. Connections List (with Auto Connect toggle) */}
{/* 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>
Socket
</CardDescription>
<CardTitle className="text-base"></CardTitle>
<CardDescription></CardDescription>
</div>
<Button
type="button"
size="sm"
variant="outline"
onClick={addConnection}
onClick={() => addListItem(setAutoConnections)}
>
<Plus data-icon="inline-start" />
</Button>
</CardHeader>
<CardContent>
<FieldGroup className="gap-3">
{connections.length === 0 ? (
<div className="rounded-lg border border-dashed p-6 text-center text-xs text-muted-foreground">
<FieldGroup className="gap-2">
{autoConnections.length === 0 ? (
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
</div>
) : (
connections.map((item, idx) => (
<div
key={item.id}
className="flex flex-col gap-3 rounded-lg border bg-card p-3 shadow-2xs sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex flex-1 items-center gap-2">
<Input
className="flex-1 font-mono text-xs"
placeholder="tcp://kne01.yoyo250.fun:4565 或 udp://..."
value={item.address}
onChange={(e) =>
updateConnection(idx, 'address', e.target.value)
}
/>
</div>
<div className="flex shrink-0 items-center justify-end gap-4">
<Field orientation="horizontal" className="flex items-center gap-2">
<FieldLabel htmlFor={`auto-conn-${item.id}`} className="cursor-pointer text-xs text-muted-foreground">
</FieldLabel>
<Switch
id={`auto-conn-${item.id}`}
checked={item.autoConnect}
onCheckedChange={(checked) =>
updateConnection(idx, 'autoConnect', checked)
}
/>
</Field>
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-destructive hover:text-destructive"
onClick={() => removeConnection(idx)}
title="删除该连接"
>
<Trash2 className="size-3.5" />
</Button>
</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>
))
)}
@@ -761,7 +678,60 @@ function SettingsForm({
</CardContent>
</Card>
{/* 2. NTP Server List */}
{/* 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>