HeaderBar.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import React, { useContext, useEffect, useState } from 'react';
  2. import { Link, useNavigate } from 'react-router-dom';
  3. import { UserContext } from '../context/User';
  4. import { useSetTheme, useTheme } from '../context/Theme';
  5. import { useTranslation } from 'react-i18next';
  6. import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
  7. import '../index.css';
  8. import fireworks from 'react-fireworks';
  9. import {
  10. IconClose,
  11. IconHelpCircle,
  12. IconHome,
  13. IconHomeStroked, IconIndentLeft,
  14. IconComment,
  15. IconKey, IconMenu,
  16. IconNoteMoneyStroked,
  17. IconPriceTag,
  18. IconUser,
  19. IconLanguage,
  20. IconInfoCircle,
  21. IconCreditCard,
  22. IconTerminal
  23. } from '@douyinfe/semi-icons';
  24. import { Avatar, Button, Dropdown, Layout, Nav, Switch, Tag } from '@douyinfe/semi-ui';
  25. import { stringToColor } from '../helpers/render';
  26. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  27. import { StyleContext } from '../context/Style/index.js';
  28. import { StatusContext } from '../context/Status/index.js';
  29. // 自定义顶部栏样式
  30. const headerStyle = {
  31. boxShadow: '0 2px 10px rgba(0, 0, 0, 0.1)',
  32. borderBottom: '1px solid var(--semi-color-border)',
  33. background: 'var(--semi-color-bg-0)',
  34. transition: 'all 0.3s ease',
  35. width: '100%'
  36. };
  37. // 自定义顶部栏按钮样式
  38. const headerItemStyle = {
  39. borderRadius: '4px',
  40. margin: '0 4px',
  41. transition: 'all 0.3s ease'
  42. };
  43. // 自定义顶部栏按钮悬停样式
  44. const headerItemHoverStyle = {
  45. backgroundColor: 'var(--semi-color-primary-light-default)',
  46. color: 'var(--semi-color-primary)'
  47. };
  48. // 自定义顶部栏Logo样式
  49. const logoStyle = {
  50. display: 'flex',
  51. alignItems: 'center',
  52. gap: '10px',
  53. padding: '0 10px',
  54. height: '100%'
  55. };
  56. // 自定义顶部栏系统名称样式
  57. const systemNameStyle = {
  58. fontWeight: 'bold',
  59. fontSize: '18px',
  60. background: 'linear-gradient(45deg, var(--semi-color-primary), var(--semi-color-secondary))',
  61. WebkitBackgroundClip: 'text',
  62. WebkitTextFillColor: 'transparent',
  63. padding: '0 5px'
  64. };
  65. // 自定义顶部栏按钮图标样式
  66. const headerIconStyle = {
  67. fontSize: '18px',
  68. transition: 'all 0.3s ease'
  69. };
  70. // 自定义头像样式
  71. const avatarStyle = {
  72. margin: '4px',
  73. cursor: 'pointer',
  74. boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
  75. transition: 'all 0.3s ease'
  76. };
  77. // 自定义下拉菜单样式
  78. const dropdownStyle = {
  79. borderRadius: '8px',
  80. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
  81. overflow: 'hidden'
  82. };
  83. // 自定义主题切换开关样式
  84. const switchStyle = {
  85. margin: '0 8px'
  86. };
  87. const HeaderBar = () => {
  88. const { t, i18n } = useTranslation();
  89. const [userState, userDispatch] = useContext(UserContext);
  90. const [styleState, styleDispatch] = useContext(StyleContext);
  91. const [statusState, statusDispatch] = useContext(StatusContext);
  92. let navigate = useNavigate();
  93. const [currentLang, setCurrentLang] = useState(i18n.language);
  94. const systemName = getSystemName();
  95. const logo = getLogo();
  96. const currentDate = new Date();
  97. // enable fireworks on new year(1.1 and 2.9-2.24)
  98. const isNewYear =
  99. (currentDate.getMonth() === 0 && currentDate.getDate() === 1);
  100. // Check if self-use mode is enabled
  101. const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false;
  102. const docsLink = statusState?.status?.docs_link || '';
  103. const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
  104. let buttons = [
  105. {
  106. text: t('首页'),
  107. itemKey: 'home',
  108. to: '/',
  109. icon: <IconHome style={headerIconStyle} />,
  110. },
  111. {
  112. text: t('控制台'),
  113. itemKey: 'detail',
  114. to: '/',
  115. icon: <IconTerminal style={headerIconStyle} />,
  116. },
  117. {
  118. text: t('定价'),
  119. itemKey: 'pricing',
  120. to: '/pricing',
  121. icon: <IconPriceTag style={headerIconStyle} />,
  122. },
  123. // Only include the docs button if docsLink exists
  124. ...(docsLink ? [{
  125. text: t('文档'),
  126. itemKey: 'docs',
  127. isExternal: true,
  128. externalLink: docsLink,
  129. icon: <IconHelpCircle style={headerIconStyle} />,
  130. }] : []),
  131. {
  132. text: t('关于'),
  133. itemKey: 'about',
  134. to: '/about',
  135. icon: <IconInfoCircle style={headerIconStyle} />,
  136. },
  137. ];
  138. async function logout() {
  139. await API.get('/api/user/logout');
  140. showSuccess(t('注销成功!'));
  141. userDispatch({ type: 'logout' });
  142. localStorage.removeItem('user');
  143. navigate('/login');
  144. }
  145. const handleNewYearClick = () => {
  146. fireworks.init('root', {});
  147. fireworks.start();
  148. setTimeout(() => {
  149. fireworks.stop();
  150. setTimeout(() => {
  151. window.location.reload();
  152. }, 10000);
  153. }, 3000);
  154. };
  155. const theme = useTheme();
  156. const setTheme = useSetTheme();
  157. useEffect(() => {
  158. if (theme === 'dark') {
  159. document.body.setAttribute('theme-mode', 'dark');
  160. } else {
  161. document.body.removeAttribute('theme-mode');
  162. }
  163. // 发送当前主题模式给子页面
  164. const iframe = document.querySelector('iframe');
  165. if (iframe) {
  166. iframe.contentWindow.postMessage({ themeMode: theme }, '*');
  167. }
  168. if (isNewYear) {
  169. console.log('Happy New Year!');
  170. }
  171. }, [theme]);
  172. useEffect(() => {
  173. const handleLanguageChanged = (lng) => {
  174. setCurrentLang(lng);
  175. const iframe = document.querySelector('iframe');
  176. if (iframe) {
  177. iframe.contentWindow.postMessage({ lang: lng }, '*');
  178. }
  179. };
  180. i18n.on('languageChanged', handleLanguageChanged);
  181. return () => {
  182. i18n.off('languageChanged', handleLanguageChanged);
  183. };
  184. }, [i18n]);
  185. const handleLanguageChange = (lang) => {
  186. i18n.changeLanguage(lang);
  187. };
  188. return (
  189. <>
  190. <Layout>
  191. <div style={{ width: '100%' }}>
  192. <Nav
  193. className={'topnav'}
  194. mode={'horizontal'}
  195. style={headerStyle}
  196. itemStyle={headerItemStyle}
  197. hoverStyle={headerItemHoverStyle}
  198. renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
  199. const routerMap = {
  200. about: '/about',
  201. login: '/login',
  202. register: '/register',
  203. pricing: '/pricing',
  204. detail: '/detail',
  205. home: '/',
  206. chat: '/chat',
  207. };
  208. return (
  209. <div onClick={(e) => {
  210. if (props.itemKey === 'home') {
  211. styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
  212. styleDispatch({ type: 'SET_SIDER', payload: false });
  213. } else {
  214. styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
  215. if (!styleState.isMobile) {
  216. styleDispatch({ type: 'SET_SIDER', payload: true });
  217. }
  218. }
  219. }}>
  220. {props.isExternal ? (
  221. <a
  222. className="header-bar-text"
  223. style={{ textDecoration: 'none' }}
  224. href={props.externalLink}
  225. target="_blank"
  226. rel="noopener noreferrer"
  227. >
  228. {itemElement}
  229. </a>
  230. ) : (
  231. <Link
  232. className="header-bar-text"
  233. style={{ textDecoration: 'none' }}
  234. to={routerMap[props.itemKey]}
  235. >
  236. {itemElement}
  237. </Link>
  238. )}
  239. </div>
  240. );
  241. }}
  242. selectedKeys={[]}
  243. // items={headerButtons}
  244. onSelect={(key) => {}}
  245. header={styleState.isMobile?{
  246. logo: (
  247. <div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
  248. {
  249. !styleState.showSider ?
  250. <Button icon={<IconMenu />} theme="light" aria-label={t('展开侧边栏')} onClick={
  251. () => styleDispatch({ type: 'SET_SIDER', payload: true })
  252. } />:
  253. <Button icon={<IconIndentLeft />} theme="light" aria-label={t('闭侧边栏')} onClick={
  254. () => styleDispatch({ type: 'SET_SIDER', payload: false })
  255. } />
  256. }
  257. {(isSelfUseMode || isDemoSiteMode) && (
  258. <Tag
  259. color={isSelfUseMode ? 'purple' : 'blue'}
  260. style={{
  261. position: 'absolute',
  262. top: '-8px',
  263. right: '-15px',
  264. fontSize: '0.7rem',
  265. padding: '0 4px',
  266. height: 'auto',
  267. lineHeight: '1.2',
  268. zIndex: 1,
  269. pointerEvents: 'none'
  270. }}
  271. >
  272. {isSelfUseMode ? t('自用模式') : t('演示站点')}
  273. </Tag>
  274. )}
  275. </div>
  276. ),
  277. }:{
  278. logo: (
  279. <div style={logoStyle}>
  280. <img src={logo} alt='logo' style={{ height: '28px' }} />
  281. </div>
  282. ),
  283. text: (
  284. <div style={{ position: 'relative', display: 'inline-block' }}>
  285. <span style={systemNameStyle}>{systemName}</span>
  286. {(isSelfUseMode || isDemoSiteMode) && (
  287. <Tag
  288. color={isSelfUseMode ? 'purple' : 'blue'}
  289. style={{
  290. position: 'absolute',
  291. top: '-10px',
  292. right: '-25px',
  293. fontSize: '0.7rem',
  294. padding: '0 4px',
  295. whiteSpace: 'nowrap',
  296. zIndex: 1,
  297. boxShadow: '0 0 3px rgba(255, 255, 255, 0.7)'
  298. }}
  299. >
  300. {isSelfUseMode ? t('自用模式') : t('演示站点')}
  301. </Tag>
  302. )}
  303. </div>
  304. ),
  305. }}
  306. items={buttons}
  307. footer={
  308. <>
  309. {isNewYear && (
  310. // happy new year
  311. <Dropdown
  312. position='bottomRight'
  313. render={
  314. <Dropdown.Menu style={dropdownStyle}>
  315. <Dropdown.Item onClick={handleNewYearClick}>
  316. Happy New Year!!!
  317. </Dropdown.Item>
  318. </Dropdown.Menu>
  319. }
  320. >
  321. <Nav.Item itemKey={'new-year'} text={'🎉'} />
  322. </Dropdown>
  323. )}
  324. {/* <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} /> */}
  325. <>
  326. <Switch
  327. checkedText='🌞'
  328. size={styleState.isMobile?'default':'large'}
  329. checked={theme === 'dark'}
  330. uncheckedText='🌙'
  331. style={switchStyle}
  332. onChange={(checked) => {
  333. setTheme(checked);
  334. }}
  335. />
  336. </>
  337. <Dropdown
  338. position='bottomRight'
  339. render={
  340. <Dropdown.Menu style={dropdownStyle}>
  341. <Dropdown.Item
  342. onClick={() => handleLanguageChange('zh')}
  343. type={currentLang === 'zh' ? 'primary' : 'tertiary'}
  344. >
  345. 中文
  346. </Dropdown.Item>
  347. <Dropdown.Item
  348. onClick={() => handleLanguageChange('en')}
  349. type={currentLang === 'en' ? 'primary' : 'tertiary'}
  350. >
  351. English
  352. </Dropdown.Item>
  353. </Dropdown.Menu>
  354. }
  355. >
  356. <Nav.Item
  357. itemKey={'language'}
  358. icon={<IconLanguage style={headerIconStyle} />}
  359. />
  360. </Dropdown>
  361. {userState.user ? (
  362. <>
  363. <Dropdown
  364. position='bottomRight'
  365. render={
  366. <Dropdown.Menu style={dropdownStyle}>
  367. <Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item>
  368. </Dropdown.Menu>
  369. }
  370. >
  371. <Avatar
  372. size='small'
  373. color={stringToColor(userState.user.username)}
  374. style={avatarStyle}
  375. >
  376. {userState.user.username[0]}
  377. </Avatar>
  378. {styleState.isMobile?null:<Text style={{ marginLeft: '4px', fontWeight: '500' }}>{userState.user.username}</Text>}
  379. </Dropdown>
  380. </>
  381. ) : (
  382. <>
  383. <Nav.Item
  384. itemKey={'login'}
  385. text={!styleState.isMobile?t('登录'):null}
  386. icon={<IconUser style={headerIconStyle} />}
  387. />
  388. {
  389. // Hide register option in self-use mode
  390. !styleState.isMobile && !isSelfUseMode && (
  391. <Nav.Item
  392. itemKey={'register'}
  393. text={t('注册')}
  394. icon={<IconKey style={headerIconStyle} />}
  395. />
  396. )
  397. }
  398. </>
  399. )}
  400. </>
  401. }
  402. ></Nav>
  403. </div>
  404. </Layout>
  405. </>
  406. );
  407. };
  408. export default HeaderBar;