ソースを参照

✨ refactor: unify layout, adopt Semi UI Forms, dynamic presets, and fix duplicate requests

- Unify TopUp into a single-page layout and remove tabs
- Replace custom inputs with Semi UI Form components (Form.Input, Form.InputNumber, Form.Slot)
- Move online recharge form into the stats Card content for tighter, consistent layout
- Add account stats Card with blue theme (consistent with InvitationCard style)
- Remove RightStatsCard and inline the stats UI directly in RechargeCard
- Change preset amount UI to horizontal quick-action Buttons; swap order with payment methods
- Replace payment method Cards with Semi UI Buttons
  - Use Button icon prop for Alipay/WeChat/Stripe with brand colors
  - Use built-in Button loading; remove custom “processing...” text
- Replace custom spinners with Semi UI Spin and keep Skeleton for amount loading
- Wrap Redeem Code in a Card; use Typography for “Looking for a code? Buy Redeem Code” link
- Show info Banner when online recharge is disabled (instead of warning)

TopUp data flow and logic
- Generate preset amounts from min_topup using multipliers [1,5,10,30,50,100,300,500]
- Deduplicate /api/user/aff using a ref guard; fetch only once on mount
- Simplify user self fetch: update context only; remove unused local states and helpers
- Normalize payment method keys to alipay/wxpay/stripe and assign default colors

Cleanup
- Delete web/src/components/topup/RightStatsCard.jsx
- Remove unused helpers and local states in index.jsx (userQuota, userDataLoading, getUsername)

Dev notes
- No API changes; UI/UX refactor only
- Lint clean (no new linter errors)

Files
- web/src/components/topup/RechargeCard.jsx
- web/src/components/topup/index.jsx
- web/src/components/topup/InvitationCard.jsx (visual parity reference)
- web/src/components/topup/RightStatsCard.jsx (removed)
t0ng7u 6 ヶ月 前
コミット
60dc032cb8

+ 2 - 2
web/src/components/table/mj-logs/MjLogsActions.jsx

