import React, { useState, useEffect } from 'react'; import { List, Typography, Menu, Layout, Button, Empty, Spin, Space } from 'antd'; import { BellOutlined, DollarOutlined, HomeOutlined, TeamOutlined, SettingOutlined, } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import api from '../../api'; import { message } from '../../ui/app-message'; import { formatNotificationText } from '../../utils/notification-display'; const { Sider, Content } = Layout; interface NotificationItem { id: number; type: string; title: string; content: string; link: string | null; isRead: boolean; createdAt: string; } const typeMap: Record = { bill_generated: { label: '账单', icon: }, bill_paid: { label: '账单', icon: }, check_in: { label: '入住', icon: }, check_out: { label: '退宿', icon: }, deposit_due: { label: '押金', icon: }, deposit_refunded: { label: '押金', icon: }, class_change: { label: '班级', icon: }, schedule_conflict: { label: '排课', icon: }, announcement: { label: '公告', icon: }, }; function timeAgo(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return '刚刚'; if (mins < 60) return `${mins}分钟前`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}小时前`; const days = Math.floor(hours / 24); if (days < 7) return `${days}天前`; return new Date(dateStr).toLocaleDateString('zh-CN'); } const NotificationsPage: React.FC = () => { const [notifications, setNotifications] = useState([]); const [filter, setFilter] = useState('all'); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const fetchData = async () => { setLoading(true); try { const data = await api.get('/notifications?limit=50') as unknown as NotificationItem[]; setNotifications(data); } catch (e: any) { console.error('加载通知失败', e); message.error(e?.message || '加载通知失败'); } setLoading(false); }; useEffect(() => { fetchData(); }, []); const handleClick = async (item: NotificationItem) => { if (!item.isRead) { try { await api.put(`/notifications/${item.id}/read`); setNotifications((prev) => prev.map((n) => (n.id === item.id ? { ...n, isRead: true } : n)), ); } catch (e: any) { console.error('标记已读失败', e); message.error(e?.message || '标记已读失败'); } } if (item.link) navigate(item.link); }; const handleMarkAll = async () => { try { await api.put('/notifications/read-all'); setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true })), ); } catch (e: any) { console.error('全部已读失败', e); message.error(e?.message || '操作失败'); } }; const filtered = filter === 'all' ? notifications : notifications.filter((n) => n.type === filter); return ( setFilter(key)} items={[ { key: 'all', icon: , label: '全部' }, { key: 'bill_generated', icon: , label: '账单' }, { key: 'check_in', icon: , label: '入住' }, { key: 'class_change', icon: , label: '班级' }, { key: 'announcement', icon: , label: '公告' }, ]} />
通知中心
{filtered.length === 0 ? ( ) : ( { const meta = typeMap[item.type] || { label: item.type, icon: }; return ( handleClick(item)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(item); } }} style={{ cursor: 'pointer', padding: '16px 0', backgroundColor: item.isRead ? 'transparent' : '#f0f7ff', }} > {meta.icon} } title={ {formatNotificationText(item.title)} {timeAgo(item.createdAt)} } description={ item.content && ( {formatNotificationText(item.content)} ) } /> ); }} /> )}
); }; export default NotificationsPage;