hookehuyr

feat(支付): 添加微信支付组件并重构支付逻辑

将支付逻辑从CheckoutPage.vue中提取到独立的WechatPayment.vue组件中,以提高代码的可维护性和复用性。同时,更新了CheckoutPage.vue以使用新的支付组件,并简化了支付状态管理。
......@@ -53,5 +53,6 @@ declare module 'vue' {
VanTabs: typeof import('vant/es')['Tabs']
VanUploader: typeof import('vant/es')['Uploader']
VideoPlayer: typeof import('./components/ui/VideoPlayer.vue')['default']
WechatPayment: typeof import('./components/payment/WechatPayment.vue')['default']
}
}
......
<template>
<!-- 支付成功状态 -->
<div v-if="paymentStatus === 'success'" class="h-screen flex flex-col items-center justify-center px-4">
<FrostedGlass class="p-6 rounded-xl text-center">
<div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-2xl font-bold mb-2">支付成功!</h2>
<p class="text-gray-600 mb-2">您的订单已经成功提交</p>
<p class="text-gray-500 text-sm mb-6">订单号: {{ orderId }}</p>
<slot name="success-action">
<button
@click="$emit('success')"
class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
>
完成
</button>
</slot>
</FrostedGlass>
</div>
<!-- 支付处理中状态 -->
<div v-else-if="paymentStatus === 'processing'" class="h-screen flex flex-col items-center justify-center px-4">
<FrostedGlass class="p-6 rounded-xl text-center">
<div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-4">
<div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<h2 class="text-2xl font-bold mb-2">支付处理中</h2>
<p class="text-gray-600 mb-6">请稍候,正在处理您的支付...</p>
</FrostedGlass>
</div>
<!-- 支付失败状态 -->
<div v-else-if="paymentStatus === 'failed'" class="h-screen flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<FrostedGlass class="w-full max-w-sm sm:max-w-md lg:max-w-lg p-6 sm:p-8 rounded-2xl text-center">
<div class="w-20 h-20 sm:w-24 sm:h-24 bg-red-50 rounded-full flex items-center justify-center mx-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 sm:h-12 sm:w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 class="text-xl sm:text-2xl font-bold mb-2 sm:mb-3">支付失败</h2>
<p class="text-sm sm:text-base text-gray-600 mb-6 sm:mb-8 px-2 sm:px-4">{{ paymentError || '支付过程中出现错误,请重试' }}</p>
<div class="space-y-2 sm:space-y-3 max-w-xs mx-auto">
<button
@click="handlePayment"
class="w-full bg-gradient-to-r from-red-500 to-red-600 text-white py-3 sm:py-3.5 rounded-xl font-medium text-base sm:text-lg hover:from-red-600 hover:to-red-700 transition-colors"
>
重新支付
</button>
<slot name="failed-action">
<button
@click="$emit('failed')"
class="w-full bg-gray-100 text-gray-700 py-3 sm:py-3.5 rounded-xl font-medium text-base sm:text-lg hover:bg-gray-200 transition-colors"
>
返回
</button>
</slot>
</div>
</FrostedGlass>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import { wxPayAPI, wxPayCheckAPI } from "@/api/wx/pay"
// 定义组件的props
const props = defineProps({
orderId: {
type: String,
required: true
}
})
// 定义组件的事件
const emit = defineEmits(['success', 'failed', 'processing'])
// 支付状态管理
const paymentStatus = ref('pending') // pending, processing, success, failed
const paymentError = ref('')
const paymentRetryCount = ref(0)
const MAX_RETRY_ATTEMPTS = 3
const RETRY_DELAY = 2000 // 2秒重试间隔
// 初始化微信支付
const initWxPay = () => {
return new Promise((resolve) => {
if (typeof WeixinJSBridge !== "undefined") {
resolve(WeixinJSBridge)
} else {
document.addEventListener('WeixinJSBridgeReady', () => {
resolve(WeixinJSBridge)
}, false)
}
})
}
// 检查支付状态
const checkPaymentStatus = async (orderId) => {
try {
const payStatus = await wxPayCheckAPI({ order_id: orderId })
if (payStatus.code) {
paymentStatus.value = 'success'
return true
}
return false
} catch (error) {
console.error('支付状态查询失败:', error)
return false
}
}
// 处理支付结果
const handlePaymentResult = async (res) => {
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功,验证支付状态
let retryCount = 0
const verifyPayment = async () => {
if (await checkPaymentStatus(props.orderId)) {
emit('success')
return
}
if (retryCount < MAX_RETRY_ATTEMPTS) {
retryCount++
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
await verifyPayment()
} else {
paymentStatus.value = 'failed'
paymentError.value = '支付状态验证失败,请联系客服'
emit('failed', paymentError.value)
}
}
await verifyPayment()
} else if (res.err_msg === "get_brand_wcpay_request:cancel") {
paymentStatus.value = 'failed'
paymentError.value = '支付已取消'
emit('failed', paymentError.value)
} else {
paymentStatus.value = 'failed'
paymentError.value = '支付失败,请重试'
emit('failed', paymentError.value)
}
}
// 处理支付流程
const handlePayment = async () => {
try {
// 重置支付状态
paymentStatus.value = 'processing'
paymentError.value = ''
paymentRetryCount.value = 0
emit('processing')
// 初始化支付
const { code, data, msg } = await wxPayAPI({ order_id: props.orderId })
if (!code) {
throw new Error(msg || '支付初始化失败,请重试')
}
// 确保WeixinJSBridge已准备就绪
const bridge = await initWxPay()
if (!bridge) {
throw new Error('微信支付环境初始化失败,请在微信中打开')
}
// 发起支付请求
bridge.invoke('getBrandWCPayRequest', { ...data.payargs }, async (res) => {
try {
await handlePaymentResult(res)
} catch (error) {
console.error('支付处理失败:', error)
paymentStatus.value = 'failed'
paymentError.value = error.message || '支付处理失败,请重试'
emit('failed', paymentError.value)
}
})
} catch (error) {
console.error('支付失败:', error)
paymentStatus.value = 'failed'
paymentError.value = error.message || '支付失败,请重试'
emit('failed', paymentError.value)
}
}
onMounted(() => {
// 初始化微信支付
initWxPay()
// 开始支付流程
handlePayment()
})
</script>
......@@ -16,62 +16,30 @@
</FrostedGlass>
</div>
<div v-else-if="orderComplete || paymentStatus === 'success'" class="h-screen flex flex-col items-center justify-center px-4">
<FrostedGlass class="p-6 rounded-xl text-center">
<div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-2xl font-bold mb-2">支付成功!</h2>
<p class="text-gray-600 mb-2">您的订单已经成功提交</p>
<p class="text-gray-500 text-sm mb-6">订单号: {{ orderId }}</p>
<WechatPayment
v-else-if="showPayment"
:order-id="orderId"
@success="handlePaymentSuccess"
@failed="handlePaymentFailed"
@processing="handlePaymentProcessing"
>
<template #success-action>
<button
@click="handleBackToHome"
class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
>
返回首页
</button>
</FrostedGlass>
</div>
<!-- 支付状态提示 -->
<div v-else-if="paymentStatus === 'processing'" class="h-screen flex flex-col items-center justify-center px-4">
<FrostedGlass class="p-6 rounded-xl text-center">
<div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-4">
<div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
</div>
<h2 class="text-2xl font-bold mb-2">支付处理中</h2>
<p class="text-gray-600 mb-6">请稍候,正在处理您的支付...</p>
</FrostedGlass>
</div>
<!-- 支付失败提示 -->
<div v-else-if="paymentStatus === 'failed'" class="h-screen flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<FrostedGlass class="w-full max-w-sm sm:max-w-md lg:max-w-lg p-6 sm:p-8 rounded-2xl text-center">
<div class="w-20 h-20 sm:w-24 sm:h-24 bg-red-50 rounded-full flex items-center justify-center mx-auto">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 sm:h-12 sm:w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 class="text-xl sm:text-2xl font-bold mb-2 sm:mb-3">支付失败</h2>
<p class="text-sm sm:text-base text-gray-600 mb-6 sm:mb-8 px-2 sm:px-4">{{ paymentError || '支付过程中出现错误,请重试' }}</p>
<div class="space-y-2 sm:space-y-3 max-w-xs mx-auto">
<button
@click="handleSubmit"
class="w-full bg-gradient-to-r from-red-500 to-red-600 text-white py-3 sm:py-3.5 rounded-xl font-medium text-base sm:text-lg hover:from-red-600 hover:to-red-700 transition-colors"
>
重新支付
</button>
</template>
<template #failed-action>
<button
@click="handleBackToHome"
class="w-full bg-gray-100 text-gray-700 py-3 sm:py-3.5 rounded-xl font-medium text-base sm:text-lg hover:bg-gray-200 transition-colors"
>
返回首页
</button>
</div>
</FrostedGlass>
</div>
</template>
</WechatPayment>
<div v-else class="pb-20">
<!-- Order Summary -->
......@@ -311,13 +279,13 @@ import { useRoute, useRouter } from 'vue-router'
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import WechatPayment from '@/components/payment/WechatPayment.vue'
import { useCart } from '@/contexts/cart'
import { useTitle } from '@vueuse/core';
import { wxPayAPI, wxPayCheckAPI } from "@/api/wx/pay";
import { useTitle } from '@vueuse/core'
const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title);
const $route = useRoute()
const $router = useRouter()
useTitle($route.meta.title)
const router = useRouter()
const { items: cartItems, mode, getTotalPrice, handleCheckout, clearCart, removeFromCart } = useCart()
......@@ -331,15 +299,11 @@ const formData = ref({
pay_type: 'WeChat'
})
// 支付状态管理
// 支付相关状态
const showPayment = ref(false)
const orderId = ref('')
const isProcessing = ref(false)
const orderComplete = ref(false)
const orderId = ref('')
const paymentStatus = ref('pending') // pending, processing, success, failed
const paymentError = ref('')
const paymentRetryCount = ref(0)
const MAX_RETRY_ATTEMPTS = 3
const RETRY_DELAY = 2000 // 2秒重试间隔
// 确认对话框状态
const showConfirmDialog = ref(false)
......@@ -356,135 +320,61 @@ const handleImageError = (e) => {
e.target.src = '/assets/images/course-placeholder.jpg'
}
// 初始化微信支付
const initWxPay = () => {
return new Promise((resolve) => {
if (typeof WeixinJSBridge !== "undefined") {
resolve(WeixinJSBridge);
} else {
document.addEventListener('WeixinJSBridgeReady', () => {
resolve(WeixinJSBridge);
}, false);
}
});
}
// 检查支付状态
const checkPaymentStatus = async (orderId) => {
try {
const payStatus = await wxPayCheckAPI({ order_id: orderId });
if (payStatus.code) {
paymentStatus.value = 'success';
orderComplete.value = true;
return true;
}
return false;
} catch (error) {
console.error('支付状态查询失败:', error);
return false;
}
}
// 处理支付结果
const handlePaymentResult = async (res) => {
if (res.err_msg === "get_brand_wcpay_request:ok") {
// 支付成功,验证支付状态
let retryCount = 0;
const verifyPayment = async () => {
if (await checkPaymentStatus(orderId.value)) {
// 支付成功后清空购物车
clearCart();
return;
}
if (retryCount < MAX_RETRY_ATTEMPTS) {
retryCount++;
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
await verifyPayment();
} else {
paymentStatus.value = 'failed';
paymentError.value = '支付状态验证失败,请联系客服';
}
}
await verifyPayment();
} else if (res.err_msg === "get_brand_wcpay_request:cancel") {
paymentStatus.value = 'failed';
paymentError.value = '支付已取消';
} else {
paymentStatus.value = 'failed';
paymentError.value = '支付失败,请重试';
}
}
onMounted(() => {
// 初始化微信支付
initWxPay();
})
// 处理表单提交和支付流程
// 处理表单提交
const handleSubmit = async (e) => {
try {
// 重置支付状态
paymentStatus.value = 'pending';
paymentError.value = '';
paymentRetryCount.value = 0;
// 表单验证
if (!formData.value.receive_name?.trim()) {
throw new Error('请输入姓名');
throw new Error('请输入姓名')
}
if (!formData.value.receive_phone?.trim()) {
throw new Error('请输入手机号码');
throw new Error('请输入手机号码')
}
if (!formData.value.pay_type) {
throw new Error('请选择支付方式');
throw new Error('请选择支付方式')
}
isProcessing.value = true;
paymentStatus.value = 'processing';
isProcessing.value = true
// 创建订单
const result = await handleCheckout(formData.value);
const result = await handleCheckout(formData.value)
if (!result?.success) {
throw new Error(result?.message || '订单创建失败,请重试');
throw new Error(result?.message || '订单创建失败,请重试')
}
orderId.value = result.orderId || '';
orderId.value = result.orderId || ''
if (!orderId.value) {
throw new Error('订单号获取失败,请重试');
throw new Error('订单号获取失败,请重试')
}
// 初始化支付
const { code, data, msg } = await wxPayAPI({ order_id: orderId.value });
if (!code) {
throw new Error(msg || '支付初始化失败,请重试');
}
// 确保WeixinJSBridge已准备就绪
const bridge = await initWxPay();
if (!bridge) {
throw new Error('微信支付环境初始化失败,请在微信中打开');
}
// 发起支付请求
bridge.invoke('getBrandWCPayRequest', { ...data.payargs }, async (res) => {
try {
await handlePaymentResult(res);
} catch (error) {
console.error('支付处理失败:', error);
paymentStatus.value = 'failed';
paymentError.value = error.message || '支付处理失败,请重试';
}
});
// 显示支付组件
showPayment.value = true
} catch (error) {
console.error('支付失败:', error);
paymentStatus.value = 'failed';
paymentError.value = error.message || '支付失败,请重试';
console.error('订单创建失败:', error)
alert(error.message || '订单创建失败,请重试')
} finally {
isProcessing.value = false;
isProcessing.value = false
}
}
// 处理支付成功
const handlePaymentSuccess = () => {
orderComplete.value = true
clearCart()
}
// 处理支付失败
const handlePaymentFailed = (error) => {
console.error('支付失败:', error)
showPayment.value = false
}
// 处理支付处理中
const handlePaymentProcessing = () => {
console.log('支付处理中...')
}
// Handle navigation back to home after order completion
const handleBackToHome = () => {
router.push('/')
......