Commit b5051217 by t0ng7u

💳 feat(TopUp): unify payment cards, add header stats, brand icons, and mobile…

💳 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
parent 45368dea
......@@ -53,8 +53,8 @@ const CardPro = ({
searchArea,
paginationArea, // 新增分页区域
// 卡片属性
shadows = 'always',
bordered = false,
shadows = '',
bordered = true,
// 自定义样式
style,
// 国际化函数
......
......@@ -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}>
......
......@@ -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">
......
......@@ -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>
{/* 底部折叠按钮 */}
......
......@@ -139,7 +139,7 @@ const CustomRequestEditor = ({
description="启用此模式后,将使用您自定义的请求体发送API请求,模型配置面板的参数设置将被忽略。"
icon={<AlertTriangle size={16} />}
className="!rounded-lg"
closable={false}
closeIcon={null}
/>
{/* JSON编辑器 */}
......
......@@ -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} />
......
......@@ -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">
......
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
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;
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import { Card, Typography, Divider } from '@douyinfe/semi-ui';
import { Wallet, Coins, BarChart2 } from 'lucide-react';
const { Text } = Typography;
const RightStatsCard = ({ t, userState, renderQuota }) => {
return (
<Card size="small" className="!rounded-xl shadow-sm" bodyStyle={{ padding: '8px 12px' }}>
<div className="flex items-center gap-3 lg:gap-4 w-full">
<div className="flex items-center justify-end gap-2 flex-1">
<Wallet size={16} className="text-slate-600 dark:text-slate-300" />
<div className="text-right">
<Text size="small" type="tertiary">{t('当前余额')}</Text>
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{renderQuota(userState?.user?.quota)}</div>
</div>
</div>
<Divider layout="vertical" className="hidden md:block" />
<div className="flex items-center justify-end gap-2 flex-1">
<Coins size={16} className="text-slate-600 dark:text-slate-300" />
<div className="text-right">
<Text size="small" type="tertiary">{t('历史消耗')}</Text>
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{renderQuota(userState?.user?.used_quota)}</div>
</div>
</div>
<Divider layout="vertical" className="hidden md:block" />
<div className="flex items-center justify-end gap-2 flex-1">
<BarChart2 size={16} className="text-slate-600 dark:text-slate-300" />
<div className="text-right">
<Text size="small" type="tertiary">{t('请求次数')}</Text>
<div className="text-xs sm:text-sm font-semibold text-gray-800 dark:text-gray-100">{userState?.user?.request_count || 0}</div>
</div>
</div>
</div>
</Card>
);
};
export default RightStatsCard;
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
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;
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
import {
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;
......@@ -21,8 +21,8 @@ For commercial licensing, please contact support@quantumnous.com
export const CHART_CONFIG = { mode: 'desktop-browser' };
export const CARD_PROPS = {
shadows: 'always',
bordered: false,
shadows: '',
bordered: true,
headerLine: true
};
......
......@@ -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",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment