better fps testing ui + more info + comment + users minor update

This commit is contained in:
2025-11-06 03:08:20 +08:00
parent 8550887bfb
commit 4c0c33382f
10 changed files with 682 additions and 197 deletions

View File

@@ -45,6 +45,15 @@ export default function RootLayout({ children }: { children: React.ReactNode })
addToast({ title: `电源计划已切换 → ${PowerPlans[current].title}` })
})
void listen<number>("tray://set_launch_index", async (event) => {
const index = event.payload
if (typeof index === "number" && index >= 0 && index < toolStore.state.launchOptions.length) {
tool.setLaunchIndex(index)
const optionName = toolStore.state.launchOptions[index].name || `启动项 ${index + 1}`
addToast({ title: `启动项已切换 → ${optionName}` })
}
})
}, [])
// 检测steam路径和游戏路径是否有效

View File

@@ -30,7 +30,7 @@ export function AuthButton() {
isIconOnly
variant="light"
size="sm"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95 cursor-pointer"
>
<Spinner size="sm" />
</Button>
@@ -46,13 +46,13 @@ export function AuthButton() {
isIconOnly
variant="light"
size="sm"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95 cursor-pointer [&>*]:cursor-pointer"
>
<Avatar
src={state.user.user_metadata?.avatar_url}
name={state.user.email || state.user.id}
size="sm"
className="w-6 h-6"
className="w-6 h-6 cursor-pointer"
/>
</Button>
</DropdownTrigger>
@@ -115,9 +115,9 @@ export function AuthButton() {
isIconOnly
variant="light"
size="sm"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95 cursor-pointer [&>*]:cursor-pointer"
>
<User size={16} />
<User size={16} className="cursor-pointer" />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="登录菜单">

View File

@@ -1,13 +1,36 @@
"use client"
import { useSteamStore } from "@/store/steam"
import { useToolStore } from "@/store/tool"
import { useFpsTestStore } from "@/store/fpsTest"
import { useFpsTestStore } from "@/store/fps_test"
import { invoke } from "@tauri-apps/api/core"
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 {
addToast,
Button,
Chip,
Spinner,
Table,
TableHeader,
TableColumn,
TableBody,
TableRow,
TableCell,
Tabs,
Tab,
Tooltip,
Input,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Textarea,
useDisclosure,
} from "@heroui/react"
import { useState, useEffect, useRef, useCallback } from "react"
import { TestTube, Power, List, Delete } from "@icon-park/react"
import { TestTube, Power, List, Delete, Play, Edit, Check, Close, Square } from "@icon-park/react"
import { allSysInfo, type AllSystemInfo } from "tauri-plugin-system-info-api"
import { ToolButton } from "../window/ToolButton"
const BENCHMARK_MAPS = [
{
@@ -24,6 +47,8 @@ const BENCHMARK_MAPS = [
},
]
const TEST_TIMEOUT = 200000 // 200秒超时时间毫秒
// 解析性能报告,提取时间戳和性能数据
function parseVProfReport(rawReport: string): { timestamp: string; data: string } | null {
if (!rawReport) return null
@@ -156,18 +181,47 @@ 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 [selectedMapIndex, setSelectedMapIndex] = useState(0)
const [isMonitoring, setIsMonitoring] = useState(false)
const [showResultsTable, setShowResultsTable] = useState(false)
const [hardwareInfo, setHardwareInfo] = useState<AllSystemInfo | null>(null)
const [isGameRunning, setIsGameRunning] = useState(false)
const [testNote, setTestNote] = useState<string>("") // 测试备注
const [editingNoteId, setEditingNoteId] = useState<string | null>(null) // 正在编辑的备注ID
const [editingNoteValue, setEditingNoteValue] = useState<string>("") // 正在编辑的备注内容
const { isOpen: isNoteModalOpen, onOpen: onNoteModalOpen, onClose: onNoteModalClose } = useDisclosure()
const monitoringIntervalRef = useRef<NodeJS.Timeout | null>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
// 记录测试开始的时间戳(用于过滤旧数据)
const testStartTimestampRef = useRef<string | null>(null)
const testStartTimeRef = useRef<number | null>(null)
// 记录测试开始时的视频设置
const testStartVideoSettingRef = useRef<typeof tool.state.videoSetting | null>(null)
// 检测游戏是否运行
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])
// 获取硬件信息
useEffect(() => {
@@ -182,6 +236,34 @@ export function FpsTest() {
void fetchHardwareInfo()
}, [])
// 停止测试
const stopTest = useCallback(async () => {
setIsMonitoring(false)
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current)
monitoringIntervalRef.current = null
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
testStartTimestampRef.current = null
testStartTimeRef.current = null
testStartVideoSettingRef.current = null
// 如果启用了自动关闭游戏,则关闭游戏
if (tool.state.autoCloseGame) {
try {
await invoke("kill_game")
addToast({ title: "已停止测试并关闭游戏" })
} catch (error) {
console.error("关闭游戏失败:", error)
}
} else {
addToast({ title: "已停止测试" })
}
}, [tool.state.autoCloseGame])
// 读取结果函数
const readResult = useCallback(
async (silent = false): Promise<boolean> => {
@@ -220,49 +302,80 @@ export function FpsTest() {
setTestTimestamp(parsed.timestamp)
// 成功读取后,清除测试开始时间戳(测试已完成)
testStartTimestampRef.current = null
testStartTimeRef.current = null
// 停止监控和超时
setIsMonitoring(false)
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current)
monitoringIntervalRef.current = null
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.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
const mapConfig = BENCHMARK_MAPS[selectedMapIndex]
// 使用测试开始时的视频设置(如果有的话),否则使用当前的
const currentVideoSetting = testStartVideoSettingRef.current || 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 || "未知地图",
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,
} : null,
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: testNote, // 保存备注
})
// 清除保存的启动时视频设置
testStartVideoSettingRef.current = null
if (!silent) {
if (avg !== null || p1 !== null) {
addToast({
title: `已读取并保存测试结果${avg !== null ? ` (avg: ${avg.toFixed(1)} FPS)` : ""}${p1 !== null ? ` (p1: ${p1.toFixed(1)} FPS)` : ""}`
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({
@@ -288,28 +401,9 @@ export function FpsTest() {
return false
}
},
[steam.state.cs2Dir, selectedMap, selectedMapLabel, fpsTest, tool.store, hardwareInfo]
[steam.state.cs2Dir, selectedMapIndex, fpsTest, tool.store, hardwareInfo, tool.state.autoCloseGame]
)
// 关闭游戏
const closeGame = useCallback(async () => {
try {
await invoke("kill_game")
addToast({ title: "已关闭CS2" })
setIsMonitoring(false)
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current)
monitoringIntervalRef.current = null
}
} catch (error) {
console.error("关闭游戏失败:", error)
addToast({
title: `关闭游戏失败: ${error instanceof Error ? error.message : String(error)}`,
variant: "flat",
})
}
}, [])
// 开始监控文件更新
useEffect(() => {
if (isMonitoring && steam.state.cs2Dir) {
@@ -317,27 +411,46 @@ export function FpsTest() {
monitoringIntervalRef.current = setInterval(async () => {
const success = await readResult(true) // 静默读取
if (success) {
// 读取成功,停止监控
// 读取成功,监控会在readResult中停止
return
}
}, 2000) // 每2秒检查一次
// 设置超时
if (testStartTimeRef.current) {
timeoutRef.current = setTimeout(() => {
// 超时,认为测试失败
setIsMonitoring(false)
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current)
monitoringIntervalRef.current = null
}
testStartTimestampRef.current = null
testStartTimeRef.current = null
testStartVideoSettingRef.current = null
addToast({
title: "测试超时200秒测试失败",
variant: "flat",
color: "warning",
})
// 如果启用了自动关闭游戏,则关闭游戏
if (autoCloseGame) {
if (tool.state.autoCloseGame) {
setTimeout(() => {
void closeGame()
}, 2000) // 延迟2秒关闭让用户看到结果
void invoke("kill_game").catch(() => {})
}, 1000)
}
}
}, 2000) // 每2秒检查一次
}, TEST_TIMEOUT)
}
} else {
// 停止监控
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current)
monitoringIntervalRef.current = null
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
}
// 清理函数
@@ -346,20 +459,48 @@ export function FpsTest() {
clearInterval(monitoringIntervalRef.current)
monitoringIntervalRef.current = null
}
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
}
}, [isMonitoring, steam.state.cs2Dir, autoCloseGame, readResult, closeGame])
}, [isMonitoring, steam.state.cs2Dir, readResult, tool.state.autoCloseGame])
const startTest = async (mapConfig: (typeof BENCHMARK_MAPS)[0]) => {
const startTest = async () => {
if (!steam.state.steamDir || !steam.state.cs2Dir) {
addToast({ title: "请先配置 Steam 和 CS2 路径", variant: "flat", color: "warning" })
return
}
setTesting(true)
setSelectedMap(mapConfig.name)
setSelectedMapLabel(mapConfig.label)
const mapConfig = BENCHMARK_MAPS[selectedMapIndex]
if (!mapConfig) {
addToast({ title: "请选择测试地图", variant: "flat", color: "warning" })
return
}
// 如果启用了自动关闭游戏,检测并关闭正在运行的游戏
if (tool.state.autoCloseGame) {
const gameRunning = await checkGameRunning()
if (gameRunning) {
try {
await invoke("kill_game")
addToast({ title: "检测到游戏正在运行,已关闭" })
// 等待一下确保游戏关闭
await new Promise((resolve) => setTimeout(resolve, 2000))
} catch (error) {
console.error("关闭游戏失败:", error)
addToast({
title: `关闭游戏失败: ${error instanceof Error ? error.message : String(error)}`,
variant: "flat",
})
return
}
}
}
setTestResult(null)
setTestTimestamp(null)
// 注意:不清空备注,让用户可以在测试过程中记住备注内容
// 记录测试开始时间戳格式MM/DD HH:mm:ss
const now = new Date()
@@ -369,6 +510,9 @@ export function FpsTest() {
const minute = String(now.getMinutes()).padStart(2, "0")
const second = String(now.getSeconds()).padStart(2, "0")
testStartTimestampRef.current = `${month}/${day} ${hour}:${minute}:${second}`
testStartTimeRef.current = now.getTime()
// 保存启动时的视频设置
testStartVideoSettingRef.current = { ...tool.store.state.videoSetting }
try {
const launchOption = `-allow_third_party_software -condebug -conclearlog +map_workshop ${mapConfig.workshopId} ${mapConfig.map}`
@@ -381,7 +525,6 @@ export function FpsTest() {
})
addToast({ title: `已启动 ${mapConfig.label} 测试,正在自动监听结果...` })
setTesting(false)
// 开始自动监听文件更新
setIsMonitoring(true)
@@ -391,10 +534,43 @@ export function FpsTest() {
title: `启动测试失败: ${error instanceof Error ? error.message : String(error)}`,
variant: "flat",
})
setTesting(false)
setIsMonitoring(false)
// 启动失败,清除测试开始时间戳
// 启动失败,清除测试开始时间戳和视频设置
testStartTimestampRef.current = null
testStartTimeRef.current = null
testStartVideoSettingRef.current = null
}
}
// 判断是否可以开始测试
const canStartTest = !tool.state.autoCloseGame ? !isGameRunning : true
// 格式化视频设置摘要
const formatVideoSettingSummary = (videoSetting: typeof tool.state.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}`
}
// 打开备注编辑对话框
const handleEditNote = (resultId: string, currentNote: string) => {
setEditingNoteId(resultId)
setEditingNoteValue(currentNote)
onNoteModalOpen()
}
// 保存备注
const handleSaveNote = () => {
if (editingNoteId) {
fpsTest.updateNote(editingNoteId, editingNoteValue)
addToast({ title: "备注已更新" })
onNoteModalClose()
setEditingNoteId(null)
setEditingNoteValue("")
}
}
@@ -403,62 +579,145 @@ export function FpsTest() {
<CardHeader>
<CardIcon>
<TestTube size={16} />
{isGameRunning && !tool.state.autoCloseGame && (
<Chip size="sm" color="warning" variant="flat" className="ml-2 cursor-help">
</Chip>
)}
</CardIcon>
<CardTool>
<Button
size="sm"
variant={showResultsTable ? "solid" : "flat"}
onPress={() => setShowResultsTable(!showResultsTable)}
className="px-3"
>
<List size={14} className="mr-1" />
</Button>
<CardTool className="justify-end">
<div className="flex items-center gap-2">
<Button
size="sm"
variant={showResultsTable ? "solid" : "flat"}
color={showResultsTable ? "secondary" : "default"}
onPress={() => setShowResultsTable(!showResultsTable)}
className="font-medium"
>
<List size={14} />
</Button>
{isMonitoring ? (
<Button
size="sm"
color="danger"
onPress={() => {
void stopTest()
}}
className="font-medium"
>
<Square size={14} />
</Button>
) : (
<Button
size="sm"
color="primary"
isDisabled={!canStartTest}
onPress={() => {
void startTest()
}}
className="font-medium"
>
<Play size={14} />
</Button>
)}
</div>
</CardTool>
</CardHeader>
<CardBody>
{showResultsTable ? (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className="max-h-[500px] overflow-auto">
<Table aria-label="测试结果表格" selectionMode="none">
<Table
aria-label="测试结果表格"
selectionMode="none"
removeWrapper
classNames={{
base: "min-h-[222px]",
th: "px-2 py-1.5 text-xs",
td: "px-2 py-1.5 text-xs",
}}
>
<TableHeader>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn>P1帧</TableColumn>
<TableColumn>CPU</TableColumn>
<TableColumn width={80}></TableColumn>
<TableColumn>AVG平均帧</TableColumn>
<TableColumn>P1</TableColumn>
<TableColumn></TableColumn>
<TableColumn>GPU</TableColumn>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn></TableColumn>
<TableColumn width={100}></TableColumn>
</TableHeader>
<TableBody emptyContent="暂无测试记录">
{fpsTest.state.results.map((result) => (
<TableRow key={result.id}>
<TableCell>{result.testTime}</TableCell>
<TableCell>{result.mapLabel}</TableCell>
<TableCell>
<TableCell className="text-xs">{result.testTime}</TableCell>
<TableCell className="text-xs">{result.mapLabel}</TableCell>
<TableCell className="text-xs">
{result.avg !== null ? `${result.avg.toFixed(1)}` : "N/A"}
</TableCell>
<TableCell>
<TableCell className="text-xs">
{result.p1 !== null ? `${result.p1.toFixed(1)}` : "N/A"}
</TableCell>
<TableCell>
{result.hardwareInfo?.cpu || "N/A"}
<TableCell className="text-xs" title={result.hardwareInfo?.os || undefined}>
{result.hardwareInfo?.os || "N/A"}
</TableCell>
<TableCell className="text-xs" title={result.hardwareInfo?.gpu || undefined}>
{result.hardwareInfo?.gpu || "N/A"}
</TableCell>
<TableCell className="text-xs">
{result.hardwareInfo?.memory ? `${result.hardwareInfo.memory}GB` : "N/A"}
</TableCell>
<TableCell
className="text-xs"
title={formatVideoSettingSummary(result.videoSetting)}
>
<Tooltip content={formatVideoSettingSummary(result.videoSetting)}>
<span className="cursor-help">
{result.videoSetting
? `${result.videoSetting.defaultres}x${result.videoSetting.defaultresheight}`
: "N/A"}
</span>
</Tooltip>
</TableCell>
<TableCell className="text-xs max-w-[150px]">
<div className="flex items-center gap-1">
<span className="truncate" title={result.note || "无备注"}>
{result.note || "无备注"}
</span>
<Button
size="sm"
isIconOnly
variant="light"
onPress={() => handleEditNote(result.id, result.note || "")}
className="h-5 min-w-5 shrink-0"
>
<Edit size={12} />
</Button>
</div>
</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>
<div className="flex items-center gap-1">
<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>
))}
@@ -467,63 +726,127 @@ export function FpsTest() {
</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)
<div className="flex flex-col gap-5">
{/* 表单区域:地图和备注同一行 */}
<div className="flex items-start gap-4">
{/* 测试地图 */}
<div className="flex flex-col gap-1.5">
<label className="text-xs text-default-500"></label>
<Tabs
selectedKey={String(selectedMapIndex)}
onSelectionChange={(key) => {
if (!isMonitoring) {
setSelectedMapIndex(Number(key))
}
}}
className="font-medium transition bg-blue-200 rounded-full select-none dark:bg-blue-900/60"
aria-label="测试地图选择"
size="md"
radius="lg"
>
{testing && selectedMap === mapConfig.name ? (
<Spinner size="sm" className="mr-2" />
) : null}
{mapConfig.label}
</Button>
))}
{BENCHMARK_MAPS.map((map, index) => (
<Tab key={String(index)} title={map.label} />
))}
</Tabs>
</div>
{/* 备注 */}
<div className="flex flex-col gap-1.5 grow">
<label className="text-xs text-default-500"></label>
<Input
size="md"
placeholder="输入测试备注"
value={testNote}
onValueChange={setTestNote}
isDisabled={isMonitoring}
/>
</div>
</div>
{/* 工具栏:按钮靠右对齐 */}
<div className="flex items-center justify-start gap-2">
<Button
size="sm"
isDisabled={isMonitoring}
variant={tool.state.autoCloseGame ? "solid" : "flat"}
color={tool.state.autoCloseGame ? "primary" : "default"}
onPress={() => {
void readResult()
tool.setAutoCloseGame(!tool.state.autoCloseGame)
}}
className="font-medium transition bg-green-200 rounded-full select-none dark:bg-green-900/60"
className="font-medium"
>
{tool.state.autoCloseGame ? <Check size={14} /> : <Close size={14} />}
</Button>
<Button
size="sm"
variant="flat"
onPress={() => {
void closeGame()
void invoke("kill_game")
.then(() => {
addToast({ title: "已关闭CS2" })
void checkGameRunning()
})
.catch((error) => {
console.error("关闭游戏失败:", error)
addToast({
title: `关闭游戏失败: ${
error instanceof Error ? error.message : String(error)
}`,
variant: "flat",
})
})
}}
className="font-medium transition bg-orange-200 rounded-full select-none dark:bg-orange-900/60"
className="font-medium"
>
<Power 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"
variant="flat"
isDisabled={isMonitoring}
onPress={() => {
void readResult()
}}
className="font-medium"
>
</Button>
{/* 测试结果显示测试时间、平均帧、P1低帧 */}
{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="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>
<div>
<div className="text-default-500">P1低帧</div>
<div className="font-medium">{p1 !== null ? `${p1.toFixed(1)}` : "N/A"}</div>
</div>
</div>
</div>
</>
)
})()}
</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>
@@ -532,6 +855,34 @@ export function FpsTest() {
</div>
)}
</CardBody>
{/* 备注编辑对话框 */}
<Modal isOpen={isNoteModalOpen} onClose={onNoteModalClose} size="md">
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
<ModalBody>
<Textarea
placeholder="输入备注内容"
value={editingNoteValue}
onValueChange={setEditingNoteValue}
minRows={3}
maxRows={5}
/>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
</Button>
<Button color="primary" onPress={handleSaveNote}>
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</Card>
)
}

View File

@@ -1,9 +1,11 @@
import { Refresh, User } from "@icon-park/react"
import { Refresh, User, FolderFocusOne, Login, Check } from "@icon-park/react"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { addToast, Button, Chip } from "@heroui/react"
import { useSteamStore } from "@/store/steam"
import { ToolButton } from "../window/ToolButton"
import { useAutoAnimate } from "@formkit/auto-animate/react"
import { invoke } from "@tauri-apps/api/core"
import path from "path"
const SteamUsers = ({ className }: { className?: string }) => {
const steam = useSteamStore()
@@ -68,10 +70,26 @@ const SteamUsers = ({ className }: { className?: string }) => {
</div>
</div>
<div className="flex items-end gap-2 p-2">
<Button size="sm" onPress={() => steam.switchLoginUser(id)}>
<Button
size="sm"
onPress={async () => {
if (!steam.state.steamDirValid) {
addToast({ title: "Steam路径不可用", color: "warning" })
return
}
await invoke("open_path", {
path: path.resolve(steam.state.steamDir, "userdata", user.steam_id32.toString(), "730", "local", "cfg"),
})
addToast({ title: "个人CFG" })
}} className="gap-1"
>
CFG
</Button>
<Button size="sm" onPress={() => steam.switchLoginUser(id)} className="gap-1">
</Button>
<Button size="sm" onPress={() => steam.selectUser(id)}>
<Button size="sm" onPress={() => steam.selectUser(id)} className="gap-1">
<Check size={14} />
</Button>
</div>

View File

@@ -1,13 +1,14 @@
import { CloseSmall, Down, Edit, Plus, SettingConfig, Up } from "@icon-park/react"
import { useEffect, useState, useCallback } from "react"
import { useEffect, useState, useCallback, useRef } from "react"
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
import { ToolButton } from "../window/ToolButton"
import { addToast, NumberInput, Tab, Tabs, Tooltip, Chip } from "@heroui/react"
import { motion } from "framer-motion"
import { useToolStore, VideoSetting as VideoConfig, VideoSettingTemplate } from "@/store/tool"
import { useSteamStore } from "@/store/steam"
import { useDebounce, useDebounceFn } from "ahooks"
import { useDebounce, useDebounceFn, useThrottleFn } from "ahooks"
import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event"
const VideoSetting = () => {
const [hide, setHide] = useState(false)
@@ -15,6 +16,11 @@ const VideoSetting = () => {
const [isGameRunning, setIsGameRunning] = useState(false)
const tool = useToolStore()
const steam = useSteamStore()
// 使用 ref 存储 edit 的最新值,供 throttle 回调使用
const editRef = useRef(edit)
useEffect(() => {
editRef.current = edit
}, [edit])
// 检测游戏是否运行
const checkGameRunning = useCallback(async () => {
@@ -42,7 +48,7 @@ const VideoSetting = () => {
addToast({ title: "请先选择用户", color: "danger" })
}
},
{ wait: 500, leading: true, trailing: false }
{ wait: 500, leading: false, trailing: true }
)
const videoSettings = (video: VideoConfig) => {
return [
@@ -286,7 +292,7 @@ const VideoSetting = () => {
// 定期检测游戏运行状态
const interval = setInterval(() => {
void checkGameRunning()
}, 2000)
}, 4000)
return () => clearInterval(interval)
}, [checkGameRunning])
@@ -301,10 +307,55 @@ const VideoSetting = () => {
trailing: true,
maxWait: 2500,
})
// 节流重新读取配置函数2秒间隔trailing模式
const { run: throttledRefreshVideoConfig } = useThrottleFn(
async () => {
if (steam.state.steamDirValid && steam.currentUser()) {
await tool.getVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0
)
// 如果不在编辑状态,更新本地状态(使用 ref 获取最新的 edit 值)
if (!editRef.current) {
setVconfig(tool.state.videoSetting)
}
addToast({ title: "检测到视频设置文件变动,已自动刷新", color: "success" })
}
},
{
wait: 2000,
leading: false,
trailing: true,
}
)
useEffect(() => {
if (steam.state.steamDirValid && steam.currentUser())
if (steam.state.steamDirValid && steam.currentUser()) {
void tool.getVideoConfig(steam.state.steamDir, steam.currentUser()?.steam_id32 || 0)
}, [debounceCurrentUserId])
// 启动文件监听
void invoke("start_watch_cs2_video", {
steamDir: steam.state.steamDir,
steamId32: steam.currentUser()?.steam_id32 || 0,
})
}
// 清理函数:停止后端监听
return () => {
void invoke("stop_watch_cs2_video").catch(() => {
// 忽略错误,可能监听器已经不存在
})
}
}, [debounceCurrentUserId, steam.state.steamDirValid, steam.state.steamDir, tool])
// 监听 cs2_video.txt 文件变动事件
useEffect(() => {
const unlisten = listen("steam://cs2_video_changed", () => {
// 文件变化时使用节流函数重新读取配置
throttledRefreshVideoConfig()
})
return () => {
void unlisten.then((fn) => fn())
}
}, [throttledRefreshVideoConfig])
return (
<Card>
@@ -346,35 +397,42 @@ const VideoSetting = () => {
>
</ToolButton>
<ToolButton
onClick={async () => {
// 检查游戏是否运行
const gameRunning = await checkGameRunning()
if (gameRunning) {
addToast({
title: "无法应用设置",
description: "检测到游戏正在运行,请先关闭游戏后再应用设置",
color: "warning",
})
return
}
await tool.setVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0,
vconfig
)
await tool.getVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0
)
setEdit(false)
addToast({ title: "应用设置成功" })
}}
disabled={isGameRunning}
<Tooltip
content={isGameRunning ? "游戏运行中,无法修改视频设置" : ""}
isDisabled={!isGameRunning}
>
<Plus />
</ToolButton>
<div>
<ToolButton
onClick={async () => {
// 检查游戏是否运行
const gameRunning = await checkGameRunning()
if (gameRunning) {
addToast({
title: "无法应用设置",
description: "检测到游戏正在运行,请先关闭游戏后再应用设置",
color: "warning",
})
return
}
await tool.setVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0,
vconfig
)
await tool.getVideoConfig(
steam.state.steamDir,
steam.currentUser()?.steam_id32 || 0
)
setEdit(false)
addToast({ title: "应用设置成功" })
}}
disabled={isGameRunning}
>
<Plus />
</ToolButton>
</div>
</Tooltip>
</>
)}
<ToolButton

View File

@@ -61,38 +61,38 @@ const Nav = () => {
{pathname !== "/" && (
<button
type="button"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={() => {
app.setInited(false)
if (pathname !== "/") router.push("/")
}}
>
<RocketOne size={16} />
<RocketOne size={16} className="cursor-pointer" />
</button>
)}
</Tooltip>
<Tooltip content="深色模式" showArrow={true} delay={300}>
<button
type="button"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={() => (theme === "light" ? setAppTheme("dark") : setAppTheme("light"))}
>
{theme === "light" ? <SunOne size={16} /> : <Moon size={16} />}
{theme === "light" ? <SunOne size={16} className="cursor-pointer" /> : <Moon size={16} className="cursor-pointer" />}
</button>
</Tooltip>
<Tooltip content="反馈" showArrow={true} delay={300}>
<Link
href="https://docs.qq.com/form/page/DZU1ieW9SQkxWU1RF"
target="_blank"
className="px-2 py-0 text-black rounded transition duration-150 dark:text-white hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 text-black transition duration-150 rounded cursor-pointer dark:text-white hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
>
<button type="button">
<Communication size={16} />
<button type="button" className="cursor-pointer">
<Communication size={16} className="cursor-pointer" />
</button>
</Link>
</Tooltip>
<AuthButtonWrapper />
{/* <AuthButtonWrapper /> */}
<ResetModal />
@@ -100,24 +100,24 @@ const Nav = () => {
<>
<button
type="button"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={minimize}
>
<Minus size={16} />
<Minus size={16} className="cursor-pointer" />
</button>
<button
type="button"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={toggleMaximize}
>
<Square size={16} />
<Square size={16} className="cursor-pointer" />
</button>
<button
type="button"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={close}
>
<Close size={16} />
<Close size={16} className="cursor-pointer" />
</button>
</>
{/* )} */}
@@ -149,10 +149,10 @@ function ResetModal() {
<Tooltip content="重置设置" showArrow={true} delay={300}>
<button
type="button"
className="px-2 py-0 rounded transition duration-150 hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
className="px-2 py-0 transition duration-150 rounded cursor-pointer hover:bg-black/10 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={onOpen}
>
<Refresh size={16} />
<Refresh size={16} className="cursor-pointer" />
</button>
</Tooltip>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>

View File

@@ -43,7 +43,7 @@ const SideButton = ({
onClick={() => router.push(route || "/")}
className={cn(
className,
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90 cursor-pointer",
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90 cursor-pointer [&>*]:cursor-pointer",
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
)}
{...rest}

View File

@@ -6,12 +6,17 @@ interface ToolButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
className?: string
selected?: boolean
}
export const ToolButton = ({ children, className, selected, ...rest }: ToolButtonProps) => {
export const ToolButton = ({ children, className, selected, disabled, ...rest }: ToolButtonProps) => {
return (
<button
type="button"
disabled={disabled}
className={cn(
"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",
"flex shrink-0 gap-0.5 items-center min-w-7 justify-center px-2 py-1.5 bg-black/5 transition rounded-md text-sm leading-none",
disabled
? "opacity-50 cursor-not-allowed"
: "active:scale-95 cursor-pointer hover:bg-black/10 dark:hover:bg-white/10",
"dark:bg-white/5",
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"

View File

@@ -19,7 +19,10 @@ export interface FpsTestResult {
cpuCount: number | null
os: string | null
memory: number | null // GB
gpu: string | null
monitor: string | null
} | null // 硬件信息
note?: string // 备注(可选,用于向后兼容)
}
const defaultValue = {
@@ -27,7 +30,7 @@ const defaultValue = {
}
export const fpsTestStore = store(
"fpsTest",
"fps_test",
{ ...defaultValue },
DEFAULT_STORE_CONFIG,
)
@@ -42,6 +45,7 @@ export const useFpsTestStore = () => {
addResult,
removeResult,
clearResults,
updateNote,
}
}
@@ -63,3 +67,10 @@ const clearResults = () => {
fpsTestStore.state.results = []
}
const updateNote = (id: string, note: string) => {
const result = fpsTestStore.state.results.find((r) => r.id === id)
if (result) {
result.note = note
}
}

View File

@@ -123,6 +123,7 @@ const defaultValue = {
] as LaunchOption[],
launchIndex: 0,
powerPlan: 0,
autoCloseGame: true, // 帧数测试自动关闭游戏
videoSetting: {
version: "15",
vendor_id: "0",
@@ -184,6 +185,7 @@ export const useToolStore = () => {
setLaunchIndex,
removeLaunchOption,
setPowerPlan,
setAutoCloseGame,
setVideoSetting,
getVideoConfig,
setVideoConfig,
@@ -198,10 +200,17 @@ const setLaunchOption = (option: LaunchOption, index: number) => {
option,
...toolStore.state.launchOptions.slice(index + 1),
]
// 同步更新托盘
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
}
const setLaunchOptions = (options: LaunchOption[]) => {
toolStore.state.launchOptions = options
// 确保索引在有效范围内
if (toolStore.state.launchIndex >= options.length) {
toolStore.state.launchIndex = options.length > 0 ? options.length - 1 : 0
}
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
}
const setLaunchIndex = (index: number) => {
@@ -214,10 +223,27 @@ const removeLaunchOption = (index: number) => {
...toolStore.state.launchOptions.slice(0, index),
...toolStore.state.launchOptions.slice(index + 1),
]
// 如果删除的是当前项或当前项在删除项之后,需要调整索引
if (index <= toolStore.state.launchIndex) {
if (toolStore.state.launchIndex > 0) {
toolStore.state.launchIndex -= 1
} else {
toolStore.state.launchIndex = 0
}
}
// 确保索引在有效范围内
if (toolStore.state.launchIndex >= toolStore.state.launchOptions.length) {
toolStore.state.launchIndex = toolStore.state.launchOptions.length > 0 ? toolStore.state.launchOptions.length - 1 : 0
}
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
}
const sendCurrentLaunchOptionToTray = (index: number) => {
void emit("tray://get_current_launch_option", toolStore.state.launchOptions[index].name || index + 1)
// 发送完整的启动项列表和当前索引到托盘
void emit("tray://update_launch_options", {
options: toolStore.state.launchOptions,
currentIndex: index,
})
}
const setPowerPlan = (plan: number) => {
toolStore.state.powerPlan = plan
@@ -227,6 +253,10 @@ const sendPowerPlanToTray = (plan: number) => {
void emit("tray://get_powerplan", plan)
}
const setAutoCloseGame = (enabled: boolean) => {
toolStore.state.autoCloseGame = enabled
}
const setVideoSetting = (setting: VideoSetting) => {
toolStore.state.videoSetting = setting
}
@@ -248,11 +278,14 @@ const addLaunchOption = (option: LaunchOption) => {
return
}
toolStore.state.launchOptions = [...toolStore.state.launchOptions, option]
// 同步更新托盘
sendCurrentLaunchOptionToTray(toolStore.state.launchIndex)
}
const resetToolStore = () => {
setLaunchOptions(defaultValue.launchOptions)
setLaunchIndex(defaultValue.launchIndex)
setPowerPlan(defaultValue.powerPlan)
setAutoCloseGame(defaultValue.autoCloseGame)
setVideoSetting(defaultValue.videoSetting)
}