mirror of
https://github.com/GSManagerXZ/GameServerManager.git
synced 2026-06-05 04:19:40 +08:00
增加游戏部署配置文件编辑
This commit is contained in:
@@ -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: '面板设置',
|
||||
|
||||
@@ -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
|
||||
@@ -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 开发者工具和高级功能'
|
||||
|
||||
@@ -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 || '删除游戏配置失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: '删除游戏配置失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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令牌
|
||||
*/
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
Reference in New Issue
Block a user