支持公网IPV4分配,单独IPV6分配,以及混合网络分配。

This commit is contained in:
MengMengCode
2026-06-09 22:22:40 +08:00
parent f4edf94800
commit 917afc3157
23 changed files with 3988 additions and 770 deletions
+273 -74
View File
@@ -9,8 +9,12 @@ import {
XCircle,
} from 'lucide-react'
import { getHostReport, HostProbeReport } from '../services/api'
import { useLanguage, type Language } from '../contexts/LanguageContext'
import { translateText } from '../utils/i18n'
export default function HostReport() {
const { language } = useLanguage()
const text = hostReportText[language]
const [report, setReport] = useState<HostProbeReport | null>(null)
const [loading, setLoading] = useState(true)
@@ -31,61 +35,61 @@ export default function HostReport() {
}, [fetchReport])
return (
<div className="space-y-6">
<div className="space-y-6" data-no-translate>
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-black">宿</h1>
<p className="mt-1 text-sm text-gray-500"></p>
<h1 className="text-2xl font-bold text-black">{text.title}</h1>
<p className="mt-1 text-sm text-gray-500">{text.subtitle}</p>
</div>
<button onClick={fetchReport} disabled={loading} className="inline-flex items-center gap-1.5 rounded-md border border-gray-200 px-3 py-2 text-sm text-gray-600 hover:bg-gray-50 disabled:opacity-50">
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
{text.refresh}
</button>
</div>
{loading && !report ? (
<div className="rounded-lg border border-gray-200 bg-white py-14 text-center text-sm text-gray-400">宿...</div>
<div className="rounded-lg border border-gray-200 bg-white py-14 text-center text-sm text-gray-400">{text.loading}</div>
) : !report ? (
<div className="rounded-lg border border-gray-200 bg-white py-14 text-center text-sm text-gray-400">宿</div>
<div className="rounded-lg border border-gray-200 bg-white py-14 text-center text-sm text-gray-400">{text.emptyReport}</div>
) : (
<div className="space-y-5">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<ProbeMetric icon={<Cpu className="h-4 w-4" />} label="CPU" value={report.cpu.model || 'Unknown'} sub={`${report.cpu.cores} 核 / ${report.cpu.threads} 线程`} />
<ProbeMetric icon={<MemoryStick className="h-4 w-4" />} label="RAM" value={formatMB(report.memory.total_mb)} sub={`${formatMB(report.memory.used_mb)} 已用`} />
<ProbeMetric icon={<HardDrive className="h-4 w-4" />} label="DISK" value={`${report.disks.length} 块硬盘`} sub={report.disks.map(d => d.type).filter(Boolean).join(' / ') || 'Unknown'} />
<ProbeMetric icon={<Activity className="h-4 w-4" />} label="运行状态" value={report.system.uptime_text} sub={`${report.system.process_count} 个进程`} />
<ProbeMetric icon={<Cpu className="h-4 w-4" />} label="CPU" value={report.cpu.model || 'Unknown'} sub={formatCPUThreads(report.cpu.cores, report.cpu.threads, language)} />
<ProbeMetric icon={<MemoryStick className="h-4 w-4" />} label="RAM" value={formatMB(report.memory.total_mb)} sub={formatUsedMemory(report.memory.used_mb, language)} />
<ProbeMetric icon={<HardDrive className="h-4 w-4" />} label="DISK" value={formatDiskCount(report.disks.length, language)} sub={report.disks.map(d => diskTypeLabel(d, language)).filter(Boolean).join(' / ') || 'Unknown'} />
<ProbeMetric icon={<Activity className="h-4 w-4" />} label={text.runtimeStatus} value={translateDynamic(report.system.uptime_text, language)} sub={formatProcessCount(report.system.process_count, language)} />
</div>
<ProbeSection title="系统概览">
<ProbeSection title={text.systemOverview}>
<ProbeRows rows={[
['主机名', report.hostname],
['操作系统', report.os],
['内核', report.kernel],
['生成时间', report.generated_at],
['CPU 架构', report.cpu.architecture],
['CPU 虚拟化指令', report.cpu.virtualization ? `支持 (${report.cpu.virtualization_key})` : '未检测到'],
['CPU 核显', report.cpu.has_integrated_gpu ? '检测到' : '未检测到'],
['显卡', report.gpus.length ? `${report.gpus.length}` : '未检测到'],
['运行能力', runtimeModeLabel(report.runtime.support_mode)],
['KVM 嵌套虚拟化', `${report.runtime.nested_virtualization ? '支持' : '未检测到'} (${report.runtime.nested_detail || '-'})`],
[text.hostname, report.hostname],
[text.os, report.os],
[text.kernel, report.kernel],
[text.generatedAt, report.generated_at],
[text.cpuArch, report.cpu.architecture],
[text.cpuVirtualization, report.cpu.virtualization ? `${text.supported} (${report.cpu.virtualization_key})` : text.notDetected],
[text.cpuIntegratedGPU, report.cpu.has_integrated_gpu ? text.detected : text.notDetected],
[text.gpu, report.gpus.length ? formatItemCount(report.gpus.length, language) : text.notDetected],
[text.runtimeCapability, runtimeModeLabel(report.runtime.support_mode, language)],
[text.kvmNested, `${report.runtime.nested_virtualization ? text.supported : text.notDetected} (${translateDynamic(report.runtime.nested_detail || '-', language)})`],
]} />
</ProbeSection>
<ProbeSection title="公网与路由">
<ProbeSection title={text.publicNetwork}>
<ProbeRows rows={[
['公网 IPv4', report.public_ipv4.length ? report.public_ipv4.join('\n') : '未检测到'],
['IPv4 地址', report.ipv4_addresses?.length ? report.ipv4_addresses.map(formatIPv4Address).join('\n') : '未检测到'],
['IPv4 段', report.ipv4_prefixes?.length ? report.ipv4_prefixes.map(formatIPv4Prefix).join('\n') : '未检测到'],
['IPv6 地址', report.ipv6_addresses.length ? report.ipv6_addresses.map(ip => `${ip.address}/${ip.prefix_len} (${ip.interface})`).join('\n') : '未检测到'],
['IPv6 段', report.ipv6_prefixes?.length ? report.ipv6_prefixes.map(formatIPv6Prefix).join('\n') : '未检测到'],
['网关', report.gateways.length ? report.gateways.map(g => `${g.family}: ${g.gateway || '-'} dev ${g.interface || '-'}`).join('\n') : '未检测到'],
[text.publicIPv4, report.public_ipv4.length ? report.public_ipv4.join('\n') : text.notDetected],
[text.ipv4Address, report.ipv4_addresses?.length ? report.ipv4_addresses.map(formatIPv4Address).join('\n') : text.notDetected],
[text.ipv4Prefix, report.ipv4_prefixes?.length ? report.ipv4_prefixes.map(formatIPv4Prefix).join('\n') : text.notDetected],
[text.ipv6Address, report.ipv6_addresses.length ? report.ipv6_addresses.map(ip => `${ip.address}/${ip.prefix_len} (${ip.interface})`).join('\n') : text.notDetected],
[text.ipv6Prefix, report.ipv6_prefixes?.length ? report.ipv6_prefixes.map(formatIPv6Prefix).join('\n') : text.notDetected],
[text.gateway, report.gateways.length ? report.gateways.map(g => `${g.family}: ${g.gateway || '-'} dev ${g.interface || '-'}`).join('\n') : text.notDetected],
]} />
</ProbeSection>
<ProbeTable
title="内存条"
empty="未检测到内存条明细,可能缺少 dmidecode 或权限受限"
headers={['插槽', '容量', '类型', '频率', '厂商', '型号/序列号']}
title={text.memoryModules}
empty={text.noMemoryModules}
headers={[text.slot, text.capacity, text.type, text.frequency, text.vendor, text.modelSerial]}
rows={(report.memory.modules || []).map(m => [
m.locator || '-',
m.size || '-',
@@ -97,29 +101,29 @@ export default function HostReport() {
/>
<ProbeTable
title="硬盘与健康"
empty="未检测到硬盘"
headers={['设备', '型号', '容量', '类型', '挂载点', '健康', '寿命', '通电', '读取', '写入', '命令数', '擦写']}
title={text.disksHealth}
empty={text.noDisks}
headers={[text.device, text.model, text.capacity, text.type, text.mountPoint, text.health, text.lifetime, text.powerOn, text.reads, text.writes, text.commands, text.eraseCount]}
rows={report.disks.map(d => [
`${d.path || d.name}\n${d.serial || ''}`,
d.model || '-',
formatBytes(d.size_bytes),
d.type || (d.rotational ? 'HDD' : 'SSD'),
diskTypeLabel(d, language),
d.mountpoints?.length ? d.mountpoints.join('\n') : '-',
`${diskHealthLabel(d.health)}\n${d.health_detail || ''}`,
formatLifeUsed(d.smart?.life_used_percent),
d.smart?.power_on_hours ? `${d.smart.power_on_hours} 小时\n${formatPowerOnDays(d.smart.power_on_hours)}` : '-',
formatBytes(d.smart?.read_data_bytes || 0),
formatBytes(d.smart?.written_data_bytes || 0),
formatCommands(d.smart?.read_commands, d.smart?.write_commands),
formatWear(d.smart?.wear_leveling_count, d.smart?.erase_count, d.smart?.power_cycle_count),
`${diskHealthLabel(d.health, language)}\n${diskHealthDetail(d, language)}`,
d.virtual ? text.unsupported : formatLifeUsed(d.smart?.life_used_percent, language),
d.virtual ? text.unsupported : (d.smart?.power_on_hours ? `${d.smart.power_on_hours} ${text.hours}\n${formatPowerOnDays(d.smart.power_on_hours, language)}` : '-'),
d.virtual ? text.unsupported : formatBytes(d.smart?.read_data_bytes || 0),
d.virtual ? text.unsupported : formatBytes(d.smart?.written_data_bytes || 0),
d.virtual ? text.unsupported : formatCommands(d.smart?.read_commands, d.smart?.write_commands, language),
d.virtual ? text.unsupported : formatWear(d.smart?.wear_leveling_count, d.smart?.erase_count, d.smart?.power_cycle_count, language),
])}
/>
<ProbeTable
title="网卡"
empty="未检测到网卡"
headers={['网卡', '状态', '驱动/速率', 'MAC', 'IPv4', 'IPv6']}
title={text.networkInterfaces}
empty={text.noNetworkInterfaces}
headers={[text.nic, text.status, text.driverSpeed, 'MAC', 'IPv4', 'IPv6']}
rows={report.network_interfaces.map(n => [
`${n.name}\n${n.model || ''}`,
n.state || '-',
@@ -131,25 +135,25 @@ export default function HostReport() {
/>
<ProbeTable
title="显卡"
empty="未检测到显卡"
headers={['名称', '厂商', '类型', '驱动']}
rows={report.gpus.map(g => [g.name, g.vendor || '-', gpuTypeLabel(g.type), g.driver || '-'])}
title={text.gpus}
empty={text.noGPUs}
headers={[text.name, text.vendor, text.type, text.driver]}
rows={report.gpus.map(g => [g.name, g.vendor || '-', gpuTypeLabel(g.type, language), g.driver || '-'])}
/>
<ProbeSection title="环境支持">
<ProbeSection title={text.environmentSupport}>
<div className="grid gap-2 md:grid-cols-2">
{report.environment.map(item => (
<div key={item.key} className="flex items-start gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2">
{item.ok ? <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-600" /> : <XCircle className={`mt-0.5 h-4 w-4 shrink-0 ${item.required ? 'text-red-600' : 'text-amber-600'}`} />}
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2 text-xs font-medium text-gray-800">
<span>{item.label}</span>
<span>{translateDynamic(item.label, language)}</span>
<span className={`rounded px-1.5 py-0.5 text-[10px] ${item.required ? 'bg-gray-100 text-gray-600' : 'bg-blue-50 text-blue-700'}`}>
{item.required ? '必要' : '可选'}
{item.required ? text.required : text.optional}
</span>
</div>
<div className="mt-1 break-all font-mono text-[11px] text-gray-500">{item.detail || '-'}</div>
<div className="mt-1 break-all font-mono text-[11px] text-gray-500">{translateDynamic(item.detail || '-', language)}</div>
</div>
</div>
))}
@@ -174,6 +178,153 @@ function ProbeMetric({ icon, label, value, sub }: { icon: ReactNode; label: stri
)
}
const hostReportText = {
zh: {
title: '宿主机信息',
subtitle: '硬件、网络、磁盘健康与运行环境探测报告',
refresh: '刷新',
loading: '正在探测宿主机环境...',
emptyReport: '暂未获取到宿主机信息',
runtimeStatus: '运行状态',
systemOverview: '系统概览',
hostname: '主机名',
os: '操作系统',
kernel: '内核',
generatedAt: '生成时间',
cpuArch: 'CPU 架构',
cpuVirtualization: 'CPU 虚拟化指令',
cpuIntegratedGPU: 'CPU 核显',
gpu: '显卡',
runtimeCapability: '运行能力',
kvmNested: 'KVM 嵌套虚拟化',
supported: '支持',
detected: '检测到',
notDetected: '未检测到',
publicNetwork: '公网与路由',
publicIPv4: '公网 IPv4',
ipv4Address: 'IPv4 地址',
ipv4Prefix: 'IPv4 段',
ipv6Address: 'IPv6 地址',
ipv6Prefix: 'IPv6 段',
gateway: '网关',
memoryModules: '内存条',
noMemoryModules: '未检测到内存条明细,可能缺少 dmidecode 或权限受限',
slot: '插槽',
capacity: '容量',
type: '类型',
frequency: '频率',
vendor: '厂商',
modelSerial: '型号/序列号',
disksHealth: '硬盘与健康',
noDisks: '未检测到硬盘',
device: '设备',
model: '型号',
mountPoint: '挂载点',
health: '健康',
lifetime: '寿命',
powerOn: '通电',
reads: '读取',
writes: '写入',
commands: '命令数',
eraseCount: '擦写',
virtualDisk: '虚拟磁盘',
virtualDiskDetail: '虚拟磁盘,真实 SMART/寿命/通电数据需在物理宿主机查看',
unsupported: '不支持',
hours: '小时',
used: '已用',
remaining: '剩余',
read: '读',
write: '写',
wear: '磨损',
erase: '擦写',
powerCycles: '启停',
networkInterfaces: '网卡',
noNetworkInterfaces: '未检测到网卡',
nic: '网卡',
status: '状态',
driverSpeed: '驱动/速率',
gpus: '显卡',
noGPUs: '未检测到显卡',
name: '名称',
driver: '驱动',
environmentSupport: '环境支持',
required: '必要',
optional: '可选',
},
en: {
title: 'Host Info',
subtitle: 'Hardware, network, disk health, and runtime environment report',
refresh: 'Refresh',
loading: 'Probing host environment...',
emptyReport: 'No host information available',
runtimeStatus: 'Runtime Status',
systemOverview: 'System Overview',
hostname: 'Hostname',
os: 'Operating System',
kernel: 'Kernel',
generatedAt: 'Generated At',
cpuArch: 'CPU Architecture',
cpuVirtualization: 'CPU Virtualization',
cpuIntegratedGPU: 'CPU Integrated GPU',
gpu: 'GPU',
runtimeCapability: 'Runtime Capability',
kvmNested: 'KVM Nested Virtualization',
supported: 'Supported',
detected: 'Detected',
notDetected: 'Not detected',
publicNetwork: 'Public Network & Routing',
publicIPv4: 'Public IPv4',
ipv4Address: 'IPv4 Addresses',
ipv4Prefix: 'IPv4 Prefixes',
ipv6Address: 'IPv6 Addresses',
ipv6Prefix: 'IPv6 Prefixes',
gateway: 'Gateway',
memoryModules: 'Memory Modules',
noMemoryModules: 'No memory module details detected. dmidecode may be missing or permissions may be limited.',
slot: 'Slot',
capacity: 'Capacity',
type: 'Type',
frequency: 'Frequency',
vendor: 'Vendor',
modelSerial: 'Model / Serial',
disksHealth: 'Disks & Health',
noDisks: 'No disks detected',
device: 'Device',
model: 'Model',
mountPoint: 'Mount Point',
health: 'Health',
lifetime: 'Lifetime',
powerOn: 'Power-on',
reads: 'Reads',
writes: 'Writes',
commands: 'Commands',
eraseCount: 'Erase Count',
virtualDisk: 'Virtual Disk',
virtualDiskDetail: 'Virtual disk. Real SMART, lifetime, and power-on data must be checked on the physical host.',
unsupported: 'Unsupported',
hours: 'hours',
used: 'used',
remaining: 'remaining',
read: 'Read',
write: 'Write',
wear: 'Wear',
erase: 'Erase',
powerCycles: 'Power cycles',
networkInterfaces: 'Network Interfaces',
noNetworkInterfaces: 'No network interfaces detected',
nic: 'NIC',
status: 'Status',
driverSpeed: 'Driver / Speed',
gpus: 'GPUs',
noGPUs: 'No GPUs detected',
name: 'Name',
driver: 'Driver',
environmentSupport: 'Environment Support',
required: 'Required',
optional: 'Optional',
},
} as const
function ProbeSection({ title, children }: { title: string; children: ReactNode }) {
return (
<section>
@@ -255,6 +406,26 @@ function formatMB(value: number) {
return `${value} MB`
}
function formatCPUThreads(cores: number, threads: number, language: Language) {
return language === 'en' ? `${cores} cores / ${threads} threads` : `${cores} 核 / ${threads} 线程`
}
function formatUsedMemory(usedMB: number, language: Language) {
return language === 'en' ? `${formatMB(usedMB)} used` : `${formatMB(usedMB)} 已用`
}
function formatDiskCount(count: number, language: Language) {
return language === 'en' ? `${count} disk${count === 1 ? '' : 's'}` : `${count} 块硬盘`
}
function formatProcessCount(count: number, language: Language) {
return language === 'en' ? `${count} process${count === 1 ? '' : 'es'}` : `${count} 个进程`
}
function formatItemCount(count: number, language: Language) {
return language === 'en' ? `${count} item${count === 1 ? '' : 's'}` : `${count}`
}
function formatBytes(value: number) {
if (!value) return '-'
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
@@ -267,20 +438,24 @@ function formatBytes(value: number) {
return `${next.toFixed(index === 0 ? 0 : 1)} ${units[index]}`
}
function formatLifeUsed(value?: number) {
function formatLifeUsed(value: number | undefined, language: Language) {
if (value === undefined || value === null) return '-'
return `${value}% 已用\n${Math.max(0, 100 - value)}% 剩余`
const text = hostReportText[language]
return `${value}% ${text.used}\n${Math.max(0, 100 - value)}% ${text.remaining}`
}
function formatPowerOnDays(hours: number) {
function formatPowerOnDays(hours: number, language: Language) {
const days = Math.floor(hours / 24)
const rest = hours % 24
return days > 0 ? `${days}${rest} 小时` : `${hours} 小时`
return language === 'en'
? (days > 0 ? `${days} days ${rest} hours` : `${hours} hours`)
: (days > 0 ? `${days}${rest} 小时` : `${hours} 小时`)
}
function formatCommands(read?: number, write?: number) {
function formatCommands(read: number | undefined, write: number | undefined, language: Language) {
if (!read && !write) return '-'
return `${formatCount(read || 0)}\n写 ${formatCount(write || 0)}`
const text = hostReportText[language]
return `${text.read} ${formatCount(read || 0)}\n${text.write} ${formatCount(write || 0)}`
}
function formatCount(value: number) {
@@ -291,38 +466,62 @@ function formatCount(value: number) {
return `${value}`
}
function formatWear(wear?: string, erase?: string, powerCycles?: number) {
function formatWear(wear: string | undefined, erase: string | undefined, powerCycles: number | undefined, language: Language) {
const text = hostReportText[language]
const rows: string[] = []
if (wear) rows.push(`磨损 ${wear}`)
if (erase) rows.push(`擦写 ${erase}`)
if (powerCycles) rows.push(`启停 ${powerCycles}`)
if (wear) rows.push(`${text.wear} ${wear}`)
if (erase) rows.push(`${text.erase} ${erase}`)
if (powerCycles) rows.push(`${text.powerCycles} ${powerCycles}`)
return rows.length ? rows.join('\n') : '-'
}
function runtimeModeLabel(value: string) {
function runtimeModeLabel(value: string, language: Language) {
switch (value) {
case 'kvm_lxc':
return '支持 KVM + LXC'
return language === 'en' ? 'KVM + LXC supported' : '支持 KVM + LXC'
case 'lxc_only':
return '仅支持 LXC'
return language === 'en' ? 'LXC only' : '仅支持 LXC'
default:
return '未满足运行环境'
return language === 'en' ? 'Runtime requirements not met' : '未满足运行环境'
}
}
function diskHealthLabel(value: string) {
function diskHealthLabel(value: string, language: Language) {
const text = hostReportText[language]
switch (value) {
case 'ok':
return '健康'
return language === 'en' ? 'Healthy' : '健康'
case 'failed':
return '异常'
return language === 'en' ? 'Failed' : '异常'
case 'virtual':
return text.virtualDisk
default:
return '未知'
return language === 'en' ? 'Unknown' : '未知'
}
}
function gpuTypeLabel(value: string) {
if (value === 'integrated') return '核显'
if (value === 'discrete') return '独显'
function diskHealthDetail(d: { virtual?: boolean; health_detail?: string }, language: Language) {
if (d.virtual) return hostReportText[language].virtualDiskDetail
return translateDynamic(d.health_detail || '', language)
}
function diskTypeLabel(d: { type?: string; rotational?: boolean; virtual?: boolean }, language: Language) {
if (d.virtual || d.type === 'Virtual') return hostReportText[language].virtualDisk
return d.type || (d.rotational ? 'HDD' : 'SSD')
}
function gpuTypeLabel(value: string, language: Language) {
if (value === 'integrated') return language === 'en' ? 'Integrated' : '核显'
if (value === 'discrete') return language === 'en' ? 'Discrete' : '独显'
return value || '-'
}
function translateDynamic(value: string, language: Language) {
if (language !== 'en' || !value) return value
return translateText(value)
.replace(/寿命已用\s*(\d+)%/g, 'Lifetime used $1%')
.replace(/通电\s*(\d+)h/g, 'Power-on $1h')
.replace(/写入\s*([^|]+)/g, 'Written $1')
.replace(/读取\s*([^|]+)/g, 'Read $1')
.replace(/介质错误\s*(\d+)/g, 'Media errors $1')
}