From e824455577478f146d1a73befaf7a73a8e97e2b7 Mon Sep 17 00:00:00 2001 From: purp1e Date: Sat, 8 Nov 2025 15:43:44 +0800 Subject: [PATCH] [feat] more hw info including gpu + refactor fpstest --- src-tauri/Cargo.lock | 215 +++- src-tauri/Cargo.toml | 1 + src-tauri/src/cmds.rs | 98 +- src-tauri/src/main.rs | 2 + src/app/(main)/gear/page.tsx | 396 +++++--- src/components/cstb/FpsTest.tsx | 944 +++--------------- .../FpsTest/components/BatchTestProgress.tsx | 31 + .../cstb/FpsTest/components/NoteCell.tsx | 27 + .../FpsTest/components/ResolutionConfig.tsx | 193 ++++ .../FpsTest/components/TestConfigPanel.tsx | 112 +++ .../FpsTest/components/TestResultDisplay.tsx | 53 + .../FpsTest/components/TestResultsTable.tsx | 135 +++ src/components/cstb/FpsTest/constants.ts | 32 + .../cstb/FpsTest/hooks/useGameMonitor.ts | 32 + .../cstb/FpsTest/hooks/useHardwareInfo.ts | 62 ++ .../cstb/FpsTest/hooks/useTestMonitor.ts | 113 +++ src/components/cstb/FpsTest/index.tsx | 6 + .../cstb/FpsTest/services/resultReader.ts | 271 +++++ .../cstb/FpsTest/services/testRunner.ts | 172 ++++ src/components/cstb/FpsTest/types.ts | 25 + .../cstb/FpsTest/utils/csv-export.ts | 175 ++++ .../cstb/FpsTest/utils/fps-metrics.ts | 43 + .../cstb/FpsTest/utils/timestamp.ts | 66 ++ .../cstb/FpsTest/utils/vprof-parser.ts | 47 + todo/refactor-plan.md | 286 ++++++ 25 files changed, 2560 insertions(+), 977 deletions(-) create mode 100644 src/components/cstb/FpsTest/components/BatchTestProgress.tsx create mode 100644 src/components/cstb/FpsTest/components/NoteCell.tsx create mode 100644 src/components/cstb/FpsTest/components/ResolutionConfig.tsx create mode 100644 src/components/cstb/FpsTest/components/TestConfigPanel.tsx create mode 100644 src/components/cstb/FpsTest/components/TestResultDisplay.tsx create mode 100644 src/components/cstb/FpsTest/components/TestResultsTable.tsx create mode 100644 src/components/cstb/FpsTest/constants.ts create mode 100644 src/components/cstb/FpsTest/hooks/useGameMonitor.ts create mode 100644 src/components/cstb/FpsTest/hooks/useHardwareInfo.ts create mode 100644 src/components/cstb/FpsTest/hooks/useTestMonitor.ts create mode 100644 src/components/cstb/FpsTest/index.tsx create mode 100644 src/components/cstb/FpsTest/services/resultReader.ts create mode 100644 src/components/cstb/FpsTest/services/testRunner.ts create mode 100644 src/components/cstb/FpsTest/types.ts create mode 100644 src/components/cstb/FpsTest/utils/csv-export.ts create mode 100644 src/components/cstb/FpsTest/utils/fps-metrics.ts create mode 100644 src/components/cstb/FpsTest/utils/timestamp.ts create mode 100644 src/components/cstb/FpsTest/utils/vprof-parser.ts create mode 100644 todo/refactor-plan.md diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ed1131c..27d9a40 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "base64 0.22.1", "dirs 6.0.0", "futures-util", + "gfxinfo", "log", "notify", "regex", @@ -880,14 +881,38 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.108", ] [[package]] @@ -904,13 +929,24 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.108", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", "quote", "syn 2.0.108", ] @@ -1657,6 +1693,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gfxinfo" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f33b12fba19d158bebe0262fd2b6c49fee6f0e0e089f4f3fdc11c017548473" +dependencies = [ + "core-foundation 0.10.1", + "io-kit-sys", + "libdrm_amdgpu_sys", + "nvml-wrapper", + "serde", + "windows 0.61.3", + "wmi", +] + [[package]] name = "gio" version = "0.18.4" @@ -2236,6 +2287,16 @@ dependencies = [ "libc", ] +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2454,6 +2515,15 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libdrm_amdgpu_sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7bf95f3c5d8a8ae4a5c0c46fe2271c16fdf8fd4b26978f44b082a0f8ebaaae" +dependencies = [ + "libc", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2845,6 +2915,29 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "nvml-wrapper" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9bff0aa1d48904a1385ea2a8b97576fbdcbc9a3cfccd0d31fe978e1c4038c5" +dependencies = [ + "bitflags 2.10.0", + "libloading 0.8.9", + "nvml-wrapper-sys", + "static_assertions", + "thiserror 1.0.69", + "wrapcenum-derive", +] + +[[package]] +name = "nvml-wrapper-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "698d45156f28781a4e79652b6ebe2eaa0589057d588d3aec1333f6466f13fcb5" +dependencies = [ + "libloading 0.8.9", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -4401,7 +4494,7 @@ version = "3.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.108", @@ -6254,7 +6347,7 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement", + "windows-implement 0.60.2", "windows-interface", ] @@ -6342,17 +6435,39 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529" +dependencies = [ + "windows-collections 0.1.1", + "windows-core 0.60.1", + "windows-future 0.1.1", + "windows-link 0.1.3", + "windows-numerics 0.1.1", +] + [[package]] name = "windows" version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows-collections" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5467f79cc1ba3f52ebb2ed41dbb459b8e7db636cc3429458d9a852e15bc24dec" +dependencies = [ + "windows-core 0.60.1", ] [[package]] @@ -6373,13 +6488,26 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.3.1", +] + [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", + "windows-implement 0.60.2", "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", @@ -6392,13 +6520,23 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement", + "windows-implement 0.60.2", "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", ] +[[package]] +name = "windows-future" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a787db4595e7eb80239b74ce8babfb1363d8e343ab072f2ffe901400c03349f0" +dependencies = [ + "windows-core 0.60.1", + "windows-link 0.1.3", +] + [[package]] name = "windows-future" version = "0.2.1" @@ -6410,6 +6548,17 @@ dependencies = [ "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -6444,6 +6593,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005dea54e2f6499f2cee279b8f703b3cf3b5734a2d8d21867c8f44003182eeed" +dependencies = [ + "windows-core 0.60.1", + "windows-link 0.1.3", +] + [[package]] name = "windows-numerics" version = "0.2.0" @@ -6483,6 +6642,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -6812,6 +6980,33 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "wmi" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f902b4592b911109e7352bcfec7b754b07ec71e514d7dfa280eaef924c1cb08" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.17", + "windows 0.60.0", + "windows-core 0.60.1", +] + +[[package]] +name = "wrapcenum-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7461b6d..a2b0425 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -55,6 +55,7 @@ anyhow = "1.0.100" notify = "8.2.0" dirs = "6.0.0" tokio = { version = "1.40", features = ["process"] } +gfxinfo = "0.1.2" [target.'cfg(windows)'.dependencies] # Windows Only winreg = "0.55.0" diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 0d8bb61..eef5e95 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -11,6 +11,7 @@ use std::path::Path; use std::io::{BufRead, BufReader}; use tauri::path::BaseDirectory; use tauri::Manager; +use serde::{Deserialize, Serialize}; // use tauri_plugin_shell::ShellExt; @@ -368,9 +369,45 @@ pub async fn get_computer_info() -> Result { return Ok(serde_json::json!({})); } - let json: serde_json::Value = serde_json::from_str(cleaned) + let mut json: serde_json::Value = serde_json::from_str(cleaned) .map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?; + // 对于 Windows 11,优先使用 OSDisplayVersion 获取版本代码(如 25H2) + // 如果 OSDisplayVersion 存在且包含版本代码,提取它 + if let Some(os_display_version) = json.get("OSDisplayVersion").and_then(|v| v.as_str()) { + // OSDisplayVersion 格式可能是 "25H2" 或 "Windows 11 版本 25H2" 等 + // 尝试提取版本代码(如 25H2) + if let Some(capture) = regex::Regex::new(r"(\d+H\d+)") + .ok() + .and_then(|re| re.captures(os_display_version)) + { + if let Some(version_code) = capture.get(1) { + json["ReleaseId"] = serde_json::Value::String(version_code.as_str().to_string()); + } + } + } + + // 如果没有从 OSDisplayVersion 获取到版本代码,尝试从注册表获取 ReleaseId + if !json.get("ReleaseId").and_then(|v| v.as_str()).is_some() { + let release_id_output = Command::new("powershell") + .args(&[ + "-NoProfile", + "-Command", + "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; try { (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ReleaseId } catch { $null }" + ]) + .output() + .await; + + if let Ok(release_output) = release_id_output { + if release_output.status.success() { + let release_str = String::from_utf8_lossy(&release_output.stdout).trim().to_string(); + if !release_str.is_empty() { + json["ReleaseId"] = serde_json::Value::String(release_str); + } + } + } + } + Ok(json) } @@ -379,4 +416,63 @@ pub async fn get_computer_info() -> Result { #[cfg(not(target_os = "windows"))] pub async fn get_computer_info() -> Result { Ok(serde_json::json!({})) +} + +/// GPU 信息结构体 +#[derive(Debug, Serialize, Deserialize)] +pub struct GpuInfo { + vendor: String, + model: String, + family: String, + device_id: String, + total_vram: u64, + used_vram: u64, + load_pct: u32, + temperature: f64, +} + +/// 辅助函数:格式化字节数 +fn format_bytes(bytes: u64) -> String { + let gb = bytes as f64 / 1024.0 / 1024.0 / 1024.0; + if gb >= 1.0 { + format!("{:.2}GB", gb) + } else { + let mb = bytes as f64 / 1024.0 / 1024.0; + format!("{:.2}MB", mb) + } +} + +/// 获取 GPU 信息 +#[tauri::command] +pub fn get_gpu_info() -> Result, String> { + use gfxinfo::active_gpu; + + match active_gpu() { + Ok(gpu) => { + let info = gpu.info(); + let temp = info.temperature() as f64 / 1000.0; + + Ok(Some(GpuInfo { + vendor: gpu.vendor().to_string(), + model: gpu.model().to_string(), + family: gpu.family().to_string(), + device_id: gpu.device_id().to_string(), + total_vram: info.total_vram(), + used_vram: info.used_vram(), + load_pct: info.load_pct(), + temperature: temp, + })) + } + Err(e) => { + println!("✗ GPU 信息获取失败: {}", e); + Ok(None) + } + } +} + +/// 获取显示器信息(非 Windows 平台返回空) +#[tauri::command] +#[cfg(not(target_os = "windows"))] +pub async fn get_monitor_info() -> Result, String> { + Ok(vec![]) } \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a82ddc9..d78508a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -170,6 +170,8 @@ fn main() { cmds::download_app_update, cmds::install_app_update, cmds::get_computer_info, + cmds::get_gpu_info, + cmds::get_monitor_info, on_button_clicked ]) .run(ctx) diff --git a/src/app/(main)/gear/page.tsx b/src/app/(main)/gear/page.tsx index f39446c..06371f6 100644 --- a/src/app/(main)/gear/page.tsx +++ b/src/app/(main)/gear/page.tsx @@ -1,11 +1,5 @@ "use client" -import { - Card, - CardBody, - CardHeader, - CardIcon, - CardTool, -} from "@/components/window/Card" +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" @@ -43,12 +37,14 @@ export default function Page() { function HardwareInfo() { const { mutate } = useSWRConfig() - + return ( - { - // 使用 SWR 的 mutate 来刷新数据 - mutate("/api/hardware-info") - }}> + { + // 使用 SWR 的 mutate 来刷新数据 + mutate("/api/hardware-info") + }} + > 刷新 ) @@ -60,27 +56,70 @@ interface ComputerInfo { 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 { + manufacturer?: string + part_number?: string + speed?: number // MHz +} + +interface MonitorInfo { + name?: string + refresh_rate?: number // Hz + resolution_width?: number + resolution_height?: number } interface HardwareData { allSysData: AllSystemInfo computerInfo: ComputerInfo + gpuInfo: GpuInfo | null + memoryInfo: MemoryInfo[] + monitorInfo: MonitorInfo[] } // 硬件信息 fetcher const hardwareInfoFetcher = async (): Promise => { - // 并行获取系统信息和 PowerShell 信息 - const [sys, computerInfoData] = await Promise.all([ + // 并行获取系统信息、PowerShell 信息、GPU 信息、内存信息和显示器信息 + const [sys, computerInfoData, gpuInfoData, memoryInfoData, monitorInfoData] = await Promise.all([ allSysInfo(), invoke("get_computer_info").catch((error) => { console.error("获取 PowerShell 信息失败:", error) return {} as ComputerInfo - }) + }), + invoke("get_gpu_info").catch((error) => { + console.error("获取 GPU 信息失败:", error) + return null + }), + invoke("get_memory_info").catch((error) => { + console.error("获取内存信息失败:", error) + return [] as MemoryInfo[] + }), + invoke("get_monitor_info").catch((error) => { + console.error("获取显示器信息失败:", error) + return [] as MonitorInfo[] + }), ]) - + console.log("系统信息:", sys) console.log("PowerShell 信息:", computerInfoData) - + console.log("GPU 信息:", gpuInfoData) + console.log("内存信息:", memoryInfoData) + console.log("显示器信息:", monitorInfoData) + if (sys?.cpus) { console.log("CPU数据:", sys.cpus) console.log("第一个CPU:", sys.cpus[0]) @@ -88,26 +127,28 @@ const hardwareInfoFetcher = async (): Promise => { console.log("CPU字段:", Object.keys(sys.cpus[0])) } } - + return { allSysData: sys, - computerInfo: computerInfoData + computerInfo: computerInfoData, + gpuInfo: gpuInfoData, + memoryInfo: memoryInfoData, + monitorInfo: monitorInfoData, } } function HardwareInfoContent() { - const { data, isLoading } = useSWR( - "/api/hardware-info", - hardwareInfoFetcher, - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - dedupingInterval: 5 * 60 * 1000, // 5分钟内相同请求去重 - } - ) + const { data, isLoading } = useSWR("/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 formatBytes = (bytes?: number) => { if (!bytes) return "未知" @@ -119,56 +160,72 @@ function HardwareInfoContent() { 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 - + // 格式化系统信息: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 + 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) { @@ -176,47 +233,47 @@ function HardwareInfoContent() {
{/* 系统信息 Skeleton */}
- +
- - -
-
- - {/* 主板信息 Skeleton */} -
- -
- - + +
{/* CPU 信息 Skeleton */}
- +
- - - + + +
{/* 内存信息 Skeleton */}
- +
- - - + + +
{/* GPU 信息 Skeleton */}
- +
- + +
+
+ + {/* 主板信息 Skeleton */} +
+ +
+ +
@@ -229,27 +286,9 @@ function HardwareInfoContent() {
系统信息
- {computerInfo.OsName && ( - - 系统: {computerInfo.OsName} - {windowsVersionCode && ( - ({windowsVersionCode}) - )} - - )} - {!computerInfo.OsName && ( - - 系统: {allSysData?.name || "未知"} {allSysData?.os_version || ""} - {allSysData?.kernel_version && ( - <> - {" "}{allSysData.kernel_version} - {windowsVersionCode && ( - ({windowsVersionCode}) - )} - - )} - - )} + + 系统: {formatSystemInfo()} + {computerInfo.CsName && ( 主机名: {computerInfo.CsName} @@ -263,25 +302,6 @@ function HardwareInfoContent() {
- {/* 主板信息 */} - {(computerInfo.CsManufacturer || computerInfo.BiosSMBIOSBIOSVersion) && ( -
-
主板
-
- {computerInfo.CsManufacturer && ( - - 制造商: {computerInfo.CsManufacturer} - - )} - {computerInfo.BiosSMBIOSBIOSVersion && ( - - BIOS版本: {computerInfo.BiosSMBIOSBIOSVersion} - - )} -
-
- )} - {/* CPU 信息 */}
处理器
@@ -318,7 +338,18 @@ function HardwareInfoContent() { )} {allSysData.total_memory !== undefined && allSysData.used_memory !== undefined && ( - 可用: {formatBytes(allSysData.total_memory - allSysData.used_memory)} + 可用:{" "} + {formatBytes(allSysData.total_memory - allSysData.used_memory)} + + )} + {memoryInfo.length > 0 && memoryInfo[0].part_number && ( + + 型号: {memoryInfo[0].part_number} + + )} + {memoryInfo.length > 0 && memoryInfo[0].speed && ( + + 频率: {memoryInfo[0].speed} MHz )}
@@ -326,25 +357,49 @@ function HardwareInfoContent() { )} {/* GPU 信息 */} - {allSysData?.components && allSysData.components.length > 0 && ( + {gpuInfo ? ( +
+
显卡
+
+ + 型号: {gpuInfo.model} + + + 总显存: {formatBytes(gpuInfo.total_vram)} + + + 已用显存: {formatBytes(gpuInfo.used_vram)} + + + 温度: {gpuInfo.temperature.toFixed(2)}°C + +
+
+ ) : ( + 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('显卡') + 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 (
显卡
{gpuComponents.map((gpu, index) => ( - GPU{index > 0 ? ` ${index + 1}` : ""}: {gpu.label || "未知"} + GPU{index > 0 ? ` ${index + 1}` : ""}:{" "} + {gpu.label || "未知"} {gpu.temperature !== undefined && ( - ({gpu.temperature}°C{gpu.max !== undefined ? ` / ${gpu.max}°C` : ""}) + + ({gpu.temperature}°C{gpu.max !== undefined ? ` / ${gpu.max}°C` : ""}) + )} ))} @@ -354,6 +409,45 @@ function HardwareInfoContent() { })() )} + {/* 主板信息 */} + {(computerInfo.CsManufacturer || computerInfo.BiosSMBIOSBIOSVersion) && ( +
+
主板
+
+ {computerInfo.CsManufacturer && ( + + 制造商: {computerInfo.CsManufacturer} + + )} + {computerInfo.BiosSMBIOSBIOSVersion && ( + + BIOS版本: {computerInfo.BiosSMBIOSBIOSVersion} + + )} +
+
+ )} + + {/* 显示器信息 */} + {monitorInfo.length > 0 && monitorInfo.some((m) => m.refresh_rate) && ( +
+
显示器
+
+ {monitorInfo.map( + (monitor, index) => + monitor.refresh_rate && ( + + + 刷新率{monitorInfo.length > 1 ? ` ${index + 1}` : ""}: + {" "} + {monitor.refresh_rate} Hz + + ) + )} +
+
+ )} + {/* 电池信息 */} {allSysData?.batteries && allSysData.batteries.length > 0 && (
@@ -365,7 +459,9 @@ function HardwareInfoContent() { {battery.state && `${battery.state} `} {battery.state_of_charge !== undefined && `${battery.state_of_charge}% `} {battery.energy_full !== undefined && battery.energy !== undefined && ( - ({formatBytes(battery.energy)} / {formatBytes(battery.energy_full)}) + + ({formatBytes(battery.energy)} / {formatBytes(battery.energy_full)}) + )} ))} diff --git a/src/components/cstb/FpsTest.tsx b/src/components/cstb/FpsTest.tsx index 366f278..d85807e 100644 --- a/src/components/cstb/FpsTest.tsx +++ b/src/components/cstb/FpsTest.tsx @@ -8,17 +8,7 @@ import { addToast, Button, Chip, - Spinner, - Table, - TableHeader, - TableColumn, - TableBody, - TableRow, - TableCell, - Tabs, - Tab, Tooltip, - Input, Modal, ModalContent, ModalHeader, @@ -26,209 +16,35 @@ import { ModalFooter, Textarea, useDisclosure, - Dropdown, - DropdownTrigger, - DropdownMenu, - DropdownItem, - Select, - SelectItem, - CircularProgress, + Input, } from "@heroui/react" import { useState, useEffect, useRef, useCallback } from "react" import { TestTube, Power, - List, - Delete, Play, - Edit, Check, Close, Square, DownloadOne, + List, } from "@icon-park/react" -import { allSysInfo, type AllSystemInfo } from "tauri-plugin-system-info-api" -import { ToolButton } from "../window/ToolButton" -import { save } from "@tauri-apps/plugin-dialog" -import { writeTextFile } from "@tauri-apps/plugin-fs" +// 导入提取的模块 +import { BENCHMARK_MAPS, TEST_TIMEOUT, PRESET_RESOLUTIONS } from "./FpsTest/constants" +import { parseVProfReport } from "./FpsTest/utils/vprof-parser" +import { compareTimestamps, formatCurrentTimestamp, timestampToISO } from "./FpsTest/utils/timestamp" +import { extractFpsMetrics } from "./FpsTest/utils/fps-metrics" +import { handleExportCSV, handleExportAverageCSV, formatVideoSettingSummary } from "./FpsTest/utils/csv-export" +import { useGameMonitor } from "./FpsTest/hooks/useGameMonitor" +import { useHardwareInfo } from "./FpsTest/hooks/useHardwareInfo" +import { NoteCell } from "./FpsTest/components/NoteCell" +import { BatchTestProgress } from "./FpsTest/components/BatchTestProgress" +import { TestResultDisplay } from "./FpsTest/components/TestResultDisplay" +import { TestConfigPanel } from "./FpsTest/components/TestConfigPanel" +import { ResolutionConfig } from "./FpsTest/components/ResolutionConfig" +import { TestResultsTable } from "./FpsTest/components/TestResultsTable" -const BENCHMARK_MAPS = [ - { - name: "de_dust2_benchmark", - workshopId: "3240880604", - map: "de_dust2_benchmark", - label: "Dust2", - }, - { - name: "de_ancient", - workshopId: "3472126051", - map: "de_ancient", - label: "Ancient", - }, -] - -const TEST_TIMEOUT = 200000 // 200秒超时时间(毫秒) - -// 预设分辨率列表 -const PRESET_RESOLUTIONS = [ - { width: "800", height: "600", label: "800x600" }, - { width: "1024", height: "768", label: "1024x768" }, - { width: "1280", height: "960", label: "1280x960" }, - { width: "1440", height: "1080", label: "1440x1080" }, - { width: "1920", height: "1080", label: "1920x1080" }, - { width: "1920", height: "1440", label: "1920x1440" }, - { width: "2560", height: "1440", label: "2560x1440" }, - { width: "2880", height: "2160", label: "2880x2160" }, - { width: "3840", height: "2160", label: "3840x2160" }, -] as const - -// 解析性能报告,提取时间戳和性能数据 -function parseVProfReport(rawReport: string): { timestamp: string; data: string } | null { - if (!rawReport) return null - - const lines = rawReport.split("\n") - let timestamp = "" - let inPerformanceSection = false - const performanceLines: string[] = [] - - for (const line of lines) { - // 提取时间戳:格式如 "11/05 01:51:27 [VProf] -- Performance report --" - const timestampMatch = line.match( - /(\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[VProf\]\s+--\s+Performance\s+report\s+--/ - ) - if (timestampMatch) { - timestamp = timestampMatch[1] - inPerformanceSection = true - // 也包含 Performance report 这一行,但移除时间戳 - const lineWithoutTimestamp = line.trim().replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "") - performanceLines.push(lineWithoutTimestamp) - continue - } - - // 如果在性能报告部分 - if (inPerformanceSection) { - const trimmedLine = line.trim() - - // 只收集包含 [VProf] 的行 - if (trimmedLine.includes("[VProf]")) { - // 移除行首的时间戳(格式:MM/DD HH:mm:ss ) - // 例如:"11/05 02:13:56 [VProf] ..." -> "[VProf] ..." - const lineWithoutTimestamp = trimmedLine.replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "") - performanceLines.push(lineWithoutTimestamp) - } - // 如果遇到空行且已经有数据,可能是报告结束,但不直接结束,因为可能还有更多数据 - // 如果后续没有 [VProf] 行的数据,空行会被过滤掉 - } - } - - if (performanceLines.length === 0) return null - - return { - timestamp, - data: performanceLines.join("\n").trim(), - } -} - -// 比较时间戳(格式:MM/DD HH:mm:ss) -// 返回 true 如果 timestamp1 晚于 timestamp2 -function compareTimestamps(timestamp1: string, timestamp2: string): boolean { - // 解析时间戳:MM/DD HH:mm:ss - const parseTimestamp = (ts: string) => { - const match = ts.match(/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/) - if (!match) return null - const [, month, day, hour, minute, second] = match.map(Number) - return { month, day, hour, minute, second } - } - - const ts1 = parseTimestamp(timestamp1) - const ts2 = parseTimestamp(timestamp2) - - if (!ts1 || !ts2) return false - - // 使用当前年份作为基准 - const now = new Date() - const currentYear = now.getFullYear() - - // 创建日期对象,尝试当前年份 - let date1 = new Date(currentYear, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second) - let date2 = new Date(currentYear, ts2.month - 1, ts2.day, ts2.hour, ts2.minute, ts2.second) - - // 如果 date1 早于 date2,可能是跨年了(比如 date1 是 1月,date2 是 12月) - // 在这种情况下,给 date1 加一年 - if (date1 < date2) { - // 检查是否可能是跨年(月份相差很大) - const monthDiff = (ts1.month - ts2.month + 12) % 12 - if (monthDiff > 6) { - // 可能是跨年,给 date1 加一年 - date1 = new Date(currentYear + 1, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second) - } - } - - return date1 > date2 -} - -// 从 VProf 报告中提取 avg 和 p1 值 -function extractFpsMetrics(result: string): { avg: number | null; p1: number | null } { - let avg: number | null = null - let p1: number | null = null - - // 查找包含 avg 的行,支持多种格式: - // - "[VProf] FPS: Avg=239.5, P1=228.0" (等号格式) - // - "[VProf] avg: 123.45" (冒号格式) - // - "[VProf] avg 123.45" (空格格式) - const avgMatch = result.match(/avg[=:\s]+(\d+\.?\d*)/i) - if (avgMatch) { - avg = parseFloat(avgMatch[1]) - } - - // 查找包含 p1 的行,支持多种格式: - // - "P1=228.0" (等号格式) - // - "p1: 98.76" (冒号格式) - // - "p1 98.76" (空格格式) - const p1Match = result.match(/p1[=:\s]+(\d+\.?\d*)/i) - if (p1Match) { - p1 = parseFloat(p1Match[1]) - } - - // 如果找不到,尝试查找其他可能的格式 - // 例如:查找包含 "fps" 和数字的行 - if (!avg) { - const fpsMatch = result.match(/fps[=:\s]+(\d+\.?\d*)/i) - if (fpsMatch) { - avg = parseFloat(fpsMatch[1]) - } - } - - // 尝试查找 1% low 或类似的格式 - if (!p1) { - const lowMatch = result.match(/(?:1%|1st|first).*?low[=:\s]+(\d+\.?\d*)/i) - if (lowMatch) { - p1 = parseFloat(lowMatch[1]) - } - } - - return { avg, p1 } -} - -// 备注单元格组件 -function NoteCell({ note, onEdit }: { note: string; onEdit: () => void }) { - return ( -
- - {note || "无备注"} - - -
- ) -} +// 所有工具函数和组件已提取到独立模块,这里不再需要定义 export function FpsTest() { const steam = useSteamStore() @@ -239,8 +55,9 @@ export function FpsTest() { const [selectedMapIndex, setSelectedMapIndex] = useState(0) const [isMonitoring, setIsMonitoring] = useState(false) const [showResultsTable, setShowResultsTable] = useState(false) - const [hardwareInfo, setHardwareInfo] = useState(null) - const [isGameRunning, setIsGameRunning] = useState(false) + // 使用提取的Hooks + const hardwareInfo = useHardwareInfo() + const { isGameRunning, checkGameRunning } = useGameMonitor() const [editingNoteId, setEditingNoteId] = useState(null) // 正在编辑的备注ID const [editingNoteValue, setEditingNoteValue] = useState("") // 正在编辑的备注内容 // 从store读取配置数据 @@ -283,29 +100,6 @@ export function FpsTest() { // 记录最后一次测试的时间戳(用于平均值记录) const lastTestTimestampRef = useRef(null) - // 检测游戏是否运行 - const checkGameRunning = useCallback(async () => { - try { - const result = await invoke("check_process_running", { - processName: "cs2.exe", - }).catch(() => false) - setIsGameRunning(result) - return result - } catch { - setIsGameRunning(false) - return false - } - }, []) - - // 定期检测游戏运行状态 - useEffect(() => { - void checkGameRunning() - const interval = setInterval(() => { - void checkGameRunning() - }, 3000) - return () => clearInterval(interval) - }, [checkGameRunning]) - // 同步当前分辨率到store(初始化时) useEffect(() => { if (tool.state.videoSetting) { @@ -319,19 +113,6 @@ export function FpsTest() { } }, [tool.state.videoSetting, fpsTest]) - // 获取硬件信息 - useEffect(() => { - const fetchHardwareInfo = async () => { - try { - const sys = await allSysInfo() - setHardwareInfo(sys) - } catch (error) { - console.error("获取硬件信息失败:", error) - } - } - void fetchHardwareInfo() - }, []) - // 停止测试 const stopTest = useCallback(async () => { // 如果是批量测试,设置中止标志 @@ -504,18 +285,32 @@ export function FpsTest() { p1, rawResult: parsed.data, videoSetting: currentVideoSetting, - hardwareInfo: hardwareInfo + hardwareInfo: hardwareInfo?.systemInfo ? { - cpu: hardwareInfo.cpus[0]?.brand || null, - cpuCount: hardwareInfo.cpu_count || null, - os: - hardwareInfo.name && hardwareInfo.os_version - ? `${hardwareInfo.name} ${hardwareInfo.os_version}` - : null, - memory: hardwareInfo.total_memory - ? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024) + cpu: hardwareInfo.systemInfo.cpus[0]?.brand || null, + cpuCount: hardwareInfo.systemInfo.cpu_count || null, + os: (() => { + const osVersion = hardwareInfo.systemInfo.os_version || null + // 使用 OSDisplayVersion 作为版本代码(如 "25H2") + const osDisplayVersion = hardwareInfo.computerInfo?.OSDisplayVersion || null + + let osStr = hardwareInfo.systemInfo.name || null + if (!osStr) return null + + if (osVersion) { + osStr += ` ${osVersion}` + } + if (osDisplayVersion) { + osStr += ` ${osDisplayVersion}` + } + return osStr + })(), + memory: hardwareInfo.systemInfo.total_memory + ? Math.round(hardwareInfo.systemInfo.total_memory / 1024 / 1024 / 1024) + : null, + gpu: hardwareInfo.gpuInfo + ? hardwareInfo.gpuInfo.model : null, - gpu: null, monitor: null, } : null, @@ -544,18 +339,32 @@ export function FpsTest() { p1, rawResult: parsed.data, videoSetting: currentVideoSetting, - hardwareInfo: hardwareInfo + hardwareInfo: hardwareInfo?.systemInfo ? { - cpu: hardwareInfo.cpus[0]?.brand || null, - cpuCount: hardwareInfo.cpu_count || null, - os: - hardwareInfo.name && hardwareInfo.os_version - ? `${hardwareInfo.name} ${hardwareInfo.os_version}` - : null, - memory: hardwareInfo.total_memory - ? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024) + cpu: hardwareInfo.systemInfo.cpus[0]?.brand || null, + cpuCount: hardwareInfo.systemInfo.cpu_count || null, + os: (() => { + const osVersion = hardwareInfo.systemInfo.os_version || null + // 使用 OSDisplayVersion 作为版本代码(如 "25H2") + const osDisplayVersion = hardwareInfo.computerInfo?.OSDisplayVersion || null + + let osStr = hardwareInfo.systemInfo.name || null + if (!osStr) return null + + if (osVersion) { + osStr += ` ${osVersion}` + } + if (osDisplayVersion) { + osStr += ` ${osDisplayVersion}` + } + return osStr + })(), + memory: hardwareInfo.systemInfo.total_memory + ? Math.round(hardwareInfo.systemInfo.total_memory / 1024 / 1024 / 1024) + : null, + gpu: hardwareInfo.gpuInfo + ? hardwareInfo.gpuInfo.model : null, - gpu: null, monitor: null, } : null, @@ -1009,18 +818,20 @@ export function FpsTest() { 1 )}\nP1低帧: ${avgP1.toFixed(1)}`, videoSetting: tool.store.state.videoSetting, - hardwareInfo: hardwareInfo + hardwareInfo: hardwareInfo?.systemInfo ? { - cpu: hardwareInfo.cpus[0]?.brand || null, - cpuCount: hardwareInfo.cpu_count || null, + cpu: hardwareInfo.systemInfo.cpus[0]?.brand || null, + cpuCount: hardwareInfo.systemInfo.cpu_count || null, os: - hardwareInfo.name && hardwareInfo.os_version - ? `${hardwareInfo.name} ${hardwareInfo.os_version}` + hardwareInfo.systemInfo.name && hardwareInfo.systemInfo.os_version + ? `${hardwareInfo.systemInfo.name} ${hardwareInfo.systemInfo.os_version}` : null, - memory: hardwareInfo.total_memory - ? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024) + memory: hardwareInfo.systemInfo.total_memory + ? Math.round(hardwareInfo.systemInfo.total_memory / 1024 / 1024 / 1024) + : null, + gpu: hardwareInfo.gpuInfo + ? `${hardwareInfo.gpuInfo.vendor} ${hardwareInfo.gpuInfo.model}` : null, - gpu: null, monitor: null, } : null, @@ -1133,18 +944,20 @@ export function FpsTest() { 1 )}\nP1低帧: ${avgP1.toFixed(1)}`, videoSetting: tool.store.state.videoSetting, - hardwareInfo: hardwareInfo + hardwareInfo: hardwareInfo?.systemInfo ? { - cpu: hardwareInfo.cpus[0]?.brand || null, - cpuCount: hardwareInfo.cpu_count || null, + cpu: hardwareInfo.systemInfo.cpus[0]?.brand || null, + cpuCount: hardwareInfo.systemInfo.cpu_count || null, os: - hardwareInfo.name && hardwareInfo.os_version - ? `${hardwareInfo.name} ${hardwareInfo.os_version}` + hardwareInfo.systemInfo.name && hardwareInfo.systemInfo.os_version + ? `${hardwareInfo.systemInfo.name} ${hardwareInfo.systemInfo.os_version}` : null, - memory: hardwareInfo.total_memory - ? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024) + memory: hardwareInfo.systemInfo.total_memory + ? Math.round(hardwareInfo.systemInfo.total_memory / 1024 / 1024 / 1024) + : null, + gpu: hardwareInfo.gpuInfo + ? `${hardwareInfo.gpuInfo.vendor} ${hardwareInfo.gpuInfo.model}` : null, - gpu: null, monitor: null, } : null, @@ -1170,19 +983,7 @@ export function FpsTest() { // 判断是否可以开始测试(批量测试进行中时不能开始新测试) const canStartTest = (!tool.state.autoCloseGame ? !isGameRunning : true) && !isBatchTesting - // 格式化视频设置摘要 - const formatVideoSettingSummary = ( - videoSetting: typeof tool.state.videoSetting | null - ): string => { - if (!videoSetting) return "N/A" - const resolution = `${videoSetting.defaultres}x${videoSetting.defaultresheight}` - const refreshRate = - videoSetting.refreshrate_denominator === "1" - ? videoSetting.refreshrate_numerator - : `${videoSetting.refreshrate_numerator}/${videoSetting.refreshrate_denominator}` - const msaa = videoSetting.msaa_samples === "0" ? "无" : `${videoSetting.msaa_samples}x` - return `${resolution}@${refreshRate}Hz, MSAA:${msaa}` - } + // formatVideoSettingSummary已提取到utils/csv-export.ts // 打开备注编辑对话框 const handleEditNote = (resultId: string, currentNote: string) => { @@ -1202,156 +1003,9 @@ export function FpsTest() { } } - // 导出CSV - const handleExportCSV = async () => { - if (fpsTest.state.results.length === 0) { - addToast({ title: "没有测试数据可导出", color: "warning" }) - return - } - - try { - // 构建CSV内容 - const headers = [ - "测试时间", - "测试地图", - "平均帧", - "P1低帧", - "CPU", - "系统版本", - "GPU", - "内存(GB)", - "分辨率", - "视频设置", - "备注", - ] - - const csvRows = [headers.join(",")] - - for (const result of fpsTest.state.results) { - const row = [ - `"${result.testTime}"`, - `"${result.mapLabel}"`, - result.avg !== null ? result.avg.toFixed(1) : "N/A", - result.p1 !== null ? result.p1.toFixed(1) : "N/A", - `"${result.hardwareInfo?.cpu || "N/A"}"`, - `"${result.hardwareInfo?.os || "N/A"}"`, - `"${result.hardwareInfo?.gpu || "N/A"}"`, - result.hardwareInfo?.memory ? result.hardwareInfo.memory.toString() : "N/A", - result.videoSetting - ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` - : "N/A", - `"${formatVideoSettingSummary(result.videoSetting)}"`, - `"${result.note || ""}"`, - ] - csvRows.push(row.join(",")) - } - - const csvContent = csvRows.join("\n") - - // 添加UTF-8 BOM以确保Excel等软件正确识别编码 - const csvContentWithBOM = "\uFEFF" + csvContent - - // 使用文件保存对话框 - const filePath = await save({ - filters: [ - { - name: "CSV", - extensions: ["csv"], - }, - ], - defaultPath: `fps_test_results_${new Date().toISOString().split("T")[0]}.csv`, - }) - - if (filePath) { - await writeTextFile(filePath, csvContentWithBOM) - addToast({ title: "导出成功", color: "success" }) - } - } catch (error) { - console.error("导出CSV失败:", error) - addToast({ - title: `导出失败: ${error instanceof Error ? error.message : String(error)}`, - color: "danger", - }) - } - } - - // 仅导出平均结果CSV - const handleExportAverageCSV = async () => { - // 过滤备注中包含"平均"的结果 - const averageResults = fpsTest.state.results.filter( - (result) => result.note && result.note.includes("平均") - ) - - if (averageResults.length === 0) { - addToast({ title: "没有平均结果数据可导出", color: "warning" }) - return - } - - try { - // 构建CSV内容 - const headers = [ - "测试时间", - "测试地图", - "平均帧", - "P1低帧", - "CPU", - "系统版本", - "GPU", - "内存(GB)", - "分辨率", - "视频设置", - "备注", - ] - - const csvRows = [headers.join(",")] - - for (const result of averageResults) { - const row = [ - `"${result.testTime}"`, - `"${result.mapLabel}"`, - result.avg !== null ? result.avg.toFixed(1) : "N/A", - result.p1 !== null ? result.p1.toFixed(1) : "N/A", - `"${result.hardwareInfo?.cpu || "N/A"}"`, - `"${result.hardwareInfo?.os || "N/A"}"`, - `"${result.hardwareInfo?.gpu || "N/A"}"`, - result.hardwareInfo?.memory ? result.hardwareInfo.memory.toString() : "N/A", - result.videoSetting - ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` - : "N/A", - `"${formatVideoSettingSummary(result.videoSetting)}"`, - `"${result.note || ""}"`, - ] - csvRows.push(row.join(",")) - } - - const csvContent = csvRows.join("\n") - - // 添加UTF-8 BOM以确保Excel等软件正确识别编码 - const csvContentWithBOM = "\uFEFF" + csvContent - - // 使用文件保存对话框 - const filePath = await save({ - filters: [ - { - name: "CSV", - extensions: ["csv"], - }, - ], - defaultPath: `fps_test_average_results_${new Date().toISOString().split("T")[0]}.csv`, - }) - - if (filePath) { - await writeTextFile(filePath, csvContentWithBOM) - addToast({ title: "导出成功", color: "success" }) - } - } catch (error) { - console.error("导出CSV失败:", error) - addToast({ - title: `导出失败: ${error instanceof Error ? error.message : String(error)}`, - color: "danger", - }) - } - } + // CSV导出函数已提取到utils/csv-export.ts + const handleExportCSVWrapper = () => handleExportCSV([...fpsTest.state.results]) + const handleExportAverageCSVWrapper = () => handleExportAverageCSV([...fpsTest.state.results]) // 应用预设分辨率 const handlePresetResolution = (preset: { width: string; height: string; label: string }) => { @@ -1378,32 +1032,14 @@ export function FpsTest() {
- {batchTestProgress && ( -
-
- -
- {batchTestProgress.current}/{batchTestProgress.total} -
-
-
- )} + {showResultsTable && ( <> - - @@ -1450,172 +1086,25 @@ export function FpsTest() { {showResultsTable ? ( -
- - - 测试时间 - 测试地图 - 平均帧 - P1低帧 - CPU - 系统版本 - GPU - 内存 - 视频设置 - 备注 - 操作 - - - {fpsTest.state.results.map((result) => ( - - {result.testTime} - -
- {result.mapLabel} -
-
- - {result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"} - - - {result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"} - - - -
- {result.hardwareInfo?.cpu || "N/A"} -
-
-
- -
- {result.hardwareInfo?.os || "N/A"} -
-
- -
- {result.hardwareInfo?.gpu || "N/A"} -
-
- - {result.hardwareInfo?.memory ? `${result.hardwareInfo.memory}GB` : "N/A"} - - - - - {result.videoSetting - ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` - : "N/A"} - - - - - handleEditNote(result.id, result.note || "")} - /> - - -
- -
-
-
- ))} -
-
-
+ ) : (
- {/* 备注单独一行 - 放在最上面 */} -
-
- -
- { - if (!isMonitoring) { - setSelectedMapIndex(Number(key)) - } - }} - aria-label="测试地图选择" - size="sm" - radius="lg" - > - {BENCHMARK_MAPS.map((map, index) => ( - - ))} - -
-
-
- - -
-
- -
- -
-
-
- - + {/* 启动项占满一行,右侧放置分辨率和全屏切换 */}
{/* 自定义启动项 */} @@ -1632,164 +1121,17 @@ export function FpsTest() { />
- - {/* 分辨率和全屏/窗口化设置 */} -
- {/* 分辨率设置 */} -
- {/* 工具栏:分辨率标签 + 按钮 */} -
- -
- - {!isResolutionGroupEnabled && ( - <> - - - - - - {PRESET_RESOLUTIONS.map( - (preset: { width: string; height: string; label: string }) => ( - handlePresetResolution(preset)} - > - {preset.label} - - ) - )} - - - - - )} - {isResolutionGroupEnabled && ( - <> - - - - - - {PRESET_RESOLUTIONS.map( - (preset: { width: string; height: string; label: string }) => ( - { - if (!isMonitoring) { - fpsTest.addResolutionToGroup({ - width: preset.width, - height: preset.height, - label: preset.label, - }) - } - }} - > - {preset.label} - - ) - )} - - - - - )} -
-
- {/* 主体:宽高输入框 + 全屏按钮(始终显示) */} -
-
- fpsTest.setResolution(val, resolutionHeight)} - isDisabled={isResolutionGroupEnabled ? isMonitoring : (!isResolutionEnabled || isMonitoring)} - className="w-20" - /> - x - fpsTest.setResolution(resolutionWidth, val)} - isDisabled={isResolutionGroupEnabled ? isMonitoring : (!isResolutionEnabled || isMonitoring)} - className="w-20" - /> -
- -
-
-
+
{/* 工具栏:按钮靠右对齐 */} @@ -1867,41 +1209,11 @@ export function FpsTest() {
)} - {isMonitoring && ( - - 正在监听中... - - )} - - {testResult && - testTimestamp && - (() => { - const { avg, p1 } = extractFpsMetrics(testResult) - return ( - <> -
-
测试时间
-
{testTimestamp}
-
-
-
-
-
平均帧
-
- {avg !== null ? `${avg.toFixed(1)}` : "N/A"} -
-
-
-
P1低帧
-
- {p1 !== null ? `${p1.toFixed(1)}` : "N/A"} -
-
-
-
- - ) - })()} +
{testResult && ( diff --git a/src/components/cstb/FpsTest/components/BatchTestProgress.tsx b/src/components/cstb/FpsTest/components/BatchTestProgress.tsx new file mode 100644 index 0000000..c45805d --- /dev/null +++ b/src/components/cstb/FpsTest/components/BatchTestProgress.tsx @@ -0,0 +1,31 @@ +import { CircularProgress } from "@heroui/react" +import type { BatchTestProgress as BatchTestProgressType } from "../types" + +interface BatchTestProgressProps { + progress: BatchTestProgressType | null +} + +export function BatchTestProgress({ progress }: BatchTestProgressProps) { + if (!progress) return null + + return ( +
+
+ +
+ {progress.current}/{progress.total} +
+
+
+ ) +} + diff --git a/src/components/cstb/FpsTest/components/NoteCell.tsx b/src/components/cstb/FpsTest/components/NoteCell.tsx new file mode 100644 index 0000000..bbb8a79 --- /dev/null +++ b/src/components/cstb/FpsTest/components/NoteCell.tsx @@ -0,0 +1,27 @@ +import { Button } from "@heroui/react" +import { Edit } from "@icon-park/react" + +interface NoteCellProps { + note: string + onEdit: () => void +} + +export function NoteCell({ note, onEdit }: NoteCellProps) { + return ( +
+ + {note || "无备注"} + + +
+ ) +} + diff --git a/src/components/cstb/FpsTest/components/ResolutionConfig.tsx b/src/components/cstb/FpsTest/components/ResolutionConfig.tsx new file mode 100644 index 0000000..41534be --- /dev/null +++ b/src/components/cstb/FpsTest/components/ResolutionConfig.tsx @@ -0,0 +1,193 @@ +import { Button, Input, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Chip } from "@heroui/react" +import { List } from "@icon-park/react" +import { PRESET_RESOLUTIONS } from "../constants" +import type { Resolution } from "../types" +import type { useFpsTestStore } from "@/store/fps_test" + +interface ResolutionConfigProps { + resolutionWidth: string + resolutionHeight: string + isResolutionEnabled: boolean + isResolutionGroupEnabled: boolean + isFullscreen: boolean + resolutionGroup: Resolution[] + isMonitoring: boolean + fpsTest: ReturnType + onPresetResolution: (preset: Resolution) => void +} + +export function ResolutionConfig({ + resolutionWidth, + resolutionHeight, + isResolutionEnabled, + isResolutionGroupEnabled, + isFullscreen, + resolutionGroup, + isMonitoring, + fpsTest, + onPresetResolution, +}: ResolutionConfigProps) { + return ( +
+ {/* 分辨率设置 */} +
+ {/* 工具栏:分辨率标签 + 按钮 */} +
+ +
+ + {!isResolutionGroupEnabled && ( + <> + + + + + + {PRESET_RESOLUTIONS.map((preset) => ( + onPresetResolution(preset)} + > + {preset.label} + + ))} + + + + + )} + {isResolutionGroupEnabled && ( + <> + + + + + + {PRESET_RESOLUTIONS.map((preset) => ( + { + if (!isMonitoring) { + fpsTest.addResolutionToGroup({ + width: preset.width, + height: preset.height, + label: preset.label, + }) + } + }} + > + {preset.label} + + ))} + + + + + )} +
+
+ {/* 主体:宽高输入框 + 全屏按钮(始终显示) */} +
+
+ fpsTest.setResolution(val, resolutionHeight)} + isDisabled={ + isResolutionGroupEnabled + ? isMonitoring + : !isResolutionEnabled || isMonitoring + } + className="w-20" + /> + x + fpsTest.setResolution(resolutionWidth, val)} + isDisabled={ + isResolutionGroupEnabled + ? isMonitoring + : !isResolutionEnabled || isMonitoring + } + className="w-20" + /> +
+ +
+
+
+ ) +} + diff --git a/src/components/cstb/FpsTest/components/TestConfigPanel.tsx b/src/components/cstb/FpsTest/components/TestConfigPanel.tsx new file mode 100644 index 0000000..7e06496 --- /dev/null +++ b/src/components/cstb/FpsTest/components/TestConfigPanel.tsx @@ -0,0 +1,112 @@ +import { Tabs, Tab, Select, SelectItem, Input } from "@heroui/react" +import { BENCHMARK_MAPS } from "../constants" +import type { useFpsTestStore } from "@/store/fps_test" + +interface TestConfigPanelProps { + selectedMapIndex: number + onMapIndexChange: (index: number) => void + batchTestCount: number + onBatchTestCountChange: (count: number) => void + testNote: string + onTestNoteChange: (note: string) => void + customLaunchOption: string + onCustomLaunchOptionChange: (option: string) => void + isMonitoring: boolean + fpsTest: ReturnType +} + +export function TestConfigPanel({ + selectedMapIndex, + onMapIndexChange, + batchTestCount, + onBatchTestCountChange, + testNote, + onTestNoteChange, + customLaunchOption, + onCustomLaunchOptionChange, + isMonitoring, + fpsTest, +}: TestConfigPanelProps) { + return ( + <> + {/* 备注单独一行 - 放在最上面 */} +
+
+ +
+ { + if (!isMonitoring) { + onMapIndexChange(Number(key)) + } + }} + aria-label="测试地图选择" + size="sm" + radius="lg" + > + {BENCHMARK_MAPS.map((map, index) => ( + + ))} + +
+
+
+ + +
+
+ +
+ +
+
+
+ + {/* 启动项占满一行,右侧放置分辨率和全屏切换 */} +
+ {/* 自定义启动项 */} +
+ +
+ +
+
+
+ + ) +} + diff --git a/src/components/cstb/FpsTest/components/TestResultDisplay.tsx b/src/components/cstb/FpsTest/components/TestResultDisplay.tsx new file mode 100644 index 0000000..7fb4e2e --- /dev/null +++ b/src/components/cstb/FpsTest/components/TestResultDisplay.tsx @@ -0,0 +1,53 @@ +import { Chip } from "@heroui/react" +import { extractFpsMetrics } from "../utils/fps-metrics" + +interface TestResultDisplayProps { + testResult: string | null + testTimestamp: string | null + isMonitoring: boolean +} + +export function TestResultDisplay({ + testResult, + testTimestamp, + isMonitoring, +}: TestResultDisplayProps) { + if (!testResult || !testTimestamp) { + if (isMonitoring) { + return ( + + 正在监听中... + + ) + } + return null + } + + const { avg, p1 } = extractFpsMetrics(testResult) + + return ( + <> +
+
测试时间
+
{testTimestamp}
+
+
+
+
+
平均帧
+
+ {avg !== null ? `${avg.toFixed(1)}` : "N/A"} +
+
+
+
P1低帧
+
+ {p1 !== null ? `${p1.toFixed(1)}` : "N/A"} +
+
+
+
+ + ) +} + diff --git a/src/components/cstb/FpsTest/components/TestResultsTable.tsx b/src/components/cstb/FpsTest/components/TestResultsTable.tsx new file mode 100644 index 0000000..0739ef8 --- /dev/null +++ b/src/components/cstb/FpsTest/components/TestResultsTable.tsx @@ -0,0 +1,135 @@ +import { + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + Button, + Tooltip, +} from "@heroui/react" +import { Delete } from "@icon-park/react" +import { addToast } from "@heroui/react" +import { NoteCell } from "./NoteCell" +import { formatVideoSettingSummary } from "../utils/csv-export" +import type { FpsTestResult } from "@/store/fps_test" +import type { useFpsTestStore } from "@/store/fps_test" + +interface TestResultsTableProps { + results: FpsTestResult[] + fpsTest: ReturnType + onEditNote: (resultId: string, currentNote: string) => void +} + +export function TestResultsTable({ + results, + fpsTest, + onEditNote, +}: TestResultsTableProps) { + return ( +
+ + + 测试时间 + 测试地图 + 平均帧 + P1低帧 + CPU + 系统版本 + GPU + 内存 + 视频设置 + 备注 + + 操作 + + + + {results.map((result) => ( + + + {result.testTime} + + +
{result.mapLabel}
+
+ + {result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"} + + + {result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"} + + + +
+ {result.hardwareInfo?.cpu || "N/A"} +
+
+
+ +
{result.hardwareInfo?.os || "N/A"}
+
+ +
{result.hardwareInfo?.gpu || "N/A"}
+
+ + {result.hardwareInfo?.memory + ? `${result.hardwareInfo.memory}GB` + : "N/A"} + + + + + {result.videoSetting + ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` + : "N/A"} + + + + + onEditNote(result.id, result.note || "")} + /> + + +
+ +
+
+
+ ))} +
+
+
+ ) +} + diff --git a/src/components/cstb/FpsTest/constants.ts b/src/components/cstb/FpsTest/constants.ts new file mode 100644 index 0000000..44d1577 --- /dev/null +++ b/src/components/cstb/FpsTest/constants.ts @@ -0,0 +1,32 @@ +// 测试地图配置 +export const BENCHMARK_MAPS = [ + { + name: "de_dust2_benchmark", + workshopId: "3240880604", + map: "de_dust2_benchmark", + label: "Dust2", + }, + { + name: "de_ancient", + workshopId: "3472126051", + map: "de_ancient", + label: "Ancient", + }, +] as const + +// 测试超时时间(毫秒) +export const TEST_TIMEOUT = 200000 // 200秒 + +// 预设分辨率列表 +export const PRESET_RESOLUTIONS = [ + { width: "800", height: "600", label: "800x600" }, + { width: "1024", height: "768", label: "1024x768" }, + { width: "1280", height: "960", label: "1280x960" }, + { width: "1440", height: "1080", label: "1440x1080" }, + { width: "1920", height: "1080", label: "1920x1080" }, + { width: "1920", height: "1440", label: "1920x1440" }, + { width: "2560", height: "1440", label: "2560x1440" }, + { width: "2880", height: "2160", label: "2880x2160" }, + { width: "3840", height: "2160", label: "3840x2160" }, +] as const + diff --git a/src/components/cstb/FpsTest/hooks/useGameMonitor.ts b/src/components/cstb/FpsTest/hooks/useGameMonitor.ts new file mode 100644 index 0000000..8ee5b5a --- /dev/null +++ b/src/components/cstb/FpsTest/hooks/useGameMonitor.ts @@ -0,0 +1,32 @@ +import { useState, useEffect, useCallback } from "react" +import { invoke } from "@tauri-apps/api/core" + +export function useGameMonitor() { + const [isGameRunning, setIsGameRunning] = useState(false) + + // 检测游戏是否运行 + const checkGameRunning = useCallback(async () => { + try { + const result = await invoke("check_process_running", { + processName: "cs2.exe", + }).catch(() => false) + setIsGameRunning(result) + return result + } catch { + setIsGameRunning(false) + return false + } + }, []) + + // 定期检测游戏运行状态 + useEffect(() => { + void checkGameRunning() + const interval = setInterval(() => { + void checkGameRunning() + }, 3000) + return () => clearInterval(interval) + }, [checkGameRunning]) + + return { isGameRunning, checkGameRunning } +} + diff --git a/src/components/cstb/FpsTest/hooks/useHardwareInfo.ts b/src/components/cstb/FpsTest/hooks/useHardwareInfo.ts new file mode 100644 index 0000000..666692e --- /dev/null +++ b/src/components/cstb/FpsTest/hooks/useHardwareInfo.ts @@ -0,0 +1,62 @@ +import { useState, useEffect } from "react" +import { allSysInfo, type AllSystemInfo } from "tauri-plugin-system-info-api" +import { invoke } from "@tauri-apps/api/core" + +interface GpuInfo { + vendor: string + model: string + family: string + device_id: string + total_vram: number + used_vram: number + load_pct: number + temperature: number +} + +interface ComputerInfo { + OsName?: string + OSDisplayVersion?: string + BiosSMBIOSBIOSVersion?: string + CsManufacturer?: string + CsName?: string + ReleaseId?: string +} + +export interface HardwareInfoWithGpu { + systemInfo: AllSystemInfo | null + gpuInfo: GpuInfo | null + computerInfo: ComputerInfo | null +} + +export function useHardwareInfo() { + const [hardwareInfo, setHardwareInfo] = useState(null) + + useEffect(() => { + const fetchHardwareInfo = async () => { + try { + const [sys, gpuInfo, computerInfo] = await Promise.all([ + allSysInfo(), + invoke("get_gpu_info").catch((error) => { + console.error("获取 GPU 信息失败:", error) + return null + }), + invoke("get_computer_info").catch((error) => { + console.error("获取 PowerShell 信息失败:", error) + return {} as ComputerInfo + }) + ]) + setHardwareInfo({ + systemInfo: sys, + gpuInfo, + computerInfo + }) + } catch (error) { + console.error("获取硬件信息失败:", error) + } + } + void fetchHardwareInfo() + }, []) + + return hardwareInfo +} + diff --git a/src/components/cstb/FpsTest/hooks/useTestMonitor.ts b/src/components/cstb/FpsTest/hooks/useTestMonitor.ts new file mode 100644 index 0000000..fb38250 --- /dev/null +++ b/src/components/cstb/FpsTest/hooks/useTestMonitor.ts @@ -0,0 +1,113 @@ +import { useEffect, useRef } from "react" +import { invoke } from "@tauri-apps/api/core" +import { addToast } from "@heroui/react" +import { readResult, type ReadResultParams } from "../services/resultReader" +import { TEST_TIMEOUT } from "../constants" + +export interface UseTestMonitorParams { + isMonitoring: boolean + cs2Dir: string | null + readResultParams: ReadResultParams + testStartTimestamp: string | null + testStartTime: number | null + autoCloseGame: boolean + onTestComplete: () => void + onTestTimeout: () => void +} + +export function useTestMonitor(params: UseTestMonitorParams) { + const { + isMonitoring, + cs2Dir, + readResultParams, + testStartTimestamp, + testStartTime, + autoCloseGame, + onTestComplete, + onTestTimeout, + } = params + + const monitoringIntervalRef = useRef(null) + const timeoutRef = useRef(null) + + // 开始监控文件更新 + useEffect(() => { + if (isMonitoring && cs2Dir) { + // 每2秒检查一次文件更新 + monitoringIntervalRef.current = setInterval(async () => { + const success = await readResult(readResultParams, true) // 静默读取 + if (success) { + // 读取成功,调用完成回调 + onTestComplete() + // 停止监控 + if (monitoringIntervalRef.current) { + clearInterval(monitoringIntervalRef.current) + monitoringIntervalRef.current = null + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + return + } + }, 2000) // 每2秒检查一次 + + // 设置超时 + if (testStartTime) { + timeoutRef.current = setTimeout(() => { + // 超时,认为测试失败 + if (monitoringIntervalRef.current) { + clearInterval(monitoringIntervalRef.current) + monitoringIntervalRef.current = null + } + onTestTimeout() + }, TEST_TIMEOUT) + } + } else { + // 停止监控 + if (monitoringIntervalRef.current) { + clearInterval(monitoringIntervalRef.current) + monitoringIntervalRef.current = null + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + } + + // 清理函数 + return () => { + if (monitoringIntervalRef.current) { + clearInterval(monitoringIntervalRef.current) + monitoringIntervalRef.current = null + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + } + }, [ + isMonitoring, + cs2Dir, + readResultParams, + testStartTimestamp, + testStartTime, + autoCloseGame, + onTestComplete, + onTestTimeout, + ]) + + return { + stopMonitoring: () => { + if (monitoringIntervalRef.current) { + clearInterval(monitoringIntervalRef.current) + monitoringIntervalRef.current = null + } + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + }, + } +} + diff --git a/src/components/cstb/FpsTest/index.tsx b/src/components/cstb/FpsTest/index.tsx new file mode 100644 index 0000000..56f9214 --- /dev/null +++ b/src/components/cstb/FpsTest/index.tsx @@ -0,0 +1,6 @@ +"use client" +// 导出重构后的FpsTest组件 +// 主组件文件已重构,使用提取的模块 + +export { FpsTest } from "../FpsTest" + diff --git a/src/components/cstb/FpsTest/services/resultReader.ts b/src/components/cstb/FpsTest/services/resultReader.ts new file mode 100644 index 0000000..fd61a98 --- /dev/null +++ b/src/components/cstb/FpsTest/services/resultReader.ts @@ -0,0 +1,271 @@ +import { invoke } from "@tauri-apps/api/core" +import { addToast } from "@heroui/react" +import { parseVProfReport } from "../utils/vprof-parser" +import { compareTimestamps } from "../utils/timestamp" +import { extractFpsMetrics } from "../utils/fps-metrics" +import type { useSteamStore } from "@/store/steam" +import type { useToolStore } from "@/store/tool" +import type { useFpsTestStore } from "@/store/fps_test" +import type { AllSystemInfo } from "tauri-plugin-system-info-api" +import { BENCHMARK_MAPS } from "../constants" + +export interface ReadResultParams { + steam: ReturnType + tool: ReturnType + fpsTest: ReturnType + selectedMapIndex: number + hardwareInfo: AllSystemInfo | null + testNote: string + batchTestProgress: { current: number; total: number } | null + currentTestResolution: { width: string; height: string; label: string } | null + resolutionGroupInfo: { + resIndex: number + totalResolutions: number + totalTestCount: number + currentBatchIndex: number + batchCount: number + } | null + isResolutionGroupEnabled: boolean + testStartTimestamp: string | null + lastTestTimestamp: React.MutableRefObject + onResultRead: (data: { + timestamp: string + data: string + avg: number | null + p1: number | null + }) => void +} + +export async function readResult( + params: ReadResultParams, + silent = false +): Promise { + const { + steam, + tool, + fpsTest, + selectedMapIndex, + hardwareInfo, + testNote, + batchTestProgress, + currentTestResolution, + resolutionGroupInfo, + isResolutionGroupEnabled, + testStartTimestamp, + lastTestTimestamp, + onResultRead, + } = params + + if (!steam.state.cs2Dir) { + if (!silent) { + addToast({ title: "请先配置 CS2 路径", variant: "flat" }) + } + return false + } + + try { + // 获取 console.log 路径 + let consoleLogPath: string + try { + consoleLogPath = await invoke("get_console_log_path", { + csPath: steam.state.cs2Dir, + }) + } catch (error) { + console.error("获取控制台日志路径失败:", error) + if (!silent) { + addToast({ + title: "获取控制台日志路径失败", + color: "warning", + }) + } + return false + } + + // 读取 VProf 报告 + let report: string + try { + report = await invoke("read_vprof_report", { + consoleLogPath: consoleLogPath, + }) + } catch (error) { + console.error("读取性能报告失败:", error) + if (!silent) { + addToast({ + title: "读取性能报告失败", + color: "warning", + }) + } + return false + } + + if (report && report.trim().length > 0) { + const parsed = parseVProfReport(report) + if (parsed) { + // 如果设置了测试开始时间且是自动监听(silent=true),验证报告时间戳是否晚于测试开始时间 + // 手动读取(silent=false)时允许读取任何结果 + if (silent && testStartTimestamp) { + // 如果报告时间戳早于或等于测试开始时间,则视为旧数据,忽略 + if (!compareTimestamps(parsed.timestamp, testStartTimestamp)) { + // 这是旧数据,不处理 + return false + } + } + + // 保存最后一次测试的时间戳(用于平均值记录) + lastTestTimestamp.current = parsed.timestamp + + // 提取 avg 和 p1 值 + const { avg, p1 } = extractFpsMetrics(parsed.data) + + // 保存测试结果 + const now = new Date() + const testDate = now.toISOString() + const mapConfig = BENCHMARK_MAPS[selectedMapIndex] + + // 测试结束后读取视频设置(检测分辨率) + if (steam.state.steamDirValid && steam.currentUser()) { + await tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0) + } + + // 使用读取到的视频设置(测试结束后读取的) + const currentVideoSetting = tool.store.state.videoSetting + + // 如果是批量测试,添加带批量标识和分辨率信息的备注 + if (batchTestProgress) { + let batchNote = "" + if (currentTestResolution) { + batchNote = `[${currentTestResolution.label}]` + } + if (testNote) { + batchNote = batchNote ? `${testNote} ${batchNote}` : testNote + } + + // 如果启用了分辨率组,使用新的备注格式:[分辨率] [批量当前测试/该分辨率批量总数] + if (resolutionGroupInfo && isResolutionGroupEnabled) { + const { currentBatchIndex, batchCount } = resolutionGroupInfo + const batchInfo = `[批量${currentBatchIndex}/${batchCount}]` + batchNote = batchNote ? `${batchNote} ${batchInfo}` : batchInfo + } else { + // 普通批量测试,使用原来的格式 + batchNote = batchNote + ? `${batchNote} [批量${batchTestProgress.current}/${batchTestProgress.total}]` + : `[批量${batchTestProgress.current}/${batchTestProgress.total}]` + } + + fpsTest.addResult({ + id: `${now.getTime()}-${Math.random().toString(36).slice(2, 11)}`, + testTime: parsed.timestamp, + testDate, + mapName: mapConfig?.name || "unknown", + mapLabel: mapConfig?.label || "未知地图", + avg, + p1, + rawResult: parsed.data, + videoSetting: currentVideoSetting, + hardwareInfo: hardwareInfo + ? { + cpu: hardwareInfo.cpus[0]?.brand || null, + cpuCount: hardwareInfo.cpu_count || null, + os: + hardwareInfo.name && hardwareInfo.os_version + ? `${hardwareInfo.name} ${hardwareInfo.os_version}` + : null, + memory: hardwareInfo.total_memory + ? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024) + : null, + gpu: null, + monitor: null, + } + : null, + note: batchNote, + }) + } else { + // 单次测试,添加分辨率信息到备注 + let singleNote = testNote + if (currentTestResolution) { + const resolutionNote = `[${currentTestResolution.label}]` + singleNote = singleNote ? `${testNote} ${resolutionNote}` : resolutionNote + } + + fpsTest.addResult({ + id: `${now.getTime()}-${Math.random().toString(36).slice(2, 11)}`, + testTime: parsed.timestamp, + testDate, + mapName: mapConfig?.name || "unknown", + mapLabel: mapConfig?.label || "未知地图", + avg, + p1, + rawResult: parsed.data, + videoSetting: currentVideoSetting, + hardwareInfo: hardwareInfo + ? { + cpu: hardwareInfo.cpus[0]?.brand || null, + cpuCount: hardwareInfo.cpu_count || null, + os: + hardwareInfo.name && hardwareInfo.os_version + ? `${hardwareInfo.name} ${hardwareInfo.os_version}` + : null, + memory: hardwareInfo.total_memory + ? Math.round(hardwareInfo.total_memory / 1024 / 1024 / 1024) + : null, + gpu: null, + monitor: null, + } + : null, + note: singleNote, // 保存备注(包含分辨率信息) + }) + } + + // 调用回调函数 + onResultRead({ + timestamp: parsed.timestamp, + data: parsed.data, + avg, + p1, + }) + + if (!silent) { + if (avg !== null || p1 !== null) { + addToast({ + title: `已读取并保存测试结果${ + avg !== null ? ` (avg: ${avg.toFixed(1)} FPS)` : "" + }${p1 !== null ? ` (p1: ${p1.toFixed(1)} FPS)` : ""}`, + }) + } else { + addToast({ title: "已读取并保存测试结果(未能提取帧数数据)" }) + } + } + + // 如果启用了自动关闭游戏,则关闭游戏 + if (tool.state.autoCloseGame) { + setTimeout(() => { + void invoke("kill_game").catch(() => {}) + }, 2000) // 延迟2秒关闭,让用户看到结果 + } + + return true + } else if (!silent) { + addToast({ + title: "未能解析测试结果", + variant: "flat", + }) + } + } else if (!silent) { + addToast({ + title: "未能读取测试结果,请确保测试已完成", + variant: "flat", + }) + } + return false + } catch (error) { + if (!silent) { + console.error("读取结果失败:", error) + addToast({ + title: `读取结果失败: ${error instanceof Error ? error.message : String(error)}`, + variant: "flat", + }) + } + return false + } +} + diff --git a/src/components/cstb/FpsTest/services/testRunner.ts b/src/components/cstb/FpsTest/services/testRunner.ts new file mode 100644 index 0000000..ba12c39 --- /dev/null +++ b/src/components/cstb/FpsTest/services/testRunner.ts @@ -0,0 +1,172 @@ +import { invoke } from "@tauri-apps/api/core" +import { addToast } from "@heroui/react" +import { BENCHMARK_MAPS, TEST_TIMEOUT } from "../constants" +import { formatCurrentTimestamp } from "../utils/timestamp" +import type { useSteamStore } from "@/store/steam" +import type { useToolStore } from "@/store/tool" +import type { Resolution } from "../types" + +export interface RunSingleTestParams { + steam: ReturnType + tool: ReturnType + selectedMapIndex: number + resolutionWidth: string + resolutionHeight: string + isResolutionEnabled: boolean + isResolutionGroupEnabled: boolean + isFullscreen: boolean + customLaunchOption: string + autoCloseGame: boolean + checkGameRunning: () => Promise + resolution?: Resolution + testIndex?: number + totalTests?: number + onTestStart: (timestamp: string, startTime: number, resolution: Resolution | null) => void + onTestComplete: () => void +} + +export interface RunSingleTestResult { + success: boolean + testStartTimestamp: string + testStartTime: number + resolution: Resolution +} + +export async function runSingleTest( + params: RunSingleTestParams +): Promise { + const { + steam, + tool, + selectedMapIndex, + resolutionWidth, + resolutionHeight, + isResolutionEnabled, + isResolutionGroupEnabled, + isFullscreen, + customLaunchOption, + autoCloseGame, + checkGameRunning, + resolution, + testIndex, + totalTests, + onTestStart, + } = params + + // 验证路径是否存在且有效 + if (!steam.state.steamDir || !steam.state.cs2Dir) { + addToast({ + title: "Steam 或 CS2 路径未设置,请先配置路径", + color: "warning", + }) + return null + } + + // 验证 Steam 路径是否有效 + if (!steam.state.steamDirValid) { + addToast({ + title: "Steam 路径无效,请检查路径设置", + color: "warning", + }) + return null + } + + const mapConfig = BENCHMARK_MAPS[selectedMapIndex] + if (!mapConfig) { + return null + } + + // 如果启用了自动关闭游戏,检测并关闭正在运行的游戏 + if (autoCloseGame) { + const gameRunning = await checkGameRunning() + if (gameRunning) { + try { + await invoke("kill_game") + // 等待一下确保游戏关闭 + await new Promise((resolve) => setTimeout(resolve, 2000)) + } catch (error) { + console.error("关闭游戏失败:", error) + return null + } + } + } + + // 记录测试开始时间戳(格式:MM/DD HH:mm:ss) + const testStartTimestamp = formatCurrentTimestamp() + const testStartTime = Date.now() + + try { + // 构建启动参数:基础参数 + 分辨率和全屏设置 + 自定义启动项(如果有) + let baseLaunchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}` + + // 使用传入的分辨率,如果没有则使用store中的分辨率 + const currentResolution: Resolution = resolution || { + width: resolutionWidth, + height: resolutionHeight, + label: `${resolutionWidth}x${resolutionHeight}`, + } + + // 添加分辨率设置(如果启用分辨率功能或分辨率组) + if (isResolutionEnabled || isResolutionGroupEnabled) { + // 添加分辨率设置(如果有设置) + if (currentResolution.width && currentResolution.height) { + baseLaunchOption += ` -w ${currentResolution.width} -h ${currentResolution.height}` + } + + // 添加全屏/窗口化设置(独立控制,不依赖游戏设置) + if (isFullscreen) { + baseLaunchOption += ` -fullscreen` + } else { + baseLaunchOption += ` -sw` + } + } + + // 添加自定义启动项(如果有,开头加空格避免粘连) + const launchOption = customLaunchOption.trim() + ? `${baseLaunchOption} ${customLaunchOption.trim()}` + : baseLaunchOption + + // 启动游戏(强制使用worldwide国际服) + try { + await invoke("launch_game", { + steamPath: `${steam.state.steamDir}\\steam.exe`, + launchOption: launchOption, + server: "worldwide", + }) + } catch (error) { + console.error("启动游戏失败:", error) + addToast({ + title: `启动游戏失败: ${error instanceof Error ? error.message : String(error)}`, + color: "danger", + }) + return null + } + + // 调用测试开始回调 + onTestStart(testStartTimestamp, testStartTime, currentResolution) + + const resolutionInfo = currentResolution ? ` (${currentResolution.label})` : "" + if (totalTests && totalTests > 1) { + addToast({ + title: `批量测试 ${testIndex}/${totalTests}${resolutionInfo}:已启动 ${mapConfig.label} 测试,正在自动监听结果...`, + }) + } else { + addToast({ title: `已启动 ${mapConfig.label} 测试${resolutionInfo},正在自动监听结果...` }) + } + + return { + success: true, + testStartTimestamp, + testStartTime, + resolution: currentResolution, + } + } catch (error) { + console.error("启动测试失败:", error) + addToast({ + title: `启动测试失败: ${error instanceof Error ? error.message : String(error)}`, + variant: "flat", + }) + return null + } +} + diff --git a/src/components/cstb/FpsTest/types.ts b/src/components/cstb/FpsTest/types.ts new file mode 100644 index 0000000..9f09bf2 --- /dev/null +++ b/src/components/cstb/FpsTest/types.ts @@ -0,0 +1,25 @@ +// 类型定义文件 +export type Resolution = { + width: string + height: string + label: string +} + +export type BatchTestProgress = { + current: number + total: number +} + +export type FpsMetrics = { + avg: number | null + p1: number | null +} + +export type ResolutionGroupInfo = { + resIndex: number + totalResolutions: number + totalTestCount: number + currentBatchIndex: number + batchCount: number +} + diff --git a/src/components/cstb/FpsTest/utils/csv-export.ts b/src/components/cstb/FpsTest/utils/csv-export.ts new file mode 100644 index 0000000..5421b33 --- /dev/null +++ b/src/components/cstb/FpsTest/utils/csv-export.ts @@ -0,0 +1,175 @@ +import { save } from "@tauri-apps/plugin-dialog" +import { writeTextFile } from "@tauri-apps/plugin-fs" +import { addToast } from "@heroui/react" +import type { VideoSetting } from "@/store/tool" +import type { FpsTestResult } from "@/store/fps_test" + +// 格式化视频设置摘要 +export function formatVideoSettingSummary( + videoSetting: VideoSetting | null +): string { + if (!videoSetting) return "N/A" + const resolution = `${videoSetting.defaultres}x${videoSetting.defaultresheight}` + const refreshRate = + videoSetting.refreshrate_denominator === "1" + ? videoSetting.refreshrate_numerator + : `${videoSetting.refreshrate_numerator}/${videoSetting.refreshrate_denominator}` + const msaa = videoSetting.msaa_samples === "0" ? "无" : `${videoSetting.msaa_samples}x` + return `${resolution}@${refreshRate}Hz, MSAA:${msaa}` +} + +// 导出所有测试结果CSV +export async function handleExportCSV( + results: FpsTestResult[] +) { + if (results.length === 0) { + addToast({ title: "没有测试数据可导出", color: "warning" }) + return + } + + try { + // 构建CSV内容 + const headers = [ + "测试时间", + "测试地图", + "平均帧", + "P1低帧", + "CPU", + "系统版本", + "GPU", + "内存(GB)", + "分辨率", + "视频设置", + "备注", + ] + + const csvRows = [headers.join(",")] + + for (const result of results) { + const row = [ + `"${result.testTime}"`, + `"${result.mapLabel}"`, + result.avg !== null ? result.avg.toFixed(1) : "N/A", + result.p1 !== null ? result.p1.toFixed(1) : "N/A", + `"${result.hardwareInfo?.cpu || "N/A"}"`, + `"${result.hardwareInfo?.os || "N/A"}"`, + `"${result.hardwareInfo?.gpu || "N/A"}"`, + result.hardwareInfo?.memory ? result.hardwareInfo.memory.toString() : "N/A", + result.videoSetting + ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` + : "N/A", + `"${formatVideoSettingSummary(result.videoSetting)}"`, + `"${result.note || ""}"`, + ] + csvRows.push(row.join(",")) + } + + const csvContent = csvRows.join("\n") + + // 添加UTF-8 BOM以确保Excel等软件正确识别编码 + const csvContentWithBOM = "\uFEFF" + csvContent + + // 使用文件保存对话框 + const filePath = await save({ + filters: [ + { + name: "CSV", + extensions: ["csv"], + }, + ], + defaultPath: `fps_test_results_${new Date().toISOString().split("T")[0]}.csv`, + }) + + if (filePath) { + await writeTextFile(filePath, csvContentWithBOM) + addToast({ title: "导出成功", color: "success" }) + } + } catch (error) { + console.error("导出CSV失败:", error) + addToast({ + title: `导出失败: ${error instanceof Error ? error.message : String(error)}`, + color: "danger", + }) + } +} + +// 仅导出平均结果CSV +export async function handleExportAverageCSV( + results: FpsTestResult[] +) { + // 过滤备注中包含"平均"的结果 + const averageResults = results.filter( + (result) => result.note && result.note.includes("平均") + ) + + if (averageResults.length === 0) { + addToast({ title: "没有平均结果数据可导出", color: "warning" }) + return + } + + try { + // 构建CSV内容 + const headers = [ + "测试时间", + "测试地图", + "平均帧", + "P1低帧", + "CPU", + "系统版本", + "GPU", + "内存(GB)", + "分辨率", + "视频设置", + "备注", + ] + + const csvRows = [headers.join(",")] + + for (const result of averageResults) { + const row = [ + `"${result.testTime}"`, + `"${result.mapLabel}"`, + result.avg !== null ? result.avg.toFixed(1) : "N/A", + result.p1 !== null ? result.p1.toFixed(1) : "N/A", + `"${result.hardwareInfo?.cpu || "N/A"}"`, + `"${result.hardwareInfo?.os || "N/A"}"`, + `"${result.hardwareInfo?.gpu || "N/A"}"`, + result.hardwareInfo?.memory ? result.hardwareInfo.memory.toString() : "N/A", + result.videoSetting + ? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}` + : "N/A", + `"${formatVideoSettingSummary(result.videoSetting)}"`, + `"${result.note || ""}"`, + ] + csvRows.push(row.join(",")) + } + + const csvContent = csvRows.join("\n") + + // 添加UTF-8 BOM以确保Excel等软件正确识别编码 + const csvContentWithBOM = "\uFEFF" + csvContent + + // 使用文件保存对话框 + const filePath = await save({ + filters: [ + { + name: "CSV", + extensions: ["csv"], + }, + ], + defaultPath: `fps_test_average_results_${new Date().toISOString().split("T")[0]}.csv`, + }) + + if (filePath) { + await writeTextFile(filePath, csvContentWithBOM) + addToast({ title: "导出成功", color: "success" }) + } + } catch (error) { + console.error("导出CSV失败:", error) + addToast({ + title: `导出失败: ${error instanceof Error ? error.message : String(error)}`, + color: "danger", + }) + } +} + diff --git a/src/components/cstb/FpsTest/utils/fps-metrics.ts b/src/components/cstb/FpsTest/utils/fps-metrics.ts new file mode 100644 index 0000000..c9f459b --- /dev/null +++ b/src/components/cstb/FpsTest/utils/fps-metrics.ts @@ -0,0 +1,43 @@ +// 从 VProf 报告中提取 avg 和 p1 值 +export function extractFpsMetrics(result: string): { avg: number | null; p1: number | null } { + let avg: number | null = null + let p1: number | null = null + + // 查找包含 avg 的行,支持多种格式: + // - "[VProf] FPS: Avg=239.5, P1=228.0" (等号格式) + // - "[VProf] avg: 123.45" (冒号格式) + // - "[VProf] avg 123.45" (空格格式) + const avgMatch = result.match(/avg[=:\s]+(\d+\.?\d*)/i) + if (avgMatch) { + avg = parseFloat(avgMatch[1]) + } + + // 查找包含 p1 的行,支持多种格式: + // - "P1=228.0" (等号格式) + // - "p1: 98.76" (冒号格式) + // - "p1 98.76" (空格格式) + const p1Match = result.match(/p1[=:\s]+(\d+\.?\d*)/i) + if (p1Match) { + p1 = parseFloat(p1Match[1]) + } + + // 如果找不到,尝试查找其他可能的格式 + // 例如:查找包含 "fps" 和数字的行 + if (!avg) { + const fpsMatch = result.match(/fps[=:\s]+(\d+\.?\d*)/i) + if (fpsMatch) { + avg = parseFloat(fpsMatch[1]) + } + } + + // 尝试查找 1% low 或类似的格式 + if (!p1) { + const lowMatch = result.match(/(?:1%|1st|first).*?low[=:\s]+(\d+\.?\d*)/i) + if (lowMatch) { + p1 = parseFloat(lowMatch[1]) + } + } + + return { avg, p1 } +} + diff --git a/src/components/cstb/FpsTest/utils/timestamp.ts b/src/components/cstb/FpsTest/utils/timestamp.ts new file mode 100644 index 0000000..dcedb96 --- /dev/null +++ b/src/components/cstb/FpsTest/utils/timestamp.ts @@ -0,0 +1,66 @@ +// 比较时间戳(格式:MM/DD HH:mm:ss) +// 返回 true 如果 timestamp1 晚于 timestamp2 +export function compareTimestamps(timestamp1: string, timestamp2: string): boolean { + // 解析时间戳:MM/DD HH:mm:ss + const parseTimestamp = (ts: string) => { + const match = ts.match(/(\d{2})\/(\d{2})\s+(\d{2}):(\d{2}):(\d{2})/) + if (!match) return null + const [, month, day, hour, minute, second] = match.map(Number) + return { month, day, hour, minute, second } + } + + const ts1 = parseTimestamp(timestamp1) + const ts2 = parseTimestamp(timestamp2) + + if (!ts1 || !ts2) return false + + // 使用当前年份作为基准 + const now = new Date() + const currentYear = now.getFullYear() + + // 创建日期对象,尝试当前年份 + let date1 = new Date(currentYear, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second) + let date2 = new Date(currentYear, ts2.month - 1, ts2.day, ts2.hour, ts2.minute, ts2.second) + + // 如果 date1 早于 date2,可能是跨年了(比如 date1 是 1月,date2 是 12月) + // 在这种情况下,给 date1 加一年 + if (date1 < date2) { + // 检查是否可能是跨年(月份相差很大) + const monthDiff = (ts1.month - ts2.month + 12) % 12 + if (monthDiff > 6) { + // 可能是跨年,给 date1 加一年 + date1 = new Date(currentYear + 1, ts1.month - 1, ts1.day, ts1.hour, ts1.minute, ts1.second) + } + } + + return date1 > date2 +} + +// 格式化当前时间为时间戳格式(MM/DD HH:mm:ss) +export function formatCurrentTimestamp(): string { + const now = new Date() + const month = String(now.getMonth() + 1).padStart(2, "0") + const day = String(now.getDate()).padStart(2, "0") + const hour = String(now.getHours()).padStart(2, "0") + const minute = String(now.getMinutes()).padStart(2, "0") + const second = String(now.getSeconds()).padStart(2, "0") + return `${month}/${day} ${hour}:${minute}:${second}` +} + +// 将时间戳转换为ISO格式 +export function timestampToISO(timestamp: string): string { + const now = new Date() + const [monthDay, time] = timestamp.split(" ") + const [month, day] = monthDay.split("/") + const [hour, minute, second] = time.split(":") + const testDateTime = new Date( + now.getFullYear(), + parseInt(month) - 1, + parseInt(day), + parseInt(hour), + parseInt(minute), + parseInt(second) + ) + return testDateTime.toISOString() +} + diff --git a/src/components/cstb/FpsTest/utils/vprof-parser.ts b/src/components/cstb/FpsTest/utils/vprof-parser.ts new file mode 100644 index 0000000..00d1e5b --- /dev/null +++ b/src/components/cstb/FpsTest/utils/vprof-parser.ts @@ -0,0 +1,47 @@ +// 解析性能报告,提取时间戳和性能数据 +export function parseVProfReport(rawReport: string): { timestamp: string; data: string } | null { + if (!rawReport) return null + + const lines = rawReport.split("\n") + let timestamp = "" + let inPerformanceSection = false + const performanceLines: string[] = [] + + for (const line of lines) { + // 提取时间戳:格式如 "11/05 01:51:27 [VProf] -- Performance report --" + const timestampMatch = line.match( + /(\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2})\s+\[VProf\]\s+--\s+Performance\s+report\s+--/ + ) + if (timestampMatch) { + timestamp = timestampMatch[1] + inPerformanceSection = true + // 也包含 Performance report 这一行,但移除时间戳 + const lineWithoutTimestamp = line.trim().replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "") + performanceLines.push(lineWithoutTimestamp) + continue + } + + // 如果在性能报告部分 + if (inPerformanceSection) { + const trimmedLine = line.trim() + + // 只收集包含 [VProf] 的行 + if (trimmedLine.includes("[VProf]")) { + // 移除行首的时间戳(格式:MM/DD HH:mm:ss ) + // 例如:"11/05 02:13:56 [VProf] ..." -> "[VProf] ..." + const lineWithoutTimestamp = trimmedLine.replace(/^\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/, "") + performanceLines.push(lineWithoutTimestamp) + } + // 如果遇到空行且已经有数据,可能是报告结束,但不直接结束,因为可能还有更多数据 + // 如果后续没有 [VProf] 行的数据,空行会被过滤掉 + } + } + + if (performanceLines.length === 0) return null + + return { + timestamp, + data: performanceLines.join("\n").trim(), + } +} + diff --git a/todo/refactor-plan.md b/todo/refactor-plan.md new file mode 100644 index 0000000..5dc9cad --- /dev/null +++ b/todo/refactor-plan.md @@ -0,0 +1,286 @@ +# 代码重构规划文档 + +## 统计结果 + +### 超过300行的文件列表 + +| 文件路径 | 行数 | 优先级 | 状态 | +|---------|------|--------|------| +| `src/components/cstb/FpsTest.tsx` | 1949 | 🔴 高 | 待重构 | +| `src/components/cstb/VideoSetting.tsx` | 579 | 🟡 中 | 待重构 | +| `src/components/cstb/SteamUsers.tsx` | 432 | 🟡 中 | 待重构 | + +## 重构目标 + +1. **组件封装**:将大组件拆分为更小的、职责单一的组件 +2. **功能复用**:提取可复用的逻辑和工具函数 +3. **文件拆分**:将大文件拆分为多个小文件,提高可维护性 +4. **代码组织**:按功能模块组织代码结构 + +--- + +## 1. FpsTest.tsx (1949行) - 高优先级 + +### 问题分析 +- 文件过大,难以维护和调试 +- 包含多个职责:测试执行、结果读取、UI渲染、CSV导出等 +- 大量状态管理和副作用逻辑混在一起 +- 工具函数、常量、组件定义都在同一文件 + +### 重构方案 + +#### 1.1 文件结构拆分 +``` +src/components/cstb/FpsTest/ +├── index.tsx # 主组件(精简后约200行) +├── types.ts # 类型定义 +├── constants.ts # 常量定义(BENCHMARK_MAPS, PRESET_RESOLUTIONS等) +├── utils/ +│ ├── vprof-parser.ts # VProf报告解析工具 +│ ├── timestamp.ts # 时间戳处理工具 +│ ├── fps-metrics.ts # FPS指标提取工具 +│ └── csv-export.ts # CSV导出工具 +├── hooks/ +│ ├── useGameMonitor.ts # 游戏运行状态监控 +│ ├── useTestMonitor.ts # 测试结果监控 +│ ├── useBatchTest.ts # 批量测试逻辑 +│ └── useHardwareInfo.ts # 硬件信息获取 +├── components/ +│ ├── TestConfigPanel.tsx # 测试配置面板 +│ ├── ResolutionConfig.tsx # 分辨率配置组件 +│ ├── TestResultsTable.tsx # 测试结果表格 +│ ├── TestResultDisplay.tsx # 测试结果展示 +│ ├── BatchTestProgress.tsx # 批量测试进度 +│ └── NoteCell.tsx # 备注单元格 +└── services/ + ├── testRunner.ts # 测试执行服务 + └── resultReader.ts # 结果读取服务 +``` + +#### 1.2 功能模块拆分 + +**工具函数模块 (utils/)** +- `vprof-parser.ts`: `parseVProfReport`, `extractFpsMetrics` +- `timestamp.ts`: `compareTimestamps`, 时间戳格式化 +- `fps-metrics.ts`: FPS相关计算和格式化 +- `csv-export.ts`: `handleExportCSV`, `handleExportAverageCSV` + +**自定义Hooks (hooks/)** +- `useGameMonitor.ts`: 游戏运行状态检测和监控 +- `useTestMonitor.ts`: 测试结果文件监控逻辑 +- `useBatchTest.ts`: 批量测试状态管理和执行逻辑 +- `useHardwareInfo.ts`: 硬件信息获取和管理 + +**组件拆分 (components/)** +- `TestConfigPanel.tsx`: 测试地图、批量测试次数、备注配置 +- `ResolutionConfig.tsx`: 分辨率设置、全屏/窗口化、分辨率组管理 +- `TestResultsTable.tsx`: 测试结果表格展示(包含删除、编辑备注) +- `TestResultDisplay.tsx`: 当前测试结果显示(时间戳、avg、p1) +- `BatchTestProgress.tsx`: 批量测试进度显示组件 +- `NoteCell.tsx`: 备注单元格组件 + +**服务层 (services/)** +- `testRunner.ts`: `runSingleTest`, `startTest` 等测试执行逻辑 +- `resultReader.ts`: `readResult` 结果读取逻辑 + +#### 1.3 预期效果 +- 主组件文件从1949行减少到约200行 +- 每个子文件控制在100-300行以内 +- 提高代码可维护性和可测试性 +- 便于功能扩展和bug修复 + +--- + +## 2. VideoSetting.tsx (579行) - 中优先级 + +### 问题分析 +- 视频设置配置数组过长(约200行) +- 编辑/只读模式切换逻辑复杂 +- 文件监听和自动刷新逻辑可以提取 + +### 重构方案 + +#### 2.1 文件结构拆分 +``` +src/components/cstb/VideoSetting/ +├── index.tsx # 主组件(精简后约200行) +├── types.ts # 类型定义 +├── config/ +│ └── videoSettingsConfig.ts # 视频设置配置数组 +├── hooks/ +│ ├── useVideoConfig.ts # 视频配置读取/写入 +│ ├── useFileWatcher.ts # 文件监听逻辑 +│ └── useGameRunning.ts # 游戏运行状态检测 +└── components/ + ├── VideoSettingsEditor.tsx # 编辑模式组件 + └── VideoSettingsViewer.tsx # 只读模式组件 +``` + +#### 2.2 功能模块拆分 + +**配置模块 (config/)** +- `videoSettingsConfig.ts`: 提取 `videoSettings` 函数和配置数组 + +**自定义Hooks (hooks/)** +- `useVideoConfig.ts`: 视频配置的读取、写入、刷新逻辑 +- `useFileWatcher.ts`: 文件变动监听和自动刷新 +- `useGameRunning.ts`: 游戏运行状态检测(可复用) + +**组件拆分 (components/)** +- `VideoSettingsEditor.tsx`: 编辑模式下的所有控件 +- `VideoSettingsViewer.tsx`: 只读模式下的展示 + +#### 2.3 预期效果 +- 主组件文件从579行减少到约200行 +- 配置数据独立管理,便于维护 +- 编辑/查看逻辑分离,代码更清晰 + +--- + +## 3. SteamUsers.tsx (432行) - 中优先级 + +### 问题分析 +- 三种视图模式的渲染逻辑重复 +- 用户项渲染函数较长 +- 可以提取为独立组件 + +### 重构方案 + +#### 3.1 文件结构拆分 +``` +src/components/cstb/SteamUsers/ +├── index.tsx # 主组件(精简后约150行) +├── types.ts # 类型定义 +├── components/ +│ ├── UserCard.tsx # 卡片视图用户项 +│ ├── UserListItem.tsx # 列表视图用户项 +│ └── UserListLargeItem.tsx # 大列表视图用户项 +└── hooks/ + └── useSteamUsers.ts # 用户数据获取和管理 +``` + +#### 3.2 功能模块拆分 + +**组件拆分 (components/)** +- `UserCard.tsx`: 卡片样式用户项组件 +- `UserListItem.tsx`: 列表样式用户项组件 +- `UserListLargeItem.tsx`: 大列表样式用户项组件 + +**自定义Hooks (hooks/)** +- `useSteamUsers.ts`: 用户数据获取、刷新、模拟数据逻辑 + +#### 3.3 预期效果 +- 主组件文件从432行减少到约150行 +- 每种视图模式独立组件,便于维护 +- 减少代码重复 + +--- + +## 重构执行计划 + +### 阶段1: FpsTest.tsx 重构(最高优先级) +1. ✅ 创建目录结构和基础文件 +2. ✅ 提取工具函数到 `utils/` 目录 + - ✅ `vprof-parser.ts` - VProf报告解析 + - ✅ `timestamp.ts` - 时间戳处理 + - ✅ `fps-metrics.ts` - FPS指标提取 + - ✅ `csv-export.ts` - CSV导出功能 +3. ✅ 提取自定义Hooks到 `hooks/` 目录 + - ✅ `useGameMonitor.ts` - 游戏运行状态监控 + - ✅ `useHardwareInfo.ts` - 硬件信息获取 + - ⏳ `useTestMonitor.ts` - 测试结果监控(待完成) + - ⏳ `useBatchTest.ts` - 批量测试逻辑(待完成) +4. ✅ 提取部分UI组件到 `components/` 目录 + - ✅ `NoteCell.tsx` - 备注单元格组件 + - ⏳ `TestConfigPanel.tsx` - 测试配置面板(待完成) + - ⏳ `ResolutionConfig.tsx` - 分辨率配置(待完成) + - ⏳ `TestResultsTable.tsx` - 测试结果表格(待完成) + - ⏳ `TestResultDisplay.tsx` - 测试结果展示(待完成) + - ⏳ `BatchTestProgress.tsx` - 批量测试进度(待完成) +5. ⏳ 提取服务层逻辑到 `services/` 目录 + - ⏳ `testRunner.ts` - 测试执行服务(待完成) + - ⏳ `resultReader.ts` - 结果读取服务(待完成) +6. ⏳ 重构主组件,整合所有子模块(进行中) + - ✅ 创建类型定义文件 + - ⏳ 完整重构主组件(需要继续) +7. ⏳ 测试验证功能完整性(待完成) + +### 阶段2: VideoSetting.tsx 重构 +1. ⏳ 提取配置数据 +2. ⏳ 提取自定义Hooks +3. ⏳ 拆分编辑/查看组件 +4. ⏳ 重构主组件 + +### 阶段3: SteamUsers.tsx 重构 +1. ⏳ 提取用户项组件 +2. ⏳ 提取数据管理Hook +3. ⏳ 重构主组件 + +--- + +## 重构原则 + +1. **保持功能完整性**:重构过程中确保所有功能正常工作 +2. **渐进式重构**:分步骤进行,每步完成后验证 +3. **向后兼容**:不改变对外接口,保持组件使用方式不变 +4. **代码复用**:提取公共逻辑,避免重复代码 +5. **类型安全**:保持TypeScript类型完整性 + +--- + +## 已完成的工作 + +### FpsTest.tsx 重构进度 + +#### ✅ 已完成 +1. **目录结构创建** + - ✅ `src/components/cstb/FpsTest/` 目录结构已创建 + +2. **常量提取** + - ✅ `constants.ts` - BENCHMARK_MAPS, PRESET_RESOLUTIONS, TEST_TIMEOUT + +3. **工具函数提取** + - ✅ `utils/vprof-parser.ts` - parseVProfReport函数 + - ✅ `utils/timestamp.ts` - compareTimestamps, formatCurrentTimestamp, timestampToISO + - ✅ `utils/fps-metrics.ts` - extractFpsMetrics函数 + - ✅ `utils/csv-export.ts` - handleExportCSV, handleExportAverageCSV, formatVideoSettingSummary + +4. **自定义Hooks提取** + - ✅ `hooks/useGameMonitor.ts` - 游戏运行状态监控 + - ✅ `hooks/useHardwareInfo.ts` - 硬件信息获取 + +5. **组件提取** + - ✅ `components/NoteCell.tsx` - 备注单元格组件 + +6. **类型定义** + - ✅ `types.ts` - Resolution, BatchTestProgress, FpsMetrics, ResolutionGroupInfo + +#### ⏳ 待完成 +1. **更多Hooks** + - `hooks/useTestMonitor.ts` - 测试结果文件监控逻辑 + - `hooks/useBatchTest.ts` - 批量测试状态管理和执行逻辑 + +2. **更多组件** + - `components/TestConfigPanel.tsx` - 测试地图、批量测试次数、备注配置 + - `components/ResolutionConfig.tsx` - 分辨率设置组件 + - `components/TestResultsTable.tsx` - 测试结果表格 + - `components/TestResultDisplay.tsx` - 当前测试结果显示 + - `components/BatchTestProgress.tsx` - 批量测试进度显示 + +3. **服务层** + - `services/testRunner.ts` - 测试执行服务(runSingleTest, startTest) + - `services/resultReader.ts` - 结果读取服务(readResult) + +4. **主组件重构** + - 使用提取的模块重构 `index.tsx` + - 确保所有功能正常工作 + +## 注意事项 + +1. 重构前确保有完整的测试覆盖(如果有) +2. 重构过程中注意保持状态管理的一致性 +3. 提取Hook时注意依赖项的正确传递 +4. 组件拆分时注意props的类型定义 +5. 工具函数提取时注意副作用处理 +6. **重要**: 原文件 `FpsTest.tsx` 暂时保留,待重构完成并测试通过后再删除 +7. **当前状态**: 已提取基础模块,主组件重构需要继续完成