HeaderBar.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useContext, useEffect, useState, useRef } from 'react';
  16. import { Link, useNavigate, useLocation } from 'react-router-dom';
  17. import { UserContext } from '../../context/User/index.js';
  18. import { useSetTheme, useTheme } from '../../context/Theme/index.js';
  19. import { useTranslation } from 'react-i18next';
  20. import { API, getLogo, getSystemName, showSuccess, stringToColor } from '../../helpers/index.js';
  21. import fireworks from 'react-fireworks';
  22. import { CN, GB } from 'country-flag-icons/react/3x2';
  23. import NoticeModal from './NoticeModal.js';
  24. import {
  25. IconClose,
  26. IconMenu,
  27. IconLanguage,
  28. IconChevronDown,
  29. IconSun,
  30. IconMoon,
  31. IconExit,
  32. IconUserSetting,
  33. IconCreditCard,
  34. IconKey,
  35. IconBell,
  36. } from '@douyinfe/semi-icons';
  37. import {
  38. Avatar,
  39. Button,
  40. Dropdown,
  41. Tag,
  42. Typography,
  43. Skeleton,
  44. Badge,
  45. } from '@douyinfe/semi-ui';
  46. import { StatusContext } from '../../context/Status/index.js';
  47. import { useIsMobile } from '../../hooks/common/useIsMobile.js';
  48. import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed.js';
  49. const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
  50. const { t, i18n } = useTranslation();
  51. const [userState, userDispatch] = useContext(UserContext);
  52. const [statusState, statusDispatch] = useContext(StatusContext);
  53. const isMobile = useIsMobile();
  54. const [collapsed, toggleCollapsed] = useSidebarCollapsed();
  55. const [isLoading, setIsLoading] = useState(true);
  56. const [logoLoaded, setLogoLoaded] = useState(false);
  57. let navigate = useNavigate();
  58. const [currentLang, setCurrentLang] = useState(i18n.language);
  59. const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
  60. const location = useLocation();
  61. const [noticeVisible, setNoticeVisible] = useState(false);
  62. const [unreadCount, setUnreadCount] = useState(0);
  63. const loadingStartRef = useRef(Date.now());
  64. const systemName = getSystemName();
  65. const logo = getLogo();
  66. const currentDate = new Date();
  67. const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1;
  68. const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false;
  69. const docsLink = statusState?.status?.docs_link || '';
  70. const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
  71. const isConsoleRoute = location.pathname.startsWith('/console');
  72. const theme = useTheme();
  73. const setTheme = useSetTheme();
  74. const announcements = statusState?.status?.announcements || [];
  75. const getAnnouncementKey = (a) => `${a?.publishDate || ''}-${(a?.content || '').slice(0, 30)}`;
  76. const calculateUnreadCount = () => {
  77. if (!announcements.length) return 0;
  78. let readKeys = [];
  79. try {
  80. readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
  81. } catch (_) {
  82. readKeys = [];
  83. }
  84. const readSet = new Set(readKeys);
  85. return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).length;
  86. };
  87. const getUnreadKeys = () => {
  88. if (!announcements.length) return [];
  89. let readKeys = [];
  90. try {
  91. readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
  92. } catch (_) {
  93. readKeys = [];
  94. }
  95. const readSet = new Set(readKeys);
  96. return announcements.filter((a) => !readSet.has(getAnnouncementKey(a))).map(getAnnouncementKey);
  97. };
  98. useEffect(() => {
  99. setUnreadCount(calculateUnreadCount());
  100. // eslint-disable-next-line react-hooks/exhaustive-deps
  101. }, [announcements]);
  102. const mainNavLinks = [
  103. {
  104. text: t('首页'),
  105. itemKey: 'home',
  106. to: '/',
  107. },
  108. {
  109. text: t('控制台'),
  110. itemKey: 'console',
  111. to: '/console',
  112. },
  113. {
  114. text: t('定价'),
  115. itemKey: 'pricing',
  116. to: '/pricing',
  117. },
  118. ...(docsLink
  119. ? [
  120. {
  121. text: t('文档'),
  122. itemKey: 'docs',
  123. isExternal: true,
  124. externalLink: docsLink,
  125. },
  126. ]
  127. : []),
  128. {
  129. text: t('关于'),
  130. itemKey: 'about',
  131. to: '/about',
  132. },
  133. ];
  134. async function logout() {
  135. await API.get('/api/user/logout');
  136. showSuccess(t('注销成功!'));
  137. userDispatch({ type: 'logout' });
  138. localStorage.removeItem('user');
  139. navigate('/login');
  140. setMobileMenuOpen(false);
  141. }
  142. const handleNewYearClick = () => {
  143. fireworks.init('root', {});
  144. fireworks.start();
  145. setTimeout(() => {
  146. fireworks.stop();
  147. }, 3000);
  148. };
  149. const handleNoticeOpen = () => {
  150. setNoticeVisible(true);
  151. };
  152. const handleNoticeClose = () => {
  153. setNoticeVisible(false);
  154. if (announcements.length) {
  155. let readKeys = [];
  156. try {
  157. readKeys = JSON.parse(localStorage.getItem('notice_read_keys')) || [];
  158. } catch (_) {
  159. readKeys = [];
  160. }
  161. const mergedKeys = Array.from(new Set([...readKeys, ...announcements.map(getAnnouncementKey)]));
  162. localStorage.setItem('notice_read_keys', JSON.stringify(mergedKeys));
  163. }
  164. setUnreadCount(0);
  165. };
  166. useEffect(() => {
  167. if (theme === 'dark') {
  168. document.body.setAttribute('theme-mode', 'dark');
  169. document.documentElement.classList.add('dark');
  170. } else {
  171. document.body.removeAttribute('theme-mode');
  172. document.documentElement.classList.remove('dark');
  173. }
  174. const iframe = document.querySelector('iframe');
  175. if (iframe) {
  176. iframe.contentWindow.postMessage({ themeMode: theme }, '*');
  177. }
  178. }, [theme, isNewYear]);
  179. useEffect(() => {
  180. const handleLanguageChanged = (lng) => {
  181. setCurrentLang(lng);
  182. const iframe = document.querySelector('iframe');
  183. if (iframe) {
  184. iframe.contentWindow.postMessage({ lang: lng }, '*');
  185. }
  186. };
  187. i18n.on('languageChanged', handleLanguageChanged);
  188. return () => {
  189. i18n.off('languageChanged', handleLanguageChanged);
  190. };
  191. }, [i18n]);
  192. useEffect(() => {
  193. if (statusState?.status !== undefined) {
  194. const elapsed = Date.now() - loadingStartRef.current;
  195. const remaining = Math.max(0, 500 - elapsed);
  196. const timer = setTimeout(() => {
  197. setIsLoading(false);
  198. }, remaining);
  199. return () => clearTimeout(timer);
  200. }
  201. }, [statusState?.status]);
  202. useEffect(() => {
  203. setLogoLoaded(false);
  204. if (!logo) return;
  205. const img = new Image();
  206. img.src = logo;
  207. img.onload = () => setLogoLoaded(true);
  208. }, [logo]);
  209. const handleLanguageChange = (lang) => {
  210. i18n.changeLanguage(lang);
  211. setMobileMenuOpen(false);
  212. };
  213. const handleNavLinkClick = (itemKey) => {
  214. if (itemKey === 'home') {
  215. // styleDispatch(styleActions.setSider(false)); // This line is removed
  216. }
  217. setMobileMenuOpen(false);
  218. };
  219. const renderNavLinks = (isMobileView = false, isLoading = false) => {
  220. if (isLoading) {
  221. const skeletonLinkClasses = isMobileView
  222. ? 'flex items-center gap-1 p-3 w-full rounded-md'
  223. : 'flex items-center gap-1 p-2 rounded-md';
  224. return Array(4)
  225. .fill(null)
  226. .map((_, index) => (
  227. <div key={index} className={skeletonLinkClasses}>
  228. <Skeleton
  229. loading={true}
  230. active
  231. placeholder={
  232. <Skeleton.Title
  233. active
  234. style={{ width: isMobileView ? 100 : 60, height: 16 }}
  235. />
  236. }
  237. />
  238. </div>
  239. ));
  240. }
  241. return mainNavLinks.map((link) => {
  242. const commonLinkClasses = isMobileView
  243. ? 'flex items-center gap-1 p-3 w-full text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors font-semibold'
  244. : 'flex items-center gap-1 p-2 text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors rounded-md font-semibold';
  245. const linkContent = (
  246. <span>{link.text}</span>
  247. );
  248. if (link.isExternal) {
  249. return (
  250. <a
  251. key={link.itemKey}
  252. href={link.externalLink}
  253. target='_blank'
  254. rel='noopener noreferrer'
  255. className={commonLinkClasses}
  256. onClick={() => handleNavLinkClick(link.itemKey)}
  257. >
  258. {linkContent}
  259. </a>
  260. );
  261. }
  262. let targetPath = link.to;
  263. if (link.itemKey === 'console' && !userState.user) {
  264. targetPath = '/login';
  265. }
  266. return (
  267. <Link
  268. key={link.itemKey}
  269. to={targetPath}
  270. className={commonLinkClasses}
  271. onClick={() => handleNavLinkClick(link.itemKey)}
  272. >
  273. {linkContent}
  274. </Link>
  275. );
  276. });
  277. };
  278. const renderUserArea = () => {
  279. if (isLoading) {
  280. return (
  281. <div className="flex items-center p-1 rounded-full bg-semi-color-fill-0 dark:bg-semi-color-fill-1">
  282. <Skeleton
  283. loading={true}
  284. active
  285. placeholder={<Skeleton.Avatar active size="extra-small" className="shadow-sm" />}
  286. />
  287. <div className="ml-1.5 mr-1">
  288. <Skeleton
  289. loading={true}
  290. active
  291. placeholder={
  292. <Skeleton.Title
  293. active
  294. style={{ width: isMobile ? 15 : 50, height: 12 }}
  295. />
  296. }
  297. />
  298. </div>
  299. </div>
  300. );
  301. }
  302. if (userState.user) {
  303. return (
  304. <Dropdown
  305. position="bottomRight"
  306. render={
  307. <Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
  308. <Dropdown.Item
  309. onClick={() => {
  310. navigate('/console/personal');
  311. setMobileMenuOpen(false);
  312. }}
  313. 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"
  314. >
  315. <div className="flex items-center gap-2">
  316. <IconUserSetting size="small" className="text-gray-500 dark:text-gray-400" />
  317. <span>{t('个人设置')}</span>
  318. </div>
  319. </Dropdown.Item>
  320. <Dropdown.Item
  321. onClick={() => {
  322. navigate('/console/token');
  323. setMobileMenuOpen(false);
  324. }}
  325. 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"
  326. >
  327. <div className="flex items-center gap-2">
  328. <IconKey size="small" className="text-gray-500 dark:text-gray-400" />
  329. <span>{t('令牌管理')}</span>
  330. </div>
  331. </Dropdown.Item>
  332. <Dropdown.Item
  333. onClick={() => {
  334. navigate('/console/topup');
  335. setMobileMenuOpen(false);
  336. }}
  337. 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"
  338. >
  339. <div className="flex items-center gap-2">
  340. <IconCreditCard size="small" className="text-gray-500 dark:text-gray-400" />
  341. <span>{t('钱包')}</span>
  342. </div>
  343. </Dropdown.Item>
  344. <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">
  345. <div className="flex items-center gap-2">
  346. <IconExit size="small" className="text-gray-500 dark:text-gray-400" />
  347. <span>{t('退出')}</span>
  348. </div>
  349. </Dropdown.Item>
  350. </Dropdown.Menu>
  351. }
  352. >
  353. <Button
  354. theme="borderless"
  355. type="tertiary"
  356. 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"
  357. >
  358. <Avatar
  359. size="extra-small"
  360. color={stringToColor(userState.user.username)}
  361. className="mr-1"
  362. >
  363. {userState.user.username[0].toUpperCase()}
  364. </Avatar>
  365. <span className="hidden md:inline">
  366. <Typography.Text className="!text-xs !font-medium !text-semi-color-text-1 dark:!text-gray-300 mr-1">
  367. {userState.user.username}
  368. </Typography.Text>
  369. </span>
  370. <IconChevronDown className="text-xs text-semi-color-text-2 dark:text-gray-400" />
  371. </Button>
  372. </Dropdown>
  373. );
  374. } else {
  375. const showRegisterButton = !isSelfUseMode;
  376. const commonSizingAndLayoutClass = "flex items-center justify-center !py-[10px] !px-1.5";
  377. 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";
  378. let loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`;
  379. let registerButtonClasses = `${commonSizingAndLayoutClass}`;
  380. const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5";
  381. const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
  382. if (showRegisterButton) {
  383. if (isMobile) {
  384. loginButtonClasses += " !rounded-full";
  385. } else {
  386. loginButtonClasses += " !rounded-l-full !rounded-r-none";
  387. }
  388. registerButtonClasses += " !rounded-r-full !rounded-l-none";
  389. } else {
  390. loginButtonClasses += " !rounded-full";
  391. }
  392. return (
  393. <div className="flex items-center">
  394. <Link to="/login" onClick={() => handleNavLinkClick('login')} className="flex">
  395. <Button
  396. theme="borderless"
  397. type="tertiary"
  398. className={loginButtonClasses}
  399. >
  400. <span className={loginButtonTextSpanClass}>
  401. {t('登录')}
  402. </span>
  403. </Button>
  404. </Link>
  405. {showRegisterButton && (
  406. <div className="hidden md:block">
  407. <Link to="/register" onClick={() => handleNavLinkClick('register')} className="flex -ml-px">
  408. <Button
  409. theme="solid"
  410. type="primary"
  411. className={registerButtonClasses}
  412. >
  413. <span className={registerButtonTextSpanClass}>
  414. {t('注册')}
  415. </span>
  416. </Button>
  417. </Link>
  418. </div>
  419. )}
  420. </div>
  421. );
  422. }
  423. };
  424. return (
  425. <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">
  426. <NoticeModal
  427. visible={noticeVisible}
  428. onClose={handleNoticeClose}
  429. isMobile={isMobile}
  430. defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
  431. unreadKeys={getUnreadKeys()}
  432. />
  433. <div className="w-full px-2">
  434. <div className="flex items-center justify-between h-16">
  435. <div className="flex items-center">
  436. <div className="md:hidden">
  437. <Button
  438. icon={
  439. isConsoleRoute
  440. ? ((isMobile ? drawerOpen : collapsed) ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
  441. : (mobileMenuOpen ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
  442. }
  443. aria-label={
  444. isConsoleRoute
  445. ? ((isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏'))
  446. : (mobileMenuOpen ? t('关闭菜单') : t('打开菜单'))
  447. }
  448. onClick={() => {
  449. if (isConsoleRoute) {
  450. // 控制侧边栏的显示/隐藏,无论是否移动设备
  451. isMobile ? onMobileMenuToggle() : toggleCollapsed();
  452. } else {
  453. // 控制HeaderBar自己的移动菜单
  454. setMobileMenuOpen(!mobileMenuOpen);
  455. }
  456. }}
  457. theme="borderless"
  458. type="tertiary"
  459. className="!p-2 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700"
  460. />
  461. </div>
  462. <Link to="/" onClick={() => handleNavLinkClick('home')} className="flex items-center gap-2 group ml-2">
  463. <div className="relative w-8 h-8 md:w-8 md:h-8">
  464. {(isLoading || !logoLoaded) && (
  465. <Skeleton.Image
  466. active
  467. className="absolute inset-0 !rounded-full"
  468. style={{ width: '100%', height: '100%' }}
  469. />
  470. )}
  471. <img
  472. src={logo}
  473. alt="logo"
  474. className={`absolute inset-0 w-full h-full transition-opacity duration-200 group-hover:scale-105 rounded-full ${(!isLoading && logoLoaded) ? 'opacity-100' : 'opacity-0'}`}
  475. />
  476. </div>
  477. <div className="hidden md:flex items-center gap-2">
  478. <div className="flex items-center gap-2">
  479. <Skeleton
  480. loading={isLoading}
  481. active
  482. placeholder={
  483. <Skeleton.Title
  484. active
  485. style={{ width: 120, height: 24 }}
  486. />
  487. }
  488. >
  489. <Typography.Title heading={4} className="!text-lg !font-semibold !mb-0">
  490. {systemName}
  491. </Typography.Title>
  492. </Skeleton>
  493. {(isSelfUseMode || isDemoSiteMode) && !isLoading && (
  494. <Tag
  495. color={isSelfUseMode ? 'purple' : 'blue'}
  496. className="text-xs px-1.5 py-0.5 rounded whitespace-nowrap shadow-sm"
  497. size="small"
  498. shape='circle'
  499. >
  500. {isSelfUseMode ? t('自用模式') : t('演示站点')}
  501. </Tag>
  502. )}
  503. </div>
  504. </div>
  505. </Link>
  506. {(isSelfUseMode || isDemoSiteMode) && !isLoading && (
  507. <div className="md:hidden">
  508. <Tag
  509. color={isSelfUseMode ? 'purple' : 'blue'}
  510. className="ml-2 text-xs px-1 py-0.5 rounded whitespace-nowrap shadow-sm"
  511. size="small"
  512. shape='circle'
  513. >
  514. {isSelfUseMode ? t('自用模式') : t('演示站点')}
  515. </Tag>
  516. </div>
  517. )}
  518. <nav className="hidden md:flex items-center gap-1 lg:gap-2 ml-6">
  519. {renderNavLinks(false, isLoading)}
  520. </nav>
  521. </div>
  522. <div className="flex items-center gap-2 md:gap-3">
  523. {isNewYear && (
  524. <Dropdown
  525. position="bottomRight"
  526. render={
  527. <Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
  528. <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">
  529. Happy New Year!!! 🎉
  530. </Dropdown.Item>
  531. </Dropdown.Menu>
  532. }
  533. >
  534. <Button
  535. theme="borderless"
  536. type="tertiary"
  537. icon={<span className="text-xl">🎉</span>}
  538. aria-label="New Year"
  539. className="!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 rounded-full"
  540. />
  541. </Dropdown>
  542. )}
  543. {unreadCount > 0 ? (
  544. <Badge count={unreadCount} type="danger" overflowCount={99}>
  545. <Button
  546. icon={<IconBell className="text-lg" />}
  547. aria-label={t('系统公告')}
  548. onClick={handleNoticeOpen}
  549. theme="borderless"
  550. type="tertiary"
  551. 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"
  552. />
  553. </Badge>
  554. ) : (
  555. <Button
  556. icon={<IconBell className="text-lg" />}
  557. aria-label={t('系统公告')}
  558. onClick={handleNoticeOpen}
  559. theme="borderless"
  560. type="tertiary"
  561. 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"
  562. />
  563. )}
  564. <Button
  565. icon={theme === 'dark' ? <IconSun size="large" className="text-yellow-500" /> : <IconMoon size="large" className="text-gray-300" />}
  566. aria-label={t('切换主题')}
  567. onClick={() => setTheme(theme === 'dark' ? false : true)}
  568. theme="borderless"
  569. type="tertiary"
  570. 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"
  571. />
  572. <Dropdown
  573. position="bottomRight"
  574. render={
  575. <Dropdown.Menu className="!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600">
  576. <Dropdown.Item
  577. onClick={() => handleLanguageChange('zh')}
  578. 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'}`}
  579. >
  580. <CN title="中文" className="!w-5 !h-auto" />
  581. <span>中文</span>
  582. </Dropdown.Item>
  583. <Dropdown.Item
  584. onClick={() => handleLanguageChange('en')}
  585. 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'}`}
  586. >
  587. <GB title="English" className="!w-5 !h-auto" />
  588. <span>English</span>
  589. </Dropdown.Item>
  590. </Dropdown.Menu>
  591. }
  592. >
  593. <Button
  594. icon={<IconLanguage className="text-lg" />}
  595. aria-label={t('切换语言')}
  596. theme="borderless"
  597. type="tertiary"
  598. 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"
  599. />
  600. </Dropdown>
  601. {renderUserArea()}
  602. </div>
  603. </div>
  604. </div>
  605. <div className="md:hidden">
  606. <div
  607. className={`
  608. absolute top-16 left-0 right-0 bg-semi-color-bg-0
  609. shadow-lg p-3
  610. transform transition-all duration-300 ease-in-out
  611. ${(!isConsoleRoute && mobileMenuOpen) ? 'translate-y-0 opacity-100 visible' : '-translate-y-4 opacity-0 invisible'}
  612. `}
  613. >
  614. <nav className="flex flex-col gap-1">
  615. {renderNavLinks(true, isLoading)}
  616. </nav>
  617. </div>
  618. </div>
  619. </header>
  620. );
  621. };
  622. export default HeaderBar;