SiderBar.js 13 KB

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