[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"
|
||||
|
||||
export default function Page() {
|
||||
return <CfgxList />
|
||||
return (
|
||||
<section className="flex flex-col gap-4 overflow-hidden">
|
||||
<CfgxList />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function CfgxList() {
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Page() {
|
||||
radius="full"
|
||||
classNames={{
|
||||
base: "min-w-0",
|
||||
tabList: "gap-1",
|
||||
tabList: "gap-0 p-0",
|
||||
tab: "min-w-0 px-3",
|
||||
tabContent: "text-xs",
|
||||
}}
|
||||
|
||||
@@ -12,12 +12,12 @@ import { Refresh, SettingConfig } from "@icon-park/react"
|
||||
// import { version } from "@tauri-apps/plugin-os"
|
||||
import { useEffect, useState } from "react"
|
||||
import { type AllSystemInfo, allSysInfo } from "tauri-plugin-system-info-api"
|
||||
import { FpsTest } from "@/components/cstb/FpsTest"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<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">
|
||||
<Card>
|
||||
<div className="flex flex-col h-full gap-4 overflow-hidden hide-scrollbar">
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader>
|
||||
<CardIcon type="menu">
|
||||
<SettingConfig /> 硬件
|
||||
@@ -37,7 +37,6 @@ export default function Page() {
|
||||
<HardwareInfo />
|
||||
</CardBody>
|
||||
</Card>
|
||||
<FpsTest />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import SmartTransfer from "@/components/cstb/SmartTranser"
|
||||
|
||||
const Home = () => {
|
||||
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">
|
||||
<Notice />
|
||||
<SmartTransfer />
|
||||
|
||||
@@ -12,42 +12,44 @@ export default function Page() {
|
||||
const githubRepo = process.env.NEXT_PUBLIC_GITHUB_REPO || ""
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
||||
<section className="flex flex-col gap-4 overflow-hidden">
|
||||
<div className="flex flex-col items-start gap-4 pt-2 pb-1">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">版本号:{app.state.version}</p>
|
||||
<p className="text-sm">是否有更新:{app.state.hasUpdate ? "有" : "无"}</p>
|
||||
<p className="text-sm">是否使用镜像源:{app.state.useMirror ? "是" : "否"}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-t border-zinc-200 dark:border-zinc-800 pt-4">
|
||||
<h3 className="text-sm font-semibold mb-3">更新检查</h3>
|
||||
{/* <div className="w-full pt-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<h3 className="mb-3 text-sm font-semibold">更新检查</h3>
|
||||
<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">
|
||||
<h3 className="text-sm font-semibold mb-3">启动设置</h3>
|
||||
<div className="flex flex-col w-full pt-4 space-y-3 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<h3 className="mb-3 text-sm font-semibold">启动设置</h3>
|
||||
<Switch
|
||||
isSelected={app.state.autoStart}
|
||||
size="sm"
|
||||
onChange={(e) => app.setAutoStart(e.target.checked)}
|
||||
>
|
||||
开机自启动 {app.state.autoStart ? "开" : "关"}
|
||||
开机自启动
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={app.state.startHidden}
|
||||
size="sm"
|
||||
onChange={(e) => app.setStartHidden(e.target.checked)}
|
||||
>
|
||||
静默启动 {app.state.startHidden ? "开" : "关"}
|
||||
静默启动
|
||||
</Switch>
|
||||
<Switch
|
||||
isSelected={app.state.hiddenOnClose}
|
||||
size="sm"
|
||||
onChange={(e) => app.setHiddenOnClose(e.target.checked)}
|
||||
>
|
||||
关闭时最小化到托盘 {app.state.hiddenOnClose ? "开" : "关"}
|
||||
关闭时最小化到托盘
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ export default function Page() {
|
||||
const steam = useSteamStore()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
|
||||
<p>Steam路径:{steam.state.steamDir}</p>
|
||||
<p>游戏路径:{steam.state.cs2Dir}</p>
|
||||
<p>Steam路径有效:{steam.state.steamDirValid ? "是" : "否"}</p>
|
||||
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
||||
<p>Steam账号:{steam.currentUser()?.account_name || " "}</p>
|
||||
</div>
|
||||
<section className="flex flex-col gap-4 overflow-hidden">
|
||||
<div className="flex flex-col items-start gap-3 pt-2 pb-1">
|
||||
<p>Steam路径:{steam.state.steamDir}</p>
|
||||
<p>游戏路径:{steam.state.cs2Dir}</p>
|
||||
<p>Steam路径有效:{steam.state.steamDirValid ? "是" : "否"}</p>
|
||||
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
||||
<p>Steam账号:{steam.currentUser()?.account_name || " "}</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"use client"
|
||||
export default function Page() {
|
||||
return <>Replay</>
|
||||
return (
|
||||
<section className="flex flex-col gap-4 overflow-hidden">
|
||||
<>Replay</>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import VideoSetting from "@/components/cstb/VideoSetting"
|
||||
import { FpsTest } from "@/components/cstb/FpsTest"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<section className="flex flex-col h-full gap-4">
|
||||
<VideoSetting />
|
||||
<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 />
|
||||
<FpsTest />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"use client"
|
||||
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({
|
||||
title: "登录失败",
|
||||
description: "无法验证登录信息,请重试",
|
||||
variant: "danger",
|
||||
|
||||
severity: "danger",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -44,7 +45,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
addToast({
|
||||
title: "登录失败",
|
||||
description: error instanceof Error ? error.message : "未知错误",
|
||||
variant: "danger",
|
||||
severity: "danger",
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
@@ -16,7 +16,7 @@ const RoundedButton = ({
|
||||
return (
|
||||
<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}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"use client"
|
||||
import { useSteamStore } from "@/store/steam"
|
||||
import { useToolStore } from "@/store/tool"
|
||||
import { useFpsTestStore } from "@/store/fpsTest"
|
||||
import { invoke } from "@tauri-apps/api/core"
|
||||
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
|
||||
import { addToast, Button, Chip, Spinner, Switch } from "@heroui/react"
|
||||
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||
import { addToast, Button, Chip, Spinner, Switch, Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/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 = [
|
||||
{
|
||||
@@ -106,18 +109,79 @@ function compareTimestamps(timestamp1: string, timestamp2: string): boolean {
|
||||
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() {
|
||||
const steam = useSteamStore()
|
||||
const tool = useToolStore()
|
||||
const fpsTest = useFpsTestStore()
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<string | null>(null)
|
||||
const [testTimestamp, setTestTimestamp] = useState<string | null>(null)
|
||||
const [selectedMap, setSelectedMap] = useState<string | null>(null)
|
||||
const [selectedMapLabel, setSelectedMapLabel] = useState<string | null>(null)
|
||||
const [autoCloseGame, setAutoCloseGame] = 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 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(
|
||||
async (silent = false): Promise<boolean> => {
|
||||
@@ -156,8 +220,48 @@ export function FpsTest() {
|
||||
setTestTimestamp(parsed.timestamp)
|
||||
// 成功读取后,清除测试开始时间戳(测试已完成)
|
||||
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) {
|
||||
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
|
||||
} else if (!silent) {
|
||||
@@ -184,7 +288,7 @@ export function FpsTest() {
|
||||
return false
|
||||
}
|
||||
},
|
||||
[steam.state.cs2Dir]
|
||||
[steam.state.cs2Dir, selectedMap, selectedMapLabel, fpsTest, tool.store, hardwareInfo]
|
||||
)
|
||||
|
||||
// 关闭游戏
|
||||
@@ -253,6 +357,7 @@ export function FpsTest() {
|
||||
|
||||
setTesting(true)
|
||||
setSelectedMap(mapConfig.name)
|
||||
setSelectedMapLabel(mapConfig.label)
|
||||
setTestResult(null)
|
||||
setTestTimestamp(null)
|
||||
|
||||
@@ -299,72 +404,133 @@ export function FpsTest() {
|
||||
<CardIcon>
|
||||
<TestTube size={16} /> 帧数测试
|
||||
</CardIcon>
|
||||
<CardTool>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={showResultsTable ? "solid" : "flat"}
|
||||
onPress={() => setShowResultsTable(!showResultsTable)}
|
||||
className="px-3"
|
||||
>
|
||||
<List size={14} className="mr-1" />
|
||||
测试结果
|
||||
</Button>
|
||||
</CardTool>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{BENCHMARK_MAPS.map((mapConfig) => (
|
||||
{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-wrap items-center gap-2">
|
||||
{BENCHMARK_MAPS.map((mapConfig) => (
|
||||
<Button
|
||||
key={mapConfig.name}
|
||||
size="sm"
|
||||
isDisabled={testing || isMonitoring}
|
||||
onPress={() => {
|
||||
void startTest(mapConfig)
|
||||
}}
|
||||
className="font-medium transition bg-blue-200 rounded-full select-none dark:bg-blue-900/60"
|
||||
>
|
||||
{testing && selectedMap === mapConfig.name ? (
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
) : null}
|
||||
{mapConfig.label}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
key={mapConfig.name}
|
||||
size="sm"
|
||||
isDisabled={testing || isMonitoring}
|
||||
isDisabled={isMonitoring}
|
||||
onPress={() => {
|
||||
void startTest(mapConfig)
|
||||
void readResult()
|
||||
}}
|
||||
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-green-200 rounded-full select-none dark:bg-green-900/60"
|
||||
>
|
||||
{testing && selectedMap === mapConfig.name ? (
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
) : null}
|
||||
{mapConfig.label}
|
||||
手动读取结果
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size="sm"
|
||||
isDisabled={isMonitoring}
|
||||
onPress={() => {
|
||||
void readResult()
|
||||
}}
|
||||
className="px-4 font-medium py-1.5 transition bg-green-200 dark:bg-green-900/60 rounded-full select-none"
|
||||
>
|
||||
手动读取结果
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
void closeGame()
|
||||
}}
|
||||
className="px-4 font-medium py-1.5 transition bg-orange-200 dark:bg-orange-900/60 rounded-full select-none"
|
||||
>
|
||||
<Power className="mr-1" size={14} />
|
||||
关闭游戏
|
||||
</Button>
|
||||
<Switch size="sm" isSelected={autoCloseGame} onValueChange={setAutoCloseGame} className="ml-4">
|
||||
测试完成自动关闭游戏
|
||||
</Switch>
|
||||
{isMonitoring && (
|
||||
<Chip size="sm" color="primary" variant="flat">
|
||||
正在监听中...
|
||||
</Chip>
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
void closeGame()
|
||||
}}
|
||||
className="font-medium transition bg-orange-200 rounded-full select-none dark:bg-orange-900/60"
|
||||
>
|
||||
<Power size={14} />
|
||||
关闭游戏
|
||||
</Button>
|
||||
<Switch size="sm" isSelected={autoCloseGame} onValueChange={setAutoCloseGame} className="ml-4">
|
||||
测试完成自动关闭游戏
|
||||
</Switch>
|
||||
{isMonitoring && (
|
||||
<Chip size="sm" color="primary" variant="flat">
|
||||
正在监听中...
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{testTimestamp && (
|
||||
<Chip size="sm" variant="flat" color="default">
|
||||
测试时间: {testTimestamp}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
<pre className="p-3 overflow-auto font-mono text-xs rounded-md bg-black/5 dark:bg-white/5 hide-scrollbar">
|
||||
{testResult}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{testResult && (
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Chip size="sm">测试结果</Chip>
|
||||
{testTimestamp && (
|
||||
<Chip size="sm" variant="flat" color="default">
|
||||
测试时间: {testTimestamp}
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
<pre className="p-3 overflow-auto font-mono text-xs rounded-md bg-black/5 dark:bg-white/5">
|
||||
{testResult}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { useState } from "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 { relaunch } from "@tauri-apps/api/process"
|
||||
import { relaunch } from "@tauri-apps/plugin-process"
|
||||
import { addToast } from "@heroui/react"
|
||||
import { useAppStore } from "@/store/app"
|
||||
|
||||
@@ -159,8 +159,8 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
|
||||
{checking ? "检查中..." : "检查更新"}
|
||||
</Button>
|
||||
{app.state.hasUpdate && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400 flex items-center gap-1">
|
||||
<CheckCircle size={16} />
|
||||
<span className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
|
||||
<CheckCorrect size={16} />
|
||||
有新版本可用
|
||||
</span>
|
||||
)}
|
||||
@@ -192,8 +192,8 @@ export function UpdateChecker({ customEndpoint, githubRepo }: UpdateCheckerProps
|
||||
<ModalBody>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold mb-1">更新说明:</p>
|
||||
<div className="text-sm text-zinc-600 dark:text-zinc-400 whitespace-pre-wrap">
|
||||
<p className="mb-1 text-sm font-semibold">更新说明:</p>
|
||||
<div className="text-sm whitespace-pre-wrap text-zinc-600 dark:text-zinc-400">
|
||||
{formatNotes(updateInfo?.notes)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,9 @@ const VideoSetting = () => {
|
||||
const checkGameRunning = useCallback(async () => {
|
||||
try {
|
||||
// 尝试检测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)
|
||||
return result
|
||||
} catch {
|
||||
@@ -310,12 +312,7 @@ const VideoSetting = () => {
|
||||
<CardIcon>
|
||||
<SettingConfig /> 视频设置
|
||||
{isGameRunning && (
|
||||
<Chip
|
||||
size="sm"
|
||||
color="warning"
|
||||
variant="flat"
|
||||
className="ml-2"
|
||||
>
|
||||
<Chip size="sm" color="warning" variant="flat" className="ml-2">
|
||||
游戏运行中
|
||||
</Chip>
|
||||
)}
|
||||
@@ -354,10 +351,10 @@ const VideoSetting = () => {
|
||||
// 检查游戏是否运行
|
||||
const gameRunning = await checkGameRunning()
|
||||
if (gameRunning) {
|
||||
addToast({
|
||||
title: "无法应用设置",
|
||||
addToast({
|
||||
title: "无法应用设置",
|
||||
description: "检测到游戏正在运行,请先关闭游戏后再应用设置",
|
||||
color: "warning"
|
||||
color: "warning",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -373,7 +370,7 @@ const VideoSetting = () => {
|
||||
setEdit(false)
|
||||
addToast({ title: "应用设置成功" })
|
||||
}}
|
||||
isDisabled={isGameRunning}
|
||||
disabled={isGameRunning}
|
||||
>
|
||||
<Plus />
|
||||
应用
|
||||
@@ -404,11 +401,7 @@ const VideoSetting = () => {
|
||||
)}
|
||||
</ToolButton>
|
||||
|
||||
<ToolButton
|
||||
onClick={debouncedGetVideoConfig}
|
||||
>
|
||||
读取
|
||||
</ToolButton>
|
||||
<ToolButton onClick={debouncedGetVideoConfig}>读取</ToolButton>
|
||||
<ToolButton onClick={() => setHide(!hide)}>
|
||||
{hide ? (
|
||||
<>
|
||||
@@ -500,7 +493,8 @@ const VideoSetting = () => {
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-xs text-default-500">分辨率</span>
|
||||
<span className="text-sm font-medium">
|
||||
{tool.state.videoSetting.defaultres} × {tool.state.videoSetting.defaultresheight}
|
||||
{tool.state.videoSetting.defaultres} ×{" "}
|
||||
{tool.state.videoSetting.defaultresheight}
|
||||
</span>
|
||||
</div>
|
||||
{videoSettings(tool.state.videoSetting).map((vid, index) => (
|
||||
|
||||
@@ -11,7 +11,7 @@ export const ToolButton = ({ children, className, selected, ...rest }: ToolButto
|
||||
<button
|
||||
type="button"
|
||||
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,
|
||||
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"
|
||||
|
||||
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 { steamStore } from "./steam"
|
||||
import { toolStore } from "./tool"
|
||||
import { fpsTestStore } from "./fpsTest"
|
||||
import path from "path"
|
||||
|
||||
export async function init() {
|
||||
@@ -11,6 +12,7 @@ export async function init() {
|
||||
await authStore.start()
|
||||
await toolStore.start()
|
||||
await steamStore.start()
|
||||
await fpsTestStore.start()
|
||||
const appConfigDirPath = await appConfigDir()
|
||||
const setPath = commands.setStoreCollectionPath("valtio")
|
||||
await setPath(path.resolve(appConfigDirPath, "cstb"))
|
||||
|
||||
Reference in New Issue
Block a user