refactor: layout logic to enhance front-end responsiveness

Merge pull request #1377 from QuantumNous/refactor/layout
This commit is contained in:
同語
2025-07-16 04:53:15 +08:00
committed by GitHub
35 changed files with 277 additions and 432 deletions

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers'; import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
@@ -7,22 +7,28 @@ import Loading from '../common/Loading';
const OAuth2Callback = (props) => { const OAuth2Callback = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [, userDispatch] = useContext(UserContext);
const navigate = useNavigate();
const [userState, userDispatch] = useContext(UserContext); // 最大重试次数
const [prompt, setPrompt] = useState(t('处理中...')); const MAX_RETRIES = 3;
let navigate = useNavigate(); const sendCode = async (code, state, retry = 0) => {
try {
const { data: resData } = await API.get(
`/api/oauth/${props.type}?code=${code}&state=${state}`,
);
const { success, message, data } = resData;
if (!success) {
throw new Error(message || 'OAuth2 callback error');
}
const sendCode = async (code, state, count) => {
const res = await API.get(
`/api/oauth/${props.type}?code=${code}&state=${state}`,
);
const { success, message, data } = res.data;
if (success) {
if (message === 'bind') { if (message === 'bind') {
showSuccess(t('绑定成功!')); showSuccess(t('绑定成功!'));
navigate('/console/setting'); navigate('/console/personal');
} else { } else {
userDispatch({ type: 'login', payload: data }); userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data)); localStorage.setItem('user', JSON.stringify(data));
@@ -31,27 +37,34 @@ const OAuth2Callback = (props) => {
showSuccess(t('登录成功!')); showSuccess(t('登录成功!'));
navigate('/console/token'); navigate('/console/token');
} }
} else { } catch (error) {
showError(message); if (retry < MAX_RETRIES) {
if (count === 0) { // 递增的退避等待
setPrompt(t('操作失败,重定向至登录界面中...')); await new Promise((resolve) => setTimeout(resolve, (retry + 1) * 2000));
navigate('/console/setting'); // in case this is failed to bind GitHub return sendCode(code, state, retry + 1);
return;
} }
count++;
setPrompt(t('出现错误,第 ${count} 次重试中...', { count })); // 重试次数耗尽,提示错误并返回设置页面
await new Promise((resolve) => setTimeout(resolve, count * 2000)); showError(error.message || t('授权失败'));
await sendCode(code, state, count); navigate('/console/personal');
} }
}; };
useEffect(() => { useEffect(() => {
let code = searchParams.get('code'); const code = searchParams.get('code');
let state = searchParams.get('state'); const state = searchParams.get('state');
sendCode(code, state, 0).then();
// 参数缺失直接返回
if (!code) {
showError(t('未获取到授权码'));
navigate('/console/personal');
return;
}
sendCode(code, state);
}, []); }, []);
return <Loading prompt={prompt} />; return <Loading />;
}; };
export default OAuth2Callback; export default OAuth2Callback;

View File

