fix: 迁移脚本补充 inspect 导入,HardwareTooltip 兼容多种硬件字段格式

- 迁移脚本缺少 inspect 导入导致运行时报错
- HardwareTooltip 使用 pickNumber/pickString 兼容不同节点上报的字段名
- 添加 tooltip 点击交互和 title 属性作为 fallback
This commit is contained in:
fawney19
2026-02-21 18:13:46 +08:00
parent 066fc01d2e
commit b649a69a5e
2 changed files with 61 additions and 12 deletions

View File

@@ -11,7 +11,7 @@ from __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
import sqlalchemy as sa import sqlalchemy as sa
from sqlalchemy import text from sqlalchemy import inspect, text
from alembic import op from alembic import op
@@ -69,6 +69,7 @@ def upgrade() -> None:
""")) """))
# 创建索引 # 创建索引
inspector = inspect(conn)
existing_indexes = {idx["name"] for idx in inspector.get_indexes("usage")} existing_indexes = {idx["name"] for idx in inspector.get_indexes("usage")}
for idx_name, table, columns in NEW_INDEXES: for idx_name, table, columns in NEW_INDEXES:
if idx_name not in existing_indexes: if idx_name not in existing_indexes:

View File

@@ -2,9 +2,10 @@
import type { ProxyNode } from '@/api/proxy-nodes' import type { ProxyNode } from '@/api/proxy-nodes'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Cpu } from 'lucide-vue-next' import { Cpu } from 'lucide-vue-next'
import { computed } from 'vue' import { computed, ref } from 'vue'
const props = defineProps<{ node: ProxyNode }>() const props = defineProps<{ node: ProxyNode }>()
const open = ref(false)
const hardwareInfo = computed<Record<string, any> | null>(() => { const hardwareInfo = computed<Record<string, any> | null>(() => {
const info = props.node.hardware_info const info = props.node.hardware_info
@@ -26,16 +27,24 @@ const hardwareRows = computed(() => {
const info = hardwareInfo.value ?? {} const info = hardwareInfo.value ?? {}
const rows: Array<{ label: string; value: string }> = [] const rows: Array<{ label: string; value: string }> = []
if (info.cpu_cores != null) { const cpuCores = pickNumber(info.cpu_cores, info.cpu_count, info.cpu?.cores)
rows.push({ label: 'CPU', value: `${info.cpu_cores} cores` }) if (cpuCores != null) {
rows.push({ label: 'CPU', value: `${cpuCores} cores` })
} }
if (info.total_memory_mb != null) { const memoryMb = pickNumber(
rows.push({ label: 'RAM', value: formatMemory(info.total_memory_mb) }) info.total_memory_mb,
info.memory_total_mb,
info.memory_mb,
info.memory?.total_mb
)
if (memoryMb != null) {
rows.push({ label: 'RAM', value: formatMemory(memoryMb) })
} }
if (info.os_info) { const osInfo = pickString(info.os_info, info.os, info.platform)
rows.push({ label: 'OS', value: String(info.os_info) }) if (osInfo) {
rows.push({ label: 'OS', value: osInfo })
} }
if (props.node.estimated_max_concurrency != null) { if (props.node.estimated_max_concurrency != null) {
@@ -45,13 +54,19 @@ const hardwareRows = computed(() => {
}) })
} }
if (info.fd_limit != null) { const fdLimit = pickNumber(info.fd_limit, info.file_descriptor_limit, info.ulimit_nofile)
rows.push({ label: 'FD Limit', value: formatNumber(info.fd_limit) }) if (fdLimit != null) {
rows.push({ label: 'FD Limit', value: formatNumber(fdLimit) })
} }
return rows return rows
}) })
const tooltipTitle = computed(() => {
if (hardwareRows.value.length === 0) return '暂无硬件信息上报'
return hardwareRows.value.map(row => `${row.label}: ${row.value}`).join(' | ')
})
const showHardwareInfo = computed( const showHardwareInfo = computed(
() => () =>
!props.node.is_manual !props.node.is_manual
@@ -69,6 +84,32 @@ function formatNumber(n: number) {
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return String(n) return String(n)
} }
function pickNumber(...values: unknown[]): number | null {
for (const value of values) {
if (value == null) continue
const parsed = typeof value === 'number' ? value : Number(value)
if (Number.isFinite(parsed)) return parsed
}
return null
}
function pickString(...values: unknown[]): string {
for (const value of values) {
if (value == null) continue
const text = String(value).trim()
if (text) return text
}
return ''
}
function toggleTooltip() {
open.value = !open.value
}
function handleOpenChange(value: boolean) {
open.value = value
}
</script> </script>
<template> <template>
@@ -76,12 +117,19 @@ function formatNumber(n: number) {
v-if="showHardwareInfo" v-if="showHardwareInfo"
:delay-duration="0" :delay-duration="0"
> >
<Tooltip> <Tooltip
:open="open"
@update:open="handleOpenChange"
>
<TooltipTrigger as-child> <TooltipTrigger as-child>
<button <button
type="button" type="button"
aria-label="硬件信息" aria-label="硬件信息"
class="inline-flex items-center justify-center rounded-sm p-0.5 hover:bg-muted/60 transition-colors" :title="tooltipTitle"
class="inline-flex items-center justify-center rounded-sm p-0.5 hover:bg-muted/60 transition-colors cursor-help"
@click.stop="toggleTooltip"
@keydown.enter.prevent.stop="toggleTooltip"
@keydown.space.prevent.stop="toggleTooltip"
> >
<Cpu class="h-3.5 w-3.5 text-muted-foreground" /> <Cpu class="h-3.5 w-3.5 text-muted-foreground" />
</button> </button>