hookehuyr

feat(支付): 添加微信支付流程及状态管理

在CheckoutPage.vue中添加了微信支付的完整流程,包括支付初始化、支付状态检查、支付结果处理等功能。同时引入了支付状态管理,支持处理中、成功、失败等状态的展示和交互。优化了表单提交逻辑,确保支付流程的完整性和用户体验。
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
16 </FrostedGlass> 16 </FrostedGlass>
17 </div> 17 </div>
18 18
19 - <div v-else-if="orderComplete" class="h-screen flex flex-col items-center justify-center px-4"> 19 + <div v-else-if="orderComplete || paymentStatus === 'success'" class="h-screen flex flex-col items-center justify-center px-4">
20 <FrostedGlass class="p-6 rounded-xl text-center"> 20 <FrostedGlass class="p-6 rounded-xl text-center">
21 <div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4"> 21 <div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
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 <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">
...@@ -35,6 +35,42 @@ ...@@ -35,6 +35,42 @@
35 </FrostedGlass> 35 </FrostedGlass>
36 </div> 36 </div>
37 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">
51 + <FrostedGlass class="p-6 rounded-xl text-center">
52 + <div class="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-4">
53 + <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 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-2xl font-bold mb-2">支付失败</h2>
58 + <p class="text-gray-600 mb-6">{{ paymentError || '支付过程中出现错误,请重试' }}</p>
59 + <button
60 + @click="handleSubmit"
61 + class="w-full bg-gradient-to-r from-red-500 to-red-600 text-white py-3 rounded-xl font-medium mb-3"
62 + >
63 + 重新支付
64 + </button>
65 + <button
66 + @click="handleBackToHome"
67 + class="w-full bg-gray-100 text-gray-700 py-3 rounded-xl font-medium"
68 + >
69 + 返回首页
70 + </button>
71 + </FrostedGlass>
72 + </div>
73 +
38 <div v-else class="pb-20"> 74 <div v-else class="pb-20">
39 <!-- Order Summary --> 75 <!-- Order Summary -->
40 <div class="p-4 bg-gradient-to-r from-green-500/10 to-blue-500/10"> 76 <div class="p-4 bg-gradient-to-r from-green-500/10 to-blue-500/10">
...@@ -142,7 +178,6 @@ ...@@ -142,7 +178,6 @@
142 type="text" 178 type="text"
143 class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm" 179 class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
144 placeholder="请输入您的详细地址" 180 placeholder="请输入您的详细地址"
145 - required
146 /> 181 />
147 </div> 182 </div>
148 183
...@@ -276,6 +311,8 @@ import FrostedGlass from '@/components/ui/FrostedGlass.vue' ...@@ -276,6 +311,8 @@ import FrostedGlass from '@/components/ui/FrostedGlass.vue'
276 import ConfirmDialog from '@/components/ui/ConfirmDialog.vue' 311 import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
277 import { useCart } from '@/contexts/cart' 312 import { useCart } from '@/contexts/cart'
278 import { useTitle } from '@vueuse/core'; 313 import { useTitle } from '@vueuse/core';
314 +import { wxPayAPI, wxPayCheckAPI } from "@/api/wx/pay";
315 +
279 const $route = useRoute(); 316 const $route = useRoute();
280 const $router = useRouter(); 317 const $router = useRouter();
281 useTitle($route.meta.title); 318 useTitle($route.meta.title);
...@@ -292,12 +329,17 @@ const formData = ref({ ...@@ -292,12 +329,17 @@ const formData = ref({
292 pay_type: 'WeChat' 329 pay_type: 'WeChat'
293 }) 330 })
294 331
295 -// Loading and success states 332 +// 支付状态管理
296 const isProcessing = ref(false) 333 const isProcessing = ref(false)
297 const orderComplete = ref(false) 334 const orderComplete = ref(false)
298 const orderId = ref('') 335 const orderId = ref('')
336 +const paymentStatus = ref('pending') // pending, processing, success, failed
337 +const paymentError = ref('')
338 +const paymentRetryCount = ref(0)
339 +const MAX_RETRY_ATTEMPTS = 3
340 +const RETRY_DELAY = 2000 // 2秒重试间隔
299 341
300 -// Confirm dialog state 342 +// 确认对话框状态
301 const showConfirmDialog = ref(false) 343 const showConfirmDialog = ref(false)
302 const itemToDelete = ref(null) 344 const itemToDelete = ref(null)
303 345
...@@ -312,44 +354,130 @@ const handleImageError = (e) => { ...@@ -312,44 +354,130 @@ const handleImageError = (e) => {
312 e.target.src = '/assets/images/course-placeholder.jpg' 354 e.target.src = '/assets/images/course-placeholder.jpg'
313 } 355 }
314 356
315 -// Handle form submission 357 +// 初始化微信支付
358 +const initWxPay = () => {
359 + return new Promise((resolve) => {
360 + if (typeof WeixinJSBridge !== "undefined") {
361 + resolve(WeixinJSBridge);
362 + } else {
363 + document.addEventListener('WeixinJSBridgeReady', () => {
364 + resolve(WeixinJSBridge);
365 + }, false);
366 + }
367 + });
368 +}
369 +
370 +// 检查支付状态
371 +const checkPaymentStatus = async (orderId) => {
372 + try {
373 + const payStatus = await wxPayCheckAPI({ order_id: orderId });
374 + if (payStatus.code) {
375 + paymentStatus.value = 'success';
376 + orderComplete.value = true;
377 + return true;
378 + }
379 + return false;
380 + } catch (error) {
381 + console.error('支付状态查询失败:', error);
382 + return false;
383 + }
384 +}
385 +
386 +// 处理支付结果
387 +const handlePaymentResult = async (res) => {
388 + if (res.err_msg === "get_brand_wcpay_request:ok") {
389 + // 支付成功,验证支付状态
390 + let retryCount = 0;
391 + const verifyPayment = async () => {
392 + if (await checkPaymentStatus(orderId.value)) {
393 + return;
394 + }
395 + if (retryCount < MAX_RETRY_ATTEMPTS) {
396 + retryCount++;
397 + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
398 + await verifyPayment();
399 + } else {
400 + paymentStatus.value = 'failed';
401 + paymentError.value = '支付状态验证失败,请联系客服';
402 + }
403 + }
404 + await verifyPayment();
405 + } else if (res.err_msg === "get_brand_wcpay_request:cancel") {
406 + paymentStatus.value = 'failed';
407 + paymentError.value = '支付已取消';
408 + } else {
409 + paymentStatus.value = 'failed';
410 + paymentError.value = '支付失败,请重试';
411 + }
412 +}
413 +
414 +onMounted(() => {
415 + // 初始化微信支付
416 + initWxPay();
417 +})
418 +
419 +// 处理表单提交和支付流程
316 const handleSubmit = async (e) => { 420 const handleSubmit = async (e) => {
317 try { 421 try {
422 + // 重置支付状态
423 + paymentStatus.value = 'pending';
424 + paymentError.value = '';
425 + paymentRetryCount.value = 0;
426 +
318 // 表单验证 427 // 表单验证
319 if (!formData.value.receive_name?.trim()) { 428 if (!formData.value.receive_name?.trim()) {
320 - throw new Error('请输入姓名') 429 + throw new Error('请输入姓名');
321 } 430 }
322 if (!formData.value.receive_phone?.trim()) { 431 if (!formData.value.receive_phone?.trim()) {
323 - throw new Error('请输入手机号码') 432 + throw new Error('请输入手机号码');
324 } 433 }
325 - // if (!formData.value.receive_address?.trim()) {
326 - // throw new Error('请输入联系地址')
327 - // }
328 if (!formData.value.pay_type) { 434 if (!formData.value.pay_type) {
329 - throw new Error('请选择支付方式') 435 + throw new Error('请选择支付方式');
330 } 436 }
331 437
332 - isProcessing.value = true 438 + isProcessing.value = true;
439 + paymentStatus.value = 'processing';
333 440
334 - // Process checkout 441 + // 创建订单
335 - const result = await handleCheckout(formData.value) 442 + const result = await handleCheckout(formData.value);
443 + if (!result?.success) {
444 + throw new Error(result?.message || '订单创建失败,请重试');
445 + }
336 446
337 - if (!result) { 447 + orderId.value = result.orderId || '';
338 - throw new Error('支付处理失败,请重试') 448 + if (!orderId.value) {
449 + throw new Error('订单号获取失败,请重试');
339 } 450 }
340 451
341 - if (result.success) { 452 + // 初始化支付
342 - orderId.value = result.orderId || '' 453 + const { code, data, msg } = await wxPayAPI({ order_id: orderId.value });
343 - orderComplete.value = true 454 + if (!code) {
344 - // TODO: 生成orderid, 并跳转到支付页面 455 + throw new Error(msg || '支付初始化失败,请重试');
345 - } else { 456 + }
346 - throw new Error(result.message || '支付失败,请重试') 457 +
458 + // 确保WeixinJSBridge已准备就绪
459 + const bridge = await initWxPay();
460 + if (!bridge) {
461 + throw new Error('微信支付环境初始化失败,请在微信中打开');
347 } 462 }
463 +
464 + // 发起支付请求
465 + bridge.invoke('getBrandWCPayRequest', { ...data.payargs }, async (res) => {
466 + try {
467 + await handlePaymentResult(res);
468 + } catch (error) {
469 + console.error('支付处理失败:', error);
470 + paymentStatus.value = 'failed';
471 + paymentError.value = error.message || '支付处理失败,请重试';
472 + }
473 + });
474 +
348 } catch (error) { 475 } catch (error) {
349 - console.error('Checkout failed:', error) 476 + console.error('支付失败:', error);
350 - alert(error.message || '支付失败,请重试') 477 + paymentStatus.value = 'failed';
478 + paymentError.value = error.message || '支付失败,请重试';
351 } finally { 479 } finally {
352 - isProcessing.value = false 480 + isProcessing.value = false;
353 } 481 }
354 } 482 }
355 483
......