From 462650478bc7152bf62fe577e4379a6baee6f69c Mon Sep 17 00:00:00 2001 From: LeonspaceX Date: Sun, 7 Dec 2025 18:34:11 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=E5=A2=9E=E5=8A=A0=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=8C=89=E9=92=AE=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E5=85=AC=E5=91=8A=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 54 ++- src/admin_api.tsx | 27 +- src/api.ts | 571 +++++++++++++++--------------- src/components/AdminDashboard.tsx | 125 ++++++- src/components/NoticeModal.tsx | 190 +++++----- 5 files changed, 573 insertions(+), 394 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index db97e69..b87eaa9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom'; import PostCard from './components/PostCard'; import MainLayout from './layouts/MainLayout'; import './App.css'; -import { fetchArticles } from './api'; +import { fetchArticles, getNotice } from './api'; import CreatePost from './components/CreatePost'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -18,6 +18,8 @@ import AdminPage from './components/AdminPage'; import InitPage from './pages/InitPage'; import NotFound from './pages/NotFound'; import ImageViewer from './components/ImageViewer'; +import NoticeModal from './components/NoticeModal'; +import type { NoticeData } from './components/NoticeModal'; function App() { const [isDarkMode, setIsDarkMode] = React.useState(() => { @@ -50,6 +52,30 @@ function App() { const lastRefreshAtRef = useRef(0); const REFRESH_COOLDOWN_MS = 5000; // 刷新冷却时间 const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false }); + const [noticeData, setNoticeData] = useState(null); + const [showNotice, setShowNotice] = useState(false); + + useEffect(() => { + getNotice().then(data => { + // Check display status + if (data.display === 'false') { + setShowNotice(false); + return; + } + + const savedVersion = localStorage.getItem('notice_version'); + // 只有当有内容且版本号大于本地存储的版本时才显示 + if (data.content && (!savedVersion || Number(savedVersion) < Number(data.version))) { + setNoticeData({ + type: data.type, + content: data.content, + version: Number(data.version), + display: data.display + }); + setShowNotice(true); + } + }).catch(console.error); + }, []); const openImageViewer = (src?: string, alt?: string) => { if (!src) return; @@ -209,6 +235,29 @@ function App() { {imageViewer.open && imageViewer.src && ( )} + {showNotice && noticeData && ( +
+ setShowNotice(false)} + onNeverShow={(version) => { + localStorage.setItem('notice_version', String(version)); + setShowNotice(false); + }} + /> +
+ )} ); } @@ -218,3 +267,6 @@ export default App; + + + diff --git a/src/admin_api.tsx b/src/admin_api.tsx index cd733ba..bf5f859 100644 --- a/src/admin_api.tsx +++ b/src/admin_api.tsx @@ -185,17 +185,30 @@ export const adminApiRequest = async ( * 修改公告(后端自动递增版本) * POST /admin/modify_notice { type: 'md' | 'url', content: string } */ -export const adminModifyNotice = async ( - payload: { type: 'md' | 'url'; content: string } -): Promise<{ status: 'OK'; version?: number }> => { +export const adminModifyNotice = async (type: 'md'|'url', content: string, version: number): Promise => { const resp = await adminApiRequest('/modify_notice', { method: 'POST', - body: JSON.stringify(payload), + body: JSON.stringify({ type, content, version }), }); if (!resp.ok) { - throw new Error(`修改公告失败: ${resp.status}`); + const err = await resp.json().catch(() => ({})); + throw new Error(err.reason || `修改公告失败: ${resp.status}`); + } +}; + +/** + * 切换公告开启状态 + * POST /admin/notice_switch { value: "true" | "false" } + */ +export const adminNoticeSwitch = async (value: boolean): Promise => { + const resp = await adminApiRequest('/notice_switch', { + method: 'POST', + body: JSON.stringify({ value: value ? "true" : "false" }), + }); + if (!resp.ok) { + const err = await resp.json().catch(() => ({})); + throw new Error(err.reason || `切换公告状态失败: ${resp.status}`); } - return resp.json(); }; /** @@ -826,4 +839,4 @@ export const setBannedKeywordsList = async (keywords: string[]): Promise<{ statu throw new Error(`保存违禁词失败: ${resp.status}${detail ? ` - ${detail}` : ''}`); } return resp.json(); -}; +}; \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 4ddd406..03fda6e 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,284 +1,287 @@ -import { API_CONFIG } from './config'; -import { toast } from 'react-hot-toast'; - -export interface Article { - id: number; - content: string; - upvotes: number; - downvotes: number; -} - -export const fetchArticles = async (page: number, signal?: AbortSignal): Promise => { - try { - const response = await fetch(`${API_CONFIG.BASE_URL}/get/10_info?page=${page}`, { signal }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - if (error instanceof Error && error.name !== 'AbortError') { - console.error('Error fetching articles:', error); - } - throw error; - } -}; - -export const voteArticle = async ( - id: number, - type: 'up' | 'down' -): Promise => { - try { - const response = await fetch(`${API_CONFIG.BASE_URL}/${type}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ id }), - }); - if (response.status === 403) { - const data = await response.json().catch(() => null); - if (data?.reason === 'Rate Limit Exceeded') { - toast.error('Rate Limit Exceeded'); - throw new Error('Rate Limit Exceeded'); - } - } - const data = await response.json(); - if (data.status !== 'OK') { - throw new Error(`Vote ${type} failed`); - } - } catch (error) { - toast.error(`点${type === 'up' ? '赞' : '踩'}失败`); - throw error; - } -}; - -interface SubmitPostResponse { - id: string; - status: "Pass" | "Pending" | "Deny"; - message?: string; -} - -export const submitPost = async (postData: { content: string }): Promise => { - try { - const response = await fetch(`${API_CONFIG.BASE_URL}/post`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ content: postData.content }), - }); - - if (response.status === 403) { - const data = await response.json().catch(() => null); - if (data?.reason === 'Rate Limit Exceeded') { - toast.error('Rate Limit Exceeded'); - throw new Error('Rate Limit Exceeded'); - } - return { status: 'Deny', message: '投稿中包含违禁词', id: 'null'}; - } - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return await response.json() as SubmitPostResponse; - } catch (error) { - console.error('Error submitting post:', error); - throw error; - } -}; - -export const uploadImage = async (formData: FormData): Promise<{ status: 'OK' | 'Error'; url?: string; message?: string }> => { - try { - const response = await fetch(`${API_CONFIG.BASE_URL}/upload_pic`, { - method: 'POST', - body: formData, - }); - if (response.status === 403) { - const data = await response.json().catch(() => null); - if (data?.reason === 'Rate Limit Exceeded') { - toast.error('Rate Limit Exceeded'); - throw new Error('Rate Limit Exceeded'); - } - } - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const result = await response.json(); - if (result.url) { - result.url = `${API_CONFIG.BASE_URL}${result.url}`; - } - - return result; - } catch (error) { - console.error('Error uploading image:', error); - throw error; - } -}; - -interface ReportPostResponse { - id: number; - status: string; -} - -export const reportPost = async (reportData: { id: number; title: string; content: string }): Promise => { - const response = await fetch(`${API_CONFIG.BASE_URL}/report`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(reportData), - }); - if (response.status === 403) { - const data = await response.json().catch(() => null); - if (data?.reason === 'Rate Limit Exceeded') { - toast.error('Rate Limit Exceeded'); - throw new Error('Rate Limit Exceeded'); - } - } - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return (await response.json()) as ReportPostResponse; -}; - -export async function getPostState(id: string): Promise<{ status: string }> { - const response = await fetch(`${API_CONFIG.BASE_URL}/get/post_state?id=${id}`); - if (!response.ok) { - throw new Error('Failed to fetch post state'); - } - return response.json(); -} - -export async function getReportState(id: string): Promise<{ status: string }> { - const response = await fetch(`${API_CONFIG.BASE_URL}/get/report_state?id=${id}`); - if (!response.ok) { - throw new Error('Failed to fetch report state'); - } - return response.json(); -} - - -export interface Comment { - id: number; - nickname: string; - content: string; - parent_comment_id: number; -} - -export const getComments = async (id: string | number): Promise => { - try { - const response = await fetch(`${API_CONFIG.BASE_URL}/get/comment?id=${id}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error('Error fetching comments:', error); - throw error; - } -}; - -export interface PostCommentRequest { - content: string; - submission_id: number; - parent_comment_id: number; - nickname: string; -} - -export interface PostCommentResponse { - id: number; - status: string; -} - -export const postComment = async (commentData: PostCommentRequest): Promise => { - try { - const response = await fetch(`${API_CONFIG.BASE_URL}/comment`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(commentData), - }); - - if (response.status === 403) { - const data = await response.json().catch(() => null); - if (data?.reason === 'Rate Limit Exceeded') { - toast.error('Rate Limit Exceeded'); - throw new Error('Rate Limit Exceeded'); - } - throw new Error('评论包含违禁词'); - } - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return await response.json(); - } catch (error) { - console.error('Error posting comment:', error); - throw error; - } -}; - -// === Backend Initialization === -export interface InitPayload { - adminToken: string; - uploadFolder: string; - allowedExtensions: string[]; - maxFileSize: number; - bannedKeywords?: string[]; - rateLimit: number; // 次/分钟,0为无限制 -} - -export const initBackend = async (payload: InitPayload): Promise<{ status: string; reason?: string }> => { - const body = { - ADMIN_TOKEN: payload.adminToken, - UPLOAD_FOLDER: payload.uploadFolder, - ALLOWED_EXTENSIONS: payload.allowedExtensions, - MAX_FILE_SIZE: payload.maxFileSize, - ...(payload.bannedKeywords ? { BANNED_KEYWORDS: payload.bannedKeywords } : {}), - RATE_LIMIT: payload.rateLimit, - }; - - const response = await fetch(`${API_CONFIG.BASE_URL}/init`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - const data = await response.json().catch(() => ({ status: 'Fail', reason: 'Invalid response' })); - - if (response.status === 403) { - throw new Error(data?.reason || '后端已初始化'); - } - if (!response.ok) { - throw new Error(data?.reason || `初始化失败,状态码 ${response.status}`); - } - - return data as { status: string; reason?: string }; -}; - -// === Notice === -export interface NoticeResponse { - type: 'md' | 'url'; - content: string; - version: string | number; -} - -export const getNotice = async (): Promise => { - const response = await fetch(`${API_CONFIG.BASE_URL}/get/notice`, { method: 'GET' }); - if (!response.ok) { - throw new Error(`获取公告失败: ${response.status}`); - } - const data = await response.json(); - return { - type: (data?.type === 'url' ? 'url' : 'md') as 'md' | 'url', - content: String(data?.content ?? ''), - version: data?.version ?? '0', - }; -}; +import { API_CONFIG } from './config'; +import { toast } from 'react-hot-toast'; + +export interface Article { + id: number; + content: string; + upvotes: number; + downvotes: number; +} + +export const fetchArticles = async (page: number, signal?: AbortSignal): Promise => { + try { + const response = await fetch(`${API_CONFIG.BASE_URL}/get/10_info?page=${page}`, { signal }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + if (error instanceof Error && error.name !== 'AbortError') { + console.error('Error fetching articles:', error); + } + throw error; + } +}; + +export const voteArticle = async ( + id: number, + type: 'up' | 'down' +): Promise => { + try { + const response = await fetch(`${API_CONFIG.BASE_URL}/${type}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id }), + }); + if (response.status === 403) { + const data = await response.json().catch(() => null); + if (data?.reason === 'Rate Limit Exceeded') { + toast.error('Rate Limit Exceeded'); + throw new Error('Rate Limit Exceeded'); + } + } + const data = await response.json(); + if (data.status !== 'OK') { + throw new Error(`Vote ${type} failed`); + } + } catch (error) { + toast.error(`点${type === 'up' ? '赞' : '踩'}失败`); + throw error; + } +}; + +interface SubmitPostResponse { + id: string; + status: "Pass" | "Pending" | "Deny"; + message?: string; +} + +export const submitPost = async (postData: { content: string }): Promise => { + try { + const response = await fetch(`${API_CONFIG.BASE_URL}/post`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content: postData.content }), + }); + + if (response.status === 403) { + const data = await response.json().catch(() => null); + if (data?.reason === 'Rate Limit Exceeded') { + toast.error('Rate Limit Exceeded'); + throw new Error('Rate Limit Exceeded'); + } + return { status: 'Deny', message: '投稿中包含违禁词', id: 'null'}; + } + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json() as SubmitPostResponse; + } catch (error) { + console.error('Error submitting post:', error); + throw error; + } +}; + +export const uploadImage = async (formData: FormData): Promise<{ status: 'OK' | 'Error'; url?: string; message?: string }> => { + try { + const response = await fetch(`${API_CONFIG.BASE_URL}/upload_pic`, { + method: 'POST', + body: formData, + }); + if (response.status === 403) { + const data = await response.json().catch(() => null); + if (data?.reason === 'Rate Limit Exceeded') { + toast.error('Rate Limit Exceeded'); + throw new Error('Rate Limit Exceeded'); + } + } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + if (result.url) { + result.url = `${API_CONFIG.BASE_URL}${result.url}`; + } + + return result; + } catch (error) { + console.error('Error uploading image:', error); + throw error; + } +}; + +interface ReportPostResponse { + id: number; + status: string; +} + +export const reportPost = async (reportData: { id: number; title: string; content: string }): Promise => { + const response = await fetch(`${API_CONFIG.BASE_URL}/report`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(reportData), + }); + if (response.status === 403) { + const data = await response.json().catch(() => null); + if (data?.reason === 'Rate Limit Exceeded') { + toast.error('Rate Limit Exceeded'); + throw new Error('Rate Limit Exceeded'); + } + } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return (await response.json()) as ReportPostResponse; +}; + +export async function getPostState(id: string): Promise<{ status: string }> { + const response = await fetch(`${API_CONFIG.BASE_URL}/get/post_state?id=${id}`); + if (!response.ok) { + throw new Error('Failed to fetch post state'); + } + return response.json(); +} + +export async function getReportState(id: string): Promise<{ status: string }> { + const response = await fetch(`${API_CONFIG.BASE_URL}/get/report_state?id=${id}`); + if (!response.ok) { + throw new Error('Failed to fetch report state'); + } + return response.json(); +} + + +export interface Comment { + id: number; + nickname: string; + content: string; + parent_comment_id: number; +} + +export const getComments = async (id: string | number): Promise => { + try { + const response = await fetch(`${API_CONFIG.BASE_URL}/get/comment?id=${id}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Error fetching comments:', error); + throw error; + } +}; + +export interface PostCommentRequest { + content: string; + submission_id: number; + parent_comment_id: number; + nickname: string; +} + +export interface PostCommentResponse { + id: number; + status: string; +} + +export const postComment = async (commentData: PostCommentRequest): Promise => { + try { + const response = await fetch(`${API_CONFIG.BASE_URL}/comment`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(commentData), + }); + + if (response.status === 403) { + const data = await response.json().catch(() => null); + if (data?.reason === 'Rate Limit Exceeded') { + toast.error('Rate Limit Exceeded'); + throw new Error('Rate Limit Exceeded'); + } + throw new Error('评论包含违禁词'); + } + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('Error posting comment:', error); + throw error; + } +}; + +// === Backend Initialization === +export interface InitPayload { + adminToken: string; + uploadFolder: string; + allowedExtensions: string[]; + maxFileSize: number; + bannedKeywords?: string[]; + rateLimit: number; // 次/分钟,0为无限制 +} + +export const initBackend = async (payload: InitPayload): Promise<{ status: string; reason?: string }> => { + const body = { + ADMIN_TOKEN: payload.adminToken, + UPLOAD_FOLDER: payload.uploadFolder, + ALLOWED_EXTENSIONS: payload.allowedExtensions, + MAX_FILE_SIZE: payload.maxFileSize, + ...(payload.bannedKeywords ? { BANNED_KEYWORDS: payload.bannedKeywords } : {}), + RATE_LIMIT: payload.rateLimit, + }; + + const response = await fetch(`${API_CONFIG.BASE_URL}/init`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + const data = await response.json().catch(() => ({ status: 'Fail', reason: 'Invalid response' })); + + if (response.status === 403) { + throw new Error(data?.reason || '后端已初始化'); + } + if (!response.ok) { + throw new Error(data?.reason || `初始化失败,状态码 ${response.status}`); + } + + return data as { status: string; reason?: string }; +}; + +// === Notice === +export interface NoticeResponse { + type: 'md' | 'url'; + content: string; + version: string | number; + display?: string; +} + +export const getNotice = async (): Promise => { + const response = await fetch(`${API_CONFIG.BASE_URL}/get/notice`, { method: 'GET' }); + if (!response.ok) { + throw new Error(`获取公告失败: ${response.status}`); + } + const data = await response.json(); + return { + type: (data?.type === 'url' ? 'url' : 'md') as 'md' | 'url', + content: String(data?.content ?? ''), + version: data?.version ?? '0', + display: data?.display ?? 'true', + }; +}; + \ No newline at end of file diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx index 34cdf95..0e614f6 100644 --- a/src/components/AdminDashboard.tsx +++ b/src/components/AdminDashboard.tsx @@ -21,7 +21,7 @@ import { } 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, - getBannedKeywords, setBannedKeywordsList } from '../admin_api'; + getBannedKeywords, setBannedKeywordsList, adminNoticeSwitch } from '../admin_api'; import { Switch } from '@fluentui/react-components'; import { toast } from 'react-hot-toast'; import { @@ -182,6 +182,7 @@ const AdminDashboard: React.FC = ({ const fileInputRef = React.useRef(null); const [selectedBackupFile, setSelectedBackupFile] = React.useState(null); const [confirmOpen, setConfirmOpen] = React.useState(false); + const [clearCacheConfirmOpen, setClearCacheConfirmOpen] = React.useState(false); // 图片管理状态 const [picPage, setPicPage] = React.useState(1); const [picLoading, setPicLoading] = React.useState(false); @@ -233,6 +234,7 @@ const AdminDashboard: React.FC = ({ const [noticeVersion, setNoticeVersion] = React.useState(0); const [noticeType, setNoticeType] = React.useState<'md' | 'url'>('md'); const [noticeContent, setNoticeContent] = React.useState(''); + const [noticeDisplay, setNoticeDisplay] = React.useState(true); React.useEffect(() => { if (activeTab === 'systemSettings') { @@ -298,21 +300,47 @@ const AdminDashboard: React.FC = ({ setNoticeLoading(true); getNotice() .then((data) => { - const ver = Number(data.version ?? 0) || 0; - setNoticeVersion(ver); + setNoticeVersion(Number(data.version ?? 0) || 0); setNoticeType((data.type === 'url' ? 'url' : 'md')); setNoticeContent(String(data.content ?? '')); + setNoticeDisplay(data.display !== 'false'); }) .catch((err: any) => { console.error(err); - const msg = String(err?.message || '获取公告失败'); + toast.error('获取公告失败'); + }) + .finally(() => setNoticeLoading(false)); + } else if (activeTab === 'postReview') { + setLoadingAudit(true); + getAuditMode() + .then(data => { + setNeedAudit(!!data.status); + }) + .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('获取公告失败'); + toast.error('获取审核模式失败'); } }) - .finally(() => setNoticeLoading(false)); + .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)); } }, [activeTab, picPage]); @@ -569,6 +597,38 @@ const AdminDashboard: React.FC = ({ void handleRecoverBackup(selectedBackupFile); }; + const handleClearCache = async () => { + // Clear localStorage + localStorage.clear(); + + // Clear sessionStorage + sessionStorage.clear(); + + // Clear cookies + document.cookie.split(";").forEach((c) => { + document.cookie = c + .replace(/^ +/, "") + .replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/"); + }); + + // Clear caches + if ('caches' in window) { + try { + const names = await caches.keys(); + await Promise.all(names.map(name => caches.delete(name))); + } catch (e) { + console.error('Failed to clear caches:', e); + } + } + + toast.success('缓存已清理'); + setClearCacheConfirmOpen(false); + + setTimeout(() => { + window.location.reload(); + }, 1500); + }; + const handleLogout = () => { try { adminLogout(); @@ -713,6 +773,25 @@ const AdminDashboard: React.FC = ({ {activeTab === 'noticeManage' ? (
公告管理 + +
+
+ { + try { + await adminNoticeSwitch(!!data.checked); + setNoticeDisplay(!!data.checked); + toast.success(`公告已${data.checked ? '开启' : '关闭'}`); + } catch (e: any) { + toast.error(e.message || '切换状态失败'); + } + }} + label={noticeDisplay ? "公告已开启" : "公告已关闭"} + /> +
+
+
当前公告版本:{noticeLoading ? '加载中...' : noticeVersion}
@@ -752,8 +831,7 @@ const AdminDashboard: React.FC = ({
+ {/* 开发者工具 */} +
+ 开发者工具 +
+ +
+ + 将清理 localStorage、Cookies 及静态资源缓存。 + +
+ {/* 确认对话框 */} setConfirmOpen(!!data.open)}> @@ -892,6 +983,22 @@ const AdminDashboard: React.FC = ({ + + {/* 清除缓存确认对话框 */} + setClearCacheConfirmOpen(!!data.open)}> + + + 确认清除缓存 + + 确定要清除所有缓存吗?这将包括登录状态和本地设置。 + + + + + + + +
) : activeTab === 'postReview' ? (
@@ -1167,3 +1274,5 @@ const AdminDashboard: React.FC = ({ }; export default AdminDashboard; + + diff --git a/src/components/NoticeModal.tsx b/src/components/NoticeModal.tsx index a58b376..4740201 100644 --- a/src/components/NoticeModal.tsx +++ b/src/components/NoticeModal.tsx @@ -1,94 +1,96 @@ -import React from 'react'; -import { makeStyles, Button, tokens, Text } from '@fluentui/react-components'; -import { Dismiss24Regular } from '@fluentui/react-icons'; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm'; -import remarkIns from 'remark-ins'; -import remarkBreaks from 'remark-breaks'; - -const useStyles = makeStyles({ - modalContent: { - backgroundColor: tokens.colorNeutralBackground1, - padding: tokens.spacingHorizontalXXL, - borderRadius: tokens.borderRadiusXLarge, - boxShadow: tokens.shadow64, - display: 'flex', - flexDirection: 'column', - gap: tokens.spacingVerticalM, - width: '520px', - maxWidth: '90vw', - position: 'relative', - }, - closeButton: { - position: 'absolute', - top: tokens.spacingVerticalS, - right: tokens.spacingHorizontalS, - }, - title: { - fontSize: tokens.fontSizeBase500, - fontWeight: tokens.fontWeightSemibold, - marginBottom: tokens.spacingVerticalS, - }, - contentBox: { - maxHeight: '60vh', - overflowY: 'auto', - }, - iframe: { - width: '100%', - height: '360px', - border: 'none', - borderRadius: tokens.borderRadiusMedium, - backgroundColor: tokens.colorNeutralBackground2, - }, - actions: { - display: 'flex', - justifyContent: 'flex-end', - gap: tokens.spacingHorizontalS, - }, -}); - -export interface NoticeData { - type: 'md' | 'url'; - content: string; - version: number; -} - -interface NoticeModalProps { - data: NoticeData; - onClose: () => void; - onNeverShow: (version: number) => void; -} - -const NoticeModal: React.FC = ({ data, onClose, onNeverShow }) => { - const styles = useStyles(); - const { type, content, version } = data; - - return ( -
-