📱 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

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

View File

@@ -41,8 +41,9 @@ import { VChart } from '@visactor/react-vchart';
import {
API,
isAdmin,
isMobile,
showError,
showSuccess,
showWarning,
timestamp2string,
timestamp2string1,
getQuotaWithUnit,
@@ -51,9 +52,9 @@ import {
renderQuota,
modelToColor,
copy,
showSuccess,
getRelativeTime
} from '../../helpers';
import { useIsMobile } from '../../hooks/useIsMobile.js';
import { UserContext } from '../../context/User/index.js';
import { StatusContext } from '../../context/Status/index.js';
import { useTranslation } from 'react-i18next';
@@ -66,6 +67,7 @@ const Detail = (props) => {
// ========== Hooks - Navigation & Translation ==========
const { t } = useTranslation();
const navigate = useNavigate();
const isMobile = useIsMobile();
// ========== Hooks - Refs ==========
const formRef = useRef();
@@ -1150,7 +1152,7 @@ const Detail = (props) => {
onOk={handleSearchConfirm}
onCancel={handleCloseModal}
closeOnEsc={true}
size={isMobile() ? 'full-width' : 'small'}
size={isMobile ? 'full-width' : 'small'}
centered
>
<Form ref={formRef} layout='vertical' className="w-full">

View File

@@ -1,6 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
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 { StatusContext } from '../../context/Status';
import { marked } from 'marked';
@@ -18,6 +19,7 @@ const Home = () => {
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState('');
const [noticeVisible, setNoticeVisible] = useState(false);
const isMobile = useIsMobile();
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
const docsLink = statusState?.status?.docs_link || '';
const serverAddress = statusState?.status?.server_address || window.location.origin;
@@ -98,7 +100,7 @@ const Home = () => {
<NoticeModal
visible={noticeVisible}
onClose={() => setNoticeVisible(false)}
isMobile={isMobile()}
isMobile={isMobile}
/>
{homePageContentLoaded && homePageContent === '' ? (
<div className="w-full overflow-x-hidden">
@@ -133,7 +135,7 @@ const Home = () => {
readonly
value={serverAddress}
className="flex-1 !rounded-full"
size={isMobile() ? 'default' : 'large'}
size={isMobile ? 'default' : 'large'}
suffix={
<div className="flex items-center gap-2">
<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">
<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('获取密钥')}
</Button>
</Link>
{isDemoSiteMode && statusState?.status?.version ? (
<Button
size={isMobile() ? "default" : "large"}
size={isMobile ? "default" : "large"}
className="flex items-center !rounded-3xl px-6 py-2"
icon={<IconGithubLogo />}
onClick={() => window.open('https://github.com/QuantumNous/new-api', '_blank')}
@@ -176,7 +178,7 @@ const Home = () => {
) : (
docsLink && (
<Button
size={isMobile() ? "default" : "large"}
size={isMobile ? "default" : "large"}
className="flex items-center !rounded-3xl px-6 py-2"
icon={<IconFile />}
onClick={() => window.open(docsLink, '_blank')}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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