增加支付宝支付相关页面和程序
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user