import { useEffect, type CSSProperties } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import DOMPurify from 'dompurify'; import { Sidebar } from '../components/Sidebar'; import { useNotificationStore } from '../store/notification'; import type { AppNotification, NotificationType } from '../types'; // 每条通知顶部标签 — 4 色一目了然 const TYPE_CHIP: Record = { announcement: { text: '公告', color: 'var(--color-primary)', bg: 'var(--color-primary-bg, rgba(0,184,230,0.12))' }, anomaly_disabled_user: { text: '安全', color: 'var(--color-danger)', bg: 'rgba(231,76,60,0.12)' }, anomaly_disabled_team: { text: '安全', color: 'var(--color-danger)', bg: 'rgba(231,76,60,0.12)' }, quota_warning: { text: '额度', color: '#faad14', bg: 'rgba(250,173,20,0.12)' }, system: { text: '系统', color: 'var(--color-text-tertiary)', bg: 'var(--color-bg-elevated)' }, }; function formatRelative(iso: string): string { const ts = new Date(iso).getTime(); if (Number.isNaN(ts)) return ''; const ms = Date.now() - ts; if (ms < 0) return '刚刚'; const sec = Math.floor(ms / 1000); if (sec < 60) return '刚刚'; if (sec < 3600) return `${Math.floor(sec / 60)} 分钟前`; if (sec < 86400) return `${Math.floor(sec / 3600)} 小时前`; if (sec < 86400 * 7) return `${Math.floor(sec / 86400)} 天前`; return new Date(iso).toLocaleDateString('zh-CN'); } const styles: Record = { layout: { display: 'flex', height: '100%', position: 'relative', zIndex: 2, }, main: { flex: 1, overflowY: 'auto', padding: '24px 32px 60px', background: 'var(--color-bg-page)', }, container: { maxWidth: 800, margin: '0 auto', }, header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 20, }, title: { fontSize: 20, fontWeight: 600, color: 'var(--color-text-primary)', margin: 0, }, markAllBtn: { padding: '6px 14px', background: 'transparent', border: '1px solid var(--color-border-card)', borderRadius: 'var(--radius-btn, 6px)', color: 'var(--color-text-secondary)', fontSize: 13, cursor: 'pointer', transition: 'background 0.15s, color 0.15s', }, list: { background: 'var(--color-bg-card)', border: '1px solid var(--color-border-modal-soft)', borderRadius: 8, overflow: 'hidden', }, row: { display: 'flex', gap: 12, padding: '14px 18px', borderBottom: '1px solid var(--color-border-modal-soft)', cursor: 'pointer', transition: 'background 0.15s', position: 'relative', }, rowLast: { borderBottom: 'none', }, unreadDot: { width: 8, height: 8, borderRadius: '50%', background: 'var(--color-primary)', flexShrink: 0, marginTop: 7, }, dotPlaceholder: { width: 8, height: 8, flexShrink: 0, }, rowContent: { flex: 1, minWidth: 0, }, rowHead: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, marginBottom: 4, }, rowTitle: { fontSize: 14, fontWeight: 600, color: 'var(--color-text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, rowTitleRead: { fontWeight: 500, color: 'var(--color-text-light)', }, rowTime: { fontSize: 12, color: 'var(--color-text-tertiary)', flexShrink: 0, whiteSpace: 'nowrap', }, rowBody: { fontSize: 13, color: 'var(--color-text-secondary)', lineHeight: 1.5, wordBreak: 'break-word', }, empty: { padding: '80px 0', textAlign: 'center', color: 'var(--color-text-tertiary)', fontSize: 14, }, loading: { padding: '60px 0', textAlign: 'center', color: 'var(--color-text-secondary)', fontSize: 14, }, pagination: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 16, marginTop: 20, color: 'var(--color-text-secondary)', fontSize: 13, }, pageBtn: { width: 28, height: 28, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', border: '1px solid var(--color-border-card)', background: 'transparent', borderRadius: 6, cursor: 'pointer', color: 'var(--color-text-secondary)', padding: 0, }, pageBtnDisabled: { cursor: 'not-allowed', opacity: 0.4, }, backBtn: { display: 'inline-flex', alignItems: 'center', gap: 6, padding: '6px 14px', background: 'transparent', border: '1px solid var(--color-border-card)', borderRadius: 'var(--radius-btn, 6px)', color: 'var(--color-text-secondary)', fontSize: 13, cursor: 'pointer', marginRight: 12, }, }; interface NotificationRowProps { item: AppNotification; isLast: boolean; onClick: (item: AppNotification) => void; } function NotificationRow({ item, isLast, onClick }: NotificationRowProps) { const rowStyle: CSSProperties = { ...styles.row, ...(isLast ? styles.rowLast : {}), background: item.is_read ? 'transparent' : 'var(--color-primary-bg, transparent)', }; const titleStyle: CSSProperties = { ...styles.rowTitle, ...(item.is_read ? styles.rowTitleRead : {}), }; const chip = TYPE_CHIP[item.type] || TYPE_CHIP.system; const isAnnouncement = item.type === 'announcement'; return (
onClick(item)} onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--color-bg-hover)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = item.is_read ? 'transparent' : 'var(--color-primary-bg, transparent)'; }} > {item.is_read ? (
) : (
)}
{chip.text} {item.title}
{formatRelative(item.created_at)}
{isAnnouncement ? (
) : (
{item.content}
)}
); } export function NotificationsPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const unreadOnly = searchParams.get('unread_only') === 'true'; const list = useNotificationStore((s) => s.list); const total = useNotificationStore((s) => s.total); const page = useNotificationStore((s) => s.page); const pageSize = useNotificationStore((s) => s.pageSize); const unreadCount = useNotificationStore((s) => s.unreadCount); const loading = useNotificationStore((s) => s.loading); const fetchList = useNotificationStore((s) => s.fetchList); const markRead = useNotificationStore((s) => s.markRead); const markAllRead = useNotificationStore((s) => s.markAllRead); // 首次加载 + URL 切换时拉第一页 useEffect(() => { fetchList({ page: 1, unread_only: unreadOnly }); }, [fetchList, unreadOnly]); const totalPages = Math.max(1, Math.ceil(total / Math.max(1, pageSize))); const goPage = (p: number) => { if (p < 1 || p > totalPages || p === page) return; fetchList({ page: p, unread_only: unreadOnly }); }; const handleRowClick = async (item: AppNotification) => { if (!item.is_read) { markRead(item.id); } if (item.link_url) { navigate(item.link_url); } }; return (

消息中心

{unreadCount > 0 && ( )}
{loading && list.length === 0 ? (
加载中...
) : list.length === 0 ? (
暂无消息
) : ( <>
{list.map((item, idx) => ( ))}
{totalPages > 1 && (
第 {page} / {totalPages} 页
)} )}
); }