赞
踩
vue2中使用jsQR、@zxing/library、html5-qrcode实现二维码扫描.
ps:需要https环境才能使用!!!
1.html5-qrcode也能实现扫码,但是个人觉得还不够完善,需要调整距离才能对二维码进行解析,还有二维码过小也无法解析。
2.@zxing/library比html5-qrcode灵敏一点,但是有些安卓手机很模糊,无法使用,苹果手机倒是正常。
3.jsQR是我用下来觉得最灵敏的,能扫描小型二维码,也能自动对焦。
npm i html5-qrcode
其中分Html5Qrcode和Html5QrcodeScanType,前者可以自己画UI,后者是封装好的UI,看自己需求引用。
去源码https://github.com/mebjas/html5-qrcode页面参考
<template> <div class="qrcode"> <div id="reader"></div> </div> </template> <script> import { Html5Qrcode, Html5QrcodeScanType, Html5QrcodeSupportedFormats, } from "html5-qrcode"; export default { name: "App", components: {}, created() { this.getCameras(); }, beforeUnmount() { //this.stop(); }, methods: { getCameras() { Html5Qrcode.getCameras() .then((devices) => { if (devices && devices.length) { this.html5QrCode = new Html5Qrcode("reader"); this.start(); //扫码 } }) .catch(() => { // handle err this.html5QrCode = new Html5Qrcode("reader"); this.error = "ERROR: 您需要授予相机访问权限"; this.$emit("err", this.error); }); }, onScanSuccess(decodedText, decodedResult) { console.log(`Code scanned = ${decodedText}`, decodedResult); if (decodedText) { this.$emit("ok", decodedText); } }, start() { // let html5QrcodeScanner = new Html5QrcodeScanner( // "reader", // { // fps: 10, // qrbox: { width: 250, height: 250 }, // disableFlip: false, // focusMode: "continuous", // facingMode: "environment", // aspectRatio: 0.5, // // aspectRatio: 1.777778, // formatsToSupport: [ // Html5QrcodeSupportedFormats.QR_CODE, // Html5QrcodeSupportedFormats.UPC_A, // Html5QrcodeSupportedFormats.UPC_E, // Html5QrcodeSupportedFormats.UPC_EAN_EXTENSION, // ], // advanced: [{ zoom: 5.0 }], // rememberLastUsedCamera:true, // experimentalFeatures: { // useBarCodeDetectorIfSupported: true, // }, // defaultZoomValueIfSupported: 5, // willReadFrequently: true, // supportedScanTypes: [ // Html5QrcodeScanType.SCAN_TYPE_CAMERA, // ], // }, // ); // html5QrcodeScanner.render(this.onScanSuccess); this.html5QrCode .start( { width: { min: 640, ideal: 1280, max: 1920 }, height: { min: 480, ideal: 720, max: 1080 }, facingMode: { ideal: "environment" }, focusMode: { ideal: "continuous" }, }, { fps: 2, //500毫秒扫描一次 qrbox: { width: 250, height: 250 }, // aspectRatio: 0.5, disableFlip: false, focusMode: "continuous", facingMode: "environment", // aspectRatio: 1.777778, formatsToSupport: [ Html5QrcodeSupportedFormats.QR_CODE, Html5QrcodeSupportedFormats.UPC_A, Html5QrcodeSupportedFormats.UPC_E, Html5QrcodeSupportedFormats.UPC_EAN_EXTENSION, ], advanced: [{ zoom: 2.0 }], rememberLastUsedCamera: true, experimentalFeatures: { useBarCodeDetectorIfSupported: true, }, defaultZoomValueIfSupported: 2, willReadFrequently: true, supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_CAMERA], videoConstraints: { aspectRatio: 2, // <-- this would be honoured width: { min: 640, ideal: 1280, max: 1920 }, height: { min: 480, ideal: 720, max: 1080 }, facingMode: { ideal: "environment" }, focusMode: { ideal: "continuous" }, advanced: [{ zoom: 2.0 }], }, autofocus: true, //自动对焦 但使用的时候没有体会到 colorDark: "#0000ff", //加深二维码黑色部分的颜色提高识别度 colorLight: "#ffffff", //这个应该是提高非黑即白部分的亮度 提高识别度 visualFeedback: true, //开启视觉反馈 没有体会到 halfSample: true, //缩小二维码提高识别精度吧 }, (decodedText) => { this.$emit("ok", decodedText); } ) .catch((err) => { console.log(`Unable to start scanning, error: ${err}`); }); // let that=this setTimeout(function () { // this.html5QrCode.applyVideoConstraints() // this.html5QrCode.applyVideoConstraints({ // focusMode: "continuous", // advanced: [{ zoom: 2.0 }], // }); }, 2000); }, stop() { this.html5QrCode .stop() .then((ignore) => { // QR Code scanning is stopped. console.log(ignore, "QR Code scanning stopped."); }) .catch((err) => { // Stop failed, handle it. console.log(err, "Unable to stop scanning."); }); }, }, }; </script> <style> body { margin: 0; background-color: rgb(133, 133, 133); } .qrcode { position: relative; height: 100%; width: 100%; overflow: hidden; } </style>
npm i @zxing/library
<template> <div class="qrcode"> <video ref="video" id="video" class="scan-video" v-show="scanTextData.showScanBoxInfo" autoplay ></video> <div class="scan-img" v-show="scanTextData.showScanBoxInfo"> <div class="scan-frame"> <span class="left-t"></span> <span class="right-t"></span> <span class="left-b"></span> <span class="right-b"></span> <span class="cross-line"></span> </div> </div><div class="scan-tip" v-show="scanTextData.showScanBoxInfo"> {{ scanTextData.tipMsg }} </div> </div> </template> <script> import { BrowserMultiFormatReader } from "@zxing/library"; export default { name: "scanCodePage", data() { return { scanTextData: { loadingShow: false, codeReader: null, scanText: "", vin: null, tipMsg: "将二维码置于屏幕中,即可识别", tipShow: false, showScanBox: false, showScanBoxInfo: false, }, hasBind: false, dataObj: { qrCodeId: undefined }, }; }, components: {}, created() { this.toScanCode(); }, beforeUnmount() { //this.stop(); }, methods: { toScanCode() { console.log("识别二维码", this.dataObj); this.scanTextData.codeReader = new BrowserMultiFormatReader(); this.scanTextData.showScanBox = true; this.openScan(); }, cancelScan() { //识别完停止使用摄像头 let Video = document.getElementById("video"); Video.srcObject.getTracks()[0].stop(); this.scanTextData.codeReader.reset(); // 重置 this.scanTextData.showScanBox = false; setTimeout(() => { this.scanTextData.showScanBoxInfo = false; }, 1000); }, async openScan() { this.scanTextData.codeReader .getVideoInputDevices() .then((videoInputDevices) => { this.scanTextData.tipShow = true; this.scanTextData.tipMsg = "正在调用摄像头..."; console.log("videoInputDevices", videoInputDevices); // 默认获取第一个摄像头设备id let firstDeviceId = videoInputDevices[0].deviceId; // 获取第一个摄像头设备的名称 const videoInputDeviceslablestr = JSON.stringify( videoInputDevices[0].label ); if (videoInputDevices.length > 1) { // 判断是否后置摄像头 if (videoInputDeviceslablestr.indexOf("back") > -1) { firstDeviceId = videoInputDevices[0].deviceId; } else { firstDeviceId = videoInputDevices[1].deviceId; } } this.decodeFromInputVideoFunc(firstDeviceId); }) .catch((err) => { this.scanTextData.tipShow = false; console.error(err); }); }, decodeFromInputVideoFunc(firstDeviceId) { this.scanTextData.codeReader.reset(); // 重置 this.scanTextData.scanText = ""; this.scanTextData.codeReader.decodeFromInputVideoDeviceContinuously( firstDeviceId, "video", (result, err) => { this.scanTextData.tipMsg = "将二维码置于屏幕中,即可识别"; this.scanTextData.scanText = ""; setTimeout(() => { this.scanTextData.showScanBoxInfo = true; }, 1000); if (result) { // console.log('扫描结果', result.text); if (result.text) { // console.log('扫描结果11', result.text); // this.scanTextData.showScanBox = false // this.scanTextData.showScanBoxInfo = false // this.scanTextData.scanText = result.text // //这里扫描出结果可以调用你想要的方法 // //识别完停止使用摄像头 // let Video = document.getElementById("video"); // Video.srcObject.getTracks()[0].stop() // this.scanTextData.codeReader.reset(); // 重置 let decodedText = result.text; this.$emit("ok", decodedText); } } else { // console.log('没出来?',result,err) } if (err && !err) { this.scanTextData.tipMsg = "识别失败"; setTimeout(() => { this.scanTextData.tipShow = false; }, 2000); console.error(err); } } ); }, }, }; </script> <style scoped lang="scss"> body { margin: 0; background-color: rgb(133, 133, 133); } .qrcode { position: relative; height: 100%; width: 100%; overflow: hidden; } .scan-index-bar { background-image: linear-gradient(-45deg, #42a5ff, #59cfff); } .van-nav-bar__title { color: #fff !important; } .scan-box { position: fixed; top: 0; left: 0; z-index: 5; height: 100%; width: 100vw; .scan-cacel { position: absolute; top: 30px; left: 30px; z-index: 9; color: #fff; font-size: 35px; } } .scan-video { height: 100vh; width: 100vw; object-fit: cover; } .scan-img { width: 300px; height: 300px; position: fixed; top: 55%; left: 77%; margin-top: -200px; margin-left: -250px; z-index: 6; .scan-frame { width: 100%; height: 100%; position: relative; .left-t, .right-t, .left-b, .right-b { position: absolute; width: 80px; height: 80px; } .left-t { top: 0; left: 0; border-top: 2px solid #17b1b7; border-left: 2px solid #17b1b7; } .right-t { top: 0; right: 0; border-top: 2px solid #17b1b7; border-right: 2px solid #17b1b7; } .left-b { bottom: 0; left: 0; border-bottom: 2px solid #17b1b7; border-left: 2px solid #17b1b7; } .right-b { bottom: 0; right: 0; border-bottom: 2px solid #17b1b7; border-right: 2px solid #17b1b7; } .cross-line { width: 300px; height: 5px; background: linear-gradient( to right, rgba(255, 255, 255, 0), #5dddd3, rgba(255, 255, 255, 0) ); position: absolute; top: 0; left: 0; animation: identifier_p 5s infinite; } @keyframes identifier_p { 0% { top: 0%; } 50% { top: 100%; } 100% { top: 0; } } } } .scan-tip { width: 100vw; text-align: center; margin-bottom: 10vh; color: white; font-size: 5vw; position: absolute; bottom: 50px; left: 0; color: #fff; } .page-scan { overflow-y: hidden; } </style>
直接下载的组件压缩包,下载地址:https://ext.dcloud.net.cn/plugin?id=7007
<mumu-getQrcode @success='qrcodeSucess' @error="qrcodeError" />
<template> <view class="canvasBox"> <template v-if="isUse"> <view class="box"> <view class="line"></view> <view class="angle"></view> </view> <view class="box2" v-if="isUseTorch"> <view class="track" @click="openTrack"> <svg t="1653920715959" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1351" width="32" height="32" > <path d="M651.353043 550.479503H378.752795L240.862609 364.315031c-3.688944-4.897391-5.660621-10.876025-5.660621-17.045466v-60.040745c0-15.773416 12.847702-28.621118 28.621118-28.621118h502.459627c15.773416 0 28.621118 12.847702 28.621118 28.621118v59.977143c0 6.105839-1.971677 12.084472-5.660621 17.045466l-137.890187 186.228074zM378.752795 598.308571v398.024348c0 15.328199 12.402484 27.667081 27.667081 27.667081h217.266087c15.328199 0 27.667081-12.402484 27.66708-27.667081V598.308571H378.752795z m136.300124 176.942112c-14.564969 0-26.331429-11.76646-26.331428-26.331428v-81.283975c0-14.564969 11.76646-26.331429 26.331428-26.331429 14.564969 0 26.331429 11.76646 26.331429 26.331429v81.283975c0 14.564969-11.76646 26.331429-26.331429 26.331428zM512 222.608696c-17.554286 0-31.801242-14.246957-31.801242-31.801243V31.801242c0-17.554286 14.246957-31.801242 31.801242-31.801242s31.801242 14.246957 31.801242 31.801242v159.006211c0 17.554286-14.246957 31.801242-31.801242 31.801243zM280.932174 205.881242c-9.47677 0-18.889938-4.197764-25.122981-12.275279L158.242981 67.991056a31.864845 31.864845 0 0 1 5.597019-44.648944 31.864845 31.864845 0 0 1 44.648944 5.597018l97.502609 125.551305a31.864845 31.864845 0 0 1-5.597019 44.648944c-5.787826 4.579379-12.656894 6.741863-19.46236 6.741863zM723.987081 205.881242c-6.805466 0-13.674534-2.162484-19.462361-6.678261a31.794882 31.794882 0 0 1-5.597018-44.648944l97.566211-125.551304a31.794882 31.794882 0 0 1 44.648944-5.597019 31.794882 31.794882 0 0 1 5.597019 44.648944l-97.566211 125.551305c-6.360248 8.077516-15.709814 12.27528-25.186584 12.275279z" fill="#ffffff" p-id="1352" ></path> </svg> {{ trackStatus ? '关闭闪光灯' : '打开闪光灯' }} </view> </view> <view class="mask1 mask" :style="'height:' + maskHeight + 'px;'"></view> <view class="mask2 mask" :style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'" ></view> <view class="mask3 mask" :style="'height:' + maskHeight + 'px;'"></view> <view class="mask4 mask" :style="'width:' + maskWidth + 'px;top:' + maskHeight + 'px;height:' + canvasHeight + 'px'" ></view> </template> <template v-else> <slot name="error"> <view class="error"> <view class="on1">相机权限被拒绝,请尝试如下操作:</view> <view>· 刷新页面后重试;</view> <view>· 在系统中检测当前App或浏览器的相机权限是否被禁用;</view> <view>· 如果依然不能体验,建议在微信中打开链接;</view> </view> </slot> </template> </view> </template> <script> import jsQR from '../assets/js/jsQR.js' export default { props: { continue: { type: Boolean, default: true // false 监听一次 true 持续监听 }, exact: { type: String, default: 'environment' // environment 后摄像头 user 前摄像头 }, size: { type: String, default: 'whole' // whole 全屏 balf 半屏 }, definition: { type: Boolean, default: false // fasle 正常 true 高清 } }, data() { return { windowWidth: 0, windowHeight: 0, video: null, canvas2d: null, canvas2d2: null, canvasWidth: 200, canvasHeight: 200, maskWidth: 0, maskHeight: 0, inter: 0, track: null, isUseTorch: false, trackStatus: false, isParse: false, isUse: true } }, mounted() { if (origin.indexOf('https') === -1) throw '请在 https 环境中使用摄像头组件。' this.windowWidth = document.documentElement.clientWidth || document.body.clientWidth this.windowHeight = document.documentElement.clientHeight || document.body.clientHeight this.windowHeight = this.size === 'whole' ? this.windowHeight : this.windowHeight / 2 this.isParse = true this.$nextTick(() => { this.createMsk() this.openScan() }) }, // destroyed() { // this.closeCamera() // }, methods: { openScan() { const width = this.transtion(this.windowHeight) const height = this.transtion(this.windowWidth) const videoParam = { audio: false, video: { facingMode: { exact: this.exact }, width, height } } navigator.mediaDevices .getUserMedia(videoParam) .then(stream => { this.video = document.createElement('video') this.video.width = this.windowWidth this.video.height = this.windowHeight const canvas = document.createElement('canvas') canvas.id = 'canvas' canvas.width = this.transtion(this.canvasWidth) canvas.height = this.transtion(this.canvasHeight) canvas.style = 'display:none;' //canvas.style = 'position: fixed;top: 0;z-index: 999;left:0' this.canvas2d = canvas.getContext('2d') // 设置当前宽高 满屏 const canvasBox = document.querySelector('.canvasBox') canvasBox.append(this.video) canvasBox.append(canvas) canvasBox.style = `width:${this.windowWidth}px;height:${this.windowHeight}px;` // 创建第二个canvas const canvas2 = document.createElement('canvas') canvas2.id = 'canvas2' canvas2.width = this.canvasWidth canvas2.height = this.canvasHeight canvas2.style = 'position: absolute;top: 50%;left: 50%;z-index: 20;transform: translate(-50%,100%);' this.canvas2d2 = canvas2.getContext('2d') canvasBox.append(canvas2) this.video.srcObject = stream this.video.setAttribute('playsinline', true) this.video.play() this.tick() this.track = stream.getVideoTracks()[0] setTimeout(() => { this.isUseTorch = this.track.getCapabilities().torch || null }, 500) }) .catch(err => { this.isUse = false this.$emit('error', err) }) }, closeCamera() { this.isParse = false if (this.video && this.video.srcObject) { this.video.srcObject.getTracks().forEach(track => { track.stop() }) } }, tick() { if (!this.isParse) return if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) { this.canvas2d.drawImage( this.video, this.transtion(this.maskWidth), this.transtion(this.maskHeight), this.transtion(200), this.transtion(200), 0, 0, this.transtion(this.canvasWidth), this.transtion(this.canvasHeight) ) const imageData = this.canvas2d.getImageData( 0, 0, this.transtion(this.canvasWidth), this.transtion(this.canvasHeight) ) const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: 'dontInvert' }) this.canvas2d2.clearRect(0, 0, this.canvasWidth, this.canvasHeight) if (code) { this.drawLine(code.location.topLeftCorner, code.location.topRightCorner) this.drawLine(code.location.topRightCorner, code.location.bottomRightCorner) this.drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner) this.drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner) if (code.data) { this.getData(code.data) } } } requestAnimationFrame(this.tick) }, drawLine(begin, end, color = '#FF3B58') { this.canvas2d2.beginPath() this.canvas2d2.moveTo(this.nutranstion(begin.x), this.nutranstion(begin.y)) this.canvas2d2.lineTo(this.nutranstion(end.x), this.nutranstion(end.y)) this.canvas2d2.lineWidth = 4 this.canvas2d2.strokeStyle = color this.canvas2d2.stroke() }, getData(data) { this.sacnSuccess=true this.$emit('success', data) if (!this.continue) { this.closeCamera() } }, openTrack() { this.trackStatus = !this.trackStatus this.track.applyConstraints({ advanced: [{ torch: this.trackStatus }] }) }, createMsk() { this.maskWidth = this.windowWidth / 2 - this.canvasWidth / 2 this.maskHeight = this.windowHeight / 2 - this.canvasHeight / 2 }, transtion(number) { return this.definition ? number * 2.8 : number * 1.8 }, nutranstion(number) { return this.definition ? number / 2.8 : number / 1.8 } } } </script> <style scoped> /* page { background-color: #333333; } */ .canvasBox { width: 100vw; height: 100vh; position: relative; /* background-image: linear-gradient( 0deg, transparent 24%, rgba(32, 255, 77, 0.1) 25%, rgba(32, 255, 77, 0.1) 26%, transparent 27%, transparent 74%, rgba(32, 255, 77, 0.1) 75%, rgba(32, 255, 77, 0.1) 76%, transparent 77%, transparent ), linear-gradient( 90deg, transparent 24%, rgba(32, 255, 77, 0.1) 25%, rgba(32, 255, 77, 0.1) 26%, transparent 27%, transparent 74%, rgba(32, 255, 77, 0.1) 75%, rgba(32, 255, 77, 0.1) 76%, transparent 77%, transparent ); background-size: 3rem 3rem; background-position: -1rem -1rem; z-index: 10; background-color: #1110; */ } .box { width: 200px; height: 200px; position: absolute; left: 50%; top: -50%; transform: translate(-50%, -200%); overflow: hidden; border: 0.1rem solid rgba(100, 156, 155, 0.2); z-index: 11; } .line { height: calc(100% - 2px); width: 100%; background: linear-gradient(180deg, rgba(0, 255, 51, 0) 43%, rgb(100, 156, 155) 211%); border-bottom: 3px solid #649c9b; transform: translateY(-100%); animation: radar-beam 2s infinite alternate; animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99); animation-delay: 1.4s; } .box:after, .box:before, .angle:after, .angle:before { content: ''; display: block; position: absolute; width: 3vw; height: 3vw; z-index: 12; border: 0.2rem solid transparent; } .box:after, .box:before { top: 0; border-top-color: #649c9b; } .angle:after, .angle:before { bottom: 0; border-bottom-color: #649c9b; } .box:before, .angle:before { left: 0; border-left-color: #649c9b; } .box:after, .angle:after { right: 0; border-right-color: #649c9b; } @keyframes radar-beam { 0% { transform: translateY(-100%); } 100% { transform: translateY(0); } } .msg { text-align: center; padding: 20rpx 0; } .box2 { width: 300px; height: 200px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 20; } .track { position: absolute; bottom: -100px; left: 50%; transform: translateX(-50%); z-index: 20; color: #fff; display: flex; flex-direction: column; align-items: center; } .mask { position: absolute; z-index: 10; background-color: rgba(0, 0, 0, 0.55); } .mask1 { top: 0; left: 0; right: 0; } .mask2 { right: 0; } .mask3 { right: 0; left: 0; bottom: 0; } .mask4 { left: 0; } .error { color: #fff; padding: 40rpx; font-size: 24rpx; background-color: #333333; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 550rpx; border-radius: 20rpx; } .error .on1 { font-size: 30rpx; } </style>
由于我装了eslint检测出jsQR.js有语法错误,直接去掉这个js文件的检查就行
在jsQR.js的第一行加上即可
/* eslint-disable */
效果图如下
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。