[feat] fps test results gathering
This commit is contained in:
@@ -5,7 +5,11 @@ import { Chip, Code, Skeleton } from "@heroui/react"
|
|||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <CfgxList />
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
|
<CfgxList />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CfgxList() {
|
function CfgxList() {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function Page() {
|
|||||||
radius="full"
|
radius="full"
|
||||||
classNames={{
|
classNames={{
|
||||||
base: "min-w-0",
|
base: "min-w-0",
|
||||||
tabList: "gap-1",
|
tabList: "gap-0 p-0",
|
||||||
tab: "min-w-0 px-3",
|
tab: "min-w-0 px-3",
|
||||||
tabContent: "text-xs",
|
tabContent: "text-xs",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import { Refresh, SettingConfig } from "@icon-park/react"
|
|||||||
// import { version } from "@tauri-apps/plugin-os"
|
// import { version } from "@tauri-apps/plugin-os"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
|
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
|
||||||
import { FpsTest } from "@/components/cstb/FpsTest"
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col gap-4 overflow-hidden rounded-lg">
|
<section className="flex flex-col gap-4 overflow-hidden rounded-lg">
|
||||||
<div className="flex flex-col gap-4 overflow-y-auto h-full hide-scrollbar">
|
<div className="flex flex-col h-full gap-4 overflow-hidden hide-scrollbar">
|
||||||
<Card>
|
<Card className="overflow-hidden">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardIcon type="menu">
|
<CardIcon type="menu">
|
||||||
<SettingConfig /> 硬件
|
<SettingConfig /> 硬件
|
||||||
@@ -37,7 +37,6 @@ export default function Page() {
|
|||||||
<HardwareInfo />
|
<HardwareInfo />
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<FpsTest />
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SmartTransfer from "@/components/cstb/SmartTranser"
|
|||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col h-full gap-4">
|
<section className="flex flex-col h-full gap-4 overflow-hidden">
|
||||||
<div className="flex flex-grow w-full gap-4">
|
<div className="flex flex-grow w-full gap-4">
|
||||||
<Notice />
|
<Notice />
|
||||||
<SmartTransfer />
|
<SmartTransfer />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default function Page() {
|
|||||||
const githubRepo = process.env.NEXT_PUBLIC_GITHUB_REPO || ""
|
const githubRepo = process.env.NEXT_PUBLIC_GITHUB_REPO || ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm">版本号:{app.state.version}</p>
|
<p className="text-sm">版本号:{app.state.version}</p>
|
||||||
@@ -19,35 +20,36 @@ export default function Page() {
|
|||||||
<p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p>
|
<p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full border-t border-zinc-200 dark:border-zinc-800 pt-4">
|
{/* <div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||||
<h3 className="text-sm font-semibold mb-3">更新检查</h3>
|
<h3 className="mb-3 text-sm font-semibold">更新检查</h3>
|
||||||
<UpdateChecker customEndpoint={customEndpoint} githubRepo={githubRepo} />
|
<UpdateChecker customEndpoint={customEndpoint} githubRepo={githubRepo} />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="w-full border-t border-zinc-200 dark:border-zinc-800 pt-4 space-y-3">
|
<div className="flex flex-col w-full pt-4 space-y-3 border-t border-zinc-200 dark:border-zinc-800">
|
||||||
<h3 className="text-sm font-semibold mb-3">启动设置</h3>
|
<h3 className="mb-3 text-sm font-semibold">启动设置</h3>
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={app.state.autoStart}
|
isSelected={app.state.autoStart}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setAutoStart(e.target.checked)}
|
onChange={(e) => app.setAutoStart(e.target.checked)}
|
||||||
>
|
>
|
||||||
开机自启动 {app.state.autoStart ? "开" : "关"}
|
开机自启动
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={app.state.startHidden}
|
isSelected={app.state.startHidden}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setStartHidden(e.target.checked)}
|
onChange={(e) => app.setStartHidden(e.target.checked)}
|
||||||
>
|
>
|
||||||
静默启动 {app.state.startHidden ? "开" : "关"}
|
静默启动
|
||||||
</Switch>
|
</Switch>
|
||||||
<Switch
|
<Switch
|
||||||
isSelected={app.state.hiddenOnClose}
|
isSelected={app.state.hiddenOnClose}
|
||||||
size="sm"
|
size="sm"
|
||||||
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
||||||
>
|
>
|
||||||
关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"}
|
关闭时最小化到托盘
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default function Page() {
|
|||||||
const steam = useSteamStore()
|
const steam = useSteamStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
|
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
|
||||||
<p>Steam路径:{steam.state.steamDir}</p>
|
<p>Steam路径:{steam.state.steamDir}</p>
|
||||||
<p>游戏路径:{steam.state.cs2Dir}</p>
|
<p>游戏路径:{steam.state.cs2Dir}</p>
|
||||||
@@ -12,5 +13,6 @@ export default function Page() {
|
|||||||
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
||||||
<p>Steam账号:{steam.currentUser()?.account_name || " "}</p>
|
<p>Steam账号:{steam.currentUser()?.account_name || " "}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <>Replay</>
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
|
<>Replay</>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import VideoSetting from "@/components/cstb/VideoSetting"
|
import VideoSetting from "@/components/cstb/VideoSetting"
|
||||||
|
import { FpsTest } from "@/components/cstb/FpsTest"
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col h-full gap-4">
|
<section className="flex flex-col h-full gap-4 overflow-hidden">
|
||||||
|
<div className="flex flex-col h-full gap-4 overflow-y-auto rounded-lg hide-scrollbar">
|
||||||
<VideoSetting />
|
<VideoSetting />
|
||||||
|
<FpsTest />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <div>Users</div>
|
return (
|
||||||
|
<section className="flex flex-col gap-4 overflow-hidden">
|
||||||
|
<div>Users</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
addToast({
|
addToast({
|
||||||
title: "登录失败",
|
title: "登录失败",
|
||||||
description: "无法验证登录信息,请重试",
|
description: "无法验证登录信息,请重试",
|
||||||
variant: "danger",
|
|
||||||
|
severity: "danger",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -44,7 +45,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
addToast({
|
addToast({
|
||||||
title: "登录失败",
|
title: "登录失败",
|
||||||
description: error instanceof Error ? error.message : "未知错误",
|
description: error instanceof Error ? error.message : "未知错误",
|
||||||
variant: "danger",
|
severity: "danger",
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const RoundedButton = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center justify-center px-3 py-1 transition rounded-full select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5 dark:bg-white/5"
|
className="flex items-center justify-center px-3 py-1 transition rounded-full cursor-pointer select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5 dark:bg-white/5"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useSteamStore } from "@/store/steam"
|
import { useSteamStore } from "@/store/steam"
|
||||||
|
import { useToolStore } from "@/store/tool"
|
||||||
|
import { useFpsTestStore } from "@/store/fpsTest"
|
||||||
import { invoke } from "@tauri-apps/api/core"
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
|
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||||
import { addToast, Button, Chip, Spinner, Switch } from "@heroui/react"
|
import { addToast, Button, Chip, Spinner, Switch, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/react"
|
||||||
import { useState, useEffect, useRef, useCallback } from "react"
|
import { useState, useEffect, useRef, useCallback } from "react"
|
||||||
import { TestTube, Power } from "@icon-park/react"
|
import { TestTube, Power, List, Delete } from "@icon-park/react"
|
||||||
|
import { allSysInfo, type AllSystemInfo } from "tauri-plugin-system-info-api"
|
||||||
|
|
||||||
const BENCHMARK_MAPS = [
|
const BENCHMARK_MAPS = [
|
||||||
{
|
{
|
||||||
@@ -106,18 +109,79 @@ function compareTimestamps(timestamp1: string, timestamp2: string): boolean {
|
|||||||
return date1 > date2
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
export function FpsTest() {
|
export function FpsTest() {
|
||||||
const steam = useSteamStore()
|
const steam = useSteamStore()
|
||||||
|
const tool = useToolStore()
|
||||||
|
const fpsTest = useFpsTestStore()
|
||||||
const [testing, setTesting] = useState(false)
|
const [testing, setTesting] = useState(false)
|
||||||
const [testResult, setTestResult] = useState<string | null>(null)
|
const [testResult, setTestResult] = useState<string | null>(null)
|
||||||
const [testTimestamp, setTestTimestamp] = useState<string | null>(null)
|
const [testTimestamp, setTestTimestamp] = useState<string | null>(null)
|
||||||
const [selectedMap, setSelectedMap] = useState<string | null>(null)
|
const [selectedMap, setSelectedMap] = useState<string | null>(null)
|
||||||
|
const [selectedMapLabel, setSelectedMapLabel] = useState<string | null>(null)
|
||||||
const [autoCloseGame, setAutoCloseGame] = useState(false)
|
const [autoCloseGame, setAutoCloseGame] = useState(false)
|
||||||
const [isMonitoring, setIsMonitoring] = useState(false)
|
const [isMonitoring, setIsMonitoring] = useState(false)
|
||||||
|
const [showResultsTable, setShowResultsTable] = useState(false)
|
||||||
|
const [hardwareInfo, setHardwareInfo] = useState<AllSystemInfo | null>(null)
|
||||||
const monitoringIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
const monitoringIntervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
// 记录测试开始的时间戳(用于过滤旧数据)
|
// 记录测试开始的时间戳(用于过滤旧数据)
|
||||||
const testStartTimestampRef = useRef<string | null>(null)
|
const testStartTimestampRef = useRef<string | null>(null)
|
||||||
|
|
||||||
|
// 获取硬件信息
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchHardwareInfo = async () => {
|
||||||
|
try {
|
||||||
|
const sys = await allSysInfo()
|
||||||
|
setHardwareInfo(sys)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取硬件信息失败:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void fetchHardwareInfo()
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 读取结果函数
|
// 读取结果函数
|
||||||
const readResult = useCallback(
|
const readResult = useCallback(
|
||||||
async (silent = false): Promise<boolean> => {
|
async (silent = false): Promise<boolean> => {
|
||||||
@@ -156,8 +220,48 @@ export function FpsTest() {
|
|||||||
setTestTimestamp(parsed.timestamp)
|
setTestTimestamp(parsed.timestamp)
|
||||||
// 成功读取后,清除测试开始时间戳(测试已完成)
|
// 成功读取后,清除测试开始时间戳(测试已完成)
|
||||||
testStartTimestampRef.current = null
|
testStartTimestampRef.current = null
|
||||||
|
|
||||||
|
// 提取 avg 和 p1 值
|
||||||
|
const { avg, p1 } = extractFpsMetrics(parsed.data)
|
||||||
|
|
||||||
|
// 保存测试结果(即使没有 selectedMap 也保存,使用 "未知地图" 作为默认值)
|
||||||
|
const now = new Date()
|
||||||
|
const testDate = now.toISOString()
|
||||||
|
const mapConfig = selectedMap ? BENCHMARK_MAPS.find(m => m.name === selectedMap) : null
|
||||||
|
|
||||||
|
// 从 store 直接获取最新的 videoSetting,避免依赖项问题
|
||||||
|
const currentVideoSetting = tool.store.state.videoSetting
|
||||||
|
|
||||||
|
fpsTest.addResult({
|
||||||
|
id: `${now.getTime()}-${Math.random().toString(36).slice(2, 11)}`,
|
||||||
|
testTime: parsed.timestamp,
|
||||||
|
testDate,
|
||||||
|
mapName: selectedMap || "unknown",
|
||||||
|
mapLabel: mapConfig?.label || selectedMapLabel || "未知地图",
|
||||||
|
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,
|
||||||
|
} : null,
|
||||||
|
})
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
addToast({ title: "已读取测试结果" })
|
if (avg !== null || p1 !== null) {
|
||||||
|
addToast({
|
||||||
|
title: `已读取并保存测试结果${avg !== null ? ` (avg: ${avg.toFixed(1)} FPS)` : ""}${p1 !== null ? ` (p1: ${p1.toFixed(1)} FPS)` : ""}`
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addToast({ title: "已读取并保存测试结果(未能提取帧数数据)" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
} else if (!silent) {
|
} else if (!silent) {
|
||||||
@@ -184,7 +288,7 @@ export function FpsTest() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[steam.state.cs2Dir]
|
[steam.state.cs2Dir, selectedMap, selectedMapLabel, fpsTest, tool.store, hardwareInfo]
|
||||||
)
|
)
|
||||||
|
|
||||||
// 关闭游戏
|
// 关闭游戏
|
||||||
@@ -253,6 +357,7 @@ export function FpsTest() {
|
|||||||
|
|
||||||
setTesting(true)
|
setTesting(true)
|
||||||
setSelectedMap(mapConfig.name)
|
setSelectedMap(mapConfig.name)
|
||||||
|
setSelectedMapLabel(mapConfig.label)
|
||||||
setTestResult(null)
|
setTestResult(null)
|
||||||
setTestTimestamp(null)
|
setTestTimestamp(null)
|
||||||
|
|
||||||
@@ -299,8 +404,69 @@ export function FpsTest() {
|
|||||||
<CardIcon>
|
<CardIcon>
|
||||||
<TestTube size={16} /> 帧数测试
|
<TestTube size={16} /> 帧数测试
|
||||||
</CardIcon>
|
</CardIcon>
|
||||||
|
<CardTool>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant={showResultsTable ? "solid" : "flat"}
|
||||||
|
onPress={() => setShowResultsTable(!showResultsTable)}
|
||||||
|
className="px-3"
|
||||||
|
>
|
||||||
|
<List size={14} className="mr-1" />
|
||||||
|
测试结果
|
||||||
|
</Button>
|
||||||
|
</CardTool>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
|
{showResultsTable ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="max-h-[500px] overflow-auto">
|
||||||
|
<Table aria-label="测试结果表格" selectionMode="none">
|
||||||
|
<TableHeader>
|
||||||
|
<TableColumn>测试时间</TableColumn>
|
||||||
|
<TableColumn>测试地图</TableColumn>
|
||||||
|
<TableColumn>平均帧数</TableColumn>
|
||||||
|
<TableColumn>P1帧数</TableColumn>
|
||||||
|
<TableColumn>CPU</TableColumn>
|
||||||
|
<TableColumn width={80}>操作</TableColumn>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody emptyContent="暂无测试记录">
|
||||||
|
{fpsTest.state.results.map((result) => (
|
||||||
|
<TableRow key={result.id}>
|
||||||
|
<TableCell>{result.testTime}</TableCell>
|
||||||
|
<TableCell>{result.mapLabel}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{result.hardwareInfo?.cpu || "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
variant="light"
|
||||||
|
onPress={() => {
|
||||||
|
fpsTest.removeResult(result.id)
|
||||||
|
addToast({
|
||||||
|
title: "已删除测试记录",
|
||||||
|
variant: "flat"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="min-w-8"
|
||||||
|
>
|
||||||
|
<Delete size={16} />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{BENCHMARK_MAPS.map((mapConfig) => (
|
{BENCHMARK_MAPS.map((mapConfig) => (
|
||||||
@@ -311,7 +477,7 @@ export function FpsTest() {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
void startTest(mapConfig)
|
void startTest(mapConfig)
|
||||||
}}
|
}}
|
||||||
className="px-4 font-medium py-1.5 transition bg-blue-200 dark:bg-blue-900/60 rounded-full select-none"
|
className="font-medium transition bg-blue-200 rounded-full select-none dark:bg-blue-900/60"
|
||||||
>
|
>
|
||||||
{testing && selectedMap === mapConfig.name ? (
|
{testing && selectedMap === mapConfig.name ? (
|
||||||
<Spinner size="sm" className="mr-2" />
|
<Spinner size="sm" className="mr-2" />
|
||||||
@@ -325,7 +491,7 @@ export function FpsTest() {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
void readResult()
|
void readResult()
|
||||||
}}
|
}}
|
||||||
className="px-4 font-medium py-1.5 transition bg-green-200 dark:bg-green-900/60 rounded-full select-none"
|
className="font-medium transition bg-green-200 rounded-full select-none dark:bg-green-900/60"
|
||||||
>
|
>
|
||||||
手动读取结果
|
手动读取结果
|
||||||
</Button>
|
</Button>
|
||||||
@@ -334,9 +500,9 @@ export function FpsTest() {
|
|||||||
onPress={() => {
|
onPress={() => {
|
||||||
void closeGame()
|
void closeGame()
|
||||||
}}
|
}}
|
||||||
className="px-4 font-medium py-1.5 transition bg-orange-200 dark:bg-orange-900/60 rounded-full select-none"
|
className="font-medium transition bg-orange-200 rounded-full select-none dark:bg-orange-900/60"
|
||||||
>
|
>
|
||||||
<Power className="mr-1" size={14} />
|
<Power size={14} />
|
||||||
关闭游戏
|
关闭游戏
|
||||||
</Button>
|
</Button>
|
||||||
<Switch size="sm" isSelected={autoCloseGame} onValueChange={setAutoCloseGame} className="ml-4">
|
<Switch size="sm" isSelected={autoCloseGame} onValueChange={setAutoCloseGame} className="ml-4">
|
||||||
@@ -352,19 +518,19 @@ export function FpsTest() {
|
|||||||
{testResult && (
|
{testResult && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Chip size="sm">测试结果</Chip>
|
|
||||||
{testTimestamp && (
|
{testTimestamp && (
|
||||||
<Chip size="sm" variant="flat" color="default">
|
<Chip size="sm" variant="flat" color="default">
|
||||||
测试时间: {testTimestamp}
|
测试时间: {testTimestamp}
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-3 overflow-auto font-mono text-xs rounded-md bg-black/5 dark:bg-white/5">
|
<pre className="p-3 overflow-auto font-mono text-xs rounded-md bg-black/5 dark:bg-white/5 hide-scrollbar">
|
||||||
{testResult}
|
{testResult}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Button, Progress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
|
import { Button, Progress, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
|
||||||
import { Download, Refresh, CheckCircle } from "@icon-park/react"
|
import { Download, Refresh, CheckCorrect } from "@icon-park/react"
|
||||||
import { invoke } from "@tauri-apps/api/core"
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
import { relaunch } from "@tauri-apps/api/process"
|
import { relaunch } from "@tauri-apps/plugin-process"
|
||||||
import { addToast } from "@heroui/react"
|
import { addToast } from "@heroui/react"
|
||||||
import { useAppStore } from "@/store/app"
|
import { useAppStore } from "@/store/app"
|
||||||
|
|
||||||
@@ -159,8 +159,8 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
|
|||||||
{checking ? "检查中..." : "检查更新"}
|
{checking ? "检查中..." : "检查更新"}
|
||||||
</Button>
|
</Button>
|
||||||
{app.state.hasUpdate && (
|
{app.state.hasUpdate && (
|
||||||
<span className="text-sm text-green-600 dark:text-green-400 flex items-center gap-1">
|
<span className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
|
||||||
<CheckCircle size={16} />
|
<CheckCorrect size={16} />
|
||||||
有新版本可用
|
有新版本可用
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -192,8 +192,8 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold mb-1">更新说明:</p>
|
<p className="mb-1 text-sm font-semibold">更新说明:</p>
|
||||||
<div className="text-sm text-zinc-600 dark:text-zinc-400 whitespace-pre-wrap">
|
<div className="text-sm whitespace-pre-wrap text-zinc-600 dark:text-zinc-400">
|
||||||
{formatNotes(updateInfo?.notes)}
|
{formatNotes(updateInfo?.notes)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ const VideoSetting = () => {
|
|||||||
const checkGameRunning = useCallback(async () => {
|
const checkGameRunning = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// 尝试检测cs2.exe进程
|
// 尝试检测cs2.exe进程
|
||||||
const result = await invoke<boolean>("check_process_running", { processName: "cs2.exe" }).catch(() => false)
|
const result = await invoke<boolean>("check_process_running", {
|
||||||
|
processName: "cs2.exe",
|
||||||
|
}).catch(() => false)
|
||||||
setIsGameRunning(result)
|
setIsGameRunning(result)
|
||||||
return result
|
return result
|
||||||
} catch {
|
} catch {
|
||||||
@@ -310,12 +312,7 @@ const VideoSetting = () => {
|
|||||||
<CardIcon>
|
<CardIcon>
|
||||||
<SettingConfig /> 视频设置
|
<SettingConfig /> 视频设置
|
||||||
{isGameRunning && (
|
{isGameRunning && (
|
||||||
<Chip
|
<Chip size="sm" color="warning" variant="flat" className="ml-2">
|
||||||
size="sm"
|
|
||||||
color="warning"
|
|
||||||
variant="flat"
|
|
||||||
className="ml-2"
|
|
||||||
>
|
|
||||||
游戏运行中
|
游戏运行中
|
||||||
</Chip>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
@@ -357,7 +354,7 @@ const VideoSetting = () => {
|
|||||||
addToast({
|
addToast({
|
||||||
title: "无法应用设置",
|
title: "无法应用设置",
|
||||||
description: "检测到游戏正在运行,请先关闭游戏后再应用设置",
|
description: "检测到游戏正在运行,请先关闭游戏后再应用设置",
|
||||||
color: "warning"
|
color: "warning",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -373,7 +370,7 @@ const VideoSetting = () => {
|
|||||||
setEdit(false)
|
setEdit(false)
|
||||||
addToast({ title: "应用设置成功" })
|
addToast({ title: "应用设置成功" })
|
||||||
}}
|
}}
|
||||||
isDisabled={isGameRunning}
|
disabled={isGameRunning}
|
||||||
>
|
>
|
||||||
<Plus />
|
<Plus />
|
||||||
应用
|
应用
|
||||||
@@ -404,11 +401,7 @@ const VideoSetting = () => {
|
|||||||
)}
|
)}
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
|
|
||||||
<ToolButton
|
<ToolButton onClick={debouncedGetVideoConfig}>读取</ToolButton>
|
||||||
onClick={debouncedGetVideoConfig}
|
|
||||||
>
|
|
||||||
读取
|
|
||||||
</ToolButton>
|
|
||||||
<ToolButton onClick={() => setHide(!hide)}>
|
<ToolButton onClick={() => setHide(!hide)}>
|
||||||
{hide ? (
|
{hide ? (
|
||||||
<>
|
<>
|
||||||
@@ -500,7 +493,8 @@ const VideoSetting = () => {
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-xs text-default-500">分辨率</span>
|
<span className="text-xs text-default-500">分辨率</span>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{tool.state.videoSetting.defaultres} × {tool.state.videoSetting.defaultresheight}
|
{tool.state.videoSetting.defaultres} ×{" "}
|
||||||
|
{tool.state.videoSetting.defaultresheight}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{videoSettings(tool.state.videoSetting).map((vid, index) => (
|
{videoSettings(tool.state.videoSetting).map((vid, index) => (
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const ToolButton = ({ children, className, selected, ...rest }: ToolButto
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-shrink-0 gap-0.5 active:scale-95 items-center min-w-7 justify-center px-2 py-1.5 bg-black/5 transition hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-md text-sm leading-none",
|
"flex shrink-0 gap-0.5 active:scale-95 cursor-pointer items-center min-w-7 justify-center px-2 py-1.5 bg-black/5 transition hover:bg-black/10 dark:bg-white/5 dark:hover:bg-white/10 rounded-md text-sm leading-none",
|
||||||
className,
|
className,
|
||||||
selected &&
|
selected &&
|
||||||
"bg-purple-500/40 hover:bg-purple-500/20 text-purple-900 dark:text-purple-100 drop-shadow-sm dark:bg-purple-500/40 dark:hover:bg-purple-500/20"
|
"bg-purple-500/40 hover:bg-purple-500/20 text-purple-900 dark:text-purple-100 drop-shadow-sm dark:bg-purple-500/40 dark:hover:bg-purple-500/20"
|
||||||
|
|||||||
65
src/store/fpsTest.ts
Normal file
65
src/store/fpsTest.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { store } from "@tauri-store/valtio"
|
||||||
|
import { useSnapshot } from "valtio"
|
||||||
|
import { DEFAULT_STORE_CONFIG } from "./config"
|
||||||
|
import type { AllSystemInfo } from "tauri-plugin-system-info-api"
|
||||||
|
import type { VideoSetting } from "./tool"
|
||||||
|
|
||||||
|
export interface FpsTestResult {
|
||||||
|
id: string // 唯一标识符,使用时间戳
|
||||||
|
testTime: string // 测试时间(MM/DD HH:mm:ss)
|
||||||
|
testDate: string // 测试日期(ISO 格式,用于排序)
|
||||||
|
mapName: string // 测试地图名称
|
||||||
|
mapLabel: string // 测试地图标签
|
||||||
|
avg: number | null // 平均帧数
|
||||||
|
p1: number | null // P1 帧数(最低1%帧数)
|
||||||
|
rawResult: string // 原始测试结果
|
||||||
|
videoSetting: VideoSetting | null // 画面设置参数
|
||||||
|
hardwareInfo: {
|
||||||
|
cpu: string | null
|
||||||
|
cpuCount: number | null
|
||||||
|
os: string | null
|
||||||
|
memory: number | null // GB
|
||||||
|
} | null // 硬件信息
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = {
|
||||||
|
results: [] as FpsTestResult[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fpsTestStore = store(
|
||||||
|
"fpsTest",
|
||||||
|
{ ...defaultValue },
|
||||||
|
DEFAULT_STORE_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useFpsTestStore = () => {
|
||||||
|
void fpsTestStore.start
|
||||||
|
const state = useSnapshot(fpsTestStore.state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
store: fpsTestStore,
|
||||||
|
addResult,
|
||||||
|
removeResult,
|
||||||
|
clearResults,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addResult = (result: FpsTestResult) => {
|
||||||
|
fpsTestStore.state.results = [result, ...fpsTestStore.state.results]
|
||||||
|
// 限制最多保存100条记录
|
||||||
|
if (fpsTestStore.state.results.length > 100) {
|
||||||
|
fpsTestStore.state.results = fpsTestStore.state.results.slice(0, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeResult = (id: string) => {
|
||||||
|
fpsTestStore.state.results = fpsTestStore.state.results.filter(
|
||||||
|
(r) => r.id !== id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearResults = () => {
|
||||||
|
fpsTestStore.state.results = []
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import { appStore } from "./app"
|
|||||||
import { authStore } from "./auth"
|
import { authStore } from "./auth"
|
||||||
import { steamStore } from "./steam"
|
import { steamStore } from "./steam"
|
||||||
import { toolStore } from "./tool"
|
import { toolStore } from "./tool"
|
||||||
|
import { fpsTestStore } from "./fpsTest"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
export async function init() {
|
export async function init() {
|
||||||
@@ -11,6 +12,7 @@ export async function init() {
|
|||||||
await authStore.start()
|
await authStore.start()
|
||||||
await toolStore.start()
|
await toolStore.start()
|
||||||
await steamStore.start()
|
await steamStore.start()
|
||||||
|
await fpsTestStore.start()
|
||||||
const appConfigDirPath = await appConfigDir()
|
const appConfigDirPath = await appConfigDir()
|
||||||
const setPath = commands.setStoreCollectionPath("valtio")
|
const setPath = commands.setStoreCollectionPath("valtio")
|
||||||
await setPath(path.resolve(appConfigDirPath, "cstb"))
|
await setPath(path.resolve(appConfigDirPath, "cstb"))
|
||||||
|
|||||||
Reference in New Issue
Block a user