pay is ok

This commit is contained in:
xiaoma
2026-02-08 22:57:05 +08:00
parent 554791d1ce
commit ff5f2cea98
12 changed files with 254 additions and 122 deletions

View File

@@ -14,6 +14,7 @@
"axios": "^1.13.4",
"framer-motion": "^12.29.2",
"jszip": "^3.10.1",
"qrcode.react": "^4.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",
@@ -4244,6 +4245,15 @@
"node": ">=6"
}
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz",

View File

@@ -16,6 +16,7 @@
"axios": "^1.13.4",
"framer-motion": "^12.29.2",
"jszip": "^3.10.1",
"qrcode.react": "^4.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.0",

View File

@@ -10,7 +10,9 @@ const api = axios.create({
export const getConfigs = () => api.get('/configs/');
export const createOrder = (data) => api.post('/orders/', data);
export const nativePay = (data) => api.post('/pay/', data);
export const getOrder = (id) => api.get(`/orders/${id}/`);
export const queryOrderStatus = (id) => api.get(`/orders/${id}/query_status/`);
export const initiatePayment = (orderId) => api.post(`/orders/${orderId}/initiate_payment/`);
export const confirmPayment = (orderId) => api.post(`/orders/${orderId}/confirm_payment/`);

View File

@@ -1,26 +1,65 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button, message, Result, Spin, QRCode } from 'antd';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Button, message, Result, Spin } from 'antd';
import { WechatOutlined, AlipayCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
import { getOrder, initiatePayment, confirmPayment } from '../api';
import { QRCodeSVG } from 'qrcode.react';
import { getOrder, initiatePayment, confirmPayment, nativePay, queryOrderStatus } from '../api';
import './Payment.css';
const Payment = () => {
const { orderId } = useParams();
const { orderId: initialOrderId } = useParams();
const navigate = useNavigate();
const [order, setOrder] = useState(null);
const [loading, setLoading] = useState(true);
const [paying, setPaying] = useState(false);
const location = useLocation();
const [currentOrderId, setCurrentOrderId] = useState(location.state?.order_id || initialOrderId);
const [order, setOrder] = useState(location.state?.orderInfo || null);
const [codeUrl, setCodeUrl] = useState(location.state?.codeUrl || null);
const [loading, setLoading] = useState(!location.state?.orderInfo && !location.state?.codeUrl);
const [paying, setPaying] = useState(!!location.state?.codeUrl);
const [paySuccess, setPaySuccess] = useState(false);
const [paymentMethod, setPaymentMethod] = useState('wechat');
useEffect(() => {
fetchOrder();
}, [orderId]);
if (codeUrl && !paying) {
setPaying(true);
}
}, [codeUrl]);
useEffect(() => {
console.log('Payment page state:', { currentOrderId, order, codeUrl, paying });
if (!order && !codeUrl) {
fetchOrder();
}
}, [currentOrderId]);
useEffect(() => {
if (paying && !codeUrl && order) {
handlePay();
}
}, [paying, codeUrl, order]);
// 轮询订单状态
useEffect(() => {
let timer;
if (paying && !paySuccess) {
timer = setInterval(async () => {
try {
const response = await queryOrderStatus(currentOrderId);
if (response.data.status === 'paid') {
setPaySuccess(true);
setPaying(false);
clearInterval(timer);
}
} catch (error) {
console.error('Check payment status failed:', error);
}
}, 3000);
}
return () => clearInterval(timer);
}, [paying, paySuccess, currentOrderId]);
const fetchOrder = async () => {
try {
const response = await getOrder(orderId);
const response = await getOrder(currentOrderId);
setOrder(response.data);
} catch (error) {
console.error('Failed to fetch order:', error);
@@ -38,69 +77,41 @@ const Payment = () => {
return;
}
if (codeUrl) {
setPaying(true);
return;
}
if (!order) {
message.error('正在加载订单信息,请稍后...');
return;
}
setPaying(true);
try {
// 1. 获取微信支付参数
const response = await initiatePayment(orderId);
const payData = response.data;
if (typeof WeixinJSBridge === 'undefined') {
message.warning('请在微信内置浏览器中打开以完成支付');
setPaying(false);
return;
}
// 2. 调用微信支付
const onBridgeReady = () => {
window.WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId": payData.appId, // 公众号名称,由商户传入
"timeStamp": payData.timeStamp, // 时间戳自1970年以来的秒数
"nonceStr": payData.nonceStr, // 随机串
"package": payData.package,
"signType": payData.signType, // 微信签名方式:
"paySign": payData.paySign // 微信签名
},
function(res) {
setPaying(false);
if (res.err_msg == "get_brand_wcpay_request:ok") {
message.success('支付成功!');
setPaySuccess(true);
// 这里可以再次调用后端查询接口确认状态,但通常 JSAPI 回调 ok 即可认为成功
// 为了保险,可以去轮询一下后端状态,或者直接展示成功页
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
message.info('支付已取消');
} else {
message.error('支付失败,请重试');
console.error('WeChat Pay Error:', res);
}
}
);
const orderData = {
goodid: order.config || order.goodid,
quantity: order.quantity,
customer_name: order.customer_name,
phone_number: order.phone_number,
shipping_address: order.shipping_address,
ref_code: order.ref_code
};
if (typeof window.WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
const response = await nativePay(orderData);
setCodeUrl(response.data.code_url);
if (response.data.order_id) {
setCurrentOrderId(response.data.order_id);
}
message.success('支付二维码已生成');
} catch (error) {
console.error(error);
if (error.response && error.response.data && error.response.data.error) {
message.error(error.response.data.error);
} else {
message.error('支付发起失败,请稍后重试');
}
message.error('生成支付二维码失败,请重试');
setPaying(false);
}
};
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" tip="正在加载订单信息..." /></div>;
if (paySuccess) {
return (
@@ -109,7 +120,7 @@ const Payment = () => {
status="success"
icon={<CheckCircleOutlined style={{ color: '#00b96b' }} />}
title={<span style={{ color: '#fff' }}>支付成功</span>}
subTitle={<span style={{ color: '#888' }}>订单 {orderId} 已完成支付我们将尽快为您发货</span>}
subTitle={<span style={{ color: '#888' }}>订单 {currentOrderId} 已完成支付我们将尽快为您发货</span>}
extra={[
<Button type="primary" key="home" onClick={() => navigate('/')}>
返回首页
@@ -135,7 +146,7 @@ const Payment = () => {
</>
) : (
<div className="payment-info">
<p>订单 ID: {orderId}</p>
<p>订单 ID: {currentOrderId}</p>
<p>无法加载详情但您可以尝试支付</p>
</div>
)}
@@ -159,9 +170,20 @@ const Payment = () => {
</div>
{paying && (
<div style={{ margin: '20px 0', padding: 20, background: '#fff', borderRadius: 8, display: 'inline-block' }}>
<QRCode value={`mock-payment-${orderId}`} bordered={false} />
<p style={{ color: '#000', marginTop: 10 }}>请扫码支付 (模拟)</p>
<div style={{ margin: '20px 0', padding: 20, background: '#fff', borderRadius: 8, display: 'inline-block', minWidth: 240, minHeight: 280 }}>
{codeUrl ? (
<>
<div style={{ background: '#fff', padding: '10px', borderRadius: '4px', display: 'inline-block' }}>
<QRCodeSVG value={codeUrl} size={200} />
</div>
<p style={{ color: '#000', marginTop: 15, fontWeight: 'bold', fontSize: 18 }}>请使用微信扫码支付</p>
<p style={{ color: '#666', fontSize: 14 }}>支付完成后将自动跳转</p>
</>
) : (
<div style={{ padding: '40px 0' }}>
<Spin tip="正在生成支付二维码..." />
</div>
)}
</div>
)}

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions } from 'antd';
import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions, Radio } from 'antd';
import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
import { getConfigs, createOrder } from '../api';
import { getConfigs, createOrder, nativePay } from '../api';
import ModelViewer from '../components/ModelViewer';
import './ProductDetail.css';
@@ -16,7 +16,7 @@ const ProductDetail = () => {
const [submitting, setSubmitting] = useState(false);
const [form] = Form.useForm();
const refCode = searchParams.get('ref');
const refCode = searchParams.get('ref') || 'flw666';
useEffect(() => {
fetchProduct();
@@ -43,17 +43,29 @@ const ProductDetail = () => {
const handleBuy = async (values) => {
setSubmitting(true);
try {
const isPickup = values.delivery_method === 'pickup';
const orderData = {
config: product.id,
goodid: product.id,
quantity: values.quantity,
customer_name: values.customer_name,
phone_number: values.phone_number,
shipping_address: values.shipping_address,
shipping_address: isPickup ? '线下自提' : values.shipping_address,
ref_code: refCode
};
const response = await createOrder(orderData);
message.success('订单创建成功');
navigate(`/payment/${response.data.id}`);
const response = await nativePay(orderData);
message.success('订单创建,请完成支付');
navigate(`/payment/${response.data.order_id}`, {
state: {
codeUrl: response.data.code_url,
order_id: response.data.order_id,
orderInfo: {
...orderData,
id: response.data.order_id,
config_name: product.name,
total_price: product.price * values.quantity
}
}
});
} catch (error) {
console.error(error);
message.error('创建订单失败,请检查填写信息');
@@ -217,8 +229,14 @@ const ProductDetail = () => {
form={form}
layout="vertical"
onFinish={handleBuy}
initialValues={{ quantity: 1 }}
initialValues={{ quantity: 1, delivery_method: 'shipping' }}
>
<Form.Item label="配送方式" name="delivery_method">
<Radio.Group buttonStyle="solid">
<Radio.Button value="shipping">快递配送</Radio.Button>
<Radio.Button value="pickup">线下自提</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="购买数量" name="quantity" rules={[{ required: true }]}>
<InputNumber min={1} max={100} style={{ width: '100%' }} />
</Form.Item>
@@ -228,8 +246,22 @@ const ProductDetail = () => {
<Form.Item label="联系电话" name="phone_number" rules={[{ required: true, message: '请输入电话' }]}>
<Input placeholder="13800000000" />
</Form.Item>
<Form.Item label="收货地址" name="shipping_address" rules={[{ required: true, message: '请输入地址' }]}>
<Input.TextArea rows={3} placeholder="北京市..." />
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) => prevValues.delivery_method !== currentValues.delivery_method}
>
{({ getFieldValue }) =>
getFieldValue('delivery_method') === 'shipping' ? (
<Form.Item label="收货地址" name="shipping_address" rules={[{ required: true, message: '请输入地址' }]}>
<Input.TextArea rows={3} placeholder="北京市..." />
</Form.Item>
) : (
<div style={{ marginBottom: 24, padding: '12px', background: '#f5f5f5', borderRadius: '4px', border: '1px solid #d9d9d9' }}>
<p style={{ margin: 0, color: '#666' }}>自提地址昆明市云纺国际商厦B座1406</p>
<p style={{ margin: 0, fontSize: '12px', color: '#999' }}>请在工作日 9:00 - 18:00 期间前往提货</p>
</div>
)
}
</Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 20 }}>

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd';
import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons';
import { getServiceDetail, createServiceOrder } from '../api';
@@ -10,12 +10,15 @@ const { Title, Paragraph } = Typography;
const ServiceDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [service, setService] = useState(null);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [form] = Form.useForm();
const refCode = searchParams.get('ref') || 'flw666';
useEffect(() => {
const fetchDetail = async () => {
try {
@@ -39,7 +42,8 @@ const ServiceDetail = () => {
company_name: values.company_name,
phone_number: values.phone_number,
email: values.email,
requirements: values.requirements
requirements: values.requirements,
ref_code: refCode
};
await createServiceOrder(orderData);
message.success('需求已提交,我们的销售顾问将尽快与您联系!');