当前位置: 首页 > article >正文

基于Vue 3的智能支付二维码弹窗组件设计与实现

前言

本文是集成qrcode-vue3和element的弹窗  请先下载qrcode-vue3 
npm install qrcode-vue3 --save

完整组件代码如下

<script setup lang="ts">
import { ref, computed, watch, onUnmounted } from 'vue'
import QRCodeVue3, { type QRCodeProps } from 'qrcode-vue3'
import type { OrderStatus } from '@/api/types' // 根据实际类型定义调整

interface Props {
  modelValue: boolean
  value: string
  orderId: string | number
  size?: number
  pollInterval?: number
  timeout?: number
  imageOptions?: QRCodeProps['imageOptions']
  qrOptions?: QRCodeProps['qrOptions']
  downloadOptions?: QRCodeProps['downloadOptions']
}

const props = withDefaults(defineProps<Props>(), {
  size: 200,
  pollInterval: 3000, // 3秒轮询
  timeout: 60000,     // 60秒超时
  imageOptions: () => ({
    hideBackgroundDots: true,
    imageSize: 0.4,
    margin: 0
  }),
  qrOptions: () => ({
    typeNumber: 0,
    mode: 'Byte',
    errorCorrectionLevel: 'H'
  }),
  downloadOptions: () => ({
    name: 'qrcode',
    extension: 'png'
  })
})

const emit = defineEmits(['update:modelValue', 'success', 'timeout'])

const dialogVisible = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})

const remainingTime = ref(0)
const pollingTimer = ref<number>()
const countdownTimer = ref<number>()

// 默认二维码样式配置
const defaultDotsOptions = {
  type: 'dots',
  color: '#26249a',
  gradient: {
    type: 'linear',
    rotation: 0,
    colorStops: [
      { offset: 0, color: '#26249a' },
      { offset: 1, color: '#26249a' }
    ]
  }
}

const defaultCornersOptions = {
  squareOptions: { type: 'dot', color: '#000000' },
  dotOptions: { type: undefined, color: '#000000' }
}

const startPolling = () => {
  stopPolling() // 先停止已有轮询
  remainingTime.value = Math.floor(props.timeout / 1000)
  
  // 启动倒计时
  countdownTimer.value = window.setInterval(() => {
    remainingTime.value -= 1
    if (remainingTime.value <= 0) {
      handleTimeout()
    }
  }, 1000)

  // 立即执行第一次查询
  pollOrderStatus()
}

const pollOrderStatus = async () => {
  try {
    const result = await getPayResultById(props.orderId) // 替换为实际的API调用
    if (result.success) {
      emit('success', result.data)
      closeDialog()
      return
    }
  } catch (error) {
    console.error('轮询请求失败:', error)
  }

  // 继续轮询
  if (remainingTime.value > 0) {
    pollingTimer.value = window.setTimeout(pollOrderStatus, props.pollInterval)
  }
}

const handleTimeout = () => {
  emit('timeout')
  closeDialog()
}

const closeDialog = () => {
  dialogVisible.value = false
  stopPolling()
}

const stopPolling = () => {
  clearTimeout(pollingTimer.value)
  clearInterval(countdownTimer.value)
  pollingTimer.value = undefined
  countdownTimer.value = undefined
}

// 监听弹窗显示状态
watch(() => props.modelValue, (newVal) => {
  if (newVal) {
    startPolling()
  } else {
    stopPolling()
  }
})

// 组件卸载时清除定时器
onUnmounted(stopPolling)

// 格式化剩余时间显示
const formatTime = (seconds: number) => {
  const mins = Math.floor(seconds / 60)
  const secs = seconds % 60
  return `${mins}:${secs.toString().padStart(2, '0')}`
}
</script>

