From efb11b8ee7672f716472ed517ab0a9a7cb3be9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=9C=B1?= <10714957+xiao-zhu245@user.noreply.gitee.com> Date: Wed, 30 Jul 2025 18:37:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AE=89=E8=A3=85=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E7=8E=AF=E5=A2=83=E6=A3=80=E6=B5=8B-=E5=86=85?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/GameDeploymentPage.tsx | 181 ++++++++++++++++++++++-- client/src/utils/api.ts | 4 + server/data/games/installgame.json | 14 +- server/src/routes/gameDeployment.ts | 77 +++++++++- 4 files changed, 255 insertions(+), 21 deletions(-) diff --git a/client/src/pages/GameDeploymentPage.tsx b/client/src/pages/GameDeploymentPage.tsx index 8d801ad..0dbde20 100644 --- a/client/src/pages/GameDeploymentPage.tsx +++ b/client/src/pages/GameDeploymentPage.tsx @@ -145,6 +145,17 @@ const GameDeploymentPage: React.FC = () => { const [showCompatibilityModal, setShowCompatibilityModal] = useState(false) const [compatibilityModalAnimating, setCompatibilityModalAnimating] = useState(false) const [pendingGameInstall, setPendingGameInstall] = useState<{ key: string; info: GameInfo } | null>(null) + + // 内存警告对话框状态 + const [showMemoryWarningModal, setShowMemoryWarningModal] = useState(false) + const [memoryWarningModalAnimating, setMemoryWarningModalAnimating] = useState(false) + const [memoryWarningInfo, setMemoryWarningInfo] = useState<{ + required: number + available: number + message: string + gameKey: string + gameInfo: GameInfo + } | null>(null) // 开服文档相关状态 const [showDocsModal, setShowDocsModal] = useState(false) @@ -1128,7 +1139,7 @@ const GameDeploymentPage: React.FC = () => { }, []) // 打开安装对话框 - const handleInstallGame = (gameKey: string, gameInfo: GameInfo) => { + const handleInstallGame = async (gameKey: string, gameInfo: GameInfo) => { // 检查游戏是否支持当前平台 if (gameInfo.supportedOnCurrentPlatform === false) { addNotification({ @@ -1138,16 +1149,48 @@ const GameDeploymentPage: React.FC = () => { }) return } - + // 检查面板是否兼容当前平台 if (gameInfo.panelCompatibleOnCurrentPlatform === false) { // 显示兼容性确认对话框 setPendingGameInstall({ key: gameKey, info: gameInfo }) setShowCompatibilityModal(true) - setTimeout(() => setCompatibilityModalAnimating(true), 10) + // 使用requestAnimationFrame确保DOM渲染完成后再触发动画 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setCompatibilityModalAnimating(true) + }) + }) return } - + + // 检查内存需求 + try { + const memoryCheckResponse = await apiClient.checkGameMemory(gameKey) + const memoryWarning = (memoryCheckResponse as any).memoryWarning + if (memoryCheckResponse.success && memoryWarning) { + // 显示内存警告对话框 + setMemoryWarningInfo({ + required: memoryWarning.required, + available: memoryWarning.available, + message: memoryWarning.message, + gameKey, + gameInfo + }) + setShowMemoryWarningModal(true) + // 使用requestAnimationFrame确保DOM渲染完成后再触发动画 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setMemoryWarningModalAnimating(true) + }) + }) + return + } + } catch (error) { + console.warn('检查内存需求失败,继续安装流程:', error) + // 内存检查失败不应阻止安装流程 + } + // 直接打开安装对话框 openInstallModal(gameKey, gameInfo) } @@ -1158,7 +1201,12 @@ const GameDeploymentPage: React.FC = () => { setInstanceName(gameInfo.game_nameCN) setInstallPath('') setShowInstallModal(true) - setTimeout(() => setInstallModalAnimating(true), 10) + // 使用requestAnimationFrame确保DOM渲染完成后再触发动画 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setInstallModalAnimating(true) + }) + }) } // 关闭安装对话框 @@ -1178,6 +1226,24 @@ const GameDeploymentPage: React.FC = () => { }, 300) } + // 关闭内存警告对话框 + const handleCloseMemoryWarningModal = () => { + setMemoryWarningModalAnimating(false) + setTimeout(() => { + setShowMemoryWarningModal(false) + setMemoryWarningInfo(null) + }, 300) + } + + // 确认继续安装(忽略内存警告) + const handleContinueInstallation = () => { + if (memoryWarningInfo) { + handleCloseMemoryWarningModal() + // 打开安装对话框 + openInstallModal(memoryWarningInfo.gameKey, memoryWarningInfo.gameInfo) + } + } + // 确认继续安装不兼容的游戏 const handleConfirmIncompatibleInstall = () => { if (pendingGameInstall) { @@ -1343,17 +1409,10 @@ const GameDeploymentPage: React.FC = () => { steamPassword: useAnonymous ? undefined : steamPassword.trim(), steamcmdCommand: fullCommand }) - + if (response.success && response.data?.terminalSessionId) { - addNotification({ - type: 'success', - title: '安装已启动', - message: `${selectedGame.info.game_nameCN} 安装已开始,即将跳转到终端页面...` - }) - // 跳转到终端页面,并将会话ID作为参数传递 - setTimeout(() => { - navigate(`/terminal?sessionId=${response.data.terminalSessionId}`) - }, 1500) // 延迟以便用户看到通知 + // 直接跳转到终端页面 + proceedWithInstallation(response.data) } else { throw new Error(response.message || '安装失败,未返回终端会话ID') } @@ -1367,6 +1426,19 @@ const GameDeploymentPage: React.FC = () => { } } + // 继续安装流程(显示成功通知并跳转) + const proceedWithInstallation = (installData: any) => { + addNotification({ + type: 'success', + title: '安装已启动', + message: `${selectedGame?.info.game_nameCN} 安装已开始,即将跳转到终端页面...` + }) + // 跳转到终端页面,并将会话ID作为参数传递 + setTimeout(() => { + navigate(`/terminal?sessionId=${installData.terminalSessionId}`) + }, 1500) // 延迟以便用户看到通知 + } + // 选择安装路径 const selectInstallPath = async () => { try { @@ -3889,6 +3961,85 @@ const GameDeploymentPage: React.FC = () => { )} + + {/* 内存警告对话框 */} + {showMemoryWarningModal && ( +
+
+
+
+
+ +
+
+

+ 内存不足警告 +

+

+ 系统内存可能不足以运行此游戏 +

+
+
+ + {memoryWarningInfo && ( +
+
+

+ {memoryWarningInfo.message} +

+
+ +
+
+
推荐内存
+
+ {memoryWarningInfo.required} GB +
+
+
+
系统内存
+
+ {memoryWarningInfo.available} GB +
+
+
+ +
+

+ 提示:您仍然可以继续安装,但可能会遇到以下问题: +

+
    +
  • • 游戏服务器启动缓慢或失败
  • +
  • • 运行过程中出现卡顿或崩溃
  • +
  • • 系统整体性能下降
  • +
+
+
+ )} +
+ +
+ + +
+
+
+ )} ) } diff --git a/client/src/utils/api.ts b/client/src/utils/api.ts index 39b07d7..ce89995 100644 --- a/client/src/utils/api.ts +++ b/client/src/utils/api.ts @@ -491,6 +491,10 @@ class ApiClient { return this.get('/game-deployment/games') } + async checkGameMemory(gameKey: string) { + return this.post('/game-deployment/check-memory', { gameKey }) + } + async installGame(data: { gameKey: string gameName: string diff --git a/server/data/games/installgame.json b/server/data/games/installgame.json index 08a2e3a..b31d9e3 100644 --- a/server/data/games/installgame.json +++ b/server/data/games/installgame.json @@ -5,7 +5,8 @@ "tip": "游戏端口:8211 UDP,配置文件位置:游戏根目录,存档位置:Pal/Saved/SaveGames,", "image":"https://shared.cdn.queniuqe.com/store_item_assets/steam/apps/1623730/44e7cf48b38e3ace008e9f49c316f8cd949f7918/header_schinese.jpg", "url":"https://store.steampowered.com/app/1623730/Palworld/", - "system":["Windows","Linux"] + "system":["Windows","Linux"], + "memory": 4 }, "SCUM": { "game_nameCN": "人渣", @@ -13,15 +14,17 @@ "tip": "Windows需要开启输出流转发\\SCUM\\Binaries\\Win64\\SCUMServer.exe -Port=7777 -QueryPort=27015 -MaxPlayers=32 -log", "image":"https://shared.cdn.queniuqe.com/store_item_assets/steam/apps/513710/dc41d8c9bad51f61f618419b8e510f0b4617ad42/header_schinese.jpg", "url":"https://store.steampowered.com/app/513710", - "system":["Windows"] + "system":["Windows"], + "memory": 6 }, "Rust": { "game_nameCN": "腐蚀", "appid": "258550", - "tip": "游戏端口:28015 UDP, 28016 TCP,配置文件位置:Serveridentity,存档位置:Serveridentity,温馨提示:服务器第一次启动时会生成地图,配置较低非固态盘可能需要很长一段时间,请确保服务器有足够的内存(推荐至少8GB)", + "tip": "游戏端口:28015 UDP, 28016 TCP,配置文件位置:Serveridentity,存档位置:Serveridentity,温馨提示:服务器第一次启动时会生成地图,配置较低非固态盘可能需要很长一段时间,请确保服务器有足够的内存(推荐至少12GB)", "image":"https://shared.cdn.queniuqe.com/store_item_assets/steam/apps/252490/header.jpg", "url":"https://store.steampowered.com/app/252490/Rust/", - "system":["Windows","Linux"] + "system":["Windows","Linux"], + "memory": 12 }, "Satisfactory": { "game_nameCN": "幸福工厂", @@ -454,7 +457,8 @@ "tip": "", "image":"https://shared.cdn.queniuqe.com/store_item_assets/steam/apps/1307550/92ed05d6cc21460e00cd5671520b8969a66a3c6f/header_alt_assets_10_schinese.jpg", "url":"https://store.steampowered.com/app/1307550/Craftopia/", - "system":["Windows","Linux"] + "system":["Windows","Linux"], + "memory": 8 }, "Night of the Dead": { "game_nameCN": "死亡之夜", diff --git a/server/src/routes/gameDeployment.ts b/server/src/routes/gameDeployment.ts index 05d2cd0..da5a28f 100644 --- a/server/src/routes/gameDeployment.ts +++ b/server/src/routes/gameDeployment.ts @@ -213,6 +213,81 @@ router.get('/games', authenticateToken, async (req: Request, res: Response) => { } }) +// 检查游戏内存需求 +router.post('/check-memory', authenticateToken, async (req: Request, res: Response) => { + try { + const { gameKey } = req.body + + if (!gameKey) { + return res.status(400).json({ + success: false, + error: '缺少游戏标识', + message: '游戏标识为必填项' + }) + } + + let memoryWarning = null + + try { + // 读取游戏配置文件 + const baseDir = process.cwd() + const possiblePaths = [ + path.join(baseDir, 'data', 'games', 'installgame.json'), + path.join(baseDir, 'server', 'data', 'games', 'installgame.json'), + ] + + let gamesFilePath = '' + for (const possiblePath of possiblePaths) { + try { + fsSync.accessSync(possiblePath, fsSync.constants.F_OK) + gamesFilePath = possiblePath + break + } catch { + // 继续尝试下一个路径 + } + } + + if (gamesFilePath) { + const gamesData = await fs.readFile(gamesFilePath, 'utf-8') + const games = JSON.parse(gamesData) + const gameInfo = games[gameKey] + + if (gameInfo && gameInfo.memory) { + const requiredMemoryGB = gameInfo.memory + const systemMemoryGB = Math.round(os.totalmem() / (1024 * 1024 * 1024)) + + logger.info(`内存检测: 游戏 ${gameKey} 需要 ${requiredMemoryGB}GB,系统总内存 ${systemMemoryGB}GB`) + + if (systemMemoryGB < requiredMemoryGB) { + memoryWarning = { + required: requiredMemoryGB, + available: systemMemoryGB, + message: `警告:${gameInfo.game_nameCN || gameKey} 推荐至少 ${requiredMemoryGB}GB 内存,但系统只有 ${systemMemoryGB}GB。继续安装可能会导致性能问题或无法正常运行。` + } + logger.warn(`内存不足警告: ${memoryWarning.message}`) + } + } + } + } catch (error) { + logger.warn('检查游戏内存需求时出错:', error) + // 内存检查失败不应阻止安装,继续执行 + } + + res.json({ + success: true, + memoryWarning + }) + + } catch (error: any) { + logger.error('检查游戏内存需求失败:', error) + res.status(500).json({ + success: false, + error: '检查内存需求失败', + message: error.message + }) + } +}) + // 安装游戏 router.post('/install', authenticateToken, async (req: Request, res: Response) => { try { @@ -243,7 +318,7 @@ router.post('/install', authenticateToken, async (req: Request, res: Response) = message: '非匿名模式下需要提供Steam用户名和密码' }) } - + // 检查安装路径是否存在 try { await fs.access(installPath)