[feat] more fps test settings and csv export button

This commit is contained in:
2025-11-06 14:26:17 +08:00
parent 72eef189da
commit e774b31396
3 changed files with 364 additions and 82 deletions

View File

@@ -17,29 +17,39 @@ export default function Page() {
className="flex flex-col items-center justify-center w-full h-screen gap-6"
data-tauri-drag-region
>
<h1 className="text-4xl font-bold tracking-wide">CS工具</h1>
<p></p>
<h1 className="text-4xl font-bold tracking-wide">CS </h1>
<p className="text-sm text-zinc-500"></p>
<div className="flex flex-col w-full gap-2 p-5 border rounded-lg bg-white/40">
<p>Steam所在文件夹</p>
<div className="flex flex-col w-full max-w-2xl gap-4 p-5 border rounded-lg bg-white/40 dark:bg-zinc-800/40">
<div className="space-y-2">
<p className="text-sm font-semibold">Steam </p>
<input
className="px-2 py-1 mb-2 rounded-lg"
className="w-full px-3 py-2 rounded-lg bg-white dark:bg-zinc-700 border border-zinc-300 dark:border-zinc-600"
placeholder="请输入 Steam 安装路径"
value={steamDir}
onChange={(e) => {
setSteamDir(e.target.value)
steam.setDir(e.target.value)
}}
/>
<p>CS2所在文件夹</p>
</div>
<div className="space-y-2">
<p className="text-sm font-semibold">CS2 </p>
<input
className="px-2 py-1 mb-2 rounded-lg"
className="w-full px-3 py-2 rounded-lg bg-white dark:bg-zinc-700 border border-zinc-300 dark:border-zinc-600"
placeholder="请输入 CS2 安装路径"
value={cs2Dir}
onChange={(e) => {
setCs2Dir(e.target.value)
steam.setCsDir(e.target.value)
}}
/>
<p>64SteamID{steam.currentUser()?.steam_id64}</p>
</div>
{steam.currentUser()?.steam_id64 && (
<p className="text-xs text-zinc-500">
64 Steam ID{steam.currentUser()?.steam_id64}
</p>
)}
</div>
</div>
)

View File