<template>
  <el-dialog
    v-model="dialogVisible"
    :title="`扫描二维码 (剩余时间 ${formatTime(remainingTime)})`"
    class="qr-code-dialog"
    width="auto"
    @closed="stopPolling"
  >
    <div class="qr-container">
      <QRCodeVue3
        :value="value"
        :width="size"
        :height="size"
        :qr-options="qrOptions"
        :image-options="imageOptions"
        :dots-options="defaultDotsOptions"
        :corners-square-options="defaultCornersOptions.squareOptions"
        :corners-dot-options="defaultCornersOptions.dotOptions"
        :background-options="{ color: '#ffffff' }"
        :download="true"
        :download-options="downloadOptions"
      />
    </div>
    
    <template #footer>
      <el-button type="danger" @click="closeDialog">
        关闭二维码 ({{ formatTime(remainingTime) }})
      </el-button>
    </template>
  </el-dialog>
</template>

<style scoped lang="scss">
.qr-code-dialog {
  .qr-container {
    padding: 20px;
    display: flex;
    justify-content: center;
    align-items: center;
    background: #fff;
    border-radius: 8px;
  }

  :deep(.el-dialog__header) {
    margin-right: 0;
  }

  :deep(.el-dialog__body) {
    padding: 0;
  }
}
</style>

一、应用场景

在支付类系统中,我们经常需要实现这样的功能:当用户发起支付请求后,展示支付二维码,并持续轮询服务器查询支付状态。本文介绍的组件完美解决了以下需求:

  1. 动态二维码展示:支持自定义样式和参数

  2. 状态轮询机制:自动查询支付结果

  3. 智能超时处理:60秒自动销毁二维码

  4. 事件驱动机制:支付成功/超时事件通知

  5. 用户体验优化:实时倒计时显示

二、核心功能设计

1. 组件参数设计

interface Props {
  modelValue: boolean    // 控制弹窗显示
  value: string          // 二维码内容
  orderId: string | number // 订单ID
  size?: number          // 二维码尺寸(默认200px)
  pollInterval?: number  // 轮询间隔(默认3000ms)
  timeout?: number       // 超时时间(默认60000ms)
  // ...其他二维码配置参数
}

2. 生命周期管理

sequenceDiagram
    participant User
    component Component
    component Server
    
    User->>Component: 打开弹窗
    Component->>Server: 首次查询支付状态
    Server-->>Component: 返回支付状态
    loop 轮询周期
        Component->>Server: 定时查询状态
        Server-->>Component: 返回最新状态
    end
    alt 支付成功
        Component->>User: 触发success事件
    else 超时未支付
        Component->>User: 触发timeout事件
    end

三、关键技术实现

1. 复合定时器管理

const startPolling = () => {
  // 启动倒计时
  countdownTimer.value = window.setInterval(() => {
    remainingTime.value -= 1
    if (remainingTime.value <= 0) handleTimeout()
  }, 1000)

  // 轮询逻辑
  const poll = async () => {
    if (remainingTime.value <= 0) return
    
    try {
      const result = await getPayResultById(props.orderId)
      if (result.success) {
        emit('success', result.data)
        closeDialog()
      } else {
        pollingTimer.value = window.setTimeout(poll, props.pollInterval)
      }
    } catch (error) {
      pollingTimer.value = window.setTimeout(poll, props.pollInterval)
    }
  }

  poll()
}

实现亮点

  • 双定时器独立管理(倒计时+轮询)

  • 错误边界处理:网络异常时自动重试

  • 自动清理机制:组件卸载时清除定时器

2. 响应式时间显示

<template>
  <el-dialog 
    :title="`扫描二维码 (剩余时间 ${formatTime(remainingTime)})`"
  >
    <!-- ... -->
    <el-button>
      关闭二维码 ({{ formatTime(remainingTime) }})
    </el-button>
  </el-dialog>
</template>

<script>
const formatTime = (seconds: number) => {
  const mins = Math.floor(seconds / 60)
  const secs = seconds % 60
  return `${mins}:${secs.toString().padStart(2, '0')}`
}
</script>

四、组件集成使用

1. 基础使用示例

