Просмотр исходного кода

🤖 fix(web/layout): rename HeaderBar -> headerbar (case sensitive)

t0ng7u 6 месяцев назад
Родитель
Сommit
daffba3641

+ 1 - 1
web/src/components/layout/PageLayout.jsx

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import HeaderBar from './headerbar/index.jsx';
+import HeaderBar from './headerbar';
 import { Layout } from '@douyinfe/semi-ui';
 import SiderBar from './SiderBar';
 import App from '../../App';

+ 74 - 0
web/src/components/layout/headerbar/ActionButtons.jsx

@@ -0,0 +1,74 @@
+/*
+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 <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React from 'react';
+import NewYearButton from './NewYearButton';
+import NotificationButton from './NotificationButton';
+import ThemeToggle from './ThemeToggle';
+import LanguageSelector from './LanguageSelector';
+import UserArea from './UserArea';
+
+const ActionButtons = ({
+  isNewYear,
+  unreadCount,
+  onNoticeOpen,
+  theme,
+  onThemeToggle,
+  currentLang,
+  onLanguageChange,
+  userState,
+  isLoading,
+  isMobile,
+  isSelfUseMode,
+  logout,
+  navigate,
+  t,
+}) => {
+  return (
+    <div className='flex items-center gap-2 md:gap-3'>
+      <NewYearButton isNewYear={isNewYear} />
+
+      <NotificationButton
+        unreadCount={unreadCount}
+        onNoticeOpen={onNoticeOpen}
+        t={t}
+      />
+
+      <ThemeToggle theme={theme} onThemeToggle={onThemeToggle} t={t} />
+
+      <LanguageSelector
+        currentLang={currentLang}
+        onLanguageChange={onLanguageChange}
+        t={t}
+      />
+
+      <UserArea
+        userState={userState}
+        isLoading={isLoading}
+        isMobile={isMobile}
+        isSelfUseMode={isSelfUseMode}
+        logout={logout}
+        navigate={navigate}
+        t={t}
+      />
+    </div>
+  );
+};
+
+export default ActionButtons;

+ 81 - 0
web/src/components/layout/headerbar/HeaderLogo.jsx

@@ -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 <https://www.gnu.org/licenses/>.
+
+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 '../components/SkeletonWrapper';
+
+const HeaderLogo = ({
+  isMobile,
+  isConsoleRoute,
+  logo,
+  logoLoaded,
+  isLoading,
+  systemName,
+  isSelfUseMode,
+  isDemoSiteMode,
+  t,
+}) => {
+  if (isMobile && isConsoleRoute) {
+    return null;
+  }
+
+  return (
+    <Link to='/' className='group flex items-center gap-2'>
+      <div className='relative w-8 h-8 md:w-8 md:h-8'>
+        <SkeletonWrapper loading={isLoading || !logoLoaded} type='image' />
+        <img
+          src={logo}
+          alt='logo'
+          className={`absolute inset-0 w-full h-full transition-all duration-200 group-hover:scale-110 rounded-full ${!isLoading && logoLoaded ? 'opacity-100' : 'opacity-0'}`}
+        />
+      </div>
+      <div className='hidden md:flex items-center gap-2'>
+        <div className='flex items-center gap-2'>
+          <SkeletonWrapper
+            loading={isLoading}
+            type='title'
+            width={120}
+            height={24}
+          >
+            <Typography.Title
+              heading={4}
+              className='!text-lg !font-semibold !mb-0'
+            >
+              {systemName}
+            </Typography.Title>
+          </SkeletonWrapper>
+          {(isSelfUseMode || isDemoSiteMode) && !isLoading && (
+            <Tag
+              color={isSelfUseMode ? 'purple' : 'blue'}
+              className='text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm'
+              size='small'
+              shape='circle'
+            >
+              {isSelfUseMode ? t('自用模式') : t('演示站点')}
+            </Tag>
+          )}
+        </div>
+      </div>
+    </Link>
+  );
+};
+
+export default HeaderLogo;

+ 59 - 0
web/src/components/layout/headerbar/LanguageSelector.jsx

@@ -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 <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React from 'react';
+import { Button, Dropdown } from '@douyinfe/semi-ui';
+import { Languages } from 'lucide-react';
+import { CN, GB } from 'country-flag-icons/react/3x2';
+
+const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
+  return (
+    <Dropdown
+      position='bottomRight'
+      render={
+        <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
+          <Dropdown.Item
+            onClick={() => 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'}`}
+          >
+            <CN title='中文' className='!w-5 !h-auto' />
+            <span>中文</span>
+          </Dropdown.Item>
+          <Dropdown.Item
+            onClick={() => 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'}`}
+          >
+            <GB title='English' className='!w-5 !h-auto' />
+            <span>English</span>
+          </Dropdown.Item>
+        </Dropdown.Menu>
+      }
+    >
+      <Button
+        icon={<Languages size={18} />}
+        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'
+      />
+    </Dropdown>
+  );
+};
+
+export default LanguageSelector;

+ 56 - 0
web/src/components/layout/headerbar/MobileMenuButton.jsx

@@ -0,0 +1,56 @@
+/*
+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 <https://www.gnu.org/licenses/>.
+
+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 (
+    <Button
+      icon={
+        (isMobile ? drawerOpen : collapsed) ? (
+          <IconClose className='text-lg' />
+        ) : (
+          <IconMenu className='text-lg' />
+        )
+      }
+      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;

+ 88 - 0
web/src/components/layout/headerbar/Navigation.jsx

@@ -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 <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React from 'react';
+import { Link } from 'react-router-dom';
+import SkeletonWrapper from '../components/SkeletonWrapper';
+
+const Navigation = ({
+  mainNavLinks,
+  isMobile,
+  isLoading,
+  userState,
+  pricingRequireAuth,
+}) => {
+  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 = <span>{link.text}</span>;
+
+      if (link.isExternal) {
+        return (
+          <a
+            key={link.itemKey}
+            href={link.externalLink}
+            target='_blank'
+            rel='noopener noreferrer'
+            className={commonLinkClasses}
+          >
+            {linkContent}
+          </a>
+        );
+      }
+
+      let targetPath = link.to;
+      if (link.itemKey === 'console' && !userState.user) {
+        targetPath = '/login';
+      }
+      if (link.itemKey === 'pricing' && pricingRequireAuth && !userState.user) {
+        targetPath = '/login';
+      }
+
+      return (
+        <Link key={link.itemKey} to={targetPath} className={commonLinkClasses}>
+          {linkContent}
+        </Link>
+      );
+    });
+  };
+
+  return (
+    <nav className='flex flex-1 items-center gap-1 lg:gap-2 mx-2 md:mx-4 overflow-x-auto whitespace-nowrap scrollbar-hide'>
+      <SkeletonWrapper
+        loading={isLoading}
+        type='navigation'
+        count={4}
+        width={60}
+        height={16}
+        isMobile={isMobile}
+      >
+        {renderNavLinks()}
+      </SkeletonWrapper>
+    </nav>
+  );
+};
+
+export default Navigation;

+ 62 - 0
web/src/components/layout/headerbar/NewYearButton.jsx

@@ -0,0 +1,62 @@
+/*
+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 <https://www.gnu.org/licenses/>.
+
+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 (
+    <Dropdown
+      position='bottomRight'
+      render={
+        <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
+          <Dropdown.Item
+            onClick={handleNewYearClick}
+            className='!text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-gray-600'
+          >
+            Happy New Year!!! 🎉
+          </Dropdown.Item>
+        </Dropdown.Menu>
+      }
+    >
+      <Button
+        theme='borderless'
+        type='tertiary'
+        icon={<span className='text-xl'>🎉</span>}
+        aria-label='New Year'
+        className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full'
+      />
+    </Dropdown>
+  );
+};
+
+export default NewYearButton;

+ 46 - 0
web/src/components/layout/headerbar/NotificationButton.jsx

@@ -0,0 +1,46 @@
+/*
+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 <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React from 'react';
+import { Button, Badge } from '@douyinfe/semi-ui';
+import { Bell } from 'lucide-react';
+
+const NotificationButton = ({ unreadCount, onNoticeOpen, t }) => {
+  const buttonProps = {
+    icon: <Bell size={18} />,
+    '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 (
+      <Badge count={unreadCount} type='danger' overflowCount={99}>
+        <Button {...buttonProps} />
+      </Badge>
+    );
+  }
+
+  return <Button {...buttonProps} />;
+};
+
+export default NotificationButton;

+ 109 - 0
web/src/components/layout/headerbar/ThemeToggle.jsx

@@ -0,0 +1,109 @@
+/*
+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 <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React, { useMemo } from 'react';
+import { Button, Dropdown } from '@douyinfe/semi-ui';
+import { Sun, Moon, Monitor } from 'lucide-react';
+import { useActualTheme } from '../../../context/Theme';
+
+const ThemeToggle = ({ theme, onThemeToggle, t }) => {
+  const actualTheme = useActualTheme();
+
+  const themeOptions = useMemo(
+    () => [
+      {
+        key: 'light',
+        icon: <Sun size={18} />,
+        buttonIcon: <Sun size={18} />,
+        label: t('浅色模式'),
+        description: t('始终使用浅色主题'),
+      },
+      {
+        key: 'dark',
+        icon: <Moon size={18} />,
+        buttonIcon: <Moon size={18} />,
+        label: t('深色模式'),
+        description: t('始终使用深色主题'),
+      },
+      {
+        key: 'auto',
+        icon: <Monitor size={18} />,
+        buttonIcon: <Monitor size={18} />,
+        label: t('自动模式'),
+        description: t('跟随系统主题设置'),
+      },
+    ],
+    [t],
+  );
+
+  const getItemClassName = (isSelected) =>
+    isSelected
+      ? '!bg-semi-color-primary-light-default !font-semibold'
+      : 'hover:!bg-semi-color-fill-1';
+
+  const currentButtonIcon = useMemo(() => {
+    const currentOption = themeOptions.find((option) => option.key === theme);
+    return currentOption?.buttonIcon || themeOptions[2].buttonIcon;
+  }, [theme, themeOptions]);
+
+  return (
+    <Dropdown
+      position='bottomRight'
+      render={
+        <Dropdown.Menu>
+          {themeOptions.map((option) => (
+            <Dropdown.Item
+              key={option.key}
+              icon={option.icon}
+              onClick={() => onThemeToggle(option.key)}
+              className={getItemClassName(theme === option.key)}
+            >
+              <div className='flex flex-col'>
+                <span>{option.label}</span>
+                <span className='text-xs text-semi-color-text-2'>
+                  {option.description}
+                </span>
+              </div>
+            </Dropdown.Item>
+          ))}
+
+          {theme === 'auto' && (
+            <>
+              <Dropdown.Divider />
+              <div className='px-3 py-2 text-xs text-semi-color-text-2'>
+                {t('当前跟随系统')}:
+                {actualTheme === 'dark' ? t('深色') : t('浅色')}
+              </div>
+            </>
+          )}
+        </Dropdown.Menu>
+      }
+    >
+      <Button
+        icon={currentButtonIcon}
+        aria-label={t('切换主题')}
+        theme='borderless'
+        type='tertiary'
+        className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1'
+      />
+    </Dropdown>
+  );
+};
+
+export default ThemeToggle;

+ 196 - 0
web/src/components/layout/headerbar/UserArea.jsx

@@ -0,0 +1,196 @@
+/*
+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 <https://www.gnu.org/licenses/>.
+
+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 { ChevronDown } from 'lucide-react';
+import {
+  IconExit,
+  IconUserSetting,
+  IconCreditCard,
+  IconKey,
+} from '@douyinfe/semi-icons';
+import { stringToColor } from '../../../helpers';
+import SkeletonWrapper from '../components/SkeletonWrapper';
+
+const UserArea = ({
+  userState,
+  isLoading,
+  isMobile,
+  isSelfUseMode,
+  logout,
+  navigate,
+  t,
+}) => {
+  if (isLoading) {
+    return (
+      <SkeletonWrapper
+        loading={true}
+        type='userArea'
+        width={50}
+        isMobile={isMobile}
+      />
+    );
+  }
+
+  if (userState.user) {
+    return (
+      <Dropdown
+        position='bottomRight'
+        render={
+          <Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
+            <Dropdown.Item
+              onClick={() => {
+                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'
+            >
+              <div className='flex items-center gap-2'>
+                <IconUserSetting
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
+                <span>{t('个人设置')}</span>
+              </div>
+            </Dropdown.Item>
+            <Dropdown.Item
+              onClick={() => {
+                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'
+            >
+              <div className='flex items-center gap-2'>
+                <IconKey
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
+                <span>{t('令牌管理')}</span>
+              </div>
+            </Dropdown.Item>
+            <Dropdown.Item
+              onClick={() => {
+                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'
+            >
+              <div className='flex items-center gap-2'>
+                <IconCreditCard
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
+                <span>{t('钱包管理')}</span>
+              </div>
+            </Dropdown.Item>
+            <Dropdown.Item
+              onClick={logout}
+              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-red-500 dark:hover:!text-white'
+            >
+              <div className='flex items-center gap-2'>
+                <IconExit
+                  size='small'
+                  className='text-gray-500 dark:text-gray-400'
+                />
+                <span>{t('退出')}</span>
+              </div>
+            </Dropdown.Item>
+          </Dropdown.Menu>
+        }
+      >
+        <Button
+          theme='borderless'
+          type='tertiary'
+          className='flex items-center gap-1.5 !p-1 !rounded-full hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
+        >
+          <Avatar
+            size='extra-small'
+            color={stringToColor(userState.user.username)}
+            className='mr-1'
+          >
+            {userState.user.username[0].toUpperCase()}
+          </Avatar>
+          <span className='hidden md:inline'>
+            <Typography.Text className='!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1'>
+              {userState.user.username}
+            </Typography.Text>
+          </span>
+          <ChevronDown
+            size={14}
+            className='text-xs text-semi-color-text-2 dark:text-gray-400'
+          />
+        </Button>
+      </Dropdown>
+    );
+  } 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 (
+      <div className='flex items-center'>
+        <Link to='/login' className='flex'>
+          <Button
+            theme='borderless'
+            type='tertiary'
+            className={loginButtonClasses}
+          >
+            <span className={loginButtonTextSpanClass}>{t('登录')}</span>
+          </Button>
+        </Link>
+        {showRegisterButton && (
+          <div className='hidden md:block'>
+            <Link to='/register' className='flex -ml-px'>
+              <Button
+                theme='solid'
+                type='primary'
+                className={registerButtonClasses}
+              >
+                <span className={registerButtonTextSpanClass}>{t('注册')}</span>
+              </Button>
+            </Link>
+          </div>
+        )}
+      </div>
+    );
+  }
+};
+
+export default UserArea;

+ 132 - 0
web/src/components/layout/headerbar/index.jsx

@@ -0,0 +1,132 @@
+/*
+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 <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React from 'react';
+import { useHeaderBar } from '../../../hooks/common/useHeaderBar';
+import { useNotifications } from '../../../hooks/common/useNotifications';
+import { useNavigation } from '../../../hooks/common/useNavigation';
+import NoticeModal from '../NoticeModal';
+import MobileMenuButton from './MobileMenuButton';
+import HeaderLogo from './HeaderLogo';
+import Navigation from './Navigation';
+import ActionButtons from './ActionButtons';
+
+const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
+  const {
+    userState,
+    statusState,
+    isMobile,
+    collapsed,
+    logoLoaded,
+    currentLang,
+    isLoading,
+    systemName,
+    logo,
+    isNewYear,
+    isSelfUseMode,
+    docsLink,
+    isDemoSiteMode,
+    isConsoleRoute,
+    theme,
+    headerNavModules,
+    pricingRequireAuth,
+    logout,
+    handleLanguageChange,
+    handleThemeToggle,
+    handleMobileMenuToggle,
+    navigate,
+    t,
+  } = useHeaderBar({ onMobileMenuToggle, drawerOpen });
+
+  const {
+    noticeVisible,
+    unreadCount,
+    handleNoticeOpen,
+    handleNoticeClose,
+    getUnreadKeys,
+  } = useNotifications(statusState);
+
+  const { mainNavLinks } = useNavigation(t, docsLink, headerNavModules);
+
+  return (
+    <header className='text-semi-color-text-0 sticky top-0 z-50 transition-colors duration-300 bg-white/75 dark:bg-zinc-900/75 backdrop-blur-lg'>
+      <NoticeModal
+        visible={noticeVisible}
+        onClose={handleNoticeClose}
+        isMobile={isMobile}
+        defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
+        unreadKeys={getUnreadKeys()}
+      />
+
+      <div className='w-full px-2'>
+        <div className='flex items-center justify-between h-16'>
+          <div className='flex items-center'>
+            <MobileMenuButton
+              isConsoleRoute={isConsoleRoute}
+              isMobile={isMobile}
+              drawerOpen={drawerOpen}
+              collapsed={collapsed}
+              onToggle={handleMobileMenuToggle}
+              t={t}
+            />
+
+            <HeaderLogo
+              isMobile={isMobile}
+              isConsoleRoute={isConsoleRoute}
+              logo={logo}
+              logoLoaded={logoLoaded}
+              isLoading={isLoading}
+              systemName={systemName}
+              isSelfUseMode={isSelfUseMode}
+              isDemoSiteMode={isDemoSiteMode}
+              t={t}
+            />
+          </div>
+
+          <Navigation
+            mainNavLinks={mainNavLinks}
+            isMobile={isMobile}
+            isLoading={isLoading}
+            userState={userState}
+            pricingRequireAuth={pricingRequireAuth}
+          />
+
+          <ActionButtons
+            isNewYear={isNewYear}
+            unreadCount={unreadCount}
+            onNoticeOpen={handleNoticeOpen}
+            theme={theme}
+            onThemeToggle={handleThemeToggle}
+            currentLang={currentLang}
+            onLanguageChange={handleLanguageChange}
+            userState={userState}
+            isLoading={isLoading}
+            isMobile={isMobile}
+            isSelfUseMode={isSelfUseMode}
+            logout={logout}
+            navigate={navigate}
+            t={t}
+          />
+        </div>
+      </div>
+    </header>
+  );
+};
+
+export default HeaderBar;