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

This commit is contained in:
1808837298@qq.com
2025-03-10 19:01:56 +08:00
parent b2938ffe2c
commit 49bfd2b719
6 changed files with 241 additions and 139 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,6 +82,16 @@ body {
.semi-navigation-horizontal .semi-navigation-header { .semi-navigation-horizontal .semi-navigation-header {
margin-right: 0; 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 { .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
@@ -162,14 +172,14 @@ code {
} }
} }
.semi-navigation-vertical { /*.semi-navigation-vertical {*/
/*flex: 0 0 auto;*/ /* !*flex: 0 0 auto;*!*/
/*display: flex;*/ /* !*display: flex;*!*/
/*flex-direction: column;*/ /* !*flex-direction: column;*!*/
/*width: 100%;*/ /* !*width: 100%;*!*/
height: 100%; /* height: 100%;*/
overflow: hidden; /* overflow: hidden;*/
} /*}*/
.main-content { .main-content {
padding: 4px; padding: 4px;
@@ -184,12 +194,6 @@ code {
font-size: 1.1em; font-size: 1.1em;
} }
@media only screen and (max-width: 600px) {
.hide-on-mobile {
display: none !important;
}
}
/* 顶部栏样式 */ /* 顶部栏样式 */
.topnav { .topnav {
padding: 0 16px; padding: 0 16px;
@@ -248,8 +252,9 @@ code {
} }
/* Custom sidebar shadow */ /* Custom sidebar shadow */
.custom-sidebar-nav { /*.custom-sidebar-nav {*/
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important; /* 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; /* -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; /* -moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/
} /* min-height: 100%;*/
/*}*/

View File

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