📱 feat(TopUp): enhance mobile UX with responsive layout and bottom fixed payment panel

- Convert copy button to Input suffix for cleaner UI design
- Add responsive grid layout for balance cards and preset amounts
  - Mobile (< md): single column layout for better readability
  - Desktop (>= md): multi-column layout for space efficiency
- Implement bottom fixed payment panel on mobile devices
  - Fixed positioning for easy access to payment options
  - Includes custom amount input and payment method buttons
  - Auto-hide on desktop to maintain original layout
- Improve mobile payment flow with sticky bottom controls
- Add proper spacing to prevent content overlap with fixed elements
- Maintain consistent functionality across all breakpoints

This update significantly improves the mobile user experience by making
payment controls easily accessible without scrolling, while preserving
the desktop layout and functionality.
This commit is contained in:
Apple\Apple
2025-06-10 00:40:47 +08:00
parent b035b4d8af
commit b605ff9b02
4 changed files with 153 additions and 87 deletions

View File

@@ -136,9 +136,8 @@ const PageLayout = () => {
flex: '1 0 auto',
overflowY: styleState.isMobile ? 'visible' : 'auto',
WebkitOverflowScrolling: 'touch',
padding: shouldInnerPadding ? '24px' : '0',
padding: shouldInnerPadding ? (styleState.isMobile ? '5px' : '24px') : '0',
position: 'relative',
marginTop: styleState.isMobile ? '2px' : '0',
}}
>
<App />

View File

@@ -46,8 +46,7 @@ import {
Gift,
User,
Settings,
CircleUser,
Users
CircleUser
} from 'lucide-react';
// 侧边栏图标颜色映射

View File

@@ -971,6 +971,8 @@
"最低": "lowest",
"划转额度": "Transfer amount",
"邀请链接": "Invitation link",
"划转邀请额度": "Transfer invitation quota",
"可用邀请额度": "Available invitation quota",
"更多优惠": "More offers",
"企业微信": "Enterprise WeChat",
"点击解绑WxPusher": "Click to unbind WxPusher",

View File

