/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useContext, useEffect, useState } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { UserContext } from '../../context/User/index.js'; import { useSetTheme, useTheme } from '../../context/Theme/index.js'; import { useTranslation } from 'react-i18next'; import { API, getLogo, getSystemName, showSuccess, stringToColor } from '../../helpers/index.js'; import fireworks from 'react-fireworks'; import { CN, GB } from 'country-flag-icons/react/3x2'; import NoticeModal from './NoticeModal.js'; import { IconClose, IconMenu, IconLanguage, IconChevronDown, IconSun, IconMoon, IconExit, IconUserSetting, IconCreditCard, IconKey, IconBell, } from '@douyinfe/semi-icons'; import { Avatar, Button, Dropdown, Tag, Typography, Skeleton, Badge, } from '@douyinfe/semi-ui'; import { StatusContext } from '../../context/Status/index.js'; import { useIsMobile } from '../../hooks/common/useIsMobile.js'; import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed.js'; import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime.js'; const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { const { t, i18n } = useTranslation(); const [userState, userDispatch] = useContext(UserContext); const [statusState, statusDispatch] = useContext(StatusContext); const isMobile = useIsMobile(); const [collapsed, toggleCollapsed] = useSidebarCollapsed(); const [logoLoaded, setLogoLoaded] = useState(false); let navigate = useNavigate(); const [currentLang, setCurrentLang] = useState(i18n.language); const location = useLocation(); const [noticeVisible, setNoticeVisible] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const loading = statusState?.status === undefined; const isLoading = useMinimumLoadingTime(loading); const systemName = getSystemName(); const logo = getLogo(); const currentDate = new Date(); const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1; const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false; 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('首页'), itemKey: 'home', to: '/', }, { text: t('控制台'), itemKey: 'console', to: '/console', }, { text: t('模型广场'), itemKey: 'pricing', to: '/pricing', }, ...(docsLink ? [ { text: t('文档'), itemKey: 'docs', isExternal: true, externalLink: docsLink, }, ] : []), { text: t('关于'), itemKey: 'about', to: '/about', }, ]; async function logout() { await API.get('/api/user/logout'); showSuccess(t('注销成功!')); userDispatch({ type: 'logout' }); localStorage.removeItem('user'); navigate('/login'); } const handleNewYearClick = () => { fireworks.init('root', {}); fireworks.start(); setTimeout(() => { fireworks.stop(); }, 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'); document.documentElement.classList.add('dark'); } else { document.body.removeAttribute('theme-mode'); document.documentElement.classList.remove('dark'); } const iframe = document.querySelector('iframe'); if (iframe) { iframe.contentWindow.postMessage({ themeMode: theme }, '*'); } }, [theme, isNewYear]); useEffect(() => { const handleLanguageChanged = (lng) => { setCurrentLang(lng); const iframe = document.querySelector('iframe'); if (iframe) { iframe.contentWindow.postMessage({ lang: lng }, '*'); } }; i18n.on('languageChanged', handleLanguageChanged); return () => { i18n.off('languageChanged', handleLanguageChanged); }; }, [i18n]); useEffect(() => { setLogoLoaded(false); if (!logo) return; const img = new Image(); img.src = logo; img.onload = () => setLogoLoaded(true); }, [logo]); const handleLanguageChange = (lang) => { i18n.changeLanguage(lang); }; const renderNavLinks = (isMobileView = false, isLoading = false) => { if (isLoading) { const skeletonLinkClasses = isMobileView ? 'flex items-center gap-1 p-1 w-full rounded-md' : 'flex items-center gap-1 p-2 rounded-md'; return Array(4) .fill(null) .map((_, index) => (
} />
)); } return mainNavLinks.map((link) => { const commonLinkClasses = isMobileView ? 'flex-shrink-0 flex items-center gap-1 p-1 font-semibold' : 'flex-shrink-0 flex items-center gap-1 p-2 font-semibold'; const linkContent = ( {link.text} ); if (link.isExternal) { return ( {linkContent} ); } let targetPath = link.to; if (link.itemKey === 'console' && !userState.user) { targetPath = '/login'; } return ( {linkContent} ); }); }; const renderUserArea = () => { if (isLoading) { return (
} />
} />
); } if (userState.user) { return ( { navigate('/console/personal'); }} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white" >
{t('个人设置')}
{ navigate('/console/token'); }} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white" >
{t('令牌管理')}
{ navigate('/console/topup'); }} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-blue-500 dark:hover:!text-white" >
{t('钱包管理')}
{t('退出')}
} >
); } else { const showRegisterButton = !isSelfUseMode; const commonSizingAndLayoutClass = "flex items-center justify-center !py-[10px] !px-1.5"; const loginButtonSpecificStyling = "!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors"; let loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`; let registerButtonClasses = `${commonSizingAndLayoutClass}`; const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5"; const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5"; if (showRegisterButton) { if (isMobile) { loginButtonClasses += " !rounded-full"; } else { loginButtonClasses += " !rounded-l-full !rounded-r-none"; } registerButtonClasses += " !rounded-r-full !rounded-l-none"; } else { loginButtonClasses += " !rounded-full"; } return (
{showRegisterButton && (
)}
); } }; return (
0 ? 'system' : 'inApp'} unreadKeys={getUnreadKeys()} />
{isConsoleRoute && isMobile && (
{/* 中间可滚动导航区域(全部设备)*/} {/* 右侧用户信息及功能按钮 */}
{isNewYear && ( Happy New Year!!! 🎉 } >
); }; export default HeaderBar;