新增安装游戏环境检测-内存

This commit is contained in:
小朱
2025-07-30 18:37:11 +08:00
parent 6bbf16fd25
commit efb11b8ee7
4 changed files with 255 additions and 21 deletions

View File

@@ -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 = () => {
</div>
</div>
)}
{/* 内存警告对话框 */}
{showMemoryWarningModal && (
<div className={`fixed inset-0 bg-black flex items-center justify-center z-50 p-4 transition-opacity duration-300 ${
memoryWarningModalAnimating ? 'bg-opacity-50' : 'bg-opacity-0'
}`}>
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full transform transition-all duration-300 ${
memoryWarningModalAnimating ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
}`}>
<div className="p-6">
<div className="flex items-center space-x-3 mb-4">
<div className="flex-shrink-0">
<AlertCircle className="w-8 h-8 text-orange-500" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
</p>
</div>
</div>
{memoryWarningInfo && (
<div className="space-y-4">
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
<p className="text-sm text-orange-800 dark:text-orange-200 leading-relaxed">
{memoryWarningInfo.message}
</p>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<div className="text-gray-600 dark:text-gray-400"></div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{memoryWarningInfo.required} GB
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<div className="text-gray-600 dark:text-gray-400"></div>
<div className="text-lg font-semibold text-gray-900 dark:text-white">
{memoryWarningInfo.available} GB
</div>
</div>
</div>
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong></strong>
</p>
<ul className="text-sm text-blue-700 dark:text-blue-300 mt-2 space-y-1 ml-4">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
)}
</div>
<div className="flex justify-end space-x-3 p-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleCloseMemoryWarningModal}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-600 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-500 transition-colors"
>
</button>
<button
onClick={handleContinueInstallation}
className="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg transition-colors flex items-center space-x-2"
>
<AlertCircle className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -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

View File

@@ -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": "死亡之夜",

View File

@@ -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)