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

💳 feat(TopUp): unify payment cards, add header stats, brand icons, and mobile refinements [[memory:5659506]]

- Add RightStatsCard and place it in RechargeCard header
  - Shows current balance, historical spend, and request count
  - Mobile: stacks under title; three metrics split equally (flex-1); vertical dividers hidden on small screens
  - Remove extra margins; small card styling

- RechargeCard
  - Replace redeem code Input icon with Semi UI IconGift
  - Style “Payable amount” number in red and bold; keep same style in confirm modal
  - Always render payment methods as Cards (remove Button variant) with adaptive grid
  - Use brand color icons: SiAlipay (#1677FF), SiWechat (#07C160), SiStripe (#635BFF)
  - Replace Stripe icon with SiStripe
  - Integrate RightStatsCard props; adjust header to flex-col on mobile and flex-row on desktop
  - Hide Banner close button when online top-up is disabled (closeIcon={null})

- InvitationCard
  - Simplify to match RechargeCard’s minimalist slate style
  - Use Card title for “Rewards” and place content directly in body
  - Improve link input and copy button; use Badge dots for bullet points

- TopUp index
  - Remove separate right-column stats card; pass userState and renderQuota to RechargeCard

- Cleanup
  - Lint passes; no functional changes to APIs or business logic
t0ng7u 4 месяцев назад
Родитель
Сommit
3749be3e09

+ 2 - 2
web/src/components/common/ui/CardPro.js

@@ -53,8 +53,8 @@ const CardPro = ({
   searchArea,
   paginationArea, // 新增分页区域
   // 卡片属性
-  shadows = 'always',
-  bordered = false,
+  shadows = '',
+  bordered = true,
   // 自定义样式
   style,
   // 国际化函数

+ 1 - 1
web/src/components/dashboard/ChartsPanel.jsx

@@ -45,7 +45,7 @@ const ChartsPanel = ({
   return (
     <Card
       {...CARD_PROPS}
-      className={`shadow-sm !rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
+      className={`!rounded-2xl ${hasApiInfoPanel ? 'lg:col-span-3' : ''}`}
       title={
         <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between w-full gap-3">
           <div className={FLEX_CENTER_GAP2}>

+ 1 - 1
web/src/components/layout/HeaderBar.js

@@ -367,7 +367,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
               >
                 <div className="flex items-center gap-2">
                   <IconCreditCard size="small" className="text-gray-500 dark:text-gray-400" />
-                  <span>{t('钱包')}</span>
+                  <span>{t('钱包管理')}</span>
                 </div>
               </Dropdown.Item>
               <Dropdown.Item onClick={logout} className="!px-3 !py-1.5 !text-sm !text-semi-color-text-0 hover:!bg-semi-color-fill-1 dark:!text-gray-200 dark:hover:!bg-red-500 dark:hover:!text-white">

+ 10 - 10
web/src/components/layout/SiderBar.js

@@ -113,7 +113,7 @@ const SiderBar = ({ onNavigate = () => { } }) => {
   const financeItems = useMemo(
     () => [
       {
-        text: t('钱包'),
+        text: t('钱包管理'),
         itemKey: 'topup',
         to: '/topup',
       },
@@ -397,6 +397,15 @@ const SiderBar = ({ onNavigate = () => { } }) => {
           {workspaceItems.map((item) => renderNavItem(item))}
         </div>
 
+        {/* 个人中心区域 */}
+        <Divider className="sidebar-divider" />
+        <div>
+          {!collapsed && (
+            <div className="sidebar-group-label">{t('个人中心')}</div>
+          )}
+          {financeItems.map((item) => renderNavItem(item))}
+        </div>
+
         {/* 管理员区域 - 只在管理员时显示 */}
         {isAdmin() && (
           <>
@@ -409,15 +418,6 @@ const SiderBar = ({ onNavigate = () => { } }) => {
             </div>
           </>
         )}
-
-        {/* 个人中心区域 */}
-        <Divider className="sidebar-divider" />
-        <div>
-          {!collapsed && (
-            <div className="sidebar-group-label">{t('个人中心')}</div>
-          )}
-          {financeItems.map((item) => renderNavItem(item))}
-        </div>
       </Nav>
 
       {/* 底部折叠按钮 */}

+ 1 - 1
web/src/components/playground/CustomRequestEditor.js

@@ -139,7 +139,7 @@ const CustomRequestEditor = ({
             description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。"
             icon={<AlertTriangle size={16} />}
             className="!rounded-lg"
-            closable={false}
+            closeIcon={null}
           />
 
           {/* JSON编辑器 */}

+ 1 - 1
web/src/components/settings/PersonalSetting.js

@@ -328,7 +328,7 @@ const PersonalSetting = () => {
   return (
     <div className="mt-[60px]">
       <div className="flex justify-center">
-        <div className="w-full max-w-7xl mx-auto px-4">
+        <div className="w-full max-w-7xl mx-auto px-2">
           {/* 顶部用户信息区域 */}
           <UserInfoHeader t={t} userState={userState} />
 

+ 1 - 1
web/src/components/settings/personal/components/UserInfoHeader.js

@@ -144,7 +144,7 @@ const UserInfoHeader = ({ t, userState }) => {
 
         {/* 移动端统计信息卡片(仅 xs 可见) */}
         <div className="sm:hidden">
-          <Card size="small" className="!rounded-xl !border-0 shadow-sm" bodyStyle={{ padding: '10px 12px' }}>
+          <Card size="small" className="!rounded-xl shadow-sm" bodyStyle={{ padding: '10px 12px' }}>
             <div className="space-y-2">
               <div className="flex items-center justify-between">
                 <div className="flex items-center gap-2">

+ 164 - 0
web/src/components/topup/InvitationCard.jsx

@@ -0,0 +1,164 @@
+/*
+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 [email protected]
+*/
+
+import React from 'react';
+import {
+  Avatar,
+  Typography,
+  Card,
+  Button,
+  Input,
+  Badge,
+} from '@douyinfe/semi-ui';
+import { Copy, Users, BarChart2, TrendingUp, Gift, Zap } from 'lucide-react';
+
+const { Text } = Typography;
+
+const InvitationCard = ({
+  t,
+  userState,
+  renderQuota,
+  setOpenTransfer,
+  affLink,
+  handleAffLinkClick,
+}) => {
+  return (
+    <Card className="!rounded-2xl shadow-sm border-0">
+      {/* 卡片头部 */}
+      <div className="flex items-center mb-4">
+        <Avatar size="small" color="green" className="mr-3 shadow-md">
+          <Gift 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>
+
+      {/* 收益展示区域 */}
+      <div className='space-y-4'>
+        {/* 主要收益卡片 - 待使用收益 */}
+        <Card className='!rounded-xl bg-slate-50 dark:bg-slate-800'>
+          <div className='flex justify-between items-center mb-3'>
+            <div className="flex items-center">
+              <TrendingUp size={16} className="mr-2 text-slate-600 dark:text-slate-300" />
+              <Text strong className='text-slate-700 dark:text-slate-200'>{t('待使用收益')}</Text>
+            </div>
+            <Button
+              type='primary'
+              theme='solid'
+              size='small'
+              disabled={!userState?.user?.aff_quota || userState?.user?.aff_quota <= 0}
+              onClick={() => setOpenTransfer(true)}
+              className='!rounded-lg !bg-slate-600 hover:!bg-slate-700'
+            >
+              <Zap size={12} className="mr-1" />
+              {t('划转到余额')}
+            </Button>
+          </div>
+          <div className='text-2xl sm:text-3xl font-bold text-slate-900 dark:text-slate-100 mb-1'>
+            {renderQuota(userState?.user?.aff_quota || 0)}
+          </div>
+          <div className="text-xs text-slate-500 dark:text-slate-400">{t('可随时划转到账户余额')}</div>
+        </Card>
+
+        {/* 统计数据网格 */}
+        <div className='grid grid-cols-2 gap-4'>
+          <Card className='!rounded-xl bg-slate-50 dark:bg-slate-800'>
+            <div className='flex items-center mb-2'>
+              <BarChart2 size={16} className='mr-2 text-slate-600 dark:text-slate-300' />
+              <Text type='tertiary' className='text-slate-600 dark:text-slate-300'>{t('总收益')}</Text>
+            </div>
+            <div className='text-xl font-semibold text-slate-900 dark:text-slate-100'>
+              {renderQuota(userState?.user?.aff_history_quota || 0)}
+            </div>
+            <div className="text-xs text-slate-500 dark:text-slate-400 mt-1">{t('累计获得')}</div>
+          </Card>
+
+          <Card className='!rounded-xl bg-slate-50 dark:bg-slate-800'>
+            <div className='flex items-center mb-2'>
+              <Users size={16} className='mr-2 text-slate-600 dark:text-slate-300' />
+              <Text type='tertiary' className='text-slate-600 dark:text-slate-300'>{t('邀请人数')}</Text>
+            </div>
+            <div className='text-xl font-semibold text-slate-900 dark:text-slate-100 flex items-center'>
+              {userState?.user?.aff_count || 0} {t('人')}
+            </div>
+            <div className="text-xs text-slate-500 dark:text-slate-400 mt-1">{t('成功邀请')}</div>
+          </Card>
+        </div>
+
+        <div className='!mb-4'>
+          {/* 邀请链接部分 */}
+          <Input
+            value={affLink}
+            readonly
+            className='!rounded-lg'
+            prefix={t('邀请链接')}
+            suffix={
+              <Button
+                type='primary'
+                theme='solid'
+                onClick={handleAffLinkClick}
+                icon={<Copy size={14} />}
+                className='!rounded-lg'
+              >
+                {t('复制')}
+              </Button>
+            }
+          />
+        </div>
+
+        {/* 奖励说明 */}
+        <Card
+          className='!rounded-xl'
+          title={
+            <Text strong className='text-slate-700'>
+              {t('奖励说明')}
+            </Text>
+          }
+        >
+          <div className='space-y-3'>
+            <div className='flex items-start gap-2'>
+              <Badge dot type='success' />
+              <Text type='tertiary' className='text-sm text-slate-600'>
+                {t('邀请好友注册,好友充值后您可获得相应奖励')}
+              </Text>
+            </div>
+
+            <div className='flex items-start gap-2'>
+              <Badge dot type='success' />
+              <Text type='tertiary' className='text-sm text-slate-600'>
+                {t('通过划转功能将奖励额度转入到您的账户余额中')}
+              </Text>
+            </div>
+
+            <div className='flex items-start gap-2'>
+              <Badge dot type='success' />
+              <Text type='tertiary' className='text-sm text-slate-600'>
+                {t('邀请的好友越多,获得的奖励越多')}
+              </Text>
+            </div>
+          </div>
+        </Card>
+      </div>
+    </Card>
+  );
+};
+
+export default InvitationCard;

+ 305 - 0
web/src/components/topup/RechargeCard.jsx

@@ -0,0 +1,305 @@
+/*
+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 [email protected]
+*/
+
+import React from 'react';
+import {
+  Avatar,
+  Typography,
+  Card,
+  Button,
+  Input,
+  InputNumber,
+  Banner,
+  Skeleton,
+  Divider,
+  Tabs,
+  TabPane,
+} from '@douyinfe/semi-ui';
+import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si';
+import { CreditCard, Gift, Link as LinkIcon, Coins } from 'lucide-react';
+import { IconGift } from '@douyinfe/semi-icons';
+import RightStatsCard from './RightStatsCard';
+
+const { Text } = Typography;
+
+const RechargeCard = ({
+  t,
+  enableOnlineTopUp,
+  enableStripeTopUp,
+  presetAmounts,
+  selectedPreset,
+  selectPresetAmount,
+  formatLargeNumber,
+  priceRatio,
+  topUpCount,
+  minTopUp,
+  renderQuotaWithAmount,
+  getAmount,
+  setTopUpCount,
+  setSelectedPreset,
+  renderAmount,
+  amountLoading,
+  payMethods,
+  preTopUp,
+  paymentLoading,
+  payWay,
+  redemptionCode,
+  setRedemptionCode,
+  topUp,
+  isSubmitting,
+  topUpLink,
+  openTopUpLink,
+  // 新增:用于右侧统计卡片
+  userState,
+  renderQuota,
+  statusLoading,
+}) => {
+  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>
+        <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('在线充值')}
+            </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) ? (
+              <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
+                        disabled={!enableOnlineTopUp && !enableStripeTopUp}
+                        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 !rounded-lg'
+                        formatter={(value) => (value ? `${value}` : '')}
+                        parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0}
+                      />
+                    </div>
+
+                    {/* 支付方式选择 */}
+                    <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} className='text-slate-500' />
+                                  )}
+                                </div>
+                                <div className='text-sm font-medium text-slate-700 dark:text-slate-200'>{payMethod.name}</div>
+                              </>
+                            )}
+                          </Card>
+                        ))}
+                      </div>
+                    </div>
+                  </div>
+                )}
+
+              </div>
+            ) : (
+              <div className='py-8'>
+                <Banner
+                  type='warning'
+                  description={t('管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。')}
+                  className='!rounded-xl'
+                  closeIcon={null}
+                />
+              </div>
+            )}
+          </div>
+        </TabPane>
+
+        {/* 兑换码充值 Tab */}
+        <TabPane
+          tab={
+            <div className="flex items-center">
+              <Gift size={16} className="mr-2" />
+              {t('兑换码充值')}
+            </div>
+          }
+          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 />}
+              />
+
+              <div className='flex flex-col sm:flex-row gap-2'>
+                {topUpLink && (
+                  <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} />}
+                  >
+                    {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>
+    </Card>
+  );
+};
+
+export default RechargeCard;

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

@@ -0,0 +1,60 @@
+/*
+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 [email protected]
+*/
+
+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;
+
+

+ 542 - 0
web/src/components/topup/index.jsx

@@ -0,0 +1,542 @@
+/*
+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 [email protected]
+*/
+
+import React, { useEffect, useState, useContext } from 'react';
+import {
+  API,
+  showError,
+  showInfo,
+  showSuccess,
+  renderQuota,
+  renderQuotaWithAmount,
+  copy,
+  getQuotaPerUnit,
+} from '../../helpers';
+import {
+  Modal,
+  Toast,
+} from '@douyinfe/semi-ui';
+import { useTranslation } from 'react-i18next';
+import { UserContext } from '../../context/User';
+import { StatusContext } from '../../context/Status/index.js';
+
+import RechargeCard from './RechargeCard';
+import InvitationCard from './InvitationCard';
+import TransferModal from './modals/TransferModal';
+import PaymentConfirmModal from './modals/PaymentConfirmModal';
+
+const TopUp = () => {
+  const { t } = useTranslation();
+  const [userState, userDispatch] = useContext(UserContext);
+  const [statusState] = useContext(StatusContext);
+
+  const [redemptionCode, setRedemptionCode] = useState('');
+  const [amount, setAmount] = useState(0.0);
+  const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1);
+  const [topUpCount, setTopUpCount] = useState(
+    statusState?.status?.min_topup || 1,
+  );
+  const [topUpLink, setTopUpLink] = useState(
+    statusState?.status?.top_up_link || '',
+  );
+  const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(
+    statusState?.status?.enable_online_topup || false,
+  );
+  const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1);
+
+  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 [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 [selectedPreset, setSelectedPreset] = useState(null);
+
+  const getUsername = () => {
+    if (userState.user) {
+      return userState.user.username;
+    } else {
+      return 'null';
+    }
+  };
+
+  const topUp = async () => {
+    if (redemptionCode === '') {
+      showInfo(t('请输入兑换码!'));
+      return;
+    }
+    setIsSubmitting(true);
+    try {
+      const res = await API.post('/api/user/topup', {
+        key: redemptionCode,
+      });
+      const { success, message, data } = res.data;
+      if (success) {
+        showSuccess(t('兑换成功!'));
+        Modal.success({
+          title: t('兑换成功!'),
+          content: t('成功兑换额度:') + renderQuota(data),
+          centered: true,
+        });
+        setUserQuota((quota) => {
+          return quota + data;
+        });
+        if (userState.user) {
+          const updatedUser = {
+            ...userState.user,
+            quota: userState.user.quota + data,
+          };
+          userDispatch({ type: 'login', payload: updatedUser });
+        }
+        setRedemptionCode('');
+      } else {
+        showError(message);
+      }
+    } catch (err) {
+      showError(t('请求失败'));
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  const openTopUpLink = () => {
+    if (!topUpLink) {
+      showError(t('超级管理员未设置充值链接!'));
+      return;
+    }
+    window.open(topUpLink, '_blank');
+  };
+
+  const preTopUp = async (payment) => {
+    if (payment === 'stripe') {
+      if (!enableStripeTopUp) {
+        showError(t('管理员未开启Stripe充值!'));
+        return;
+      }
+    } else {
+      if (!enableOnlineTopUp) {
+        showError(t('管理员未开启在线充值!'));
+        return;
+      }
+    }
+
+    setPayWay(payment);
+    setPaymentLoading(true);
+    try {
+      if (payment === 'stripe') {
+        await getStripeAmount();
+      } else {
+        await getAmount();
+      }
+
+      if (topUpCount < minTopUp) {
+        showError(t('充值数量不能小于') + minTopUp);
+        return;
+      }
+      setOpen(true);
+    } catch (error) {
+      showError(t('获取金额失败'));
+    } finally {
+      setPaymentLoading(false);
+    }
+  };
+
+  const onlineTopUp = async () => {
+    if (payWay === 'stripe') {
+      // Stripe 支付处理
+      if (amount === 0) {
+        await getStripeAmount();
+      }
+    } else {
+      // 普通支付处理
+      if (amount === 0) {
+        await getAmount();
+      }
+    }
+
+    if (topUpCount < minTopUp) {
+      showError('充值数量不能小于' + minTopUp);
+      return;
+    }
+    setConfirmLoading(true);
+    try {
+      let res;
+      if (payWay === 'stripe') {
+        // Stripe 支付请求
+        res = await API.post('/api/user/stripe/pay', {
+          amount: parseInt(topUpCount),
+          payment_method: 'stripe',
+        });
+      } else {
+        // 普通支付请求
+        res = await API.post('/api/user/pay', {
+          amount: parseInt(topUpCount),
+          payment_method: payWay,
+        });
+      }
+
+      if (res !== undefined) {
+        const { message, data } = res.data;
+        if (message === 'success') {
+          if (payWay === 'stripe') {
+            // Stripe 支付回调处理
+            window.open(data.pay_link, '_blank');
+          } else {
+            // 普通支付表单提交
+            let params = data;
+            let url = res.data.url;
+            let form = document.createElement('form');
+            form.action = url;
+            form.method = 'POST';
+            let isSafari =
+              navigator.userAgent.indexOf('Safari') > -1 &&
+              navigator.userAgent.indexOf('Chrome') < 1;
+            if (!isSafari) {
+              form.target = '_blank';
+            }
+            for (let key in params) {
+              let input = document.createElement('input');
+              input.type = 'hidden';
+              input.name = key;
+              input.value = params[key];
+              form.appendChild(input);
+            }
+            document.body.appendChild(form);
+            form.submit();
+            document.body.removeChild(form);
+          }
+        } else {
+          showError(data);
+        }
+      } else {
+        showError(res);
+      }
+    } catch (err) {
+      console.log(err);
+      showError(t('支付请求失败'));
+    } finally {
+      setOpen(false);
+      setConfirmLoading(false);
+    }
+  };
+
+  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);
+  };
+
+  // 获取邀请链接
+  const getAffLink = async () => {
+    const res = await API.get('/api/user/aff');
+    const { success, message, data } = res.data;
+    if (success) {
+      let link = `${window.location.origin}/register?aff=${data}`;
+      setAffLink(link);
+    } else {
+      showError(message);
+    }
+  };
+
+  // 划转邀请额度
+  const transfer = async () => {
+    if (transferAmount < getQuotaPerUnit()) {
+      showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
+      return;
+    }
+    const res = await API.post(`/api/user/aff_transfer`, {
+      quota: transferAmount,
+    });
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess(message);
+      setOpenTransfer(false);
+      getUserQuota().then();
+    } else {
+      showError(message);
+    }
+  };
+
+  // 复制邀请链接
+  const handleAffLinkClick = async () => {
+    await copy(affLink);
+    showSuccess(t('邀请链接已复制到剪切板'));
+  };
+
+  useEffect(() => {
+    if (userState?.user?.id) {
+      setUserDataLoading(false);
+      setUserQuota(userState.user.quota);
+    } else {
+      getUserQuota().then();
+    }
+    getAffLink().then();
+    setTransferAmount(getQuotaPerUnit());
+
+    let payMethods = localStorage.getItem('pay_methods');
+    try {
+      payMethods = JSON.parse(payMethods);
+      if (payMethods && payMethods.length > 0) {
+        // 检查name和type是否为空
+        payMethods = payMethods.filter((method) => {
+          return method.name && method.type;
+        });
+        // 如果没有color,则设置默认颜色
+        payMethods = payMethods.map((method) => {
+          if (!method.color) {
+            if (method.type === 'zfb') {
+              method.color = 'rgba(var(--semi-blue-5), 1)';
+            } else if (method.type === 'wx') {
+              method.color = 'rgba(var(--semi-green-5), 1)';
+            } else if (method.type === 'stripe') {
+              method.color = 'rgba(var(--semi-purple-5), 1)';
+            } else {
+              method.color = 'rgba(var(--semi-primary-5), 1)';
+            }
+          }
+          return method;
+        });
+      } else {
+        payMethods = [];
+      }
+
+      // 如果启用了 Stripe 支付,添加到支付方法列表
+      if (statusState?.status?.enable_stripe_topup) {
+        const hasStripe = payMethods.some(method => method.type === 'stripe');
+        if (!hasStripe) {
+          payMethods.push({
+            name: 'Stripe',
+            type: 'stripe',
+            color: 'rgba(var(--semi-purple-5), 1)'
+          });
+        }
+      }
+
+      setPayMethods(payMethods);
+    } catch (e) {
+      console.log(e);
+      showError(t('支付方式配置错误, 请联系管理员'));
+    }
+  }, [statusState?.status?.enable_stripe_topup]);
+
+  useEffect(() => {
+    if (statusState?.status) {
+      setMinTopUp(statusState.status.min_topup || 1);
+      setTopUpCount(statusState.status.min_topup || 1);
+      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);
+      setStatusLoading(false);
+    }
+  }, [statusState?.status]);
+
+  const renderAmount = () => {
+    return amount + ' ' + t('元');
+  };
+
+  const getAmount = async (value) => {
+    if (value === undefined) {
+      value = topUpCount;
+    }
+    setAmountLoading(true);
+    try {
+      const res = await API.post('/api/user/amount', {
+        amount: parseFloat(value),
+      });
+      if (res !== undefined) {
+        const { message, data } = res.data;
+        if (message === 'success') {
+          setAmount(parseFloat(data));
+        } else {
+          setAmount(0);
+          Toast.error({ content: '错误:' + data, id: 'getAmount' });
+        }
+      } else {
+        showError(res);
+      }
+    } catch (err) {
+      console.log(err);
+    }
+    setAmountLoading(false);
+  };
+
+  const getStripeAmount = async (value) => {
+    if (value === undefined) {
+      value = topUpCount;
+    }
+    setAmountLoading(true);
+    try {
+      const res = await API.post('/api/user/stripe/amount', {
+        amount: parseFloat(value),
+      });
+      if (res !== undefined) {
+        const { message, data } = res.data;
+        if (message === 'success') {
+          setAmount(parseFloat(data));
+        } else {
+          setAmount(0);
+          Toast.error({ content: '错误:' + data, id: 'getAmount' });
+        }
+      } else {
+        showError(res);
+      }
+    } catch (err) {
+      console.log(err);
+    } finally {
+      setAmountLoading(false);
+    }
+  }
+
+  const handleCancel = () => {
+    setOpen(false);
+  };
+
+  const handleTransferCancel = () => {
+    setOpenTransfer(false);
+  };
+
+  // 选择预设充值额度
+  const selectPresetAmount = (preset) => {
+    setTopUpCount(preset.value);
+    setSelectedPreset(preset.value);
+    setAmount(preset.value * priceRatio);
+  };
+
+  // 格式化大数字显示
+  const formatLargeNumber = (num) => {
+    return num.toString();
+  };
+
+  return (
+    <div className='mx-auto relative min-h-screen lg:min-h-0 mt-[60px] px-2'>
+      {/* 划转模态框 */}
+      <TransferModal
+        t={t}
+        openTransfer={openTransfer}
+        transfer={transfer}
+        handleTransferCancel={handleTransferCancel}
+        userState={userState}
+        renderQuota={renderQuota}
+        getQuotaPerUnit={getQuotaPerUnit}
+        transferAmount={transferAmount}
+        setTransferAmount={setTransferAmount}
+      />
+
+      {/* 充值确认模态框 */}
+      <PaymentConfirmModal
+        t={t}
+        open={open}
+        onlineTopUp={onlineTopUp}
+        handleCancel={handleCancel}
+        confirmLoading={confirmLoading}
+        topUpCount={topUpCount}
+        renderQuotaWithAmount={renderQuotaWithAmount}
+        amountLoading={amountLoading}
+        renderAmount={renderAmount}
+        payWay={payWay}
+        payMethods={payMethods}
+      />
+
+      {/* 用户信息头部 */}
+      <div className='space-y-6'>
+        <div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
+          {/* 左侧充值区域 */}
+          <div className='lg:col-span-7 space-y-6 w-full'>
+            <RechargeCard
+              t={t}
+              enableOnlineTopUp={enableOnlineTopUp}
+              enableStripeTopUp={enableStripeTopUp}
+              presetAmounts={presetAmounts}
+              selectedPreset={selectedPreset}
+              selectPresetAmount={selectPresetAmount}
+              formatLargeNumber={formatLargeNumber}
+              priceRatio={priceRatio}
+              topUpCount={topUpCount}
+              minTopUp={minTopUp}
+              renderQuotaWithAmount={renderQuotaWithAmount}
+              getAmount={getAmount}
+              setTopUpCount={setTopUpCount}
+              setSelectedPreset={setSelectedPreset}
+              renderAmount={renderAmount}
+              amountLoading={amountLoading}
+              payMethods={payMethods}
+              preTopUp={preTopUp}
+              paymentLoading={paymentLoading}
+              payWay={payWay}
+              redemptionCode={redemptionCode}
+              setRedemptionCode={setRedemptionCode}
+              topUp={topUp}
+              isSubmitting={isSubmitting}
+              topUpLink={topUpLink}
+              openTopUpLink={openTopUpLink}
+              userState={userState}
+              renderQuota={renderQuota}
+              statusLoading={statusLoading}
+            />
+          </div>
+
+          {/* 右侧信息区域 */}
+          <div className='lg:col-span-5'>
+            <InvitationCard
+              t={t}
+              userState={userState}
+              renderQuota={renderQuota}
+              setOpenTransfer={setOpenTransfer}
+              affLink={affLink}
+              handleAffLinkClick={handleAffLinkClick}
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TopUp;

+ 133 - 0
web/src/components/topup/modals/PaymentConfirmModal.jsx

@@ -0,0 +1,133 @@
+/*
+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 [email protected]
+*/
+
+import React from 'react';
+import {
+  Modal,
+  Typography,
+  Card,
+  Skeleton,
+} from '@douyinfe/semi-ui';
+import { SiAlipay, SiWechat } from 'react-icons/si';
+import { CreditCard } from 'lucide-react';
+
+const { Text } = Typography;
+
+const PaymentConfirmModal = ({
+  t,
+  open,
+  onlineTopUp,
+  handleCancel,
+  confirmLoading,
+  topUpCount,
+  renderQuotaWithAmount,
+  amountLoading,
+  renderAmount,
+  payWay,
+  payMethods,
+}) => {
+  return (
+    <Modal
+      title={
+        <div className='flex items-center'>
+          <CreditCard className='mr-2' size={18} />
+          {t('充值确认')}
+        </div>
+      }
+      visible={open}
+      onOk={onlineTopUp}
+      onCancel={handleCancel}
+      maskClosable={false}
+      size='small'
+      centered
+      confirmLoading={confirmLoading}
+    >
+      <div className='space-y-4'>
+        <Card className='!rounded-xl !border-0 bg-slate-50 dark:bg-slate-800'>
+          <div className='space-y-3'>
+            <div className='flex justify-between items-center'>
+              <Text strong className='text-slate-700 dark:text-slate-200'>{t('充值数量')}:</Text>
+              <Text className='text-slate-900 dark:text-slate-100'>{renderQuotaWithAmount(topUpCount)}</Text>
+            </div>
+            <div className='flex justify-between items-center'>
+              <Text strong className='text-slate-700 dark:text-slate-200'>{t('实付金额')}:</Text>
+              {amountLoading ? (
+                <Skeleton.Title style={{ width: '60px', height: '16px' }} />
+              ) : (
+                <Text strong className='font-bold' style={{ color: 'red' }}>
+                  {renderAmount()}
+                </Text>
+              )}
+            </div>
+            <div className='flex justify-between items-center'>
+              <Text strong className='text-slate-700 dark:text-slate-200'>{t('支付方式')}:</Text>
+              <div className='flex items-center'>
+                {(() => {
+                  const payMethod = payMethods.find(
+                    (method) => method.type === payWay,
+                  );
+                  if (payMethod) {
+                    return (
+                      <>
+                        {payMethod.type === 'zfb' ? (
+                          <SiAlipay className='mr-2' size={16} />
+                        ) : payMethod.type === 'wx' ? (
+                          <SiWechat className='mr-2' size={16} />
+                        ) : (
+                          <CreditCard className='mr-2' size={16} />
+                        )}
+                        <Text className='text-slate-900 dark:text-slate-100'>{payMethod.name}</Text>
+                      </>
+                    );
+                  } else {
+                    // 默认充值方式
+                    if (payWay === 'zfb') {
+                      return (
+                        <>
+                          <SiAlipay className='mr-2' size={16} />
+                          <Text className='text-slate-900 dark:text-slate-100'>{t('支付宝')}</Text>
+                        </>
+                      );
+                    } else if (payWay === 'stripe') {
+                      return (
+                        <>
+                          <CreditCard className='mr-2' size={16} />
+                          <Text className='text-slate-900 dark:text-slate-100'>Stripe</Text>
+                        </>
+                      );
+                    } else {
+                      return (
+                        <>
+                          <SiWechat className='mr-2' size={16} />
+                          <Text className='text-slate-900 dark:text-slate-100'>{t('微信')}</Text>
+                        </>
+                      );
+                    }
+                  }
+                })()}
+              </div>
+            </div>
+          </div>
+        </Card>
+      </div>
+    </Modal>
+  );
+};
+
+export default PaymentConfirmModal;

+ 82 - 0
web/src/components/topup/modals/TransferModal.jsx

@@ -0,0 +1,82 @@
+/*
+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 [email protected]
+*/
+
+import React from 'react';
+import {
+  Modal,
+  Typography,
+  Input,
+  InputNumber,
+} from '@douyinfe/semi-ui';
+import { CreditCard } from 'lucide-react';
+
+const TransferModal = ({
+  t,
+  openTransfer,
+  transfer,
+  handleTransferCancel,
+  userState,
+  renderQuota,
+  getQuotaPerUnit,
+  transferAmount,
+  setTransferAmount,
+}) => {
+  return (
+    <Modal
+      title={
+        <div className='flex items-center'>
+          <CreditCard className='mr-2' size={18} />
+          {t('划转邀请额度')}
+        </div>
+      }
+      visible={openTransfer}
+      onOk={transfer}
+      onCancel={handleTransferCancel}
+      maskClosable={false}
+      centered
+    >
+      <div className='space-y-4'>
+        <div>
+          <Typography.Text strong className='block mb-2'>
+            {t('可用邀请额度')}
+          </Typography.Text>
+          <Input
+            value={renderQuota(userState?.user?.aff_quota)}
+            disabled
+            className='!rounded-lg'
+          />
+        </div>
+        <div>
+          <Typography.Text strong className='block mb-2'>
+            {t('划转额度')} · {t('最低') + renderQuota(getQuotaPerUnit())}
+          </Typography.Text>
+          <InputNumber
+            min={getQuotaPerUnit()}
+            max={userState?.user?.aff_quota || 0}
+            value={transferAmount}
+            onChange={(value) => setTransferAmount(value)}
+            className='w-full !rounded-lg'
+          />
+        </div>
+      </div>
+    </Modal>
+  );
+};
+
+export default TransferModal;

+ 2 - 2
web/src/constants/dashboard.constants.js

@@ -21,8 +21,8 @@ For commercial licensing, please contact [email protected]
 export const CHART_CONFIG = { mode: 'desktop-browser' };
 
 export const CARD_PROPS = {
-  shadows: 'always',
-  bordered: false,
+  shadows: '',
+  bordered: true,
   headerLine: true
 };
 

+ 9 - 3
web/src/i18n/locales/en.json

@@ -853,8 +853,15 @@
   "支付宝": "Alipay",
   "待使用收益": "Proceeds to be used",
   "邀请人数": "Number of people invited",
+  "可随时划转到账户余额": "Can be transferred to the account balance at any time",
+  "成功邀请": "Successfully invited",
+  "累计获得": "Accumulated",
   "兑换码充值": "Redemption code recharge",
-  "使用兑换码快速充值": "Use redemption code to quickly recharge",
+  "奖励说明": "Reward description",
+  "人": "",
+  "选择支付方式": "Select payment method",
+  "账户充值": "Account recharge",
+  "多种充值方式,安全便捷": "Multiple recharge methods, safe and convenient",
   "支付方式": "Payment method",
   "邀请奖励": "Invite reward",
   "或输入自定义金额": "Or enter a custom amount",
@@ -937,7 +944,7 @@
   "不是合法的 JSON 字符串": "Not a valid JSON string",
   "个人中心": "Personal center",
   "代理商": "Agent",
-  "钱包": "Wallet",
+  "钱包管理": "Wallet",
   "备注": "Remark",
   "工作台": "Workbench",
   "已复制:": "Copied:",
@@ -1218,7 +1225,6 @@
   "充值数量": "Recharge quantity",
   "实付金额": "Actual payment amount",
   "是否确认充值?": "Confirm recharge?",
-  "我的钱包": "My wallet",
   "默认聊天页面链接": "Default chat page link",
   "聊天页面 2 链接": "Chat page 2 link",
   "失败重试次数": "Failed retry times",

+ 1 - 1309
web/src/pages/TopUp/index.js

@@ -17,1314 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact [email protected]
 */
 
-import React, { useEffect, useState, useContext } from 'react';
-import {
-  API,
-  showError,
-  showInfo,
-  showSuccess,
-  renderQuota,
-  renderQuotaWithAmount,
-  copy,
-  getQuotaPerUnit,
-} from '../../helpers';
-import {
-  Avatar,
-  Typography,
-  Card,
-  Button,
-  Modal,
-  Toast,
-  Input,
-  InputNumber,
-  Banner,
-  Skeleton,
-  Divider,
-} from '@douyinfe/semi-ui';
-import { SiAlipay, SiWechat } from 'react-icons/si';
-import { useTranslation } from 'react-i18next';
-import { UserContext } from '../../context/User';
-import { StatusContext } from '../../context/Status/index.js';
-import { useTheme } from '../../context/Theme';
-import {
-  CreditCard,
-  Gift,
-  Link as LinkIcon,
-  Copy,
-  Users,
-  User,
-  Coins,
-} from 'lucide-react';
-
-const { Text, Title } = Typography;
-
-const TopUp = () => {
-  const { t } = useTranslation();
-  const [userState, userDispatch] = useContext(UserContext);
-  const [statusState] = useContext(StatusContext);
-  const theme = useTheme();
-
-  const [redemptionCode, setRedemptionCode] = useState('');
-  const [topUpCode, setTopUpCode] = useState('');
-  const [amount, setAmount] = useState(0.0);
-  const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1);
-  const [topUpCount, setTopUpCount] = useState(
-    statusState?.status?.min_topup || 1,
-  );
-  const [topUpLink, setTopUpLink] = useState(
-    statusState?.status?.top_up_link || '',
-  );
-  const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(
-    statusState?.status?.enable_online_topup || false,
-  );
-  const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1);
-
-  const [stripeAmount, setStripeAmount] = useState(0.0);
-  const [stripeMinTopUp, setStripeMinTopUp] = useState(statusState?.status?.stripe_min_topup || 1);
-  const [stripeTopUpCount, setStripeTopUpCount] = useState(statusState?.status?.stripe_min_topup || 1);
-  const [enableStripeTopUp, setEnableStripeTopUp] = useState(statusState?.status?.enable_stripe_topup || false);
-  const [stripeOpen, setStripeOpen] = useState(false);
-
-  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 [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 [selectedPreset, setSelectedPreset] = useState(null);
-
-  const getUsername = () => {
-    if (userState.user) {
-      return userState.user.username;
-    } else {
-      return 'null';
-    }
-  };
-
-  const getUserRole = () => {
-    if (!userState.user) return t('普通用户');
-
-    switch (userState.user.role) {
-      case 100:
-        return t('超级管理员');
-      case 10:
-        return t('管理员');
-      case 0:
-      default:
-        return t('普通用户');
-    }
-  };
-
-  const topUp = async () => {
-    if (redemptionCode === '') {
-      showInfo(t('请输入兑换码!'));
-      return;
-    }
-    setIsSubmitting(true);
-    try {
-      const res = await API.post('/api/user/topup', {
-        key: redemptionCode,
-      });
-      const { success, message, data } = res.data;
-      if (success) {
-        showSuccess(t('兑换成功!'));
-        Modal.success({
-          title: t('兑换成功!'),
-          content: t('成功兑换额度:') + renderQuota(data),
-          centered: true,
-        });
-        setUserQuota((quota) => {
-          return quota + data;
-        });
-        if (userState.user) {
-          const updatedUser = {
-            ...userState.user,
-            quota: userState.user.quota + data,
-          };
-          userDispatch({ type: 'login', payload: updatedUser });
-        }
-        setRedemptionCode('');
-      } else {
-        showError(message);
-      }
-    } catch (err) {
-      showError(t('请求失败'));
-    } finally {
-      setIsSubmitting(false);
-    }
-  };
-
-  const openTopUpLink = () => {
-    if (!topUpLink) {
-      showError(t('超级管理员未设置充值链接!'));
-      return;
-    }
-    window.open(topUpLink, '_blank');
-  };
-
-  const preTopUp = async (payment) => {
-    if (!enableOnlineTopUp) {
-      showError(t('管理员未开启在线充值!'));
-      return;
-    }
-    setPayWay(payment);
-    setPaymentLoading(true);
-    try {
-      await getAmount();
-      if (topUpCount < minTopUp) {
-        showError(t('充值数量不能小于') + minTopUp);
-        return;
-      }
-      setOpen(true);
-    } catch (error) {
-      showError(t('获取金额失败'));
-    } finally {
-      setPaymentLoading(false);
-    }
-  };
-
-  const onlineTopUp = async () => {
-    if (amount === 0) {
-      await getAmount();
-    }
-    if (topUpCount < minTopUp) {
-      showError('充值数量不能小于' + minTopUp);
-      return;
-    }
-    setConfirmLoading(true);
-    try {
-      const res = await API.post('/api/user/pay', {
-        amount: parseInt(topUpCount),
-        top_up_code: topUpCode,
-        payment_method: payWay,
-      });
-      if (res !== undefined) {
-        const { message, data } = res.data;
-        if (message === 'success') {
-          let params = data;
-          let url = res.data.url;
-          let form = document.createElement('form');
-          form.action = url;
-          form.method = 'POST';
-          let isSafari =
-            navigator.userAgent.indexOf('Safari') > -1 &&
-            navigator.userAgent.indexOf('Chrome') < 1;
-          if (!isSafari) {
-            form.target = '_blank';
-          }
-          for (let key in params) {
-            let input = document.createElement('input');
-            input.type = 'hidden';
-            input.name = key;
-            input.value = params[key];
-            form.appendChild(input);
-          }
-          document.body.appendChild(form);
-          form.submit();
-          document.body.removeChild(form);
-        } else {
-          showError(data);
-        }
-      } else {
-        showError(res);
-      }
-    } catch (err) {
-      console.log(err);
-      showError(t('支付请求失败'));
-    } finally {
-      setOpen(false);
-      setConfirmLoading(false);
-    }
-  };
-
-  const stripePreTopUp = async () => {
-    if (!enableStripeTopUp) {
-      showError(t('管理员未开启在线充值!'));
-      return;
-    }
-    setPayWay('stripe');
-    setPaymentLoading(true);
-    try {
-      await getStripeAmount();
-      if (stripeTopUpCount < stripeMinTopUp) {
-        showError(t('充值数量不能小于') + stripeMinTopUp);
-        return;
-      }
-      setStripeOpen(true);
-    } catch (error) {
-      showError(t('获取金额失败'));
-    } finally {
-      setPaymentLoading(false);
-    }
-  };
-
-  const onlineStripeTopUp = async () => {
-    if (stripeAmount === 0) {
-      await getStripeAmount();
-    }
-    if (stripeTopUpCount < stripeMinTopUp) {
-      showError(t('充值数量不能小于') + stripeMinTopUp);
-      return;
-    }
-    setConfirmLoading(true);
-    try {
-      const res = await API.post('/api/user/stripe/pay', {
-        amount: parseInt(stripeTopUpCount),
-        payment_method: 'stripe',
-      });
-      if (res !== undefined) {
-        const { message, data } = res.data;
-        if (message === 'success') {
-          processStripeCallback(data);
-        } else {
-          showError(data);
-        }
-      } else {
-        showError(res);
-      }
-    } catch (err) {
-      console.log(err);
-      showError(t('支付请求失败'));
-    } finally {
-      setStripeOpen(false);
-      setConfirmLoading(false);
-    }
-  }
-
-  const processStripeCallback = (data) => {
-    window.open(data.pay_link, '_blank');
-  };
-
-  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);
-  };
-
-  // 获取邀请链接
-  const getAffLink = async () => {
-    const res = await API.get('/api/user/aff');
-    const { success, message, data } = res.data;
-    if (success) {
-      let link = `${window.location.origin}/register?aff=${data}`;
-      setAffLink(link);
-    } else {
-      showError(message);
-    }
-  };
-
-  // 划转邀请额度
-  const transfer = async () => {
-    if (transferAmount < getQuotaPerUnit()) {
-      showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
-      return;
-    }
-    const res = await API.post(`/api/user/aff_transfer`, {
-      quota: transferAmount,
-    });
-    const { success, message } = res.data;
-    if (success) {
-      showSuccess(message);
-      setOpenTransfer(false);
-      getUserQuota().then();
-    } else {
-      showError(message);
-    }
-  };
-
-  // 复制邀请链接
-  const handleAffLinkClick = async () => {
-    await copy(affLink);
-    showSuccess(t('邀请链接已复制到剪切板'));
-  };
-
-  useEffect(() => {
-    if (userState?.user?.id) {
-      setUserDataLoading(false);
-      setUserQuota(userState.user.quota);
-    } else {
-      getUserQuota().then();
-    }
-    getAffLink().then();
-    setTransferAmount(getQuotaPerUnit());
-
-    let payMethods = localStorage.getItem('pay_methods');
-    try {
-      payMethods = JSON.parse(payMethods);
-      if (payMethods && payMethods.length > 0) {
-        // 检查name和type是否为空
-        payMethods = payMethods.filter((method) => {
-          return method.name && method.type;
-        });
-        // 如果没有color,则设置默认颜色
-        payMethods = payMethods.map((method) => {
-          if (!method.color) {
-            if (method.type === 'zfb') {
-              method.color = 'rgba(var(--semi-blue-5), 1)';
-            } else if (method.type === 'wx') {
-              method.color = 'rgba(var(--semi-green-5), 1)';
-            } else {
-              method.color = 'rgba(var(--semi-primary-5), 1)';
-            }
-          }
-          return method;
-        });
-        setPayMethods(payMethods);
-      }
-    } catch (e) {
-      console.log(e);
-      showError(t('支付方式配置错误, 请联系管理员'));
-    }
-  }, []);
-
-  useEffect(() => {
-    if (statusState?.status) {
-      setMinTopUp(statusState.status.min_topup || 1);
-      setTopUpCount(statusState.status.min_topup || 1);
-      setTopUpLink(statusState.status.top_up_link || '');
-      setEnableOnlineTopUp(statusState.status.enable_online_topup || false);
-      setPriceRatio(statusState.status.price || 1);
-
-      setStripeMinTopUp(statusState.status.stripe_min_topup || 1);
-      setStripeTopUpCount(statusState.status.stripe_min_topup || 1);
-      setEnableStripeTopUp(statusState.status.enable_stripe_topup || false);
-    }
-  }, [statusState?.status]);
-
-  const renderAmount = () => {
-    return amount + ' ' + t('元');
-  };
-
-  const renderStripeAmount = () => {
-    return stripeAmount + ' ' + t('元');
-  };
-
-  const getAmount = async (value) => {
-    if (value === undefined) {
-      value = topUpCount;
-    }
-    setAmountLoading(true);
-    try {
-      const res = await API.post('/api/user/amount', {
-        amount: parseFloat(value),
-        top_up_code: topUpCode,
-      });
-      if (res !== undefined) {
-        const { message, data } = res.data;
-        if (message === 'success') {
-          setAmount(parseFloat(data));
-        } else {
-          setAmount(0);
-          Toast.error({ content: '错误:' + data, id: 'getAmount' });
-        }
-      } else {
-        showError(res);
-      }
-    } catch (err) {
-      console.log(err);
-    }
-    setAmountLoading(false);
-  };
-
-  const getStripeAmount = async (value) => {
-    if (value === undefined) {
-      value = stripeTopUpCount
-    }
-    setAmountLoading(true);
-    try {
-      const res = await API.post('/api/user/stripe/amount', {
-        amount: parseFloat(value),
-      });
-      if (res !== undefined) {
-        const { message, data } = res.data;
-        // showInfo(message);
-        if (message === 'success') {
-          setStripeAmount(parseFloat(data));
-        } else {
-          setStripeAmount(0);
-          Toast.error({ content: '错误:' + data, id: 'getAmount' });
-        }
-      } else {
-        showError(res);
-      }
-    } catch (err) {
-      console.log(err);
-    } finally {
-      setAmountLoading(false);
-    }
-  }
-
-  const handleCancel = () => {
-    setOpen(false);
-  };
-
-  const handleStripeCancel = () => {
-    setStripeOpen(false);
-  };
-
-  const handleTransferCancel = () => {
-    setOpenTransfer(false);
-  };
-
-  // 选择预设充值额度
-  const selectPresetAmount = (preset) => {
-    setTopUpCount(preset.value);
-    setSelectedPreset(preset.value);
-    setAmount(preset.value * priceRatio);
-
-    setStripeTopUpCount(preset.value);
-    setStripeAmount(preset.value);
-  };
-
-  // 格式化大数字显示
-  const formatLargeNumber = (num) => {
-    return num.toString();
-  };
-
-  return (
-    <div className='mx-auto relative min-h-screen lg:min-h-0 mt-[60px]'>
-      {/* 划转模态框 */}
-      <Modal
-        title={
-          <div className='flex items-center'>
-            <CreditCard className='mr-2' size={18} />
-            {t('划转邀请额度')}
-          </div>
-        }
-        visible={openTransfer}
-        onOk={transfer}
-        onCancel={handleTransferCancel}
-        maskClosable={false}
-        size='small'
-        centered
-      >
-        <div className='space-y-4'>
-          <div>
-            <Typography.Text strong className='block mb-2'>
-              {t('可用邀请额度')}
-            </Typography.Text>
-            <Input
-              value={renderQuota(userState?.user?.aff_quota)}
-              disabled
-              size='large'
-            />
-          </div>
-          <div>
-            <Typography.Text strong className='block mb-2'>
-              {t('划转额度')} ({t('最低') + renderQuota(getQuotaPerUnit())})
-            </Typography.Text>
-            <InputNumber
-              min={getQuotaPerUnit()}
-              max={userState?.user?.aff_quota || 0}
-              value={transferAmount}
-              onChange={(value) => setTransferAmount(value)}
-              size='large'
-              className='w-full'
-            />
-          </div>
-        </div>
-      </Modal>
-
-      {/* 充值确认模态框 */}
-      <Modal
-        title={
-          <div className='flex items-center'>
-            <CreditCard className='mr-2' size={18} />
-            {t('充值确认')}
-          </div>
-        }
-        visible={open}
-        onOk={onlineTopUp}
-        onCancel={handleCancel}
-        maskClosable={false}
-        size='small'
-        centered
-        confirmLoading={confirmLoading}
-      >
-        <div className='space-y-4'>
-          <div className='flex justify-between items-center py-2'>
-            <Text strong>{t('充值数量')}:</Text>
-            <Text>{renderQuotaWithAmount(topUpCount)}</Text>
-          </div>
-          <div className='flex justify-between items-center py-2'>
-            <Text strong>{t('实付金额')}:</Text>
-            {amountLoading ? (
-              <Skeleton.Title style={{ width: '60px', height: '16px' }} />
-            ) : (
-              <Text type='danger' strong>
-                {renderAmount()}
-              </Text>
-            )}
-          </div>
-          <div className='flex justify-between items-center py-2'>
-            <Text strong>{t('支付方式')}:</Text>
-            <Text>
-              {(() => {
-                const payMethod = payMethods.find(
-                  (method) => method.type === payWay,
-                );
-                if (payMethod) {
-                  return (
-                    <div className='flex items-center'>
-                      {payMethod.type === 'zfb' ? (
-                        <SiAlipay className='mr-1' size={16} />
-                      ) : payMethod.type === 'wx' ? (
-                        <SiWechat className='mr-1' size={16} />
-                      ) : (
-                        <CreditCard className='mr-1' size={16} />
-                      )}
-                      {payMethod.name}
-                    </div>
-                  );
-                } else {
-                  // 默认充值方式
-                  return payWay === 'zfb' ? (
-                    <div className='flex items-center'>
-                      <SiAlipay className='mr-1' size={16} />
-                      {t('支付宝')}
-                    </div>
-                  ) : (
-                    <div className='flex items-center'>
-                      <SiWechat className='mr-1' size={16} />
-                      {t('微信')}
-                    </div>
-                  );
-                }
-              })()}
-            </Text>
-          </div>
-        </div>
-      </Modal>
-
-      <Modal
-          title={t('确定要充值吗')}
-          visible={stripeOpen}
-          onOk={onlineStripeTopUp}
-          onCancel={handleStripeCancel}
-          maskClosable={false}
-          size='small'
-          centered
-          confirmLoading={confirmLoading}
-      >
-        <p>
-          {t('充值数量')}:{stripeTopUpCount}
-        </p>
-        <p>
-          {t('实付金额')}:{renderStripeAmount()}
-        </p>
-        <p>{t('是否确认充值?')}</p>
-      </Modal>
-
-      <div className='grid grid-cols-1 lg:grid-cols-12 gap-6'>
-        {/* 左侧充值区域 */}
-        <div className='lg:col-span-7 space-y-6 w-full'>
-          {/* 在线充值卡片 */}
-          <Card
-            className='!rounded-2xl'
-            shadows='always'
-            bordered={false}
-            header={
-              <div className='px-5 py-4 pb-0'>
-                <div className='flex items-center justify-between'>
-                  <div className='flex items-center'>
-                    <Avatar
-                      className='mr-3 shadow-md flex-shrink-0'
-                      color='blue'
-                    >
-                      <CreditCard size={24} />
-                    </Avatar>
-                    <div>
-                      <Title heading={5} style={{ margin: 0 }}>
-                        {t('在线充值')}
-                      </Title>
-                      <Text type='tertiary' className='text-sm'>
-                        {t('快速方便的充值方式')}
-                      </Text>
-                    </div>
-                  </div>
-
-                  <div className='flex items-center'>
-                    {userDataLoading ? (
-                      <Skeleton.Paragraph style={{ width: '120px' }} rows={1} />
-                    ) : (
-                      <Text type='tertiary' className='hidden sm:block'>
-                        <div className='flex items-center'>
-                          <User size={14} className='mr-1' />
-                          <span className='hidden md:inline'>
-                            {getUsername()} ({getUserRole()})
-                          </span>
-                          <span className='md:hidden'>{getUsername()}</span>
-                        </div>
-                      </Text>
-                    )}
-                  </div>
-                </div>
-              </div>
-            }
-          >
-            <div className='space-y-4'>
-              {/* 账户余额信息 */}
-              <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('当前余额')}
-                  </Text>
-                  {userDataLoading ? (
-                    <Skeleton.Title
-                      style={{ width: '100px', height: '30px' }}
-                    />
-                  ) : (
-                    <div className='text-xl font-semibold mt-2'>
-                      {renderQuota(userState?.user?.quota || userQuota)}
-                    </div>
-                  )}
-                </Card>
-                <Card className='!rounded-2xl'>
-                  <Text type='tertiary' className='mb-1'>
-                    {t('历史消耗')}
-                  </Text>
-                  {userDataLoading ? (
-                    <Skeleton.Title
-                      style={{ width: '100px', height: '30px' }}
-                    />
-                  ) : (
-                    <div className='text-xl font-semibold mt-2'>
-                      {renderQuota(userState?.user?.used_quota || 0)}
-                    </div>
-                  )}
-                </Card>
-              </div>
-
-              {enableOnlineTopUp && (
-                <>
-                  {/* 预设充值额度卡片网格 */}
-                  <div>
-                    <Text strong className='block mb-3'>
-                      {t('选择充值额度')}
-                    </Text>
-                    <div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-3'>
-                      {presetAmounts.map((preset, index) => (
-                        <Card
-                          key={index}
-                          onClick={() => selectPresetAmount(preset)}
-                          className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${selectedPreset === preset.value
-                            ? 'border-blue-500'
-                            : 'border-gray-200 hover:border-gray-300'
-                            }`}
-                          bodyStyle={{ textAlign: 'center' }}
-                        >
-                          <div className='font-medium text-lg flex items-center justify-center mb-1'>
-                            <Coins size={16} className='mr-0.5' />
-                            {formatLargeNumber(preset.value)}
-                          </div>
-                          <div className='text-xs text-gray-500'>
-                            {t('实付')} ¥
-                            {(preset.value * priceRatio).toFixed(2)}
-                          </div>
-                        </Card>
-                      ))}
-                    </div>
-                  </div>
-                  {/* 桌面端显示的自定义金额和支付按钮 */}
-                  <div className='hidden md:block space-y-4'>
-                    <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>
-                      <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>
-                      <Text strong className='block mb-3'>
-                        {t('选择支付方式')}
-                      </Text>
-                      {payMethods.length === 2 ? (
-                        <div className='grid grid-cols-1 sm:grid-cols-2 gap-3'>
-                          {payMethods.map((payMethod) => (
-                            <Button
-                              key={payMethod.type}
-                              type='primary'
-                              onClick={() => preTopUp(payMethod.type)}
-                              size='large'
-                              disabled={!enableOnlineTopUp}
-                              loading={paymentLoading && payWay === payMethod.type}
-                              icon={
-                                payMethod.type === 'zfb' ? (
-                                  <SiAlipay size={16} />
-                                ) : payMethod.type === 'wx' ? (
-                                  <SiWechat size={16} />
-                                ) : (
-                                  <CreditCard size={16} />
-                                )
-                              }
-                              style={{
-                                height: '40px',
-                                color: payMethod.color,
-                              }}
-                              className='transition-all hover:shadow-md w-full'
-                            >
-                              <span className='ml-1'>{payMethod.name}</span>
-                            </Button>
-                          ))}
-                        </div>
-                      ) : payMethods.length === 3 ? (
-                        <div className='grid grid-cols-1 sm:grid-cols-3 gap-3'>
-                          {payMethods.map((payMethod) => (
-                            <Button
-                              key={payMethod.type}
-                              type='primary'
-                              onClick={() => preTopUp(payMethod.type)}
-                              size='large'
-                              disabled={!enableOnlineTopUp}
-                              loading={paymentLoading && payWay === payMethod.type}
-                              icon={
-                                payMethod.type === 'zfb' ? (
-                                  <SiAlipay size={16} />
-                                ) : payMethod.type === 'wx' ? (
-                                  <SiWechat size={16} />
-                                ) : (
-                                  <CreditCard size={16} />
-                                )
-                              }
-                              style={{
-                                height: '40px',
-                                color: payMethod.color,
-                              }}
-                              className='transition-all hover:shadow-md w-full'
-                            >
-                              <span className='ml-1'>{payMethod.name}</span>
-                            </Button>
-                          ))}
-                        </div>
-                      ) : payMethods.length > 3 ? (
-                        <div className='grid grid-cols-2 sm:grid-cols-4 gap-3'>
-                          {payMethods.map((payMethod) => (
-                            <Card
-                              key={payMethod.type}
-                              onClick={() => preTopUp(payMethod.type)}
-                              disabled={!enableOnlineTopUp}
-                              className={`cursor-pointer !rounded-xl p-0 transition-all hover:shadow-md ${paymentLoading && payWay === payMethod.type
-                                ? 'border-blue-400'
-                                : 'border-gray-200 hover:border-gray-300'
-                                }`}
-                              bodyStyle={{
-                                padding: '10px',
-                                textAlign: 'center',
-                                opacity: !enableOnlineTopUp ? 0.5 : 1
-                              }}
-                            >
-                              {paymentLoading && payWay === payMethod.type ? (
-                                <div className='flex flex-col items-center justify-center h-full'>
-                                  <div className='mb-1'>
-                                    <div className='animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500'></div>
-                                  </div>
-                                  <div className='text-xs text-gray-500'>{t('处理中')}</div>
-                                </div>
-                              ) : (
-                                <>
-                                  <div className='flex items-center justify-center mb-1'>
-                                    {payMethod.type === 'zfb' ? (
-                                      <SiAlipay size={20} color={payMethod.color} />
-                                    ) : payMethod.type === 'wx' ? (
-                                      <SiWechat size={20} color={payMethod.color} />
-                                    ) : (
-                                      <CreditCard size={20} color={payMethod.color} />
-                                    )}
-                                  </div>
-                                  <div className='text-sm font-medium'>{payMethod.name}</div>
-                                </>
-                              )}
-                            </Card>
-                          ))}
-                        </div>
-                      ) : (
-                        <div className='grid grid-cols-1 gap-3'>
-                          {payMethods.map((payMethod) => (
-                            <Button
-                              key={payMethod.type}
-                              type='primary'
-                              onClick={() => preTopUp(payMethod.type)}
-                              size='large'
-                              disabled={!enableOnlineTopUp}
-                              loading={paymentLoading && payWay === payMethod.type}
-                              icon={
-                                payMethod.type === 'zfb' ? (
-                                  <SiAlipay size={16} />
-                                ) : payMethod.type === 'wx' ? (
-                                  <SiWechat size={16} />
-                                ) : (
-                                  <CreditCard size={16} />
-                                )
-                              }
-                              style={{
-                                height: '40px',
-                                color: payMethod.color,
-                              }}
-                              className='transition-all hover:shadow-md w-full'
-                            >
-                              <span className='ml-1'>{payMethod.name}</span>
-                            </Button>
-                          ))}
-                        </div>
-                      )}
-                    </div>
-                  </div>
-                </>
-              )}
-
-              {!enableOnlineTopUp && !enableStripeTopUp && (
-                <Banner
-                  type='warning'
-                  description={t(
-                    '管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。',
-                  )}
-                  closeIcon={null}
-                  className='!rounded-2xl'
-                />
-              )}
-
-              {enableStripeTopUp && (
-                  <>
-                    {/* 桌面端显示的自定义金额和支付按钮 */}
-                    <div className='hidden md:block space-y-4'>
-                      <Divider style={{ margin: '24px 0' }}>
-                        <Text className='text-sm font-medium'>
-                          {t(!enableOnlineTopUp ? '或输入自定义金额' : 'Stripe')}
-                        </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('实付金额:') + renderStripeAmount()}
-                              </Text>
-                          )}
-                        </div>
-                        <InputNumber
-                            disabled={!enableStripeTopUp}
-                            placeholder={
-                                t('充值数量,最低 ') + renderQuotaWithAmount(stripeMinTopUp)
-                            }
-                            value={stripeTopUpCount}
-                            min={stripeMinTopUp}
-                            max={999999999}
-                            step={1}
-                            precision={0}
-                            onChange={async (value) => {
-                              if (value && value >= 1) {
-                                setStripeTopUpCount(value);
-                                setSelectedPreset(null);
-                                await getStripeAmount(value);
-                              }
-                            }}
-                            onBlur={(e) => {
-                              const value = parseInt(e.target.value);
-                              if (!value || value < 1) {
-                                setStripeTopUpCount(1);
-                                getStripeAmount(1);
-                              }
-                            }}
-                            size='large'
-                            className='w-full'
-                            formatter={(value) => (value ? `${value}` : '')}
-                            parser={(value) =>
-                                value ? parseInt(value.replace(/[^\d]/g, '')) : 0
-                            }
-                        />
-                      </div>
-
-                      <div>
-                        <Text strong className='block mb-3'>
-                          {t('选择支付方式')}
-                        </Text>
-                          <div className='grid grid-cols-1 gap-3'>
-                            <Button
-                                key='stripe'
-                                type='primary'
-                                onClick={() => stripePreTopUp()}
-                                size='large'
-                                disabled={!enableStripeTopUp}
-                                loading={paymentLoading && payWay === 'stripe'}
-                                icon={<CreditCard size={16} />}
-                                style={{
-                                  height: '40px',
-                                  color: '#b161fe',
-                                }}
-                                className='transition-all hover:shadow-md w-full'
-                            >
-                              <span className='ml-1'>Stripe</span>
-                            </Button>
-                          </div>
-                      </div>
-                    </div>
-                  </>
-              )}
-
-              <Divider style={{ margin: '24px 0' }}>
-                <Text className='text-sm font-medium'>{t('兑换码充值')}</Text>
-              </Divider>
-
-              <Card className='!rounded-2xl'>
-                <div className='flex items-start mb-4'>
-                  <Gift size={16} className='mr-2 mt-0.5' />
-                  <Text strong>{t('使用兑换码快速充值')}</Text>
-                </div>
-
-                <div className='mb-4'>
-                  <Input
-                    placeholder={t('请输入兑换码')}
-                    value={redemptionCode}
-                    onChange={(value) => setRedemptionCode(value)}
-                    size='large'
-                  />
-                </div>
-
-                <div className='flex flex-col sm:flex-row gap-3'>
-                  {topUpLink && (
-                    <Button
-                      type='secondary'
-                      onClick={openTopUpLink}
-                      size='large'
-                      className='flex-1'
-                      icon={<LinkIcon size={16} />}
-                      style={{ height: '40px' }}
-                    >
-                      {t('获取兑换码')}
-                    </Button>
-                  )}
-                  <Button
-                    type='primary'
-                    onClick={topUp}
-                    disabled={isSubmitting || !redemptionCode}
-                    loading={isSubmitting}
-                    size='large'
-                    className='flex-1'
-                    style={{ height: '40px' }}
-                  >
-                    {isSubmitting ? t('兑换中...') : t('兑换')}
-                  </Button>
-                </div>
-              </Card>
-            </div>
-          </Card>
-        </div>
-
-        {/* 右侧邀请信息卡片 */}
-        <div className='lg:col-span-5'>
-          <Card
-            className='!rounded-2xl'
-            shadows='always'
-            bordered={false}
-            header={
-              <div className='px-5 py-4 pb-0'>
-                <div className='flex items-center justify-between'>
-                  <div className='flex items-center'>
-                    <Avatar
-                      className='mr-3 shadow-md flex-shrink-0'
-                      color='green'
-                    >
-                      <Users size={24} />
-                    </Avatar>
-                    <div>
-                      <Title heading={5} style={{ margin: 0 }}>
-                        {t('邀请奖励')}
-                      </Title>
-                      <Text type='tertiary' className='text-sm'>
-                        {t('邀请好友获得额外奖励')}
-                      </Text>
-                    </div>
-                  </div>
-                </div>
-              </div>
-            }
-          >
-            <div className='space-y-6'>
-              <div className='grid grid-cols-1 gap-4'>
-                <Card className='!rounded-2xl'>
-                  <div className='flex justify-between items-center'>
-                    <Text type='tertiary'>{t('待使用收益')}</Text>
-                    <Button
-                      type='primary'
-                      theme='solid'
-                      size='small'
-                      disabled={
-                        !userState?.user?.aff_quota ||
-                        userState?.user?.aff_quota <= 0
-                      }
-                      onClick={() => setOpenTransfer(true)}
-                    >
-                      {t('划转到余额')}
-                    </Button>
-                  </div>
-                  <div className='text-2xl font-semibold mt-2'>
-                    {renderQuota(userState?.user?.aff_quota || 0)}
-                  </div>
-                </Card>
-
-                <div className='grid grid-cols-2 gap-4'>
-                  <Card className='!rounded-2xl'>
-                    <Text type='tertiary'>{t('总收益')}</Text>
-                    <div className='text-xl font-semibold mt-2'>
-                      {renderQuota(userState?.user?.aff_history_quota || 0)}
-                    </div>
-                  </Card>
-                  <Card className='!rounded-2xl'>
-                    <Text type='tertiary'>{t('邀请人数')}</Text>
-                    <div className='text-xl font-semibold mt-2 flex items-center'>
-                      <Users size={16} className='mr-1' />
-                      {userState?.user?.aff_count || 0}
-                    </div>
-                  </Card>
-                </div>
-              </div>
-
-              <div className='space-y-4'>
-                <Title heading={6}>{t('邀请链接')}</Title>
-                <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'>
-                    <div className='space-y-4'>
-                      <div className='flex items-start'>
-                        <div className='w-1.5 h-1.5 rounded-full bg-blue-500 mt-2 mr-3 flex-shrink-0'></div>
-                        <Text type='tertiary' className='text-sm leading-6'>
-                          {t('邀请好友注册,好友充值后您可获得相应奖励')}
-                        </Text>
-                      </div>
-                      <div className='flex items-start'>
-                        <div className='w-1.5 h-1.5 rounded-full bg-green-500 mt-2 mr-3 flex-shrink-0'></div>
-                        <Text type='tertiary' className='text-sm leading-6'>
-                          {t('通过划转功能将奖励额度转入到您的账户余额中')}
-                        </Text>
-                      </div>
-                      <div className='flex items-start'>
-                        <div className='w-1.5 h-1.5 rounded-full bg-purple-500 mt-2 mr-3 flex-shrink-0'></div>
-                        <Text type='tertiary' className='text-sm leading-6'>
-                          {t('邀请的好友越多,获得的奖励越多')}
-                        </Text>
-                      </div>
-                    </div>
-                  </Card>
-                </div>
-              </div>
-            </div>
-          </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>
-              {payMethods.length === 2 ? (
-                <div className='grid grid-cols-2 gap-3'>
-                  {payMethods.map((payMethod) => (
-                    <Button
-                      key={payMethod.type}
-                      type='primary'
-                      onClick={() => preTopUp(payMethod.type)}
-                      disabled={!enableOnlineTopUp}
-                      loading={paymentLoading && payWay === payMethod.type}
-                      icon={
-                        payMethod.type === 'zfb' ? (
-                          <SiAlipay size={16} />
-                        ) : payMethod.type === 'wx' ? (
-                          <SiWechat size={16} />
-                        ) : (
-                          <CreditCard size={16} />
-                        )
-                      }
-                      style={{
-                        color: payMethod.color,
-                      }}
-                      className='h-10'
-                    >
-                      <span className='ml-1'>{payMethod.name}</span>
-                    </Button>
-                  ))}
-                </div>
-              ) : (
-                <div className='grid grid-cols-4 gap-2'>
-                  {payMethods.map((payMethod) => (
-                    <Card
-                      key={payMethod.type}
-                      onClick={() => preTopUp(payMethod.type)}
-                      disabled={!enableOnlineTopUp}
-                      className={`cursor-pointer !rounded-xl p-0 transition-all ${paymentLoading && payWay === payMethod.type
-                        ? 'border-blue-400'
-                        : 'border-gray-200'
-                        }`}
-                      bodyStyle={{
-                        padding: '8px',
-                        textAlign: 'center',
-                        opacity: !enableOnlineTopUp ? 0.5 : 1
-                      }}
-                    >
-                      {paymentLoading && payWay === payMethod.type ? (
-                        <div className='animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500 mx-auto'></div>
-                      ) : (
-                        <>
-                          <div className='flex justify-center'>
-                            {payMethod.type === 'zfb' ? (
-                              <SiAlipay size={18} color={payMethod.color} />
-                            ) : payMethod.type === 'wx' ? (
-                              <SiWechat size={18} color={payMethod.color} />
-                            ) : (
-                              <CreditCard size={18} color={payMethod.color} />
-                            )}
-                          </div>
-                          <div className='text-xs mt-1'>{payMethod.name}</div>
-                        </>
-                      )}
-                    </Card>
-                  ))}
-                </div>
-              )}
-            </div>
-          </div>
-        </div>
-      )}
-    </div>
-  );
-};
+import TopUp from '../../components/topup';
 
 export default TopUp;