mirror of
https://github.com/GSManagerXZ/GameServerManager.git
synced 2026-06-03 03:19:38 +08:00
新增壁纸
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,6 +23,8 @@ terminal-sessions.json
|
||||
captchas.json
|
||||
users.json
|
||||
server/data/environment
|
||||
server/data/wallpapers/
|
||||
data/wallpapers/
|
||||
|
||||
dist/
|
||||
out/
|
||||
|
||||
@@ -2,9 +2,11 @@ import React, { useState, useEffect } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
import { useWallpaperStore } from '@/stores/wallpaperStore'
|
||||
import socketClient from '@/utils/socket'
|
||||
import apiClient from '@/utils/api'
|
||||
import LogoutTransition from './LogoutTransition'
|
||||
import WallpaperBackground from './WallpaperBackground'
|
||||
import {
|
||||
Home,
|
||||
Terminal,
|
||||
@@ -56,6 +58,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const location = useLocation()
|
||||
const { user, logout } = useAuthStore()
|
||||
const { theme, toggleTheme } = useThemeStore()
|
||||
const { settings: wallpaperSettings } = useWallpaperStore()
|
||||
|
||||
const navigation = [
|
||||
{ name: '首页', href: '/', icon: Home },
|
||||
@@ -332,7 +335,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-game-gradient">
|
||||
<div className={`min-h-screen relative ${wallpaperSettings.enabled ? '' : 'bg-game-gradient'}`}>
|
||||
{/* 背景壁纸 */}
|
||||
<WallpaperBackground />
|
||||
|
||||
{/* 移动端侧边栏遮罩 */}
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
@@ -487,7 +493,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
</div>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
<div className={`transition-all duration-300 ${sidebarCollapsed ? 'lg:pl-16' : 'lg:pl-64'}`}>
|
||||
<div className={`transition-all duration-300 relative z-10 ${sidebarCollapsed ? 'lg:pl-16' : 'lg:pl-64'}`}>
|
||||
{/* 顶部栏 */}
|
||||
<div className="sticky top-0 z-30 flex h-16 items-center justify-between px-6 glass border-b border-gray-200 dark:border-gray-700/30">
|
||||
<button
|
||||
|
||||
97
client/src/components/WallpaperBackground.tsx
Normal file
97
client/src/components/WallpaperBackground.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useWallpaperStore } from '@/stores/wallpaperStore'
|
||||
|
||||
interface WallpaperBackgroundProps {
|
||||
isLoginPage?: boolean
|
||||
}
|
||||
|
||||
const WallpaperBackground: React.FC<WallpaperBackgroundProps> = ({ isLoginPage = false }) => {
|
||||
const { settings } = useWallpaperStore()
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
|
||||
// 确定使用哪个壁纸
|
||||
const shouldShow = isLoginPage
|
||||
? (settings.syncWithMain ? settings.enabled : settings.loginEnabled)
|
||||
: settings.enabled
|
||||
|
||||
const imageUrl = isLoginPage
|
||||
? (settings.syncWithMain ? settings.imageUrl : settings.loginImageUrl)
|
||||
: settings.imageUrl
|
||||
|
||||
const brightness = isLoginPage
|
||||
? (settings.syncWithMain ? settings.brightness : settings.loginBrightness)
|
||||
: settings.brightness
|
||||
|
||||
// 当壁纸URL变化时重置加载状态
|
||||
useEffect(() => {
|
||||
setImageLoaded(false)
|
||||
|
||||
// 检查图片是否已经缓存完成
|
||||
const checkImageLoaded = () => {
|
||||
if (imgRef.current?.complete) {
|
||||
setImageLoaded(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 立即检查
|
||||
checkImageLoaded()
|
||||
|
||||
// 延迟检查(处理某些边缘情况)
|
||||
const timer = setTimeout(checkImageLoaded, 100)
|
||||
|
||||
// 安全超时:3秒后强制显示图片,避免一直卡加载
|
||||
const safetyTimer = setTimeout(() => {
|
||||
setImageLoaded(true)
|
||||
}, 3000)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
clearTimeout(safetyTimer)
|
||||
}
|
||||
}, [imageUrl])
|
||||
|
||||
// 组件挂载时检查图片是否已缓存
|
||||
useEffect(() => {
|
||||
if (imgRef.current?.complete) {
|
||||
setImageLoaded(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (!shouldShow || !imageUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 背景壁纸 - 使用img标签以支持GIF动画 */}
|
||||
<div className="fixed inset-0 z-0 overflow-hidden">
|
||||
{/* 加载占位符 */}
|
||||
{!imageLoaded && (
|
||||
<div className="absolute inset-0 bg-gray-900/50 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
ref={imgRef}
|
||||
src={imageUrl}
|
||||
alt="壁纸背景"
|
||||
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-500 ${
|
||||
imageLoaded ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
filter: `brightness(${brightness}%)`,
|
||||
}}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
onError={() => setImageLoaded(true)}
|
||||
/>
|
||||
{/* 可选的渐变叠加层,以确保文字可读性 */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/30" />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WallpaperBackground
|
||||
|
||||
@@ -2,10 +2,12 @@ import React, { useState, useEffect } from 'react'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { useNotificationStore } from '@/stores/notificationStore'
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
import { useWallpaperStore } from '@/stores/wallpaperStore'
|
||||
import { Eye, EyeOff, Gamepad2, Sun, Moon, Loader2, RefreshCw, UserPlus, HelpCircle } from 'lucide-react'
|
||||
import apiClient from '@/utils/api'
|
||||
import { CaptchaData } from '@/types'
|
||||
import LoginTransition from '@/components/LoginTransition'
|
||||
import WallpaperBackground from '@/components/WallpaperBackground'
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const [username, setUsername] = useState('')
|
||||
@@ -29,6 +31,7 @@ const LoginPage: React.FC = () => {
|
||||
const { login, loading, error } = useAuthStore()
|
||||
const { addNotification } = useNotificationStore()
|
||||
const { theme, toggleTheme } = useThemeStore()
|
||||
const { settings: wallpaperSettings } = useWallpaperStore()
|
||||
|
||||
// 页面加载动画
|
||||
useEffect(() => {
|
||||
@@ -283,12 +286,17 @@ const LoginPage: React.FC = () => {
|
||||
setShowLoginTransition(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 背景壁纸 */}
|
||||
<WallpaperBackground isLoginPage={true} />
|
||||
|
||||
<div className={`
|
||||
min-h-screen flex items-center justify-center p-4 transition-all duration-1000
|
||||
${theme === 'dark'
|
||||
? 'bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 animate-background-shift'
|
||||
: 'bg-gradient-to-br from-blue-50 via-white to-purple-50'
|
||||
min-h-screen flex items-center justify-center p-4 transition-all duration-1000 relative
|
||||
${!wallpaperSettings.syncWithMain && !wallpaperSettings.loginEnabled && !wallpaperSettings.enabled
|
||||
? theme === 'dark'
|
||||
? 'bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 animate-background-shift'
|
||||
: 'bg-gradient-to-br from-blue-50 via-white to-purple-50'
|
||||
: ''
|
||||
}
|
||||
`}>
|
||||
{/* 主题切换按钮 */}
|
||||
@@ -296,7 +304,7 @@ const LoginPage: React.FC = () => {
|
||||
onClick={toggleTheme}
|
||||
className={`
|
||||
fixed top-4 right-4 p-3 glass rounded-full text-black dark:text-white
|
||||
hover:bg-white/20 transition-all duration-200 z-10
|
||||
hover:bg-white/20 transition-all duration-200 z-20
|
||||
${isAnimating ? 'opacity-0 translate-y-[-20px]' : 'opacity-100 translate-y-0 animate-form-field-slide-in animate-delay-500'}
|
||||
`}
|
||||
>
|
||||
@@ -304,7 +312,7 @@ const LoginPage: React.FC = () => {
|
||||
</button>
|
||||
|
||||
<div className={`
|
||||
w-full max-w-md transition-all duration-600
|
||||
w-full max-w-md transition-all duration-600 relative z-10
|
||||
${isAnimating ? 'opacity-0 translate-y-10 scale-95' : 'opacity-100 translate-y-0 scale-100 animate-login-slide-in'}
|
||||
${loginSuccess ? 'animate-page-transition-out' : ''}
|
||||
`}>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/authStore'
|
||||
import { useNotificationStore } from '@/stores/notificationStore'
|
||||
import { useOnboardingStore } from '@/stores/onboardingStore'
|
||||
import { useSystemStore } from '@/stores/systemStore'
|
||||
import { useWallpaperStore } from '@/stores/wallpaperStore'
|
||||
import AutoRedirectControl from '@/components/AutoRedirectControl'
|
||||
import apiClient from '@/utils/api'
|
||||
import {
|
||||
@@ -30,7 +31,11 @@ import {
|
||||
Code,
|
||||
AlertTriangle,
|
||||
Lock,
|
||||
Clock
|
||||
Clock,
|
||||
Image,
|
||||
Upload,
|
||||
Trash2,
|
||||
Sun as SunIcon
|
||||
} from 'lucide-react'
|
||||
import SecurityWarningModal from '@/components/SecurityWarningModal'
|
||||
|
||||
@@ -41,6 +46,7 @@ const SettingsPage: React.FC = () => {
|
||||
const { addNotification } = useNotificationStore()
|
||||
const { resetOnboarding, setShowOnboarding } = useOnboardingStore()
|
||||
const { systemInfo, fetchSystemInfo } = useSystemStore()
|
||||
const { settings: wallpaperSettings, setSettings: setWallpaperSettings, updateMainWallpaper, updateLoginWallpaper } = useWallpaperStore()
|
||||
const [showDeveloperWarning, setShowDeveloperWarning] = useState(false)
|
||||
|
||||
// 城市选项数据
|
||||
@@ -150,6 +156,12 @@ const SettingsPage: React.FC = () => {
|
||||
// Steam游戏部署清单更新状态
|
||||
const [gameListUpdateLoading, setGameListUpdateLoading] = useState(false)
|
||||
|
||||
// 壁纸设置状态
|
||||
const [wallpaperUploading, setWallpaperUploading] = useState(false)
|
||||
const [loginWallpaperUploading, setLoginWallpaperUploading] = useState(false)
|
||||
const mainWallpaperInputRef = React.useRef<HTMLInputElement>(null)
|
||||
const loginWallpaperInputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
// 安全配置状态
|
||||
const [securityConfig, setSecurityConfig] = useState({
|
||||
tokenResetRule: 'startup' as 'startup' | 'expire',
|
||||
@@ -984,6 +996,162 @@ const SettingsPage: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 壁纸处理函数
|
||||
const handleMainWallpaperUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// 验证文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: '请选择图片文件'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小 (10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: '图片大小不能超过10MB'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setWallpaperUploading(true)
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('wallpaper', file)
|
||||
formData.append('type', 'main')
|
||||
|
||||
const response = await fetch('/api/wallpaper/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('gsm3_token')}`
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
updateMainWallpaper(result.data.imageUrl)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '壁纸上传成功',
|
||||
message: '主面板壁纸已更新'
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: result.message || '壁纸上传失败'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: '网络错误,请稍后重试'
|
||||
})
|
||||
} finally {
|
||||
setWallpaperUploading(false)
|
||||
// 清空input以允许重新上传同一文件
|
||||
if (mainWallpaperInputRef.current) {
|
||||
mainWallpaperInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoginWallpaperUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: '请选择图片文件'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: '图片大小不能超过10MB'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setLoginWallpaperUploading(true)
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('wallpaper', file)
|
||||
formData.append('type', 'login')
|
||||
|
||||
const response = await fetch('/api/wallpaper/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('gsm3_token')}`
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
updateLoginWallpaper(result.data.imageUrl)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '壁纸上传成功',
|
||||
message: '登录页壁纸已更新'
|
||||
})
|
||||
} else {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: result.message || '壁纸上传失败'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: '上传失败',
|
||||
message: '网络错误,请稍后重试'
|
||||
})
|
||||
} finally {
|
||||
setLoginWallpaperUploading(false)
|
||||
if (loginWallpaperInputRef.current) {
|
||||
loginWallpaperInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveMainWallpaper = () => {
|
||||
updateMainWallpaper(null)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '壁纸已移除',
|
||||
message: '主面板壁纸已移除'
|
||||
})
|
||||
}
|
||||
|
||||
const handleRemoveLoginWallpaper = () => {
|
||||
updateLoginWallpaper(null)
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: '壁纸已移除',
|
||||
message: '登录页壁纸已移除'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
@@ -1170,6 +1338,238 @@ const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 壁纸设置 */}
|
||||
<div className="card-game p-6">
|
||||
<div className="flex items-center space-x-3 mb-6">
|
||||
<Image className="w-5 h-5 text-pink-500" />
|
||||
<h2 className="text-lg font-semibold text-black dark:text-white">背景壁纸</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 主面板壁纸 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-gray-800 dark:text-gray-200">主面板壁纸</h3>
|
||||
|
||||
{/* 壁纸预览 */}
|
||||
{wallpaperSettings.imageUrl && (
|
||||
<div className="relative w-full h-32 rounded-lg overflow-hidden border-2 border-white/20">
|
||||
<img
|
||||
src={wallpaperSettings.imageUrl}
|
||||
alt="主面板壁纸预览"
|
||||
className="w-full h-full object-cover"
|
||||
style={{ filter: `brightness(${wallpaperSettings.brightness}%)` }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleRemoveMainWallpaper}
|
||||
className="absolute top-2 right-2 p-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
|
||||
title="移除壁纸"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传按钮 */}
|
||||
<div>
|
||||
<input
|
||||
ref={mainWallpaperInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleMainWallpaperUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
onClick={() => mainWallpaperInputRef.current?.click()}
|
||||
disabled={wallpaperUploading}
|
||||
className="w-full btn-game py-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
|
||||
>
|
||||
{wallpaperUploading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4" />
|
||||
)}
|
||||
<span>{wallpaperUploading ? '上传中...' : wallpaperSettings.imageUrl ? '更换壁纸' : '上传壁纸'}</span>
|
||||
</button>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-2">
|
||||
支持 JPG、PNG、GIF(含动画)、WEBP 格式,最大 10MB
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 亮度设置 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-800 dark:text-gray-200 mb-2 flex items-center space-x-2">
|
||||
<SunIcon className="w-4 h-4 text-yellow-500" />
|
||||
<span>亮度调节</span>
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="100"
|
||||
value={wallpaperSettings.brightness}
|
||||
onChange={(e) => setWallpaperSettings({ brightness: parseInt(e.target.value) })}
|
||||
className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||||
disabled={!wallpaperSettings.imageUrl}
|
||||
/>
|
||||
<span className="text-sm text-gray-800 dark:text-gray-200 w-12 text-right">
|
||||
{wallpaperSettings.brightness}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
调整壁纸亮度以获得最佳视觉效果
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 启用/禁用开关 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-800 dark:text-gray-200">启用壁纸</label>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">在主面板显示背景壁纸</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setWallpaperSettings({ enabled: !wallpaperSettings.enabled })}
|
||||
disabled={!wallpaperSettings.imageUrl}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
|
||||
${wallpaperSettings.enabled && wallpaperSettings.imageUrl ? 'bg-pink-600' : 'bg-gray-300'}
|
||||
${!wallpaperSettings.imageUrl ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
||||
${wallpaperSettings.enabled && wallpaperSettings.imageUrl ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<div className="border-t border-gray-700"></div>
|
||||
|
||||
{/* 登录页壁纸 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-gray-800 dark:text-gray-200">登录页壁纸</h3>
|
||||
|
||||
{/* 同步主面板壁纸开关 */}
|
||||
<div className="flex items-center justify-between bg-blue-500/10 p-3 rounded-lg border border-blue-500/20">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-800 dark:text-gray-200">同步主面板壁纸</label>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">登录页使用与主面板相同的壁纸</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setWallpaperSettings({ syncWithMain: !wallpaperSettings.syncWithMain })}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
|
||||
${wallpaperSettings.syncWithMain ? 'bg-blue-600' : 'bg-gray-300'}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
||||
${wallpaperSettings.syncWithMain ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 独立登录页壁纸设置 */}
|
||||
{!wallpaperSettings.syncWithMain && (
|
||||
<>
|
||||
{/* 壁纸预览 */}
|
||||
{wallpaperSettings.loginImageUrl && (
|
||||
<div className="relative w-full h-32 rounded-lg overflow-hidden border-2 border-white/20">
|
||||
<img
|
||||
src={wallpaperSettings.loginImageUrl}
|
||||
alt="登录页壁纸预览"
|
||||
className="w-full h-full object-cover"
|
||||
style={{ filter: `brightness(${wallpaperSettings.loginBrightness}%)` }}
|
||||
/>
|
||||
<button
|
||||
onClick={handleRemoveLoginWallpaper}
|
||||
className="absolute top-2 right-2 p-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
|
||||
title="移除壁纸"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 上传按钮 */}
|
||||
<div>
|
||||
<input
|
||||
ref={loginWallpaperInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleLoginWallpaperUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
onClick={() => loginWallpaperInputRef.current?.click()}
|
||||
disabled={loginWallpaperUploading}
|
||||
className="w-full btn-game py-2 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
|
||||
>
|
||||
{loginWallpaperUploading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4" />
|
||||
)}
|
||||
<span>{loginWallpaperUploading ? '上传中...' : wallpaperSettings.loginImageUrl ? '更换壁纸' : '上传壁纸'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 亮度设置 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-800 dark:text-gray-200 mb-2 flex items-center space-x-2">
|
||||
<SunIcon className="w-4 h-4 text-yellow-500" />
|
||||
<span>亮度调节</span>
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="range"
|
||||
min="10"
|
||||
max="100"
|
||||
value={wallpaperSettings.loginBrightness}
|
||||
onChange={(e) => setWallpaperSettings({ loginBrightness: parseInt(e.target.value) })}
|
||||
className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer"
|
||||
disabled={!wallpaperSettings.loginImageUrl}
|
||||
/>
|
||||
<span className="text-sm text-gray-800 dark:text-gray-200 w-12 text-right">
|
||||
{wallpaperSettings.loginBrightness}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 启用/禁用开关 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-800 dark:text-gray-200">启用登录页壁纸</label>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">在登录页显示背景壁纸</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setWallpaperSettings({ loginEnabled: !wallpaperSettings.loginEnabled })}
|
||||
disabled={!wallpaperSettings.loginImageUrl}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
|
||||
${wallpaperSettings.loginEnabled && wallpaperSettings.loginImageUrl ? 'bg-pink-600' : 'bg-gray-300'}
|
||||
${!wallpaperSettings.loginImageUrl ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
||||
${wallpaperSettings.loginEnabled && wallpaperSettings.loginImageUrl ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SteamCMD设置 - 检测到ARM架构时隐藏 */}
|
||||
{!(systemInfo?.arch === 'arm64' || systemInfo?.arch === 'aarch64') && (
|
||||
|
||||
111
client/src/stores/wallpaperStore.ts
Normal file
111
client/src/stores/wallpaperStore.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
export interface WallpaperSettings {
|
||||
// 主面板壁纸
|
||||
enabled: boolean
|
||||
imageUrl: string | null
|
||||
brightness: number // 0-100
|
||||
|
||||
// 登录页壁纸
|
||||
loginEnabled: boolean
|
||||
loginImageUrl: string | null
|
||||
loginBrightness: number
|
||||
syncWithMain: boolean // 是否同步主面板壁纸
|
||||
}
|
||||
|
||||
interface WallpaperStore {
|
||||
settings: WallpaperSettings
|
||||
setSettings: (settings: Partial<WallpaperSettings>) => void
|
||||
resetSettings: () => void
|
||||
updateMainWallpaper: (imageUrl: string | null) => void
|
||||
updateLoginWallpaper: (imageUrl: string | null) => void
|
||||
setBrightness: (brightness: number) => void
|
||||
setLoginBrightness: (brightness: number) => void
|
||||
toggleWallpaper: (enabled: boolean) => void
|
||||
toggleLoginWallpaper: (enabled: boolean) => void
|
||||
toggleSyncWithMain: (sync: boolean) => void
|
||||
}
|
||||
|
||||
const defaultSettings: WallpaperSettings = {
|
||||
enabled: false,
|
||||
imageUrl: null,
|
||||
brightness: 50,
|
||||
loginEnabled: false,
|
||||
loginImageUrl: null,
|
||||
loginBrightness: 50,
|
||||
syncWithMain: true,
|
||||
}
|
||||
|
||||
export const useWallpaperStore = create<WallpaperStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
settings: defaultSettings,
|
||||
|
||||
setSettings: (newSettings: Partial<WallpaperSettings>) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, ...newSettings }
|
||||
}))
|
||||
},
|
||||
|
||||
resetSettings: () => {
|
||||
set({ settings: defaultSettings })
|
||||
},
|
||||
|
||||
updateMainWallpaper: (imageUrl: string | null) => {
|
||||
set((state) => ({
|
||||
settings: {
|
||||
...state.settings,
|
||||
imageUrl,
|
||||
enabled: imageUrl !== null,
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
updateLoginWallpaper: (imageUrl: string | null) => {
|
||||
set((state) => ({
|
||||
settings: {
|
||||
...state.settings,
|
||||
loginImageUrl: imageUrl,
|
||||
loginEnabled: imageUrl !== null,
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
setBrightness: (brightness: number) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, brightness }
|
||||
}))
|
||||
},
|
||||
|
||||
setLoginBrightness: (brightness: number) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, loginBrightness: brightness }
|
||||
}))
|
||||
},
|
||||
|
||||
toggleWallpaper: (enabled: boolean) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, enabled }
|
||||
}))
|
||||
},
|
||||
|
||||
toggleLoginWallpaper: (enabled: boolean) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, loginEnabled: enabled }
|
||||
}))
|
||||
},
|
||||
|
||||
toggleSyncWithMain: (sync: boolean) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, syncWithMain: sync }
|
||||
}))
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'gsm3-wallpaper',
|
||||
partialize: (state) => ({ settings: state.settings }),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
161
docs/壁纸功能使用说明.md
Normal file
161
docs/壁纸功能使用说明.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 🎨 背景壁纸功能使用说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
GSM3 游戏面板现已支持自定义背景壁纸功能,包括静态图片和**动态GIF**!
|
||||
|
||||
## ✨ 主要特性
|
||||
|
||||
### 1. 支持的格式
|
||||
- **JPG / JPEG** - 静态图片
|
||||
- **PNG** - 静态图片(支持透明)
|
||||
- **GIF** - **动态图片(支持动画)** 🎬
|
||||
- **WEBP** - 现代图片格式
|
||||
|
||||
### 2. 文件限制
|
||||
- 最大文件大小:**10MB**
|
||||
- 推荐分辨率:1920x1080 或更高
|
||||
|
||||
### 3. 两种壁纸模式
|
||||
|
||||
#### 主面板壁纸
|
||||
- 应用于登录后的所有页面
|
||||
- 可独立设置和调整
|
||||
|
||||
#### 登录页壁纸
|
||||
- **同步模式**:与主面板使用相同壁纸
|
||||
- **独立模式**:可单独上传不同的壁纸
|
||||
|
||||
## 🎮 使用方法
|
||||
|
||||
### 上传壁纸
|
||||
|
||||
1. 进入 **设置页面**
|
||||
2. 找到 **背景壁纸** 设置区域
|
||||
3. 点击 **上传壁纸** 按钮
|
||||
4. 选择图片文件(支持 JPG、PNG、GIF、WEBP)
|
||||
5. 等待上传完成
|
||||
|
||||
### 调整亮度
|
||||
|
||||
- 使用亮度滑块调节壁纸明暗度(10% - 100%)
|
||||
- 较低的亮度可以提高文字可读性
|
||||
- 推荐设置:40% - 60%
|
||||
|
||||
### 启用/禁用壁纸
|
||||
|
||||
- 使用切换开关快速启用或禁用壁纸
|
||||
- 禁用后自动恢复默认渐变背景
|
||||
|
||||
### 登录页壁纸设置
|
||||
|
||||
#### 同步主面板壁纸(默认)
|
||||
- 开启后,登录页自动使用主面板壁纸
|
||||
- 亮度设置也会同步
|
||||
|
||||
#### 独立设置
|
||||
- 关闭同步后,可以为登录页上传不同的壁纸
|
||||
- 独立调整登录页壁纸的亮度
|
||||
- 适合想要区分登录前后视觉效果的用户
|
||||
|
||||
## 🎬 GIF 动画壁纸特性
|
||||
|
||||
### 性能优化
|
||||
- 使用 `<img>` 标签原生支持 GIF 动画
|
||||
- 自动播放,无需额外配置
|
||||
- 包含加载动画,提升用户体验
|
||||
|
||||
### 推荐使用场景
|
||||
- 动态背景效果
|
||||
- 游戏主题动画
|
||||
- 氛围营造
|
||||
|
||||
### 注意事项
|
||||
- GIF 文件较大时加载可能需要几秒钟
|
||||
- 复杂的 GIF 动画可能影响低配设备性能
|
||||
- 建议使用优化过的 GIF 文件
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 图片选择
|
||||
1. **高质量图片**:分辨率至少 1920x1080
|
||||
2. **合适的亮度**:避免过暗或过亮的图片
|
||||
3. **主题一致**:选择与游戏面板风格匹配的壁纸
|
||||
4. **文件大小**:尽量压缩到 5MB 以下以获得更快的加载速度
|
||||
|
||||
### GIF 优化建议
|
||||
1. 使用在线工具压缩 GIF(如 ezgif.com)
|
||||
2. 降低帧率(15-30 FPS 即可)
|
||||
3. 减少颜色数量
|
||||
4. 控制尺寸在合理范围
|
||||
|
||||
### 亮度设置建议
|
||||
- **浅色主题**:30% - 50%
|
||||
- **深色主题**:50% - 70%
|
||||
- 根据壁纸内容调整
|
||||
|
||||
## 🔧 技术实现
|
||||
|
||||
### 存储位置
|
||||
- 上传的壁纸存储在:`server/data/wallpapers/`
|
||||
- 自动创建目录结构
|
||||
- 文件命名:`wallpaper-{type}-{timestamp}.{ext}`
|
||||
|
||||
### 配置持久化
|
||||
- 壁纸设置保存在浏览器本地存储
|
||||
- 使用 Zustand 状态管理
|
||||
- 支持多设备独立配置
|
||||
|
||||
### 安全性
|
||||
- 文件类型验证
|
||||
- 文件大小限制
|
||||
- 需要身份验证才能上传
|
||||
|
||||
## 🎯 未来规划
|
||||
|
||||
- [ ] 支持视频背景(MP4)
|
||||
- [ ] 壁纸库功能
|
||||
- [ ] 在线壁纸下载
|
||||
- [ ] 壁纸预设主题
|
||||
- [ ] 模糊效果调节
|
||||
- [ ] 渐变叠加层自定义
|
||||
|
||||
## 📝 常见问题
|
||||
|
||||
### Q: 壁纸不显示?
|
||||
**A:** 请检查:
|
||||
1. 壁纸是否已启用(切换开关)
|
||||
2. 浏览器是否支持图片格式
|
||||
3. 清除浏览器缓存后重试
|
||||
|
||||
### Q: GIF 动画不播放?
|
||||
**A:** GIF 动画应该自动播放,如果不播放:
|
||||
1. 检查文件是否是真正的 GIF 格式
|
||||
2. 尝试重新上传
|
||||
3. 确认浏览器支持 GIF
|
||||
|
||||
### Q: 壁纸加载慢?
|
||||
**A:** 可能原因:
|
||||
1. 文件太大(建议 < 5MB)
|
||||
2. 网络较慢
|
||||
3. 使用在线压缩工具优化图片
|
||||
|
||||
### Q: 能否同时设置多个壁纸?
|
||||
**A:** 目前支持:
|
||||
- 1 个主面板壁纸
|
||||
- 1 个独立登录页壁纸(可选)
|
||||
|
||||
### Q: 壁纸会同步到其他设备吗?
|
||||
**A:** 不会。壁纸设置存储在本地浏览器,每个设备需要单独设置。
|
||||
|
||||
## 🤝 反馈与建议
|
||||
|
||||
如有问题或建议,欢迎:
|
||||
- 提交 GitHub Issue
|
||||
- 联系项目开发者
|
||||
- 参与社区讨论
|
||||
|
||||
---
|
||||
|
||||
**享受您的个性化游戏面板体验!** 🎮✨
|
||||
|
||||
@@ -44,6 +44,7 @@ import gameConfigRouter from './routes/gameconfig.js'
|
||||
import rconRouter from './routes/rcon.js'
|
||||
import environmentRouter, { setEnvironmentSocketIO, setEnvironmentConfigManager } from './routes/environment.js'
|
||||
import { setupDeveloperRoutes } from './routes/developer.js'
|
||||
import wallpaperRouter from './routes/wallpaper.js'
|
||||
|
||||
// 获取当前文件目录
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
@@ -663,6 +664,9 @@ async function startServer() {
|
||||
// 设置开发者路由
|
||||
app.use('/api/developer', setupDeveloperRoutes(configManager))
|
||||
|
||||
// 壁纸路由
|
||||
app.use('/api/wallpaper', wallpaperRouter)
|
||||
|
||||
// 设置安全配置路由
|
||||
const { setSecurityConfigManager } = await import('./routes/security.js')
|
||||
setSecurityConfigManager(configManager)
|
||||
|
||||
175
server/src/routes/wallpaper.ts
Normal file
175
server/src/routes/wallpaper.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { Router } from 'express'
|
||||
import multer from 'multer'
|
||||
import path from 'path'
|
||||
import { promises as fs } from 'fs'
|
||||
import { authenticateToken } from '../middleware/auth.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 配置multer存储
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
const baseDir = process.cwd()
|
||||
const possiblePaths = [
|
||||
path.join(baseDir, 'data', 'wallpapers'),
|
||||
path.join(baseDir, 'server', 'data', 'wallpapers'),
|
||||
]
|
||||
|
||||
let uploadPath = possiblePaths[0]
|
||||
|
||||
// 尝试找到正确的路径
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
await fs.access(path.dirname(p))
|
||||
uploadPath = p
|
||||
break
|
||||
} catch (error) {
|
||||
// 继续尝试下一个路径
|
||||
}
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
try {
|
||||
await fs.mkdir(uploadPath, { recursive: true })
|
||||
} catch (error) {
|
||||
console.error('创建壁纸目录失败:', error)
|
||||
}
|
||||
|
||||
cb(null, uploadPath)
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const type = req.body.type || 'main' // main 或 login
|
||||
const ext = path.extname(file.originalname)
|
||||
const filename = `wallpaper-${type}-${Date.now()}${ext}`
|
||||
cb(null, filename)
|
||||
}
|
||||
})
|
||||
|
||||
// 文件过滤器
|
||||
const fileFilter = (req: any, file: any, cb: any) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']
|
||||
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true)
|
||||
} else {
|
||||
cb(new Error('只支持 JPG, PNG, GIF 和 WEBP 格式的图片'), false)
|
||||
}
|
||||
}
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB
|
||||
}
|
||||
})
|
||||
|
||||
// 上传壁纸
|
||||
router.post('/upload', authenticateToken, upload.single('wallpaper'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要上传的图片'
|
||||
})
|
||||
}
|
||||
|
||||
const type = req.body.type || 'main'
|
||||
const imageUrl = `/api/wallpaper/image/${req.file.filename}`
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '壁纸上传成功',
|
||||
data: {
|
||||
imageUrl,
|
||||
type,
|
||||
filename: req.file.filename
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('上传壁纸失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '上传壁纸失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 获取壁纸图片
|
||||
router.get('/image/:filename', async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params
|
||||
const baseDir = process.cwd()
|
||||
const possiblePaths = [
|
||||
path.join(baseDir, 'data', 'wallpapers', filename),
|
||||
path.join(baseDir, 'server', 'data', 'wallpapers', filename),
|
||||
]
|
||||
|
||||
let imagePath = ''
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
await fs.access(p)
|
||||
imagePath = p
|
||||
break
|
||||
} catch (error) {
|
||||
// 继续尝试下一个路径
|
||||
}
|
||||
}
|
||||
|
||||
if (!imagePath) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '壁纸文件不存在'
|
||||
})
|
||||
}
|
||||
|
||||
res.sendFile(imagePath)
|
||||
} catch (error) {
|
||||
console.error('获取壁纸失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取壁纸失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 删除壁纸
|
||||
router.delete('/delete/:filename', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params
|
||||
const baseDir = process.cwd()
|
||||
const possiblePaths = [
|
||||
path.join(baseDir, 'data', 'wallpapers', filename),
|
||||
path.join(baseDir, 'server', 'data', 'wallpapers', filename),
|
||||
]
|
||||
|
||||
let imagePath = ''
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
await fs.access(p)
|
||||
imagePath = p
|
||||
break
|
||||
} catch (error) {
|
||||
// 继续尝试下一个路径
|
||||
}
|
||||
}
|
||||
|
||||
if (imagePath) {
|
||||
await fs.unlink(imagePath)
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '壁纸删除成功'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除壁纸失败:', error)
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除壁纸失败'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user