'use client'; import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; import { useSiteConfig } from '@/lib/site-config'; import { useAuth } from '@/lib/auth-store'; import { Crown, Globe, Loader2, Logs, Mail, Save, Send, ToggleLeft, Upload } from 'lucide-react'; import { toast } from 'sonner'; // ============================================================ // Tab 6: Settings // ============================================================ type EmailSettingsForm = { enabled: boolean; smtpHost: string; smtpPort: number; smtpSecure: boolean; smtpUser: string; smtpPassword: string; smtpPasswordPreview: string; fromEmail: string; fromName: string; replyTo: string; appName: string; appBaseUrl: string; logoUrl: string; contactEmail: string; copyright: string; codeLength: number; codeCharset: 'alphanumeric' | 'numeric' | 'letters'; codeTtlMinutes: number; }; type EmailRecipient = { id: string; email: string; nickname: string; phone: string | null; avatarUrl: string | null; emailVerified: boolean; }; type SettingsSection = 'site' | 'footer' | 'logs' | 'email' | 'mail' | 'features'; const DEFAULT_EMAIL_SETTINGS: EmailSettingsForm = { enabled: false, smtpHost: '', smtpPort: 465, smtpSecure: true, smtpUser: '', smtpPassword: '', smtpPasswordPreview: '****', fromEmail: '', fromName: '妙境官方通知', replyTo: '', appName: '妙境', appBaseUrl: '', logoUrl: '/logo.png', contactEmail: '', copyright: '', codeLength: 6, codeCharset: 'alphanumeric', codeTtlMinutes: 5, }; function SectionMenu({ items, activeValue, onChange, }: { items: Array<{ value: T; label: string; description?: string }>; activeValue: T; onChange: (value: T) => void; }) { return (
{items.map(item => { const active = activeValue === item.value; return ( ); })}
); } export default function SettingsTab() { const { config: siteConfig, loaded: siteConfigLoaded, saveSiteConfig } = useSiteConfig(); const { accessToken } = useAuth(); const logoInputRef = useRef(null); const faviconInputRef = useRef(null); // Local form state (not committed until save) const [formSiteName, setFormSiteName] = useState(''); const [formTabTitle, setFormTabTitle] = useState(''); const [formLogoBase64, setFormLogoBase64] = useState(null); const [formFaviconBase64, setFormFaviconBase64] = useState(null); const [formMembershipEnabled, setFormMembershipEnabled] = useState(true); const [formTermsOfService, setFormTermsOfService] = useState(''); const [formPrivacyPolicy, setFormPrivacyPolicy] = useState(''); const [formAboutUs, setFormAboutUs] = useState(''); const [formHelpCenter, setFormHelpCenter] = useState(''); const [formFilingInfo, setFormFilingInfo] = useState(''); const [formFilingUrl, setFormFilingUrl] = useState(''); const [formPublicSecurityFilingInfo, setFormPublicSecurityFilingInfo] = useState(''); const [formPublicSecurityFilingUrl, setFormPublicSecurityFilingUrl] = useState(''); const [formLogRetentionDays, setFormLogRetentionDays] = useState(30); const [saving, setSaving] = useState(false); const [initialized, setInitialized] = useState(false); const [emailSettings, setEmailSettings] = useState(DEFAULT_EMAIL_SETTINGS); const [emailPreviewHtml, setEmailPreviewHtml] = useState(''); const [emailLoading, setEmailLoading] = useState(false); const [emailSaving, setEmailSaving] = useState(false); const [emailTesting, setEmailTesting] = useState(false); const [testEmail, setTestEmail] = useState(''); const [mailMode, setMailMode] = useState<'all' | 'selected'>('selected'); const [mailKind, setMailKind] = useState<'notification' | 'admin'>('notification'); const [mailTitle, setMailTitle] = useState(''); const [mailContent, setMailContent] = useState(''); const [mailButtonText, setMailButtonText] = useState(''); const [mailButtonUrl, setMailButtonUrl] = useState(''); const [recipientQuery, setRecipientQuery] = useState(''); const [recipientTotal, setRecipientTotal] = useState(0); const [recipientResults, setRecipientResults] = useState([]); const [selectedRecipients, setSelectedRecipients] = useState([]); const [recipientsLoading, setRecipientsLoading] = useState(false); const [mailSending, setMailSending] = useState(false); const [activeSection, setActiveSection] = useState('site'); // Sync site config to form when loaded useEffect(() => { if (siteConfigLoaded && !initialized) { setFormSiteName(siteConfig.siteName); setFormTabTitle(siteConfig.siteTabTitle); setFormTermsOfService(siteConfig.termsOfService); setFormPrivacyPolicy(siteConfig.privacyPolicy); setFormAboutUs(siteConfig.aboutUs); setFormHelpCenter(siteConfig.helpCenter); setFormFilingInfo(siteConfig.filingInfo); setFormFilingUrl(siteConfig.filingUrl); setFormPublicSecurityFilingInfo(siteConfig.publicSecurityFilingInfo); setFormPublicSecurityFilingUrl(siteConfig.publicSecurityFilingUrl); setFormLogRetentionDays(siteConfig.logRetentionDays); setInitialized(true); } }, [siteConfig, siteConfigLoaded, initialized]); useEffect(() => { setFormMembershipEnabled(siteConfig.membershipEnabled !== false); }, [siteConfig.membershipEnabled]); const loadEmailSettings = useCallback(async () => { if (!accessToken) return; setEmailLoading(true); try { const response = await fetch('/api/admin/email-settings', { headers: { Authorization: `Bearer ${accessToken}` }, }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || '邮箱配置加载失败'); setEmailSettings({ ...DEFAULT_EMAIL_SETTINGS, ...(data.settings || {}), smtpPassword: '' }); setEmailPreviewHtml(data.preview || ''); } catch (error) { toast.error(error instanceof Error ? error.message : '邮箱配置加载失败'); } finally { setEmailLoading(false); } }, [accessToken]); useEffect(() => { loadEmailSettings(); }, [loadEmailSettings]); const loadEmailRecipients = useCallback(async (query = '') => { if (!accessToken) return; setRecipientsLoading(true); try { const params = new URLSearchParams(); if (query.trim()) params.set('q', query.trim()); params.set('limit', '40'); const response = await fetch(`/api/admin/email-recipients?${params.toString()}`, { headers: { Authorization: `Bearer ${accessToken}` }, }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || '收件用户加载失败'); setRecipientResults(Array.isArray(data.users) ? data.users : []); setRecipientTotal(Number(data.total || 0)); } catch (error) { toast.error(error instanceof Error ? error.message : '收件用户加载失败'); } finally { setRecipientsLoading(false); } }, [accessToken]); useEffect(() => { if (mailMode !== 'selected') return; const timer = window.setTimeout(() => { loadEmailRecipients(recipientQuery); }, 250); return () => window.clearTimeout(timer); }, [mailMode, recipientQuery, loadEmailRecipients]); useEffect(() => { if (mailMode === 'all') { loadEmailRecipients(''); } }, [mailMode, loadEmailRecipients]); const handleFileUpload = async ( file: File, setter: (val: string | null) => void, maxSizeKB: number = 2048, targetSize: number = 64, ) => { if (file.size > maxSizeKB * 1024) { toast.error(`文件大小不能超过 ${maxSizeKB >= 1024 ? `${maxSizeKB / 1024}MB` : `${maxSizeKB}KB`}`); return; } // SVG: read as text data URL directly if (file.type === 'image/svg+xml') { const reader = new FileReader(); reader.onload = (e) => { const result = e.target?.result as string; if (result) setter(result); }; reader.readAsDataURL(file); return; } // PNG/JPG: resize to target dimensions try { const bitmap = await createImageBitmap(file); const canvas = document.createElement('canvas'); canvas.width = targetSize; canvas.height = targetSize; const ctx = canvas.getContext('2d'); if (!ctx) { toast.error('浏览器不支持 Canvas'); return; } ctx.drawImage(bitmap, 0, 0, targetSize, targetSize); bitmap.close(); setter(canvas.toDataURL('image/png')); } catch { const reader = new FileReader(); reader.onload = (e) => { const result = e.target?.result as string; if (result) setter(result); }; reader.readAsDataURL(file); } }; const handleSave = async () => { setSaving(true); try { await saveSiteConfig({ siteName: formSiteName, siteTabTitle: formTabTitle, logoBase64: formLogoBase64 || undefined, faviconBase64: formFaviconBase64 || undefined, membershipEnabled: formMembershipEnabled, termsOfService: formTermsOfService, privacyPolicy: formPrivacyPolicy, aboutUs: formAboutUs, helpCenter: formHelpCenter, filingInfo: formFilingInfo, filingUrl: formFilingUrl, publicSecurityFilingInfo: formPublicSecurityFilingInfo, publicSecurityFilingUrl: formPublicSecurityFilingUrl, logRetentionDays: formLogRetentionDays, }); // Clear pending uploads after save setFormLogoBase64(null); setFormFaviconBase64(null); toast.success('网站配置已保存,所有访客将看到更新'); } catch (err) { toast.error(err instanceof Error ? err.message : '保存失败'); } finally { setSaving(false); } }; const handleMembershipToggle = async (checked: boolean) => { setFormMembershipEnabled(checked); try { await saveSiteConfig({ membershipEnabled: checked }); toast.success(checked ? '会员功能已开启' : '会员功能已关闭'); } catch (err) { setFormMembershipEnabled(!checked); toast.error(err instanceof Error ? err.message : '会员功能保存失败'); } }; const handleEmailSettingChange = (key: K, value: EmailSettingsForm[K]) => { setEmailSettings(prev => ({ ...prev, [key]: value })); }; const handleSaveEmailSettings = async () => { if (!accessToken) return; setEmailSaving(true); try { const payload = { ...emailSettings, smtpPassword: emailSettings.smtpPassword || undefined, }; const response = await fetch('/api/admin/email-settings', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(payload), }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || '邮箱配置保存失败'); setEmailSettings({ ...DEFAULT_EMAIL_SETTINGS, ...(data.settings || {}), smtpPassword: '' }); toast.success(data.message || '邮箱配置已保存'); loadEmailSettings(); } catch (error) { toast.error(error instanceof Error ? error.message : '邮箱配置保存失败'); } finally { setEmailSaving(false); } }; const handleSendTestEmail = async () => { if (!accessToken) return; if (!testEmail) { toast.error('请填写测试收件邮箱'); return; } setEmailTesting(true); try { const response = await fetch('/api/admin/email-settings', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ to: testEmail }), }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || '测试邮件发送失败'); toast.success(data.message || '测试邮件已发送'); } catch (error) { toast.error(error instanceof Error ? error.message : '测试邮件发送失败'); } finally { setEmailTesting(false); } }; const selectedRecipientIds = useMemo( () => new Set(selectedRecipients.map(user => user.id)), [selectedRecipients], ); const toggleRecipient = (user: EmailRecipient) => { setSelectedRecipients(prev => { if (prev.some(item => item.id === user.id)) { return prev.filter(item => item.id !== user.id); } return [...prev, user]; }); }; const handleSendUserEmail = async () => { if (!accessToken) return; if (!mailTitle.trim() || !mailContent.trim()) { toast.error('请填写邮件标题和正文内容'); return; } if (mailMode === 'selected' && selectedRecipients.length === 0) { toast.error('请至少选择一个收件用户'); return; } const confirmed = window.confirm( mailMode === 'all' ? `确定要发送给全部 ${recipientTotal || '非管理员'} 用户吗?` : `确定要发送给 ${selectedRecipients.length} 个指定用户吗?`, ); if (!confirmed) return; setMailSending(true); try { const response = await fetch('/api/admin/send-email', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ mode: mailMode, userIds: mailMode === 'selected' ? selectedRecipients.map(user => user.id) : undefined, mailKind, title: mailTitle, content: mailContent, buttonText: mailButtonText || undefined, buttonUrl: mailButtonUrl || undefined, }), }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || data.message || '邮件发送失败'); toast.success(data.message || '邮件已发送'); if (!data.failedCount) { setMailTitle(''); setMailContent(''); setMailButtonText(''); setMailButtonUrl(''); setSelectedRecipients([]); } } catch (error) { toast.error(error instanceof Error ? error.message : '邮件发送失败'); } finally { setMailSending(false); } }; const currentLogoSrc = formLogoBase64 || siteConfig.logoUrl || '/logo.png'; const currentFaviconSrc = formFaviconBase64 || siteConfig.faviconUrl || '/favicon.png'; return (
{/* Site Config */} {activeSection === 'site' && ( 网站配置 自定义网站名称、Logo 和浏览器标签页图标,保存后所有访客可见 {/* Site Name */}

显示在导航栏、首页标题等位置

setFormSiteName(e.target.value)} placeholder="妙境" />
{/* Browser Tab Title */}

显示在浏览器标签页上的文字

setFormTabTitle(e.target.value)} placeholder="妙境 - AI创作平台" />

显示在网站页脚,例如:京ICP备XXXXXXXX号

setFormFilingInfo(e.target.value)} placeholder="京ICP备XXXXXXXX号" />

