增加游戏部署配置文件编辑

This commit is contained in:
小朱
2025-09-08 22:04:31 +08:00
parent 3e1fc1fef0
commit b127ca0fb9
9 changed files with 1043 additions and 18 deletions

View File

@@ -1,16 +1,17 @@
import React from 'react'
import {
Code,
Shield,
Package,
Settings,
Database,
Terminal,
FileText,
import {
Code,
Shield,
Package,
Settings,
Database,
Terminal,
FileText,
Activity,
Wrench,
Info,
ChevronRight
ChevronRight,
GamepadIcon
} from 'lucide-react'
interface SidebarItem {
@@ -40,6 +41,13 @@ const DeveloperSidebar: React.FC<DeveloperSidebarProps> = ({
icon: Info,
description: '开发者工具概览和状态'
},
{
id: 'game-config',
label: '游戏部署配置文件编辑',
icon: GamepadIcon,
description: '编辑游戏部署配置文件',
disabled: !isAuthenticated
},
{
id: 'panel',
label: '面板设置',

View File

@@ -0,0 +1,546 @@
import React, { useState, useEffect } from 'react'
import {
Plus,
Edit,
Trash2,
Save,
X,
Search,
GamepadIcon,
ExternalLink,
Monitor,
HardDrive,
AlertCircle,
CheckCircle,
Loader2
} from 'lucide-react'
import { developerApi } from '../../services/developerApi'
import type { GameConfig, GameConfigData } from '../../types/developer'
interface GameConfigSectionProps {
isAuthenticated: boolean
}
const GameConfigSection: React.FC<GameConfigSectionProps> = ({ isAuthenticated }) => {
const [configs, setConfigs] = useState<GameConfigData>({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [editingConfig, setEditingConfig] = useState<GameConfig | null>(null)
const [isCreating, setIsCreating] = useState(false)
const [actionLoading, setActionLoading] = useState<string | null>(null)
// 加载游戏配置
const loadConfigs = async () => {
try {
setLoading(true)
setError(null)
const data = await developerApi.getGameConfigs()
setConfigs(data)
} catch (err) {
setError(err instanceof Error ? err.message : '加载游戏配置失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
if (isAuthenticated) {
loadConfigs()
}
}, [isAuthenticated])
// 过滤配置
const filteredConfigs = Object.entries(configs).filter(([key, config]) => {
const searchLower = searchTerm.toLowerCase()
return (
key.toLowerCase().includes(searchLower) ||
config.game_nameCN.toLowerCase().includes(searchLower) ||
config.appid.includes(searchTerm)
)
})
// 开始创建新配置
const handleCreate = () => {
setEditingConfig({
key: '',
game_nameCN: '',
appid: '',
tip: '',
image: '',
url: '',
system: ['Windows'],
system_info: [],
memory: 4
})
setIsCreating(true)
}
// 开始编辑配置
const handleEdit = (key: string, config: Omit<GameConfig, 'key'>) => {
setEditingConfig({ key, ...config })
setIsCreating(false)
}
// 保存配置
const handleSave = async () => {
if (!editingConfig) return
try {
setActionLoading('save')
if (isCreating) {
await developerApi.createGameConfig(editingConfig)
} else {
const { key, ...configData } = editingConfig
await developerApi.updateGameConfig(key, configData)
}
await loadConfigs()
setEditingConfig(null)
setIsCreating(false)
} catch (err) {
setError(err instanceof Error ? err.message : '保存配置失败')
} finally {
setActionLoading(null)
}
}
// 删除配置
const handleDelete = async (key: string) => {
if (!confirm(`确定要删除游戏配置 "${key}" 吗?`)) return
try {
setActionLoading(`delete-${key}`)
await developerApi.deleteGameConfig(key)
await loadConfigs()
} catch (err) {
setError(err instanceof Error ? err.message : '删除配置失败')
} finally {
setActionLoading(null)
}
}
// 取消编辑
const handleCancel = () => {
setEditingConfig(null)
setIsCreating(false)
}
if (!isAuthenticated) {
return (
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6">
<div className="flex items-center space-x-3">
<AlertCircle className="w-6 h-6 text-yellow-600 dark:text-yellow-400" />
<div>
<h3 className="font-medium text-yellow-900 dark:text-yellow-100">
</h3>
<p className="text-sm text-yellow-700 dark:text-yellow-300 mt-1">
访
</p>
</div>
</div>
</div>
)
}
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-500" />
<p className="text-gray-600 dark:text-gray-400">...</p>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* 错误提示 */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 mt-0.5" />
<div>
<h4 className="font-medium text-red-900 dark:text-red-100"></h4>
<p className="text-sm text-red-700 dark:text-red-300 mt-1">{error}</p>
</div>
</div>
</div>
)}
{/* 头部操作栏 */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0">
<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 mt-1">
installgame.json
</p>
</div>
<div className="flex items-center space-x-3">
{/* 搜索框 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="搜索游戏..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* 添加按钮 */}
<button
onClick={handleCreate}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
<Plus className="w-4 h-4" />
<span></span>
</button>
</div>
</div>
</div>
{/* 游戏配置列表 */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
{filteredConfigs.map(([key, config]) => (
<div key={key} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* 游戏图片 */}
<div className="aspect-video bg-gray-100 dark:bg-gray-700 relative">
{config.image ? (
<img
src={config.image}
alt={config.game_nameCN}
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
}}
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<GamepadIcon className="w-12 h-12 text-gray-400" />
</div>
)}
{/* 操作按钮 */}
<div className="absolute top-2 right-2 flex space-x-1">
<button
onClick={() => handleEdit(key, config)}
className="p-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors"
title="编辑"
>
<Edit className="w-3 h-3" />
</button>
<button
onClick={() => handleDelete(key)}
disabled={actionLoading === `delete-${key}`}
className="p-1.5 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors disabled:opacity-50"
title="删除"
>
{actionLoading === `delete-${key}` ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Trash2 className="w-3 h-3" />
)}
</button>
</div>
</div>
{/* 游戏信息 */}
<div className="p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="font-medium text-gray-900 dark:text-white">
{config.game_nameCN}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{key}
</p>
</div>
{config.url && (
<a
href={config.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
title="查看Steam页面"
>
<ExternalLink className="w-4 h-4" />
</a>
)}
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center space-x-2">
<span className="text-gray-500 dark:text-gray-400">App ID:</span>
<span className="text-gray-900 dark:text-white font-mono">{config.appid}</span>
</div>
{config.system && config.system.length > 0 && (
<div className="flex items-center space-x-2">
<Monitor className="w-4 h-4 text-gray-400" />
<div className="flex flex-wrap gap-1">
{config.system.map((sys) => (
<span
key={sys}
className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs"
>
{sys}
</span>
))}
</div>
</div>
)}
{config.memory && (
<div className="flex items-center space-x-2">
<HardDrive className="w-4 h-4 text-gray-400" />
<span className="text-gray-700 dark:text-gray-300">
{config.memory}GB
</span>
</div>
)}
</div>
{config.tip && (
<div className="mt-3 p-2 bg-gray-50 dark:bg-gray-700 rounded text-xs text-gray-600 dark:text-gray-400">
{config.tip.length > 100 ? `${config.tip.substring(0, 100)}...` : config.tip}
</div>
)}
</div>
</div>
))}
</div>
{filteredConfigs.length === 0 && !loading && (
<div className="text-center py-12">
<GamepadIcon className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
{searchTerm ? '未找到匹配的游戏' : '暂无游戏配置'}
</h3>
<p className="text-gray-500 dark:text-gray-400">
{searchTerm ? '尝试使用其他关键词搜索' : '点击"添加游戏"按钮创建第一个游戏配置'}
</p>
</div>
)}
{/* 编辑模态框 */}
{editingConfig && (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" onClick={handleCancel} />
<div className="inline-block w-full max-w-2xl p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-gray-800 shadow-xl rounded-lg">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{isCreating ? '添加游戏配置' : '编辑游戏配置'}
</h3>
<button
onClick={handleCancel}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="space-y-4 max-h-96 overflow-y-auto">
{/* 游戏标识 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
() *
</label>
<input
type="text"
value={editingConfig.key}
onChange={(e) => setEditingConfig({ ...editingConfig, key: e.target.value })}
disabled={!isCreating}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white disabled:bg-gray-100 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
placeholder="例如: Palworld"
/>
{!isCreating && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
</p>
)}
</div>
{/* 游戏中文名 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
*
</label>
<input
type="text"
value={editingConfig.game_nameCN}
onChange={(e) => setEditingConfig({ ...editingConfig, game_nameCN: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="例如: 幻兽帕鲁"
/>
</div>
{/* Steam App ID */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Steam App ID *
</label>
<input
type="text"
value={editingConfig.appid}
onChange={(e) => setEditingConfig({ ...editingConfig, appid: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="例如: 2394010"
/>
</div>
{/* 游戏图片URL */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
URL
</label>
<input
type="url"
value={editingConfig.image}
onChange={(e) => setEditingConfig({ ...editingConfig, image: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="https://..."
/>
</div>
{/* Steam商店URL */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Steam商店URL
</label>
<input
type="url"
value={editingConfig.url}
onChange={(e) => setEditingConfig({ ...editingConfig, url: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="https://store.steampowered.com/app/..."
/>
</div>
{/* 支持系统 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<div className="flex flex-wrap gap-2">
{['Windows', 'Linux', 'macOS'].map((system) => (
<label key={system} className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingConfig.system?.includes(system) || false}
onChange={(e) => {
const currentSystems = editingConfig.system || []
if (e.target.checked) {
setEditingConfig({
...editingConfig,
system: [...currentSystems, system]
})
} else {
setEditingConfig({
...editingConfig,
system: currentSystems.filter(s => s !== system)
})
}
}}
className="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">{system}</span>
</label>
))}
</div>
</div>
{/* 内存要求 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
(GB)
</label>
<input
type="number"
min="1"
max="128"
value={editingConfig.memory || ''}
onChange={(e) => setEditingConfig({
...editingConfig,
memory: e.target.value ? parseInt(e.target.value) : undefined
})}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="4"
/>
</div>
{/* 文档链接 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<input
type="url"
value={editingConfig.docs || ''}
onChange={(e) => setEditingConfig({ ...editingConfig, docs: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="https://docs.gsm.xiaozhuhouses.asia/..."
/>
</div>
{/* 游戏提示 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
</label>
<textarea
value={editingConfig.tip}
onChange={(e) => setEditingConfig({ ...editingConfig, tip: e.target.value })}
rows={3}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="游戏端口、配置文件位置、注意事项等..."
/>
</div>
</div>
{/* 操作按钮 */}
<div className="flex justify-end space-x-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
<button
onClick={handleCancel}
className="px-4 py-2 text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
>
</button>
<button
onClick={handleSave}
disabled={!editingConfig.key || !editingConfig.game_nameCN || !editingConfig.appid || actionLoading === 'save'}
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white rounded-lg transition-colors disabled:cursor-not-allowed"
>
{actionLoading === 'save' ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
<span>...</span>
</>
) : (
<>
<Save className="w-4 h-4" />
<span></span>
</>
)}
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}
export default GameConfigSection

View File

@@ -4,6 +4,7 @@ import { useDeveloperAuth } from '../hooks/useDeveloperAuth'
import DeveloperLayout from '../components/DeveloperLayout'
import OverviewSection from '../components/sections/OverviewSection'
import PanelSection from '../components/sections/PanelSection'
import GameConfigSection from '../components/sections/GameConfigSection'
const DeveloperPage: React.FC = () => {
const [activeSection, setActiveSection] = useState('overview')
@@ -32,6 +33,9 @@ const DeveloperPage: React.FC = () => {
case 'overview':
return <OverviewSection isAuthenticated={auth.isAuthenticated} />
case 'game-config':
return <GameConfigSection isAuthenticated={auth.isAuthenticated} />
case 'panel':
return (
<PanelSection
@@ -51,6 +55,7 @@ const DeveloperPage: React.FC = () => {
const getSectionTitle = () => {
const titles: Record<string, string> = {
overview: '概览',
'game-config': '游戏部署配置文件编辑',
panel: '面板设置'
}
return titles[activeSection] || '开发者工具'
@@ -59,6 +64,7 @@ const DeveloperPage: React.FC = () => {
const getSectionDescription = () => {
const descriptions: Record<string, string> = {
overview: '开发者工具概览和系统状态',
'game-config': '管理 installgame.json 文件中的游戏配置',
panel: '开发者面板配置和管理'
}
return descriptions[activeSection] || 'GSM3 开发者工具和高级功能'

View File

@@ -2,7 +2,11 @@ import type {
DeveloperAuth,
DeveloperAuthResponse,
DeveloperStatusResponse,
ProductionPackageResponse
ProductionPackageResponse,
GameConfig,
GameConfigData,
GameConfigResponse,
GameConfigOperationResponse
} from '../types/developer'
/**
@@ -116,6 +120,78 @@ export class DeveloperApiService {
return result.data
}
/**
* 获取游戏配置列表
*/
async getGameConfigs(): Promise<GameConfigData> {
const response = await fetch('/api/developer/game-configs', {
headers: this.getHeaders(true)
})
const result: GameConfigResponse = await response.json()
if (!result.success || !result.data) {
throw new Error(result.message || '获取游戏配置失败')
}
return result.data
}
/**
* 创建游戏配置
*/
async createGameConfig(config: GameConfig): Promise<GameConfig> {
const { key, ...configData } = config
const response = await fetch('/api/developer/game-configs', {
method: 'POST',
headers: this.getHeaders(true),
body: JSON.stringify({ key, config: configData })
})
const result: GameConfigOperationResponse = await response.json()
if (!result.success || !result.data) {
throw new Error(result.message || '创建游戏配置失败')
}
return result.data
}
/**
* 更新游戏配置
*/
async updateGameConfig(key: string, config: Omit<GameConfig, 'key'>): Promise<GameConfig> {
const response = await fetch(`/api/developer/game-configs/${encodeURIComponent(key)}`, {
method: 'PUT',
headers: this.getHeaders(true),
body: JSON.stringify({ config })
})
const result: GameConfigOperationResponse = await response.json()
if (!result.success || !result.data) {
throw new Error(result.message || '更新游戏配置失败')
}
return result.data
}
/**
* 删除游戏配置
*/
async deleteGameConfig(key: string): Promise<void> {
const response = await fetch(`/api/developer/game-configs/${encodeURIComponent(key)}`, {
method: 'DELETE',
headers: this.getHeaders(true)
})
const result: GameConfigOperationResponse = await response.json()
if (!result.success) {
throw new Error(result.message || '删除游戏配置失败')
}
}
}
// 导出单例实例

View File

@@ -62,3 +62,54 @@ export interface DeveloperToolActionState {
loading: boolean
error?: string
}
/**
* 游戏配置项
*/
export interface GameConfig {
/** 游戏英文名作为key */
key: string
/** 游戏中文名 */
game_nameCN: string
/** Steam应用ID */
appid: string
/** 游戏提示信息 */
tip: string
/** 游戏图片URL */
image: string
/** Steam商店URL */
url: string
/** 支持的系统 */
system?: string[]
/** 系统信息 */
system_info?: string[]
/** 内存要求GB */
memory?: number
/** 文档链接 */
docs?: string
}
/**
* 游戏配置文件数据
*/
export interface GameConfigData {
[key: string]: Omit<GameConfig, 'key'>
}
/**
* 游戏配置API响应
*/
export interface GameConfigResponse {
success: boolean
data?: GameConfigData
message?: string
}
/**
* 游戏配置操作响应
*/
export interface GameConfigOperationResponse {
success: boolean
data?: GameConfig
message?: string
}

View File

@@ -1,9 +1,12 @@
import { Response } from 'express'
import type {
import type {
DeveloperRequest,
DeveloperStatusResponse,
DeveloperAuthResponse,
ProductionPackageResponse
ProductionPackageResponse,
GameConfigResponse,
GameConfigOperationResponse,
GameConfig
} from '../types/developer.js'
import { DeveloperService } from '../services/DeveloperService.js'
import logger from '../../../utils/logger.js'
@@ -157,4 +160,143 @@ export class DeveloperController {
})
}
}
/**
* 获取游戏配置列表
*/
getGameConfigs = async (req: DeveloperRequest, res: Response<GameConfigResponse>) => {
try {
const configs = await this.developerService.getGameConfigs()
res.json({
success: true,
data: configs
})
} catch (error) {
logger.error('获取游戏配置失败:', error)
res.status(500).json({
success: false,
message: '获取游戏配置失败'
})
}
}
/**
* 创建游戏配置
*/
createGameConfig = async (req: DeveloperRequest, res: Response<GameConfigOperationResponse>) => {
try {
const { key, config } = req.body
if (!key || !config) {
return res.status(400).json({
success: false,
message: '缺少必要参数'
})
}
const gameConfig: GameConfig = { key, ...config }
const result = await this.developerService.createGameConfig(gameConfig)
res.json({
success: true,
data: result,
message: '游戏配置创建成功'
})
} catch (error) {
logger.error('创建游戏配置失败:', error)
if (error instanceof Error) {
if (error.message.includes('已存在') || error.message.includes('必填字段')) {
return res.status(400).json({
success: false,
message: error.message
})
}
}
res.status(500).json({
success: false,
message: '创建游戏配置失败'
})
}
}
/**
* 更新游戏配置
*/
updateGameConfig = async (req: DeveloperRequest, res: Response<GameConfigOperationResponse>) => {
try {
const { key } = req.params
const { config } = req.body
if (!key || !config) {
return res.status(400).json({
success: false,
message: '缺少必要参数'
})
}
const result = await this.developerService.updateGameConfig(key, config)
res.json({
success: true,
data: result,
message: '游戏配置更新成功'
})
} catch (error) {
logger.error('更新游戏配置失败:', error)
if (error instanceof Error) {
if (error.message.includes('不存在') || error.message.includes('必填字段')) {
return res.status(400).json({
success: false,
message: error.message
})
}
}
res.status(500).json({
success: false,
message: '更新游戏配置失败'
})
}
}
/**
* 删除游戏配置
*/
deleteGameConfig = async (req: DeveloperRequest, res: Response<GameConfigOperationResponse>) => {
try {
const { key } = req.params
if (!key) {
return res.status(400).json({
success: false,
message: '缺少游戏标识参数'
})
}
await this.developerService.deleteGameConfig(key)
res.json({
success: true,
message: '游戏配置删除成功'
})
} catch (error) {
logger.error('删除游戏配置失败:', error)
if (error instanceof Error && error.message.includes('不存在')) {
return res.status(404).json({
success: false,
message: error.message
})
}
res.status(500).json({
success: false,
message: '删除游戏配置失败'
})
}
}
}

View File

@@ -34,11 +34,32 @@ export function createDeveloperRoutes(configManager: ConfigManager): Router {
)
// 需要开发者认证的路由
router.post('/production-package',
developerAuthMiddleware.authenticate,
router.post('/production-package',
developerAuthMiddleware.authenticate,
developerController.executeProductionPackage
)
// 游戏配置管理路由
router.get('/game-configs',
developerAuthMiddleware.authenticate,
developerController.getGameConfigs
)
router.post('/game-configs',
developerAuthMiddleware.authenticate,
developerController.createGameConfig
)
router.put('/game-configs/:key',
developerAuthMiddleware.authenticate,
developerController.updateGameConfig
)
router.delete('/game-configs/:key',
developerAuthMiddleware.authenticate,
developerController.deleteGameConfig
)
return router
}

View File

@@ -1,12 +1,15 @@
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import fs from 'fs/promises'
import fsSync from 'fs'
import path from 'path'
import type { ConfigManager } from '../../config/ConfigManager.js'
import type {
DeveloperAuth,
DeveloperJWTPayload,
ProductionPackageResult
import type {
DeveloperAuth,
DeveloperJWTPayload,
ProductionPackageResult,
GameConfig,
GameConfigData
} from '../types/developer.js'
import logger from '../../../utils/logger.js'
@@ -165,6 +168,135 @@ export class DeveloperService {
return result
}
/**
* 获取installgame.json文件路径
*/
private getInstallGameJsonPath(): string {
const baseDir = process.cwd()
const possiblePaths = [
path.join(baseDir, 'data', 'games', 'installgame.json'), // 打包后的路径
path.join(baseDir, 'server', 'data', 'games', 'installgame.json'), // 开发环境路径
]
for (const possiblePath of possiblePaths) {
try {
fsSync.accessSync(possiblePath, fsSync.constants.F_OK)
return possiblePath
} catch {
// 继续尝试下一个路径
}
}
// 如果都不存在,返回默认路径(会在后续操作中创建)
return possiblePaths[0]
}
/**
* 获取游戏配置列表
*/
async getGameConfigs(): Promise<GameConfigData> {
try {
const filePath = this.getInstallGameJsonPath()
const content = await fs.readFile(filePath, 'utf-8')
return JSON.parse(content)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
// 文件不存在,返回空对象
return {}
}
throw error
}
}
/**
* 保存游戏配置到文件
*/
private async saveGameConfigs(configs: GameConfigData): Promise<void> {
const filePath = this.getInstallGameJsonPath()
// 确保目录存在
const dir = path.dirname(filePath)
try {
await fs.access(dir)
} catch {
await fs.mkdir(dir, { recursive: true })
}
// 保存文件格式化JSON
await fs.writeFile(filePath, JSON.stringify(configs, null, 2), 'utf-8')
logger.info(`游戏配置已保存到: ${filePath}`)
}
/**
* 创建游戏配置
*/
async createGameConfig(config: GameConfig): Promise<GameConfig> {
const configs = await this.getGameConfigs()
// 检查是否已存在
if (configs[config.key]) {
throw new Error(`游戏配置 "${config.key}" 已存在`)
}
// 验证必填字段
if (!config.key || !config.game_nameCN || !config.appid) {
throw new Error('游戏标识、中文名和App ID为必填字段')
}
// 添加新配置
const { key, ...configData } = config
configs[key] = configData
await this.saveGameConfigs(configs)
logger.info(`创建游戏配置: ${config.key}`)
return config
}
/**
* 更新游戏配置
*/
async updateGameConfig(key: string, configData: Omit<GameConfig, 'key'>): Promise<GameConfig> {
const configs = await this.getGameConfigs()
// 检查是否存在
if (!configs[key]) {
throw new Error(`游戏配置 "${key}" 不存在`)
}
// 验证必填字段
if (!configData.game_nameCN || !configData.appid) {
throw new Error('中文名和App ID为必填字段')
}
// 更新配置
configs[key] = configData
await this.saveGameConfigs(configs)
logger.info(`更新游戏配置: ${key}`)
return { key, ...configData }
}
/**
* 删除游戏配置
*/
async deleteGameConfig(key: string): Promise<void> {
const configs = await this.getGameConfigs()
// 检查是否存在
if (!configs[key]) {
throw new Error(`游戏配置 "${key}" 不存在`)
}
// 删除配置
delete configs[key]
await this.saveGameConfigs(configs)
logger.info(`删除游戏配置: ${key}`)
}
/**
* 生成开发者JWT令牌
*/

View File

@@ -82,3 +82,46 @@ export interface DeveloperStatusResponse extends ApiResponse<DeveloperAuth> {}
* 正式环境封装响应
*/
export interface ProductionPackageResponse extends ApiResponse<ProductionPackageResult> {}
/**
* 游戏配置项
*/
export interface GameConfig {
/** 游戏英文名作为key */
key: string
/** 游戏中文名 */
game_nameCN: string
/** Steam应用ID */
appid: string
/** 游戏提示信息 */
tip: string
/** 游戏图片URL */
image: string
/** Steam商店URL */
url: string
/** 支持的系统 */
system?: string[]
/** 系统信息 */
system_info?: string[]
/** 内存要求GB */
memory?: number
/** 文档链接 */
docs?: string
}
/**
* 游戏配置文件数据
*/
export interface GameConfigData {
[key: string]: Omit<GameConfig, 'key'>
}
/**
* 游戏配置API响应
*/
export interface GameConfigResponse extends ApiResponse<GameConfigData> {}
/**
* 游戏配置操作响应
*/
export interface GameConfigOperationResponse extends ApiResponse<GameConfig> {}