[feat] better hw info (still no gpu)

This commit is contained in:
2025-11-08 13:24:00 +08:00
parent 7e097cc9cc
commit e7e0bbd953
8 changed files with 377 additions and 34 deletions

BIN
bun.lockb

Binary file not shown.

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -50,7 +50,7 @@
"tauri-plugin-system-info-api": "^2.0.10"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.16",
"@tailwindcss/postcss": "^4.1.17",
"@tauri-apps/cli": "^2.9.3",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
@@ -73,7 +73,7 @@
"postcss-import": "^16.1.1",
"postcss-nesting": "^13.0.2",
"tailwind-merge": "3.0.2",
"tailwindcss": "^4.1.16",
"tailwindcss": "^4.1.17",
"typescript": "^5.9.3"
},
"browserslist": {

1
src-tauri/Cargo.lock generated
View File

@@ -34,6 +34,7 @@ dependencies = [
"tauri-plugin-store",
"tauri-plugin-system-info",
"tauri-plugin-valtio",
"tokio",
"walkdir",
"window-vibrancy",
"winreg 0.55.0",

View File

@@ -54,6 +54,7 @@ tauri-plugin-deep-link = "2.4.5"
anyhow = "1.0.100"
notify = "8.2.0"
dirs = "6.0.0"
tokio = { version = "1.40", features = ["process"] }
[target.'cfg(windows)'.dependencies] # Windows Only
winreg = "0.55.0"

View File

@@ -336,3 +336,47 @@ pub async fn download_app_update(
pub fn install_app_update(installer_path: String) -> Result<(), String> {
wrap_err!(install_update(&installer_path))
}
/// 获取 PowerShell Get-ComputerInfo 信息(异步版本)
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
use tokio::process::Command;
// 异步执行 PowerShell 命令获取计算机信息并转换为 JSON
let output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-ComputerInfo | Select-Object OsName, OSDisplayVersion, BiosSMBIOSBIOSVersion, CsManufacturer, CsName | ConvertTo-Json -Compress"
])
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
// 处理 PowerShell 输出,移除 BOM 和空白字符
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}'); // 移除 BOM
// 如果输出为空,返回空对象
if cleaned.is_empty() {
return Ok(serde_json::json!({}));
}
let json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
Ok(json)
}
/// 获取 PowerShell Get-ComputerInfo 信息(非 Windows 平台返回空对象)
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
Ok(serde_json::json!({}))
}

View File

@@ -169,6 +169,7 @@ fn main() {
cmds::check_app_update,
cmds::download_app_update,
cmds::install_app_update,
cmds::get_computer_info,
on_button_clicked
])
.run(ctx)

View File

