SiderBar.js 14 KB

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