hookehuyr

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

将支付逻辑从CheckoutPage.vue中提取到独立的WechatPayment.vue组件中,以提高代码的可维护性和复用性。同时,更新了CheckoutPage.vue以使用新的支付组件,并简化了支付状态管理。
...@@ -53,5 +53,6 @@ declare module 'vue' { ...@@ -53,5 +53,6 @@ declare module 'vue' {
53 VanTabs: typeof import('vant/es')['Tabs'] 53 VanTabs: typeof import('vant/es')['Tabs']
54 VanUploader: typeof import('vant/es')['Uploader'] 54 VanUploader: typeof import('vant/es')['Uploader']
55 VideoPlayer: typeof import('./components/ui/VideoPlayer.vue')['default'] 55 VideoPlayer: typeof import('./components/ui/VideoPlayer.vue')['default']
56 + WechatPayment: typeof import('./components/payment/WechatPayment.vue')['default']
56 } 57 }
57 } 58 }
......
1 +<template>
2 + <!-- 支付成功状态 -->
3 + <div v-if="paymentStatus === 'success'" class="h-screen flex flex-col items-center justify-center px-4">
4 + <FrostedGlass class="p-6 rounded-xl text-center">
5 + <div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
6 + <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">
7 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
8 + </svg>
9 + </div>
10 + <h2 class="text-2xl font-bold mb-2">支付成功!</h2>
11 + <p class="text-gray-600 mb-2">您的订单已经成功提交</p>
12 + <p class="text-gray-500 text-sm mb-6">订单号: {{ orderId }}</p>
13 + <slot name="success-action">
14 + <button
15 + @click="$emit('success')"
16 + class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
17 + >
18 + 完成
19 + </button>
20 + </slot>
21 + </FrostedGlass>
22 + </div>
23 +
24 + <!-- 支付处理中状态 -->
25 + <div v-else-if="paymentStatus === 'processing'" class="h-screen flex flex-col items-center justify-center px-4">
26 + <FrostedGlass class="p-6 rounded-xl text-center">
27 + <div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-4">
28 + <div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
29 + </div>
30 + <h2 class="text-2xl font-bold mb-2">支付处理中</h2>
31 + <p class="text-gray-600 mb-6">请稍候,正在处理您的支付...</p>
32 + </FrostedGlass>
33 + </div>
34 +
35 + <!-- 支付失败状态 -->
36 + <div v-else-if="paymentStatus === 'failed'" class="h-screen flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
37 + <FrostedGlass class="w-full max-w-sm sm:max-w-md lg:max-w-lg p-6 sm:p-8 rounded-2xl text-center">
38 + <div class="w-20 h-20 sm:w-24 sm:h-24 bg-red-50 rounded-full flex items-center justify-center mx-auto">
39 + <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">
40 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
41 + </svg>
42 + </div>
43 + <h2 class="text-xl sm:text-2xl font-bold mb-2 sm:mb-3">支付失败</h2>
44 + <p class="text-sm sm:text-base text-gray-600 mb-6 sm:mb-8 px-2 sm:px-4">{{ paymentError || '支付过程中出现错误,请重试' }}</p>
45 + <div class="space-y-2 sm:space-y-3 max-w-xs mx-auto">
46 + <button
47 + @click="handlePayment"
48 + 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"
49 + >
50 + 重新支付
51 + </button>
52 + <slot name="failed-action">
53 + <button
54 + @click="$emit('failed')"
55 + 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"
56 + >
57 + 返回
58 + </button>
59 + </slot>
60 + </div>
61 + </FrostedGlass>
62 + </div>
63 +</template>
64 +
65 +<script setup>
66 +import { ref, onMounted } from 'vue'
67 +import FrostedGlass from '@/components/ui/FrostedGlass.vue'
68 +import { wxPayAPI, wxPayCheckAPI } from "@/api/wx/pay"
69 +
70 +// 定义组件的props
71 +const props = defineProps({
72 + orderId: {
73 + type: String,
74 + required: true
75 + }
76 +})
77 +
78 +// 定义组件的事件
79 +const emit = defineEmits(['success', 'failed', 'processing'])
80 +
81 +// 支付状态管理
82 +const paymentStatus = ref('pending') // pending, processing, success, failed
83 +const paymentError = ref('')
84 +const paymentRetryCount = ref(0)
85 +const MAX_RETRY_ATTEMPTS = 3
86 +const RETRY_DELAY = 2000 // 2秒重试间隔
87 +
88 +// 初始化微信支付
89 +const initWxPay = () => {
90 + return new Promise((resolve) => {
91 + if (typeof WeixinJSBridge !== "undefined") {
92 + resolve(WeixinJSBridge)
93 + } else {
94 + document.addEventListener('WeixinJSBridgeReady', () => {
95 + resolve(WeixinJSBridge)
96 + }, false)
97 + }
98 + })
99 +}
100 +
101 +// 检查支付状态
102 +const checkPaymentStatus = async (orderId) => {
103 + try {
104 + const payStatus = await wxPayCheckAPI({ order_id: orderId })
105 + if (payStatus.code) {
106 + paymentStatus.value = 'success'
107 + return true
108 + }
109 + return false
110 + } catch (error) {
111 + console.error('支付状态查询失败:', error)
112 + return false
113 + }
114 +}
115 +
116 +// 处理支付结果
117 +const handlePaymentResult = async (res) => {
118 + if (res.err_msg === "get_brand_wcpay_request:ok") {
119 + // 支付成功,验证支付状态
120 + let retryCount = 0
121 + const verifyPayment = async () => {
122 + if (await checkPaymentStatus(props.orderId)) {
123 + emit('success')
124 + return
125 + }
126 + if (retryCount < MAX_RETRY_ATTEMPTS) {
127 + retryCount++
128 + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY))
129 + await verifyPayment()
130 + } else {
131 + paymentStatus.value = 'failed'
132 + paymentError.value = '支付状态验证失败,请联系客服'
133 + emit('failed', paymentError.value)
134 + }
135 + }
136 + await verifyPayment()
137 + } else if (res.err_msg === "get_brand_wcpay_request:cancel") {
138 + paymentStatus.value = 'failed'
139 + paymentError.value = '支付已取消'
140 + emit('failed', paymentError.value)
141 + } else {
142 + paymentStatus.value = 'failed'
143 + paymentError.value = '支付失败,请重试'
144 + emit('failed', paymentError.value)
145 + }
146 +}
147 +
148 +// 处理支付流程
149 +const handlePayment = async () => {
150 + try {
151 + // 重置支付状态
152 + paymentStatus.value = 'processing'
153 + paymentError.value = ''
154 + paymentRetryCount.value = 0
155 + emit('processing')
156 +
157 + // 初始化支付
158 + const { code, data, msg } = await wxPayAPI({ order_id: props.orderId })
159 + if (!code) {
160 + throw new Error(msg || '支付初始化失败,请重试')
161 + }
162 +
163 + // 确保WeixinJSBridge已准备就绪
164 + const bridge = await initWxPay()
165 + if (!bridge) {
166 + throw new Error('微信支付环境初始化失败,请在微信中打开')
167 + }
168 +
169 + // 发起支付请求
170 + bridge.invoke('getBrandWCPayRequest', { ...data.payargs }, async (res) => {
171 + try {
172 + await handlePaymentResult(res)
173 + } catch (error) {
174 + console.error('支付处理失败:', error)
175 + paymentStatus.value = 'failed'
176 + paymentError.value = error.message || '支付处理失败,请重试'
177 + emit('failed', paymentError.value)
178 + }
179 + })
180 +
181 + } catch (error) {
182 + console.error('支付失败:', error)
183 + paymentStatus.value = 'failed'
184 + paymentError.value = error.message || '支付失败,请重试'
185 + emit('failed', paymentError.value)
186 + }
187 +}
188 +
189 +onMounted(() => {
190 + // 初始化微信支付
191 + initWxPay()
192 + // 开始支付流程
193 + handlePayment()
194 +})
195 +</script>
...@@ -16,62 +16,30 @@ ...@@ -16,62 +16,30 @@
16 </FrostedGlass> 16 </FrostedGlass>
17 </div> 17 </div>
18 18
19 - <div v-else-if="orderComplete || paymentStatus === 'success'" class="h-screen flex flex-col items-center justify-center px-4"> 19 + <WechatPayment
20 - <FrostedGlass class="p-6 rounded-xl text-center"> 20 + v-else-if="showPayment"
21 - <div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4"> 21 + :order-id="orderId"
22 - <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"> 22 + @success="handlePaymentSuccess"
23 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> 23 + @failed="handlePaymentFailed"
24 - </svg> 24 + @processing="handlePaymentProcessing"
25 - </div> 25 + >
26 - <h2 class="text-2xl font-bold mb-2">支付成功!</h2> 26 + <template #success-action>
27 - <p class="text-gray-600 mb-2">您的订单已经成功提交</p>
28 - <p class="text-gray-500 text-sm mb-6">订单号: {{ orderId }}</p>
29 <button 27 <button
30 @click="handleBackToHome" 28 @click="handleBackToHome"
31 class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium" 29 class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
32 > 30 >
33 返回首页 31 返回首页
34 </button> 32 </button>
35 - </FrostedGlass> 33 + </template>
36 - </div> 34 + <template #failed-action>
37 -
38 - <!-- 支付状态提示 -->
39 - <div v-else-if="paymentStatus === 'processing'" class="h-screen flex flex-col items-center justify-center px-4">
40 - <FrostedGlass class="p-6 rounded-xl text-center">
41 - <div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-4">
42 - <div class="w-10 h-10 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
43 - </div>
44 - <h2 class="text-2xl font-bold mb-2">支付处理中</h2>
45 - <p class="text-gray-600 mb-6">请稍候,正在处理您的支付...</p>
46 - </FrostedGlass>
47 - </div>
48 -
49 - <!-- 支付失败提示 -->
50 - <div v-else-if="paymentStatus === 'failed'" class="h-screen flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
51 - <FrostedGlass class="w-full max-w-sm sm:max-w-md lg:max-w-lg p-6 sm:p-8 rounded-2xl text-center">
52 - <div class="w-20 h-20 sm:w-24 sm:h-24 bg-red-50 rounded-full flex items-center justify-center mx-auto">
53 - <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">
54 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
55 - </svg>
56 - </div>
57 - <h2 class="text-xl sm:text-2xl font-bold mb-2 sm:mb-3">支付失败</h2>
58 - <p class="text-sm sm:text-base text-gray-600 mb-6 sm:mb-8 px-2 sm:px-4">{{ paymentError || '支付过程中出现错误,请重试' }}</p>
59 - <div class="space-y-2 sm:space-y-3 max-w-xs mx-auto">
60 - <button
61 - @click="handleSubmit"
62 - 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"
63 - >
64 - 重新支付
65 - </button>
66 <button 35 <button
67 @click="handleBackToHome" 36 @click="handleBackToHome"
68 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" 37 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"
69 > 38 >
70 返回首页 39 返回首页
71 </button> 40 </button>
72 - </div> 41 + </template>
73 - </FrostedGlass> 42 + </WechatPayment>
74 - </div>
75 43
76 <div v-else class="pb-20"> 44 <div v-else class="pb-20">
77 <!-- Order Summary --> 45 <!-- Order Summary -->
...@@ -311,13 +279,13 @@ import { useRoute, useRouter } from 'vue-router' ...@@ -311,13 +279,13 @@ import { useRoute, useRouter } from 'vue-router'
311 import AppLayout from '@/components/layout/AppLayout.vue' 279 import AppLayout from '@/components/layout/AppLayout.vue'
312 import FrostedGlass from '@/components/ui/FrostedGlass.vue' 280 import FrostedGlass from '@/components/ui/FrostedGlass.vue'
313 import ConfirmDialog from '@/components/ui/ConfirmDialog.vue' 281 import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
282 +import WechatPayment from '@/components/payment/WechatPayment.vue'
314 import { useCart } from '@/contexts/cart' 283 import { useCart } from '@/contexts/cart'
315 -import { useTitle } from '@vueuse/core'; 284 +import { useTitle } from '@vueuse/core'
316 -import { wxPayAPI, wxPayCheckAPI } from "@/api/wx/pay";
317 285
318 -const $route = useRoute(); 286 +const $route = useRoute()
319 -const $router = useRouter(); 287 +const $router = useRouter()
320 -useTitle($route.meta.title); 288 +useTitle($route.meta.title)
321 const router = useRouter() 289 const router = useRouter()
322 const { items: cartItems, mode, getTotalPrice, handleCheckout, clearCart, removeFromCart } = useCart() 290 const { items: cartItems, mode, getTotalPrice, handleCheckout, clearCart, removeFromCart } = useCart()
323 291
...@@ -331,15 +299,11 @@ const formData = ref({ ...@@ -331,15 +299,11 @@ const formData = ref({
331 pay_type: 'WeChat' 299 pay_type: 'WeChat'
332 }) 300 })
333 301
334 -// 支付状态管理 302 +// 支付相关状态
303 +const showPayment = ref(false)
304 +const orderId = ref('')
335 const isProcessing = ref(false) 305 const isProcessing = ref(false)
336 const orderComplete = ref(false) 306 const orderComplete = ref(false)
337 -const orderId = ref('')
338 -const paymentStatus = ref('pending') // pending, processing, success, failed
339 -const paymentError = ref('')
340 -const paymentRetryCount = ref(0)
341 -const MAX_RETRY_ATTEMPTS = 3
342 -const RETRY_DELAY = 2000 // 2秒重试间隔
343 307
344 // 确认对话框状态 308 // 确认对话框状态
345 const showConfirmDialog = ref(false) 309 const showConfirmDialog = ref(false)
...@@ -356,135 +320,61 @@ const handleImageError = (e) => { ...@@ -356,135 +320,61 @@ const handleImageError = (e) => {
356 e.target.src = '/assets/images/course-placeholder.jpg' 320 e.target.src = '/assets/images/course-placeholder.jpg'
357 } 321 }
358 322
359 -// 初始化微信支付 323 +// 处理表单提交
360 -const initWxPay = () => {
361 - return new Promise((resolve) => {
362 - if (typeof WeixinJSBridge !== "undefined") {
363 - resolve(WeixinJSBridge);
364 - } else {
365 - document.addEventListener('WeixinJSBridgeReady', () => {
366 - resolve(WeixinJSBridge);
367 - }, false);
368 - }
369 - });
370 -}
371 -
372 -// 检查支付状态
373 -const checkPaymentStatus = async (orderId) => {
374 - try {
375 - const payStatus = await wxPayCheckAPI({ order_id: orderId });
376 - if (payStatus.code) {
377 - paymentStatus.value = 'success';
378 - orderComplete.value = true;
379 - return true;
380 - }
381 - return false;
382 - } catch (error) {
383 - console.error('支付状态查询失败:', error);
384 - return false;
385 - }
386 -}
387 -
388 -// 处理支付结果
389 -const handlePaymentResult = async (res) => {
390 - if (res.err_msg === "get_brand_wcpay_request:ok") {
391 - // 支付成功,验证支付状态
392 - let retryCount = 0;
393 - const verifyPayment = async () => {
394 - if (await checkPaymentStatus(orderId.value)) {
395 - // 支付成功后清空购物车
396 - clearCart();
397 - return;
398 - }
399 - if (retryCount < MAX_RETRY_ATTEMPTS) {
400 - retryCount++;
401 - await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
402 - await verifyPayment();
403 - } else {
404 - paymentStatus.value = 'failed';
405 - paymentError.value = '支付状态验证失败,请联系客服';
406 - }
407 - }
408 - await verifyPayment();
409 - } else if (res.err_msg === "get_brand_wcpay_request:cancel") {
410 - paymentStatus.value = 'failed';
411 - paymentError.value = '支付已取消';
412 - } else {
413 - paymentStatus.value = 'failed';
414 - paymentError.value = '支付失败,请重试';
415 - }
416 -}
417 -
418 -onMounted(() => {
419 - // 初始化微信支付
420 - initWxPay();
421 -})
422 -
423 -// 处理表单提交和支付流程
424 const handleSubmit = async (e) => { 324 const handleSubmit = async (e) => {
425 try { 325 try {
426 - // 重置支付状态
427 - paymentStatus.value = 'pending';
428 - paymentError.value = '';
429 - paymentRetryCount.value = 0;
430 -
431 // 表单验证 326 // 表单验证
432 if (!formData.value.receive_name?.trim()) { 327 if (!formData.value.receive_name?.trim()) {
433 - throw new Error('请输入姓名'); 328 + throw new Error('请输入姓名')
434 } 329 }
435 if (!formData.value.receive_phone?.trim()) { 330 if (!formData.value.receive_phone?.trim()) {
436 - throw new Error('请输入手机号码'); 331 + throw new Error('请输入手机号码')
437 } 332 }
438 if (!formData.value.pay_type) { 333 if (!formData.value.pay_type) {
439 - throw new Error('请选择支付方式'); 334 + throw new Error('请选择支付方式')
440 } 335 }
441 336
442 - isProcessing.value = true; 337 + isProcessing.value = true
443 - paymentStatus.value = 'processing';
444 338
445 // 创建订单 339 // 创建订单
446 - const result = await handleCheckout(formData.value); 340 + const result = await handleCheckout(formData.value)
447 if (!result?.success) { 341 if (!result?.success) {
448 - throw new Error(result?.message || '订单创建失败,请重试'); 342 + throw new Error(result?.message || '订单创建失败,请重试')
449 } 343 }
450 344
451 - orderId.value = result.orderId || ''; 345 + orderId.value = result.orderId || ''
452 if (!orderId.value) { 346 if (!orderId.value) {
453 - throw new Error('订单号获取失败,请重试'); 347 + throw new Error('订单号获取失败,请重试')
454 } 348 }
455 349
456 - // 初始化支付 350 + // 显示支付组件
457 - const { code, data, msg } = await wxPayAPI({ order_id: orderId.value }); 351 + showPayment.value = true
458 - if (!code) {
459 - throw new Error(msg || '支付初始化失败,请重试');
460 - }
461 -
462 - // 确保WeixinJSBridge已准备就绪
463 - const bridge = await initWxPay();
464 - if (!bridge) {
465 - throw new Error('微信支付环境初始化失败,请在微信中打开');
466 - }
467 -
468 - // 发起支付请求
469 - bridge.invoke('getBrandWCPayRequest', { ...data.payargs }, async (res) => {
470 - try {
471 - await handlePaymentResult(res);
472 - } catch (error) {
473 - console.error('支付处理失败:', error);
474 - paymentStatus.value = 'failed';
475 - paymentError.value = error.message || '支付处理失败,请重试';
476 - }
477 - });
478 352
479 } catch (error) { 353 } catch (error) {
480 - console.error('支付失败:', error); 354 + console.error('订单创建失败:', error)
481 - paymentStatus.value = 'failed'; 355 + alert(error.message || '订单创建失败,请重试')
482 - paymentError.value = error.message || '支付失败,请重试';
483 } finally { 356 } finally {
484 - isProcessing.value = false; 357 + isProcessing.value = false
485 } 358 }
486 } 359 }
487 360
361 +// 处理支付成功
362 +const handlePaymentSuccess = () => {
363 + orderComplete.value = true
364 + clearCart()
365 +}
366 +
367 +// 处理支付失败
368 +const handlePaymentFailed = (error) => {
369 + console.error('支付失败:', error)
370 + showPayment.value = false
371 +}
372 +
373 +// 处理支付处理中
374 +const handlePaymentProcessing = () => {
375 + console.log('支付处理中...')
376 +}
377 +
488 // Handle navigation back to home after order completion 378 // Handle navigation back to home after order completion
489 const handleBackToHome = () => { 379 const handleBackToHome = () => {
490 router.push('/') 380 router.push('/')
......