import { useCallback, useEffect, useMemo, useState } from 'react' import { Network, RefreshCw, Route, Search, Server, X } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { getRoutingInfo, RoutingInfo, NAT4Route, IPv6Route } from '../services/api' export default function Routing() { const navigate = useNavigate() const [routing, setRouting] = useState(null) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [nat4Page, setNat4Page] = useState(1) const [ipv6Page, setIPv6Page] = useState(1) const [nat4Search, setNat4Search] = useState('') const [ipv6Search, setIPv6Search] = useState('') const fetchData = useCallback(async () => { try { const res = await getRoutingInfo() setRouting(res.data.data || null) } catch (err) { console.error(err) } finally { setLoading(false) setRefreshing(false) } }, []) useEffect(() => { fetchData() }, [fetchData]) const nat4Mappings = routing?.nat4_mappings || [] const ipv6Assignments = routing?.ipv6_assignments || [] const ipv6Prefix = routing?.ipv6_prefixes?.[0]?.prefix || '-' // Filter helpers const matchesNat4Search = (m: NAT4Route, query: string) => { if (!query) return true const q = query.toLowerCase() return ( String(m.host_port).includes(q) || String(m.container_port).includes(q) || m.container_name.toLowerCase().includes(q) || m.lxc_name.toLowerCase().includes(q) || (m.ip || '').toLowerCase().includes(q) ) } const matchesIPv6Search = (item: IPv6Route, query: string) => { if (!query) return true const q = query.toLowerCase() return ( (item.address || '').toLowerCase().includes(q) || item.container_name.toLowerCase().includes(q) || item.lxc_name.toLowerCase().includes(q) ) } const filteredNat4 = useMemo(() => nat4Mappings.filter(m => matchesNat4Search(m, nat4Search)), [nat4Mappings, nat4Search]) const filteredIPv6 = useMemo(() => ipv6Assignments.filter(m => matchesIPv6Search(m, ipv6Search)), [ipv6Assignments, ipv6Search]) // Reset page on search change useEffect(() => { setNat4Page(1) }, [nat4Search]) useEffect(() => { setIPv6Page(1) }, [ipv6Search]) if (loading) { return (
) } const pageSize = 10 const nat4TotalPages = Math.max(1, Math.ceil(filteredNat4.length / pageSize)) const ipv6TotalPages = Math.max(1, Math.ceil(filteredIPv6.length / pageSize)) const currentNat4Page = Math.min(nat4Page, nat4TotalPages) const currentIPv6Page = Math.min(ipv6Page, ipv6TotalPages) const pagedNat4Mappings = filteredNat4.slice((currentNat4Page - 1) * pageSize, currentNat4Page * pageSize) const pagedIPv6Assignments = filteredIPv6.slice((currentIPv6Page - 1) * pageSize, currentIPv6Page * pageSize) return (

路由管理

宿主机分配给 LXC 的 NAT4 端口和 IPv6 地址

} remaining={routing?.nat4.remaining || '0'} total={routing?.nat4.total || '0'} used={routing?.nat4.used || 0} label="剩余端口 / 端口总数" /> } remaining={formatCapacity(routing?.ipv6.remaining || '0')} total={formatCapacity(routing?.ipv6.total || '0')} used={routing?.ipv6.used || 0} label={`剩余地址 / 地址总数 · ${ipv6Prefix}`} />
NAT4 端口分配
{nat4Search ? `搜索 "${nat4Search}" 结果 ${filteredNat4.length} 条,` : ''}共 {nat4Mappings.length} 条映射
setNat4Search(e.target.value)} placeholder="搜索端口/容器..." className="w-full pl-8 pr-7 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-black dark:text-white focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white" /> {nat4Search && ( )}
{nat4Mappings.length === 0 ? ( } text="暂无 NAT4 端口映射" /> ) : ( <>
{pagedNat4Mappings.map((mapping, index) => ( ))}
容器 LXC 名称 容器 IPv4 宿主机端口 容器端口 协议 说明 状态
{mapping.lxc_name} {mapping.ip || '-'} {mapping.host_port} {mapping.container_port} {mapping.protocol || '-'} {mapping.description || '-'}
)}
IPv6 地址分配
{ipv6Search ? `搜索 "${ipv6Search}" 结果 ${filteredIPv6.length} 条,` : ''}共 {ipv6Assignments.length} 个地址
setIPv6Search(e.target.value)} placeholder="搜索地址/容器..." className="w-full pl-8 pr-7 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-black dark:text-white focus:outline-none focus:ring-1 focus:ring-black dark:focus:ring-white" /> {ipv6Search && ( )}
{ipv6Assignments.length === 0 ? ( } text="暂无 IPv6 地址分配" /> ) : ( <>
{pagedIPv6Assignments.map((item) => ( ))}
容器 LXC 名称 IPv6 地址 前缀 出口网卡 状态
{item.lxc_name} {item.address} /{item.prefix_len || '-'} {item.interface || '-'}
)}
) } function Pagination({ page, totalPages, totalItems, pageSize, onPageChange }: { page: number totalPages: number totalItems: number pageSize: number onPageChange: (page: number) => void }) { if (totalPages <= 1) return null const start = (page - 1) * pageSize + 1 const end = Math.min(page * pageSize, totalItems) return (
显示 {start}-{end},共 {totalItems} 条
{page} / {totalPages}
) } function CapacityCard({ title, icon, remaining, total, used, label }: { title: string icon: React.ReactNode remaining: string total: string used: number label: string }) { return (
{title}
{remaining} / {total}
{icon}
{label}
已分配 {used}
) } function EmptyState({ icon, text }: { icon: React.ReactNode; text: string }) { return (
{icon}
{text}
) } function StatusBadge({ status }: { status: string }) { const running = status === 'running' return ( {running ? '运行中' : (status || '未知')} ) } function formatCapacity(value: string): string { if (value === 'large') return '充足' return value }