import { FormEvent, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { Loader2, Save, SettingsIcon } from 'lucide-react' import toast from 'react-hot-toast' import { adminAPI } from '../api/admin' import { libraryAPI } from '../api/library' import type { Library, Setting } from '../types' import { APIConfigsPanel } from '../components/APIConfigsPanel' import { AdultSettingsPanel } from './AdultSettingsPanel' import { DatabaseSettingsPanel } from './DatabaseSettingsPanel' import { RecognitionWordsPanel } from './RecognitionWordsPanel' import { SettingRow } from './SettingsRow' import { ALL_KEYS, GROUPS } from './settingsGroups' export function SettingsPage() { const [searchParams] = useSearchParams() const [activeGroup, setActiveGroup] = useState(() => { const fromQuery = searchParams.get('group') return GROUPS.some((g) => g.key === fromQuery) ? (fromQuery as string) : GROUPS[0].key }) const [values, setValues] = useState>({}) const [dirty, setDirty] = useState>(new Set()) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [libraries, setLibraries] = useState([]) useEffect(() => { const fromQuery = searchParams.get('group') if (fromQuery && GROUPS.some((g) => g.key === fromQuery)) { setActiveGroup(fromQuery) } }, [searchParams]) const refresh = async () => { setLoading(true) try { const [all, libs] = await Promise.all([ adminAPI.listSettings(), libraryAPI.list({ includeHidden: true }).catch(() => [] as Library[]), ]) const idx: Record = {} for (const s of all as Setting[]) { if (ALL_KEYS.has(s.key)) idx[s.key] = s.value } setValues(idx) setLibraries(libs as Library[]) setDirty(new Set()) } finally { setLoading(false) } } useEffect(() => { refresh().catch(() => undefined) }, []) const onChange = (key: string, value: string) => { setValues((v) => ({ ...v, [key]: value })) setDirty((d) => new Set(d).add(key)) } const onSave = async (e: FormEvent) => { e.preventDefault() if (dirty.size === 0) return const wantHTTPS = values['https.enabled'] === 'true' || values['https.enabled'] === '1' // 证书/私钥任一来源可用即可:路径优先,其次粘贴的内容。 const materialOK = (content?: string, path?: string) => Boolean((path ?? '').trim() || (content ?? '').trim()) if (wantHTTPS && !(materialOK(values['https.cert'], values['https.cert_path']) && materialOK(values['https.key'], values['https.key_path']))) { toast.error('启用 HTTPS 前请先填写 SSL 证书和私钥(内容或路径任选其一)') return } setSaving(true) try { // 证书/私钥(内容与路径)先保存、启用开关最后保存,后端校验开关时才能读到最新的配置。 const rank = (k: string) => k === 'https.cert' || k === 'https.key' || k === 'https.cert_path' || k === 'https.key_path' ? 0 : k === 'https.enabled' ? 2 : 1 const orderedKeys = [...dirty].sort((a, b) => rank(a) - rank(b)) const failures: string[] = [] for (const key of orderedKeys) { try { await adminAPI.updateSetting(key, values[key] ?? '') } catch (err) { failures.push( (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? `保存 ${key} 失败`, ) } } if (failures.length > 0) { toast.error(failures[0]) return } toast.success(`已保存 ${dirty.size} 项配置`) setDirty(new Set()) // 切换 HTTPS 后(或关闭 HTTPS 后)连接会短暂中断,自动跳转到对应协议地址。 const currentIsHTTPS = window.location.protocol === 'https:' const targetProto = wantHTTPS ? 'https:' : 'http:' if (wantHTTPS !== currentIsHTTPS) { toast(wantHTTPS ? '正在切换到 HTTPS 访问…' : '正在切换回 HTTP 访问…') window.setTimeout(() => { const url = targetProto + '//' + window.location.host + window.location.pathname + window.location.search window.location.replace(url) }, 800) } } finally { setSaving(false) } } const group = GROUPS.find((g) => g.key === activeGroup)! return (

系统设置

按分组编辑转码 / 网盘转存 / Adult 等关键配置

{GROUPS.map((g) => ( ))}
{loading && (
)} {!loading && (
{group.key === 'database' && } {group.key === 'api-configs' && } {group.key === 'recognition-words' && } {group.key === 'adult' && } {group.key !== 'adult' && group.key !== 'library' && group.items.length > 0 && (
{group.description &&

{group.description}

} {group.items.map((it) => ( onChange(it.key, v)} libraries={libraries} /> ))}
{dirty.size > 0 ? `有 ${dirty.size} 项未保存` : '所有更改已保存'}
)}
)}
) }