From 1ad2f63f85b876bd7de660a8f505be3715bba0ac Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Sat, 21 Jun 2025 06:06:21 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Enhance=20announcements=20U?= =?UTF-8?q?X=20with=20unread=20badge,=20tabbed=20NoticeModal,=20and=20shin?= =?UTF-8?q?e=20animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • HeaderBar - Added dynamic unread badge; click now opens NoticeModal on “System Announcements” tab - Passes `defaultTab` and `unreadKeys` props to NoticeModal for contextual behaviour • NoticeModal - Introduced Tabs inside the modal title with Lucide icons (Bell, Megaphone) - Displays in-app notice (markdown) and system announcements separately - Highlights unread announcements with “shine” text animation - Accepts new props `defaultTab`, `unreadKeys` to control initial tab and highlight logic • CSS (index.css) - Implemented `sweep-shine` keyframes and `.shine-text` utility for left-to-right glow - Added dark-mode variant for better contrast - Ensured cross-browser support with standard `background-clip` Overall, users now see an unread counter, are directed to new announcements automatically, and benefit from an eye-catching glow effect that works in both light and dark themes. --- web/src/components/layout/HeaderBar.js | 92 +++++++++++++++++--- web/src/components/layout/NoticeModal.js | 104 +++++++++++++++++++++-- web/src/i18n/locales/en.json | 3 +- web/src/index.css | 28 ++++++ 4 files changed, 206 insertions(+), 21 deletions(-) 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 = () => { )} -