825 lines
39 KiB
TypeScript
825 lines
39 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'
|
||
|
||
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)
|
||
|
||
// 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 [enableUserActivation, setEnableUserActivation] = useState(false)
|
||
const [enableRegistration, setEnableRegistration] = useState(true)
|
||
const [enableAiImageEdit, setEnableAiImageEdit] = useState(false)
|
||
const [enableTunnelService, setEnableTunnelService] = useState(false)
|
||
|
||
// 权限检查
|
||
useEffect(() => {
|
||
if (status === 'unauthenticated') {
|
||
router.push('/login')
|
||
} else if (status === 'authenticated' && session?.user?.role !== 'ADMIN') {
|
||
router.push('/')
|
||
}
|
||
}, [status, session, router])
|
||
|
||
// 加载设置
|
||
useEffect(() => {
|
||
if (status === 'authenticated' && session?.user?.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 || '')
|
||
setEnableUserActivation(data.enableUserActivation ?? false)
|
||
setEnableRegistration(data.enableRegistration ?? true)
|
||
setEnableAiImageEdit(data.enableAiImageEdit ?? false)
|
||
setEnableTunnelService(data.enableTunnelService ?? false)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching settings:', error)
|
||
alert('加载设置失败')
|
||
} 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,
|
||
enableUserActivation,
|
||
enableRegistration,
|
||
enableAiImageEdit,
|
||
enableTunnelService
|
||
}),
|
||
})
|
||
|
||
if (response.ok) {
|
||
alert('保存成功!页面将自动刷新以应用新设置')
|
||
// 刷新页面以应用新设置
|
||
window.location.reload()
|
||
} else {
|
||
const error = await response.json()
|
||
alert(error.error || '保存失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('Save settings error:', error)
|
||
alert('保存失败,请稍后重试')
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
const handleHeaderLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (!file) return
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith('image/')) {
|
||
alert('请选择图片文件')
|
||
return
|
||
}
|
||
|
||
// 验证文件大小(最大5MB)
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
alert('LOGO文件大小不能超过5MB')
|
||
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)
|
||
alert('导航栏LOGO上传成功!')
|
||
} else {
|
||
const error = await response.json()
|
||
alert(error.error || 'LOGO上传失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload logo error:', error)
|
||
alert('上传失败,请稍后重试')
|
||
} finally {
|
||
setUploadingHeader(false)
|
||
}
|
||
}
|
||
|
||
const handleLoginLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (!file) return
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith('image/')) {
|
||
alert('请选择图片文件')
|
||
return
|
||
}
|
||
|
||
// 验证文件大小(最大5MB)
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
alert('LOGO文件大小不能超过5MB')
|
||
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)
|
||
alert('登录页LOGO上传成功!')
|
||
} else {
|
||
const error = await response.json()
|
||
alert(error.error || 'LOGO上传失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('Upload logo error:', error)
|
||
alert('上传失败,请稍后重试')
|
||
} finally {
|
||
setUploadingLogin(false)
|
||
}
|
||
}
|
||
|
||
const handleColorReset = () => {
|
||
setHeaderBgColor('#1f2937')
|
||
}
|
||
|
||
const handleHeaderLogoReset = () => {
|
||
const def = process.env.NEXT_PUBLIC_DEFAULT_HEADER_LOGO_URL || ''
|
||
setHeaderLogoPreview(def)
|
||
setHeaderLogoUrl(def)
|
||
}
|
||
|
||
const handleLoginLogoReset = () => {
|
||
const def = process.env.NEXT_PUBLIC_DEFAULT_LOGIN_LOGO_URL || ''
|
||
setLoginLogoPreview(def)
|
||
setLoginLogoUrl(def)
|
||
}
|
||
|
||
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.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>
|
||
|
||
{/* 功能开关 */}
|
||
<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="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
功能开关
|
||
</h2>
|
||
<div className="space-y-6">
|
||
{/* 用户激活功能开关 */}
|
||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||
用户激活功能
|
||
</h3>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
开启后,新注册的用户需要管理员激活才能登录。默认关闭。
|
||
</p>
|
||
</div>
|
||
<label className={`relative inline-flex items-center ${!(session?.user as any)?.isSuperAdmin ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}>
|
||
<input
|
||
type="checkbox"
|
||
checked={enableUserActivation}
|
||
onChange={(e) => setEnableUserActivation(e.target.checked)}
|
||
disabled={!(session?.user as any)?.isSuperAdmin}
|
||
className="sr-only peer"
|
||
/>
|
||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||
</label>
|
||
</div>
|
||
{!(session?.user as any)?.isSuperAdmin && (
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||
仅最高权限账号可修改此开关
|
||
</p>
|
||
)}
|
||
|
||
{/* 用户注册功能开关 */}
|
||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||
用户手动注册功能
|
||
</h3>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
关闭后,用户将无法通过注册页面手动注册。默认开启。
|
||
</p>
|
||
</div>
|
||
<label className="relative inline-flex items-center cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={enableRegistration}
|
||
onChange={(e) => setEnableRegistration(e.target.checked)}
|
||
className="sr-only peer"
|
||
/>
|
||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||
</label>
|
||
</div>
|
||
|
||
{/* AI改图功能 */}
|
||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||
AI改图功能(预留)
|
||
</h3>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
控制素材详情页的 AI 改图按钮显示。功能目前为占位,启用后将连通正式 API。
|
||
</p>
|
||
</div>
|
||
<label
|
||
className={`relative inline-flex items-center ${
|
||
!(session?.user as any)?.isSuperAdmin ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
|
||
}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={enableAiImageEdit}
|
||
onChange={(e) => setEnableAiImageEdit(e.target.checked)}
|
||
disabled={!(session?.user as any)?.isSuperAdmin}
|
||
className="sr-only peer"
|
||
/>
|
||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||
</label>
|
||
</div>
|
||
|
||
{/* 内网穿透功能 */}
|
||
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
||
内网穿透功能(付费)
|
||
</h3>
|
||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||
开启后可在后台调度付费内网穿透服务,目前仅展示占位操作。请确认购买服务后再启用。
|
||
</p>
|
||
</div>
|
||
<label
|
||
className={`relative inline-flex items-center ${
|
||
!(session?.user as any)?.isSuperAdmin ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
|
||
}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={enableTunnelService}
|
||
onChange={(e) => setEnableTunnelService(e.target.checked)}
|
||
disabled={!(session?.user as any)?.isSuperAdmin}
|
||
className="sr-only peer"
|
||
/>
|
||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||
</label>
|
||
</div>
|
||
|
||
{!(session?.user as any)?.isSuperAdmin && (
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||
最高权限账号才可修改 AI/内网穿透等受保护功能开关
|
||
</p>
|
||
)}
|
||
</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>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|
||
|