[feat] more fps test settings and csv export button
This commit is contained in:
@@ -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>当前用户64位SteamID:{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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user