Vue2 封装二维码弹窗组件

场景:

前端项目中偶尔有需要扫描二维码的需求,现在封装一个公共组件,后期直接使用

效果:

实现:

1、二维码组件:QrCodeDialog.vue

javascript 复制代码
<template>
  <el-dialog
      :title="title"
      :visible.sync="visible"
      :width="width"
      :before-close="handleBeforeClose"
      :close-on-click-modal="closeOnClickModal"
      :close-on-press-escape="closeOnPressEscape"
      v-drag
  >
    <!-- 二维码容器 -->
    <div class="qrcode-container">
      <!-- 加载状态 -->
      <div v-if="loading" class="loading" v-loading="loading" :element-loading-text="loadingText">
      </div>

      <!-- 错误提示 -->
      <div v-if="errorMsg" class="error">{{ errorMsg }}</div>
      <!-- 二维码内容(非加载/错误状态时显示) -->
      <div class="qrcode-content" v-if="!loading && !errorMsg">
        <slot name="qrCode">
          <!-- 二维码图片 -->
          <div class="qrcode-img-box">
            <img :src="qrCodeSrc" alt="二维码" class="qrcode-img" v-if="qrCodeSrc">
            <div class="empty-state" v-else>二维码生成中...</div>
          </div>
          <!-- 已过期提示 -->
          <div class="status-expired qrcode-status" v-if="timer <= 0">
            <span class="text">二维码已过期</span>
            <el-button size="mini" @click="handleRefresh">刷新</el-button>
          </div>
          <!-- 扫描完成等待认证 -->
          <div class="status-scanned qrcode-status" v-if="timer > 0 && currentScanStatus === scanSuccessStatus">
            <span class="text">扫描完成<br><br>等待认证</span>
          </div>
        </slot>
      </div>
      <!-- 倒计时 -->
      <div class="status-countdown" v-if="!errorMsg && !loading && timer > 0">
        <span class="text">倒计时:{{ timer }} S</span>
      </div>
      <!-- 底部提示文字 -->
      <div class="qrcode-tip" v-if="tipText">{{ tipText }}</div>
    </div>

    <!-- 底部按钮(默认隐藏,可通过slot自定义) -->
    <div slot="footer" class="dialog-footer" v-if="$slots.footer">
      <slot name="footer"></slot>
    </div>
  </el-dialog>
</template>

<script>
import QRCode from 'qrcode'; // 依赖 qrcode 库生成二维码

