123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- <!-- 支付方式 -->
- <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>{{ countdown }}</div>
- <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', 'payed'])
- 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;
- emit('payed')
- return
- }
- if (orderInfo.value.status === 30) {
- // 支付关闭
- payStatus.value = -1;
- return;
- }
- payStatus.value = 1; // 待支付
- }
- const countdownInterval = ref(null)
- const countdownIntervalInit = () => {
- countdownInterval.value = setInterval(() => {
- countdown.value
- }, 1000)
- }
- // 获得支付订单信息
- const setOrder = async () => {
- if (!props.id-0) return emit('getOrderFail')
- try {
- const data = await getOrder(props.id, true) // 获取待支付的订单 (order:业务订单; orderInfo:支付订单)
- if (!data) {
- payStatus.value = -2;
- return
- }
- orderInfo.value = data || null
- // 剩余支付时间倒计时
- countdownIntervalInit()
- // 设置支付状态
- 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 (countdownInterval.value) clearInterval(countdownInterval.value); countdownInterval.value = null
- 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 countdown = computed(() => {
- const now = Date.now();
- const distance = orderInfo.value.expireTime - now
- if (distance < 0) {
- clearInterval(countdownInterval.value)
- router.go(0)
- return '已过期'
- }
- const seconds = Math.floor((distance / 1000) % 60)
- const minutes = Math.floor((distance / (1000 * 60)) % 60)
- const hours = Math.floor((distance / (1000 * 60 * 60)) % 24)
- // const days = Math.floor(distance / (1000 * 60 * 60 * 24))
- return '剩余支付时间: ' + hours + ':' + minutes + ':' + seconds
- });
- const open = () => {
- window.open('/personalRecharge')
- }
- </script>
- <style lang="scss" scoped>
- .font-size-40 { font-size: 40px; }
- </style>
|