HeaderBar.js 24 KB

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