|
@@ -0,0 +1,331 @@
|
|
|
+<!-- 支付方式 -->
|
|
|
+<template>
|
|
|
+ <v-card elevation="0" :loading="loading" :disabled="loading">
|
|
|
+ <!-- 加载样式 -->
|
|
|
+ <template v-slot:loader="{ isActive }">
|
|
|
+ <v-progress-linear
|
|
|
+ :active="isActive"
|
|
|
+ color="var(--v-primary-base)"
|
|
|
+ height="1"
|
|
|
+ indeterminate
|
|
|
+ ></v-progress-linear>
|
|
|
+ </template>
|
|
|
+ <div style="color: var(--v-error-base); font-weight: bold; text-align: center;">
|
|
|
+ <span class="font-size-13 mr-2">¥</span>
|
|
|
+ <span class="font-size-40"> {{ orderInfo?.price ? orderInfo?.price / 100 : 0 }}</span>
|
|
|
+ </div>
|
|
|
+ <template v-if="payMethods?.length">
|
|
|
+ <v-chip-group v-model="payment" selected-class="text-primary" column mandatory @update:modelValue="payTypeChange">
|
|
|
+ <v-chip filter v-for="k in payMethods" :key="k.code" :value="k.code" class="mr-3" label>
|
|
|
+ {{ k.name }}
|
|
|
+ <svg-icon v-if="k.icon" class="ml-1" :name="k.icon" :size="k.size"></svg-icon>
|
|
|
+ </v-chip>
|
|
|
+ </v-chip-group>
|
|
|
+ <div v-if="tip" style="text-align: center;" class="mt-2">{{ tip }}</div>
|
|
|
+ <div v-if="isQrCodePay && remainder<=0" style="text-align: center;" class="my-10">
|
|
|
+ 二维码失效,请重试!
|
|
|
+ <!-- 二维码刷新 -->
|
|
|
+ <span @click="submitOrderFun(true)">
|
|
|
+ <v-icon size="20" style="color: var(--v-primary-base)">mdi-refresh</v-icon>
|
|
|
+ <span class="text-decoration-underline cursor-pointer mr-2" style="color: var(--v-primary-base)">刷新</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <!-- 钱包支付 -->
|
|
|
+ <div v-if="isWalletPay" class="py-10" style="text-align: center;">
|
|
|
+ <div>
|
|
|
+ <span>剩余现金:</span>
|
|
|
+ <span style="color: var(--v-primary-base);">{{ balance ? (balance / 100.0).toFixed(2) : 0 }}</span>
|
|
|
+ </div>
|
|
|
+ <!-- 余额刷新 -->
|
|
|
+ <div class="mt-3" @click="updateAccountInfo">
|
|
|
+ <v-icon size="20" style="color: var(--v-primary-base)">mdi-refresh</v-icon>
|
|
|
+ <span class="text-decoration-underline cursor-pointer mr-2" style="color: var(--v-primary-base)">刷新</span>
|
|
|
+ </div>
|
|
|
+ <div class="my-3" v-if="notEnoughMoney">
|
|
|
+ <span class="color-error">
|
|
|
+ 余额不足,请微信扫码付款
|
|
|
+ <span class="text-decoration-underline cursor-pointer" @click="open">(充值)</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 模拟支付 -->
|
|
|
+ <div v-if="payment === 'mock'" class="py-10"></div>
|
|
|
+ <!-- 二维码支付 -->
|
|
|
+ <div v-if="isQrCodePay && remainder>0" style="text-align: center;">
|
|
|
+ <QrCode :text="payQrCodeTxt" :width="170" style="margin: 0 auto;" />
|
|
|
+ <div
|
|
|
+ v-if="payQrCodeTxt"
|
|
|
+ class="mb-5"
|
|
|
+ style="color: var(--v-error-base);"
|
|
|
+ >
|
|
|
+ 扫码支付时请勿离开
|
|
|
+ <span v-if="remainderZhShow">{{ remainderZhShow }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 钱包支付确认按钮 -->
|
|
|
+ <div v-if="(isWalletPay && !notEnoughMoney) || payment === 'mock'" class="mt-2" style="text-align: center;">
|
|
|
+ <v-btn
|
|
|
+ class="buttons" color="primary"
|
|
|
+ :loading="payLoading"
|
|
|
+ @click="submitBtn"
|
|
|
+ >
|
|
|
+ 确认
|
|
|
+ </v-btn>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </v-card>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+defineOptions({ name: 'mall-pay'})
|
|
|
+import { computed, onBeforeUnmount, ref } from 'vue'
|
|
|
+import QrCode from '@/components/QrCode'
|
|
|
+import { definePayTypeList, qrCodePay, walletPay } from '@/utils/payType'
|
|
|
+import { getEnableCodeList, getOrderPayStatus } from '@/api/common'
|
|
|
+import { getOrder, submitOrder } from '@/api/mall/trade'
|
|
|
+const emit = defineEmits(['payTypeChange', 'paySuccess', 'stopInterval', 'getOrderFail'])
|
|
|
+const props = defineProps({
|
|
|
+ code: {
|
|
|
+ type: String,
|
|
|
+ default: 'mall' // mall:商城付款
|
|
|
+ },
|
|
|
+ appId: {
|
|
|
+ type: Number,
|
|
|
+ default: 12 // 12:商城付款
|
|
|
+ },
|
|
|
+ id: {
|
|
|
+ type: String,
|
|
|
+ default: '',
|
|
|
+ }
|
|
|
+})
|
|
|
+const loading = ref(true)
|
|
|
+const tip = ref('')
|
|
|
+// const orderType = ref('goods') // 订单类型; goods - 商品订单, recharge - 充值订单
|
|
|
+const orderInfo = ref({})
|
|
|
+const payStatus = ref(0) // 0=检测支付环境, -2=未查询到支付单信息, -1=支付已过期, 1=待支付,2=订单已支付
|
|
|
+const payMethods = ref([]) // 可选的支付方式
|
|
|
+const payment = ref('') // 选中的支付方式
|
|
|
+
|
|
|
+import { useUserStore } from '@/store/user'; const userStore = useUserStore()
|
|
|
+const userAccount = ref(JSON.parse(localStorage.getItem('userAccount')) || {}) // 账户信息
|
|
|
+userStore.$subscribe((mutation, state) => {
|
|
|
+ if (Object.keys(state.userAccount).length) userAccount.value = state.userAccount
|
|
|
+})
|
|
|
+
|
|
|
+const updateAccountInfo = async (isSnackbar = true) => {
|
|
|
+ await userStore.getUserAccountBalance()
|
|
|
+ userAccount.value = JSON.parse(localStorage.getItem('userAccount')) || {}
|
|
|
+ if (isSnackbar) Snackbar.success('刷新成功!')
|
|
|
+}
|
|
|
+
|
|
|
+// 对比余额是否不足 订单金额:orderInfo?.price-0
|
|
|
+const balance = computed(() => {
|
|
|
+ return userAccount.value?.balance ? userAccount.value.balance-0 : 0
|
|
|
+})
|
|
|
+const notEnoughMoney = computed(() => {
|
|
|
+ return orderInfo.value?.price-0 > balance.value
|
|
|
+})
|
|
|
+
|
|
|
+const Destroyed = ref(false)
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ Destroyed.value = true // 避免执行paySubmit中关闭支付弹窗,造成setInterval一直存在
|
|
|
+ clear()
|
|
|
+})
|
|
|
+
|
|
|
+// 提交支付订单 (提交后倒计时显示及支付状态轮巡)
|
|
|
+const timer = ref(null) // 支付状态轮询
|
|
|
+const submitOrderFun = async (showLoading = false) => {
|
|
|
+ if (!payment.value) return
|
|
|
+ try {
|
|
|
+ if (orderInfo.value) {
|
|
|
+ // 提交支付订单
|
|
|
+ // channelExtras: { openid: null} // 特殊逻辑:微信公众号、小程序支付时,必须传入 openid
|
|
|
+ const params = {
|
|
|
+ id: orderInfo.value.id, // 支付单编号
|
|
|
+ channelCode: payment.value, // 支付渠道
|
|
|
+ // returnUrl: , // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址
|
|
|
+ }
|
|
|
+ if (showLoading) loading.value = true
|
|
|
+ const res = await submitOrder(params)
|
|
|
+ // 二维码内容赋值
|
|
|
+ payQrCodeTxt.value = res?.displayContent || ''
|
|
|
+ remainder.value = 1
|
|
|
+ //
|
|
|
+ initIntervalFun() // 倒计时显示及支付状态轮巡
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ } finally {
|
|
|
+ if (showLoading) loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 状态转换:payOrder.status => payStatus
|
|
|
+const checkPayStatus = () => {
|
|
|
+ if (orderInfo.value.status === 10 || orderInfo.value.status === 20) {
|
|
|
+ // 支付成功
|
|
|
+ payStatus.value = 2;
|
|
|
+ Snackbar.warning('订单已支付')
|
|
|
+ // emit('payed')
|
|
|
+ router.go(0)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (orderInfo.value.status === 30) {
|
|
|
+ // 支付关闭
|
|
|
+ payStatus.value = -1;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ payStatus.value = 1; // 待支付
|
|
|
+}
|
|
|
+
|
|
|
+// 获得支付订单信息
|
|
|
+const setOrder = async () => {
|
|
|
+ if (!props.id-0) return emit('getOrderFail')
|
|
|
+ try {
|
|
|
+ const data = await getOrder(props.id, true) // 获取待支付的订单 (order:业务订单; orderInfo:支付订单)
|
|
|
+ orderInfo.value = data || null
|
|
|
+ // 设置支付状态
|
|
|
+ checkPayStatus()
|
|
|
+ // await updateAccountInfo()
|
|
|
+ // 获得支付方式
|
|
|
+ await setPayMethods()
|
|
|
+ // 获得支付方式
|
|
|
+ // await setPayMethods();
|
|
|
+ if (isQrCodePay.value) submitOrderFun() // 二维码支付时自动创建订单获取二维码内容展示
|
|
|
+ } catch (error) {
|
|
|
+ console.log('error:', error)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+setOrder()
|
|
|
+
|
|
|
+// 清空setInterval
|
|
|
+function clear() {
|
|
|
+ remainder.value = 1
|
|
|
+ payQrCodeTxt.value = '' // 二维码内容
|
|
|
+ if (timer.value) clearInterval(timer.value); timer.value = null
|
|
|
+ if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null
|
|
|
+}
|
|
|
+
|
|
|
+// 1.获得支付方式
|
|
|
+const isWalletPay = ref(false)
|
|
|
+const isQrCodePay = ref(false)
|
|
|
+const payTypeChange = (value) => {
|
|
|
+ payment.value = value
|
|
|
+ tip.value = payMethods.value.find(e => e.code === payment.value)?.tip || ''
|
|
|
+ isQrCodePay.value = qrCodePay.includes(payment.value)
|
|
|
+ isWalletPay.value = walletPay.includes(payment.value)
|
|
|
+ if (isQrCodePay.value) submitOrderFun() // 二维码支付时自动创建订单获取二维码内容展示
|
|
|
+ else { clear() }
|
|
|
+}
|
|
|
+// 1.支付方式
|
|
|
+const setPayMethods = async () => {
|
|
|
+ let list = []
|
|
|
+ payMethods.value = []
|
|
|
+ try {
|
|
|
+ // list = await getEnableCodeList2(props.code)
|
|
|
+ list = await getEnableCodeList({ appId: props.appId })
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ } finally {
|
|
|
+ if (definePayTypeList?.length && list?.length) {
|
|
|
+ list.forEach(code => {
|
|
|
+ const item = definePayTypeList.find(p => p.code === code)
|
|
|
+ if (item) {
|
|
|
+ if (!payment.value) {
|
|
|
+ tip.value = item.tip || ''
|
|
|
+ payTypeChange(code) // 默认值赋值
|
|
|
+ }
|
|
|
+ payMethods.value.push(item)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+const payLoading = ref(false)
|
|
|
+const payQrCodeTxt = ref('')
|
|
|
+
|
|
|
+// 钱包支付(余额支付)、模拟支付
|
|
|
+const submitBtn = () => {
|
|
|
+ payLoading.value = true
|
|
|
+ submitOrderFun()
|
|
|
+}
|
|
|
+
|
|
|
+import Snackbar from '@/plugins/snackbar'
|
|
|
+import { useRoute } from 'vue-router'; const route = useRoute()
|
|
|
+import { useRouter } from 'vue-router'; const router = useRouter()
|
|
|
+const getPayStatus = async () => {
|
|
|
+ if (Destroyed.value) return clear() // 用户关闭支付
|
|
|
+ try {
|
|
|
+ const data = await getOrderPayStatus({ id: orderInfo.value.id || orderInfo.value.payOrderId })
|
|
|
+ if ((data?.status - 0) === 10) {
|
|
|
+ // 支付成功
|
|
|
+ clear()
|
|
|
+ setTimeout(() => {
|
|
|
+ emit('paySuccess', { price: orderInfo.value.price })
|
|
|
+ if (isWalletPay.value) updateAccountInfo() // 更新余额
|
|
|
+ // Snackbar.success('支付成功')
|
|
|
+ if (route.fullPath === props.returnUrl) router.go(0) // 刷新页面
|
|
|
+ else if (props.returnUrl) router.push(props.returnUrl) // 返回指定页面
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 倒计时
|
|
|
+const countdownTime = 60000 * 3 // 倒计时三分钟
|
|
|
+const remainder = ref(1) // number 初始化不能为假,否则不能显示二维码
|
|
|
+const remainderZhShow = ref('') // 倒计时展示
|
|
|
+
|
|
|
+const remainderTimer = ref(null)
|
|
|
+// 初始化倒计时
|
|
|
+const initIntervalFun = async () => {
|
|
|
+ remainder.value = countdownTime // 初始倒计时时间
|
|
|
+ if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null // 每一次点击都清除上一个轮询
|
|
|
+ if (timer.value) clearInterval(timer.value); timer.value = null
|
|
|
+ //
|
|
|
+ await getPayStatus() // 立即查询一次支付状态
|
|
|
+ remainderCalc(); remainderTimer.value = setInterval(() => { remainderCalc() }, 1000) // 倒计时计算
|
|
|
+ timer.value = setInterval(() => { getPayStatus() }, 2000) // 轮巡支付状态
|
|
|
+}
|
|
|
+
|
|
|
+const remainderCalc = () => {
|
|
|
+ if (Destroyed.value) return clear() // 用户关闭支付
|
|
|
+ remainder.value -= 1000
|
|
|
+ remainderZhShow.value = formatDuration(remainder.value)
|
|
|
+ if (remainder.value <= 0) { // 倒计时结束
|
|
|
+ tip.value = ''
|
|
|
+ if (timer.value) clearInterval(timer.value); timer.value = null
|
|
|
+ emit('stopInterval') // 倒计时结束,关闭倒计时弹窗
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const formatDuration = (remainder) => {
|
|
|
+ // 将毫秒转换为秒
|
|
|
+ var seconds = Math.floor(remainder / 1000)
|
|
|
+ // 计算分钟和剩余的秒数
|
|
|
+ var minutes = Math.floor(seconds / 60)
|
|
|
+ var remainingSeconds = seconds % 60
|
|
|
+ // 格式化分钟和秒数,确保秒数为两位数(如果小于10,则前面补0)
|
|
|
+ minutes = minutes.toString().padStart(2, '0')
|
|
|
+ remainingSeconds = remainingSeconds.toString().padStart(2, '0')
|
|
|
+ // 返回格式化的字符串
|
|
|
+ return `${minutes}分${remainingSeconds}秒`
|
|
|
+}
|
|
|
+
|
|
|
+const open = () => {
|
|
|
+ window.open('/personalRecharge')
|
|
|
+}
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.font-size-40 { font-size: 40px; }
|
|
|
+</style>
|