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

feat: Enhance mobile UI responsiveness and layout for ChannelsTable and SiderBar

[email protected] 9 месяцев назад
Родитель
Сommit
49bfd2b719

+ 168 - 86
web/src/components/ChannelsTable.js

@@ -605,7 +605,7 @@ const ChannelsTable = () => {
             <Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
           </>
         }
-        style={{ width: 500 }}
+        style={{ width: isMobile() ? '90%' : 500 }}
         bodyStyle={{ padding: '24px' }}
       >
         <div style={{ marginBottom: 20 }}>
@@ -633,7 +633,11 @@ const ChannelsTable = () => {
             }
             
             return (
-              <div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
+              <div key={column.key} style={{ 
+                width: isMobile() ? '100%' : '50%', 
+                marginBottom: 16, 
+                paddingRight: 8 
+              }}>
                 <Checkbox
                   checked={!!visibleColumns[column.key]}
                   onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
@@ -1253,87 +1257,137 @@ const ChannelsTable = () => {
       <Divider style={{ marginBottom: 15 }} />
       <div
         style={{
-          display: isMobile() ? '' : 'flex',
+          display: 'flex',
+          flexDirection: isMobile() ? 'column' : 'row',
           marginTop: isMobile() ? 0 : -45,
           zIndex: 999,
           pointerEvents: 'none'
         }}
       >
         <Space
-          style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
+          style={{ 
+            pointerEvents: 'auto', 
+            marginTop: isMobile() ? 0 : 45,
+            marginBottom: isMobile() ? 16 : 0,
+            display: 'flex',
+            flexWrap: isMobile() ? 'wrap' : 'nowrap',
+            gap: '8px'
+          }}
         >
-          <Typography.Text strong>{t('使用ID排序')}</Typography.Text>
-          <Switch
-            checked={idSort}
-            label={t('使用ID排序')}
-            uncheckedText={t('关')}
-            aria-label={t('是否用ID排序')}
-            onChange={(v) => {
-              localStorage.setItem('id-sort', v + '');
-              setIdSort(v);
-              loadChannels(0, pageSize, v, enableTagMode)
-                .then()
-                .catch((reason) => {
-                  showError(reason);
+          <div style={{ 
+            display: 'flex', 
+            alignItems: 'center',
+            marginRight: 16,
+            flexWrap: 'nowrap'
+          }}>
+            <Typography.Text strong style={{ marginRight: 8 }}>{t('使用ID排序')}</Typography.Text>
+            <Switch
+              checked={idSort}
+              label={t('使用ID排序')}
+              uncheckedText={t('关')}
+              aria-label={t('是否用ID排序')}
+              onChange={(v) => {
+                localStorage.setItem('id-sort', v + '');
+                setIdSort(v);
+                loadChannels(0, pageSize, v, enableTagMode)
+                  .then()
+                  .catch((reason) => {
+                    showError(reason);
+                  });
+              }}
+            ></Switch>
+          </div>
+          
+          <div style={{ 
+            display: 'flex', 
+            flexWrap: 'wrap',
+            gap: '8px'
+          }}>
+            <Button
+              theme="light"
+              type="primary"
+              icon={<IconPlus />}
+              onClick={() => {
+                setEditingChannel({
+                  id: undefined
                 });
-            }}
-          ></Switch>
-          <Button
-            theme="light"
-            type="primary"
-            style={{ marginRight: 8 }}
-            onClick={() => {
-              setEditingChannel({
-                id: undefined
-              });
-              setShowEdit(true);
-            }}
-          >
-            {t('添加渠道')}
-          </Button>
-          <Popconfirm
-            title={t('确定?')}
-            okType={'warning'}
-            onConfirm={testAllChannels}
-            position={isMobile() ? 'top' : 'top'}
-          >
-            <Button theme="light" type="warning" style={{ marginRight: 8 }}>
-              {t('测试所有通道')}
-            </Button>
-          </Popconfirm>
-          <Popconfirm
-            title={t('确定?')}
-            okType={'secondary'}
-            onConfirm={updateAllChannelsBalance}
-          >
-            <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
-              {t('更新所有已启用通道余额')}
+                setShowEdit(true);
+              }}
+            >
+              {t('添加渠道')}
             </Button>
-          </Popconfirm>
-          <Popconfirm
-            title={t('确定是否要删除禁用通道?')}
-            content={t('此修改将不可逆')}
-            okType={'danger'}
-            onConfirm={deleteAllDisabledChannels}
-          >
-            <Button theme="light" type="danger" style={{ marginRight: 8 }}>
-              {t('删除禁用通道')}
+            
+            <Button
+              theme="light"
+              type="primary"
+              icon={<IconRefresh />}
+              onClick={refresh}
+            >
+              {t('刷新')}
             </Button>
-          </Popconfirm>
-
-          <Button
-            theme="light"
-            type="primary"
-            style={{ marginRight: 8 }}
-            onClick={refresh}
-          >
-            {t('刷新')}
-          </Button>
+            
+            <Dropdown
+              trigger="click"
+              render={
+                <Dropdown.Menu>
+                  <Dropdown.Item>
+                    <Popconfirm
+                      title={t('确定?')}
+                      okType={'warning'}
+                      onConfirm={testAllChannels}
+                      position={isMobile() ? 'top' : 'top'}
+                    >
+                      <Button theme="light" type="warning" style={{ width: '100%' }}>
+                        {t('测试所有通道')}
+                      </Button>
+                    </Popconfirm>
+                  </Dropdown.Item>
+                  <Dropdown.Item>
+                    <Popconfirm
+                      title={t('确定?')}
+                      okType={'secondary'}
+                      onConfirm={updateAllChannelsBalance}
+                    >
+                      <Button theme="light" type="secondary" style={{ width: '100%' }}>
+                        {t('更新所有已启用通道余额')}
+                      </Button>
+                    </Popconfirm>
+                  </Dropdown.Item>
+                  <Dropdown.Item>
+                    <Popconfirm
+                      title={t('确定是否要删除禁用通道?')}
+                      content={t('此修改将不可逆')}
+                      okType={'danger'}
+                      onConfirm={deleteAllDisabledChannels}
+                    >
+                      <Button theme="light" type="danger" style={{ width: '100%' }}>
+                        {t('删除禁用通道')}
+                      </Button>
+                    </Popconfirm>
+                  </Dropdown.Item>
+                </Dropdown.Menu>
+              }
+            >
+              <Button theme="light" type="tertiary" icon={<IconSetting />}>
+                {t('批量操作')}
+              </Button>
+            </Dropdown>
+          </div>
         </Space>
       </div>
-      <div style={{ marginTop: 20 }}>
-        <Space>
-          <Typography.Text strong>{t('开启批量操作')}</Typography.Text>
+      <div style={{ 
+        marginTop: 20,
+        display: 'flex',
+        flexDirection: isMobile() ? 'column' : 'row',
+        alignItems: isMobile() ? 'flex-start' : 'center',
+        gap: isMobile() ? '8px' : '16px'
+      }}>
+        <div style={{ 
+          display: 'flex', 
+          alignItems: 'center',
+          marginBottom: isMobile() ? 8 : 0
+        }}>
+          <Typography.Text strong style={{ marginRight: 8 }}>{t('开启批量操作')}</Typography.Text>
           <Switch
             label={t('开启批量操作')}
             uncheckedText={t('关')}
@@ -1341,20 +1395,25 @@ const ChannelsTable = () => {
             onChange={(v) => {
               setEnableBatchDelete(v);
             }}
-          ></Switch>
+          />
+        </div>
+        
+        <div style={{ 
+          display: 'flex', 
+          flexWrap: 'wrap',
+          gap: '8px'
+        }}>
           <Popconfirm
             title={t('确定是否要删除所选通道?')}
             content={t('此修改将不可逆')}
             okType={'danger'}
             onConfirm={batchDeleteChannels}
             disabled={!enableBatchDelete}
-            position={'top'}
           >
             <Button
               disabled={!enableBatchDelete}
               theme="light"
               type="danger"
-              style={{ marginRight: 8 }}
             >
               {t('删除所选通道')}
             </Button>
@@ -1364,17 +1423,27 @@ const ChannelsTable = () => {
             content={t('进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用')}
             okType={'warning'}
             onConfirm={fixChannelsAbilities}
-            position={'top'}
           >
-            <Button theme="light" type="secondary" style={{ marginRight: 8 }}>
+            <Button theme="light" type="secondary">
               {t('修复数据库一致性')}
             </Button>
           </Popconfirm>
-        </Space>
+        </div>
       </div>
-      <div style={{ marginTop: 20 }}>
-        <Space>
-          <Typography.Text strong>{t('标签聚合模式')}</Typography.Text>
+      
+      <div style={{ 
+        marginTop: 20,
+        display: 'flex',
+        flexDirection: isMobile() ? 'column' : 'row',
+        alignItems: isMobile() ? 'flex-start' : 'center',
+        gap: isMobile() ? '8px' : '16px'
+      }}>
+        <div style={{ 
+          display: 'flex', 
+          alignItems: 'center',
+          marginBottom: isMobile() ? 8 : 0
+        }}>
+          <Typography.Text strong style={{ marginRight: 8 }}>{t('标签聚合模式')}</Typography.Text>
           <Switch
             checked={enableTagMode}
             label={t('标签聚合模式')}
@@ -1385,28 +1454,33 @@ const ChannelsTable = () => {
               loadChannels(0, pageSize, idSort, v);
             }}
           />
+        </div>
+        
+        <div style={{ 
+          display: 'flex', 
+          flexWrap: 'wrap',
+          gap: '8px'
+        }}>
           <Button
             disabled={!enableBatchDelete}
             theme="light"
             type="primary"
-            style={{ marginRight: 8 }}
             onClick={() => setShowBatchSetTag(true)}
           >
             {t('批量设置标签')}
           </Button>
+          
           <Button
             theme="light"
             type="tertiary"
             icon={<IconSetting />}
             onClick={() => setShowColumnSelector(true)}
-            style={{ marginRight: 8 }}
           >
             {t('列设置')}
           </Button>
-        </Space>
+        </div>
       </div>
 
-
       <Table
         loading={loading}
         columns={getVisibleColumns()}
@@ -1423,6 +1497,7 @@ const ChannelsTable = () => {
           },
           onPageChange: handlePageChange
         }}
+        expandAllRows={false}
         onRow={handleRow}
         rowSelection={
           enableBatchDelete
@@ -1442,6 +1517,7 @@ const ChannelsTable = () => {
         onCancel={() => setShowBatchSetTag(false)}
         maskClosable={false}
         centered={true}
+        style={{ width: isMobile() ? '90%' : 500 }}
       >
         <div style={{ marginBottom: 20 }}>
           <Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
@@ -1450,7 +1526,13 @@ const ChannelsTable = () => {
           placeholder={t('请输入标签名称')}
           value={batchSetTagValue}
           onChange={(v) => setBatchSetTagValue(v)}
+          size="large"
         />
+        <div style={{ marginTop: 16 }}>
+          <Typography.Text type="secondary">
+            {t('已选择 ${count} 个渠道').replace('${count}', selectedChannels.length)}
+          </Typography.Text>
+        </div>
       </Modal>
       
       {/* 模型测试弹窗 */}
@@ -1464,7 +1546,6 @@ const ChannelsTable = () => {
         footer={null}
         maskClosable={true}
         centered={true}
-        width={600}
       >
         <div style={{ maxHeight: '500px', overflowY: 'auto', padding: '10px' }}>
           {currentTestChannel && (
@@ -1477,8 +1558,9 @@ const ChannelsTable = () => {
               <Input
                 placeholder={t('搜索模型...')}
                 value={modelSearchKeyword}
-                onChange={(value) => setModelSearchKeyword(value)}
+                onChange={(v) => setModelSearchKeyword(v)}
                 style={{ marginBottom: '16px' }}
+                prefix={<IconFilter />}
                 showClear
               />
               

+ 7 - 2
web/src/components/Footer.js

@@ -1,12 +1,14 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useContext } from 'react';
 import { useTranslation } from 'react-i18next';
 import { getFooterHTML, getSystemName } from '../helpers';
 import { Layout, Tooltip } from '@douyinfe/semi-ui';
+import { StyleContext } from '../context/Style/index.js';
 
 const FooterBar = () => {
   const { t } = useTranslation();
   const systemName = getSystemName();
   const [footer, setFooter] = useState(getFooterHTML());
+  const [styleState] = useContext(StyleContext);
   let remainCheckTimes = 5;
 
   const loadFooter = () => {
@@ -57,7 +59,10 @@ const FooterBar = () => {
   }, []);
 
   return (
-    <div style={{ textAlign: 'center' }}>
+    <div style={{
+      textAlign: 'center',
+      paddingBottom: styleState?.isMobile ? '112px' : '5px',
+    }}>
       {footer ? (
         <div
           className='custom-footer'

+ 30 - 20
web/src/components/PageLayout.js

@@ -71,7 +71,12 @@ const PageLayout = () => {
   const isSidebarCollapsed = localStorage.getItem('default_collapse_sidebar') === 'true';
 
   return (
-    <Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
+    <Layout style={{ 
+      height: '100vh', 
+      display: 'flex', 
+      flexDirection: 'column',
+      overflow: 'hidden'
+    }}>
       <Header style={{ 
         padding: 0, 
         height: 'auto', 
@@ -85,46 +90,51 @@ const PageLayout = () => {
         <HeaderBar />
       </Header>
       <Layout style={{ 
-        marginTop: '56px', 
-        height: 'calc(100vh - 56px)', 
-        overflow: styleState.isMobile ? 'auto' : 'hidden' 
+        marginTop: '56px',
+        height: 'calc(100vh - 56px)',
+        overflow: 'auto',
+        display: 'flex',
+        flexDirection: 'column'
       }}>
         {styleState.showSider && (
-          <Sider style={{ 
-            height: 'calc(100vh - 56px)', 
+          <Sider style={{
             position: 'fixed',
             left: 0,
             top: '56px',
-            zIndex: 90,
-            overflowY: 'auto',
-            overflowX: 'hidden',
-            width: 'auto',
-            background: 'transparent',
-            boxShadow: 'none',
+            zIndex: 99,
+            background: 'var(--semi-color-bg-1)',
+            boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
             border: 'none',
-            paddingRight: '5px'
+            paddingRight: '0',
+            transition: 'transform 0.3s ease',
+            height: 'calc(100vh - 56px)',
           }}>
             <SiderBar />
           </Sider>
         )}
         <Layout style={{ 
-          marginLeft: styleState.showSider ? (isSidebarCollapsed ? '60px' : '200px') : '0', 
+          marginLeft: styleState.isMobile ? '0' : (styleState.showSider ? (isSidebarCollapsed ? '60px' : '200px') : '0'), 
           transition: 'margin-left 0.3s ease',
-          height: '100%',
-          overflow: 'auto'
+          flex: '1 1 auto',
+          display: 'flex',
+          flexDirection: 'column'
         }}>
           <Content
             style={{ 
-              height: '100%',
-              overflowY: 'auto', 
+              flex: '1 0 auto',
+              overflowY: 'auto',
               WebkitOverflowScrolling: 'touch',
               padding: styleState.shouldInnerPadding? '24px': '0',
-              position: 'relative'
+              position: 'relative',
+              paddingBottom: styleState.isMobile ? '80px' : '0' // 移动端底部额外内边距
             }}
           >
             <App />
           </Content>
-          <Layout.Footer>
+          <Layout.Footer style={{ 
+            flex: '0 0 auto',
+            width: '100%'
+          }}>
             <FooterBar />
           </Layout.Footer>
         </Layout>

+ 11 - 12
web/src/components/SiderBar.js

@@ -33,6 +33,7 @@ import { setStatusData } from '../helpers/data.js';
 import { stringToColor } from '../helpers/render.js';
 import { useSetTheme, useTheme } from '../context/Theme/index.js';
 import { StyleContext } from '../context/Style/index.js';
+import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 
 // 自定义侧边栏按钮样式
 const navItemStyle = {
@@ -298,16 +299,16 @@ const SiderBar = () => {
         className="custom-sidebar-nav"
         style={{ 
           width: isCollapsed ? '60px' : '200px',
-          height: '100%',
-          boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)',
+          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
           borderRight: '1px solid var(--semi-color-border)',
-          background: 'var(--semi-color-bg-0)',
-          borderRadius: '0 8px 8px 0',
+          background: 'var(--semi-color-bg-1)',
+          borderRadius: styleState.isMobile ? '0' : '0 8px 8px 0',
           transition: 'all 0.3s ease',
           position: 'relative',
           zIndex: 95,
+          height: '100%',
           overflowY: 'auto',
-          WebkitOverflowScrolling: 'touch' // Improve scrolling on iOS devices
+          WebkitOverflowScrolling: 'touch', // Improve scrolling on iOS devices
         }}
         defaultIsCollapsed={
           localStorage.getItem('default_collapse_sidebar') === 'true'
@@ -419,7 +420,7 @@ const SiderBar = () => {
         <Divider style={dividerStyle} />
 
         {/* Workspace Section */}
-        {!isCollapsed && <div style={groupLabelStyle}>{t('控制台')}</div>}
+        {!isCollapsed && <Text style={groupLabelStyle}>{t('控制台')}</Text>}
         {workspaceItems.map((item) => (
           <Nav.Item
             key={item.itemKey}
@@ -436,7 +437,7 @@ const SiderBar = () => {
             <Divider style={dividerStyle} />
 
             {/* Admin Section */}
-            {!isCollapsed && <div style={groupLabelStyle}>{t('管理员')}</div>}
+            {!isCollapsed && <Text style={groupLabelStyle}>{t('管理员')}</Text>}
             {adminItems.map((item) => (
               <Nav.Item
                 key={item.itemKey}
@@ -453,7 +454,7 @@ const SiderBar = () => {
         <Divider style={dividerStyle} />
 
         {/* Finance Management Section */}
-        {!isCollapsed && <div style={groupLabelStyle}>{t('个人中心')}</div>}
+        {!isCollapsed && <Text style={groupLabelStyle}>{t('个人中心')}</Text>}
         {financeItems.map((item) => (
           <Nav.Item
             key={item.itemKey}
@@ -465,12 +466,10 @@ const SiderBar = () => {
         ))}
 
         <Nav.Footer
-          collapseButton={true}
           style={{
-            borderTop: '1px solid var(--semi-color-border)',
-            padding: '12px 0',
-            marginTop: 'auto'
+            paddingBottom: styleState?.isMobile ? '112px' : '0',
           }}
+          collapseButton={true}
           collapseText={(collapsed)=>
             {
               if(collapsed){

+ 24 - 19
web/src/index.css

@@ -82,6 +82,16 @@ body {
   .semi-navigation-horizontal .semi-navigation-header {
     margin-right: 0;
   }
+
+  /* 确保移动端内容可滚动 */
+  .semi-layout-content {
+    -webkit-overflow-scrolling: touch !important;
+  }
+
+  /* 隐藏在移动设备上 */
+  .hide-on-mobile {
+    display: none !important;
+  }
 }
 
 .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
@@ -162,14 +172,14 @@ code {
   }
 }
 
-.semi-navigation-vertical {
-  /*flex: 0 0 auto;*/
-  /*display: flex;*/
-  /*flex-direction: column;*/
-  /*width: 100%;*/
-  height: 100%;
-  overflow: hidden;
-}
+/*.semi-navigation-vertical {*/
+/*  !*flex: 0 0 auto;*!*/
+/*  !*display: flex;*!*/
+/*  !*flex-direction: column;*!*/
+/*  !*width: 100%;*!*/
+/*  height: 100%;*/
+/*  overflow: hidden;*/
+/*}*/
 
 .main-content {
   padding: 4px;
@@ -184,12 +194,6 @@ code {
   font-size: 1.1em;
 }
 
-@media only screen and (max-width: 600px) {
-  .hide-on-mobile {
-    display: none !important;
-  }
-}
-
 /* 顶部栏样式 */
 .topnav {
   padding: 0 16px;
@@ -248,8 +252,9 @@ code {
 }
 
 /* Custom sidebar shadow */
-.custom-sidebar-nav {
-  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
-  -webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
-  -moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;
-}
+/*.custom-sidebar-nav {*/
+/*  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
+/*  -webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
+/*  -moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
+/*  min-height: 100%;*/
+/*}*/

+ 1 - 0
web/vite.config.js

@@ -52,6 +52,7 @@ export default defineConfig({
     },
   },
   server: {
+    host: '0.0.0.0',
     proxy: {
       '/api': {
         target: 'http://localhost:3000',