[feat] more hw info including gpu + refactor fpstest
This commit is contained in:
215
src-tauri/Cargo.lock
generated
215
src-tauri/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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<serde_json::Value, String> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -380,3 +417,62 @@ pub async fn get_computer_info() -> Result<serde_json::Value, String> {
|
||||
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
|
||||
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<Option<GpuInfo>, 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<Vec<MonitorInfo>, String> {
|
||||
Ok(vec![])
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
@@ -45,10 +39,12 @@ function HardwareInfo() {
|
||||
const { mutate } = useSWRConfig()
|
||||
|
||||
return (
|
||||
<ToolButton onClick={() => {
|
||||
<ToolButton
|
||||
onClick={() => {
|
||||
// 使用 SWR 的 mutate 来刷新数据
|
||||
mutate("/api/hardware-info")
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Refresh /> 刷新
|
||||
</ToolButton>
|
||||
)
|
||||
@@ -60,26 +56,69 @@ 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<HardwareData> => {
|
||||
// 并行获取系统信息和 PowerShell 信息
|
||||
const [sys, computerInfoData] = await Promise.all([
|
||||
// 并行获取系统信息、PowerShell 信息、GPU 信息、内存信息和显示器信息
|
||||
const [sys, computerInfoData, gpuInfoData, memoryInfoData, monitorInfoData] = await Promise.all([
|
||||
allSysInfo(),
|
||||
invoke<ComputerInfo>("get_computer_info").catch((error) => {
|
||||
console.error("获取 PowerShell 信息失败:", error)
|
||||
return {} as ComputerInfo
|
||||
})
|
||||
}),
|
||||
invoke<GpuInfo | null>("get_gpu_info").catch((error) => {
|
||||
console.error("获取 GPU 信息失败:", error)
|
||||
return null
|
||||
}),
|
||||
invoke<MemoryInfo[]>("get_memory_info").catch((error) => {
|
||||
console.error("获取内存信息失败:", error)
|
||||
return [] as MemoryInfo[]
|
||||
}),
|
||||
invoke<MonitorInfo[]>("get_monitor_info").catch((error) => {
|
||||
console.error("获取显示器信息失败:", error)
|
||||
return [] as MonitorInfo[]
|
||||
}),
|
||||
])
|
||||
|
||||
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)
|
||||
@@ -91,23 +130,25 @@ const hardwareInfoFetcher = async (): Promise<HardwareData> => {
|
||||
|
||||
return {
|
||||
allSysData: sys,
|
||||
computerInfo: computerInfoData
|
||||
computerInfo: computerInfoData,
|
||||
gpuInfo: gpuInfoData,
|
||||
memoryInfo: memoryInfoData,
|
||||
monitorInfo: monitorInfoData,
|
||||
}
|
||||
}
|
||||
|
||||
function HardwareInfoContent() {
|
||||
const { data, isLoading } = useSWR<HardwareData>(
|
||||
"/api/hardware-info",
|
||||
hardwareInfoFetcher,
|
||||
{
|
||||
const { data, isLoading } = useSWR<HardwareData>("/api/hardware-info", hardwareInfoFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
dedupingInterval: 5 * 60 * 1000, // 5分钟内相同请求去重
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const allSysData = data?.allSysData
|
||||
const computerInfo = data?.computerInfo || {}
|
||||
const gpuInfo = data?.gpuInfo
|
||||
const memoryInfo = data?.memoryInfo || []
|
||||
const monitorInfo = data?.monitorInfo || []
|
||||
|
||||
const formatBytes = (bytes?: number) => {
|
||||
if (!bytes) return "未知"
|
||||
@@ -119,25 +160,41 @@ 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
|
||||
// 格式化系统信息: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 averageCpuFrequency =
|
||||
allSysData?.cpus && allSysData.cpus.length > 0
|
||||
? (() => {
|
||||
// 尝试多个可能的频率字段名
|
||||
const frequencies = allSysData.cpus
|
||||
.map(cpu => {
|
||||
.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
|
||||
return typeof freq === "number" && !isNaN(freq) && freq > 0
|
||||
})
|
||||
|
||||
if (frequencies.length === 0) {
|
||||
@@ -176,47 +233,47 @@ function HardwareInfoContent() {
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* 系统信息 Skeleton */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-24 rounded" />
|
||||
<Skeleton className="w-24 h-5 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" />
|
||||
<Skeleton className="w-48 h-8 rounded-full" />
|
||||
<Skeleton className="w-32 h-8 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CPU 信息 Skeleton */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-20 rounded" />
|
||||
<Skeleton className="w-20 h-5 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" />
|
||||
<Skeleton className="w-56 h-8 rounded-full" />
|
||||
<Skeleton className="w-32 h-8 rounded-full" />
|
||||
<Skeleton className="h-8 rounded-full w-28" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内存信息 Skeleton */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-16 rounded" />
|
||||
<Skeleton className="w-16 h-5 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" />
|
||||
<Skeleton className="h-8 rounded-full w-36" />
|
||||
<Skeleton className="w-40 h-8 rounded-full" />
|
||||
<Skeleton className="h-8 rounded-full w-36" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GPU 信息 Skeleton */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-16 rounded" />
|
||||
<Skeleton className="w-16 h-5 rounded" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="h-8 w-52 rounded-full" />
|
||||
<Skeleton className="h-8 rounded-full w-52" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主板信息 Skeleton */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="w-16 h-5 rounded" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Skeleton className="w-40 h-8 rounded-full" />
|
||||
<Skeleton className="h-8 rounded-full w-36" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,27 +286,9 @@ function HardwareInfoContent() {
|
||||
<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>
|
||||
)}
|
||||
<span className="font-medium">系统:</span> {formatSystemInfo()}
|
||||
</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}
|
||||
@@ -263,25 +302,6 @@ function HardwareInfoContent() {
|
||||
</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>
|
||||
@@ -318,7 +338,18 @@ function HardwareInfoContent() {
|
||||
)}
|
||||
{allSysData.total_memory !== undefined && allSysData.used_memory !== undefined && (
|
||||
<Chip>
|
||||
<span className="font-medium">可用:</span> {formatBytes(allSysData.total_memory - allSysData.used_memory)}
|
||||
<span className="font-medium">可用:</span>{" "}
|
||||
{formatBytes(allSysData.total_memory - allSysData.used_memory)}
|
||||
</Chip>
|
||||
)}
|
||||
{memoryInfo.length > 0 && memoryInfo[0].part_number && (
|
||||
<Chip>
|
||||
<span className="font-medium">型号:</span> {memoryInfo[0].part_number}
|
||||
</Chip>
|
||||
)}
|
||||
{memoryInfo.length > 0 && memoryInfo[0].speed && (
|
||||
<Chip>
|
||||
<span className="font-medium">频率:</span> {memoryInfo[0].speed} MHz
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
@@ -326,12 +357,33 @@ function HardwareInfoContent() {
|
||||
)}
|
||||
|
||||
{/* GPU 信息 */}
|
||||
{allSysData?.components && allSysData.components.length > 0 && (
|
||||
{gpuInfo ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-medium text-foreground-600">显卡</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Chip>
|
||||
<span className="font-medium">型号:</span> {gpuInfo.model}
|
||||
</Chip>
|
||||
<Chip>
|
||||
<span className="font-medium">总显存:</span> {formatBytes(gpuInfo.total_vram)}
|
||||
</Chip>
|
||||
<Chip>
|
||||
<span className="font-medium">已用显存:</span> {formatBytes(gpuInfo.used_vram)}
|
||||
</Chip>
|
||||
<Chip>
|
||||
<span className="font-medium">温度:</span> {gpuInfo.temperature.toFixed(2)}°C
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
allSysData?.components &&
|
||||
allSysData.components.length > 0 &&
|
||||
(() => {
|
||||
const gpuComponents = allSysData.components.filter((comp) =>
|
||||
comp.label?.toLowerCase().includes('gpu') ||
|
||||
comp.label?.toLowerCase().includes('graphics') ||
|
||||
comp.label?.toLowerCase().includes('显卡')
|
||||
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
|
||||
@@ -342,9 +394,12 @@ function HardwareInfoContent() {
|
||||
<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 || "未知"}
|
||||
<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>
|
||||
<span className="ml-1">
|
||||
({gpu.temperature}°C{gpu.max !== undefined ? ` / ${gpu.max}°C` : ""})
|
||||
</span>
|
||||
)}
|
||||
</Chip>
|
||||
))}
|
||||
@@ -354,6 +409,45 @@ function HardwareInfoContent() {
|
||||
})()
|
||||
)}
|
||||
|
||||
{/* 主板信息 */}
|
||||
{(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>
|
||||
)}
|
||||
|
||||
{/* 显示器信息 */}
|
||||
{monitorInfo.length > 0 && monitorInfo.some((m) => m.refresh_rate) && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-medium text-foreground-600">显示器</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{monitorInfo.map(
|
||||
(monitor, index) =>
|
||||
monitor.refresh_rate && (
|
||||
<Chip key={index}>
|
||||
<span className="font-medium">
|
||||
刷新率{monitorInfo.length > 1 ? ` ${index + 1}` : ""}:
|
||||
</span>{" "}
|
||||
{monitor.refresh_rate} Hz
|
||||
</Chip>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 电池信息 */}
|
||||
{allSysData?.batteries && allSysData.batteries.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -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 && (
|
||||
<span>({formatBytes(battery.energy)} / {formatBytes(battery.energy_full)})</span>
|
||||
<span>
|
||||
({formatBytes(battery.energy)} / {formatBytes(battery.energy_full)})
|
||||
</span>
|
||||
)}
|
||||
</Chip>
|
||||
))}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
31
src/components/cstb/FpsTest/components/BatchTestProgress.tsx
Normal file
31
src/components/cstb/FpsTest/components/BatchTestProgress.tsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-col gap-1.5 items-center justify-center">
|
||||
<div className="relative">
|
||||
<CircularProgress
|
||||
aria-label="批量测试进度"
|
||||
value={(progress.current / progress.total) * 100}
|
||||
color="primary"
|
||||
size="sm"
|
||||
showValueLabel={false}
|
||||
classNames={{
|
||||
svg: " ",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-xs font-medium">
|
||||
{progress.current}/{progress.total}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
27
src/components/cstb/FpsTest/components/NoteCell.tsx
Normal file
27
src/components/cstb/FpsTest/components/NoteCell.tsx
Normal file
@@ -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 (
|
||||
<div className="flex items-center min-w-0 gap-1">
|
||||
<span className="flex-1 min-w-0 truncate select-text">
|
||||
{note || "无备注"}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={onEdit}
|
||||
className="h-5 min-w-5 shrink-0"
|
||||
>
|
||||
<Edit size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
193
src/components/cstb/FpsTest/components/ResolutionConfig.tsx
Normal file
193
src/components/cstb/FpsTest/components/ResolutionConfig.tsx
Normal file
@@ -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<typeof useFpsTestStore>
|
||||
onPresetResolution: (preset: Resolution) => void
|
||||
}
|
||||
|
||||
export function ResolutionConfig({
|
||||
resolutionWidth,
|
||||
resolutionHeight,
|
||||
isResolutionEnabled,
|
||||
isResolutionGroupEnabled,
|
||||
isFullscreen,
|
||||
resolutionGroup,
|
||||
isMonitoring,
|
||||
fpsTest,
|
||||
onPresetResolution,
|
||||
}: ResolutionConfigProps) {
|
||||
return (
|
||||
<div className="flex items-end gap-2 shrink-0">
|
||||
{/* 分辨率设置 */}
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{/* 工具栏:分辨率标签 + 按钮 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-xs text-default-500 shrink-0">分辨率</label>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isResolutionGroupEnabled ? "solid" : "flat"}
|
||||
color={isResolutionGroupEnabled ? "primary" : "default"}
|
||||
onPress={() => {
|
||||
if (!isMonitoring) {
|
||||
const newValue = !isResolutionGroupEnabled
|
||||
fpsTest.setIsResolutionGroupEnabled(newValue)
|
||||
// 启用分辨率组时,自动启用分辨率功能
|
||||
if (newValue && !isResolutionEnabled) {
|
||||
fpsTest.setIsResolutionEnabled(true)
|
||||
}
|
||||
}
|
||||
}}
|
||||
isDisabled={isMonitoring}
|
||||
className="h-5 gap-1 flex px-1.5 min-w-fit text-xs font-medium"
|
||||
>
|
||||
<List size={12} />
|
||||
多组
|
||||
</Button>
|
||||
{!isResolutionGroupEnabled && (
|
||||
<>
|
||||
<Dropdown placement="bottom-end" className="min-w-fit">
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
className="h-5 min-w-[40px] px-1.5 text-xs"
|
||||
isDisabled={!isResolutionEnabled || isMonitoring}
|
||||
>
|
||||
预设
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="预设分辨率" className="">
|
||||
{PRESET_RESOLUTIONS.map((preset) => (
|
||||
<DropdownItem
|
||||
key={preset.label}
|
||||
onPress={() => onPresetResolution(preset)}
|
||||
>
|
||||
{preset.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isResolutionEnabled ? "solid" : "flat"}
|
||||
color={isResolutionEnabled ? "primary" : "default"}
|
||||
onPress={() => fpsTest.setIsResolutionEnabled(!isResolutionEnabled)}
|
||||
isDisabled={isMonitoring}
|
||||
className="h-5 min-w-[40px] px-1.5 text-xs font-medium"
|
||||
>
|
||||
{isResolutionEnabled ? "启用" : "关闭"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isResolutionGroupEnabled && (
|
||||
<>
|
||||
<Dropdown placement="bottom-end" className="min-w-fit">
|
||||
<DropdownTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
className="h-5 min-w-[40px] px-1.5 text-xs"
|
||||
isDisabled={isMonitoring}
|
||||
>
|
||||
预设
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="预设分辨率" className="">
|
||||
{PRESET_RESOLUTIONS.map((preset) => (
|
||||
<DropdownItem
|
||||
key={preset.label}
|
||||
onPress={() => {
|
||||
if (!isMonitoring) {
|
||||
fpsTest.addResolutionToGroup({
|
||||
width: preset.width,
|
||||
height: preset.height,
|
||||
label: preset.label,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{preset.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => {
|
||||
if (!isMonitoring && resolutionWidth && resolutionHeight) {
|
||||
fpsTest.addResolutionToGroup({
|
||||
width: resolutionWidth,
|
||||
height: resolutionHeight,
|
||||
label: `${resolutionWidth}x${resolutionHeight}`,
|
||||
})
|
||||
}
|
||||
}}
|
||||
isDisabled={isMonitoring || !resolutionWidth || !resolutionHeight}
|
||||
className="h-5 min-w-[40px] px-1.5 text-xs"
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 主体:宽高输入框 + 全屏按钮(始终显示) */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Input
|
||||
size="sm"
|
||||
type="number"
|
||||
placeholder="宽"
|
||||
value={resolutionWidth}
|
||||
onValueChange={(val) => fpsTest.setResolution(val, resolutionHeight)}
|
||||
isDisabled={
|
||||
isResolutionGroupEnabled
|
||||
? isMonitoring
|
||||
: !isResolutionEnabled || isMonitoring
|
||||
}
|
||||
className="w-20"
|
||||
/>
|
||||
<span className="text-xs text-default-400">x</span>
|
||||
<Input
|
||||
size="sm"
|
||||
type="number"
|
||||
placeholder="高"
|
||||
value={resolutionHeight}
|
||||
onValueChange={(val) => fpsTest.setResolution(resolutionWidth, val)}
|
||||
isDisabled={
|
||||
isResolutionGroupEnabled
|
||||
? isMonitoring
|
||||
: !isResolutionEnabled || isMonitoring
|
||||
}
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={isFullscreen ? "solid" : "flat"}
|
||||
color={isFullscreen ? "primary" : "default"}
|
||||
onPress={() => fpsTest.setIsFullscreen(!isFullscreen)}
|
||||
isDisabled={isMonitoring || (!isResolutionEnabled && !isResolutionGroupEnabled)}
|
||||
className="font-medium"
|
||||
>
|
||||
{isFullscreen ? "全屏" : "窗口化"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
112
src/components/cstb/FpsTest/components/TestConfigPanel.tsx
Normal file
112
src/components/cstb/FpsTest/components/TestConfigPanel.tsx
Normal file
@@ -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<typeof useFpsTestStore>
|
||||
}
|
||||
|
||||
export function TestConfigPanel({
|
||||
selectedMapIndex,
|
||||
onMapIndexChange,
|
||||
batchTestCount,
|
||||
onBatchTestCountChange,
|
||||
testNote,
|
||||
onTestNoteChange,
|
||||
customLaunchOption,
|
||||
onCustomLaunchOptionChange,
|
||||
isMonitoring,
|
||||
fpsTest,
|
||||
}: TestConfigPanelProps) {
|
||||
return (
|
||||
<>
|
||||
{/* 备注单独一行 - 放在最上面 */}
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-default-500">测试地图</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tabs
|
||||
selectedKey={String(selectedMapIndex)}
|
||||
onSelectionChange={(key) => {
|
||||
if (!isMonitoring) {
|
||||
onMapIndexChange(Number(key))
|
||||
}
|
||||
}}
|
||||
aria-label="测试地图选择"
|
||||
size="sm"
|
||||
radius="lg"
|
||||
>
|
||||
{BENCHMARK_MAPS.map((map, index) => (
|
||||
<Tab key={String(index)} title={map.label} />
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-default-500">批量测试</label>
|
||||
<Select
|
||||
size="md"
|
||||
selectedKeys={[String(batchTestCount)]}
|
||||
onSelectionChange={(keys) => {
|
||||
const value = Array.from(keys)[0]
|
||||
if (value && !isMonitoring) {
|
||||
onBatchTestCountChange(Number(value))
|
||||
}
|
||||
}}
|
||||
isDisabled={isMonitoring}
|
||||
className="w-24"
|
||||
aria-label="批量测试次数"
|
||||
>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((count) => (
|
||||
<SelectItem
|
||||
key={String(count)}
|
||||
title={count === 1 ? "单次" : `${count}次`}
|
||||
></SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 flex-1">
|
||||
<label className="text-xs text-default-500">备注</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
size="md"
|
||||
placeholder="输入测试备注"
|
||||
value={testNote}
|
||||
onValueChange={onTestNoteChange}
|
||||
isDisabled={isMonitoring}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 启动项占满一行,右侧放置分辨率和全屏切换 */}
|
||||
<div className="flex items-start gap-4">
|
||||
{/* 自定义启动项 */}
|
||||
<div className="flex flex-col flex-1 gap-1.5">
|
||||
<label className="h-5 text-xs text-default-500">自定义启动项</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
size="md"
|
||||
placeholder="输入自定义启动参数(可选)"
|
||||
value={customLaunchOption}
|
||||
onValueChange={onCustomLaunchOptionChange}
|
||||
isDisabled={isMonitoring}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
53
src/components/cstb/FpsTest/components/TestResultDisplay.tsx
Normal file
53
src/components/cstb/FpsTest/components/TestResultDisplay.tsx
Normal file
@@ -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 (
|
||||
<Chip size="lg" color="primary" variant="flat" className="text-xs">
|
||||
正在监听中...
|
||||
</Chip>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const { avg, p1 } = extractFpsMetrics(testResult)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-3 py-1.5 h-12 rounded-md bg-default-100 dark:bg-default-50 text-xs flex flex-col justify-center">
|
||||
<div className="text-xs text-default-500">测试时间</div>
|
||||
<div className="font-medium">{testTimestamp}</div>
|
||||
</div>
|
||||
<div className="px-3 py-1.5 h-12 rounded-md bg-default-100 dark:bg-default-50 text-xs flex items-center">
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-default-500">平均帧</div>
|
||||
<div className="font-medium">
|
||||
{avg !== null ? `${avg.toFixed(1)}` : "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-default-500">P1低帧</div>
|
||||
<div className="font-medium">
|
||||
{p1 !== null ? `${p1.toFixed(1)}` : "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
135
src/components/cstb/FpsTest/components/TestResultsTable.tsx
Normal file
135
src/components/cstb/FpsTest/components/TestResultsTable.tsx
Normal file
@@ -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<typeof useFpsTestStore>
|
||||
onEditNote: (resultId: string, currentNote: string) => void
|
||||
}
|
||||
|
||||
export function TestResultsTable({
|
||||
results,
|
||||
fpsTest,
|
||||
onEditNote,
|
||||
}: TestResultsTableProps) {
|
||||
return (
|
||||
<div className="relative flex flex-col gap-2">
|
||||
<Table
|
||||
aria-label="测试结果表格"
|
||||
selectionMode="none"
|
||||
classNames={{
|
||||
wrapper: "overflow-auto",
|
||||
base: "min-h-[222px]",
|
||||
table: "min-w-full",
|
||||
th: "px-3 py-2 text-xs font-semibold whitespace-nowrap",
|
||||
td: "px-3 py-2 text-xs",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn minWidth={140}>测试时间</TableColumn>
|
||||
<TableColumn width={80}>测试地图</TableColumn>
|
||||
<TableColumn width={80}>平均帧</TableColumn>
|
||||
<TableColumn width={80}>P1低帧</TableColumn>
|
||||
<TableColumn width={100}>CPU</TableColumn>
|
||||
<TableColumn minWidth={80}>系统版本</TableColumn>
|
||||
<TableColumn minWidth={100}>GPU</TableColumn>
|
||||
<TableColumn width={80}>内存</TableColumn>
|
||||
<TableColumn width={120}>视频设置</TableColumn>
|
||||
<TableColumn minWidth={100}>备注</TableColumn>
|
||||
<TableColumn width={80} align="center">
|
||||
操作
|
||||
</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody emptyContent="暂无测试记录">
|
||||
{results.map((result) => (
|
||||
<TableRow key={result.id}>
|
||||
<TableCell className="text-xs whitespace-nowrap">
|
||||
{result.testTime}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.mapLabel}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs max-w-[175px]">
|
||||
<Tooltip
|
||||
content={result.hardwareInfo?.cpu || "N/A"}
|
||||
delay={500}
|
||||
placement="top"
|
||||
>
|
||||
<div className="truncate cursor-help">
|
||||
{result.hardwareInfo?.cpu || "N/A"}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.hardwareInfo?.os || "N/A"}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="truncate">{result.hardwareInfo?.gpu || "N/A"}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs whitespace-nowrap">
|
||||
{result.hardwareInfo?.memory
|
||||
? `${result.hardwareInfo.memory}GB`
|
||||
: "N/A"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<Tooltip content={formatVideoSettingSummary(result.videoSetting)}>
|
||||
<span className="truncate cursor-help">
|
||||
{result.videoSetting
|
||||
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
|
||||
: "N/A"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<NoteCell
|
||||
note={result.note || ""}
|
||||
onEdit={() => onEditNote(result.id, result.note || "")}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
fpsTest.removeResult(result.id)
|
||||
addToast({
|
||||
title: "已删除测试记录",
|
||||
variant: "flat",
|
||||
})
|
||||
}}
|
||||
className="h-6 min-w-6"
|
||||
>
|
||||
<Delete size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
32
src/components/cstb/FpsTest/constants.ts
Normal file
32
src/components/cstb/FpsTest/constants.ts
Normal file
@@ -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
|
||||
|
||||
32
src/components/cstb/FpsTest/hooks/useGameMonitor.ts
Normal file
32
src/components/cstb/FpsTest/hooks/useGameMonitor.ts
Normal file
@@ -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<boolean>("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 }
|
||||
}
|
||||
|
||||
62
src/components/cstb/FpsTest/hooks/useHardwareInfo.ts
Normal file
62
src/components/cstb/FpsTest/hooks/useHardwareInfo.ts
Normal file
@@ -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<HardwareInfoWithGpu | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchHardwareInfo = async () => {
|
||||
try {
|
||||
const [sys, gpuInfo, computerInfo] = await Promise.all([
|
||||
allSysInfo(),
|
||||
invoke<GpuInfo | null>("get_gpu_info").catch((error) => {
|
||||
console.error("获取 GPU 信息失败:", error)
|
||||
return null
|
||||
}),
|
||||
invoke<ComputerInfo>("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
|
||||
}
|
||||
|
||||
113
src/components/cstb/FpsTest/hooks/useTestMonitor.ts
Normal file
113
src/components/cstb/FpsTest/hooks/useTestMonitor.ts
Normal file
@@ -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<NodeJS.Timeout | null>(null)
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(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
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
6
src/components/cstb/FpsTest/index.tsx
Normal file
6
src/components/cstb/FpsTest/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
"use client"
|
||||
// 导出重构后的FpsTest组件
|
||||
// 主组件文件已重构,使用提取的模块
|
||||
|
||||
export { FpsTest } from "../FpsTest"
|
||||
|
||||
271
src/components/cstb/FpsTest/services/resultReader.ts
Normal file
271
src/components/cstb/FpsTest/services/resultReader.ts
Normal file
@@ -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<typeof useSteamStore>
|
||||
tool: ReturnType<typeof useToolStore>
|
||||
fpsTest: ReturnType<typeof useFpsTestStore>
|
||||
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<string | null>
|
||||
onResultRead: (data: {
|
||||
timestamp: string
|
||||
data: string
|
||||
avg: number | null
|
||||
p1: number | null
|
||||
}) => void
|
||||
}
|
||||
|
||||
export async function readResult(
|
||||
params: ReadResultParams,
|
||||
silent = false
|
||||
): Promise<boolean> {
|
||||
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<string>("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<string>("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
|
||||
}
|
||||
}
|
||||
|
||||
172
src/components/cstb/FpsTest/services/testRunner.ts
Normal file
172
src/components/cstb/FpsTest/services/testRunner.ts
Normal file
@@ -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<typeof useSteamStore>
|
||||
tool: ReturnType<typeof useToolStore>
|
||||
selectedMapIndex: number
|
||||
resolutionWidth: string
|
||||
resolutionHeight: string
|
||||
isResolutionEnabled: boolean
|
||||
isResolutionGroupEnabled: boolean
|
||||
isFullscreen: boolean
|
||||
customLaunchOption: string
|
||||
autoCloseGame: boolean
|
||||
checkGameRunning: () => Promise<boolean>
|
||||
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<RunSingleTestResult | null> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
25
src/components/cstb/FpsTest/types.ts
Normal file
25
src/components/cstb/FpsTest/types.ts
Normal file
@@ -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
|
||||
}
|
||||
|
||||
175
src/components/cstb/FpsTest/utils/csv-export.ts
Normal file
175
src/components/cstb/FpsTest/utils/csv-export.ts
Normal file
@@ -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",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
43
src/components/cstb/FpsTest/utils/fps-metrics.ts
Normal file
43
src/components/cstb/FpsTest/utils/fps-metrics.ts
Normal file
@@ -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 }
|
||||
}
|
||||
|
||||
66
src/components/cstb/FpsTest/utils/timestamp.ts
Normal file
66
src/components/cstb/FpsTest/utils/timestamp.ts
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
47
src/components/cstb/FpsTest/utils/vprof-parser.ts
Normal file
47
src/components/cstb/FpsTest/utils/vprof-parser.ts
Normal file
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
286
todo/refactor-plan.md
Normal file
286
todo/refactor-plan.md
Normal file
@@ -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. **当前状态**: 已提取基础模块,主组件重构需要继续完成
|
||||
Reference in New Issue
Block a user