📱 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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user