| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- import React, { useContext, useEffect, useMemo, useState } from 'react';
- import { Link, useNavigate, useLocation } from 'react-router-dom';
- import { UserContext } from '../context/User';
- import { StatusContext } from '../context/Status';
- import { useTranslation } from 'react-i18next';
- import {
- API,
- getLogo,
- getSystemName,
- isAdmin,
- isMobile,
- showError,
- } from '../helpers';
- import '../index.css';
- import {
- IconCalendarClock, IconChecklistStroked,
- IconComment, IconCommentStroked,
- IconCreditCard,
- IconGift, IconHelpCircle,
- IconHistogram,
- IconHome,
- IconImage,
- IconKey,
- IconLayers,
- IconPriceTag,
- IconSetting,
- IconUser
- } from '@douyinfe/semi-icons';
- import { Avatar, Dropdown, Layout, Nav, Switch, Divider } from '@douyinfe/semi-ui';
- import { setStatusData } from '../helpers/data.js';
- import { stringToColor } from '../helpers/render.js';
- import { useSetTheme, useTheme } from '../context/Theme/index.js';
- import { StyleContext } from '../context/Style/index.js';
- import Text from '@douyinfe/semi-ui/lib/es/typography/text';
- // 自定义侧边栏按钮样式
- const navItemStyle = {
- borderRadius: '6px',
- margin: '4px 8px',
- transition: 'all 0.3s ease'
- };
- // 自定义侧边栏按钮悬停样式
- const navItemHoverStyle = {
- backgroundColor: 'var(--semi-color-primary-light-default)',
- color: 'var(--semi-color-primary)'
- };
- // 自定义侧边栏按钮选中样式
- const navItemSelectedStyle = {
- backgroundColor: 'var(--semi-color-primary-light-default)',
- color: 'var(--semi-color-primary)',
- fontWeight: '600'
- };
- // 自定义图标样式
- const iconStyle = (itemKey, selectedKeys) => {
- return {
- fontSize: '18px',
- color: selectedKeys.includes(itemKey) ? 'var(--semi-color-primary)' : 'var(--semi-color-text-2)',
- transition: 'all 0.3s ease'
- };
- };
- const SiderBar = () => {
- const { t } = useTranslation();
- const [styleState, styleDispatch] = useContext(StyleContext);
- const [statusState, statusDispatch] = useContext(StatusContext);
- const defaultIsCollapsed =
- localStorage.getItem('default_collapse_sidebar') === 'true';
- const [selectedKeys, setSelectedKeys] = useState(['home']);
- const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
- const [chatItems, setChatItems] = useState([]);
- const [openedKeys, setOpenedKeys] = useState([]);
- const theme = useTheme();
- const setTheme = useSetTheme();
- const location = useLocation();
- // 预先计算所有可能的图标样式
- const allItemKeys = useMemo(() => {
- const keys = ['home', 'channel', 'token', 'redemption', 'topup', 'user', 'log', 'midjourney',
- 'setting', 'about', 'chat', 'detail', 'pricing', 'task', 'playground', 'personal'];
- // 添加聊天项的keys
- for (let i = 0; i < chatItems.length; i++) {
- keys.push('chat' + i);
- }
- return keys;
- }, [chatItems]);
- // 使用useMemo一次性计算所有图标样式
- const iconStyles = useMemo(() => {
- const styles = {};
- allItemKeys.forEach(key => {
- styles[key] = iconStyle(key, selectedKeys);
- });
- return styles;
- }, [allItemKeys, selectedKeys]);
- const routerMap = {
- home: '/',
- channel: '/channel',
- token: '/token',
- redemption: '/redemption',
- topup: '/topup',
- user: '/user',
- log: '/log',
- midjourney: '/midjourney',
- setting: '/setting',
- about: '/about',
- chat: '/chat',
- detail: '/detail',
- pricing: '/pricing',
- task: '/task',
- playground: '/playground',
- personal: '/personal',
- };
- const workspaceItems = useMemo(
- () => [
- {
- text: t('数据看板'),
- itemKey: 'detail',
- to: '/detail',
- icon: <IconCalendarClock />,
- className:
- localStorage.getItem('enable_data_export') === 'true'
- ? ''
- : 'tableHiddle',
- },
- {
- text: t('API令牌'),
- itemKey: 'token',
- to: '/token',
- icon: <IconKey />,
- },
- {
- text: t('使用日志'),
- itemKey: 'log',
- to: '/log',
- icon: <IconHistogram />,
- },
- {
- text: t('绘图日志'),
- itemKey: 'midjourney',
- to: '/midjourney',
- icon: <IconImage />,
- className:
- localStorage.getItem('enable_drawing') === 'true'
- ? ''
- : 'tableHiddle',
- },
- {
- text: t('任务日志'),
- itemKey: 'task',
- to: '/task',
- icon: <IconChecklistStroked />,
- className:
- localStorage.getItem('enable_task') === 'true'
- ? ''
- : 'tableHiddle',
- }
- ],
- [
- localStorage.getItem('enable_data_export'),
- localStorage.getItem('enable_drawing'),
- localStorage.getItem('enable_task'),
- t,
- ],
- );
- const financeItems = useMemo(
- () => [
- {
- text: t('钱包'),
- itemKey: 'topup',
- to: '/topup',
- icon: <IconCreditCard />,
- },
- {
- text: t('个人设置'),
- itemKey: 'personal',
- to: '/personal',
- icon: <IconUser />,
- },
- ],
- [t],
- );
- const adminItems = useMemo(
- () => [
- {
- text: t('渠道'),
- itemKey: 'channel',
- to: '/channel',
- icon: <IconLayers />,
- className: isAdmin() ? '' : 'tableHiddle',
- },
- {
- text: t('兑换码'),
- itemKey: 'redemption',
- to: '/redemption',
- icon: <IconGift />,
- className: isAdmin() ? '' : 'tableHiddle',
- },
- {
- text: t('用户管理'),
- itemKey: 'user',
- to: '/user',
- icon: <IconUser />,
- },
- {
- text: t('系统设置'),
- itemKey: 'setting',
- to: '/setting',
- icon: <IconSetting />,
- },
- ],
- [isAdmin(), t],
- );
- const chatMenuItems = useMemo(
- () => [
- {
- text: 'Playground',
- itemKey: 'playground',
- to: '/playground',
- icon: <IconCommentStroked />,
- },
- {
- text: t('聊天'),
- itemKey: 'chat',
- items: chatItems,
- icon: <IconComment />,
- },
- ],
- [chatItems, t],
- );
- useEffect(() => {
- const currentPath = location.pathname;
- const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
-
- if (matchingKey) {
- setSelectedKeys([matchingKey]);
- } else if (currentPath.startsWith('/chat/')) {
- setSelectedKeys(['chat']);
- }
- let chats = localStorage.getItem('chats');
- if (chats) {
- // console.log(chats);
- try {
- chats = JSON.parse(chats);
- if (Array.isArray(chats)) {
- let chatItems = [];
- for (let i = 0; i < chats.length; i++) {
- let chat = {};
- for (let key in chats[i]) {
- chat.text = key;
- chat.itemKey = 'chat' + i;
- chat.to = '/chat/' + i;
- }
- // setRouterMap({ ...routerMap, chat: '/chat/' + i })
- chatItems.push(chat);
- }
- setChatItems(chatItems);
- }
- } catch (e) {
- console.error(e);
- showError('聊天数据解析失败')
- }
- }
- setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true');
- }, [location.pathname]);
- // Custom divider style
- const dividerStyle = {
- margin: '8px 0',
- opacity: 0.6,
- };
- // Custom group label style
- const groupLabelStyle = {
- padding: '8px 16px',
- color: 'var(--semi-color-text-2)',
- fontSize: '12px',
- fontWeight: 'bold',
- textTransform: 'uppercase',
- letterSpacing: '0.5px',
- };
- return (
- <>
- <Nav
- className="custom-sidebar-nav"
- style={{
- width: isCollapsed ? '60px' : '200px',
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
- borderRight: '1px solid var(--semi-color-border)',
- background: 'var(--semi-color-bg-1)',
- borderRadius: styleState.isMobile ? '0' : '0 8px 8px 0',
- transition: 'all 0.3s ease',
- position: 'relative',
- zIndex: 95,
- height: '100%',
- overflowY: 'auto',
- WebkitOverflowScrolling: 'touch', // Improve scrolling on iOS devices
- }}
- defaultIsCollapsed={
- localStorage.getItem('default_collapse_sidebar') === 'true'
- }
- isCollapsed={isCollapsed}
- onCollapseChange={(collapsed) => {
- setIsCollapsed(collapsed);
- localStorage.setItem('default_collapse_sidebar', collapsed);
- // 始终保持侧边栏显示,只是宽度不同
- styleDispatch({ type: 'SET_SIDER', payload: true });
-
- // 确保在收起侧边栏时有选中的项目,避免不必要的计算
- if (selectedKeys.length === 0) {
- const currentPath = location.pathname;
- const matchingKey = Object.keys(routerMap).find(key => routerMap[key] === currentPath);
-
- if (matchingKey) {
- setSelectedKeys([matchingKey]);
- } else if (currentPath.startsWith('/chat/')) {
- setSelectedKeys(['chat']);
- } else {
- setSelectedKeys(['home']); // 默认选中首页
- }
- }
- }}
- selectedKeys={selectedKeys}
- itemStyle={navItemStyle}
- hoverStyle={navItemHoverStyle}
- selectedStyle={navItemSelectedStyle}
- renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
- let chats = localStorage.getItem('chats');
- if (chats) {
- chats = JSON.parse(chats);
- if (Array.isArray(chats) && chats.length > 0) {
- for (let i = 0; i < chats.length; i++) {
- routerMap['chat' + i] = '/chat/' + i;
- }
- if (chats.length > 1) {
- // delete /chat
- if (routerMap['chat']) {
- delete routerMap['chat'];
- }
- } else {
- // rename /chat to /chat/0
- routerMap['chat'] = '/chat/0';
- }
- }
- }
- return (
- <Link
- style={{ textDecoration: 'none' }}
- to={routerMap[props.itemKey]}
- >
- {itemElement}
- </Link>
- );
- }}
- onSelect={(key) => {
- if (key.itemKey.toString().startsWith('chat')) {
- styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
- } else {
- styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
- }
-
- // 如果点击的是已经展开的子菜单的父项,则收起子菜单
- if (openedKeys.includes(key.itemKey)) {
- setOpenedKeys(openedKeys.filter(k => k !== key.itemKey));
- }
-
- setSelectedKeys([key.itemKey]);
- }}
- openKeys={openedKeys}
- onOpenChange={(data) => {
- setOpenedKeys(data.openKeys);
- }}
- >
- {/* Chat Section - Only show if there are chat items */}
- {chatMenuItems.map((item) => {
- if (item.items && item.items.length > 0) {
- return (
- <Nav.Sub
- key={item.itemKey}
- itemKey={item.itemKey}
- text={item.text}
- icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
- >
- {item.items.map((subItem) => (
- <Nav.Item
- key={subItem.itemKey}
- itemKey={subItem.itemKey}
- text={subItem.text}
- />
- ))}
- </Nav.Sub>
- );
- } else {
- return (
- <Nav.Item
- key={item.itemKey}
- itemKey={item.itemKey}
- text={item.text}
- icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
- />
- );
- }
- })}
- {/* Divider */}
- <Divider style={dividerStyle} />
- {/* Workspace Section */}
- {!isCollapsed && <Text style={groupLabelStyle}>{t('控制台')}</Text>}
- {workspaceItems.map((item) => (
- <Nav.Item
- key={item.itemKey}
- itemKey={item.itemKey}
- text={item.text}
- icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
- className={item.className}
- />
- ))}
- {isAdmin() && (
- <>
- {/* Divider */}
- <Divider style={dividerStyle} />
- {/* Admin Section */}
- {!isCollapsed && <Text style={groupLabelStyle}>{t('管理员')}</Text>}
- {adminItems.map((item) => (
- <Nav.Item
- key={item.itemKey}
- itemKey={item.itemKey}
- text={item.text}
- icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
- className={item.className}
- />
- ))}
- </>
- )}
- {/* Divider */}
- <Divider style={dividerStyle} />
- {/* Finance Management Section */}
- {!isCollapsed && <Text style={groupLabelStyle}>{t('个人中心')}</Text>}
- {financeItems.map((item) => (
- <Nav.Item
- key={item.itemKey}
- itemKey={item.itemKey}
- text={item.text}
- icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
- className={item.className}
- />
- ))}
- <Nav.Footer
- style={{
- paddingBottom: styleState?.isMobile ? '112px' : '0',
- }}
- collapseButton={true}
- collapseText={(collapsed)=>
- {
- if(collapsed){
- return t('展开侧边栏')
- }
- return t('收起侧边栏')
- }
- }
- />
- </Nav>
- </>
- );
- };
- export default SiderBar;
|