增加支付宝支付相关页面和程序

This commit is contained in:
2026-06-10 11:33:23 +08:00
parent d5a7ba8f14
commit 913dfee9e0
9 changed files with 444 additions and 96 deletions
+121 -51
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography } from 'antd';
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { Layout, Avatar, Dropdown, Space, Modal, Form, Input, message, Tooltip, Tag, Button, Typography, Radio } from 'antd';
import { QRCodeSVG } from 'qrcode.react';
import {
PlayCircleOutlined,
@@ -21,12 +21,13 @@ import {
FireFilled,
CrownFilled,
BankFilled,
QrcodeOutlined,
CloseOutlined,
WechatOutlined,
AlipayCircleOutlined,
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getRechargePackages, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
import { getMenuConfigs, getRechargePackages, createRechargeOrder, getPaymentOrders, getNotifications, markNotificationRead, getSiteInfo } from '../../api';
import NotificationPopup from '../NotificationPopup';
interface MenuConfig {
@@ -88,7 +89,10 @@ const AppLayout: React.FC = () => {
const [siteName, setSiteName] = useState('VideoGen.AI');
const [siteLogo, setSiteLogo] = useState('');
const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false);
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string } | null>(null);
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string; method: string } | null>(null);
const [paymentMethod, setPaymentMethod] = useState<string>('alipay');
const [paying, setPaying] = useState(false);
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// 监听预览弹窗状态,关闭浮动按钮
useEffect(() => {
@@ -173,6 +177,43 @@ const AppLayout: React.FC = () => {
setRechargeModalOpen(true);
};
const stopPolling = useCallback(() => {
if (pollingTimerRef.current) {
clearInterval(pollingTimerRef.current);
pollingTimerRef.current = null;
}
}, []);
const startPolling = useCallback((orderNo: string) => {
stopPolling();
let attempts = 0;
const maxAttempts = 120; // 2 minutes at 1s interval
const timer = setInterval(async () => {
attempts++;
if (attempts > maxAttempts) {
clearInterval(timer);
pollingTimerRef.current = null;
return;
}
try {
const orders = await getPaymentOrders();
const order = orders.find((o: any) => o.order_no === orderNo);
if (order && order.status === 'paid') {
clearInterval(timer);
pollingTimerRef.current = null;
message.success('支付成功!积分已到账');
useAuthStore.getState().refreshUser();
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
setSelectedPlan(null);
}
} catch {
// ignore polling errors
}
}, 1000);
pollingTimerRef.current = timer;
}, [stopPolling]);
return (
<Layout style={{ minHeight: '100vh' }}>
{/* Desktop Sidebar */}
@@ -516,28 +557,64 @@ const AppLayout: React.FC = () => {
);
})}
</div>
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'flex-end' }}>
{/* Payment method selection */}
<div style={{ marginTop: 20, marginBottom: 8 }}>
<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 }}>
<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>
<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={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Button size="large" onClick={() => { setRechargeModalOpen(false); setSelectedPlan(null); }} style={{ borderRadius: 10, marginRight: 12 }}></Button>
<Button type="primary" size="large" disabled={!selectedPlan}
onClick={() => {
<Button type="primary" size="large" disabled={!selectedPlan} loading={paying}
onClick={async () => {
const plan = rechargeOptions.find((opt: any) => opt.id === selectedPlan);
if (plan) {
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
// 生成随机支付内容(模拟微信支付订单号)
const orderId = `WX${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
const paymentContent = JSON.stringify({
orderId,
amount: plan.price,
credits: totalCredits,
timestamp: Date.now()
});
setCurrentPaymentInfo({
price: plan.price,
credits: totalCredits,
qrCode: paymentContent
});
setRechargeModalOpen(false);
setQrCodeModalOpen(true);
if (!plan) return;
const totalCredits = (plan.credits || 0) + (plan.bonus_credits || plan.bonusCredits || 0);
try {
setPaying(true);
const order = await createRechargeOrder(plan.id, paymentMethod);
if (paymentMethod === 'alipay' && order.qr_url) {
// Alipay: show the real QR code URL from the backend
setCurrentPaymentInfo({
price: plan.price,
credits: totalCredits,
qrCode: order.qr_url,
method: 'alipay',
});
setRechargeModalOpen(false);
setQrCodeModalOpen(true);
// Start polling for payment status
startPolling(order.order_no);
} else {
// WeChat or mock mode (mock auto-completes, no QR needed)
message.success('充值成功!积分已到账');
useAuthStore.getState().refreshUser();
setRechargeModalOpen(false);
setSelectedPlan(null);
}
} catch (err: any) {
message.error(err?.message || '创建订单失败,请重试');
} finally {
setPaying(false);
}
}}
style={{
@@ -554,7 +631,7 @@ const AppLayout: React.FC = () => {
{/* QR Code Payment Modal */}
<Modal
open={qrCodeModalOpen}
onCancel={() => { setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }}
onCancel={() => { stopPolling(); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }}
footer={null}
width={400}
closable={false}
@@ -567,15 +644,25 @@ const AppLayout: React.FC = () => {
<div style={{ textAlign: 'center', marginBottom: 24 }}>
<div style={{
width: 48, height: 48,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
background: currentPaymentInfo?.method === 'alipay'
? 'linear-gradient(135deg, #1677ff, #0958d9)'
: 'linear-gradient(135deg, #07c160, #06ae56)',
borderRadius: 16,
display: 'flex', alignItems: 'center', justifyContent: 'center',
margin: '0 auto 12px',
}}>
<QrcodeOutlined style={{ fontSize: 24, color: '#fff' }} />
{currentPaymentInfo?.method === 'alipay'
? <AlipayCircleOutlined style={{ fontSize: 24, color: '#fff' }} />
: <WechatOutlined style={{ fontSize: 24, color: '#fff' }} />}
</div>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>使</Typography.Text>
<Typography.Title level={4} style={{ margin: 0 }}>
{currentPaymentInfo?.method === 'alipay' ? '支付宝支付' : '微信支付'}
</Typography.Title>
<Typography.Text style={{ color: '#94a3b8', fontSize: 13 }}>
{currentPaymentInfo?.method === 'alipay'
? '请使用支付宝扫描二维码完成支付'
: '请使用微信扫描二维码完成支付'}
</Typography.Text>
</div>
{/* QR Code */}
@@ -640,32 +727,15 @@ const AppLayout: React.FC = () => {
</div>
{/* Footer Buttons */}
<div style={{ marginTop: 20, display: 'flex', gap: 12 }}>
<div style={{ marginTop: 20 }}>
<Button
size="large"
onClick={() => { setQrCodeModalOpen(false); setCurrentPaymentInfo(null); }}
style={{ flex: 1, borderRadius: 10 }}
block
onClick={() => { stopPolling(); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setSelectedPlan(null); }}
style={{ borderRadius: 10 }}
>
</Button>
<Button
type="primary"
size="large"
onClick={() => {
message.success('支付成功!积分已到账');
setQrCodeModalOpen(false);
setCurrentPaymentInfo(null);
setSelectedPlan(null);
}}
style={{
flex: 1,
borderRadius: 10,
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
border: 'none',
}}
>
</Button>
</div>
</div>
</Modal>