hookehuyr

feat(支付): 重构微信支付逻辑并添加支付倒计时功能

将微信支付逻辑提取到独立工具函数中,实现支付失败后可重试
在预约卡片组件中添加支付倒计时显示和重新支付功能
优化支付失败后的用户提示和交互流程
<!--
* @Date: 2024-01-24 16:38:13
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-13 15:04:21
* @LastEditTime: 2026-01-16 19:54:03
* @FilePath: /xyxBooking-weapp/src/components/reserveCard.vue
* @Description: 预约记录卡组件
-->
......@@ -22,16 +22,20 @@
<view class="booking-price">支付金额:<text>¥ {{ reserve_info.total_amt }}</text></view>
<view class="booking-time">下单时间:<text>{{ reserve_info.order_time }}</text></view>
</view>
<view class="booking-list-item-footer">
<!-- 倒计时逻辑省略,如果需要可添加 -->
<view v-if="is_pay_pending" class="booking-list-item-footer" @tap.stop>
<view v-if="countdown_seconds > 0" class="countdown">剩余支付时间:{{ countdown_text }}</view>
<view v-else class="countdown timeout">支付已超时</view>
<view v-if="countdown_seconds > 0" class="repay-btn" @tap.stop="onRepay">重新支付</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue'
import { computed, ref, watch, onUnmounted } from 'vue'
import Taro from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import { wechat_pay } from '@/utils/wechatPay'
const go = useGo();
......@@ -64,6 +68,116 @@ const CodeStatus = {
REFUNDING: '11'
}
/**
* 是否支付待处理状态
*/
const is_pay_pending = computed(() => {
if (is_offline.value) return false
return reserve_info.value?.status === CodeStatus.APPLY && !!reserve_info.value?.pay_id
})
const countdown_seconds = ref(0)
const format_two_digits = (n) => {
const num = Number(n) || 0
return num < 10 ? `0${num}` : String(num)
}
const countdown_text = computed(() => {
const seconds = Number(countdown_seconds.value) || 0
const minutes = Math.floor(seconds / 60)
const remain = seconds % 60
return `${format_two_digits(minutes)}:${format_two_digits(remain)}`
})
const parse_created_time_ms = (created_time) => {
const raw = String(created_time || '')
if (!raw) return 0
const fixed = raw.replace(/-/g, '/')
const date = new Date(fixed)
const time = date.getTime()
return Number.isFinite(time) ? time : 0
}
let countdown_timer = null
/**
* 停止倒计时
*/
const stop_countdown = () => {
if (countdown_timer) {
clearInterval(countdown_timer)
countdown_timer = null
}
}
const update_countdown = () => {
const start_ms = parse_created_time_ms(reserve_info.value?.created_time)
if (!start_ms) {
countdown_seconds.value = 0
stop_countdown()
return
}
const end_ms = start_ms + 10 * 60 * 1000
const diff_ms = end_ms - Date.now()
const seconds = Math.max(0, Math.floor(diff_ms / 1000))
countdown_seconds.value = seconds
if (seconds <= 0) {
stop_countdown()
}
}
const start_countdown = () => {
stop_countdown()
update_countdown()
if (countdown_seconds.value <= 0) return
countdown_timer = setInterval(update_countdown, 1000)
}
let is_showing_pay_modal = false
const show_pay_modal = async (content) => {
if (is_showing_pay_modal) return false
is_showing_pay_modal = true
try {
const res = await Taro.showModal({
title: '提示',
content: content || '支付未完成',
showCancel: true,
cancelText: '取消',
confirmText: '继续支付',
})
return !!res?.confirm
} finally {
is_showing_pay_modal = false
}
}
/**
* 重新支付
*/
const onRepay = async () => {
if (!is_pay_pending.value) return
if (countdown_seconds.value <= 0) {
Taro.showToast({ title: '支付已超时', icon: 'none' })
return
}
let should_continue = true
while (should_continue) {
const pay_id = reserve_info.value?.pay_id
const pay_res = await wechat_pay({ pay_id })
if (pay_res && pay_res.code == 1) {
go('/success', { pay_id })
return
}
should_continue = await show_pay_modal(pay_res?.msg || '支付未完成')
}
}
const formatStatus = (status) => {
switch (status) {
case CodeStatus.APPLY:
......@@ -116,6 +230,23 @@ const goToDetail = (item) => {
go(props.detail_path, { pay_id: item.pay_id });
}
}
/**
* 监听支付待处理状态变化
*/
watch(is_pay_pending, (val) => {
if (val) {
start_countdown()
} else {
countdown_seconds.value = 0
stop_countdown()
}
}, { immediate: true })
onUnmounted(() => {
stop_countdown()
})
</script>
<style lang="less">
......@@ -187,5 +318,29 @@ const goToDetail = (item) => {
}
}
}
.booking-list-item-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16rpx;
.countdown {
color: #A67939;
font-size: 28rpx;
&.timeout {
color: #999;
}
}
.repay-btn {
padding: 8rpx 20rpx;
border-radius: 12rpx;
background-color: #A67939;
color: #FFF;
font-size: 28rpx;
}
}
}
</style>
......
<!--
* @Date: 2024-01-16 11:37:10
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-13 11:43:13
* @LastEditTime: 2026-01-16 19:41:35
* @FilePath: /xyxBooking-weapp/src/pages/bookingList/index.vue
* @Description: 预约记录列表页
-->
......
<!--
* @Date: 2024-01-15 16:25:51
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-16 18:05:18
* @LastEditTime: 2026-01-16 19:47:01
* @FilePath: /xyxBooking-weapp/src/pages/submit/index.vue
* @Description: 预约人员信息
-->
......@@ -59,13 +59,15 @@
import { ref, computed } from 'vue'
import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import { useGo, useReplace } from '@/hooks/useGo'
import icon_check1 from '@/assets/images/多选01@2x.png'
import icon_check2 from '@/assets/images/多选02@2x.png'
import { personListAPI, addReserveAPI, wxPayAPI } from '@/api/index'
import { personListAPI, addReserveAPI } from '@/api/index'
import { wechat_pay } from '@/utils/wechatPay'
const router = useTaroRouter();
const go = useGo();
const replace = useReplace();
const visitorList = ref([]);
const date = ref('');
......@@ -154,12 +156,14 @@ const showPayErrorModal = async (content) => {
if (is_showing_pay_modal) return;
is_showing_pay_modal = true;
try {
await Taro.showModal({
const res = await Taro.showModal({
title: '提示',
content: content || '支付失败,请稍后再试',
showCancel: false,
confirmText: '我知道了',
showCancel: true,
cancelText: '离开',
confirmText: '继续支付',
});
return !!res?.confirm;
} finally {
is_showing_pay_modal = false;
}
......@@ -204,36 +208,23 @@ const submitBtn = async () => {
// 以接口返回的 need_pay 为准:1=需要支付,0=不需要支付
if (Number(need_pay) === 1 || need_pay === true) {
Taro.showLoading({ title: '支付准备中...' });
let payParams = null;
try {
payParams = await wxPayAPI({ pay_id }); // 参数接口
} finally {
Taro.hideLoading();
}
if (payParams && payParams.code == 1) {
let pay_params = payParams.data;
Taro.requestPayment({
timeStamp: pay_params.timeStamp,
nonceStr: pay_params.nonceStr,
package: pay_params.package,
signType: pay_params.signType,
paySign: pay_params.paySign,
success(res) {
let should_continue = true;
// 循环支付直到支付成功或用户取消支付
while (should_continue) {
const pay_res = await wechat_pay({ pay_id })
if (pay_res && pay_res.code == 1) {
pending_pay_id.value = null;
pending_need_pay.value = null;
go('/success', { pay_id });
},
fail(res) {
refreshVisitorList({ reset_checked: true }).catch(() => {});
showPayErrorModal('支付未完成,可再次点击提交订单继续支付').catch(() => {});
return
}
})
} else {
// 刷新参观者列表, 清除已预约标记
refreshVisitorList({ reset_checked: true }).catch(() => {});
showPayErrorModal(payParams?.msg || '获取支付信息失败,请稍后再试').catch(() => {});
should_continue = await showPayErrorModal(pay_res?.msg || '支付未完成,可再次点击提交订单继续支付')
}
replace('/bookingList')
return
} else {
pending_pay_id.value = null;
pending_need_pay.value = null;
......
/*
* @Date: 2026-01-16 19:41:09
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-16 19:56:36
* @FilePath: /xyxBooking-weapp/src/utils/wechatPay.js
* @Description: 微信支付工具函数
*/
import Taro from '@tarojs/taro'
import { wxPayAPI } from '@/api/index'
/**
* @description 微信支付
* @param {*} pay_id 订单号
* @returns {*} 支付结果
*/
export const wechat_pay = async ({ pay_id }) => {
const normalized_pay_id = String(pay_id || '')
if (!normalized_pay_id) {
return { code: 0, data: null, msg: '缺少订单号' }
}
Taro.showLoading({ title: '支付准备中...' })
let pay_params_res = null
try {
pay_params_res = await wxPayAPI({ pay_id: normalized_pay_id })
} finally {
Taro.hideLoading()
}
if (!pay_params_res || pay_params_res.code != 1) {
return { code: 0, data: null, msg: pay_params_res?.msg || '获取支付信息失败,请稍后再试' }
}
const pay_params = pay_params_res?.data || {}
const pay_result = await new Promise((resolve) => {
Taro.requestPayment({
timeStamp: pay_params.timeStamp,
nonceStr: pay_params.nonceStr,
package: pay_params.package,
signType: pay_params.signType,
paySign: pay_params.paySign,
success: (res) => resolve({ ok: true, res }),
fail: (err) => resolve({ ok: false, err }),
})
})
if (pay_result?.ok) {
return { code: 1, data: pay_result.res || null, msg: '支付成功' }
}
return { code: 0, data: pay_result?.err || null, msg: pay_result?.err?.errMsg || '支付未完成' }
}