@@ -7,11 +7,12 @@ import {
CardTool,
} from "@/components/window/Card"
import { ToolButton } from "@/components/window/ToolButton"
import { Chip } from "@heroui/react"
import { Chip, Skeleton } from "@heroui/react"
import { Refresh, SettingConfig } from "@icon-park/react"
// import { version } from "@tauri-apps/plugin-os"
import { useEffect, useState } from "react"
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
import { invoke } from "@tauri-apps/api/core"
export default function Page() {
return (
@@ -27,14 +28,12 @@ export default function Page() {
<UploadOne />
云同步
</ToolButton> */}
<ToolButton>
<Refresh />
</ToolButton>
<HardwareInfo />
</CardTool>
</CardHeader>
<CardBody>
<HardwareInfo />
<HardwareInfoContent />
</CardBody>
</Card>
</div>
@@ -43,39 +42,336 @@ export default function Page() {
}
function HardwareInfo() {
const [allSysData, setAllSysData] = useState<AllSystemInfo>()
// const [memInfo, setMemInfo] = useState("")
// const [staticData, setStaticData] = useState("")
// const [cpuData, setCpuData] = useState("")
// const [batteryData, setBatteryData] = useState("")
return (
<ToolButton onClick={() => {
// 触发刷新事件
window.dispatchEvent(new CustomEvent('refresh-hardware-info'))
}}>
<Refresh />
</ToolButton>
)
}
interface ComputerInfo {
OsName?: string
OSDisplayVersion?: string
BiosSMBIOSBIOSVersion?: string
CsManufacturer?: string
CsName?: string
}
function HardwareInfoContent() {
const [allSysData, setAllSysData] = useState<AllSystemInfo>()
const [computerInfo, setComputerInfo] = useState<ComputerInfo>({})
const [loading, setLoading] = useState(false)
const fetchData = async () => {
setLoading(true)
try {
// 并行获取系统信息和 PowerShell 信息
const [sys, computerInfoData] = await Promise.all([
allSysInfo(),
invoke<ComputerInfo>("get_computer_info").catch((error) => {
console.error("获取 PowerShell 信息失败:", error)
return {} as ComputerInfo
})
])
console.log("系统信息:", sys)
console.log("PowerShell 信息:", computerInfoData)
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]))
}
}
setAllSysData(sys)
setComputerInfo(computerInfoData)
} catch (error) {
console.error("获取系统信息失败:", error)
} finally {
setLoading(false)
}
}
useEffect(() => {
const fetchData = async () => {
const sys = await allSysInfo()
console.log(sys)
setAllSysData(sys)
// console.log(await memoryInfo())
// console.log(await staticInfo())
// console.log(await cpuInfo())
// console.log(await batteries())
}
void fetchData()
// 监听刷新事件
const handleRefresh = () => {
void fetchData()
}
window.addEventListener('refresh-hardware-info', handleRefresh)
return () => {
window.removeEventListener('refresh-hardware-info', handleRefresh)
}
}, [])
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`
}
// 如果 PowerShell 提供了 OSDisplayVersion直接使用否则尝试从 kernel_version 推断
const windowsVersionCode = computerInfo.OSDisplayVersion || null
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 (loading) {
return (
<div className="flex flex-col gap-4">
{/* 系统信息 Skeleton */}
<div className="flex flex-col gap-2">
<Skeleton className="h-5 w-24 rounded" />
<div className="flex flex-wrap gap-2">
<Skeleton className="h-8 w-48 rounded-full" />
<Skeleton className="h-8 w-32 rounded-full" />
</div>
</div>
{/* 主板信息 Skeleton */}
<div className="flex flex-col gap-2">
<Skeleton className="h-5 w-16 rounded" />
<div className="flex flex-wrap gap-2">
<Skeleton className="h-8 w-40 rounded-full" />
<Skeleton className="h-8 w-36 rounded-full" />
</div>
</div>
{/* CPU 信息 Skeleton */}
<div className="flex flex-col gap-2">
<Skeleton className="h-5 w-20 rounded" />
<div className="flex flex-wrap gap-2">
<Skeleton className="h-8 w-56 rounded-full" />
<Skeleton className="h-8 w-32 rounded-full" />
<Skeleton className="h-8 w-28 rounded-full" />
</div>
</div>
{/* 内存信息 Skeleton */}
<div className="flex flex-col gap-2">
<Skeleton className="h-5 w-16 rounded" />
<div className="flex flex-wrap gap-2">
<Skeleton className="h-8 w-36 rounded-full" />
<Skeleton className="h-8 w-40 rounded-full" />
<Skeleton className="h-8 w-36 rounded-full" />
</div>
</div>
{/* GPU 信息 Skeleton */}
<div className="flex flex-col gap-2">
<Skeleton className="h-5 w-16 rounded" />
<div className="flex flex-wrap gap-2">
<Skeleton className="h-8 w-52 rounded-full" />
</div>
</div>
</div>
)
}
return (
<div className="flex flex-col gap-1.5">
<Chip>CPU型号 {allSysData?.cpus[0]?.brand}</Chip>
<Chip>线 {allSysData?.cpu_count}</Chip>
<Chip>
{allSysData?.name} {allSysData?.os_version}
</Chip>
<Chip>
{allSysData?.total_memory &&
`${(allSysData.total_memory / 1024 / 1024 / 1024).toFixed(0)}GB`}
</Chip>
<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">
{computerInfo.OsName && (
<Chip>
<span className="font-medium"></span> {computerInfo.OsName}
{windowsVersionCode && (
<span className="ml-1 text-primary">({windowsVersionCode})</span>
)}
</Chip>
)}
{!computerInfo.OsName && (
<Chip>
<span className="font-medium"></span> {allSysData?.name || "未知"} {allSysData?.os_version || ""}
{allSysData?.kernel_version && (
<>
{" "}{allSysData.kernel_version}
{windowsVersionCode && (
<span className="ml-1 text-primary">({windowsVersionCode})</span>
)}
</>
)}
</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>
{/* 主板信息 */}
{(computerInfo.CsManufacturer || 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">
{computerInfo.CsManufacturer && (
<Chip>
<span className="font-medium"></span> {computerInfo.CsManufacturer}
</Chip>
)}
{computerInfo.BiosSMBIOSBIOSVersion && (
<Chip>
<span className="font-medium">BIOS版本</span> {computerInfo.BiosSMBIOSBIOSVersion}
</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>
)}
{allSysData.total_memory !== undefined && allSysData.used_memory !== undefined && (
<Chip>
<span className="font-medium"></span> {formatBytes(allSysData.total_memory - allSysData.used_memory)}
</Chip>
)}
</div>
</div>
)}
{/* GPU 信息 */}
{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>
)
})()
)}
{/* 电池信息 */}
{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>
)
}