<template>
  <QrPaymentDialog
    v-model="showDialog"
    :value="qrUrl"
    :order-id="orderId"
    @success="handleSuccess"
    @timeout="handleTimeout"
  />
</template>

<script setup>
const handleSuccess = (orderData) => {
  ElNotification.success('支付成功')
  router.push('/payment/success')
}

const handleTimeout = () => {
  ElMessage.error('支付超时')
  // 重新生成订单...
}
</script>

2. 自定义配置示例

<QrPaymentDialog
  :size="300"
  :poll-interval="5000" 
  :timeout="120000"
  :download-options="{
    name: 'wechat-pay',
    extension: 'jpg'
  }"
/>

五、性能优化实践

  1. 定时器优化

    onUnmounted(() => {
      clearTimeout(pollingTimer.value)
      clearInterval(countdownTimer.value)
    })

  2. 渲染优化

    <QRCodeVue3
      v-if="dialogVisible" 
      <!-- 仅在可见时渲染 -->
    />

  3. 网络请求优化

    let isPending = false
    
    const poll = async () => {
      if (isPending) return
      isPending = true
      try {
        // 请求处理...
      } finally {
        isPending = false
      }
    }

六、扩展方向建议

  1. 二维码刷新机制

    const refreshQR = () => {
      qrVersion.value++ // 通过key强制重新渲染
    }

  2. 支付方式切换

    <template>
      <el-radio-group v-model="payMethod">
        <el-radio label="wechat">微信</el-radio>
        <el-radio label="alipay">支付宝</el-radio>
      </el-radio-group>
    </template>

  3. 多通道轮询

    const channels = ['/api/pay/status', '/api/pay/backup-status']
    let currentChannel = 0
    
    const poll = () => {
      get(channels[currentChannel]).catch(() => {
        currentChannel = (currentChannel + 1) % channels.length
      })
    }

七、最佳实践建议

  1. 服务端配合优化

    • 实现幂等性查询接口

    • 设置合适的HTTP缓存头

    • 返回扩展状态码(如:100-继续轮询,200-支付成功)

  2. 前端监控

    const poll = async () => {
      const start = Date.now()
      try {
        await getPayResult()
      } finally {
        const duration = Date.now() - start
        trackApiPerformance(duration) // 上报性能数据
      }
    }

  3. 降级方案

    <template>
      <div v-if="qrError" class="fallback">
        <p>二维码加载失败</p>
        <button @click="retry">重新生成</button>
      </div>
    </template>

八、总结

本文介绍的智能支付二维码组件具有以下优势:

特性优势说明
开箱即用简单props配置即可快速集成
高可定制性支持样式/定时策略全方位定制
健壮性完善的错误处理和定时器清理机制
良好的用户体验实时反馈+倒计时提示
类型安全完整的TypeScript支持

如果对你有帮助,请帮忙点个赞


http://www.kler.cn/a/583094.html

相关文章:

  • STC51 中断允许寄存器 IE
  • DOM与CSS:网页设计的核心力量
  • maven wrapper的使用
  • 嵌入式学习L6网络编程D5UDP编程
  • 【系统架构设计师】性能评估
  • Cadence 学习笔记(1)
  • 【ES6】ES6中的类
  • QT5.9.2项目复制到新电脑上后“error: LNK2019: 无法解析的外部符号”错误
  • Vue:列表操作
  • nginx实现tomcat反向代理
  • vscode更新后: 适用于 Linux 的 Windows 子系统必须更新到最新版本才能继续。可通过运行 “wsl.exe --update” 进行更新
  • OpenHarmony子系统开发 - 模块配置规则
  • word处理控件Aspose.Words教程:使用 Python 删除 Word 中的空白页
  • HCIE学习是自学还是选择培训机构?
  • 二分查找易错点分析报告
  • Linux软件包管理与Vim编辑器指南
  • rust学习笔记13-18. 四数之和
  • 【Spring】@PostConstruct详解
  • Conda:CondaSSLError
  • LabVIEW VI Scripting实现连接器窗格自动化