Просмотр исходного кода

✨ refactor: layout logic to enhance front-end responsiveness

Merge pull request #1377 from QuantumNous/refactor/layout
同語 7 месяцев назад
Родитель
Сommit
3bf0748389
35 измененных файлов с 277 добавлено и 432 удалено
  1. 39 26
      web/src/components/auth/OAuth2Callback.js
  2. 6 14
      web/src/components/common/Loading.js
  3. 23 16
      web/src/components/layout/HeaderBar.js
  4. 26 21
      web/src/components/layout/PageLayout.js
  5. 39 53
      web/src/components/layout/SiderBar.js
  6. 3 2
      web/src/components/settings/ChannelSelectorModal.js
  7. 4 2
      web/src/components/table/ChannelsTable.js
  8. 0 227
      web/src/context/Style/index.js
  9. 4 2
      web/src/helpers/render.js
  10. 4 4
      web/src/helpers/utils.js
  11. 16 0
      web/src/hooks/useIsMobile.js
  12. 22 0
      web/src/hooks/useSidebarCollapsed.js
  13. 0 2
      web/src/i18n/locales/en.json
  14. 16 0
      web/src/index.css
  15. 15 5
      web/src/index.js
  16. 1 1
      web/src/pages/About/index.js
  17. 3 2
      web/src/pages/Channel/EditChannel.js
  18. 1 1
      web/src/pages/Channel/index.js
  19. 1 1
      web/src/pages/Chat2Link/index.js
  20. 6 4
      web/src/pages/Detail/index.js
  21. 8 6
      web/src/pages/Home/index.js
  22. 1 1
      web/src/pages/Log/index.js
  23. 1 1
      web/src/pages/Midjourney/index.js
  24. 14 25
      web/src/pages/Playground/index.js
  25. 1 1
      web/src/pages/Pricing/index.js
  26. 3 2
      web/src/pages/Redemption/EditRedemption.js
  27. 1 1
      web/src/pages/Redemption/index.js
  28. 5 2
      web/src/pages/Setting/Ratio/UpstreamRatioSync.js
  29. 1 1
      web/src/pages/Setting/index.js
  30. 1 1
      web/src/pages/Task/index.js
  31. 3 2
      web/src/pages/Token/EditToken.js
  32. 1 1
      web/src/pages/Token/index.js
  33. 4 2
      web/src/pages/User/AddUser.js
  34. 3 2
      web/src/pages/User/EditUser.js
  35. 1 1
      web/src/pages/User/index.js

+ 39 - 26
web/src/components/auth/OAuth2Callback.js

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

+ 6 - 14
web/src/components/common/Loading.js

@@ -1,22 +1,14 @@
 import React from 'react';
 import { Spin } from '@douyinfe/semi-ui';
-import { useTranslation } from 'react-i18next';
 
-const Loading = ({ prompt: name = '', size = 'large' }) => {
-  const { t } = useTranslation();
+const Loading = ({ size = 'small' }) => {
 
   return (
-    <div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]">
-      <div className="flex flex-col items-center">
-        <Spin
-          size={size}
-          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 className="fixed inset-0 w-screen h-screen flex items-center justify-center">
+      <Spin
+        size={size}
+        spinning={true}
+      />
     </div>
   );
 };

+ 23 - 16
web/src/components/layout/HeaderBar.js

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

+ 26 - 21
web/src/components/layout/PageLayout.js

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

+ 39 - 53
web/src/components/layout/SiderBar.js

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

+ 3 - 2
web/src/components/settings/ChannelSelectorModal.js

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

+ 4 - 2
web/src/components/table/ChannelsTable.js

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

+ 0 - 227
web/src/context/Style/index.js

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

+ 4 - 2
web/src/helpers/render.js

@@ -1,6 +1,7 @@
 import i18next from 'i18next';
 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 {
   OpenAI,
@@ -669,7 +670,8 @@ const measureTextWidth = (
 };
 
 export function truncateText(text, maxWidth = 200) {
-  if (!isMobile()) {
+  const isMobileScreen = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches;
+  if (!isMobileScreen) {
     return text;
   }
   if (!text) return text;

+ 4 - 4
web/src/helpers/utils.js

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

+ 16 - 0
web/src/hooks/useIsMobile.js

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

+ 22 - 0
web/src/hooks/useSidebarCollapsed.js

@@ -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];
+}; 

+ 0 - 2
web/src/i18n/locales/en.json

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

+ 16 - 0
web/src/index.css

@@ -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 {
   font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif;
   color: var(--semi-color-text-0);

+ 15 - 5
web/src/index.js

@@ -6,11 +6,18 @@ import { UserProvider } from './context/User';
 import 'react-toastify/dist/ReactToastify.css';
 import { StatusProvider } from './context/Status';
 import { ThemeProvider } from './context/Theme';
-import { StyleProvider } from './context/Style/index.js';
 import PageLayout from './components/layout/PageLayout.js';
 import './i18n/i18n.js';
 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
 
 const root = ReactDOM.createRoot(document.getElementById('root'));
@@ -18,11 +25,14 @@ root.render(
   <React.StrictMode>
     <StatusProvider>
       <UserProvider>
-        <BrowserRouter>
+        <BrowserRouter
+          future={{
+            v7_startTransition: true,
+            v7_relativeSplatPath: true,
+          }}
+        >
           <ThemeProvider>
-            <StyleProvider>
-              <PageLayout />
-            </StyleProvider>
+            <PageLayout />
           </ThemeProvider>
         </BrowserRouter>
       </UserProvider>

+ 1 - 1
web/src/pages/About/index.js

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

+ 3 - 2
web/src/pages/Channel/EditChannel.js

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

+ 1 - 1
web/src/pages/Channel/index.js

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

+ 1 - 1
web/src/pages/Chat2Link/index.js

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

+ 6 - 4
web/src/pages/Detail/index.js

@@ -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();
@@ -1118,7 +1120,7 @@ const Detail = (props) => {
   }, []);
 
   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">
         <h2
           className="text-2xl font-semibold text-gray-800 transition-opacity duration-1000 ease-in-out"
@@ -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">

+ 8 - 6
web/src/pages/Home/index.js

@@ -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')}

+ 1 - 1
web/src/pages/Log/index.js

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

+ 1 - 1
web/src/pages/Midjourney/index.js

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

+ 14 - 25
web/src/pages/Playground/index.js

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

+ 1 - 1
web/src/pages/Pricing/index.js

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

+ 3 - 2
web/src/pages/Redemption/EditRedemption.js

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

+ 1 - 1
web/src/pages/Redemption/index.js

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

+ 5 - 2
web/src/pages/Setting/Ratio/UpstreamRatioSync.js

@@ -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 - 1
web/src/pages/Setting/index.js

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

+ 1 - 1
web/src/pages/Task/index.js

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

+ 3 - 2
web/src/pages/Token/EditToken.js

@@ -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 - 1
web/src/pages/Token/index.js

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

+ 4 - 2
web/src/pages/User/AddUser.js

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

+ 3 - 2
web/src/pages/User/EditUser.js

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

+ 1 - 1
web/src/pages/User/index.js

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