(() => { /** * 文件职责:通知中心视图,负责通知列表展示、已读状态操作与手动刷新交互。 */ const { Box, Typography, Button, Chip } = MaterialUI; const NOTIFICATION_TYPE_META = { // 不同类型通知映射到不同的 UI 标签与颜色语义,便于用户快速区分内容性质。 UPDATE: { label: '更新通知', color: 'info' }, BUGFIX: { label: '修复通知', color: 'success' }, NOTICE: { label: '公告通知', color: 'warning' }, SECURITY: { label: '安全通知', color: 'error' }, TEST: { label: '测试通知', color: 'default' }, }; const NOTIFICATION_TYPE_ALIASES = { // 兼容后端可能输出的别名类型,统一归并后再进入主映射表。 INFO: 'NOTICE', }; function resolveNotificationTypeMeta(type) { const normalizedType = String(type || '').trim().toUpperCase(); const canonicalType = NOTIFICATION_TYPE_ALIASES[normalizedType] || normalizedType; // 即使类型未知,也返回兜底标签,避免前端因为类型缺失出现空白 chip。 return NOTIFICATION_TYPE_META[canonicalType] || { label: '其他通知', color: 'default' }; } function renderNotificationContent(item) { // 通知正文无论来自纯文本还是 Markdown,都先走统一内容规范化,避免换行格式不一致。 const content = window.MarkdownRenderUtil.normalizeMarkdownContent(item?.content || '--'); const format = String(item?.content_format || 'text').trim().toLowerCase(); const shouldRenderMarkdown = format === 'markdown' || window.MarkdownRenderUtil.isProbablyMarkdown(content); if (!shouldRenderMarkdown) { return ( {content} ); } return ( ); } function NotificationsView({ onRefresh }) { const { state, dispatch } = useAppContext(); const api = useApi(); // 兜底保证渲染层始终拿到数组,避免 map 时出现空值异常。 const notifications = Array.isArray(state.notifications) ? state.notifications : []; const notificationsMeta = state.notificationsMeta || { unread_count: 0, last_sync_at: '', total_count: 0 }; const displayTimezone = state.config?.displayTimezone || 'Asia/Shanghai'; // busyAction 用单字符串标记当前进行中的动作,便于按钮粒度控制禁用状态。 const [busyAction, setBusyAction] = React.useState(''); const refreshFromServer = async () => { setBusyAction('refresh'); try { const payload = await api.refreshNotifications(); // 先写回刷新接口直接返回的最新通知快照,再触发上层全量刷新补齐其它区域状态。 dispatch({ type: 'SET_NOTIFICATIONS', payload: payload.items || [] }); dispatch({ type: 'SET_NOTIFICATIONS_META', payload: payload.meta || null }); await onRefresh(); } catch (e) { dispatch({ type: 'SET_ERROR', payload: e.message || '刷新通知失败' }); } finally { setBusyAction(''); } }; const markOneAsRead = async (id) => { setBusyAction(`read-${id}`); try { const payload = await api.readNotification(id); dispatch({ type: 'SET_NOTIFICATIONS_META', payload: payload.meta || null }); dispatch({ type: 'SET_NOTIFICATIONS', // 单条已读时直接在本地列表上做映射更新,避免为了一个按钮再整页重载。 payload: notifications.map((item) => Number(item.id) === Number(id) ? { ...item, _read: true } : item ), }); } catch (e) { dispatch({ type: 'SET_ERROR', payload: e.message || '标记已读失败' }); } finally { setBusyAction(''); } }; const markAllAsRead = async () => { setBusyAction('read-all'); try { const payload = await api.readAllNotifications(); dispatch({ type: 'SET_NOTIFICATIONS_META', payload: payload.meta || null }); dispatch({ type: 'SET_NOTIFICATIONS', // 全部已读时直接把当前列表统一标成已读,保证交互即时反馈。 payload: notifications.map((item) => ({ ...item, _read: true })), }); } catch (e) { dispatch({ type: 'SET_ERROR', payload: e.message || '全部已读失败' }); } finally { setBusyAction(''); } }; return ( 通知中心 ({notificationsMeta.total_count || notifications.length}) 0 ? 'rgba(245, 158, 11, 0.28)' : 'rgba(46, 125, 50, 0.18)', background: Number(notificationsMeta.unread_count ?? 0) > 0 ? 'rgba(245, 158, 11, 0.12)' : 'rgba(46, 125, 50, 0.08)', color: Number(notificationsMeta.unread_count ?? 0) > 0 ? '#B45309' : '#2E7D32', }} > {Number(notificationsMeta.unread_count ?? 0) > 0 ? `未读 ${Number(notificationsMeta.unread_count ?? 0)} 条` : '全部已读'} 未读通知 {Number(notificationsMeta.unread_count ?? 0)} 上次同步 {notificationsMeta.last_sync_at ? formatDateTime(notificationsMeta.last_sync_at, displayTimezone, { includeYear: true, includeSeconds: true }) : '--'} 用于接收插件更新、修复说明、注意事项等官方通知。 全部已读 🔄} sx={{ borderRadius: 3, boxShadow: 'none', px: 2.25 }} > 立即同步 {notifications.length === 0 ? ( 🔔 暂无通知 当前还没有可展示的系统通知,后续若有插件更新、修复说明或注意事项,这里会自动出现。 ) : ( {notifications.map((item) => { const meta = resolveNotificationTypeMeta(item.type); const isRead = Boolean(item._read) || false; return ( {item.title || '--'} {isRead ? '已读' : '未读'} {formatDateTime(item.created_at, displayTimezone, { includeYear: true, includeSeconds: true })} {renderNotificationContent(item)} markOneAsRead(item.id)} sx={{ borderRadius: 999, minWidth: 112, boxShadow: 'none' }} > {isRead ? '已读' : '确认已读'} ); })} )} ); } window.NotificationsView = NotificationsView; })();