Files
ele-HeiXiu/src/components/navbar/AnnouncementDrawer.tsx
YoungestSongMo 76e66a37c8 refactor: 消除占位页面 + 删除 barrel 导出 — 页面集中注册 + 直接导入
- 删除 8 个占位页面文件(Account/Vip/Recharge/Billing/Orders/Projects/Quota/ComplianceAssets)
- PageDispatcher: PAGE_MAP 支持无组件页面自动渲染 PlaceholderPage
- 新增页面只需在 PAGE_MAP 加一行配置,无需创建独立文件
- 删除 services/modules/index.ts barrel,13 个文件改为直接导入
- IDE 跳转到定义一步到位,import 自文档化
- TypeScript 编译零错误

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:32:14 +08:00

204 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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, type AnnouncementListResponse } from '@/services/modules/announcement';
const {Text, Paragraph} = Typography;
// ---------- 辅助 ----------
/** 公告类型 → Tag 颜色映射 */
const typeColorMap: Record<string, string> = {
system: 'blue',
feature: 'purple',
activity: 'orange',
maintenance: 'red',
};
/** 公告类型 → 中文标签 */
const typeLabelMap: Record<string, string> = {
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<Announcement[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 详情 Modal
const [detail, setDetail] = useState<Announcement | null>(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 (
<>
<Drawer
title={
<span className="flex items-center gap-2">
<NotificationOutlined />
</span>
}
placement="right"
size={380}
open={open}
onClose={onClose}
styles={{body: {padding: 0}}}
>
{loading ? (
<div className="flex items-center justify-center h-64">
<Spin description="加载中..." />
</div>
) : error ? (
<div className="flex items-center justify-center h-64">
<Empty description={error} />
</div>
) : list.length === 0 ? (
<div className="flex items-center justify-center h-64">
<Empty description="暂无公告" />
</div>
) : (
<div className="flex flex-col gap-3 p-3">
{list.map((item:Announcement) => (
<Tooltip
key={item.id}
title={
item.content.length > 100
? item.content.slice(0, 200) + '…'
: undefined
}
placement="left"
>
<Card
size="small"
className="rounded-md! border-0! shadow-sm! cursor-pointer hover:shadow-md! transition-shadow"
title={
<Text strong className="text-sm" ellipsis>
{item.pinned && '📌 '}
{item.title}
</Text>
}
extra={
<Tag
color={typeColorMap[item.type] || 'default'}
className="m-0! text-xs"
>
{typeLabelMap[item.type] || item.type}
</Tag>
}
onClick={() => setDetail(item)}
>
<Paragraph
className="mb-2! text-xs"
type="secondary"
ellipsis={{rows: 2}}
>
{item.content}
</Paragraph>
<Text type="secondary" className="text-xs">
{formatDate(item.created_at)}
</Text>
</Card>
</Tooltip>
))}
</div>
)}
</Drawer>
{/* ======== 公告详情 Modal ======== */}
<Modal
open={!!detail}
onCancel={() => setDetail(null)}
footer={null}
width={480}
centered
title={
detail && (
<span className="flex items-center gap-2">
{detail.pinned && '📌 '}
{detail.title}
</span>
)
}
>
{detail && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Tag
color={typeColorMap[detail.type] || 'default'}
className="m-0! text-xs"
>
{typeLabelMap[detail.type] || detail.type}
</Tag>
<Text type="secondary" className="text-xs">
{formatDate(detail.created_at)}
</Text>
</div>
<div className="px-1 py-2 rounded-md"
style={{
background: 'var(--color-fill-tertiary, rgba(0,0,0,0.04))',
}}
>
<Paragraph className="mb-0! text-sm leading-relaxed whitespace-pre-wrap">
{detail.content}
</Paragraph>
</div>
</div>
)}
</Modal>
</>
);
}