📱 refactor(web): remove legacy isMobile util and migrate to useIsMobile hook

BREAKING CHANGE:
helpers/utils.js no longer exports `isMobile()`.
Any external code that relied on this function must switch to the `useIsMobile` React hook.

Summary
-------
1. Deleted the obsolete `isMobile()` function from helpers/utils.js.
2. Introduced `MOBILE_BREAKPOINT` constant and `matchMedia`-based detection for non-React contexts.
3. Reworked toast positioning logic in utils.js to rely on `matchMedia`.
4. Updated render.js:
   • Removed isMobile import.
   • Added MOBILE_BREAKPOINT detection in `truncateText`.
5. Migrated every page/component to the `useIsMobile` hook:
   • Layout: HeaderBar, PageLayout, SiderBar
   • Pages: Home, Detail, Playground, User (Add/Edit), Token, Channel, Redemption, Ratio Sync
   • Components: ChannelsTable, ChannelSelectorModal, ConflictConfirmModal
6. Purged all remaining `isMobile()` calls and legacy imports.
7. Added missing `const isMobile = useIsMobile()` declarations where required.

Benefits
--------
• Unifies mobile detection with a React-friendly hook.
• Eliminates duplicated logic and improves maintainability.
• Keeps non-React helpers lightweight by using `matchMedia` directly.
This commit is contained in:
t0ng7u
2025-07-16 02:54:58 +08:00
parent b2b018ab93
commit a44fc51007
21 changed files with 176 additions and 353 deletions

View File

@@ -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);
@@ -207,7 +209,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 +295,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 +390,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 +438,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 +449,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';
@@ -16,7 +17,9 @@ const { Sider, Content, Header } = Layout;
const PageLayout = () => { const PageLayout = () => {
const [userState, userDispatch] = useContext(UserContext); const [userState, userDispatch] = useContext(UserContext);
const [statusState, statusDispatch] = useContext(StatusContext); const [statusState, 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,16 @@ 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);
// Ensure sidebar not collapsed when opening drawer on mobile
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) {
@@ -76,7 +89,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,25 +103,26 @@ 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: isMobile ? 'fixed' : 'fixed',
left: 0, left: 0,
top: '64px', top: '64px',
zIndex: 99, zIndex: 99,
border: 'none', border: 'none',
paddingRight: '0', paddingRight: '0',
height: 'calc(100vh - 64px)', height: 'calc(100vh - 64px)',
width: 'var(--sidebar-current-width)',
}} }}
> >
<SiderBar /> <SiderBar />
@@ -116,14 +130,7 @@ const PageLayout = () => {
)} )}
<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 +139,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,8 @@ 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 { useIsMobile } from '../../hooks/useIsMobile.js';
import { useSidebarCollapsed } from '../../hooks/useSidebarCollapsed.js';
import { import {
isAdmin, isAdmin,
isRoot, isRoot,
@@ -36,10 +37,10 @@ const routerMap = {
const SiderBar = () => { const SiderBar = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { state: styleState, dispatch: styleDispatch } = useStyle(); const isMobile = useIsMobile();
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 +218,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 +328,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"
@@ -383,7 +369,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 +378,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 +389,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 +400,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))}
@@ -425,16 +411,14 @@ const SiderBar = () => {
<div <div
className="sidebar-collapse-button" className="sidebar-collapse-button"
onClick={() => { onClick={() => {
const newCollapsed = !isCollapsed; toggleCollapsed();
setIsCollapsed(newCollapsed);
styleDispatch(styleActions.setSiderCollapsed(newCollapsed));
}} }}
> >
<Tooltip content={isCollapsed ? t('展开侧边栏') : t('收起侧边栏')} position="right"> <Tooltip content={collapsed ? t('展开侧边栏') : t('收起侧边栏')} position="right">
<div className="sidebar-collapse-button-inner"> <div className="sidebar-collapse-button-inner">
<span <span
className="sidebar-collapse-icon-container" className="sidebar-collapse-icon-container"
style={{ transform: isCollapsed ? 'rotate(180deg)' : 'rotate(0deg)' }} style={{ transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)' }}
> >
<ChevronLeft size={16} strokeWidth={2.5} color="var(--semi-color-text-2)" /> <ChevronLeft size={16} strokeWidth={2.5} color="var(--semi-color-text-2)" />
</span> </span>

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,15 @@
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,
);
};

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

@@ -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,7 +6,6 @@ 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';
@@ -20,9 +19,7 @@ root.render(
<UserProvider> <UserProvider>
<BrowserRouter> <BrowserRouter>
<ThemeProvider> <ThemeProvider>
<StyleProvider> <PageLayout />
<PageLayout />
</StyleProvider>
</ThemeProvider> </ThemeProvider>
</BrowserRouter> </BrowserRouter>
</UserProvider> </UserProvider>

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

@@ -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();
@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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>