import { useEffect, useState, useCallback } from 'react'; import { videoApi } from '../lib/api'; import { useAuthStore } from '../store/auth'; import { useNotificationStore } from '../store/notification'; import { AnnouncementModal } from './AnnouncementModal'; /** * 全局公告网关 — 挂在 App.tsx 顶层,所有登录用户在任意路由都会触发。 * * 行为: * - 用户登录后 / tab 可见性变化时 / 路由变化时调 GET /announcement * - 有未读公告就强制弹 modal,用户必须点关闭才能继续用页面 * - 关闭 → POST /announcement/read 标记所有未读公告已读 → 同步刷 sidebar 铃铛红点 * * 重看历史公告 → 走 sidebar 大铃铛 → 消息中心页(本组件不参与重看 flow)。 */ export function GlobalAnnouncementGate() { const user = useAuthStore((s) => s.user); const fetchUnreadCount = useNotificationStore((s) => s.fetchUnreadCount); const [unread, setUnread] = useState<{ content: string; notification_id: number } | null>(null); const check = useCallback(async () => { try { const { data } = await videoApi.getAnnouncement(); if (data.enabled && data.announcement && !data.is_read) { setUnread({ content: data.announcement, // 兼容老前端响应,notification_id 可能不存在(后端已加) notification_id: (data as { notification_id?: number }).notification_id ?? 0, }); } } catch { // 静默失败 — 网络抖动不炸 UI } }, []); useEffect(() => { if (!user) { setUnread(null); return; } check(); const onVis = () => { if (!document.hidden) check(); }; document.addEventListener('visibilitychange', onVis); return () => document.removeEventListener('visibilitychange', onVis); }, [user, check]); const handleClose = useCallback(async () => { try { await videoApi.readAnnouncement(); } catch { // 即便后端标失败,前端也得让用户能关 — 否则页面卡死 } setUnread(null); fetchUnreadCount(); // 同步 sidebar 铃铛红点 -1 }, [fetchUnreadCount]); if (!unread) return null; return ; }