积分充值修改
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user