场景:
前端项目中偶尔有需要扫描二维码的需求,现在封装一个公共组件,后期直接使用
效果:
实现:
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() {}
}
}