SiderBar.js 14 KB

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