export default {
  name: 'QrCodeDialog',
  components: {},
  props: {
    // 弹窗标题
    title: {
      type: String,
      default: '二维码认证'
    },
    // 弹窗是否可见(.sync 修饰符双向绑定)
    visible: {
      type: Boolean,
      default: false
    },
    // 弹窗宽度
    width: {
      type: String,
      default: '50%'
    },
    // 点击遮罩层是否关闭
    closeOnClickModal: {
      type: Boolean,
      default: false
    },
    // 按ESC键是否关闭
    closeOnPressEscape: {
      type: Boolean,
      default: false
    },
    // 二维码过期时间(秒)
    expireTime: {
      type: Number,
      default: 60
    },
    // 二维码图片的宽度
    imgWidth: {
      type: Number,
      default: 128
    },
    // 底部提示文字
    tipText: {
      type: String,
      default: '请使用指定应用扫描二维码完成操作'
    },
    // 加载状态文字
    loadingText: {
      type: String,
      default: '加载中...'
    },
    // 轮询时间
    scanTimer: {
      type: Number,
      default: 2000
    },
    // 当前扫描状态
    currentScanStatus: {
      type: Number,
      default: 0
    },
    // 扫描完成状态
    scanSuccessStatus: {
      type: Number,
      default: 2
    },
    // 认证成功状态
    authSuccessStatus: {
      type: Number,
      default: 5
    },
    // 错误提示
    errorMsg: {
      type: String,
      default: ""
    },
    // 生成二维码的加密字符串
    qrContent: {
      type: String,
      default: "",
      required: true
    },
  },
  data() {
    return {
      timer: this.expireTime, // 二维码倒计时器
      countdownInterval: null, // 二维码倒计时定时器
      scanTimerInterval: null, // 轮询定时器
      loading: false, // 加载状态
      qrCodeSrc: "", // 二维码图片地址,加密字符串直接赋值
    };
  },
  watch: {
    // 监听弹窗显示状态,显示时生成二维码并启动倒计时
    visible(newVal) {
      if (newVal) {
        this.loading = true;
      } else {
        this.cleanup(); // 关闭时清理资源
      }
    },
    // 监听生成二维码的加密字符串
    qrContent(newVal) {
      if (newVal) {
        this.initQrCode();
      }
    },
  },
  methods: {
    /**
     * 初始化二维码
     */
    async initQrCode() {
      // this.loading = true;
      this.timer = this.expireTime;

      try {
        // 1. 将加密字符串转换为二维码图片
        this.qrCodeSrc = await this.generateQrCode(this.qrContent);
        if(!this.errorMsg){
          // 启动倒计时
          this.startCountdown(); // 二维码倒计时
          this.startScanPolling() // 轮询时间
        }
      } catch (err) {
        console.error('二维码生成错误:', err);
      } finally {
        this.loading = false;
      }
    },

    /**
     * 生成二维码图片
     * @param content 二维码内容
     * @returns {Promise<string>} 图片base64地址
     */
    generateQrCode(content) {
      return new Promise((resolve, reject) => {
        // 使用 qrcode 库生成 base64 格式图片
        QRCode.toDataURL(
            content,
            {width: this.imgWidth, margin: 1}, // 配置二维码大小、边距
            (err, url) => {
              if (err) reject(err);
              else resolve(url);
            }
        );
      });
    },
    /**
     * 启动二维码倒计时
     */
    startCountdown() {
      // 清除已有定时器
      if (this.countdownInterval) {
        clearInterval(this.countdownInterval);
      }
      this.countdownInterval = setInterval(() => { // 二维码倒计时
        this.timer--;
        if (this.timer <= 0) {
          // 清除二维码倒计时
          clearInterval(this.countdownInterval);
          this.countdownInterval = null;
          // 清除轮询定时器
          clearInterval(this.scanTimerInterval);
          this.scanTimerInterval = null;
          this.$emit('expired'); // 触发过期事件
        }
      }, 1000);
    },
    /**
     * 启动轮询检查扫描结果
     */
    startScanPolling() {
      if (this.scanTimerInterval) {
        clearInterval(this.scanTimerInterval)
      }
      // 启动新的定时器
      this.scanTimerInterval = setInterval(() => {
        // 获取扫描结果
        this.$emit("startScanPolling")
      }, this.scanTimer)
    },
    /**
     * 刷新二维码
     */
    handleRefresh() {
      this.loading = true
      this.$emit('refresh'); // 触发刷新事件
    },
    /**
     * 关闭前回调
     */
    handleBeforeClose() {
      this.$emit('update:visible', false); // 触发关闭(.sync 双向绑定)
      this.$emit('close');
    },
    /**
     * 清理资源
     */
    cleanup() {
      clearInterval(this.countdownInterval);
      clearInterval(this.scanTimerInterval);
      this.countdownInterval = null;
      this.scanTimerInterval = null;
      this.timer = this.expireTime;
      this.loading = false;
      this.errorMsg = '';
    }
  },
  beforeDestroy() {
    this.cleanup(); // 组件销毁时清理
  }
};
</script>

<style scoped lang="scss">
.qrcode-container {
  text-align: center;
}

.loading {
  //min-height: 240px;
}

.error {
  color: #f56c6c;
  padding: 40px 0;
  font-size: 14px;
}

