新增壁纸

This commit is contained in:
yxsj245
2025-10-06 16:09:03 +08:00
parent b05cabd1bb
commit db90c3e03d
9 changed files with 973 additions and 9 deletions

2
.gitignore vendored
View File

@@ -23,6 +23,8 @@ terminal-sessions.json
captchas.json
users.json
server/data/environment
server/data/wallpapers/
data/wallpapers/
dist/
out/

View File

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

View 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

View File

@@ -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' : ''}
`}>

View File

@@ -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">
JPGPNGGIFWEBP 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') && (

View 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 }),
}
)
)

View 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
- 联系项目开发者
- 参与社区讨论
---
**享受您的个性化游戏面板体验!** 🎮✨

View File

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

View 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