fix(frontend): HardwareTooltip 组件健壮性重构

- hardware_info 支持 string/object/null 类型的安全解析
- 用 computed 统一提取展示行,无硬件信息时显示提示文案
- 增加 button 的 title/aria-label 属性
This commit is contained in:
fawney19
2026-02-11 17:17:53 +08:00
parent 0ddeccf698
commit 37e0785775

View File

@@ -6,8 +6,61 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@/components/ui' } from '@/components/ui'
import { Cpu } from 'lucide-vue-next' import { Cpu } from 'lucide-vue-next'
import { computed } from 'vue'
defineProps<{ node: ProxyNode }>() const props = defineProps<{ node: ProxyNode }>()
const hardwareInfo = computed<Record<string, any> | null>(() => {
const info = props.node.hardware_info
if (info == null) return null
if (typeof info === 'string') {
try {
const parsed = JSON.parse(info)
if (parsed && typeof parsed === 'object') return parsed as Record<string, any>
} catch {
return {}
}
return {}
}
if (typeof info === 'object') return info as Record<string, any>
return {}
})
const hardwareRows = computed(() => {
const info = hardwareInfo.value ?? {}
const rows: Array<{ label: string; value: string }> = []
if (info.cpu_cores != null) {
rows.push({ label: 'CPU', value: `${info.cpu_cores} cores` })
}
if (info.total_memory_mb != null) {
rows.push({ label: 'RAM', value: formatMemory(info.total_memory_mb) })
}
if (info.os_info) {
rows.push({ label: 'OS', value: String(info.os_info) })
}
if (props.node.estimated_max_concurrency != null) {
rows.push({
label: 'Max Concurrency',
value: `~${formatNumber(props.node.estimated_max_concurrency)}`,
})
}
if (info.fd_limit != null) {
rows.push({ label: 'FD Limit', value: formatNumber(info.fd_limit) })
}
return rows
})
const showHardwareInfo = computed(
() =>
!props.node.is_manual
&& (hardwareInfo.value !== null || props.node.estimated_max_concurrency != null)
)
function formatMemory(mb: number | null) { function formatMemory(mb: number | null) {
if (mb == null) return '-' if (mb == null) return '-'
@@ -23,10 +76,12 @@ function formatNumber(n: number) {
</script> </script>
<template> <template>
<Popover v-if="!node.is_manual && node.hardware_info"> <Popover v-if="showHardwareInfo">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<button <button
type="button" type="button"
title="Hardware info"
aria-label="Hardware info"
class="inline-flex items-center justify-center rounded-sm p-0.5 hover:bg-muted/60 transition-colors" class="inline-flex items-center justify-center rounded-sm p-0.5 hover:bg-muted/60 transition-colors"
> >
<Cpu class="h-3.5 w-3.5 text-muted-foreground" /> <Cpu class="h-3.5 w-3.5 text-muted-foreground" />
@@ -37,21 +92,20 @@ function formatNumber(n: number) {
:side-offset="8" :side-offset="8"
class="w-auto p-3 text-xs space-y-1" class="w-auto p-3 text-xs space-y-1"
> >
<div v-if="node.hardware_info.cpu_cores"> <div
CPU: {{ node.hardware_info.cpu_cores }} cores v-if="hardwareRows.length === 0"
</div> class="text-muted-foreground"
<div v-if="node.hardware_info.total_memory_mb"> >
RAM: {{ formatMemory(node.hardware_info.total_memory_mb) }} No hardware info reported.
</div>
<div v-if="node.hardware_info.os_info">
OS: {{ node.hardware_info.os_info }}
</div>
<div v-if="node.estimated_max_concurrency">
Max Concurrency: ~{{ formatNumber(node.estimated_max_concurrency) }}
</div>
<div v-if="node.hardware_info.fd_limit">
FD Limit: {{ formatNumber(node.hardware_info.fd_limit) }}
</div> </div>
<template v-else>
<div
v-for="row in hardwareRows"
:key="row.label"
>
{{ row.label }}: {{ row.value }}
</div>
</template>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</template> </template>