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




实现:
1、二维码组件:QrCodeDialog.vue
<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 = truethis.$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、父组件
// html<qr-code-dialogtitle="二次认证":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>// jsconst 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() {}}
}