517 lines
18 KiB
TypeScript
517 lines
18 KiB
TypeScript
"use client"
|
||
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "@/components/window/Card"
|
||
import { ToolButton } from "@/components/window/ToolButton"
|
||
import { Chip, Skeleton } from "@heroui/react"
|
||
import { Refresh, SettingConfig } from "@icon-park/react"
|
||
// import { version } from "@tauri-apps/plugin-os"
|
||
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
|
||
import { invoke } from "@tauri-apps/api/core"
|
||
import useSWR, { useSWRConfig } from "swr"
|
||
|
||
export default function Page() {
|
||
return (
|
||
<section className="flex flex-col gap-4 overflow-hidden rounded-lg">
|
||
<div className="flex flex-col h-full gap-4 overflow-hidden hide-scrollbar">
|
||
<Card className="overflow-hidden">
|
||
<CardHeader>
|
||
<CardIcon type="menu">
|
||
<SettingConfig /> 硬件
|
||
</CardIcon>
|
||
<CardTool>
|
||
{/* <ToolButton>
|
||
<UploadOne />
|
||
云同步
|
||
</ToolButton> */}
|
||
<HardwareInfo />
|
||
</CardTool>
|
||
</CardHeader>
|
||
|
||
<CardBody>
|
||
<HardwareInfoContent />
|
||
</CardBody>
|
||
</Card>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function HardwareInfo() {
|
||
const { mutate } = useSWRConfig()
|
||
|
||
return (
|
||
<ToolButton
|
||
onClick={() => {
|
||
// 使用 SWR 的 mutate 来刷新数据
|
||
mutate("/api/hardware-info")
|
||
}}
|
||
>
|
||
<Refresh /> 刷新
|
||
</ToolButton>
|
||
)
|
||
}
|
||
|
||
interface ComputerInfo {
|
||
OsName?: string
|
||
OSDisplayVersion?: string
|
||
BiosSMBIOSBIOSVersion?: string
|
||
CsManufacturer?: string
|
||
CsName?: string
|
||
ReleaseId?: string
|
||
}
|
||
|
||
interface GpuInfo {
|
||
vendor: string
|
||
model: string
|
||
family: string
|
||
device_id: string
|
||
total_vram: number
|
||
used_vram: number
|
||
load_pct: number
|
||
temperature: number
|
||
}
|
||
|
||
interface MemoryInfo {
|
||
capacity?: number // 容量(字节)
|
||
manufacturer?: string
|
||
speed?: number // MHz,实际频率 ConfiguredClockSpeed
|
||
default_speed?: number // MHz,默认频率 Speed(如果存在)
|
||
}
|
||
|
||
interface MonitorInfo {
|
||
name?: string
|
||
refresh_rate?: number // Hz
|
||
resolution_width?: number
|
||
resolution_height?: number
|
||
}
|
||
|
||
interface MotherboardInfo {
|
||
manufacturer?: string // 制造商
|
||
model?: string // 型号
|
||
version?: string
|
||
}
|
||
|
||
interface HardwareData {
|
||
allSysData: AllSystemInfo
|
||
computerInfo: ComputerInfo
|
||
gpuInfo: GpuInfo | null
|
||
memoryInfo: MemoryInfo[]
|
||
monitorInfo: MonitorInfo[]
|
||
motherboardInfo: MotherboardInfo | null
|
||
}
|
||
|
||
// 硬件信息 fetcher
|
||
const hardwareInfoFetcher = async (): Promise<HardwareData> => {
|
||
// 并行获取系统信息、PowerShell 信息、GPU 信息、内存信息、显示器信息和主板信息
|
||
const [sys, computerInfoData, gpuInfoData, memoryInfoData, monitorInfoData, motherboardInfoData] =
|
||
await Promise.all([
|
||
allSysInfo(),
|
||
invoke<ComputerInfo>("get_computer_info").catch((error) => {
|
||
console.error("获取 PowerShell 信息失败:", error)
|
||
return {} as ComputerInfo
|
||
}),
|
||
invoke<GpuInfo | null>("get_gpu_info").catch((error) => {
|
||
console.error("获取 GPU 信息失败:", error)
|
||
return null
|
||
}),
|
||
invoke<MemoryInfo[]>("get_memory_info").catch((error) => {
|
||
console.error("获取内存信息失败:", error)
|
||
return [] as MemoryInfo[]
|
||
}),
|
||
invoke<MonitorInfo[]>("get_monitor_info").catch((error) => {
|
||
console.error("获取显示器信息失败:", error)
|
||
return [] as MonitorInfo[]
|
||
}),
|
||
invoke<MotherboardInfo>("get_motherboard_info").catch((error) => {
|
||
console.error("获取主板信息失败:", error)
|
||
return { manufacturer: undefined, model: undefined, version: undefined } as MotherboardInfo
|
||
}),
|
||
])
|
||
|
||
console.log("系统信息:", sys)
|
||
console.log("PowerShell 信息:", computerInfoData)
|
||
console.log("GPU 信息:", gpuInfoData)
|
||
console.log("内存信息:", memoryInfoData)
|
||
console.log("显示器信息:", monitorInfoData)
|
||
console.log("主板信息:", motherboardInfoData)
|
||
|
||
if (sys?.cpus) {
|
||
console.log("CPU数据:", sys.cpus)
|
||
console.log("第一个CPU:", sys.cpus[0])
|
||
if (sys.cpus[0]) {
|
||
console.log("CPU字段:", Object.keys(sys.cpus[0]))
|
||
}
|
||
}
|
||
|
||
return {
|
||
allSysData: sys,
|
||
computerInfo: computerInfoData,
|
||
gpuInfo: gpuInfoData,
|
||
memoryInfo: memoryInfoData,
|
||
monitorInfo: monitorInfoData,
|
||
motherboardInfo: motherboardInfoData,
|
||
}
|
||
}
|
||
|
||
function HardwareInfoContent() {
|
||
const { data, isLoading, isValidating } = useSWR<HardwareData>("/api/hardware-info", hardwareInfoFetcher, {
|
||
revalidateOnFocus: false,
|
||
revalidateOnReconnect: false,
|
||
dedupingInterval: 5 * 60 * 1000, // 5分钟内相同请求去重
|
||
})
|
||
|
||
const allSysData = data?.allSysData
|
||
const computerInfo = data?.computerInfo || {}
|
||
const gpuInfo = data?.gpuInfo
|
||
const memoryInfo = data?.memoryInfo || []
|
||
const monitorInfo = data?.monitorInfo || []
|
||
const motherboardInfo = data?.motherboardInfo
|
||
|
||
const formatBytes = (bytes?: number) => {
|
||
if (!bytes) return "未知"
|
||
const gb = bytes / 1024 / 1024 / 1024
|
||
if (gb >= 1) {
|
||
return `${gb.toFixed(2)}GB`
|
||
}
|
||
const mb = bytes / 1024 / 1024
|
||
return `${mb.toFixed(2)}MB`
|
||
}
|
||
|
||
// 格式化系统信息:Windows 11 (26200) 25H2
|
||
const formatSystemInfo = () => {
|
||
const osVersion = allSysData?.os_version || null
|
||
// 使用 OSDisplayVersion 作为版本代码(如 "25H2")
|
||
const osDisplayVersion = computerInfo.OSDisplayVersion || null
|
||
|
||
let systemStr = allSysData?.name || "未知"
|
||
if (osVersion) {
|
||
systemStr += ` ${osVersion}`
|
||
}
|
||
if (osDisplayVersion) {
|
||
systemStr += ` ${osDisplayVersion}`
|
||
}
|
||
|
||
return systemStr
|
||
}
|
||
const memoryUsagePercent =
|
||
allSysData?.total_memory && allSysData?.used_memory !== undefined
|
||
? Math.round((allSysData.used_memory / allSysData.total_memory) * 100)
|
||
: null
|
||
|
||
// 计算所有CPU核心的平均频率(统一转换为GHz)
|
||
const averageCpuFrequency =
|
||
allSysData?.cpus && allSysData.cpus.length > 0
|
||
? (() => {
|
||
// 尝试多个可能的频率字段名
|
||
const frequencies = allSysData.cpus
|
||
.map((cpu) => {
|
||
// 尝试不同的字段名
|
||
const freq = (cpu as any).frequency ?? (cpu as any).freq ?? (cpu as any).clock_speed
|
||
return freq
|
||
})
|
||
.filter((freq): freq is number => {
|
||
// 确保是有效的数字且大于0
|
||
return typeof freq === "number" && !isNaN(freq) && freq > 0
|
||
})
|
||
|
||
if (frequencies.length === 0) {
|
||
console.log("未找到有效的CPU频率数据,CPU对象:", allSysData.cpus[0])
|
||
return null
|
||
}
|
||
|
||
const sum = frequencies.reduce((acc, freq) => acc + freq, 0)
|
||
const avg = sum / frequencies.length
|
||
|
||
// 判断单位并统一转换为GHz
|
||
// 如果值在2-10范围,可能是GHz
|
||
// 如果值在2000-10000范围,可能是MHz(需要除以1000)
|
||
// 如果值在百万级别(2000000+),可能是Hz(需要除以1,000,000)
|
||
let freqInGhz: number
|
||
if (avg >= 1_000_000) {
|
||
// Hz单位,转换为GHz
|
||
freqInGhz = avg / 1_000_000
|
||
} else if (avg >= 1000) {
|
||
// MHz单位,转换为GHz
|
||
freqInGhz = avg / 1000
|
||
} else {
|
||
// 已经是GHz单位
|
||
freqInGhz = avg
|
||
}
|
||
|
||
console.log("CPU频率数据:", frequencies, "原始平均值:", avg, "转换为GHz:", freqInGhz)
|
||
|
||
return freqInGhz
|
||
})()
|
||
: null
|
||
|
||
// 如果正在加载或正在验证(包括刷新),显示 Skeleton 骨架屏
|
||
if (isLoading || isValidating) {
|
||
return (
|
||
<div className="flex flex-col gap-4">
|
||
{/* 系统信息 Skeleton */}
|
||
<div className="flex flex-col gap-2">
|
||
<Skeleton className="w-24 h-5 rounded" />
|
||
<div className="flex flex-wrap gap-2">
|
||
<Skeleton className="w-48 h-8 rounded-full" />
|
||
<Skeleton className="w-32 h-8 rounded-full" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* CPU 信息 Skeleton */}
|
||
<div className="flex flex-col gap-2">
|
||
<Skeleton className="w-20 h-5 rounded" />
|
||
<div className="flex flex-wrap gap-2">
|
||
<Skeleton className="w-56 h-8 rounded-full" />
|
||
<Skeleton className="w-32 h-8 rounded-full" />
|
||
<Skeleton className="h-8 rounded-full w-28" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 内存信息 Skeleton */}
|
||
<div className="flex flex-col gap-2">
|
||
<Skeleton className="w-16 h-5 rounded" />
|
||
<div className="flex flex-wrap gap-2">
|
||
<Skeleton className="h-8 rounded-full w-36" />
|
||
<Skeleton className="w-40 h-8 rounded-full" />
|
||
<Skeleton className="h-8 rounded-full w-36" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* GPU 信息 Skeleton */}
|
||
<div className="flex flex-col gap-2">
|
||
<Skeleton className="w-16 h-5 rounded" />
|
||
<div className="flex flex-wrap gap-2">
|
||
<Skeleton className="h-8 rounded-full w-52" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 主板信息 Skeleton */}
|
||
<div className="flex flex-col gap-2">
|
||
<Skeleton className="w-16 h-5 rounded" />
|
||
<div className="flex flex-wrap gap-2">
|
||
<Skeleton className="w-40 h-8 rounded-full" />
|
||
<Skeleton className="h-8 rounded-full w-36" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4">
|
||
{/* 系统信息 */}
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">系统信息</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Chip>
|
||
<span className="font-medium">系统:</span> {formatSystemInfo()}
|
||
</Chip>
|
||
{computerInfo.CsName && (
|
||
<Chip>
|
||
<span className="font-medium">主机名:</span> {computerInfo.CsName}
|
||
</Chip>
|
||
)}
|
||
{!computerInfo.CsName && allSysData?.hostname && (
|
||
<Chip>
|
||
<span className="font-medium">主机名:</span> {allSysData.hostname}
|
||
</Chip>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* CPU 信息 */}
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">处理器</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Chip>
|
||
<span className="font-medium">型号:</span> {allSysData?.cpus?.[0]?.brand || "未知"}
|
||
</Chip>
|
||
{averageCpuFrequency !== null && (
|
||
<Chip>
|
||
<span className="font-medium">频率:</span> {averageCpuFrequency.toFixed(2)} GHz
|
||
</Chip>
|
||
)}
|
||
<Chip>
|
||
<span className="font-medium">核心数:</span> {allSysData?.cpu_count || "未知"}
|
||
</Chip>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 内存信息 */}
|
||
{allSysData?.total_memory && (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">内存</div>
|
||
{/* 第一行:总容量和已用内存 */}
|
||
<div className="flex flex-wrap gap-2">
|
||
<Chip>
|
||
<span className="font-medium">总容量:</span> {formatBytes(allSysData.total_memory)}
|
||
</Chip>
|
||
{allSysData.used_memory !== undefined && (
|
||
<Chip>
|
||
<span className="font-medium">已用:</span> {formatBytes(allSysData.used_memory)}
|
||
{memoryUsagePercent !== null && (
|
||
<span className="ml-1">({memoryUsagePercent}%)</span>
|
||
)}
|
||
</Chip>
|
||
)}
|
||
</div>
|
||
{/* 每个内存条的详细信息 */}
|
||
{memoryInfo.length > 0 && (
|
||
<div className="flex flex-col gap-1.5">
|
||
{memoryInfo.map((mem, index) => (
|
||
<div key={index} className="flex flex-wrap gap-2">
|
||
{mem.capacity && (
|
||
<Chip>
|
||
<span className="font-medium">容量:</span> {formatBytes(mem.capacity)}
|
||
</Chip>
|
||
)}
|
||
{mem.manufacturer && (
|
||
<Chip>
|
||
<span className="font-medium">制造商:</span> {mem.manufacturer}
|
||
</Chip>
|
||
)}
|
||
{mem.speed && (
|
||
<Chip>
|
||
<span className="font-medium">频率:</span> {mem.speed} MHz
|
||
{mem.default_speed && (
|
||
<span className="ml-1">({mem.default_speed} MHz)</span>
|
||
)}
|
||
</Chip>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* GPU 信息 */}
|
||
{gpuInfo ? (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">显卡</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Chip>
|
||
<span className="font-medium">型号:</span> {gpuInfo.model}
|
||
</Chip>
|
||
<Chip>
|
||
<span className="font-medium">总显存:</span> {formatBytes(gpuInfo.total_vram)}
|
||
</Chip>
|
||
<Chip>
|
||
<span className="font-medium">已用显存:</span> {formatBytes(gpuInfo.used_vram)}
|
||
</Chip>
|
||
<Chip>
|
||
<span className="font-medium">温度:</span> {gpuInfo.temperature.toFixed(2)}°C
|
||
</Chip>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
allSysData?.components &&
|
||
allSysData.components.length > 0 &&
|
||
(() => {
|
||
const gpuComponents = allSysData.components.filter(
|
||
(comp) =>
|
||
comp.label?.toLowerCase().includes("gpu") ||
|
||
comp.label?.toLowerCase().includes("graphics") ||
|
||
comp.label?.toLowerCase().includes("显卡")
|
||
)
|
||
|
||
if (gpuComponents.length === 0) return null
|
||
|
||
return (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">显卡</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{gpuComponents.map((gpu, index) => (
|
||
<Chip key={index}>
|
||
<span className="font-medium">GPU{index > 0 ? ` ${index + 1}` : ""}:</span>{" "}
|
||
{gpu.label || "未知"}
|
||
{gpu.temperature !== undefined && (
|
||
<span className="ml-1">
|
||
({gpu.temperature}°C{gpu.max !== undefined ? ` / ${gpu.max}°C` : ""})
|
||
</span>
|
||
)}
|
||
</Chip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
})()
|
||
)}
|
||
|
||
{/* 主板信息 */}
|
||
{(motherboardInfo?.model ||
|
||
motherboardInfo?.manufacturer ||
|
||
motherboardInfo?.version ||
|
||
computerInfo.BiosSMBIOSBIOSVersion) && (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">主板</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{motherboardInfo?.model && (
|
||
<Chip>
|
||
<span className="font-medium">型号:</span> {motherboardInfo.model}
|
||
</Chip>
|
||
)}
|
||
{motherboardInfo?.manufacturer && (
|
||
<Chip>
|
||
<span className="font-medium">品牌:</span> {motherboardInfo.manufacturer}
|
||
</Chip>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{motherboardInfo?.version && (
|
||
<Chip>
|
||
<span className="font-medium">版本:</span> {motherboardInfo.version}
|
||
</Chip>
|
||
)}
|
||
{computerInfo.BiosSMBIOSBIOSVersion && (
|
||
<Chip>
|
||
<span className="font-medium">BIOS版本:</span> {computerInfo.BiosSMBIOSBIOSVersion}
|
||
</Chip>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 显示器信息 */}
|
||
{monitorInfo.length > 0 && monitorInfo.some((m) => m.refresh_rate) && (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">显示器</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{monitorInfo.map(
|
||
(monitor, index) =>
|
||
monitor.refresh_rate && (
|
||
<Chip key={index}>
|
||
<span className="font-medium">
|
||
刷新率{monitorInfo.length > 1 ? ` ${index + 1}` : ""}:
|
||
</span>{" "}
|
||
{monitor.refresh_rate} Hz
|
||
</Chip>
|
||
)
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 电池信息 */}
|
||
{allSysData?.batteries && allSysData.batteries.length > 0 && (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="text-sm font-medium text-foreground-600">电池</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{allSysData.batteries.map((battery, index) => (
|
||
<Chip key={index}>
|
||
<span className="font-medium">电池{index > 0 ? ` ${index + 1}` : ""}:</span>
|
||
{battery.state && `${battery.state} `}
|
||
{battery.state_of_charge !== undefined && `${battery.state_of_charge}% `}
|
||
{battery.energy_full !== undefined && battery.energy !== undefined && (
|
||
<span>
|
||
({formatBytes(battery.energy)} / {formatBytes(battery.energy_full)})
|
||
</span>
|
||
)}
|
||
</Chip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|