Files
ele-HeiXiu/src/components/navbar/AnnouncementDrawer.tsx
YoungestSongMo 7032d1c3f2 feat: 忘记密码/修改密码 + 设置页结构重组 + barrel cleanup + LoginPage 版本统一
新增:
  - ForgotPasswordModal — 手机号+短信验证码重置密码 (AuthAPI.resetPassword)
  - ChangePasswordModal — 当前密码验证后修改密码 (AuthAPI.changePassword)
  - useSmsCooldown Hook — 通用短信发送冷却逻辑,供注册/忘记密码复用
  - PasswordSetting — 设置页账号安全区块,双选项:修改密码 / 短信验证码重置

  修改:
  - LoginForm \"忘记密码?\" 链接接入 ForgotPasswordModal(替换 TODO)
  - LoginPage 移除 edition 业务版本 UI 区分,登录/注册 Tab 全局统一
  - 设置页 Section 组件移入 blocks/ 子目录(git mv 保留历史)
  - auth.ts sendSms URL 修正: /sms/send → /send-sms

  清理:
  - settings/index.ts 移除 4 个未使用 re-export,精简为仅 SettingsPage
  - navbar/index.ts 移除未使用 re-export,精简为仅 NavBar

  文档:
  - WORKLOG.md 新增 2026-06-08 工作日志
  - ARCHITECTURE.md / PROJECT.md 更新设置页目录结构与新组件
  - TODO.md 忘记密码流程标记已完成 + 短信人机验证列入后期优化
2026-06-08 11:57:35 +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, AnnouncementListResponse} from '@/services/modules';
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>
</>
);
}