796 lines
37 KiB
TypeScript
796 lines
37 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { useSession } from 'next-auth/react'
|
||
import { useRouter } from 'next/navigation'
|
||
import Header from '@/components/Header'
|
||
import { useToast } from '@/hooks/useToast'
|
||
import ToastBubble from '@/components/ToastBubble'
|
||
import { parseFetchError, handleError } from '@/lib/error-handler'
|
||
|
||
export default function SystemSettings() {
|
||
const { data: session, status } = useSession()
|
||
const router = useRouter()
|
||
const [loading, setLoading] = useState(true)
|
||
const [saving, setSaving] = useState(false)
|
||
const [uploading, setUploading] = useState(false)
|
||
|
||
// 设置状态
|
||
const [headerBgColor, setHeaderBgColor] = useState('#1f2937')
|
||
const [siteName, setSiteName] = useState('由壹点3D素材')
|
||
const [headerLogoUrl, setHeaderLogoUrl] = useState('')
|
||
const [headerLogoPreview, setHeaderLogoPreview] = useState('')
|
||
const [loginLogoUrl, setLoginLogoUrl] = useState('')
|
||
const [loginLogoPreview, setLoginLogoPreview] = useState('')
|
||
const [uploadingHeader, setUploadingHeader] = useState(false)
|
||
const [uploadingLogin, setUploadingLogin] = useState(false)
|
||
const { toast, showToast } = useToast()
|
||
|
||
// S3配置状态
|
||
const [s3Endpoint, setS3Endpoint] = useState('')
|
||
const [s3AccessKey, setS3AccessKey] = useState('')
|
||
const [s3SecretKey, setS3SecretKey] = useState('')
|
||
const [s3Bucket, setS3Bucket] = useState('')
|
||
const [s3Region, setS3Region] = useState('us-east-1')
|
||
const [s3PublicUrl, setS3PublicUrl] = useState('')
|
||
|
||
// 维护通知状态
|
||
const [maintenanceEnabled, setMaintenanceEnabled] = useState(false)
|
||
const [maintenanceMessage, setMaintenanceMessage] = useState('')
|
||
const [maintenanceStartTime, setMaintenanceStartTime] = useState('')
|
||
const [maintenanceEndTime, setMaintenanceEndTime] = useState('')
|
||
|
||
// 权限检查
|
||
useEffect(() => {
|
||
if (status === 'unauthenticated') {
|
||
router.push('/login')
|
||
} else if (status === 'authenticated' && (session?.user as any)?.role !== 'ADMIN') {
|
||
router.push('/')
|
||
}
|
||
}, [status, session, router])
|
||
|
||
// 加载设置
|
||
useEffect(() => {
|
||
if (status === 'authenticated' && (session?.user as any)?.role === 'ADMIN') {
|
||
fetchSettings()
|
||
}
|
||
}, [status, session])
|
||
|
||
const fetchSettings = async () => {
|
||
try {
|
||
const response = await fetch('/api/system-settings')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setHeaderBgColor(data.headerBgColor || '#1f2937')
|
||
setSiteName(data.siteName || '由壹点3D素材')
|
||
setHeaderLogoUrl(data.headerLogoUrl || '')
|
||
setHeaderLogoPreview(data.headerLogoUrl || '')
|
||
setLoginLogoUrl(data.loginLogoUrl || '')
|
||
setLoginLogoPreview(data.loginLogoUrl || '')
|
||
setS3Endpoint(data.s3Endpoint || '')
|
||
setS3AccessKey(data.s3AccessKey || '')
|
||
setS3SecretKey(data.s3SecretKey || '')
|
||
setS3Bucket(data.s3Bucket || '')
|
||
setS3Region(data.s3Region || 'us-east-1')
|
||
setS3PublicUrl(data.s3PublicUrl || '')
|
||
setMaintenanceEnabled(data.maintenanceEnabled || false)
|
||
setMaintenanceMessage(data.maintenanceMessage || '')
|
||
setMaintenanceStartTime(data.maintenanceStartTime ? new Date(data.maintenanceStartTime).toISOString().slice(0, 16) : '')
|
||
setMaintenanceEndTime(data.maintenanceEndTime ? new Date(data.maintenanceEndTime).toISOString().slice(0, 16) : '')
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching settings:', error)
|
||
const { message } = handleError(error)
|
||
showToast(message, { variant: 'error' })
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
|
||
const handleSave = async () => {
|
||
setSaving(true)
|
||
try {
|
||
const response = await fetch('/api/system-settings', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
headerBgColor,
|
||
siteName,
|
||
headerLogoUrl,
|
||
loginLogoUrl,
|
||
s3Endpoint,
|
||
s3AccessKey,
|
||
s3SecretKey,
|
||
s3Bucket,
|
||
s3Region,
|
||
s3PublicUrl,
|
||
maintenanceEnabled,
|
||
maintenanceMessage: maintenanceMessage || null,
|
||
maintenanceStartTime: maintenanceStartTime || null,
|
||
maintenanceEndTime: maintenanceEndTime || null,
|
||
}),
|
||
})
|
||
|
||
if (response.ok) {
|
||
showToast('保存成功!页面将自动刷新以应用新设置', { variant: 'success', duration: 3000 })
|
||
// 刷新页面以应用新设置
|
||
setTimeout(() => {
|
||
window.location.reload()
|
||
}, 1000)
|
||
} else {
|
||
const error = await response.json()
|
||
const parsedError = await parseFetchError(response)
|
||
showToast(parsedError.message, { variant: 'error' })
|
||
}
|
||
} catch (error) {
|
||
console.error('Save settings error:', error)
|
||
const { message } = handleError(error)
|
||
showToast(message, { variant: 'error' })
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
const handleHeaderLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (!file) return
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith('image/')) {
|
||
showToast('请选择图片文件', { variant: 'error' })
|
||
return
|
||
}
|
||
|
||
// 验证文件大小(最大5MB)
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
showToast('LOGO文件大小不能超过5MB', { variant: 'error' })
|
||
return
|
||
}
|
||
|
||
setUploadingHeader(true)
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('logo', file)
|
||
formData.append('type', 'header')
|
||
|
||
const response = await fetch('/api/system-settings/upload-logo', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setHeaderLogoUrl(data.logoUrl)
|
||
setHeaderLogoPreview(data.logoUrl)
|
||
showToast('导航栏LOGO上传成功!', { variant: 'success' })
|
||
} else {
|
||
const error = await response.json()
|
||
const parsedError = await parseFetchError(response)
|
||
showToast(parsedError.message, { variant: 'error' })
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload logo error:', error)
|
||
const { message } = handleError(error)
|
||
showToast(message, { variant: 'error' })
|
||
} finally {
|
||
setUploadingHeader(false)
|
||
}
|
||
}
|
||
|
||
const handleLoginLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (!file) return
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith('image/')) {
|
||
showToast('请选择图片文件', { variant: 'error' })
|
||
return
|
||
}
|
||
|
||
// 验证文件大小(最大5MB)
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
showToast('LOGO文件大小不能超过5MB', { variant: 'error' })
|
||
return
|
||
}
|
||
|
||
setUploadingLogin(true)
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('logo', file)
|
||
formData.append('type', 'login')
|
||
|
||
const response = await fetch('/api/system-settings/upload-logo', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setLoginLogoUrl(data.logoUrl)
|
||
setLoginLogoPreview(data.logoUrl)
|
||
showToast('登录页LOGO上传成功!', { variant: 'success' })
|
||
} else {
|
||
const error = await response.json()
|
||
const parsedError = await parseFetchError(response)
|
||
showToast(parsedError.message, { variant: 'error' })
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload logo error:', error)
|
||
const { message } = handleError(error)
|
||
showToast(message, { variant: 'error' })
|
||
} finally {
|
||
setUploadingLogin(false)
|
||
}
|
||
}
|
||
|
||
const handleColorReset = () => {
|
||
setHeaderBgColor('#1f2937')
|
||
}
|
||
|
||
const handleHeaderLogoReset = () => {
|
||
setHeaderLogoPreview('https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png')
|
||
setHeaderLogoUrl('https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png')
|
||
}
|
||
|
||
const handleLoginLogoReset = () => {
|
||
setLoginLogoPreview('https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png')
|
||
setLoginLogoUrl('https://s3.1080p.hk/shuzhou/2025/05/18/8ebadf0a7cc34eaea00e8619b8efb827.png')
|
||
}
|
||
|
||
if (status === 'loading' || loading) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center dark:bg-gray-900">
|
||
<div className="text-xl text-gray-600 dark:text-gray-400">加载中...</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!session || (session.user as any)?.role !== 'ADMIN') {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||
<Header session={session} />
|
||
|
||
<main className="container mx-auto px-4 py-8 max-w-4xl">
|
||
<div className="mb-6">
|
||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">系统设置</h1>
|
||
<p className="text-gray-600 dark:text-gray-400">自定义网站外观和品牌信息</p>
|
||
</div>
|
||
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 space-y-8">
|
||
{/* 网站名称设置 */}
|
||
<div>
|
||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||
<svg className="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||
</svg>
|
||
网站名称
|
||
</h2>
|
||
<div className="space-y-2">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
站点名称
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={siteName}
|
||
onChange={(e) => setSiteName(e.target.value)}
|
||
placeholder="由壹点3D素材"
|
||
className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
该名称将显示在顶部导航栏和登录页面
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 导航栏LOGO设置 */}
|
||
<div className="border-t border-gray-200 dark:border-gray-700 pt-8">
|
||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||
<svg className="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||
</svg>
|
||
导航栏LOGO(深色背景)
|
||
</h2>
|
||
<div className="space-y-4">
|
||
{/* LOGO预览 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
当前LOGO预览
|
||
</label>
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-32 h-32 rounded-lg border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center overflow-hidden" style={{ backgroundColor: headerBgColor }}>
|
||
{headerLogoPreview ? (
|
||
<img src={headerLogoPreview} alt="导航栏LOGO" className="max-w-full max-h-full object-contain" />
|
||
) : (
|
||
<svg className="w-16 h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||
</svg>
|
||
)}
|
||
</div>
|
||
<div className="flex-1">
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||
用于深色导航栏,建议使用浅色LOGO
|
||
</p>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||
建议尺寸:200x200px,支持PNG、JPG、SVG格式
|
||
</p>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
文件大小不超过5MB
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 上传按钮 */}
|
||
<div className="flex gap-3">
|
||
<label className="btn-primary cursor-pointer inline-flex items-center gap-2 disabled:opacity-50">
|
||
{uploadingHeader ? (
|
||
<>
|
||
<svg className="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
上传中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||
</svg>
|
||
上传导航栏LOGO
|
||
</>
|
||
)}
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleHeaderLogoUpload}
|
||
disabled={uploadingHeader}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
<button
|
||
onClick={handleHeaderLogoReset}
|
||
className="btn-secondary"
|
||
disabled={uploadingHeader}
|
||
>
|
||
恢复默认
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 登录页LOGO设置 */}
|
||
<div className="border-t border-gray-200 dark:border-gray-700 pt-8">
|
||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||
<svg className="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||
</svg>
|
||
登录页LOGO(浅色背景)
|
||
</h2>
|
||
<div className="space-y-4">
|
||
{/* LOGO预览 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
当前LOGO预览
|
||
</label>
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-32 h-32 rounded-lg border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center overflow-hidden bg-white">
|
||
{loginLogoPreview ? (
|
||
<img src={loginLogoPreview} alt="登录页LOGO" className="max-w-full max-h-full object-contain" />
|
||
) : (
|
||
<svg className="w-16 h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||
</svg>
|
||
)}
|
||
</div>
|
||
<div className="flex-1">
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||
用于浅色登录页面,建议使用深色LOGO
|
||
</p>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||
建议尺寸:200x200px,支持PNG、JPG、SVG格式
|
||
</p>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
文件大小不超过5MB
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 上传按钮 */}
|
||
<div className="flex gap-3">
|
||
<label className="btn-primary cursor-pointer inline-flex items-center gap-2 disabled:opacity-50">
|
||
{uploadingLogin ? (
|
||
<>
|
||
<svg className="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
上传中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||
</svg>
|
||
上传登录页LOGO
|
||
</>
|
||
)}
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleLoginLogoUpload}
|
||
disabled={uploadingLogin}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
<button
|
||
onClick={handleLoginLogoReset}
|
||
className="btn-secondary"
|
||
disabled={uploadingLogin}
|
||
>
|
||
恢复默认
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 主题颜色设置 */}
|
||
<div className="border-t border-gray-200 dark:border-gray-700 pt-8">
|
||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||
<svg className="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
|
||
</svg>
|
||
主题颜色
|
||
</h2>
|
||
<div className="space-y-4">
|
||
{/* 颜色预览 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
颜色预览
|
||
</label>
|
||
<div
|
||
className="w-full h-16 rounded-lg border-2 border-gray-300 dark:border-gray-600 shadow-inner"
|
||
style={{ backgroundColor: headerBgColor }}
|
||
>
|
||
<div className="flex items-center h-full px-4">
|
||
<span className="text-white font-medium" style={{ textShadow: '0 1px 2px rgba(0,0,0,0.3)' }}>
|
||
顶部导航栏预览
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 颜色选择器 */}
|
||
<div className="flex items-center gap-4">
|
||
<div className="flex-1">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
选择颜色
|
||
</label>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="color"
|
||
value={headerBgColor}
|
||
onChange={(e) => setHeaderBgColor(e.target.value)}
|
||
className="w-16 h-12 rounded cursor-pointer border border-gray-300 dark:border-gray-600"
|
||
/>
|
||
<input
|
||
type="text"
|
||
value={headerBgColor}
|
||
onChange={(e) => setHeaderBgColor(e.target.value)}
|
||
placeholder="#1f2937"
|
||
className="flex-1 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all font-mono"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 opacity-0">
|
||
操作
|
||
</label>
|
||
<button
|
||
onClick={handleColorReset}
|
||
className="btn-secondary whitespace-nowrap"
|
||
>
|
||
恢复默认
|
||
</button>
|
||
</div>
|
||
</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">
|
||
{[
|
||
{ name: '默认灰', color: '#1f2937' },
|
||
{ name: '深蓝', color: '#1e3a8a' },
|
||
{ name: '靛蓝', color: '#4f46e5' },
|
||
{ name: '紫色', color: '#7c3aed' },
|
||
{ name: '粉色', color: '#db2777' },
|
||
{ name: '红色', color: '#dc2626' },
|
||
{ name: '橙色', color: '#ea580c' },
|
||
{ name: '绿色', color: '#059669' },
|
||
{ name: '青色', color: '#0891b2' },
|
||
{ name: '深灰', color: '#374151' },
|
||
].map((preset) => (
|
||
<button
|
||
key={preset.color}
|
||
onClick={() => setHeaderBgColor(preset.color)}
|
||
className={`px-4 py-2 rounded-lg border-2 transition-all ${
|
||
headerBgColor === preset.color
|
||
? 'border-blue-500 ring-2 ring-blue-200'
|
||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'
|
||
}`}
|
||
style={{ backgroundColor: preset.color }}
|
||
title={preset.color}
|
||
>
|
||
<span className="text-white text-sm font-medium" style={{ textShadow: '0 1px 2px rgba(0,0,0,0.5)' }}>
|
||
{preset.name}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 保存按钮 */}
|
||
<div className="border-t border-gray-200 dark:border-gray-700 pt-6 flex justify-between items-center">
|
||
<button
|
||
onClick={() => router.push('/admin')}
|
||
className="btn-secondary"
|
||
>
|
||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||
</svg>
|
||
返回
|
||
</button>
|
||
<button
|
||
onClick={handleSave}
|
||
disabled={saving}
|
||
className="btn-primary text-lg px-8 py-3 disabled:opacity-50"
|
||
>
|
||
{saving ? (
|
||
<>
|
||
<svg className="w-5 h-5 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||
</svg>
|
||
保存中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
保存设置
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* 提示信息 */}
|
||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||
<div className="flex items-start gap-3">
|
||
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<div className="text-sm text-blue-800 dark:text-blue-300">
|
||
<p className="font-medium mb-1">温馨提示:</p>
|
||
<ul className="list-disc list-inside space-y-1">
|
||
<li>修改设置后点击“保存设置”按钮,页面会自动刷新以应用新的设置</li>
|
||
<li>LOGO会自动上传到MinIO S3存储桶,确保全站可访问</li>
|
||
<li>建议使用高质量的LOGO图片以获得最佳显示效果</li>
|
||
<li>主题颜色建议选择深色系,以确保白色文字清晰可见;主题颜色会应用到导航栏、按钮等UI元素</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* S3存储桶配置 */}
|
||
<div className="border-t border-gray-200 dark:border-gray-700 pt-8">
|
||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
|
||
<svg className="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" />
|
||
</svg>
|
||
S3存储桶配置
|
||
</h2>
|
||
<div className="space-y-4">
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
配置S3对象存储,用于存储上传的图片和文件
|
||
</p>
|
||
|
||
{/* S3端点 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
S3端点 (Endpoint)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={s3Endpoint}
|
||
onChange={(e) => setS3Endpoint(e.target.value)}
|
||
placeholder="例如: s3.1080p.hk"
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
</div>
|
||
|
||
{/* Access Key & Secret Key */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Access Key (访问密钥)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={s3AccessKey}
|
||
onChange={(e) => setS3AccessKey(e.target.value)}
|
||
placeholder="输入Access Key"
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all font-mono text-sm"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Secret Key (密钥)
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={s3SecretKey}
|
||
onChange={(e) => setS3SecretKey(e.target.value)}
|
||
placeholder="输入Secret Key"
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all font-mono text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Bucket & Region */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
存储桶名称 (Bucket)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={s3Bucket}
|
||
onChange={(e) => setS3Bucket(e.target.value)}
|
||
placeholder="例如: my-bucket"
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
区域 (Region)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={s3Region}
|
||
onChange={(e) => setS3Region(e.target.value)}
|
||
placeholder="例如: us-east-1"
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 公网访问URL */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
公网访问URL (Public URL)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={s3PublicUrl}
|
||
onChange={(e) => setS3PublicUrl(e.target.value)}
|
||
placeholder="例如: https://cdn.example.com"
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
如果配置了CDN或自定义域名,请填写此项
|
||
</p>
|
||
</div>
|
||
|
||
{/* 提示信息 */}
|
||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mt-4">
|
||
<div className="flex items-start gap-3">
|
||
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<div className="text-sm text-blue-800 dark:text-blue-300">
|
||
<p className="font-semibold mb-2">配置说明:</p>
|
||
<ul className="space-y-1 list-disc list-inside">
|
||
<li>S3端点通常是您的对象存储服务器地址</li>
|
||
<li>Access Key和Secret Key用于身份验证</li>
|
||
<li>存储桶需要预先创建并设置公开读取权限</li>
|
||
<li>修改配置后需要重启应用才能生效</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 系统维护通知 */}
|
||
<div className="card-apple">
|
||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">系统维护通知</h2>
|
||
<div className="space-y-6">
|
||
{/* 启用维护通知 */}
|
||
<div className="flex items-center gap-3">
|
||
<input
|
||
type="checkbox"
|
||
id="maintenanceEnabled"
|
||
checked={maintenanceEnabled}
|
||
onChange={(e) => setMaintenanceEnabled(e.target.checked)}
|
||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||
/>
|
||
<label htmlFor="maintenanceEnabled" className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
启用系统维护通知
|
||
</label>
|
||
</div>
|
||
|
||
{maintenanceEnabled && (
|
||
<>
|
||
{/* 维护通知内容 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
维护通知内容 <span className="text-red-500">*</span>
|
||
</label>
|
||
<textarea
|
||
value={maintenanceMessage}
|
||
onChange={(e) => setMaintenanceMessage(e.target.value)}
|
||
placeholder="例如:系统将于今晚22:00-24:00进行维护升级,期间可能无法正常访问,请提前做好准备。"
|
||
rows={4}
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
<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="datetime-local"
|
||
value={maintenanceStartTime}
|
||
onChange={(e) => setMaintenanceStartTime(e.target.value)}
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
<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="datetime-local"
|
||
value={maintenanceEndTime}
|
||
onChange={(e) => setMaintenanceEndTime(e.target.value)}
|
||
className="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-blue-500 dark:focus:border-blue-400 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-600 outline-none transition-all"
|
||
/>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||
到达结束时间后,通知将自动隐藏
|
||
</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
{/* 统一的Toast提示 */}
|
||
{toast && toast.show && (
|
||
<ToastBubble message={toast.message} variant={toast.variant} />
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|