@@ -1,22 +1,14 @@
import React from 'react'; import React from 'react';
import { Spin } from '@douyinfe/semi-ui'; import { Spin } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
const Loading = ({ prompt: name = '', size = 'large' }) => { const Loading = ({ size = 'small' }) => {
const { t } = useTranslation();
return ( return (
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]"> <div className="fixed inset-0 w-screen h-screen flex items-center justify-center">
<div className="flex flex-col items-center"> <Spin
<Spin size={size}
size={size} spinning={true}
spinning={true} />
tip={null}
/>
<span className="whitespace-nowrap mt-2 text-center" style={{ color: 'var(--semi-color-primary)' }}>
{name ? t('{{name}}', { name }) : t('加载中...')}
</span>
</div>
</div> </div>
); );
}; };

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState, useRef } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
import { useSetTheme, useTheme } from '../../context/Theme/index.js'; import { useSetTheme, useTheme } from '../../context/Theme/index.js';
@@ -31,13 +31,15 @@ import {
Badge, Badge,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { StatusContext } from '../../context/Status/index.js'; import { StatusContext } from '../../context/Status/index.js';
import { useStyle, styleActions } from '../../context/Style/index.js'; import { useIsMobile } from '../../hooks/useIsMobile.js';
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
const HeaderBar = () => { const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
const { state: styleState, dispatch: styleDispatch } = useStyle(); const isMobile = useIsMobile();
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
let navigate = useNavigate(); let navigate = useNavigate();
const [currentLang, setCurrentLang] = useState(i18n.language); const [currentLang, setCurrentLang] = useState(i18n.language);
@@ -45,6 +47,7 @@ const HeaderBar = () => {
const location = useLocation(); const location = useLocation();
const [noticeVisible, setNoticeVisible] = useState(false); const [noticeVisible, setNoticeVisible] = useState(false);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const loadingStartRef = useRef(Date.now());
const systemName = getSystemName(); const systemName = getSystemName();
const logo = getLogo(); const logo = getLogo();
@@ -194,11 +197,15 @@ const HeaderBar = () => {
}, [i18n]); }, [i18n]);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { if (statusState?.status !== undefined) {
setIsLoading(false); const elapsed = Date.now() - loadingStartRef.current;
}, 500); const remaining = Math.max(0, 500 - elapsed);
return () => clearTimeout(timer); const timer = setTimeout(() => {
}, []); setIsLoading(false);
}, remaining);
return () => clearTimeout(timer);
}
}, [statusState?.status]);
const handleLanguageChange = (lang) => { const handleLanguageChange = (lang) => {
i18n.changeLanguage(lang); i18n.changeLanguage(lang);
@@ -207,7 +214,7 @@ const HeaderBar = () => {
const handleNavLinkClick = (itemKey) => { const handleNavLinkClick = (itemKey) => {
if (itemKey === 'home') { if (itemKey === 'home') {
styleDispatch(styleActions.setSider(false)); // styleDispatch(styleActions.setSider(false)); // This line is removed
} }
setMobileMenuOpen(false); setMobileMenuOpen(false);
}; };
@@ -293,7 +300,7 @@ const HeaderBar = () => {
placeholder={ placeholder={
<Skeleton.Title <Skeleton.Title
active active
style={{ width: styleState.isMobile ? 15 : 50, height: 12 }} style={{ width: isMobile ? 15 : 50, height: 12 }}
/> />
} }
/> />
@@ -388,7 +395,7 @@ const HeaderBar = () => {
const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5"; const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5";
if (showRegisterButton) { if (showRegisterButton) {
if (styleState.isMobile) { if (isMobile) {
loginButtonClasses += " !rounded-full"; loginButtonClasses += " !rounded-full";
} else { } else {
loginButtonClasses += " !rounded-l-full !rounded-r-none"; loginButtonClasses += " !rounded-l-full !rounded-r-none";
@@ -436,7 +443,7 @@ const HeaderBar = () => {
<NoticeModal <NoticeModal
visible={noticeVisible} visible={noticeVisible}
onClose={handleNoticeClose} onClose={handleNoticeClose}
isMobile={styleState.isMobile} isMobile={isMobile}
defaultTab={unreadCount > 0 ? 'system' : 'inApp'} defaultTab={unreadCount > 0 ? 'system' : 'inApp'}
unreadKeys={getUnreadKeys()} unreadKeys={getUnreadKeys()}
/> />
@@ -447,18 +454,18 @@ const HeaderBar = () => {
<Button <Button
icon={ icon={
isConsoleRoute isConsoleRoute
? (styleState.showSider ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />) ? ((isMobile ? drawerOpen : collapsed) ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
: (mobileMenuOpen ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />) : (mobileMenuOpen ? <IconClose className="text-lg" /> : <IconMenu className="text-lg" />)
} }
aria-label={ aria-label={
isConsoleRoute isConsoleRoute
? (styleState.showSider ? t('关闭侧边栏') : t('打开侧边栏')) ? ((isMobile ? drawerOpen : collapsed) ? t('关闭侧边栏') : t('打开侧边栏'))
: (mobileMenuOpen ? t('关闭菜单') : t('打开菜单')) : (mobileMenuOpen ? t('关闭菜单') : t('打开菜单'))
} }
onClick={() => { onClick={() => {
if (isConsoleRoute) { if (isConsoleRoute) {
// 控制侧边栏的显示/隐藏,无论是否移动设备 // 控制侧边栏的显示/隐藏,无论是否移动设备
styleDispatch(styleActions.toggleSider()); isMobile ? onMobileMenuToggle() : toggleCollapsed();
} else { } else {
// 控制HeaderBar自己的移动菜单 // 控制HeaderBar自己的移动菜单
setMobileMenuOpen(!mobileMenuOpen); setMobileMenuOpen(!mobileMenuOpen);

View File

@@ -4,8 +4,9 @@ import SiderBar from './SiderBar.js';
import App from '../../App.js'; import App from '../../App.js';
import FooterBar from './Footer.js'; import FooterBar from './Footer.js';
import { ToastContainer } from 'react-toastify'; import { ToastContainer } from 'react-toastify';
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { useStyle } from '../../context/Style/index.js'; import { useIsMobile } from '../../hooks/useIsMobile.js';
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js'; import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js';
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
@@ -14,9 +15,11 @@ import { useLocation } from 'react-router-dom';
const { Sider, Content, Header } = Layout; const { Sider, Content, Header } = Layout;
const PageLayout = () => { const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext); const [, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext); const [, statusDispatch] = useContext(StatusContext);
const { state: styleState } = useStyle(); const isMobile = useIsMobile();
const [collapsed, , setCollapsed] = useSidebarCollapsed();
const [drawerOpen, setDrawerOpen] = useState(false);
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const location = useLocation(); const location = useLocation();
@@ -26,6 +29,15 @@ const PageLayout = () => {
!location.pathname.startsWith('/console/chat') && !location.pathname.startsWith('/console/chat') &&
location.pathname !== '/console/playground'; location.pathname !== '/console/playground';
const isConsoleRoute = location.pathname.startsWith('/console');
const showSider = isConsoleRoute && (!isMobile || drawerOpen);
useEffect(() => {
if (isMobile && drawerOpen && collapsed) {
setCollapsed(false);
}
}, [isMobile, drawerOpen, collapsed, setCollapsed]);
const loadUser = () => { const loadUser = () => {
let user = localStorage.getItem('user'); let user = localStorage.getItem('user');
if (user) { if (user) {
@@ -63,7 +75,6 @@ const PageLayout = () => {
linkElement.href = logo; linkElement.href = logo;
} }
} }
// 从localStorage获取上次使用的语言
const savedLang = localStorage.getItem('i18nextLng'); const savedLang = localStorage.getItem('i18nextLng');
if (savedLang) { if (savedLang) {
i18n.changeLanguage(savedLang); i18n.changeLanguage(savedLang);
@@ -76,7 +87,7 @@ const PageLayout = () => {
height: '100vh', height: '100vh',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: styleState.isMobile ? 'visible' : 'hidden', overflow: isMobile ? 'visible' : 'hidden',
}} }}
> >
<Header <Header
@@ -90,16 +101,16 @@ const PageLayout = () => {
zIndex: 100, zIndex: 100,
}} }}
> >
<HeaderBar /> <HeaderBar onMobileMenuToggle={() => setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} />
</Header> </Header>
<Layout <Layout
style={{ style={{
overflow: styleState.isMobile ? 'visible' : 'auto', overflow: isMobile ? 'visible' : 'auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
{styleState.showSider && ( {showSider && (
<Sider <Sider
style={{ style={{
position: 'fixed', position: 'fixed',
@@ -109,21 +120,15 @@ const PageLayout = () => {
border: 'none', border: 'none',
paddingRight: '0', paddingRight: '0',
height: 'calc(100vh - 64px)', height: 'calc(100vh - 64px)',
width: 'var(--sidebar-current-width)',
}} }}
> >
<SiderBar /> <SiderBar onNavigate={() => { if (isMobile) setDrawerOpen(false); }} />
</Sider> </Sider>
)} )}
<Layout <Layout
style={{ style={{
marginLeft: styleState.isMobile marginLeft: isMobile ? '0' : showSider ? 'var(--sidebar-current-width)' : '0',
? '0'
: styleState.showSider
? styleState.siderCollapsed
? '60px'
: '180px'
: '0',
transition: 'margin-left 0.3s ease',
flex: '1 1 auto', flex: '1 1 auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@@ -132,9 +137,9 @@ const PageLayout = () => {
<Content <Content
style={{ style={{
flex: '1 0 auto', flex: '1 0 auto',
overflowY: styleState.isMobile ? 'visible' : 'hidden', overflowY: isMobile ? 'visible' : 'hidden',
WebkitOverflowScrolling: 'touch', WebkitOverflowScrolling: 'touch',
padding: shouldInnerPadding ? (styleState.isMobile ? '5px' : '24px') : '0', padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0',
position: 'relative', position: 'relative',
}} }}
> >

View File

@@ -3,7 +3,7 @@ import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { getLucideIcon, sidebarIconColors } from '../../helpers/render.js'; import { getLucideIcon, sidebarIconColors } from '../../helpers/render.js';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import { useStyle, styleActions } from '../../context/Style/index.js'; import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
import { import {
isAdmin, isAdmin,
isRoot, isRoot,
@@ -13,7 +13,7 @@ import {
import { import {
Nav, Nav,
Divider, Divider,
Tooltip, Button,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
const routerMap = { const routerMap = {
@@ -34,12 +34,11 @@ const routerMap = {
personal: '/console/personal', personal: '/console/personal',
}; };
const SiderBar = () => { const SiderBar = ({ onNavigate = () => { } }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { state: styleState, dispatch: styleDispatch } = useStyle(); const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const [selectedKeys, setSelectedKeys] = useState(['home']); const [selectedKeys, setSelectedKeys] = useState(['home']);
const [isCollapsed, setIsCollapsed] = useState(styleState.siderCollapsed);
const [chatItems, setChatItems] = useState([]); const [chatItems, setChatItems] = useState([]);
const [openedKeys, setOpenedKeys] = useState([]); const [openedKeys, setOpenedKeys] = useState([]);
const location = useLocation(); const location = useLocation();
@@ -217,10 +216,14 @@ const SiderBar = () => {
} }
}, [location.pathname, routerMapState]); }, [location.pathname, routerMapState]);
// 同步折叠状态 // 监控折叠状态变化以更新 body class
useEffect(() => { useEffect(() => {
setIsCollapsed(styleState.siderCollapsed); if (collapsed) {
}, [styleState.siderCollapsed]); document.body.classList.add('sidebar-collapsed');
} else {
document.body.classList.remove('sidebar-collapsed');
}
}, [collapsed]);
// 获取菜单项对应的颜色 // 获取菜单项对应的颜色
const getItemColor = (itemKey) => { const getItemColor = (itemKey) => {
@@ -323,32 +326,13 @@ const SiderBar = () => {
return ( return (
<div <div
className="sidebar-container" className="sidebar-container"
style={{ width: isCollapsed ? '60px' : '180px' }} style={{ width: 'var(--sidebar-current-width)' }}
> >
<Nav <Nav
className="sidebar-nav" className="sidebar-nav"
defaultIsCollapsed={styleState.siderCollapsed} defaultIsCollapsed={collapsed}
isCollapsed={isCollapsed} isCollapsed={collapsed}
onCollapseChange={(collapsed) => { onCollapseChange={toggleCollapsed}
setIsCollapsed(collapsed);
styleDispatch(styleActions.setSiderCollapsed(collapsed));
// 确保在收起侧边栏时有选中的项目
if (selectedKeys.length === 0) {
const currentPath = location.pathname;
const matchingKey = Object.keys(routerMapState).find(
(key) => routerMapState[key] === currentPath,
);
if (matchingKey) {
setSelectedKeys([matchingKey]);
} else if (currentPath.startsWith('/console/chat/')) {
setSelectedKeys(['chat']);
} else {
setSelectedKeys(['detail']); // 默认选中首页
}
}
}}
selectedKeys={selectedKeys} selectedKeys={selectedKeys}
itemStyle="sidebar-nav-item" itemStyle="sidebar-nav-item"
hoverStyle="sidebar-nav-item:hover" hoverStyle="sidebar-nav-item:hover"
@@ -363,6 +347,7 @@ const SiderBar = () => {
<Link <Link
style={{ textDecoration: 'none' }} style={{ textDecoration: 'none' }}
to={to} to={to}
onClick={onNavigate}
> >
{itemElement} {itemElement}
</Link> </Link>
@@ -383,7 +368,7 @@ const SiderBar = () => {
> >
{/* 聊天区域 */} {/* 聊天区域 */}
<div className="sidebar-section"> <div className="sidebar-section">
{!isCollapsed && ( {!collapsed && (
<div className="sidebar-group-label">{t('聊天')}</div> <div className="sidebar-group-label">{t('聊天')}</div>
)} )}
{chatMenuItems.map((item) => renderSubItem(item))} {chatMenuItems.map((item) => renderSubItem(item))}
@@ -392,7 +377,7 @@ const SiderBar = () => {
{/* 控制台区域 */} {/* 控制台区域 */}
<Divider className="sidebar-divider" /> <Divider className="sidebar-divider" />
<div> <div>
{!isCollapsed && ( {!collapsed && (
<div className="sidebar-group-label">{t('控制台')}</div> <div className="sidebar-group-label">{t('控制台')}</div>
)} )}
{workspaceItems.map((item) => renderNavItem(item))} {workspaceItems.map((item) => renderNavItem(item))}
@@ -403,7 +388,7 @@ const SiderBar = () => {
<> <>
<Divider className="sidebar-divider" /> <Divider className="sidebar-divider" />
<div> <div>
{!isCollapsed && ( {!collapsed && (
<div className="sidebar-group-label">{t('管理员')}</div> <div className="sidebar-group-label">{t('管理员')}</div>
)} )}
{adminItems.map((item) => renderNavItem(item))} {adminItems.map((item) => renderNavItem(item))}
@@ -414,7 +399,7 @@ const SiderBar = () => {
{/* 个人中心区域 */} {/* 个人中心区域 */}
<Divider className="sidebar-divider" /> <Divider className="sidebar-divider" />
<div> <div>
{!isCollapsed && ( {!collapsed && (
<div className="sidebar-group-label">{t('个人中心')}</div> <div className="sidebar-group-label">{t('个人中心')}</div>
)} )}
{financeItems.map((item) => renderNavItem(item))} {financeItems.map((item) => renderNavItem(item))}
@@ -422,24 +407,25 @@ const SiderBar = () => {
</Nav> </Nav>
{/* 底部折叠按钮 */} {/* 底部折叠按钮 */}
<div <div className="sidebar-collapse-button">
className="sidebar-collapse-button" <Button
onClick={() => { theme="outline"
const newCollapsed = !isCollapsed; type="tertiary"
setIsCollapsed(newCollapsed); size="small"
styleDispatch(styleActions.setSiderCollapsed(newCollapsed)); icon={
}} <ChevronLeft
> size={16}
<Tooltip content={isCollapsed ? t('展开侧边栏') : t('收起侧边栏')} position="right"> strokeWidth={2.5}
<div className="sidebar-collapse-button-inner"> color="var(--semi-color-text-2)"
<span style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
className="sidebar-collapse-icon-container" />
style={{ transform: isCollapsed ? 'rotate(180deg)' : 'rotate(0deg)' }} }
> onClick={toggleCollapsed}
<ChevronLeft size={16} strokeWidth={2.5} color="var(--semi-color-text-2)" /> iconOnly={collapsed}
</span> style={collapsed ? { padding: '4px', width: '100%' } : { padding: '4px 12px', width: '100%' }}
</div> >
</Tooltip> {!collapsed ? t('收起侧边栏') : null}
</Button>
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { isMobile } from '../../helpers'; import { useIsMobile } from '../../hooks/useIsMobile.js';
import { import {
Modal, Modal,
Table, Table,
@@ -26,6 +26,7 @@ const ChannelSelectorModal = forwardRef(({
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const isMobile = useIsMobile();
const [filteredData, setFilteredData] = useState([]); const [filteredData, setFilteredData] = useState([]);
@@ -186,7 +187,7 @@ const ChannelSelectorModal = forwardRef(({
onCancel={onCancel} onCancel={onCancel}
onOk={onOk} onOk={onOk}
title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>} title={<span className="text-lg font-semibold">{t('选择同步渠道')}</span>}
size={isMobile() ? 'full-width' : 'large'} size={isMobile ? 'full-width' : 'large'}
keepDOM keepDOM
lazyRender={false} lazyRender={false}
> >

View File

@@ -44,7 +44,8 @@ import {
IconMore, IconMore,
IconDescend2 IconDescend2
} from '@douyinfe/semi-icons'; } from '@douyinfe/semi-icons';
import { loadChannelModels, isMobile, copy } from '../../helpers'; import { loadChannelModels, copy } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import EditTagModal from '../../pages/Channel/EditTagModal.js'; import EditTagModal from '../../pages/Channel/EditTagModal.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useTableCompactMode } from '../../hooks/useTableCompactMode'; import { useTableCompactMode } from '../../hooks/useTableCompactMode';
@@ -52,6 +53,7 @@ import { FaRandom } from 'react-icons/fa';
const ChannelsTable = () => { const ChannelsTable = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const isMobile = useIsMobile();
let type2label = undefined; let type2label = undefined;
@@ -2031,7 +2033,7 @@ const ChannelsTable = () => {
} }
maskClosable={!isBatchTesting} maskClosable={!isBatchTesting}
className="!rounded-lg" className="!rounded-lg"
size={isMobile() ? 'full-width' : 'large'} size={isMobile ? 'full-width' : 'large'}
> >
<div className="model-test-scroll"> <div className="model-test-scroll">
{currentTestChannel && ( {currentTestChannel && (

View File

@@ -1,227 +0,0 @@
// contexts/Style/index.js
import React, { useReducer, useEffect, useMemo, createContext } from 'react';
import { useLocation } from 'react-router-dom';
import { isMobile as getIsMobile } from '../../helpers';
// Action Types
const ACTION_TYPES = {
TOGGLE_SIDER: 'TOGGLE_SIDER',
SET_SIDER: 'SET_SIDER',
SET_MOBILE: 'SET_MOBILE',
SET_SIDER_COLLAPSED: 'SET_SIDER_COLLAPSED',
BATCH_UPDATE: 'BATCH_UPDATE',
};
// Constants
const STORAGE_KEYS = {
SIDEBAR_COLLAPSED: 'default_collapse_sidebar',
};
const ROUTE_PATTERNS = {
CONSOLE: '/console',
};
/**
* 判断路径是否为控制台路由
* @param {string} pathname - 路由路径
* @returns {boolean} 是否为控制台路由
*/
const isConsoleRoute = (pathname) => {
return pathname === ROUTE_PATTERNS.CONSOLE ||
pathname.startsWith(ROUTE_PATTERNS.CONSOLE + '/');
};
/**
* 获取初始状态
* @param {string} pathname - 当前路由路径
* @returns {Object} 初始状态对象
*/
const getInitialState = (pathname) => {
const isMobile = getIsMobile();
const isConsole = isConsoleRoute(pathname);
const isCollapsed = localStorage.getItem(STORAGE_KEYS.SIDEBAR_COLLAPSED) === 'true';
return {
isMobile,
showSider: isConsole && !isMobile,
siderCollapsed: isCollapsed,
isManualSiderControl: false,
};
};
/**
* Style reducer
* @param {Object} state - 当前状态
* @param {Object} action - action 对象
* @returns {Object} 新状态
*/
const styleReducer = (state, action) => {
switch (action.type) {
case ACTION_TYPES.TOGGLE_SIDER:
return {
...state,
showSider: !state.showSider,
isManualSiderControl: true,
};
case ACTION_TYPES.SET_SIDER:
return {
...state,
showSider: action.payload,
isManualSiderControl: action.isManualControl ?? false,
};
case ACTION_TYPES.SET_MOBILE:
return {
...state,
isMobile: action.payload,
};
case ACTION_TYPES.SET_SIDER_COLLAPSED:
// 自动保存到 localStorage
localStorage.setItem(STORAGE_KEYS.SIDEBAR_COLLAPSED, action.payload.toString());
return {
...state,
siderCollapsed: action.payload,
};
case ACTION_TYPES.BATCH_UPDATE:
return {
...state,
...action.payload,
};
default:
return state;
}
};
// Context (内部使用,不导出)
const StyleContext = createContext(null);
/**
* 自定义 Hook - 处理窗口大小变化
* @param {Function} dispatch - dispatch 函数
* @param {Object} state - 当前状态
* @param {string} pathname - 当前路径
*/
const useWindowResize = (dispatch, state, pathname) => {
useEffect(() => {
const handleResize = () => {
const isMobile = getIsMobile();
dispatch({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile });
// 只有在非手动控制的情况下,才根据屏幕大小自动调整侧边栏
if (!state.isManualSiderControl && isConsoleRoute(pathname)) {
dispatch({
type: ACTION_TYPES.SET_SIDER,
payload: !isMobile,
isManualControl: false
});
}
};
let timeoutId;
const debouncedResize = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(handleResize, 150);
};
window.addEventListener('resize', debouncedResize);
return () => {
window.removeEventListener('resize', debouncedResize);
clearTimeout(timeoutId);
};
}, [dispatch, state.isManualSiderControl, pathname]);
};
/**
* 自定义 Hook - 处理路由变化
* @param {Function} dispatch - dispatch 函数
* @param {string} pathname - 当前路径
*/
const useRouteChange = (dispatch, pathname) => {
useEffect(() => {
const isMobile = getIsMobile();
const isConsole = isConsoleRoute(pathname);
dispatch({
type: ACTION_TYPES.BATCH_UPDATE,
payload: {
showSider: isConsole && !isMobile,
isManualSiderControl: false,
},
});
}, [pathname, dispatch]);
};
/**
* 自定义 Hook - 处理移动设备侧边栏自动收起
* @param {Object} state - 当前状态
* @param {Function} dispatch - dispatch 函数
*/
const useMobileSiderAutoHide = (state, dispatch) => {
useEffect(() => {
// 移动设备上,如果不是手动控制且侧边栏是打开的,则自动关闭
if (state.isMobile && state.showSider && !state.isManualSiderControl) {
dispatch({ type: ACTION_TYPES.SET_SIDER, payload: false });
}
}, [state.isMobile, state.showSider, state.isManualSiderControl, dispatch]);
};
/**
* Style Provider 组件
*/
export const StyleProvider = ({ children }) => {
const location = useLocation();
const pathname = location.pathname;
const [state, dispatch] = useReducer(
styleReducer,
pathname,
getInitialState
);
useWindowResize(dispatch, state, pathname);
useRouteChange(dispatch, pathname);
useMobileSiderAutoHide(state, dispatch);
const contextValue = useMemo(
() => ({ state, dispatch }),
[state]
);
return (
<StyleContext.Provider value={contextValue}>
{children}
</StyleContext.Provider>
);
};
/**
* 自定义 Hook - 使用 StyleContext
* @returns {{state: Object, dispatch: Function}} context value
*/
export const useStyle = () => {
const context = React.useContext(StyleContext);
if (!context) {
throw new Error('useStyle must be used within StyleProvider');
}
return context;
};
// 导出 action creators 以便外部使用
export const styleActions = {
toggleSider: () => ({ type: ACTION_TYPES.TOGGLE_SIDER }),
setSider: (show, isManualControl = false) => ({
type: ACTION_TYPES.SET_SIDER,
payload: show,
isManualControl
}),
setMobile: (isMobile) => ({ type: ACTION_TYPES.SET_MOBILE, payload: isMobile }),
setSiderCollapsed: (collapsed) => ({
type: ACTION_TYPES.SET_SIDER_COLLAPSED,
payload: collapsed
}),
};

View File

@@ -1,6 +1,7 @@
import i18next from 'i18next'; import i18next from 'i18next';
import { Modal, Tag, Typography } from '@douyinfe/semi-ui'; import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
import { copy, isMobile, showSuccess } from './utils'; import { copy, showSuccess } from './utils';
import { MOBILE_BREAKPOINT } from '../hooks/useIsMobile.js';
import { visit } from 'unist-util-visit'; import { visit } from 'unist-util-visit';
import { import {
OpenAI, OpenAI,
@@ -669,7 +670,8 @@ const measureTextWidth = (
}; };
export function truncateText(text, maxWidth = 200) { export function truncateText(text, maxWidth = 200) {
if (!isMobile()) { const isMobileScreen = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches;
if (!isMobileScreen) {
return text; return text;
} }
if (!text) return text; if (!text) return text;

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants'; import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
import { TABLE_COMPACT_MODES_KEY } from '../constants'; import { TABLE_COMPACT_MODES_KEY } from '../constants';
import { MOBILE_BREAKPOINT } from '../hooks/useIsMobile.js';
const HTMLToastContent = ({ htmlContent }) => { const HTMLToastContent = ({ htmlContent }) => {
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />; return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
@@ -67,9 +68,7 @@ export async function copy(text) {
return okay; return okay;
} }
export function isMobile() { // isMobile 函数已移除,请改用 useIsMobile Hook
return window.innerWidth <= 600;
}
let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT }; let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT };
let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT }; let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT };
@@ -77,7 +76,8 @@ let showSuccessOptions = { autoClose: toastConstants.SUCCESS_TIMEOUT };
let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT }; let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT };
let showNoticeOptions = { autoClose: false }; let showNoticeOptions = { autoClose: false };
if (isMobile()) { const isMobileScreen = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches;
if (isMobileScreen) {
showErrorOptions.position = 'top-center'; showErrorOptions.position = 'top-center';
// showErrorOptions.transition = 'flip'; // showErrorOptions.transition = 'flip';

View File

@@ -0,0 +1,16 @@
export const MOBILE_BREAKPOINT = 768;
import { useSyncExternalStore } from 'react';
export const useIsMobile = () => {
const query = `(max-width: ${MOBILE_BREAKPOINT - 1}px)`;
return useSyncExternalStore(
(callback) => {
const mql = window.matchMedia(query);
mql.addEventListener('change', callback);
return () => mql.removeEventListener('change', callback);
},
() => window.matchMedia(query).matches,
() => false,
);
};

View File

@@ -0,0 +1,22 @@
import { useState, useCallback } from 'react';
const KEY = 'default_collapse_sidebar';
export const useSidebarCollapsed = () => {
const [collapsed, setCollapsed] = useState(() => localStorage.getItem(KEY) === 'true');
const toggle = useCallback(() => {
setCollapsed(prev => {
const next = !prev;
localStorage.setItem(KEY, next.toString());
return next;
});
}, []);
const set = useCallback((value) => {
setCollapsed(value);
localStorage.setItem(KEY, value.toString());
}, []);
return [collapsed, toggle, set];
};

View File

@@ -179,7 +179,6 @@
"注销": "Logout", "注销": "Logout",
"登录": "Sign in", "登录": "Sign in",
"注册": "Sign up", "注册": "Sign up",
"加载{name}中...": "Loading {name}...",
"未登录或登录已过期,请重新登录!": "Not logged in or session expired. Please login again!", "未登录或登录已过期,请重新登录!": "Not logged in or session expired. Please login again!",
"用户登录": "User Login", "用户登录": "User Login",
"密码": "Password", "密码": "Password",
@@ -933,7 +932,6 @@
"更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.", "更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.",
"一小时": "One hour", "一小时": "One hour",
"新建数量": "New quantity", "新建数量": "New quantity",
"加载失败,请稍后重试": "Loading failed, please try again later",
"未设置": "Not set", "未设置": "Not set",
"API文档": "API documentation", "API文档": "API documentation",
"不是合法的 JSON 字符串": "Not a valid JSON string", "不是合法的 JSON 字符串": "Not a valid JSON string",

View File

@@ -14,6 +14,22 @@
} }
/* ==================== 全局基础样式 ==================== */ /* ==================== 全局基础样式 ==================== */
/* 侧边栏宽度相关的 CSS 变量,配合 .sidebar-collapsed 类和媒体查询实现响应式布局 */
:root {
--sidebar-width: 180px;
/* 展开时宽度 */
--sidebar-width-collapsed: 60px; /* 折叠后宽度,显示图标栏 */
/* 折叠后宽度 */
--sidebar-current-width: var(--sidebar-width);
}
/* 当 body 上存在 .sidebar-collapsed 类时,使用折叠宽度 */
body.sidebar-collapsed {
--sidebar-current-width: var(--sidebar-width-collapsed);
}
/* 移除了在移动端强制设为 0 的限制,改由 React 控制是否渲染侧边栏以实现显示/隐藏 */
body { body {
font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif; font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif;
color: var(--semi-color-text-0); color: var(--semi-color-text-0);

View File

@@ -6,11 +6,18 @@ import { UserProvider } from './context/User';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { StatusProvider } from './context/Status'; import { StatusProvider } from './context/Status';
import { ThemeProvider } from './context/Theme'; import { ThemeProvider } from './context/Theme';
import { StyleProvider } from './context/Style/index.js';
import PageLayout from './components/layout/PageLayout.js'; import PageLayout from './components/layout/PageLayout.js';
import './i18n/i18n.js'; import './i18n/i18n.js';
import './index.css'; import './index.css';
// 欢迎信息(二次开发者不准将此移除)
// Welcome message (Secondary developers are not allowed to remove this)
if (typeof window !== 'undefined') {
console.log('%cWe ❤ NewAPI%c Github: https://github.com/QuantumNous/new-api',
'color: #10b981; font-weight: bold; font-size: 24px;',
'color: inherit; font-size: 14px;');
}
// initialization // initialization
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
@@ -18,11 +25,14 @@ root.render(
<React.StrictMode> <React.StrictMode>
<StatusProvider> <StatusProvider>
<UserProvider> <UserProvider>
<BrowserRouter> <BrowserRouter
future={{
v7_startTransition: true,
v7_relativeSplatPath: true,
}}
>
<ThemeProvider> <ThemeProvider>
<StyleProvider> <PageLayout />
<PageLayout />
</StyleProvider>
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>
</UserProvider> </UserProvider>

View File

@@ -105,7 +105,7 @@ const About = () => {
); );
return ( return (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
{aboutLoaded && about === '' ? ( {aboutLoaded && about === '' ? (
<div className="flex justify-center items-center h-screen p-8"> <div className="flex justify-center items-center h-screen p-8">
<Empty <Empty

View File

@@ -3,12 +3,12 @@ import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
API, API,
isMobile,
showError, showError,
showInfo, showInfo,
showSuccess, showSuccess,
verifyJSON, verifyJSON,
} from '../../helpers'; } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { CHANNEL_OPTIONS } from '../../constants'; import { CHANNEL_OPTIONS } from '../../constants';
import { import {
SideSheet, SideSheet,
@@ -81,6 +81,7 @@ const EditChannel = (props) => {
const channelId = props.editingChannel.id; const channelId = props.editingChannel.id;
const isEdit = channelId !== undefined; const isEdit = channelId !== undefined;
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
const isMobile = useIsMobile();
const handleCancel = () => { const handleCancel = () => {
props.handleClose(); props.handleClose();
}; };
@@ -693,7 +694,7 @@ const EditChannel = (props) => {
} }
bodyStyle={{ padding: '0' }} bodyStyle={{ padding: '0' }}
visible={props.visible} visible={props.visible}
width={isMobile() ? '100%' : 600} width={isMobile ? '100%' : 600}
footer={ footer={
<div className="flex justify-end bg-white"> <div className="flex justify-end bg-white">
<Space> <Space>

View File

@@ -3,7 +3,7 @@ import ChannelsTable from '../../components/table/ChannelsTable';
const File = () => { const File = () => {
return ( return (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<ChannelsTable /> <ChannelsTable />
</div> </div>
); );

View File

@@ -17,7 +17,7 @@ const chat2page = () => {
} }
return ( return (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<h3>正在加载请稍候...</h3> <h3>正在加载请稍候...</h3>
</div> </div>
); );

View File

@@ -41,8 +41,9 @@ import { VChart } from '@visactor/react-vchart';
import { import {
API, API,
isAdmin, isAdmin,
isMobile,
showError, showError,
showSuccess,
showWarning,
timestamp2string, timestamp2string,
timestamp2string1, timestamp2string1,
getQuotaWithUnit, getQuotaWithUnit,
@@ -51,9 +52,9 @@ import {
renderQuota, renderQuota,
modelToColor, modelToColor,
copy, copy,
showSuccess,
getRelativeTime getRelativeTime
} from '../../helpers'; } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
import { StatusContext } from '../../context/Status/index.js'; import { StatusContext } from '../../context/Status/index.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -66,6 +67,7 @@ const Detail = (props) => {
// ========== Hooks - Navigation & Translation ========== // ========== Hooks - Navigation & Translation ==========
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const isMobile = useIsMobile();
// ========== Hooks - Refs ========== // ========== Hooks - Refs ==========
const formRef = useRef(); const formRef = useRef();
@@ -1118,7 +1120,7 @@ const Detail = (props) => {
}, []); }, []);
return ( return (
<div className="bg-gray-50 h-full mt-[64px]"> <div className="bg-gray-50 h-full mt-[64px] px-2">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 <h2
className="text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out" className="text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out"
@@ -1150,7 +1152,7 @@ const Detail = (props) => {
onOk={handleSearchConfirm} onOk={handleSearchConfirm}
onCancel={handleCloseModal} onCancel={handleCloseModal}
closeOnEsc={true} closeOnEsc={true}
size={isMobile() ? 'full-width' : 'small'} size={isMobile ? 'full-width' : 'small'}
centered centered
> >
<Form ref={formRef} layout='vertical' className="w-full"> <Form ref={formRef} layout='vertical' className="w-full">

View File

@@ -1,6 +1,7 @@
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Button, Typography, Tag, Input, ScrollList, ScrollItem } from '@douyinfe/semi-ui'; import { Button, Typography, Tag, Input, ScrollList, ScrollItem } from '@douyinfe/semi-ui';
import { API, showError, isMobile, copy, showSuccess } from '../../helpers'; import { API, showError, copy, showSuccess } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { API_ENDPOINTS } from '../../constants/common.constant'; import { API_ENDPOINTS } from '../../constants/common.constant';
import { StatusContext } from '../../context/Status'; import { StatusContext } from '../../context/Status';
import { marked } from 'marked'; import { marked } from 'marked';
@@ -18,6 +19,7 @@ const Home = () => {
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState(''); const [homePageContent, setHomePageContent] = useState('');
const [noticeVisible, setNoticeVisible] = useState(false); const [noticeVisible, setNoticeVisible] = useState(false);
const isMobile = useIsMobile();
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false; const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
const docsLink = statusState?.status?.docs_link || ''; const docsLink = statusState?.status?.docs_link || '';
const serverAddress = statusState?.status?.server_address || window.location.origin; const serverAddress = statusState?.status?.server_address || window.location.origin;
@@ -98,7 +100,7 @@ const Home = () => {
<NoticeModal <NoticeModal
visible={noticeVisible} visible={noticeVisible}
onClose={() => setNoticeVisible(false)} onClose={() => setNoticeVisible(false)}
isMobile={isMobile()} isMobile={isMobile}
/> />
{homePageContentLoaded && homePageContent === '' ? ( {homePageContentLoaded && homePageContent === '' ? (
<div className="w-full overflow-x-hidden"> <div className="w-full overflow-x-hidden">
@@ -133,7 +135,7 @@ const Home = () => {
readonly readonly
value={serverAddress} value={serverAddress}
className="flex-1 !rounded-full" className="flex-1 !rounded-full"
size={isMobile() ? 'default' : 'large'} size={isMobile ? 'default' : 'large'}
suffix={ suffix={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ScrollList bodyHeight={32} style={{ border: 'unset', boxShadow: 'unset' }}> <ScrollList bodyHeight={32} style={{ border: 'unset', boxShadow: 'unset' }}>
@@ -160,13 +162,13 @@ const Home = () => {
{/* 操作按钮 */} {/* 操作按钮 */}
<div className="flex flex-row gap-4 justify-center items-center"> <div className="flex flex-row gap-4 justify-center items-center">
<Link to="/console"> <Link to="/console">
<Button theme="solid" type="primary" size={isMobile() ? "default" : "large"} className="!rounded-3xl px-8 py-2" icon={<IconPlay />}> <Button theme="solid" type="primary" size={isMobile ? "default" : "large"} className="!rounded-3xl px-8 py-2" icon={<IconPlay />}>
{t('获取密钥')} {t('获取密钥')}
</Button> </Button>
</Link> </Link>
{isDemoSiteMode && statusState?.status?.version ? ( {isDemoSiteMode && statusState?.status?.version ? (
<Button <Button
size={isMobile() ? "default" : "large"} size={isMobile ? "default" : "large"}
className="flex items-center !rounded-3xl px-6 py-2" className="flex items-center !rounded-3xl px-6 py-2"
icon={<IconGithubLogo />} icon={<IconGithubLogo />}
onClick={() => window.open('https://github.com/QuantumNous/new-api', '_blank')} onClick={() => window.open('https://github.com/QuantumNous/new-api', '_blank')}
@@ -176,7 +178,7 @@ const Home = () => {
) : ( ) : (
docsLink && ( docsLink && (
<Button <Button
size={isMobile() ? "default" : "large"} size={isMobile ? "default" : "large"}
className="flex items-center !rounded-3xl px-6 py-2" className="flex items-center !rounded-3xl px-6 py-2"
icon={<IconFile />} icon={<IconFile />}
onClick={() => window.open(docsLink, '_blank')} onClick={() => window.open(docsLink, '_blank')}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import LogsTable from '../../components/table/LogsTable'; import LogsTable from '../../components/table/LogsTable';
const Token = () => ( const Token = () => (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<LogsTable /> <LogsTable />
</div> </div>
); );

View File

@@ -2,7 +2,7 @@ import React from 'react';
import MjLogsTable from '../../components/table/MjLogsTable'; import MjLogsTable from '../../components/table/MjLogsTable';
const Midjourney = () => ( const Midjourney = () => (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<MjLogsTable /> <MjLogsTable />
</div> </div>
); );

View File

@@ -5,7 +5,7 @@ import { Layout, Toast, Modal } from '@douyinfe/semi-ui';
// Context // Context
import { UserContext } from '../../context/User/index.js'; import { UserContext } from '../../context/User/index.js';
import { useStyle, styleActions } from '../../context/Style/index.js'; import { useIsMobile } from '../../hooks/useIsMobile.js';
// hooks // hooks
import { usePlaygroundState } from '../../hooks/usePlaygroundState.js'; import { usePlaygroundState } from '../../hooks/usePlaygroundState.js';
@@ -59,7 +59,8 @@ const generateAvatarDataUrl = (username) => {
const Playground = () => { const Playground = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [userState] = useContext(UserContext); const [userState] = useContext(UserContext);
const { state: styleState, dispatch: styleDispatch } = useStyle(); const isMobile = useIsMobile();
const styleState = { isMobile };
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const state = usePlaygroundState(); const state = usePlaygroundState();
@@ -321,19 +322,7 @@ const Playground = () => {
} }
}, [searchParams, t]); }, [searchParams, t]);
// 处理窗口大小变化 // Playground 组件无需再监听窗口变化isMobile 由 useIsMobile Hook 自动更新
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth < 768;
if (styleState.isMobile !== mobile) {
styleDispatch(styleActions.setMobile(mobile));
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [styleState.isMobile, styleDispatch]);
// 构建预览payload // 构建预览payload
useEffect(() => { useEffect(() => {
@@ -365,26 +354,26 @@ const Playground = () => {
return ( return (
<div className="h-full bg-gray-50 mt-[64px]"> <div className="h-full bg-gray-50 mt-[64px]">
<Layout style={{ height: '100%', background: 'transparent' }} className="flex flex-col md:flex-row"> <Layout style={{ height: '100%', background: 'transparent' }} className="flex flex-col md:flex-row">
{(showSettings || !styleState.isMobile) && ( {(showSettings || !isMobile) && (
<Layout.Sider <Layout.Sider
style={{ style={{
background: 'transparent', background: 'transparent',
borderRight: 'none', borderRight: 'none',
flexShrink: 0, flexShrink: 0,
minWidth: styleState.isMobile ? '100%' : 320, minWidth: isMobile ? '100%' : 320,
maxWidth: styleState.isMobile ? '100%' : 320, maxWidth: isMobile ? '100%' : 320,
height: styleState.isMobile ? 'auto' : 'calc(100vh - 66px)', height: isMobile ? 'auto' : 'calc(100vh - 66px)',
overflow: 'auto', overflow: 'auto',
position: styleState.isMobile ? 'fixed' : 'relative', position: isMobile ? 'fixed' : 'relative',
zIndex: styleState.isMobile ? 1000 : 1, zIndex: isMobile ? 1000 : 1,
width: '100%', width: '100%',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
}} }}
width={styleState.isMobile ? '100%' : 320} width={isMobile ? '100%' : 320}
className={styleState.isMobile ? 'bg-white shadow-lg' : ''} className={isMobile ? 'bg-white shadow-lg' : ''}
> >
<OptimizedSettingsPanel <OptimizedSettingsPanel
inputs={inputs} inputs={inputs}
@@ -432,7 +421,7 @@ const Playground = () => {
</div> </div>
{/* 调试面板 - 桌面端 */} {/* 调试面板 - 桌面端 */}
{showDebugPanel && !styleState.isMobile && ( {showDebugPanel && !isMobile && (
<div className="w-96 flex-shrink-0 h-full"> <div className="w-96 flex-shrink-0 h-full">
<OptimizedDebugPanel <OptimizedDebugPanel
debugData={debugData} debugData={debugData}
@@ -446,7 +435,7 @@ const Playground = () => {
</div> </div>
{/* 调试面板 - 移动端覆盖层 */} {/* 调试面板 - 移动端覆盖层 */}
{showDebugPanel && styleState.isMobile && ( {showDebugPanel && isMobile && (
<div <div
style={{ style={{
position: 'fixed', position: 'fixed',

View File

@@ -2,7 +2,7 @@ import React from 'react';
import ModelPricing from '../../components/table/ModelPricing.js'; import ModelPricing from '../../components/table/ModelPricing.js';
const Pricing = () => ( const Pricing = () => (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<ModelPricing /> <ModelPricing />
</div> </div>
); );

View File

@@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next';
import { import {
API, API,
downloadTextAsFile, downloadTextAsFile,
isMobile,
showError, showError,
showSuccess, showSuccess,
renderQuota, renderQuota,
renderQuotaWithPrompt, renderQuotaWithPrompt,
} from '../../helpers'; } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { import {
Button, Button,
Modal, Modal,
@@ -36,6 +36,7 @@ const EditRedemption = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const isEdit = props.editingRedemption.id !== undefined; const isEdit = props.editingRedemption.id !== undefined;
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
const isMobile = useIsMobile();
const formApiRef = useRef(null); const formApiRef = useRef(null);
const getInitValues = () => ({ const getInitValues = () => ({
@@ -155,7 +156,7 @@ const EditRedemption = (props) => {
} }
bodyStyle={{ padding: '0' }} bodyStyle={{ padding: '0' }}
visible={props.visiable} visible={props.visiable}
width={isMobile() ? '100%' : 600} width={isMobile ? '100%' : 600}
footer={ footer={
<div className="flex justify-end bg-white"> <div className="flex justify-end bg-white">
<Space> <Space>

View File

@@ -3,7 +3,7 @@ import RedemptionsTable from '../../components/table/RedemptionsTable';
const Redemption = () => { const Redemption = () => {
return ( return (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<RedemptionsTable /> <RedemptionsTable />
</div> </div>
); );

View File

@@ -18,7 +18,8 @@ import {
AlertTriangle, AlertTriangle,
CheckCircle, CheckCircle,
} from 'lucide-react'; } from 'lucide-react';
import { API, showError, showSuccess, showWarning, stringToColor, isMobile } from '../../../helpers'; import { API, showError, showSuccess, showWarning, stringToColor } from '../../../helpers';
import { useIsMobile } from '../../../hooks/useIsMobile.js';
import { DEFAULT_ENDPOINT } from '../../../constants'; import { DEFAULT_ENDPOINT } from '../../../constants';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
@@ -28,6 +29,7 @@ import {
import ChannelSelectorModal from '../../../components/settings/ChannelSelectorModal'; import ChannelSelectorModal from '../../../components/settings/ChannelSelectorModal';
function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) { function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
const isMobile = useIsMobile();
const columns = [ const columns = [
{ title: t('渠道'), dataIndex: 'channel' }, { title: t('渠道'), dataIndex: 'channel' },
{ title: t('模型'), dataIndex: 'model' }, { title: t('模型'), dataIndex: 'model' },
@@ -49,7 +51,7 @@ function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
visible={visible} visible={visible}
onCancel={onCancel} onCancel={onCancel}
onOk={onOk} onOk={onOk}
size={isMobile() ? 'full-width' : 'large'} size={isMobile ? 'full-width' : 'large'}
> >
<Table columns={columns} dataSource={items} pagination={false} size="small" /> <Table columns={columns} dataSource={items} pagination={false} size="small" />
</Modal> </Modal>
@@ -61,6 +63,7 @@ export default function UpstreamRatioSync(props) {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [syncLoading, setSyncLoading] = useState(false); const [syncLoading, setSyncLoading] = useState(false);
const isMobile = useIsMobile();
// 渠道选择相关 // 渠道选择相关
const [allChannels, setAllChannels] = useState([]); const [allChannels, setAllChannels] = useState([]);

View File

@@ -150,7 +150,7 @@ const Setting = () => {
} }
}, [location.search]); }, [location.search]);
return ( return (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<Layout> <Layout>
<Layout.Content> <Layout.Content>
<Tabs <Tabs

View File

@@ -2,7 +2,7 @@ import React from 'react';
import TaskLogsTable from '../../components/table/TaskLogsTable.js'; import TaskLogsTable from '../../components/table/TaskLogsTable.js';
const Task = () => ( const Task = () => (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<TaskLogsTable /> <TaskLogsTable />
</div> </div>
); );

View File

@@ -1,7 +1,6 @@
import React, { useEffect, useState, useContext, useRef } from 'react'; import React, { useEffect, useState, useContext, useRef } from 'react';
import { import {
API, API,
isMobile,
showError, showError,
showSuccess, showSuccess,
timestamp2string, timestamp2string,
@@ -9,6 +8,7 @@ import {
renderQuotaWithPrompt, renderQuotaWithPrompt,
getModelCategories, getModelCategories,
} from '../../helpers'; } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { import {
Button, Button,
SideSheet, SideSheet,
@@ -38,6 +38,7 @@ const EditToken = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, statusDispatch] = useContext(StatusContext);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const isMobile = useIsMobile();
const formApiRef = useRef(null); const formApiRef = useRef(null);
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
const [groups, setGroups] = useState([]); const [groups, setGroups] = useState([]);
@@ -277,7 +278,7 @@ const EditToken = (props) => {
} }
bodyStyle={{ padding: '0' }} bodyStyle={{ padding: '0' }}
visible={props.visiable} visible={props.visiable}
width={isMobile() ? '100%' : 600} width={isMobile ? '100%' : 600}
footer={ footer={
<div className='flex justify-end bg-white'> <div className='flex justify-end bg-white'>
<Space> <Space>

View File

@@ -3,7 +3,7 @@ import TokensTable from '../../components/table/TokensTable';
const Token = () => { const Token = () => {
return ( return (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<TokensTable /> <TokensTable />
</div> </div>
); );

View File

@@ -1,5 +1,6 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { API, isMobile, showError, showSuccess } from '../../helpers'; import { API, showError, showSuccess } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { import {
Button, Button,
SideSheet, SideSheet,
@@ -26,6 +27,7 @@ const AddUser = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const formApiRef = useRef(null); const formApiRef = useRef(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const isMobile = useIsMobile();
const getInitValues = () => ({ const getInitValues = () => ({
username: '', username: '',
@@ -67,7 +69,7 @@ const AddUser = (props) => {
} }
bodyStyle={{ padding: '0' }} bodyStyle={{ padding: '0' }}
visible={props.visible} visible={props.visible}
width={isMobile() ? '100%' : 600} width={isMobile ? '100%' : 600}
footer={ footer={
<div className="flex justify-end bg-white"> <div className="flex justify-end bg-white">
<Space> <Space>

View File

@@ -2,12 +2,12 @@ import React, { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
API, API,
isMobile,
showError, showError,
showSuccess, showSuccess,
renderQuota, renderQuota,
renderQuotaWithPrompt, renderQuotaWithPrompt,
} from '../../helpers'; } from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { import {
Button, Button,
Modal, Modal,
@@ -41,6 +41,7 @@ const EditUser = (props) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [addQuotaModalOpen, setIsModalOpen] = useState(false); const [addQuotaModalOpen, setIsModalOpen] = useState(false);
const [addQuotaLocal, setAddQuotaLocal] = useState(''); const [addQuotaLocal, setAddQuotaLocal] = useState('');
const isMobile = useIsMobile();
const [groupOptions, setGroupOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]);
const formApiRef = useRef(null); const formApiRef = useRef(null);
@@ -137,7 +138,7 @@ const EditUser = (props) => {
} }
bodyStyle={{ padding: 0 }} bodyStyle={{ padding: 0 }}
visible={props.visible} visible={props.visible}
width={isMobile() ? '100%' : 600} width={isMobile ? '100%' : 600}
footer={ footer={
<div className='flex justify-end bg-white'> <div className='flex justify-end bg-white'>
<Space> <Space>

View File

@@ -3,7 +3,7 @@ import UsersTable from '../../components/table/UsersTable';
const User = () => { const User = () => {
return ( return (
<div className="mt-[64px]"> <div className="mt-[64px] px-2">
<UsersTable /> <UsersTable />
</div> </div>
); );