WuYongJie f646b71813
Some checks failed
CI Check / check (push) Has been cancelled
chore: 初始化本地仓库并同步最近改动
2025-11-18 17:50:16 +08:00

825 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>修改设置后点击&ldquo;保存设置&rdquo;按钮,页面会自动刷新以应用新的设置</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>
)
}