diff --git a/web/src/components/layout/HeaderBar.js b/web/src/components/layout/HeaderBar.js index 6317c576..b7425645 100644 --- a/web/src/components/layout/HeaderBar.js +++ b/web/src/components/layout/HeaderBar.js @@ -28,6 +28,7 @@ import { Tag, Typography, Skeleton, + Badge, } from '@douyinfe/semi-ui'; import { StatusContext } from '../../context/Status/index.js'; import { useStyle, styleActions } from '../../context/Style/index.js'; @@ -43,6 +44,7 @@ const HeaderBar = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const location = useLocation(); const [noticeVisible, setNoticeVisible] = useState(false); + const [unreadCount, setUnreadCount] = useState(0); const systemName = getSystemName(); const logo = getLogo(); @@ -53,9 +55,44 @@ const HeaderBar = () => { const docsLink = statusState?.status?.docs_link || ''; const isDemoSiteMode = statusState?.status?.demo_site_enabled || false; + const isConsoleRoute = location.pathname.startsWith('/console'); + const theme = useTheme(); const setTheme = useSetTheme(); + const announcements = statusState?.status?.announcements || []; + + const getAnnouncementKey = (a) => `${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`; + + const calculateUnreadCount = () => { + if (!announcements.length) return 0; + let readKeys = []; + try { + readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || []; + } catch (_) { + readKeys = []; + } + const readSet = new Set(readKeys); + return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).length; + }; + + const getUnreadKeys = () => { + if (!announcements.length) return []; + let readKeys = []; + try { + readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || []; + } catch (_) { + readKeys = []; + } + const readSet = new Set(readKeys); + return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).map(getAnnouncementKey); + }; + + useEffect(() => { + setUnreadCount(calculateUnreadCount()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [announcements]); + const mainNavLinks = [ { text: t('首页'), @@ -106,6 +143,25 @@ const HeaderBar = () => { }, 3000); }; + const handleNoticeOpen = () => { + setNoticeVisible(true); + }; + + const handleNoticeClose = () => { + setNoticeVisible(false); + if (announcements.length) { + let readKeys = []; + try { + readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || []; + } catch (_) { + readKeys = []; + } + const mergedKeys = Array.from(new Set([...readKeys, ...announcements.map(getAnnouncementKey)])); + localStorage.setItem('notice_read_keys', JSON.stringify(mergedKeys)); + } + setUnreadCount(0); + }; + useEffect(() => { if (theme === 'dark') { document.body.setAttribute('theme-mode', 'dark'); @@ -353,15 +409,14 @@ const HeaderBar = () => { } }; - // 检查当前路由是否以/console开头 - const isConsoleRoute = location.pathname.startsWith('/console'); - return (
setNoticeVisible(false)} + onClose={handleNoticeClose} isMobile={styleState.isMobile} + defaultTab={unreadCount > 0 ? 'system' : 'inApp'} + unreadKeys={getUnreadKeys()} />
@@ -462,14 +517,27 @@ const HeaderBar = () => { )} -