import { useEffect, useState } from 'react'; import { Database, Shield, Server, SlidersHorizontal } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Spinner } from '@/components/ui/spinner'; import { notify } from '@/lib/notify'; import { api } from '../../api/client'; import { useAdminGuard } from '../../layouts/AdminLayout'; import { invalidateForumLimitsCache } from '../../hooks/useForumLimits'; import type { AdminSettings, ForumLimits } from '../../api/types'; type TabId = 'limits' | 'filter' | 'system'; type SettingRow = { key: keyof ForumLimits; label: string; unit?: string; hint?: string; min?: number; }; type SettingSection = { id: string; title: string; summary: string; rows: SettingRow[]; }; const SETTING_SECTIONS: SettingSection[] = [ { id: 'rule', title: '编辑规则', summary: '控制普通用户修改自己帖子的时限', rows: [ { key: 'post_edit_window_hours', label: '可编辑时限', unit: '小时', hint: '0 = 不限', min: 0 }, ], }, { id: 'rate', title: '操作限流', summary: '同一用户或 IP 在窗口期内的最大请求次数', rows: [ { key: 'rate_limit_window_sec', label: '限流窗口', unit: '秒', min: 10 }, { key: 'rate_limit_post', label: '发帖', unit: '次', min: 1 }, { key: 'rate_limit_comment', label: '评论', unit: '次', min: 1 }, { key: 'rate_limit_register', label: '注册', unit: '次', min: 1 }, { key: 'rate_limit_login', label: '登录', unit: '次', min: 1 }, ], }, { id: 'content', title: '内容长度', summary: '发帖与评论的字数上限,服务端强制校验', rows: [ { key: 'post_title_max', label: '帖子标题', unit: '字', min: 1 }, { key: 'post_tags_max', label: '帖子标签', unit: '字', hint: '0 = 不限', min: 0 }, { key: 'post_content_max', label: '帖子正文', unit: '字', hint: '0 = 不限', min: 0 }, { key: 'comment_max', label: '评论内容', unit: '字', min: 1 }, ], }, { id: 'search', title: '搜索与列表', summary: '关键词长度与帖子列表分页', rows: [ { key: 'search_keyword_min', label: '关键词最短', unit: '字', min: 0 }, { key: 'search_keyword_max', label: '关键词最长', unit: '字', min: 1 }, { key: 'page_size_default', label: '默认每页', unit: '条', min: 1 }, { key: 'page_size_max', label: '最大每页', unit: '条', min: 1 }, { key: 'feed_max_pages', label: '首页自动加载页数', unit: '页', hint: '超出后需手动加载', min: 1 }, { key: 'feed_max_items', label: '首页自动加载条数', unit: '条', hint: '与页数取先到上限', min: 1 }, ], }, { id: 'user', title: '用户账号', summary: '注册、改密与头像上传限制', rows: [ { key: 'password_min_len', label: '密码最短', unit: '位', min: 4 }, { key: 'avatar_max_mb', label: '头像上限', unit: 'MB', min: 1 }, ], }, ]; const TABS: { id: TabId; label: string; icon: typeof SlidersHorizontal }[] = [ { id: 'limits', label: '论坛限制', icon: SlidersHorizontal }, { id: 'filter', label: '敏感词', icon: Shield }, { id: 'system', label: '系统维护', icon: Server }, ]; function SettingTable({ sections, limits, onChange, }: { sections: SettingSection[]; limits: ForumLimits; onChange: (key: keyof ForumLimits, value: string) => void; }) { return (
{sections.map(section => (

{section.title}

{section.summary}

{section.rows.map(row => (
onChange(row.key, e.target.value)} className="admin-settings-input" /> {row.unit && {row.unit}}
{row.hint ?? ''}
))}
))}
); } export default function AdminSettingsPage() { const { ready } = useAdminGuard(); const [settings, setSettings] = useState(null); const [limits, setLimits] = useState(null); const [filterWords, setFilterWords] = useState(''); const [activeTab, setActiveTab] = useState('limits'); const [loading, setLoading] = useState(true); const [backing, setBacking] = useState(false); const [savingForum, setSavingForum] = useState(false); const [savingFilter, setSavingFilter] = useState(false); useEffect(() => { if (!ready) return; api.adminSettings() .then(s => { setSettings(s); setLimits(s.limits); setFilterWords(s.filter_words); }) .finally(() => setLoading(false)); }, [ready]); const handleLimitChange = (key: keyof ForumLimits, value: string) => { const n = parseInt(value, 10); if (Number.isNaN(n)) return; setLimits(prev => prev ? { ...prev, [key]: n } : prev); }; const handleSaveForumSettings = async () => { if (!limits) return; setSavingForum(true); try { const r = await api.adminUpdateForumSettings(limits); notify.success(r.message); invalidateForumLimitsCache(); setLimits(r.limits); setSettings(s => s ? { ...s, limits: r.limits } : s); } catch (e: unknown) { notify.error(e instanceof Error ? e.message : '保存失败'); } finally { setSavingForum(false); } }; const handleSaveFilterWords = async () => { setSavingFilter(true); try { const r = await api.adminUpdateFilterWords(filterWords); notify.success(r.message); setSettings(s => s ? { ...s, filter_words: filterWords, filter_word_count: r.word_count } : s); } catch (e: unknown) { notify.error(e instanceof Error ? e.message : '保存失败'); } finally { setSavingFilter(false); } }; const handleBackup = async () => { setBacking(true); try { const r = await api.adminBackup(); notify.success(r.message); window.location.href = r.download; } catch (e: unknown) { notify.error(e instanceof Error ? e.message : '备份失败'); } finally { setBacking(false); } }; if (!ready || loading) { return
; } if (!settings || !limits) return null; return (

系统设置

管理论坛运行规则、敏感词过滤与数据维护

{activeTab === 'limits' && (
论坛限制 共 {SETTING_SECTIONS.length} 组

修改后请点击保存,新规则立即对全部用户生效

)} {activeTab === 'filter' && (
敏感词过滤 {settings.filter_word_count} 个词条

每行一个词,# 开头为注释。保存后写入 filter_words.txt 并立即生效。