留空则备案信息仅展示不可点击

setFormFilingUrl(e.target.value)} placeholder="https://beian.miit.gov.cn/" />

显示在网站页脚,例如:京公网安备 XXXXXXXXXXXXXX号

setFormPublicSecurityFilingInfo(e.target.value)} placeholder="京公网安备 XXXXXXXXXXXXXX号" />

留空则公安备案信息仅展示不可点击

setFormPublicSecurityFilingUrl(e.target.value)} placeholder="https://www.beian.gov.cn/portal/registerSystemInfo" />
{/* Logo Upload */}

支持 PNG / JPG / SVG 格式,建议尺寸 64×64px,正方形,最大 2MB

Logo
{ const file = e.target.files?.[0]; if (file) handleFileUpload(file, setFormLogoBase64, 2048, 64); e.target.value = ''; }} />
{/* Favicon Upload */}

支持 PNG / JPG / SVG 格式,建议尺寸 32×32px 或 64×64px,正方形,最大 1MB

Favicon
{ const file = e.target.files?.[0]; if (file) handleFileUpload(file, setFormFaviconBase64, 1024, 32); e.target.value = ''; }} />
{/* Save Button */}
)} {activeSection === 'footer' && ( 页脚页面 配置首页右下角“关于我们、使用条款、隐私政策、帮助中心”对应页面内容