会员积分改版V1

This commit is contained in:
2026-08-11 09:24:18 +08:00
parent fe24e51b97
commit b9fd07f293
111 changed files with 9355 additions and 3900 deletions
+111 -403
View File
@@ -65,7 +65,8 @@ import {
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getRechargePackages, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api';
import { getMenuConfigs, getCreditProductCatalog, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api';
import type { CreditProduct, CreditProductCatalog } from '../../types';
import NotificationPopup from '../NotificationPopup';
import './AppLayout.css';
import bg1 from '../../assets/bg1.png';
@@ -319,79 +320,33 @@ const MEMBERSHIP_GRADIENTS = [
{ 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();
const { user, logout, refreshUser, setOptimizeHoldCredits } = useAuthStore();
const { user, logout, refreshUser } = useAuthStore();
const [pwdModalOpen, setPwdModalOpen] = useState(false);
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
const [contactModalOpen, setContactModalOpen] = useState(false);
const [pwdForm] = Form.useForm();
const [usernameForm] = Form.useForm();
const [selectedPlan, setSelectedPlan] = useState<number | null>(null);
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
const [menuItems, setMenuItems] = useState<MenuConfig[]>([]);
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
const [rechargeOptions, setRechargeOptions] = useState<CreditProduct[]>([]);
const [subscriptionProducts, setSubscriptionProducts] = useState<CreditProduct[]>([]);
const [currentSubscription, setCurrentSubscription] = useState<CreditProductCatalog['currentSubscription']>(null);
const loadCreditCatalog = useCallback(async () => {
try {
const data = await getCreditProductCatalog();
setSubscriptionProducts((data.subscriptionProducts || []).filter((product) => product.isActive));
setRechargeOptions((data.creditAddons || []).filter((product) => product.isActive));
setCurrentSubscription(data.currentSubscription || null);
} catch {
setSubscriptionProducts([]);
setRechargeOptions([]);
setCurrentSubscription(null);
}
}, []);
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);
@@ -483,10 +438,6 @@ const AppLayout: React.FC = () => {
setOperationManualUrl(info.operationManual);
}
if (info.optimizeHoldCredits !== undefined) {
setOptimizeHoldCredits(info.optimizeHoldCredits);
} else {
}
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
}).catch((err) => {
@@ -625,15 +576,13 @@ const AppLayout: React.FC = () => {
const items = data.filter((m: any) => m.is_active !== false && m.isActive !== false);
setMenuItems(items);
}).catch(() => { });
getRechargePackages().then(data => {
setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false));
}).catch(() => { });
loadCreditCatalog();
getPaymentMethods().then(data => {
setEnabledMethods(data);
if (data.alipay) setPaymentMethod('alipay');
else if (data.wechat) setPaymentMethod('wechat');
}).catch(() => { });
}, []);
}, [loadCreditCatalog]);
useEffect(() => {
if (user) {
@@ -817,6 +766,7 @@ const AppLayout: React.FC = () => {
localStorage.removeItem(PENDING_ORDER_KEY);
message.success('支付成功!积分已到账');
useAuthStore.getState().refreshUser();
loadCreditCatalog();
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
setSelectedPlan(null);
@@ -849,7 +799,7 @@ const AppLayout: React.FC = () => {
});
}, 1000);
countdownTimerRef.current = countdownTimer;
}, [stopPolling]);
}, [loadCreditCatalog, stopPolling]);
const renderMenuItem = (item: MenuConfig, depth: number = 0) => {
const isActive = item.path === selectedKey;
@@ -1077,22 +1027,22 @@ const AppLayout: React.FC = () => {
<style>{`
@keyframes arrowFlash {
0%, 100% {
transform: translateY(0) scale(1);
opacity: 0.6;
0%, 100% {
transform: translateY(0) scale(1);
opacity: 0.6;
filter: drop-shadow(0 0 4px rgba(99, 102, 241, 0.4));
}
50% {
transform: translateY(4px) scale(1.1);
opacity: 1;
50% {
transform: translateY(4px) scale(1.1);
opacity: 1;
filter: drop-shadow(0 0 12px rgba(99, 102, 241, 0.7));
}
}
@keyframes creditGlow {
0%, 100% {
0%, 100% {
text-shadow: 0 0 0 transparent;
}
50% {
50% {
text-shadow: 0 0 15px rgba(99, 102, 241, 0.6), 0 0 30px rgba(99, 102, 241, 0.3);
}
}
@@ -1101,21 +1051,21 @@ const AppLayout: React.FC = () => {
100% { background-position: -200% center; }
}
@keyframes cardBreath {
0%, 100% {
0%, 100% {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255,255,255,0.8);
border-color: rgba(99, 102, 241, 0.15);
}
50% {
50% {
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1), inset 0 1px 0 rgba(255,255,255,0.8);
border-color: rgba(99, 102, 241, 0.25);
}
}
@keyframes avatarPulse {
0%, 100% {
0%, 100% {
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
transform: scale(1);
}
50% {
50% {
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
transform: scale(1.02);
}
@@ -1432,328 +1382,84 @@ const AppLayout: React.FC = () => {
title={null}
placement="bottom"
open={rechargeModalOpen}
onClose={() => { setRechargeModalOpen(false); setSelectedPlan(null); setSelectedTierIndex(null); }}
height="95vh"
onClose={() => { setRechargeModalOpen(false); setSelectedPlan(null); }}
height="92vh"
className="recharge-drawer"
styles={{
header: { display: 'none' },
body: { padding: '20px 24px 0', overflowY: 'auto', position: 'relative' },
}}
styles={{ header: { display: 'none' }, body: { padding: '24px', overflowY: 'auto' } }}
footer={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0', borderTop: '1px solid #f0f0f0' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space><WalletOutlined style={{ color: '#6366f1' }} /><Typography.Text></Typography.Text><Typography.Text strong style={{ color: '#6366f1', fontSize: 18 }}>{user?.credits ?? 0}</Typography.Text></Space>
<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}
<Button onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }}></Button>
<Button type="primary" disabled={!selectedPlan || (!enabledMethods.alipay && !enabledMethods.wechat)} 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);
const product = subscriptionProducts.find((item) => item.id === selectedPlan);
if (!product) return;
try {
setPaying(true);
const order = await createRechargeOrder(product.id, paymentMethod);
const qrCode = order.qrUrl || order.codeUrl || order.qr_code || order.code_url;
const paymentInfo = { price: Number(order.amount ?? product.currentPrice ?? product.price ?? 0), credits: Number(product.monthlyGrantCredits || 0), qrCode, method: order.paymentMethod };
if ((order.paymentMethod === 'alipay' || order.paymentMethod === 'wechat') && qrCode) {
setCurrentPaymentInfo(paymentInfo); setRechargeModalOpen(false); setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo;
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({ orderNo: order.orderNo, ...paymentInfo, createdAt: order.createdAt || new Date().toISOString(), timeoutSeconds: 180 }));
startPolling(order.orderNo);
} else {
message.success('订阅购买成功,首期积分已到账');
await useAuthStore.getState().refreshUser();
await loadCreditCatalog();
setRechargeModalOpen(false); setSelectedPlan(null);
}
}
}}
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>
} catch (err: any) { message.error(err?.message || '创建订阅订单失败'); }
finally { setPaying(false); }
}}></Button>
</Space>
</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 style={{ position: 'relative' }}>
<CloseOutlined style={{ position: 'absolute', right: 0, top: 0, cursor: 'pointer', color: '#94a3b8' }} onClick={() => setRechargeModalOpen(false)} />
<div style={{ textAlign: 'center', marginBottom: 20 }}>
<Typography.Title level={3} style={{ marginBottom: 8 }}></Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
</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)';
}}
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>
{(!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>
{currentSubscription && (
<div style={{ marginBottom: 20, padding: 16, borderRadius: 12, background: '#f8fafc', border: '1px solid #e2e8f0' }}>
<Space direction="vertical" size={4}>
<Typography.Text strong>{currentSubscription.tierCode || '订阅套餐'}{currentSubscription.billingCycle === 'monthly' ? '月' : currentSubscription.billingCycle === 'quarterly' ? '季' : '年'}</Typography.Text>
<Typography.Text type="secondary"> {currentSubscription.grantedCount}/{currentSubscription.grantCount} {Number(currentSubscription.monthlyGrantCredits || 0).toLocaleString()} </Typography.Text>
<Typography.Text type="secondary">{new Date(currentSubscription.expiresAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false })}</Typography.Text>
</Space>
</div>
)}
<div style={{
padding: '12px 16px',
background: 'rgba(99, 102, 241, 0.06)',
borderRadius: 10,
marginBottom: 16,
display: 'flex',
alignItems: 'center',
gap: 8,
}}>
<InfoOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<Typography.Text style={{ color: '#64748b', fontSize: 13 }}>
/
<Typography.Text
style={{
color: '#ff0000ff',
cursor: 'pointer',
textDecoration: 'underline',
}}
onClick={() => { setRechargeModalOpen(false); setContactModalOpen(true); }}
></Typography.Text>
</Typography.Text>
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 20 }}>
<Radio.Group value={selectedPeriod} onChange={(e) => { setSelectedPeriod(e.target.value); setSelectedPlan(null); }} buttonStyle="solid">
<Radio.Button value="monthly"></Radio.Button><Radio.Button value="quarterly"></Radio.Button><Radio.Button value="yearly"></Radio.Button>
</Radio.Group>
</div>
{subscriptionProducts.filter((item) => item.billingCycle === selectedPeriod).length === 0 ? (
<div style={{ padding: 48, textAlign: 'center', color: '#94a3b8' }}></div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: 16 }}>
{subscriptionProducts.filter((item) => item.billingCycle === selectedPeriod).map((product, index) => {
const selected = selectedPlan === product.id;
const gradient = MEMBERSHIP_GRADIENTS[index % MEMBERSHIP_GRADIENTS.length];
return <div key={product.id} onClick={() => product.canPurchase !== false && setSelectedPlan(product.id)} style={{ border: selected ? '2px solid #6366f1' : '1px solid #e5e7eb', borderRadius: 16, padding: 20, cursor: product.canPurchase === false ? 'not-allowed' : 'pointer', opacity: product.canPurchase === false ? .6 : 1, background: '#fff' }}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Space><div style={{ width: 36, height: 36, borderRadius: 10, background: gradient.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>{gradient.icon}</div><div><Typography.Text strong style={{ fontSize: 16 }}>{product.name}</Typography.Text><div><Typography.Text type="secondary">{product.description || product.tierCode}</Typography.Text></div></div></Space>
<div><span style={{ fontSize: 28, fontWeight: 800, color: '#6366f1' }}>¥{product.currentPrice}</span><Tag style={{ marginLeft: 8 }}>{product.priceType === 'first_purchase' ? '首充价' : product.priceType === 'activity' ? '活动价' : product.canUpgrade ? '升级价' : '原价'}</Tag></div>
{product.canUpgrade && Number(product.deductionAmount || 0) > 0 && <Typography.Text type="secondary"> ¥{product.targetPrice} ¥{product.deductionAmount}</Typography.Text>}
<Tag color="purple"> {Number(product.monthlyGrantCredits || 0).toLocaleString()} {product.grantCount} </Tag>
{(product.features || []).map((feature) => <div key={feature}><CheckCircleFilled style={{ color: '#6366f1', marginRight: 6 }} />{feature}</div>)}
{product.unavailableReason && <Typography.Text type="danger">{product.unavailableReason}</Typography.Text>}
</Space>
</div>;
})}
</div>
)}
<div style={{ marginTop: 24 }}><Button type="link" icon={<WalletOutlined />} onClick={() => { setSelectedPlan(null); setRechargeModalOpen(false); setCreditsModalOpen(true); }}></Button></div>
{(!enabledMethods.alipay && !enabledMethods.wechat) && <div style={{ marginTop: 16, padding: 12, background: '#fef2f2', color: '#dc2626', borderRadius: 8 }}></div>}
<div style={{ marginTop: 16 }}><Radio.Group value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}>{enabledMethods.alipay && <Radio.Button value="alipay"><AlipayCircleOutlined /> </Radio.Button>}{enabledMethods.wechat && <Radio.Button value="wechat"><WechatOutlined /> </Radio.Button>}</Radio.Group></div>
</div>
</Drawer>
@@ -1770,14 +1476,14 @@ const AppLayout: React.FC = () => {
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);
const totalCredits = Number(plan.grantCredits || 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,
price: Number(order.amount ?? plan.currentPrice ?? plan.price ?? 0),
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
@@ -1789,7 +1495,7 @@ const AppLayout: React.FC = () => {
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo,
price: plan.price,
price: Number(order.amount ?? plan.currentPrice ?? plan.price ?? 0),
credits: totalCredits,
qrCode: qrCode,
method: order.paymentMethod,
@@ -1800,7 +1506,8 @@ const AppLayout: React.FC = () => {
startPolling(order.orderNo);
} else {
message.success('充值成功!积分已到账');
useAuthStore.getState().refreshUser();
await useAuthStore.getState().refreshUser();
await loadCreditCatalog();
setCreditsModalOpen(false);
setSelectedPlan(null);
}
@@ -1863,9 +1570,10 @@ const AppLayout: React.FC = () => {
)}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{rechargeOptions.length === 0 && <div style={{ width: '100%', padding: 32, textAlign: 'center', color: '#94a3b8' }}></div>}
{rechargeOptions.map((opt, idx) => {
const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0);
const totalCredits = Number(opt.grantCredits || 0);
return (
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 180, borderRadius: 16, padding: '18px 14px',
@@ -1890,7 +1598,7 @@ const AppLayout: React.FC = () => {
<div style={{
fontSize: 20, fontWeight: 800, marginTop: 2,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div>
}}>¥{opt.currentPrice || opt.price}</div>
</div>
</div>
</div>