SiderBar.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. import React, { useContext, useEffect, useMemo, useState } from 'react';
  2. import { Link, useNavigate, useLocation } from 'react-router-dom';
  3. import { UserContext } from '../context/User';
  4. import { StatusContext } from '../context/Status';
  5. import { useTranslation } from 'react-i18next';
  6. import {
  7. API,
  8. getLogo,
  9. getSystemName,
  10. isAdmin,
  11. isMobile,
  12. showError,
  13. } from '../helpers';
  14. import '../index.css';
  15. import {
  16. IconCalendarClock,
  17. IconChecklistStroked,
  18. IconComment,
  19. IconCommentStroked,
  20. IconCreditCard,
  21. IconGift,
  22. IconHelpCircle,
  23. IconHistogram,
  24. IconHome,
  25. IconImage,
  26. IconKey,
  27. IconLayers,
  28. IconPriceTag,
  29. IconSetting,
  30. IconUser,
  31. } from '@douyinfe/semi-icons';
  32. import {
  33. Avatar,
  34. Dropdown,
  35. Layout,
  36. Nav,
  37. Switch,
  38. Divider,
  39. } from '@douyinfe/semi-ui';
  40. import { setStatusData } from '../helpers/data.js';
  41. import { stringToColor } from '../helpers/render.js';
  42. import { useSetTheme, useTheme } from '../context/Theme/index.js';
  43. import { StyleContext } from '../context/Style/index.js';
  44. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  45. // 自定义侧边栏按钮样式
  46. const navItemStyle = {
  47. borderRadius: '6px',
  48. margin: '4px 8px',
  49. };
  50. // 自定义侧边栏按钮悬停样式
  51. const navItemHoverStyle = {
  52. backgroundColor: 'var(--semi-color-primary-light-default)',
  53. color: 'var(--semi-color-primary)',
  54. };
  55. // 自定义侧边栏按钮选中样式
  56. const navItemSelectedStyle = {
  57. backgroundColor: 'var(--semi-color-primary-light-default)',
  58. color: 'var(--semi-color-primary)',
  59. fontWeight: '600',
  60. };
  61. // 自定义图标样式
  62. const iconStyle = (itemKey, selectedKeys) => {
  63. return {
  64. fontSize: '18px',
  65. color: selectedKeys.includes(itemKey)
  66. ? 'var(--semi-color-primary)'
  67. : 'var(--semi-color-text-2)',
  68. };
  69. };
  70. // Define routerMap as a constant outside the component
  71. const routerMap = {
  72. home: '/',
  73. channel: '/channel',
  74. token: '/token',
  75. redemption: '/redemption',
  76. topup: '/topup',
  77. user: '/user',
  78. log: '/log',
  79. midjourney: '/midjourney',
  80. setting: '/setting',
  81. about: '/about',
  82. detail: '/detail',
  83. pricing: '/pricing',
  84. task: '/task',
  85. playground: '/playground',
  86. personal: '/personal',
  87. };
  88. const SiderBar = () => {
  89. const { t } = useTranslation();
  90. const [styleState, styleDispatch] = useContext(StyleContext);
  91. const [statusState, statusDispatch] = useContext(StatusContext);
  92. const defaultIsCollapsed =
  93. localStorage.getItem('default_collapse_sidebar') === 'true';
  94. const [selectedKeys, setSelectedKeys] = useState(['home']);
  95. const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
  96. const [chatItems, setChatItems] = useState([]);
  97. const [openedKeys, setOpenedKeys] = useState([]);
  98. const theme = useTheme();
  99. const setTheme = useSetTheme();
  100. const location = useLocation();
  101. const [routerMapState, setRouterMapState] = useState(routerMap);
  102. // 预先计算所有可能的图标样式
  103. const allItemKeys = useMemo(() => {
  104. const keys = [
  105. 'home',
  106. 'channel',
  107. 'token',
  108. 'redemption',
  109. 'topup',
  110. 'user',
  111. 'log',
  112. 'midjourney',
  113. 'setting',
  114. 'about',
  115. 'chat',
  116. 'detail',
  117. 'pricing',
  118. 'task',
  119. 'playground',
  120. 'personal',
  121. ];
  122. // 添加聊天项的keys
  123. for (let i = 0; i < chatItems.length; i++) {
  124. keys.push('chat' + i);
  125. }
  126. return keys;
  127. }, [chatItems]);
  128. // 使用useMemo一次性计算所有图标样式
  129. const iconStyles = useMemo(() => {
  130. const styles = {};
  131. allItemKeys.forEach((key) => {
  132. styles[key] = iconStyle(key, selectedKeys);
  133. });
  134. return styles;
  135. }, [allItemKeys, selectedKeys]);
  136. const workspaceItems = useMemo(
  137. () => [
  138. {
  139. text: t('数据看板'),
  140. itemKey: 'detail',
  141. to: '/detail',
  142. icon: <IconCalendarClock />,
  143. className:
  144. localStorage.getItem('enable_data_export') === 'true'
  145. ? ''
  146. : 'tableHiddle',
  147. },
  148. {
  149. text: t('API令牌'),
  150. itemKey: 'token',
  151. to: '/token',
  152. icon: <IconKey />,
  153. },
  154. {
  155. text: t('使用日志'),
  156. itemKey: 'log',
  157. to: '/log',
  158. icon: <IconHistogram />,
  159. },
  160. {
  161. text: t('绘图日志'),
  162. itemKey: 'midjourney',
  163. to: '/midjourney',
  164. icon: <IconImage />,
  165. className:
  166. localStorage.getItem('enable_drawing') === 'true'
  167. ? ''
  168. : 'tableHiddle',
  169. },
  170. {
  171. text: t('任务日志'),
  172. itemKey: 'task',
  173. to: '/task',
  174. icon: <IconChecklistStroked />,
  175. className:
  176. localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
  177. },
  178. ],
  179. [
  180. localStorage.getItem('enable_data_export'),
  181. localStorage.getItem('enable_drawing'),
  182. localStorage.getItem('enable_task'),
  183. t,
  184. ],
  185. );
  186. const financeItems = useMemo(
  187. () => [
  188. {
  189. text: t('钱包'),
  190. itemKey: 'topup',
  191. to: '/topup',
  192. icon: <IconCreditCard />,
  193. },
  194. {
  195. text: t('个人设置'),
  196. itemKey: 'personal',
  197. to: '/personal',
  198. icon: <IconUser />,
  199. },
  200. ],
  201. [t],
  202. );
  203. const adminItems = useMemo(
  204. () => [
  205. {
  206. text: t('渠道'),
  207. itemKey: 'channel',
  208. to: '/channel',
  209. icon: <IconLayers />,
  210. className: isAdmin() ? '' : 'tableHiddle',
  211. },
  212. {
  213. text: t('兑换码'),
  214. itemKey: 'redemption',
  215. to: '/redemption',
  216. icon: <IconGift />,
  217. className: isAdmin() ? '' : 'tableHiddle',
  218. },
  219. {
  220. text: t('用户管理'),
  221. itemKey: 'user',
  222. to: '/user',
  223. icon: <IconUser />,
  224. },
  225. {
  226. text: t('系统设置'),
  227. itemKey: 'setting',
  228. to: '/setting',
  229. icon: <IconSetting />,
  230. },
  231. ],
  232. [isAdmin(), t],
  233. );
  234. const chatMenuItems = useMemo(
  235. () => [
  236. {
  237. text: 'Playground',
  238. itemKey: 'playground',
  239. to: '/playground',
  240. icon: <IconCommentStroked />,
  241. },
  242. {
  243. text: t('聊天'),
  244. itemKey: 'chat',
  245. items: chatItems,
  246. icon: <IconComment />,
  247. },
  248. ],
  249. [chatItems, t],
  250. );
  251. // Function to update router map with chat routes
  252. const updateRouterMapWithChats = (chats) => {
  253. const newRouterMap = { ...routerMap };
  254. if (Array.isArray(chats) && chats.length > 0) {
  255. for (let i = 0; i < chats.length; i++) {
  256. newRouterMap['chat' + i] = '/chat/' + i;
  257. }
  258. }
  259. setRouterMapState(newRouterMap);
  260. return newRouterMap;
  261. };
  262. // Update the useEffect for chat items
  263. useEffect(() => {
  264. let chats = localStorage.getItem('chats');
  265. if (chats) {
  266. try {
  267. chats = JSON.parse(chats);
  268. if (Array.isArray(chats)) {
  269. let chatItems = [];
  270. for (let i = 0; i < chats.length; i++) {
  271. let chat = {};
  272. for (let key in chats[i]) {
  273. chat.text = key;
  274. chat.itemKey = 'chat' + i;
  275. chat.to = '/chat/' + i;
  276. }
  277. chatItems.push(chat);
  278. }
  279. setChatItems(chatItems);
  280. // Update router map with chat routes
  281. updateRouterMapWithChats(chats);
  282. }
  283. } catch (e) {
  284. console.error(e);
  285. showError('聊天数据解析失败');
  286. }
  287. }
  288. }, []);
  289. // Update the useEffect for route selection
  290. useEffect(() => {
  291. const currentPath = location.pathname;
  292. let matchingKey = Object.keys(routerMapState).find(
  293. (key) => routerMapState[key] === currentPath,
  294. );
  295. // Handle chat routes
  296. if (!matchingKey && currentPath.startsWith('/chat/')) {
  297. const chatIndex = currentPath.split('/').pop();
  298. if (!isNaN(chatIndex)) {
  299. matchingKey = 'chat' + chatIndex;
  300. } else {
  301. matchingKey = 'chat';
  302. }
  303. }
  304. // If we found a matching key, update the selected keys
  305. if (matchingKey) {
  306. setSelectedKeys([matchingKey]);
  307. }
  308. }, [location.pathname, routerMapState]);
  309. useEffect(() => {
  310. setIsCollapsed(styleState.siderCollapsed);
  311. }, [styleState.siderCollapsed]);
  312. // Custom divider style
  313. const dividerStyle = {
  314. margin: '8px 0',
  315. opacity: 0.6,
  316. };
  317. // Custom group label style
  318. const groupLabelStyle = {
  319. padding: '8px 16px',
  320. color: 'var(--semi-color-text-2)',
  321. fontSize: '12px',
  322. fontWeight: 'bold',
  323. textTransform: 'uppercase',
  324. letterSpacing: '0.5px',
  325. };
  326. return (
  327. <>
  328. <Nav
  329. className='custom-sidebar-nav'
  330. style={{
  331. width: isCollapsed ? '60px' : '200px',
  332. boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
  333. borderRight: '1px solid var(--semi-color-border)',
  334. background: 'var(--semi-color-bg-1)',
  335. borderRadius: styleState.isMobile ? '0' : '0 8px 8px 0',
  336. position: 'relative',
  337. zIndex: 95,
  338. height: '100%',
  339. overflowY: 'auto',
  340. WebkitOverflowScrolling: 'touch', // Improve scrolling on iOS devices
  341. }}
  342. defaultIsCollapsed={
  343. localStorage.getItem('default_collapse_sidebar') === 'true'
  344. }
  345. isCollapsed={isCollapsed}
  346. onCollapseChange={(collapsed) => {
  347. setIsCollapsed(collapsed);
  348. // styleDispatch({ type: 'SET_SIDER', payload: true });
  349. styleDispatch({ type: 'SET_SIDER_COLLAPSED', payload: collapsed });
  350. localStorage.setItem('default_collapse_sidebar', collapsed);
  351. // 确保在收起侧边栏时有选中的项目,避免不必要的计算
  352. if (selectedKeys.length === 0) {
  353. const currentPath = location.pathname;
  354. const matchingKey = Object.keys(routerMapState).find(
  355. (key) => routerMapState[key] === currentPath,
  356. );
  357. if (matchingKey) {
  358. setSelectedKeys([matchingKey]);
  359. } else if (currentPath.startsWith('/chat/')) {
  360. setSelectedKeys(['chat']);
  361. } else {
  362. setSelectedKeys(['detail']); // 默认选中首页
  363. }
  364. }
  365. }}
  366. selectedKeys={selectedKeys}
  367. itemStyle={navItemStyle}
  368. hoverStyle={navItemHoverStyle}
  369. selectedStyle={navItemSelectedStyle}
  370. renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
  371. return (
  372. <Link
  373. style={{ textDecoration: 'none' }}
  374. to={routerMapState[props.itemKey] || routerMap[props.itemKey]}
  375. >
  376. {itemElement}
  377. </Link>
  378. );
  379. }}
  380. onSelect={(key) => {
  381. if (key.itemKey.toString().startsWith('chat')) {
  382. styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
  383. } else {
  384. styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
  385. }
  386. // 如果点击的是已经展开的子菜单的父项,则收起子菜单
  387. if (openedKeys.includes(key.itemKey)) {
  388. setOpenedKeys(openedKeys.filter((k) => k !== key.itemKey));
  389. }
  390. setSelectedKeys([key.itemKey]);
  391. }}
  392. openKeys={openedKeys}
  393. onOpenChange={(data) => {
  394. setOpenedKeys(data.openKeys);
  395. }}
  396. >
  397. {/* Chat Section - Only show if there are chat items */}
  398. {chatMenuItems.map((item) => {
  399. if (item.items && item.items.length > 0) {
  400. return (
  401. <Nav.Sub
  402. key={item.itemKey}
  403. itemKey={item.itemKey}
  404. text={item.text}
  405. icon={React.cloneElement(item.icon, {
  406. style: iconStyles[item.itemKey],
  407. })}
  408. >
  409. {item.items.map((subItem) => (
  410. <Nav.Item
  411. key={subItem.itemKey}
  412. itemKey={subItem.itemKey}
  413. text={subItem.text}
  414. />
  415. ))}
  416. </Nav.Sub>
  417. );
  418. } else {
  419. return (
  420. <Nav.Item
  421. key={item.itemKey}
  422. itemKey={item.itemKey}
  423. text={item.text}
  424. icon={React.cloneElement(item.icon, {
  425. style: iconStyles[item.itemKey],
  426. })}
  427. />
  428. );
  429. }
  430. })}
  431. {/* Divider */}
  432. <Divider style={dividerStyle} />
  433. {/* Workspace Section */}
  434. {!isCollapsed && <Text style={groupLabelStyle}>{t('控制台')}</Text>}
  435. {workspaceItems.map((item) => (
  436. <Nav.Item
  437. key={item.itemKey}
  438. itemKey={item.itemKey}
  439. text={item.text}
  440. icon={React.cloneElement(item.icon, {
  441. style: iconStyles[item.itemKey],
  442. })}
  443. className={item.className}
  444. />
  445. ))}
  446. {isAdmin() && (
  447. <>
  448. {/* Divider */}
  449. <Divider style={dividerStyle} />
  450. {/* Admin Section */}
  451. {!isCollapsed && <Text style={groupLabelStyle}>{t('管理员')}</Text>}
  452. {adminItems.map((item) => (
  453. <Nav.Item
  454. key={item.itemKey}
  455. itemKey={item.itemKey}
  456. text={item.text}
  457. icon={React.cloneElement(item.icon, {
  458. style: iconStyles[item.itemKey],
  459. })}
  460. className={item.className}
  461. />
  462. ))}
  463. </>
  464. )}
  465. {/* Divider */}
  466. <Divider style={dividerStyle} />
  467. {/* Finance Management Section */}
  468. {!isCollapsed && <Text style={groupLabelStyle}>{t('个人中心')}</Text>}
  469. {financeItems.map((item) => (
  470. <Nav.Item
  471. key={item.itemKey}
  472. itemKey={item.itemKey}
  473. text={item.text}
  474. icon={React.cloneElement(item.icon, {
  475. style: iconStyles[item.itemKey],
  476. })}
  477. className={item.className}
  478. />
  479. ))}
  480. <Nav.Footer
  481. style={{
  482. paddingBottom: styleState?.isMobile ? '112px' : '',
  483. }}
  484. collapseButton={true}
  485. collapseText={(collapsed) => {
  486. if (collapsed) {
  487. return t('展开侧边栏');
  488. }
  489. return t('收起侧边栏');
  490. }}
  491. />
  492. </Nav>
  493. </>
  494. );
  495. };
  496. export default SiderBar;