@@ -36,7 +36,7 @@ const MjLogsActions = ({
   const showSkeleton = useMinimumLoadingTime(loading);
 
   const placeholder = (
-    <div className="flex items-center text-orange-500 mb-2 md:mb-0">
+    <div className="flex items-center mb-2 md:mb-0">
       <IconEyeOpened className="mr-2" />
       <Skeleton.Title style={{ width: 300, height: 21, borderRadius: 6 }} />
     </div>
@@ -45,7 +45,7 @@ const MjLogsActions = ({
   return (
     <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-2 w-full">
       <Skeleton loading={showSkeleton} active placeholder={placeholder}>
-        <div className="flex items-center text-orange-500 mb-2 md:mb-0">
+        <div className="flex items-center mb-2 md:mb-0">
           <IconEyeOpened className="mr-2" />
           <Text>
             {isAdminUser && showBanner

+ 1 - 1
web/src/components/topup/InvitationCard.jsx

@@ -48,7 +48,7 @@ const InvitationCard = ({
         </Avatar>
         <div>
           <Typography.Text className="text-lg font-medium">{t('邀请奖励')}</Typography.Text>
-          <div className="text-xs text-gray-600 dark:text-gray-400">{t('邀请好友获得额外奖励')}</div>
+          <div className="text-xs">{t('邀请好友获得额外奖励')}</div>
         </div>
       </div>
 

+ 213 - 186
web/src/components/topup/RechargeCard.jsx

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import React from 'react';
+import React, { useRef } from 'react';
 import {
   Avatar,
   Typography,
@@ -28,13 +28,17 @@ import {
   Banner,
   Skeleton,
   Divider,
-  Tabs,
-  TabPane,
+  Form,
+  Space,
+  Row,
+  Col,
+  Spin,
 } from '@douyinfe/semi-ui';
 import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
-import { CreditCard, Gift, Link as LinkIcon, Coins } from 'lucide-react';
+import { CreditCard, Gift, Link as LinkIcon, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react';
 import { IconGift } from '@douyinfe/semi-icons';
-import RightStatsCard from './RightStatsCard';
+import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime';
+
 
 const { Text } = Typography;
 
@@ -70,91 +74,98 @@ const RechargeCard = ({
   renderQuota,
   statusLoading,
 }) => {
+  const onlineFormApiRef = useRef(null);
+  const redeemFormApiRef = useRef(null);
+  const showAmountSkeleton = useMinimumLoadingTime(amountLoading);
   return (
     <Card className="!rounded-2xl shadow-sm border-0">
       {/* 卡片头部 */}
-      <div className="flex flex-col md:flex-row md:items-center md:justify-between mb-4 gap-3">
-        <div className="flex items-center">
-          <Avatar size="small" color="blue" className="mr-3 shadow-md">
-            <CreditCard size={16} />
-          </Avatar>
-          <div>
-            <Typography.Text className="text-lg font-medium">{t('账户充值')}</Typography.Text>
-            <div className="text-xs text-gray-600 dark:text-gray-400">{t('多种充值方式,安全便捷')}</div>
-          </div>
+      <div className="flex items-center mb-4">
+        <Avatar size="small" color="blue" className="mr-3 shadow-md">
+          <CreditCard size={16} />
+        </Avatar>
+        <div>
+          <Typography.Text className="text-lg font-medium">{t('账户充值')}</Typography.Text>
+          <div className="text-xs">{t('多种充值方式,安全便捷')}</div>
         </div>
-        <RightStatsCard t={t} userState={userState} renderQuota={renderQuota} />
       </div>
 
-      <Tabs type="card" defaultActiveKey="online">
-        {/* 在线充值 Tab */}
-        <TabPane
-          tab={
-            <div className="flex items-center">
-              <CreditCard size={16} className="mr-2" />
-              {t('在线充值')}
+      <Space vertical style={{ width: '100%' }}>
+        {/* 统计数据 */}
+        <Card
+          className='!rounded-xl w-full'
+          cover={
+            <div
+              className="relative h-30"
+              style={{
+                '--palette-primary-darkerChannel': '37 99 235',
+                backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`,
+                backgroundSize: 'cover',
+                backgroundPosition: 'center',
+                backgroundRepeat: 'no-repeat'
+              }}
+            >
+              <div className="relative z-10 h-full flex flex-col justify-between p-4">
+                <div className='flex justify-between items-center'>
+                  <Text strong style={{ color: 'white', fontSize: '16px' }}>{t('账户统计')}</Text>
+                </div>
+
+                {/* 统计数据 */}
+                <div className='grid grid-cols-3 gap-6 mt-4'>
+                  {/* 当前余额 */}
+                  <div className='text-center'>
+                    <div className='text-2xl font-bold mb-2' style={{ color: 'white' }}>
+                      {renderQuota(userState?.user?.quota)}
+                    </div>
+                    <div className='flex items-center justify-center text-sm'>
+                      <Wallet size={14} className="mr-1" style={{ color: 'rgba(255,255,255,0.8)' }} />
+                      <Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>{t('当前余额')}</Text>
+                    </div>
+                  </div>
+
+                  {/* 历史消耗 */}
+                  <div className='text-center'>
+                    <div className='text-2xl font-bold mb-2' style={{ color: 'white' }}>
+                      {renderQuota(userState?.user?.used_quota)}
+                    </div>
+                    <div className='flex items-center justify-center text-sm'>
+                      <TrendingUp size={14} className="mr-1" style={{ color: 'rgba(255,255,255,0.8)' }} />
+                      <Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>{t('历史消耗')}</Text>
+                    </div>
+                  </div>
+
+                  {/* 请求次数 */}
+                  <div className='text-center'>
+                    <div className='text-2xl font-bold mb-2' style={{ color: 'white' }}>
+                      {userState?.user?.request_count || 0}
+                    </div>
+                    <div className='flex items-center justify-center text-sm'>
+                      <BarChart2 size={14} className="mr-1" style={{ color: 'rgba(255,255,255,0.8)' }} />
+                      <Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>{t('请求次数')}</Text>
+                    </div>
+                  </div>
+                </div>
+              </div>
             </div>
           }
-          itemKey="online"
         >
-          <div className="py-4">
-            {statusLoading ? (
-              <div className='py-8 flex justify-center'>
-                <div className='animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500'></div>
-              </div>
-            ) : (enableOnlineTopUp || enableStripeTopUp) ? (
+          {/* 在线充值表单 */}
+          {statusLoading ? (
+            <div className='py-8 flex justify-center'>
+              <Spin size="large" />
+            </div>
+          ) : (enableOnlineTopUp || enableStripeTopUp) ? (
+            <Form
+              getFormApi={(api) => (onlineFormApiRef.current = api)}
+              initValues={{ topUpCount: topUpCount }}
+            >
               <div className='space-y-6'>
-                {/* 预设充值额度选择 */}
-                {(enableOnlineTopUp || enableStripeTopUp) && (
-                  <div>
-                    <Text strong className='block mb-3'>
-                      {t('选择充值额度')}
-                    </Text>
-                    <div className='grid grid-cols-2 sm:grid-cols-4 gap-3'>
-                      {presetAmounts.map((preset, index) => (
-                        <Card
-                          key={index}
-                          onClick={() => selectPresetAmount(preset)}
-                          className={`cursor-pointer !rounded-xl transition-all hover:shadow-md ${selectedPreset === preset.value
-                            ? 'border-blue-500 shadow-md'
-                            : 'border-slate-200 hover:border-slate-300 dark:border-slate-600 dark:hover:border-slate-500'
-                            }`}
-                          bodyStyle={{ textAlign: 'center', padding: '12px' }}
-                        >
-                          <div className='font-medium text-lg flex items-center justify-center mb-1'>
-                            <Coins size={16} className='mr-1' />
-                            {formatLargeNumber(preset.value)}
-                          </div>
-                          <div className='text-xs text-gray-500 dark:text-gray-400'>
-                            {t('实付')} ¥{(preset.value * priceRatio).toFixed(2)}
-                          </div>
-                        </Card>
-                      ))}
-                    </div>
-                  </div>
-                )}
-
-                {/* 自定义充值金额 */}
                 {(enableOnlineTopUp || enableStripeTopUp) && (
-                  <div className='space-y-4'>
-                    <Divider style={{ margin: '24px 0' }}>
-                      <Text className='text-sm font-medium text-slate-600 dark:text-slate-400'>
-                        {t('或输入自定义金额')}
-                      </Text>
-                    </Divider>
-
-                    <div>
-                      <div className='flex justify-between mb-2'>
-                        <Text strong className='text-slate-700 dark:text-slate-200'>{t('充值数量')}</Text>
-                        {amountLoading ? (
-                          <Skeleton.Title style={{ width: '80px', height: '16px' }} />
-                        ) : (
-                          <Text className='text-red-600 font-semibold'>
-                            {t('实付金额:')}<span className='font-bold' style={{ color: 'red' }}>{renderAmount()}</span>
-                          </Text>
-                        )}
-                      </div>
-                      <InputNumber
+                  <Row gutter={12}>
+                    <Col xs={24} sm={24} md={24} lg={10} xl={10}>
+                      <Form.InputNumber
+                        field='topUpCount'
+                        label={t('充值数量')}
                         disabled={!enableOnlineTopUp && !enableStripeTopUp}
                         placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
                         value={topUpCount}
@@ -176,126 +187,142 @@ const RechargeCard = ({
                             getAmount(1);
                           }
                         }}
-                        className='w-full !rounded-lg'
                         formatter={(value) => (value ? `${value}` : '')}
                         parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
+                        extraText={
+                          <Skeleton
+                            loading={showAmountSkeleton}
+                            active
+                            placeholder={<Skeleton.Title style={{ width: 120, height: 20, borderRadius: 6 }} />}
+                          >
+                            <Text type="secondary" className='text-red-600'>
+                              {t('实付金额:')}<span style={{ color: 'red' }}>{renderAmount()}</span>
+                            </Text>
+                          </Skeleton>
+                        }
+                        style={{ width: '100%' }}
                       />
-                    </div>
+                    </Col>
+                    <Col xs={24} sm={24} md={24} lg={14} xl={14}>
+                      <Form.Slot label={t('选择支付方式')}>
+                        <Space wrap>
+                          {payMethods.map((payMethod) => (
+                            <Button
+                              key={payMethod.type}
+                              theme='outline'
+                              type='tertiary'
+                              onClick={() => preTopUp(payMethod.type)}
+                              disabled={(!enableOnlineTopUp && payMethod.type !== 'stripe') ||
+                                (!enableStripeTopUp && payMethod.type === 'stripe')}
+                              loading={paymentLoading && payWay === payMethod.type}
+                              icon={
+                                payMethod.type === 'alipay' ? (
+                                  <SiAlipay size={18} color="#1677FF" />
+                                ) : payMethod.type === 'wxpay' ? (
+                                  <SiWechat size={18} color="#07C160" />
+                                ) : payMethod.type === 'stripe' ? (
+                                  <SiStripe size={18} color="#635BFF" />
+                                ) : (
+                                  <CreditCard size={18} color={payMethod.color || 'var(--semi-color-text-2)'} />
+                                )
+                              }
+                            >
+                              {payMethod.name}
+                            </Button>
+                          ))}
+                        </Space>
+                      </Form.Slot>
+                    </Col>
+                  </Row>
+                )}
 
-                    {/* 支付方式选择 */}
-                    <div>
-                      <Text strong className='block mb-3 text-slate-700 dark:text-slate-200'>
-                        {t('选择支付方式')}
-                      </Text>
-                      <div className={`grid gap-3 ${payMethods.length <= 2
-                        ? 'grid-cols-1 sm:grid-cols-2'
-                        : payMethods.length === 3
-                          ? 'grid-cols-1 sm:grid-cols-3'
-                          : 'grid-cols-2 sm:grid-cols-4'
-                        }`}>
-                        {payMethods.map((payMethod) => (
-                          <Card
-                            key={payMethod.type}
-                            onClick={() => preTopUp(payMethod.type)}
-                            className={`cursor-pointer !rounded-xl transition-all hover:shadow-md ${paymentLoading && payWay === payMethod.type
-                              ? 'border-blue-500 shadow-md'
-                              : 'border-slate-200 hover:border-slate-300 dark:border-slate-600 dark:hover:border-slate-500'
-                              } ${(!enableOnlineTopUp && payMethod.type !== 'stripe') ||
-                                (!enableStripeTopUp && payMethod.type === 'stripe')
-                                ? 'opacity-50 cursor-not-allowed'
-                                : ''
-                              }`}
-                            bodyStyle={{ padding: '12px', textAlign: 'center' }}
-                          >
-                            {paymentLoading && payWay === payMethod.type ? (
-                              <div className='flex flex-col items-center justify-center'>
-                                <div className='mb-2'>
-                                  <div className='animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500'></div>
-                                </div>
-                                <div className='text-xs text-slate-500 dark:text-slate-400'>{t('处理中')}</div>
-                              </div>
-                            ) : (
-                              <>
-                                <div className='flex items-center justify-center mb-2'>
-                                  {payMethod.type === 'zfb' ? (
-                                    <SiAlipay size={24} color="#1677FF" />
-                                  ) : payMethod.type === 'wx' ? (
-                                    <SiWechat size={24} color="#07C160" />
-                                  ) : payMethod.type === 'stripe' ? (
-                                    <SiStripe size={24} color="#635BFF" />
-                                  ) : (
-                                    <CreditCard size={24} color={payMethod.color || 'var(--semi-color-text-2)'} />
-                                  )}
-                                </div>
-                                <div className='text-sm font-medium text-slate-700 dark:text-slate-200'>{payMethod.name}</div>
-                              </>
-                            )}
-                          </Card>
-                        ))}
-                      </div>
-                    </div>
-                  </div>
+                {(enableOnlineTopUp || enableStripeTopUp) && (
+                  <Form.Slot label={t('选择充值额度')}>
+                    <Space wrap>
+                      {presetAmounts.map((preset, index) => (
+                        <Button
+                          key={index}
+                          theme={selectedPreset === preset.value ? 'solid' : 'outline'}
+                          type={selectedPreset === preset.value ? 'primary' : 'tertiary'}
+                          onClick={() => {
+                            selectPresetAmount(preset);
+                            onlineFormApiRef.current?.setValue('topUpCount', preset.value);
+                          }}
+                          className='!rounded-lg !py-2 !px-3'
+                        >
+                          <div className='flex items-center gap-2'>
+                            <Coins size={14} className='opacity-80' />
+                            <span className='font-medium'>{formatLargeNumber(preset.value)}</span>
+                            <span className='text-xs text-gray-500'>¥{(preset.value * priceRatio).toFixed(2)}</span>
+                          </div>
+                        </Button>
+                      ))}
+                    </Space>
+                  </Form.Slot>
                 )}
               </div>
-            ) : (
-              <Banner
-                type='warning'
-                description={t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
-                className='!rounded-xl'
-                closeIcon={null}
-              />
-            )}
-          </div>
-        </TabPane>
+            </Form>
+          ) : (
+            <Banner
+              type='info'
+              description={t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
+              className='!rounded-xl'
+              closeIcon={null}
+            />
+          )}
+        </Card>
 
-        {/* 兑换码充值 Tab */}
-        <TabPane
-          tab={
-            <div className="flex items-center">
-              <Gift size={16} className="mr-2" />
+        {/* 兑换码充值 */}
+        <Card
+          className='!rounded-xl w-full'
+          title={
+            <Text type='tertiary' strong>
               {t('兑换码充值')}
-            </div>
+            </Text>
           }
-          itemKey="redeem"
         >
-          <div className="py-4">
-            <div className='space-y-4'>
-              <Input
-                placeholder={t('请输入兑换码')}
-                value={redemptionCode}
-                onChange={(value) => setRedemptionCode(value)}
-                className='!rounded-lg'
-                prefix={<IconGift />}
-                showClear
-              />
-
-              <div className='flex flex-col sm:flex-row gap-2'>
-                {topUpLink && (
+          <Form
+            getFormApi={(api) => (redeemFormApiRef.current = api)}
+            initValues={{ redemptionCode: redemptionCode }}
+          >
+            <Form.Input
+              field='redemptionCode'
+              noLabel={true}
+              placeholder={t('请输入兑换码')}
+              value={redemptionCode}
+              onChange={(value) => setRedemptionCode(value)}
+              prefix={<IconGift />}
+              suffix={
+                <div className='flex items-center gap-2'>
                   <Button
-                    type='secondary'
-                    theme='outline'
-                    onClick={openTopUpLink}
-                    className='flex-1 !rounded-lg !border-slate-300 !text-slate-600 hover:!border-slate-400 hover:!text-slate-700'
-                    icon={<LinkIcon size={16} />}
+                    type='primary'
+                    theme='solid'
+                    onClick={topUp}
+                    loading={isSubmitting}
                   >
-                    {t('获取兑换码')}
+                    {t('兑换额度')}
                   </Button>
-                )}
-                <Button
-                  type='primary'
-                  theme='solid'
-                  onClick={topUp}
-                  disabled={isSubmitting || !redemptionCode}
-                  loading={isSubmitting}
-                  className='flex-1 !rounded-lg !bg-slate-600 hover:!bg-slate-700'
-                >
-                  {isSubmitting ? t('兑换中...') : t('兑换')}
-                </Button>
-              </div>
-            </div>
-          </div>
-        </TabPane>
-      </Tabs>
+                </div>
+              }
+              showClear
+              style={{ width: '100%' }}
+              extraText={topUpLink && (
+                <Text type="tertiary">
+                  {t('在找兑换码?')}
+                  <Text
+                    type="secondary"
+                    underline
+                    className="cursor-pointer"
+                    onClick={openTopUpLink}
+                  >
+                    {t('购买兑换码')}
+                  </Text>
+                </Text>
+              )}
+            />
+          </Form>
+        </Card>
+      </Space>
     </Card>
   );
 };

+ 0 - 60
web/src/components/topup/RightStatsCard.jsx

@@ -1,60 +0,0 @@
-/*
-Copyright (C) 2025 QuantumNous
- 
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
- 
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
- 
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <https://www.gnu.org/licenses/>.
- 
-For commercial licensing, please contact support@quantumnous.com
-*/
-
-import React from 'react';
-import { Card, Typography, Divider } from '@douyinfe/semi-ui';
-import { Wallet, Coins, BarChart2 } from 'lucide-react';
-
-const { Text } = Typography;
-
-const RightStatsCard = ({ t, userState, renderQuota }) => {
-  return (
-    <Card size="small" className="!rounded-xl shadow-sm" bodyStyle={{ padding: '8px 12px' }}>
-      <div className="flex items-center gap-3 lg:gap-4 w-full">
-        <div className="flex items-center justify-end gap-2 flex-1">
-          <Wallet size={16} className="text-slate-600 dark:text-slate-300" />
-          <div className="text-right">
-            <Text size="small" type="tertiary">{t('当前余额')}</Text>
-            <div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{renderQuota(userState?.user?.quota)}</div>
-          </div>
-        </div>
-        <Divider layout="vertical" className="hidden md:block" />
-        <div className="flex items-center justify-end gap-2 flex-1">
-          <Coins size={16} className="text-slate-600 dark:text-slate-300" />
-          <div className="text-right">
-            <Text size="small" type="tertiary">{t('历史消耗')}</Text>
-            <div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{renderQuota(userState?.user?.used_quota)}</div>
-          </div>
-        </div>
-        <Divider layout="vertical" className="hidden md:block" />
-        <div className="flex items-center justify-end gap-2 flex-1">
-          <BarChart2 size={16} className="text-slate-600 dark:text-slate-300" />
-          <div className="text-right">
-            <Text size="small" type="tertiary">{t('请求次数')}</Text>
-            <div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{userState?.user?.request_count || 0}</div>
-          </div>
-        </div>
-      </div>
-    </Card>
-  );
-};
-
-export default RightStatsCard;
-
-

+ 30 - 36
web/src/components/topup/index.jsx

@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 
-import React, { useEffect, useState, useContext } from 'react';
+import React, { useEffect, useState, useContext, useRef } from 'react';
 import {
   API,
   showError,
@@ -63,42 +63,25 @@ const TopUp = () => {
   const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
   const [statusLoading, setStatusLoading] = useState(true);
 
-  const [userQuota, setUserQuota] = useState(0);
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [open, setOpen] = useState(false);
   const [payWay, setPayWay] = useState('');
-  const [userDataLoading, setUserDataLoading] = useState(true);
   const [amountLoading, setAmountLoading] = useState(false);
   const [paymentLoading, setPaymentLoading] = useState(false);
   const [confirmLoading, setConfirmLoading] = useState(false);
   const [payMethods, setPayMethods] = useState([]);
 
+  const affFetchedRef = useRef(false);
+
   // 邀请相关状态
   const [affLink, setAffLink] = useState('');
   const [openTransfer, setOpenTransfer] = useState(false);
   const [transferAmount, setTransferAmount] = useState(0);
 
   // 预设充值额度选项
-  const [presetAmounts, setPresetAmounts] = useState([
-    { value: 5 },
-    { value: 10 },
-    { value: 30 },
-    { value: 50 },
-    { value: 100 },
-    { value: 300 },
-    { value: 500 },
-    { value: 1000 },
-  ]);
+  const [presetAmounts, setPresetAmounts] = useState([]);
   const [selectedPreset, setSelectedPreset] = useState(null);
 
-  const getUsername = () => {
-    if (userState.user) {
-      return userState.user.username;
-    } else {
-      return 'null';
-    }
-  };
-
   const topUp = async () => {
     if (redemptionCode === '') {
       showInfo(t('请输入兑换码!'));
@@ -117,9 +100,6 @@ const TopUp = () => {
           content: t('成功兑换额度:') + renderQuota(data),
           centered: true,
         });
-        setUserQuota((quota) => {
-          return quota + data;
-        });
         if (userState.user) {
           const updatedUser = {
             ...userState.user,
@@ -260,16 +240,13 @@ const TopUp = () => {
   };
 
   const getUserQuota = async () => {
-    setUserDataLoading(true);
     let res = await API.get(`/api/user/self`);
     const { success, message, data } = res.data;
     if (success) {
-      setUserQuota(data.quota);
       userDispatch({ type: 'login', payload: data });
     } else {
       showError(message);
     }
-    setUserDataLoading(false);
   };
 
   // 获取邀请链接
@@ -310,13 +287,9 @@ const TopUp = () => {
   };
 
   useEffect(() => {
-    if (userState?.user?.id) {
-      setUserDataLoading(false);
-      setUserQuota(userState.user.quota);
-    } else {
+    if (!userState?.user?.id) {
       getUserQuota().then();
     }
-    getAffLink().then();
     setTransferAmount(getQuotaPerUnit());
 
     let payMethods = localStorage.getItem('pay_methods');
@@ -330,9 +303,9 @@ const TopUp = () => {
         // 如果没有color,则设置默认颜色
         payMethods = payMethods.map((method) => {
           if (!method.color) {
-            if (method.type === 'zfb') {
+            if (method.type === 'alipay') {
               method.color = 'rgba(var(--semi-blue-5), 1)';
-            } else if (method.type === 'wx') {
+            } else if (method.type === 'wxpay') {
               method.color = 'rgba(var(--semi-green-5), 1)';
             } else if (method.type === 'stripe') {
               method.color = 'rgba(var(--semi-purple-5), 1)';
@@ -365,14 +338,27 @@ const TopUp = () => {
     }
   }, [statusState?.status?.enable_stripe_topup]);
 
+  useEffect(() => {
+    if (affFetchedRef.current) return;
+    affFetchedRef.current = true;
+    getAffLink().then();
+  }, []);
+
   useEffect(() => {
     if (statusState?.status) {
-      setMinTopUp(statusState.status.min_topup || 1);
-      setTopUpCount(statusState.status.min_topup || 1);
+      const minTopUpValue = statusState.status.min_topup || 1;
+      setMinTopUp(minTopUpValue);
+      setTopUpCount(minTopUpValue);
       setTopUpLink(statusState.status.top_up_link || '');
       setEnableOnlineTopUp(statusState.status.enable_online_topup || false);
       setPriceRatio(statusState.status.price || 1);
       setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
+
+      // 根据最小充值金额生成预设充值额度选项
+      setPresetAmounts(generatePresetAmounts(minTopUpValue));
+      // 初始化显示实付金额
+      getAmount(minTopUpValue);
+
       setStatusLoading(false);
     }
   }, [statusState?.status]);
@@ -454,6 +440,14 @@ const TopUp = () => {
     return num.toString();
   };
 
+  // 根据最小充值金额生成预设充值额度选项
+  const generatePresetAmounts = (minAmount) => {
+    const multipliers = [1, 5, 10, 30, 50, 100, 300, 500];
+    return multipliers.map(multiplier => ({
+      value: minAmount * multiplier
+    }));
+  };
+
   return (
     <div className='mx-auto relative min-h-screen lg:min-h-0 mt-[60px] px-2'>
       {/* 划转模态框 */}

+ 3 - 3
web/src/components/topup/modals/PaymentConfirmModal.jsx

@@ -85,9 +85,9 @@ const PaymentConfirmModal = ({
                   if (payMethod) {
                     return (
                       <>
-                        {payMethod.type === 'zfb' ? (
+                        {payMethod.type === 'alipay' ? (
                           <SiAlipay className='mr-2' size={16} color="#1677FF" />
-                        ) : payMethod.type === 'wx' ? (
+                        ) : payMethod.type === 'wxpay' ? (
                           <SiWechat className='mr-2' size={16} color="#07C160" />
                         ) : payMethod.type === 'stripe' ? (
                           <SiStripe className='mr-2' size={16} color="#635BFF" />
@@ -99,7 +99,7 @@ const PaymentConfirmModal = ({
                     );
                   } else {
                     // 默认充值方式
-                    if (payWay === 'zfb') {
+                    if (payWay === 'alipay') {
                       return (
                         <>
                           <SiAlipay className='mr-2' size={16} color="#1677FF" />

+ 4 - 4
web/src/i18n/locales/en.json

@@ -159,7 +159,6 @@
   "测试所有已启用通道": "Test all enabled channels",
   "更新所有已启用通道余额": "Update balance for all enabled channels",
   "刷新": "Refresh",
-  "处理中...": "Processing...",
   "绑定成功!": "Binding successful!",
   "登录成功!": "Login successful!",
   "操作失败,重定向至登录界面中...": "Operation failed, redirecting to login page...",
@@ -168,7 +167,7 @@
   "渠道": "Channel",
   "渠道管理": "Channels",
   "令牌": "Tokens",
-  "兑换": "Redeem",
+  "兑换额度": "Redeem",
   "充值": "Recharge",
   "用户": "Users",
   "日志": "Logs",
@@ -846,7 +845,6 @@
   "充值记录": "Recharge record",
   "返利记录": "Rebate record",
   "确定要充值 $": "Confirm to top up $",
-  "兑换中...": "Redemming",
   "微信/支付宝 实付金额:": "WeChat/Alipay actual payment amount:",
   "Stripe 实付金额:": "Stripe actual payment amount:",
   "支付中...": "Paying",
@@ -857,7 +855,9 @@
   "兑换码充值": "Redemption code recharge",
   "奖励说明": "Reward description",
   "选择支付方式": "Select payment method",
-  "处理中": "Processing",
+  "在找兑换码?": "Looking for a redemption code? ",
+  "购买兑换码": "Buy redemption code",
+  "账户统计": "Account statistics",
   "账户充值": "Account recharge",
   "多种充值方式,安全便捷": "Multiple recharge methods, safe and convenient",
   "支付方式": "Payment method",