@@ -6,7 +6,7 @@ import { useCallback, useState } from "react"
export default function Page() {
const [buttonDesc, setButtonDesc] = useState<string>(
"Waiting to be clicked. This calls 'on_button_clicked' from Rust.",
"等待点击。这将调用 Rust 中的 'on_button_clicked' 命令。",
)
const onButtonClick = () => {
invoke<string>("on_button_clicked")
@@ -14,7 +14,7 @@ export default function Page() {
setButtonDesc(value)
})
.catch(() => {
setButtonDesc("Failed to invoke Rust command 'on_button_clicked'")
setButtonDesc("调用 Rust 命令 'on_button_clicked' 失败")
})
}
@@ -27,7 +27,7 @@ export default function Page() {
<div className="flex flex-col">
<main className="flex flex-col items-center justify-center flex-1 py-8">
<h1 className="m-0 text-6xl text-center">
Welcome to{" "}
使{" "}
<a
href="https://nextjs.org"
target="_blank"
@@ -39,7 +39,7 @@ export default function Page() {
</h1>
<p className="my-12 text-2xl leading-9 text-center">
Get started by editing{" "}
{" "}
<code className="p-2 font-mono text-xl bg-gray-200 rounded-md">
src/pages/index.tsx
</code>
@@ -48,7 +48,7 @@ export default function Page() {
<div className="flex flex-wrap items-center justify-center max-w-3xl">
<CardButton
onClick={onButtonClick}
title="Tauri Invoke"
title="Tauri 调用"
description={buttonDesc}
/>
</div>

View File

@@ -26,11 +26,17 @@ import {
ModalFooter,
Textarea,
useDisclosure,
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
} from "@heroui/react"
import { useState, useEffect, useRef, useCallback } from "react"
import { TestTube, Power, List, Delete, Play, Edit, Check, Close, Square } from "@icon-park/react"
import { TestTube, Power, List, Delete, Play, Edit, Check, Close, Square, DownloadOne } from "@icon-park/react"
import { allSysInfo, type AllSystemInfo } from "tauri-plugin-system-info-api"
import { ToolButton } from "../window/ToolButton"
import { save } from "@tauri-apps/plugin-dialog"
import { writeTextFile } from "@tauri-apps/plugin-fs"
const BENCHMARK_MAPS = [
{
@@ -49,6 +55,19 @@ const BENCHMARK_MAPS = [
const TEST_TIMEOUT = 200000 // 200秒超时时间毫秒
// 预设分辨率列表
const PRESET_RESOLUTIONS = [
{ width: "800", height: "600", label: "800x600" },
{ width: "1024", height: "768", label: "1024x768" },
{ width: "1280", height: "960", label: "1280x960" },
{ width: "1440", height: "1080", label: "1440x1080" },
{ width: "1920", height: "1080", label: "1920x1080" },
{ width: "1920", height: "1440", label: "1920x1440" },
{ width: "2560", height: "1440", label: "2560x1440" },
{ width: "2880", height: "2160", label: "2880x2160" },
{ width: "3840", height: "2160", label: "3840x2160" },
] as const
// 解析性能报告,提取时间戳和性能数据
function parseVProfReport(rawReport: string): { timestamp: string; data: string } | null {
if (!rawReport) return null
@@ -191,6 +210,11 @@ export function FpsTest() {
const [testNote, setTestNote] = useState<string>("") // 测试备注
const [editingNoteId, setEditingNoteId] = useState<string | null>(null) // 正在编辑的备注ID
const [editingNoteValue, setEditingNoteValue] = useState<string>("") // 正在编辑的备注内容
const [customLaunchOption, setCustomLaunchOption] = useState<string>("") // 自定义启动项
const [isResolutionEnabled, setIsResolutionEnabled] = useState<boolean>(true) // 是否启用分辨率和全屏设置
const [resolutionWidth, setResolutionWidth] = useState<string>("") // 分辨率宽度
const [resolutionHeight, setResolutionHeight] = useState<string>("") // 分辨率高度
const [isFullscreen, setIsFullscreen] = useState<boolean>(true) // 全屏模式(默认全屏)
const { isOpen: isNoteModalOpen, onOpen: onNoteModalOpen, onClose: onNoteModalClose } = useDisclosure()
const monitoringIntervalRef = useRef<NodeJS.Timeout | null>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
@@ -223,6 +247,15 @@ export function FpsTest() {
return () => clearInterval(interval)
}, [checkGameRunning])
// 同步当前分辨率到状态(初始化时)
useEffect(() => {
if (tool.state.videoSetting) {
setResolutionWidth(tool.state.videoSetting.defaultres || "")
setResolutionHeight(tool.state.videoSetting.defaultresheight || "")
setIsFullscreen(tool.state.videoSetting.fullscreen === "1")
}
}, [tool.state.videoSetting])
// 获取硬件信息
useEffect(() => {
const fetchHardwareInfo = async () => {
@@ -515,9 +548,30 @@ export function FpsTest() {
testStartVideoSettingRef.current = { ...tool.store.state.videoSetting }
try {
const launchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}`
// 构建启动参数:基础参数 + 分辨率和全屏设置 + 自定义启动项(如果有)
let baseLaunchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}`
// 启动游戏
// 只有在启用分辨率和全屏设置时才添加相关参数
if (isResolutionEnabled) {
// 添加分辨率设置(如果有设置)
if (resolutionWidth && resolutionHeight) {
baseLaunchOption += ` -w ${resolutionWidth} -h ${resolutionHeight}`
}
// 添加全屏/窗口化设置
if (isFullscreen) {
baseLaunchOption += ` -fullscreen`
} else {
baseLaunchOption += ` -sw`
}
}
// 添加自定义启动项(如果有,开头加空格避免粘连)
const launchOption = customLaunchOption.trim()
? `${baseLaunchOption} ${customLaunchOption.trim()}`
: baseLaunchOption
// 启动游戏强制使用worldwide国际服
await invoke("launch_game", {
steamPath: `${steam.state.steamDir}\\steam.exe`,
launchOption: launchOption,
@@ -574,11 +628,104 @@ export function FpsTest() {
}
}
// 导出CSV
const handleExportCSV = async () => {
if (fpsTest.state.results.length === 0) {
addToast({ title: "没有测试数据可导出", color: "warning" })
return
}
try {
// 构建CSV内容
const headers = [
"测试时间",
"测试地图",
"AVG平均帧",
"P1低帧",
"CPU",
"系统版本",
"GPU",
"内存(GB)",
"分辨率",
"视频设置",
"备注",
]
const csvRows = [headers.join(",")]
for (const result of fpsTest.state.results) {
const row = [
`"${result.testTime}"`,
`"${result.mapLabel}"`,
result.avg !== null ? result.avg.toFixed(1) : "N/A",
result.p1 !== null ? result.p1.toFixed(1) : "N/A",
`"${result.hardwareInfo?.cpu || "N/A"}"`,
`"${result.hardwareInfo?.os || "N/A"}"`,
`"${result.hardwareInfo?.gpu || "N/A"}"`,
result.hardwareInfo?.memory ? result.hardwareInfo.memory.toString() : "N/A",
result.videoSetting
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
: "N/A",
`"${formatVideoSettingSummary(result.videoSetting)}"`,
`"${result.note || ""}"`,
]
csvRows.push(row.join(","))
}
const csvContent = csvRows.join("\n")
// 使用文件保存对话框
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, csvContent)
addToast({ title: "导出成功", color: "success" })
}
} catch (error) {
console.error("导出CSV失败:", error)
addToast({
title: `导出失败: ${error instanceof Error ? error.message : String(error)}`,
color: "danger",
})
}
}
// 切换全屏/窗口化(仅更新本地状态,不修改视频配置文件)
const handleToggleFullscreen = () => {
setIsFullscreen(!isFullscreen)
}
// 设置分辨率(仅更新本地状态,不修改视频配置文件)
const handleSetResolution = (width: string, height: string) => {
setResolutionWidth(width)
setResolutionHeight(height)
}
// 应用预设分辨率
const handlePresetResolution = (preset: { width: string; height: string; label: string }) => {
void handleSetResolution(preset.width, preset.height)
}
return (
<Card className="w-full">
<CardHeader>
<CardIcon>
<Tooltip
placement="bottom-start"
content="需订阅创意工坊地图 CS2 FPS BENCHMARK DUST2 和 CS2 FPS BENCHMARK ANCIENT"
>
<div className="flex items-center gap-1 cursor-help">
<TestTube size={16} />
</div>
</Tooltip>
{isGameRunning && !tool.state.autoCloseGame && (
<Chip size="sm" color="warning" variant="flat" className="ml-2 cursor-help">
@@ -587,6 +734,12 @@ export function FpsTest() {
</CardIcon>
<CardTool className="justify-end">
<div className="flex items-center gap-2">
{showResultsTable && (
<Button size="sm" variant="flat" onPress={handleExportCSV} className="font-medium">
<DownloadOne size={14} />
CSV
</Button>
)}
<Button
size="sm"
variant={showResultsTable ? "solid" : "flat"}
@@ -645,6 +798,7 @@ export function FpsTest() {
<TableColumn></TableColumn>
<TableColumn>AVG平均帧</TableColumn>
<TableColumn>P1低帧</TableColumn>
<TableColumn>CPU</TableColumn>
<TableColumn></TableColumn>
<TableColumn>GPU</TableColumn>
<TableColumn></TableColumn>
@@ -663,6 +817,9 @@ export function FpsTest() {
<TableCell className="text-xs">
{result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"}
</TableCell>
<TableCell className="text-xs" title={result.hardwareInfo?.cpu || undefined}>
{result.hardwareInfo?.cpu || "N/A"}
</TableCell>
<TableCell className="text-xs" title={result.hardwareInfo?.os || undefined}>
{result.hardwareInfo?.os || "N/A"}
</TableCell>
@@ -726,12 +883,12 @@ export function FpsTest() {
</div>
</div>
) : (
<div className="flex flex-col gap-5">
{/* 表单区域:地图和备注同一行 */}
<div className="flex items-start gap-4">
{/* 测试地图 */}
<div className="flex flex-col gap-4">
{/* 备注单独一行 - 放在最上面 */}
<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) => {
@@ -748,19 +905,129 @@ export function FpsTest() {
))}
</Tabs>
</div>
{/* 备注 */}
<div className="flex flex-col gap-1.5 grow">
</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={setTestNote}
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={setCustomLaunchOption}
isDisabled={isMonitoring}
className="flex-1"
/>
</div>
</div>
{/* 分辨率和全屏/窗口化设置 */}
<div className="flex items-end gap-2 shrink-0">
{/* 分辨率设置 */}
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-1">
<label className="text-xs text-default-500"></label>
<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: { width: string; height: string; label: string }) => (
<DropdownItem
key={preset.label}
onPress={() => handlePresetResolution(preset)}
>
{preset.label}
</DropdownItem>
)
)}
</DropdownMenu>
</Dropdown>
<Button
size="sm"
variant={isResolutionEnabled ? "solid" : "flat"}
color={isResolutionEnabled ? "primary" : "default"}
onPress={() => setIsResolutionEnabled(!isResolutionEnabled)}
isDisabled={isMonitoring}
className="h-5 min-w-[40px] px-1.5 text-xs font-medium"
>
{isResolutionEnabled ? "启用" : "关闭"}
</Button>
</div>
<div className="flex items-center gap-1">
<Input
size="md"
type="number"
placeholder="宽"
value={resolutionWidth}
onValueChange={setResolutionWidth}
isDisabled={!isResolutionEnabled || isMonitoring}
className="w-20"
onBlur={() => {
if (resolutionWidth && resolutionHeight) {
void handleSetResolution(resolutionWidth, resolutionHeight)
}
}}
/>
<span className="text-xs text-default-400">x</span>
<Input
size="md"
type="number"
placeholder="高"
value={resolutionHeight}
onValueChange={setResolutionHeight}
isDisabled={!isResolutionEnabled || isMonitoring}
className="w-20"
onBlur={() => {
if (resolutionWidth && resolutionHeight) {
void handleSetResolution(resolutionWidth, resolutionHeight)
}
}}
/>
</div>
</div>
{/* 全屏/窗口化切换 */}
<div className="flex flex-col gap-1.5">
<div className="h-5" /> {/* 对齐标签高度 */}
<Button
size="md"
variant={isFullscreen ? "solid" : "flat"}
color={isFullscreen ? "primary" : "default"}
onPress={handleToggleFullscreen}
isDisabled={!isResolutionEnabled || isMonitoring}
className="font-medium"
>
{isFullscreen ? "全屏" : "窗口化"}
</Button>
</div>
</div>
</div>
{/* 工具栏:按钮靠右对齐 */}
<div className="flex items-center justify-start gap-2">
@@ -819,24 +1086,29 @@ export function FpsTest() {
</Button>
{/* 测试结果显示测试时间、平均帧、P1低帧 */}
{testResult && testTimestamp && (() => {
{testResult &&
testTimestamp &&
(() => {
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-default-500"></div>
<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-default-500"></div>
<div className="font-medium">{avg !== null ? `${avg.toFixed(1)}` : "N/A"}</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-default-500">P1低帧</div>
<div className="font-medium">{p1 !== null ? `${p1.toFixed(1)}` : "N/A"}</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>