.qrcode-container {
  .qrcode-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    position: relative;
    margin-bottom: 10px;

    * {
      font-weight: bold;
      color: #000;
      font-size: 14px;
    }

    .qrcode-status {
      position: absolute;
      background: rgba(255, 255, 255, 0.9);
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 10px;
    }

    .status-expired {
      display: flex;
      flex-direction: column;
      align-items: center;

      .text {
        font-size: 14px;
        margin-bottom: 5px;
      }

      .el-button {
        font-weight: normal;
        background: $textColor;
        color: #fff;
      }
    }
  }
}

// 倒计时
.status-countdown {
  font-size: 14px;
  font-weight: bold;
  margin-bottom: 10px;
}

// 提示语
.qrcode-tip {
  line-height: 1.5;
  font-weight: bold;
}

.qrcode-img-box {
  display: flex;
  align-items: center;
  justify-content: center;
}

.qrcode-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.empty-state {
  color: #999;
  font-size: 14px;
}

.status-scanned .text {
  font-size: 14px;
}

.dialog-footer {
  text-align: center;
}
</style>

2、父组件

javascript 复制代码
// html

<qr-code-dialog
        title="二次认证"
        :visible.sync="qrCodeDialog.visible"
        :expireTime="qrCodeDialog.expireTime"
        :img-width="qrCodeDialog.imgWidth"
        :current-scan-status="qrCodeDialog.scanStatus"
        :qr-code-src="qrCodeDialog.qrCodeSrc"
        :error-msg="qrCodeDialog.errorMsg"
        :scan-timer="qrCodeDialog.scanTimer"
        :qr-content="qrCodeDialog.qrContent"
        :tipText="qrCodeDialog.tipText"
        @refresh="handleQrRefresh"
        @close="handleQrClose"
        @startScanPolling="startScanPolling"
        ref="qrCodeDialog"
    ></qr-code-dialog>


// js

const QRCODE_DIALOG_TIP = '请使用应用扫描二维码,完成人脸识别认证'
const GET_SCAN_RESULT_TIMER = 2000 // 轮询获取扫描结果,2000
const QRCODE_DIALOG_TIMER = 180 // 二维码弹窗的倒计时,180
const QRCODE_IMG_WIDTH = 128 // 二维码尺寸
const SCAN_SUCCESS = 2 // 人脸扫描成功的状态码
const AUTH_SUCCESS = 5 // 认证成功的状态码
const AUTH_RESULT = '人脸识别认证通过,可继续进行审批' // 人脸识别提示结果

export default {
  data() {
    return {
      qrCodeDialog: { // 二维码相关
        count: 1, // 模拟扫描信息
        visible: false,
        errorMsg: "", // 二维码获取错误信息
        scanStatus: 0, // 人脸识别状态
        imgWidth: QRCODE_IMG_WIDTH,
        scanTimer: GET_SCAN_RESULT_TIMER, // 轮询时间
        qrContent: "", // 加密字符串
        expireTime: QRCODE_DIALOG_TIMER, // 弹窗倒计时
        tipText: QRCODE_DIALOG_TIP,
      },
    }
  },
  methods: {
    /**
     * 刷新
     */
    handleQrRefresh(){
    },
    /**
     * 关闭二维码弹窗
     */
    handleQrClose(){
    },
    /**
     * 子组件:启动轮询检查扫描结果
     */
    startScanPolling() {}
  }
}
相关推荐
凉柚ˇ3 小时前
Vue图片压缩方案
前端·javascript·vue.js
ByteCraze3 小时前
秋招被问到的常见问题
开发语言·javascript·原型模式
优弧3 小时前
Vue 和 React 框架对比分析:优缺点与使用场景
vue.js
渣哥4 小时前
从代理到切面:Spring AOP 的本质与应用场景解析
javascript·后端·面试
UIUV4 小时前
JavaScript代理模式实战解析:从对象字面量到情感传递的优雅设计
javascript
Kimser4 小时前
基于 VxeTable 的高级表格选择组件
前端·vue.js
摸着石头过河的石头4 小时前
JavaScript 防抖与节流:提升应用性能的两大利器
前端·javascript
_大学牲4 小时前
FuncAvatar: 你的头像氛围感神器 🤥🤥🤥
前端·javascript·程序员