积分充值修改

This commit is contained in:
sjy
2026-08-04 17:33:52 +08:00
parent 97edb17062
commit fe24e51b97
6 changed files with 927 additions and 716 deletions
+451 -70
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer, Tabs } from 'antd';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio, Drawer, Tabs, Collapse } from 'antd';
import { QRCodeSVG } from 'qrcode.react';
import {
PlayCircleOutlined,
@@ -20,7 +20,9 @@ import {
StarFilled,
FireFilled,
CrownFilled,
CrownOutlined,
BankFilled,
CheckCircleFilled,
CloseOutlined,
WechatOutlined,
AlipayCircleOutlined,
@@ -310,6 +312,72 @@ const GRADIENTS = [
{ gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> },
];
const MEMBERSHIP_GRADIENTS = [
{ gradient: 'linear-gradient(135deg, #94a3b8, #64748b)', shadow: 'rgba(148,163,184,0.25)', icon: <StarFilled /> },
{ gradient: 'linear-gradient(135deg, #c9a96e, #a67c52)', shadow: 'rgba(201,169,110,0.25)', icon: <StarFilled /> },
{ gradient: 'linear-gradient(135deg, #718096, #4a5568)', shadow: 'rgba(113,128,150,0.25)', icon: <CrownFilled /> },
{ gradient: 'linear-gradient(135deg, #5a67d8, #434190)', shadow: 'rgba(90,103,216,0.25)', icon: <BankFilled /> },
];
const MEMBERSHIP_FEATURES = [
'每日赠送积分 当日清零',
'可充值更多积分',
'基础加速通道',
'作品去除水印',
'可使用网页版和APP上全部AI功能',
'高分辨率',
'批量创作 更多同时生成任务',
];
const MEMBERSHIP_PLANS = [
{
tier: '基础会员',
desc: '开启创作之旅',
monthlyPrice: 79,
monthlyCredits: 1000,
onlyMonthly: true,
features: MEMBERSHIP_FEATURES.map((f) => f.replace('基础加速通道', '基础加速通道')),
speedLabel: '基础加速通道',
},
{
tier: '标准会员',
desc: '畅享基础创作权益',
monthlyPrice: 154,
monthlyCredits: 2000,
quarterlyPrice: 1029,
quarterlyCredits: 5000,
yearlyPrice: 7308,
yearlyCredits: 10000,
features: MEMBERSHIP_FEATURES,
speedLabel: '标准加速通道',
},
{
tier: '高级会员',
desc: '解锁高级创作权益',
monthlyPrice: 221,
monthlyCredits: 3000,
quarterlyPrice: 1197,
quarterlyCredits: 6000,
yearlyPrice: 10710,
yearlyCredits: 15000,
features: MEMBERSHIP_FEATURES,
speedLabel: '高级加速通道',
},
{
tier: '超级会员',
desc: '释放无限创作生产力',
monthlyPrice: 280,
monthlyCredits: 4000,
quarterlyPrice: 1367,
quarterlyCredits: 7000,
yearlyPrice: 13440,
yearlyCredits: 20000,
features: MEMBERSHIP_FEATURES,
speedLabel: '高级加速通道',
hot: true,
},
];
const AppLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
@@ -322,6 +390,9 @@ const AppLayout: React.FC = () => {
const [selectedPlan, setSelectedPlan] = useState<number | null>(null);
const [menuItems, setMenuItems] = useState<MenuConfig[]>([]);
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
const [creditsModalOpen, setCreditsModalOpen] = useState(false);
const [selectedTierIndex, setSelectedTierIndex] = useState<number | null>(null);
const [selectedPeriod, setSelectedPeriod] = useState<'monthly' | 'quarterly' | 'yearly'>('monthly');
const [unreadCount, setUnreadCount] = useState(0);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [contactHovered, setContactHovered] = useState(false);
@@ -1357,72 +1428,274 @@ const AppLayout: React.FC = () => {
</Tabs>
</Modal>
<Modal title={<Space><GiftOutlined /></Space>} open={rechargeModalOpen}
onCancel={() => { setRechargeModalOpen(false); setSelectedPlan(null); }}
width={680}
className="recharge-modal"
<Drawer
title={null}
placement="bottom"
open={rechargeModalOpen}
onClose={() => { setRechargeModalOpen(false); setSelectedPlan(null); setSelectedTierIndex(null); }}
height="95vh"
className="recharge-drawer"
styles={{
header: { display: 'none' },
body: { padding: '20px 24px 0', overflowY: 'auto', position: 'relative' },
}}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '12px 0 0', borderTop: '1px solid #f0f0f0' }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button>
<Button type="primary" size="large" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} loading={paying}
onClick={async () => {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (!plan) return;
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
try {
setPaying(true);
const order = await createRechargeOrder(plan.id, paymentMethod);
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) {
const paymentInfo = {
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
};
setCurrentPaymentInfo(paymentInfo);
setRechargeModalOpen(false);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180,
}));
startPolling(order.orderNo);
} else {
message.success('充值成功!积分已到账');
useAuthStore.getState().refreshUser();
setRechargeModalOpen(false);
setSelectedPlan(null);
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0', borderTop: '1px solid #f0f0f0' }}>
<Space>
<WalletOutlined style={{ color: '#6366f1' }} />
<Typography.Text style={{ color: '#64748b' }}></Typography.Text>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 18 }}>{user?.credits ?? 0}</Typography.Text>
</Space>
<div style={{ display: 'flex', gap: 12 }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); setSelectedTierIndex(null); }} style={{ borderRadius: 10 }}></Button>
<Button
type="primary"
size="large"
disabled={!selectedPlan && selectedTierIndex === null}
loading={paying}
onClick={async () => {
if (selectedPlan) {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (!plan) return;
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
try {
setPaying(true);
const order = await createRechargeOrder(plan.id, paymentMethod);
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) {
const paymentInfo = {
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
};
setCurrentPaymentInfo(paymentInfo);
setRechargeModalOpen(false);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180,
}));
startPolling(order.orderNo);
} else {
message.success('充值成功!积分已到账');
useAuthStore.getState().refreshUser();
setRechargeModalOpen(false);
setSelectedPlan(null);
}
} catch (err: any) {
message.error(err?.message || '创建订单失败,请重试');
} finally {
setPaying(false);
}
}
} catch (err: any) {
message.error(err?.message || '创建订单失败,请重试');
} finally {
setPaying(false);
}
}}
style={{
borderRadius: 10, fontWeight: 600,
background: selectedPlan ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' : '#d1d5db',
border: 'none', boxShadow: selectedPlan ? '0 8px 24px rgba(99,102,241,0.3)' : 'none',
}}>
</Button>
</div>
</div>
}
>
<div>
<div style={{ position: 'absolute', top: 8, right: 16, zIndex: 10 }}>
<CloseOutlined
style={{ fontSize: 18, color: '#94a3b8', cursor: 'pointer', padding: 8, borderRadius: '50%', transition: 'color 0.2s' }}
onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); setSelectedTierIndex(null); }}
onMouseEnter={(e) => { (e.target as HTMLElement).style.color = '#6366f1'; }}
onMouseLeave={(e) => { (e.target as HTMLElement).style.color = '#94a3b8'; }}
/>
</div>
<div style={{ textAlign: 'center', marginBottom: 20, marginTop: 16 }}>
<Typography.Title level={4} style={{ margin: 0, fontSize: 20, fontWeight: 700, color: '#1a1a2e' }}>
</Typography.Title>
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 12, marginTop: 12 }}>
<div style={{
display: 'inline-flex', background: '#f5f5fa', borderRadius: 10, padding: 4,
gap: 4,
}}>
{[
{ key: 'yearly', label: '连续包年 8折' },
{ key: 'quarterly', label: '连续包季 8折' },
{ key: 'monthly', label: '连续包月' },
].map((p) => (
<div
key={p.key}
onClick={() => {
const period = p.key as 'monthly' | 'quarterly' | 'yearly';
setSelectedPeriod(period);
if (period !== 'monthly' && selectedTierIndex !== null && MEMBERSHIP_PLANS[selectedTierIndex]?.onlyMonthly) {
setSelectedTierIndex(null);
}
}}
style={{
padding: '6px 16px', borderRadius: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500,
background: selectedPeriod === p.key ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' : 'transparent',
color: selectedPeriod === p.key ? '#fff' : '#64748b',
transition: 'all 0.2s', whiteSpace: 'nowrap',
}}
>
{p.label}
</div>
))}
</div>
{/* <Tag
style={{
borderRadius: 8, fontSize: 12, padding: '4px 12px', cursor: 'pointer',
background: '#f0f0ff', color: '#6366f1', border: '1px solid #e0e0ff',
}}
>
API服务
</Tag> */}
</div>
</div>
<div style={{ position: 'absolute', top: 12, right: 48, zIndex: 10 }}>
<div
onClick={() => setCreditsModalOpen(true)}
style={{
display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer',
padding: '6px 14px', borderRadius: 20,
background: 'linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.08))',
border: '1px solid rgba(99,102,241,0.2)',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.15), rgba(139,92,246,0.15))';
e.currentTarget.style.borderColor = 'rgba(99,102,241,0.4)';
}}
style={{
borderRadius: 10, fontWeight: 600,
background: selectedPlan ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' : '#d1d5db',
border: 'none', boxShadow: selectedPlan ? '0 8px 24px rgba(99,102,241,0.3)' : 'none',
}}>
</Button>
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(99,102,241,0.08), rgba(139,92,246,0.08))';
e.currentTarget.style.borderColor = 'rgba(99,102,241,0.2)';
}}
>
<WalletOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<span style={{ fontSize: 13, fontWeight: 600, color: '#6366f1' }}></span>
{/* <Tag color="purple" style={{ marginLeft: 2, borderRadius: 6, fontSize: 10, margin: 0, padding: '0 6px', lineHeight: '18px' }}>8折</Tag> */}
</div>
</div>
<div style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
{MEMBERSHIP_PLANS
.map((plan, idx) => ({ plan, idx }))
.filter(({ plan }) => selectedPeriod === 'monthly' || !plan.onlyMonthly)
.map(({ plan, idx }) => {
const g = MEMBERSHIP_GRADIENTS[idx % MEMBERSHIP_GRADIENTS.length];
const price = selectedPeriod === 'monthly' ? plan.monthlyPrice
: selectedPeriod === 'quarterly' ? plan.quarterlyPrice : plan.yearlyPrice;
const credits = selectedPeriod === 'monthly' ? plan.monthlyCredits
: selectedPeriod === 'quarterly' ? plan.quarterlyCredits : plan.yearlyCredits;
const creditsPer10 = Math.round((credits / price) * 10);
const isSelected = selectedTierIndex === idx;
const isHot = plan.hot;
return (
<div
key={idx}
onClick={() => setSelectedTierIndex(idx)}
style={{
flex: 1,
borderRadius: 16,
padding: '20px 16px 16px',
background: isHot
? 'linear-gradient(180deg, #f0edff 0%, #fafbff 30%)'
: '#fafbff',
border: isSelected
? '2px solid #6366f1'
: isHot
? '2px solid #8b5cf6'
: '1px solid #f0f0f5',
cursor: 'pointer',
position: 'relative',
transition: 'all 0.2s',
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
}}
>
{isHot && (
<div style={{
position: 'absolute', top: -10, right: 12,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', color: '#fff',
fontSize: 11, padding: '3px 10px', borderRadius: 8, fontWeight: 600,
}}></div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<Typography.Text strong style={{ fontSize: 15, color: '#1a1a2e' }}>{plan.tier}</Typography.Text>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>{plan.desc}</Typography.Text>
</div>
<div style={{ marginTop: 8, marginBottom: 4 }}>
<span style={{
fontSize: 28, fontWeight: 800,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{price}</span>
<span style={{ fontSize: 12, color: '#94a3b8', marginLeft: 4 }}>/</span>
</div>
<Typography.Text style={{ fontSize: 11, color: '#94a3b8', marginBottom: 8 }}>
</Typography.Text>
<div style={{
display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap',
}}>
<div style={{
fontSize: 11, color: '#6366f1', background: 'rgba(99,102,241,0.08)',
padding: '3px 8px', borderRadius: 6, fontWeight: 600,
}}>
{credits.toLocaleString()}
</div>
<div style={{
fontSize: 11, color: '#94a3b8', background: '#f5f5fa',
padding: '3px 8px', borderRadius: 6,
}}>
¥10 = {creditsPer10}
</div>
</div>
<Button
block
size="small"
onClick={(e) => { e.stopPropagation(); setSelectedTierIndex(idx); }}
style={{
borderRadius: 10, marginBottom: 12, fontWeight: 600, height: 36,
background: isSelected
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
: isHot
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
: '#1a1a2e',
border: 'none', color: '#fff', fontSize: 13,
}}
>
{plan.tier}
</Button>
<div style={{
borderTop: '1px solid #f0f0f5', paddingTop: 10,
display: 'flex', flexDirection: 'column', gap: 6,
}}>
{plan.features.map((feature: string, fi: number) => (
<div key={fi} style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
<CheckCircleFilled style={{ fontSize: 12, color: '#6366f1', flexShrink: 0, marginTop: 2 }} />
<Typography.Text style={{ fontSize: 11, color: '#64748b', lineHeight: 1.5 }}>{feature}</Typography.Text>
</div>
))}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
<CheckCircleFilled style={{ fontSize: 12, color: '#6366f1', flexShrink: 0, marginTop: 2 }} />
<Typography.Text style={{ fontSize: 11, color: '#64748b', lineHeight: 1.5 }}>
</Typography.Text>
</div>
</div>
</div>
);
})}
</div>
}>
<div style={{ marginTop: 16 }}>
<Space style={{ marginBottom: 16 }}>
<WalletOutlined style={{ color: '#6366f1' }} />
<Typography.Text style={{ color: '#64748b', letterSpacing: 0 }}></Typography.Text>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 20, fontWeight: 600 }}>{user?.credits ?? 0}</Typography.Text>
</Space>
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
<div style={{ marginBottom: 16, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
@@ -1481,13 +1754,121 @@ const AppLayout: React.FC = () => {
></Typography.Text>
</Typography.Text>
</div>
</div>
</Drawer>
{/* 积分充值独立弹窗 */}
<Modal
title={<Space><WalletOutlined style={{ color: '#6366f1' }} /></Space>}
open={creditsModalOpen}
onCancel={() => { setCreditsModalOpen(false); setSelectedPlan(null); }}
width={560}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '12px 0 0', borderTop: '1px solid #f0f0f0' }}>
<Button size="large" onClick={() => { setCreditsModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button>
<Button type="primary" size="large" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} loading={paying}
onClick={async () => {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (!plan) return;
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
try {
setPaying(true);
const order = await createRechargeOrder(plan.id, paymentMethod);
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) {
const paymentInfo = {
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
};
setCurrentPaymentInfo(paymentInfo);
setCreditsModalOpen(false);
setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
price: plan.price,
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180,
}));
startPolling(order.orderNo);
} else {
message.success('充值成功!积分已到账');
useAuthStore.getState().refreshUser();
setCreditsModalOpen(false);
setSelectedPlan(null);
}
} catch (err: any) {
message.error(err?.message || '创建订单失败,请重试');
} finally {
setPaying(false);
}
}}
style={{
borderRadius: 10, fontWeight: 600,
background: selectedPlan ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' : '#d1d5db',
border: 'none', boxShadow: selectedPlan ? '0 8px 24px rgba(99,102,241,0.3)' : 'none',
}}>
</Button>
</div>
}
>
<div style={{ marginTop: 16 }}>
<Space style={{ marginBottom: 16 }}>
<WalletOutlined style={{ color: '#6366f1' }} />
<Typography.Text style={{ color: '#64748b', letterSpacing: 0 }}></Typography.Text>
<Typography.Text strong style={{ color: '#6366f1', fontSize: 20, fontWeight: 600 }}>{user?.credits ?? 0}</Typography.Text>
</Space>
{(!enabledMethods.alipay && !enabledMethods.wechat) ? (
<div style={{ marginBottom: 16, padding: 16, background: '#fef2f2', borderRadius: 12, border: '1px solid #fecaca' }}>
<Typography.Text style={{ color: '#dc2626', fontSize: 13 }}>
</Typography.Text>
</div>
) : (
<div style={{ marginBottom: 16 }}>
<Typography.Text style={{ color: '#64748b', fontSize: 13, marginBottom: 8, display: 'block' }}></Typography.Text>
<Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}
style={{ display: 'flex', gap: 12 }}>
{enabledMethods.alipay && (
<Radio.Button value="alipay" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'alipay' ? '#1677ff' : undefined,
color: paymentMethod === 'alipay' ? '#1677ff' : undefined,
}}>
<AlipayCircleOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
{enabledMethods.wechat && (
<Radio.Button value="wechat" style={{
flex: 1, textAlign: 'center', borderRadius: 10, height: 44, lineHeight: '42px',
borderColor: paymentMethod === 'wechat' ? '#07c160' : undefined,
color: paymentMethod === 'wechat' ? '#07c160' : undefined,
}}>
<WechatOutlined style={{ fontSize: 16, marginRight: 6 }} />
</Radio.Button>
)}
</Radio.Group>
</div>
)}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{rechargeOptions.map((opt, idx) => {
const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0);
return (
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px',
flex: '1 1 45%', minWidth: 180, borderRadius: 16, padding: '18px 14px',
background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff',
border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5',
cursor: 'pointer', position: 'relative', transition: 'all 0.2s',
@@ -1495,19 +1876,19 @@ const AppLayout: React.FC = () => {
{opt.description && (
<Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
width: 40, height: 40, borderRadius: 12, flexShrink: 0,
background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
fontSize: 16, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 14, fontWeight: 600, color: '#6366f1' }}>{totalCredits.toLocaleString()} </Typography.Text>
<Typography.Text strong style={{ fontSize: 14 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 13, fontWeight: 600, color: '#6366f1' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div>
<div style={{
fontSize: 22, fontWeight: 800, marginTop: 4,
fontSize: 20, fontWeight: 800, marginTop: 2,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div>
</div>