diff --git a/README.md b/README.md index da50ceb..f0873f6 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,22 @@ location / { 后端API已部署完成喵!接下来,请调用/init接口进行初始化 + + +## TODO + + + +目前收到的几个建议: + + + +1、开设不同讨论板块 + + + + + ## License diff --git a/public/about.md b/public/about.md index 6ab044f..b1c81b1 100644 --- a/public/about.md +++ b/public/about.md @@ -1,4 +1,8 @@ # Hi~欢迎来到Sycamore_Whisper匿名投稿站! +这是一个实例关于页面。 + + + 这里的内容来自开发环境下的/public/about.md,请编辑此文件以便在这里显示自己的内容! 如果你不了解Markdown文档的语法,可以前往[这里](https://www.runoob.com/markdown/md-tutorial.html)简单学习 diff --git a/src/App.tsx b/src/App.tsx index d2d4d25..ad91581 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -113,8 +113,8 @@ function App() { } /> } /> - } /> - } /> + setIsDarkMode(!isDarkMode)} />} /> + } /> diff --git a/src/admin_api.tsx b/src/admin_api.tsx index 9cbcab4..fa69ef0 100644 --- a/src/admin_api.tsx +++ b/src/admin_api.tsx @@ -729,4 +729,44 @@ export const modifyComment = async ( throw new Error(msg); } return resp.json(); +}; + +/** + * 获取违禁词列表 + * GET /admin/get/banned_keywords -> { keywords: string[] } + */ +export const getBannedKeywords = async (): Promise => { + const resp = await adminApiRequest('/get/banned_keywords', { method: 'GET' }); + if (!resp.ok) { + throw new Error(`获取违禁词失败: ${resp.status}`); + } + const data = await resp.json(); + const list = Array.isArray(data?.keywords) ? data.keywords : (Array.isArray(data) ? data : []); + return list.map((x: any) => String(x)).filter(Boolean); +}; + +/** + * 保存违禁词列表 + * POST /admin/banned_keywords { BANNED_KEYWORDS: string[] } + */ +export const setBannedKeywordsList = async (keywords: string[]): Promise<{ status: 'OK' }> => { + const clean = (keywords || []).map((x) => String(x).trim()).filter((x) => !!x); + const resp = await adminApiRequest('/banned_keywords', { + method: 'POST', + body: JSON.stringify({ BANNED_KEYWORDS: clean }), + }); + if (!resp.ok) { + let detail = ''; + try { + const ct = resp.headers.get('Content-Type') || ''; + if (ct.includes('application/json')) { + const d = await resp.json(); + detail = typeof d === 'string' ? d : (d?.reason || JSON.stringify(d)); + } else { + detail = await resp.text(); + } + } catch {} + throw new Error(`保存违禁词失败: ${resp.status}${detail ? ` - ${detail}` : ''}`); + } + return resp.json(); }; \ No newline at end of file diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx index 8247259..711ec6b 100644 --- a/src/components/AdminDashboard.tsx +++ b/src/components/AdminDashboard.tsx @@ -12,15 +12,23 @@ import { DialogTitle, DialogContent, DialogActions, + Input, + Tooltip, + shorthands, } from '@fluentui/react-components'; import type { TabValue } from '@fluentui/react-components'; -import { getAuditMode, setAuditMode, getBackupZip, recoverBackup, getPicLinks, deletePic, type PicLink, getPendingReports, approveReport, rejectReport, type PendingReport, getAdminPostInfo, getPendingPosts, getRejectedPosts, type AdminPostListItem, approvePost, disapprovePost, reauditPost, deletePost } from '../admin_api'; +import { getAuditMode, setAuditMode, getBackupZip, recoverBackup, getPicLinks, deletePic, type PicLink, getPendingReports, approveReport, rejectReport, type PendingReport, getAdminPostInfo, getPendingPosts, getRejectedPosts, type AdminPostListItem, approvePost, disapprovePost, reauditPost, deletePost, modifyPost, + getBannedKeywords, setBannedKeywordsList } from '../admin_api'; import { Switch } from '@fluentui/react-components'; import { toast } from 'react-hot-toast'; import { SignOut24Regular, WeatherSunny24Regular, - WeatherMoon24Regular + WeatherMoon24Regular, + Dismiss12Regular, + Add20Regular, + Save20Regular, + QuestionCircle20Regular, } from '@fluentui/react-icons'; import { adminLogout } from '../admin_api'; import { SITE_TITLE } from '../config'; @@ -105,6 +113,40 @@ const useStyles = makeStyles({ alignItems: 'center', zIndex: 999, }, + bannedRow: { + display: 'flex', + alignItems: 'center', + flexWrap: 'wrap', + gap: tokens.spacingHorizontalS, + ...shorthands.padding(tokens.spacingVerticalS, 0), + }, + chip: { + display: 'inline-flex', + alignItems: 'center', + gap: tokens.spacingHorizontalXS, + backgroundColor: tokens.colorNeutralBackground1, + ...shorthands.border('1px', 'solid', tokens.colorNeutralStroke1), + ...shorthands.borderRadius(tokens.borderRadiusLarge), + ...shorthands.padding(tokens.spacingVerticalXS, tokens.spacingHorizontalS), + }, + chipText: { + fontSize: tokens.fontSizeBase300, + lineHeight: '20px', + }, + chipDismiss: { + cursor: 'pointer', + color: tokens.colorNeutralForeground3, + }, + addInput: { + width: '220px', + }, + dashedAdd: { + backgroundColor: tokens.colorNeutralBackground1, + ...shorthands.border('1px', 'dashed', tokens.colorNeutralStroke1), + }, + fileInputHidden: { + display: 'none', + }, }); interface AdminDashboardProps { @@ -163,6 +205,14 @@ const AdminDashboard: React.FC = ({ // 评论管理弹窗 const [manageCommentsModal, setManageCommentsModal] = React.useState<{ open: boolean; id?: number }>({ open: false }); + // 违禁词状态 + const [bannedKeywords, setBannedKeywords] = React.useState([]); + const [newKeyword, setNewKeyword] = React.useState(''); + const [bannedLoading, setBannedLoading] = React.useState(false); + const [bannedSaving, setBannedSaving] = React.useState(false); + const fileImportRef = React.useRef(null); + const [importing, setImporting] = React.useState(false); + React.useEffect(() => { if (activeTab === 'systemSettings') { setLoadingAudit(true); @@ -172,7 +222,7 @@ const AdminDashboard: React.FC = ({ }) .catch((err: any) => { console.error(err); - const msg = String(err?.message || ''); + const msg = String(err?.message || '获取审核模式失败'); if (msg.includes('401') || msg.includes('403') || msg.includes('登录已过期')) { toast.error('身份验证失败,请重新登陆'); } else { @@ -180,6 +230,21 @@ const AdminDashboard: React.FC = ({ } }) .finally(() => setLoadingAudit(false)); + + // 加载违禁词 + setBannedLoading(true); + getBannedKeywords() + .then((list) => setBannedKeywords(Array.isArray(list) ? list : [])) + .catch((err: any) => { + console.error(err); + const msg = String(err?.message || '获取违禁词失败'); + if (msg.includes('401') || msg.includes('403') || msg.includes('登录已过期')) { + toast.error('身份验证失败,请重新登陆'); + } else { + toast.error('获取违禁词失败'); + } + }) + .finally(() => setBannedLoading(false)); } else if (activeTab === 'imageManage') { setPicLoading(true); getPicLinks(picPage) @@ -353,6 +418,62 @@ const AdminDashboard: React.FC = ({ } }; + // 违禁词操作 + const handleAddKeyword = () => { + const raw = newKeyword.trim(); + if (!raw) return; + const parts = raw.split(/[,,\s]+/).map(x => x.trim()).filter(Boolean); + const set = new Set([...bannedKeywords.map(x => x.trim()), ...parts]); + setBannedKeywords([...set]); + setNewKeyword(''); + }; + + const handleRemoveKeyword = (word: string) => { + setBannedKeywords(prev => prev.filter(x => x !== word)); + }; + + const handleSaveKeywords = async () => { + try { + setBannedSaving(true); + await setBannedKeywordsList(bannedKeywords); + toast.success('已保存违禁词列表'); + } catch (e: any) { + const msg = String(e?.message || '保存失败'); + if (msg.includes('401') || msg.includes('403') || msg.includes('登录已过期')) { + toast.error('身份验证失败,请重新登陆'); + } else { + toast.error(msg); + } + } finally { + setBannedSaving(false); + } + }; + + const handleClickImportFile = () => { + fileImportRef.current?.click(); + }; + + const handleImportFromText = async (file: File) => { + try { + setImporting(true); + const text = await file.text(); + const parts = text + .split(/[\n\r\t,,;;\s]+/) + .map(s => s.trim()) + .filter(Boolean); + const set = new Set([...bannedKeywords.map(x => x.trim()), ...parts]); + const added = Math.max(0, [...set].length - bannedKeywords.length); + setBannedKeywords([...set]); + toast.success(`已导入 ${added} 个违禁词`); + } catch (e: any) { + const msg = String(e?.message || '读取文件失败'); + toast.error(msg.includes('Failed') ? '读取文件失败,请确认为TXT文本' : msg); + } finally { + setImporting(false); + if (fileImportRef.current) fileImportRef.current.value = ''; + } + }; + const handleCreateBackup = async () => { try { const { blob, filename } = await getBackupZip(); @@ -551,6 +672,8 @@ const AdminDashboard: React.FC = ({ {activeTab === 'systemSettings' ? (
系统设置 + + {/* 审核开关 */}
新文章是否需要审核
@@ -565,6 +688,59 @@ const AdminDashboard: React.FC = ({
+ {/* 违禁词设置 */} +
+ 违禁词 +
+ {bannedKeywords.map((word) => ( + + {word} + handleRemoveKeyword(word)}> + + + + ))} + setNewKeyword((e.target as HTMLInputElement).value)} + className={styles.addInput} + /> + + { + const f = e.target.files?.[0]; + if (f) void handleImportFromText(f); + }} + disabled={bannedLoading || importing} + /> + + + + 分隔符支持:换行、逗号(,)、中文逗号(,)、分号(;)、中文分号(;)、空格、制表符。
+ 示例:垃圾, 你妈;nm 月吗
+ 约吗 + + )} + > +
+
+ {/* 备份 */}
备份 diff --git a/src/components/AdminPage.tsx b/src/components/AdminPage.tsx index 8c3f142..f3eac06 100644 --- a/src/components/AdminPage.tsx +++ b/src/components/AdminPage.tsx @@ -4,7 +4,12 @@ import AdminLogin from './AdminLogin'; import AdminDashboard from './AdminDashboard'; import { Toaster } from 'react-hot-toast'; -const AdminPage: React.FC = () => { +interface AdminPageProps { + isDarkMode: boolean; + onToggleTheme: () => void; +} + +const AdminPage: React.FC = ({ isDarkMode, onToggleTheme }) => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -34,7 +39,7 @@ const AdminPage: React.FC = () => { return ( <> {isLoggedIn ? ( - + ) : ( )}