@@ -347,7 +347,7 @@ const TopUp = () => {
};
return (
<div className="mx-auto">
<div className="mx-auto relative min-h-screen lg:min-h-0">
{/* 划转模态框 */}
<Modal
title={
@@ -485,7 +485,7 @@ const TopUp = () => {
>
<div className="space-y-4">
{/* 账户余额信息 */}
<div className="grid grid-cols-2 gap-4 mb-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
<Card className="!rounded-2xl">
<Text type="tertiary" className="mb-1">
{t('当前余额')}
@@ -517,7 +517,7 @@ const TopUp = () => {
{/* 预设充值额度卡片网格 */}
<div>
<Text strong className="block mb-3">{t('选择充值额度')}</Text>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3">
{presetAmounts.map((preset, index) => (
<Card
key={index}
@@ -539,72 +539,74 @@ const TopUp = () => {
))}
</div>
</div>
{/* 桌面端显示的自定义金额和支付按钮 */}
<div className="hidden md:block space-y-4">
<Divider style={{ margin: '24px 0' }}>
<Text className="text-sm font-medium">{t('或输入自定义金额')}</Text>
</Divider>
<Divider style={{ margin: '24px 0' }}>
<Text className="text-sm font-medium">{t('或输入自定义金额')}</Text>
</Divider>
<div>
<div className="flex justify-between mb-2">
<Text strong>{t('充值数量')}</Text>
{amountLoading ? (
<Skeleton.Title style={{ width: '80px', height: '16px' }} />
) : (
<Text type="tertiary">{t('实付金额:') + renderAmount()}</Text>
)}
<div>
<div className="flex justify-between mb-2">
<Text strong>{t('充值数量')}</Text>
{amountLoading ? (
<Skeleton.Title style={{ width: '80px', height: '16px' }} />
) : (
<Text type="tertiary">{t('实付金额:') + renderAmount()}</Text>
)}
</div>
<InputNumber
disabled={!enableOnlineTopUp}
placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
value={topUpCount}
min={minTopUp}
max={999999999}
step={1}
precision={0}
onChange={async (value) => {
if (value && value >= 1) {
setTopUpCount(value);
setSelectedPreset(null);
await getAmount(value);
}
}}
onBlur={(e) => {
const value = parseInt(e.target.value);
if (!value || value < 1) {
setTopUpCount(1);
getAmount(1);
}
}}
size="large"
className="w-full"
formatter={(value) => value ? `${value}` : ''}
parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
/>
</div>
<InputNumber
disabled={!enableOnlineTopUp}
placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
value={topUpCount}
min={minTopUp}
max={999999999}
step={1}
precision={0}
onChange={async (value) => {
if (value && value >= 1) {
setTopUpCount(value);
setSelectedPreset(null);
await getAmount(value);
}
}}
onBlur={(e) => {
const value = parseInt(e.target.value);
if (!value || value < 1) {
setTopUpCount(1);
getAmount(1);
}
}}
size="large"
className="w-full"
formatter={(value) => value ? `${value}` : ''}
parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Button
type="primary"
onClick={() => preTopUp('zfb')}
size="large"
disabled={!enableOnlineTopUp}
loading={paymentLoading && payWay === 'zfb'}
icon={<SiAlipay size={18} />}
style={{ height: '44px' }}
>
<span className="ml-2">{t('支付宝')}</span>
</Button>
<Button
type="primary"
onClick={() => preTopUp('wx')}
size="large"
disabled={!enableOnlineTopUp}
loading={paymentLoading && payWay === 'wx'}
icon={<SiWechat size={18} />}
style={{ height: '44px' }}
>
<span className="ml-2">{t('微信')}</span>
</Button>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Button
type="primary"
onClick={() => preTopUp('zfb')}
size="large"
disabled={!enableOnlineTopUp}
loading={paymentLoading && payWay === 'zfb'}
icon={<SiAlipay size={18} />}
style={{ height: '44px' }}
>
<span className="ml-2">{t('支付宝')}</span>
</Button>
<Button
type="primary"
onClick={() => preTopUp('wx')}
size="large"
disabled={!enableOnlineTopUp}
loading={paymentLoading && payWay === 'wx'}
icon={<SiWechat size={18} />}
style={{ height: '44px' }}
>
<span className="ml-2">{t('微信')}</span>
</Button>
</div>
</div>
</>
)}
@@ -612,7 +614,7 @@ const TopUp = () => {
{!enableOnlineTopUp && (
<Banner
type="warning"
description={t('管理员未开启在线充值功能,请联系管理员或使用兑换码充值。')}
description={t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
closeIcon={null}
className="!rounded-2xl"
/>
@@ -735,22 +737,21 @@ const TopUp = () => {
<div className="space-y-4">
<Title heading={6}>{t('邀请链接')}</Title>
<div className="relative">
<Input
value={affLink}
readOnly
size="large"
/>
<Button
type="primary"
theme="light"
onClick={handleAffLinkClick}
className="absolute right-1 top-1 bottom-1"
icon={<Copy size={14} />}
>
{t('复制')}
</Button>
</div>
<Input
value={affLink}
readOnly
size="large"
suffix={
<Button
type="primary"
theme="light"
onClick={handleAffLinkClick}
icon={<Copy size={14} />}
>
{t('复制')}
</Button>
}
/>
<div className="mt-4">
<Card className="!rounded-2xl">
@@ -781,6 +782,71 @@ const TopUp = () => {
</Card>
</div>
</div>
{/* 移动端底部固定的自定义金额和支付区域 */}
{enableOnlineTopUp && (
<div className="md:hidden fixed bottom-0 left-0 right-0 p-4 shadow-lg z-50" style={{ background: 'var(--semi-color-bg-0)' }}>
<div className="space-y-4">
<div>
<div className="flex justify-between mb-2">
<Text strong>{t('充值数量')}</Text>
{amountLoading ? (
<Skeleton.Title style={{ width: '80px', height: '16px' }} />
) : (
<Text type="tertiary">{t('实付金额:') + renderAmount()}</Text>
)}
</div>
<InputNumber
disabled={!enableOnlineTopUp}
placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
value={topUpCount}
min={minTopUp}
max={999999999}
step={1}
precision={0}
onChange={async (value) => {
if (value && value >= 1) {
setTopUpCount(value);
setSelectedPreset(null);
await getAmount(value);
}
}}
onBlur={(e) => {
const value = parseInt(e.target.value);
if (!value || value < 1) {
setTopUpCount(1);
getAmount(1);
}
}}
className="w-full"
formatter={(value) => value ? `${value}` : ''}
parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Button
type="primary"
onClick={() => preTopUp('zfb')}
disabled={!enableOnlineTopUp}
loading={paymentLoading && payWay === 'zfb'}
icon={<SiAlipay size={18} />}
>
<span className="ml-2">{t('支付宝')}</span>
</Button>
<Button
type="primary"
onClick={() => preTopUp('wx')}
disabled={!enableOnlineTopUp}
loading={paymentLoading && payWay === 'wx'}
icon={<SiWechat size={18} />}
>
<span className="ml-2">{t('微信')}</span>
</Button>
</div>
</div>
</div>
)}
</div>
);
};