feat(支付): 添加微信支付流程及状态管理
在CheckoutPage.vue中添加了微信支付的完整流程,包括支付初始化、支付状态检查、支付结果处理等功能。同时引入了支付状态管理,支持处理中、成功、失败等状态的展示和交互。优化了表单提交逻辑,确保支付流程的完整性和用户体验。
Showing
1 changed file
with
153 additions
and
25 deletions
| ... | @@ -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('微信支付环境初始化失败,请在微信中打开'); | ||
| 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 || '支付处理失败,请重试'; | ||
| 347 | } | 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 | ... | ... |
-
Please register or login to post a comment