-
- );
-};
-
-export default HeaderBar;
\ No newline at end of file
+export { default } from './HeaderBar/index.js';
diff --git a/web/src/components/layout/HeaderBar/ActionButtons.js b/web/src/components/layout/HeaderBar/ActionButtons.js
new file mode 100644
index 00000000..01c75d5b
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/ActionButtons.js
@@ -0,0 +1,78 @@
+/*
+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 from 'react';
+import NewYearButton from './NewYearButton.js';
+import NotificationButton from './NotificationButton.js';
+import ThemeToggle from './ThemeToggle.js';
+import LanguageSelector from './LanguageSelector.js';
+import UserArea from './UserArea.js';
+
+const ActionButtons = ({
+ isNewYear,
+ unreadCount,
+ onNoticeOpen,
+ theme,
+ onThemeToggle,
+ currentLang,
+ onLanguageChange,
+ userState,
+ isLoading,
+ isMobile,
+ isSelfUseMode,
+ logout,
+ navigate,
+ t,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ActionButtons;
diff --git a/web/src/components/layout/HeaderBar/HeaderLogo.js b/web/src/components/layout/HeaderBar/HeaderLogo.js
new file mode 100644
index 00000000..77e27bad
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/HeaderLogo.js
@@ -0,0 +1,81 @@
+/*
+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 from 'react';
+import { Link } from 'react-router-dom';
+import { Typography, Tag } from '@douyinfe/semi-ui';
+import SkeletonWrapper from './SkeletonWrapper.js';
+
+const HeaderLogo = ({
+ isMobile,
+ isConsoleRoute,
+ logo,
+ logoLoaded,
+ isLoading,
+ systemName,
+ isSelfUseMode,
+ isDemoSiteMode,
+ t,
+}) => {
+ if (isMobile && isConsoleRoute) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default HeaderLogo;
diff --git a/web/src/components/layout/HeaderBar/LanguageSelector.js b/web/src/components/layout/HeaderBar/LanguageSelector.js
new file mode 100644
index 00000000..1f5e0bb7
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/LanguageSelector.js
@@ -0,0 +1,59 @@
+/*
+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 from 'react';
+import { Button, Dropdown } from '@douyinfe/semi-ui';
+import { IconLanguage } from '@douyinfe/semi-icons';
+import { CN, GB } from 'country-flag-icons/react/3x2';
+
+const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
+ return (
+
+ onLanguageChange('zh')}
+ className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
+ >
+
+ 中文
+
+ onLanguageChange('en')}
+ className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'en' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
+ >
+
+ English
+
+
+ }
+ >
+ }
+ aria-label={t('切换语言')}
+ theme="borderless"
+ type="tertiary"
+ className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
+ />
+
+ );
+};
+
+export default LanguageSelector;
diff --git a/web/src/components/layout/HeaderBar/MobileMenuButton.js b/web/src/components/layout/HeaderBar/MobileMenuButton.js
new file mode 100644
index 00000000..bc5d737b
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/MobileMenuButton.js
@@ -0,0 +1,50 @@
+/*
+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 from 'react';
+import { Button } from '@douyinfe/semi-ui';
+import { IconClose, IconMenu } from '@douyinfe/semi-icons';
+
+const MobileMenuButton = ({
+ isConsoleRoute,
+ isMobile,
+ drawerOpen,
+ collapsed,
+ onToggle,
+ t,
+}) => {
+ if (!isConsoleRoute || !isMobile) {
+ return null;
+ }
+
+ return (
+ :
+ }
+ aria-label={(isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏')}
+ onClick={onToggle}
+ theme="borderless"
+ type="tertiary"
+ className="!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700"
+ />
+ );
+};
+
+export default MobileMenuButton;
diff --git a/web/src/components/layout/HeaderBar/Navigation.js b/web/src/components/layout/HeaderBar/Navigation.js
new file mode 100644
index 00000000..ef418783
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/Navigation.js
@@ -0,0 +1,88 @@
+/*
+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 from 'react';
+import { Link } from 'react-router-dom';
+import SkeletonWrapper from './SkeletonWrapper.js';
+
+const Navigation = ({
+ mainNavLinks,
+ isMobile,
+ isLoading,
+ userState
+}) => {
+ const renderNavLinks = () => {
+ const baseClasses = 'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';
+ const hoverClasses = 'hover:text-semi-color-primary';
+ const spacingClasses = isMobile ? 'p-1' : 'p-2';
+
+ const commonLinkClasses = `${baseClasses} ${spacingClasses} ${hoverClasses}`;
+
+ return mainNavLinks.map((link) => {
+
+ const linkContent = {link.text};
+
+ if (link.isExternal) {
+ return (
+
+ {linkContent}
+
+ );
+ }
+
+ let targetPath = link.to;
+ if (link.itemKey === 'console' && !userState.user) {
+ targetPath = '/login';
+ }
+
+ return (
+
+ {linkContent}
+
+ );
+ });
+ };
+
+ return (
+
+ );
+};
+
+export default Navigation;
diff --git a/web/src/components/layout/HeaderBar/NewYearButton.js b/web/src/components/layout/HeaderBar/NewYearButton.js
new file mode 100644
index 00000000..8406a326
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/NewYearButton.js
@@ -0,0 +1,59 @@
+/*
+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 from 'react';
+import { Button, Dropdown } from '@douyinfe/semi-ui';
+import fireworks from 'react-fireworks';
+
+const NewYearButton = ({ isNewYear }) => {
+ if (!isNewYear) {
+ return null;
+ }
+
+ const handleNewYearClick = () => {
+ fireworks.init('root', {});
+ fireworks.start();
+ setTimeout(() => {
+ fireworks.stop();
+ }, 3000);
+ };
+
+ return (
+
+
+ Happy New Year!!! 🎉
+
+
+ }
+ >
+
+ );
+};
+
+export default NewYearButton;
diff --git a/web/src/components/layout/HeaderBar/NotificationButton.js b/web/src/components/layout/HeaderBar/NotificationButton.js
new file mode 100644
index 00000000..9b3cc225
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/NotificationButton.js
@@ -0,0 +1,45 @@
+/*
+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 from 'react';
+import { Button, Badge } from '@douyinfe/semi-ui';
+import { IconBell } from '@douyinfe/semi-icons';
+
+const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
+ const buttonProps = {
+ icon: ,
+ 'aria-label': t('系统公告'),
+ onClick: onNoticeOpen,
+ theme: "borderless",
+ type: "tertiary",
+ className: "!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2",
+ };
+
+ if (unreadCount > 0) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+};
+
+export default NotificationButton;
diff --git a/web/src/components/layout/HeaderBar/SkeletonWrapper.js b/web/src/components/layout/HeaderBar/SkeletonWrapper.js
new file mode 100644
index 00000000..9afbc3c3
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/SkeletonWrapper.js
@@ -0,0 +1,154 @@
+/*
+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 from 'react';
+import { Skeleton } from '@douyinfe/semi-ui';
+
+const SkeletonWrapper = ({
+ loading = false,
+ type = 'text',
+ count = 1,
+ width = 60,
+ height = 16,
+ isMobile = false,
+ className = '',
+ children,
+ ...props
+}) => {
+ if (!loading) {
+ return children;
+ }
+
+ // 导航链接骨架屏
+ const renderNavigationSkeleton = () => {
+ const skeletonLinkClasses = isMobile
+ ? 'flex items-center gap-1 p-1 w-full rounded-md'
+ : 'flex items-center gap-1 p-2 rounded-md';
+
+ return Array(count)
+ .fill(null)
+ .map((_, index) => (
+
+ );
+ };
+
+ // 根据类型渲染不同的骨架屏
+ switch (type) {
+ case 'navigation':
+ return renderNavigationSkeleton();
+ case 'userArea':
+ return renderUserAreaSkeleton();
+ case 'image':
+ return renderImageSkeleton();
+ case 'title':
+ return renderTitleSkeleton();
+ case 'text':
+ default:
+ return renderTextSkeleton();
+ }
+};
+
+export default SkeletonWrapper;
diff --git a/web/src/components/layout/HeaderBar/ThemeToggle.js b/web/src/components/layout/HeaderBar/ThemeToggle.js
new file mode 100644
index 00000000..810a15db
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/ThemeToggle.js
@@ -0,0 +1,37 @@
+/*
+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 from 'react';
+import { Button } from '@douyinfe/semi-ui';
+import { IconSun, IconMoon } from '@douyinfe/semi-icons';
+
+const ThemeToggle = ({ theme, onThemeToggle, t }) => {
+ return (
+ : }
+ aria-label={t('切换主题')}
+ onClick={onThemeToggle}
+ theme="borderless"
+ type="tertiary"
+ className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2"
+ />
+ );
+};
+
+export default ThemeToggle;
diff --git a/web/src/components/layout/HeaderBar/UserArea.js b/web/src/components/layout/HeaderBar/UserArea.js
new file mode 100644
index 00000000..9d9dc7ad
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/UserArea.js
@@ -0,0 +1,184 @@
+/*
+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 from 'react';
+import { Link } from 'react-router-dom';
+import {
+ Avatar,
+ Button,
+ Dropdown,
+ Typography,
+} from '@douyinfe/semi-ui';
+import {
+ IconChevronDown,
+ IconExit,
+ IconUserSetting,
+ IconCreditCard,
+ IconKey,
+} from '@douyinfe/semi-icons';
+import { stringToColor } from '../../../helpers/index.js';
+import SkeletonWrapper from './SkeletonWrapper.js';
+
+const UserArea = ({
+ userState,
+ isLoading,
+ isMobile,
+ isSelfUseMode,
+ logout,
+ navigate,
+ t,
+}) => {
+ 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"
+ >
+
+ );
+ }
+};
+
+export default UserArea;
diff --git a/web/src/components/layout/HeaderBar/index.js b/web/src/components/layout/HeaderBar/index.js
new file mode 100644
index 00000000..2bdf0aca
--- /dev/null
+++ b/web/src/components/layout/HeaderBar/index.js
@@ -0,0 +1,129 @@
+/*
+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 from 'react';
+import { useHeaderBar } from '../../../hooks/common/useHeaderBar.js';
+import { useNotifications } from '../../../hooks/common/useNotifications.js';
+import { useNavigation } from '../../../hooks/common/useNavigation.js';
+import NoticeModal from '../NoticeModal.js';
+import MobileMenuButton from './MobileMenuButton.js';
+import HeaderLogo from './HeaderLogo.js';
+import Navigation from './Navigation.js';
+import ActionButtons from './ActionButtons.js';
+
+const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
+ const {
+ userState,
+ statusState,
+ isMobile,
+ collapsed,
+ logoLoaded,
+ currentLang,
+ isLoading,
+ systemName,
+ logo,
+ isNewYear,
+ isSelfUseMode,
+ docsLink,
+ isDemoSiteMode,
+ isConsoleRoute,
+ theme,
+ logout,
+ handleLanguageChange,
+ handleThemeToggle,
+ handleMobileMenuToggle,
+ navigate,
+ t,
+ } = useHeaderBar({ onMobileMenuToggle, drawerOpen });
+
+ const {
+ noticeVisible,
+ unreadCount,
+ handleNoticeOpen,
+ handleNoticeClose,
+ getUnreadKeys,
+ } = useNotifications(statusState);
+
+ const { mainNavLinks } = useNavigation(t, docsLink);
+
+ return (
+
+ 0 ? 'system' : 'inApp'}
+ unreadKeys={getUnreadKeys()}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default HeaderBar;
diff --git a/web/src/hooks/common/useHeaderBar.js b/web/src/hooks/common/useHeaderBar.js
new file mode 100644
index 00000000..13b23e30
--- /dev/null
+++ b/web/src/hooks/common/useHeaderBar.js
@@ -0,0 +1,153 @@
+/*
+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 { useState, useEffect, useContext } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { UserContext } from '../../context/User/index.js';
+import { StatusContext } from '../../context/Status/index.js';
+import { useSetTheme, useTheme } from '../../context/Theme/index.js';
+import { getLogo, getSystemName, API, showSuccess } from '../../helpers/index.js';
+import { useIsMobile } from './useIsMobile.js';
+import { useSidebarCollapsed } from './useSidebarCollapsed.js';
+import { useMinimumLoadingTime } from './useMinimumLoadingTime.js';
+
+export const useHeaderBar = ({ 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);
+ const navigate = useNavigate();
+ const [currentLang, setCurrentLang] = useState(i18n.language);
+ const location = useLocation();
+
+ 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();
+
+ // Logo loading effect
+ useEffect(() => {
+ setLogoLoaded(false);
+ if (!logo) return;
+ const img = new Image();
+ img.src = logo;
+ img.onload = () => setLogoLoaded(true);
+ }, [logo]);
+
+ // Theme effect
+ 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]);
+
+ // Language change effect
+ 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]);
+
+ // Actions
+ const logout = async () => {
+ await API.get('/api/user/logout');
+ showSuccess(t('注销成功!'));
+ userDispatch({ type: 'logout' });
+ localStorage.removeItem('user');
+ navigate('/login');
+ };
+
+ const handleLanguageChange = (lang) => {
+ i18n.changeLanguage(lang);
+ };
+
+ const handleThemeToggle = () => {
+ setTheme(theme === 'dark' ? false : true);
+ };
+
+ const handleMobileMenuToggle = () => {
+ if (isMobile) {
+ onMobileMenuToggle();
+ } else {
+ toggleCollapsed();
+ }
+ };
+
+ return {
+ // State
+ userState,
+ statusState,
+ isMobile,
+ collapsed,
+ logoLoaded,
+ currentLang,
+ location,
+ isLoading,
+ systemName,
+ logo,
+ isNewYear,
+ isSelfUseMode,
+ docsLink,
+ isDemoSiteMode,
+ isConsoleRoute,
+ theme,
+ drawerOpen,
+
+ // Actions
+ logout,
+ handleLanguageChange,
+ handleThemeToggle,
+ handleMobileMenuToggle,
+ navigate,
+ t,
+ };
+};
diff --git a/web/src/hooks/common/useNavigation.js b/web/src/hooks/common/useNavigation.js
new file mode 100644
index 00000000..0fbe3d09
--- /dev/null
+++ b/web/src/hooks/common/useNavigation.js
@@ -0,0 +1,59 @@
+/*
+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 { useMemo } from 'react';
+
+export const useNavigation = (t, docsLink) => {
+ const mainNavLinks = useMemo(() => [
+ {
+ 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',
+ },
+ ], [t, docsLink]);
+
+ return {
+ mainNavLinks,
+ };
+};
diff --git a/web/src/hooks/common/useNotifications.js b/web/src/hooks/common/useNotifications.js
new file mode 100644
index 00000000..3df154de
--- /dev/null
+++ b/web/src/hooks/common/useNotifications.js
@@ -0,0 +1,88 @@
+/*
+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 { useState, useEffect } from 'react';
+
+export const useNotifications = (statusState) => {
+ const [noticeVisible, setNoticeVisible] = useState(false);
+ const [unreadCount, setUnreadCount] = useState(0);
+
+ const announcements = statusState?.status?.announcements || [];
+
+ // Helper functions
+ 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);
+ };
+
+ // Effects
+ useEffect(() => {
+ setUnreadCount(calculateUnreadCount());
+ }, [announcements]);
+
+ // Actions
+ 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);
+ };
+
+ return {
+ noticeVisible,
+ unreadCount,
+ announcements,
+ handleNoticeOpen,
+ handleNoticeClose,
+ getUnreadKeys,
+ };
+};