// ============================================================ // AnnouncementDrawer — 公告侧拉抽屉(SPA 内部弹出) // 数据来源:GET /api/v1/announcements // ============================================================ import {useEffect, useState, useCallback} from 'react'; import {Drawer, Card, Typography, Empty, Tag, Spin, Modal, Tooltip} from 'antd'; import {NotificationOutlined} from '@ant-design/icons'; import {fetchAnnouncements, type Announcement, AnnouncementListResponse} from '@/services/modules'; const {Text, Paragraph} = Typography; // ---------- 辅助 ---------- /** 公告类型 → Tag 颜色映射 */ const typeColorMap: Record = { system: 'blue', feature: 'purple', activity: 'orange', maintenance: 'red', }; /** 公告类型 → 中文标签 */ const typeLabelMap: Record = { system: '系统', feature: '功能', activity: '活动', maintenance: '维护', }; /** 格式化日期(YYYY-MM-DD HH:mm) */ function formatDate(iso: string): string { try { return new Date(iso).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } catch { return iso; } } // ---------- 组件 ---------- interface AnnouncementDrawerProps { open: boolean; onClose: () => void; } export function AnnouncementDrawer({open, onClose}: AnnouncementDrawerProps) { const [list, setList] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 详情 Modal const [detail, setDetail] = useState(null); // 打开抽屉时请求公告列表 const loadAnnouncements = useCallback(async () => { setLoading(true); setError(null); try { const data: AnnouncementListResponse = await fetchAnnouncements(); setList(data.filter((item: Announcement) => item.is_active)); } catch (err) { setError((err as Error).message || '加载公告失败'); setList([]); } finally { setLoading(false); } }, []); useEffect(() => { if (open) { loadAnnouncements(); } }, [open, loadAnnouncements]); // ---- 渲染 ---- return ( <> 公告 } placement="right" size={380} open={open} onClose={onClose} styles={{body: {padding: 0}}} > {loading ? (
) : error ? (
) : list.length === 0 ? (
) : (
{list.map((item:Announcement) => ( 100 ? item.content.slice(0, 200) + '…' : undefined } placement="left" > {item.pinned && '📌 '} {item.title} } extra={ {typeLabelMap[item.type] || item.type} } onClick={() => setDetail(item)} > {item.content} {formatDate(item.created_at)} ))}
)}
{/* ======== 公告详情 Modal ======== */} setDetail(null)} footer={null} width={480} centered title={ detail && ( {detail.pinned && '📌 '} {detail.title} ) } > {detail && (
{typeLabelMap[detail.type] || detail.type} {formatDate(detail.created_at)}
{detail.content}
)}
); }