之前写过一个demo也是web端扫描条形码和二维码,但是有个问题。程序扫描的是整个可视区域范围不是扫描框,这就会导致如果扫描框也有条码会出错。这次的例子修复了这个问题,扫描区域就是扫描框的区域。而且对不同情况都有良好的兼容,可以把可视区域和扫描框长宽传入组件,组件会进行计算从摄像头中得到扫描框的图像进行识别。
Vue.component('scanner-box',{ props: { // 视口长宽 viewportWidth:{ type: Number }, viewportHeight:{ type: Number }, // 扫描框长宽 scanWidth:{ type: Number }, scanHeight:{ type: Number } }, data(){ return { // 摄像头分辨率 cameraWidth: 0, cameraHeight: 0, canvas1Width: 0, canvas1Height: 0 } }, methods: { handleImageSize(video,item1,item2){ let videoWidth = this.cameraWidth, videoHeight = this.cameraHeight, viewWidth = this.viewportWidth, viewHeight = this.viewportHeight, scanW = this.scanWidth, scanH = this.scanHeight, w,h; let context1 = item1.getContext("2d"), context2 = item2.getContext("2d"); if (viewWidth/viewHeight > videoWidth/videoHeight){ // 视口比大于摄像头分辨率比 => 摄像头高度需要裁剪 w = viewWidth,h = parseInt(w/(videoWidth/videoHeight)); }else if (viewWidth/viewHeight < videoWidth/videoHeight){ // 视口比小于摄像头分辨率比 => 摄像头宽度需要裁剪 h = viewHeight,w = parseInt(h*(videoWidth/videoHeight)); }else { // 视口比等于摄像头分辨率比 => 摄像头长宽都不需要裁剪 w = viewWidth,h = viewHeight; } this.canvas1Width = w; this.canvas1Height = h; this.$nextTick(() => { context1.drawImage(video,0,0,videoWidth,videoHeight,0,0,w,h); context2.drawImage(item1,(w/2)-(scanW/2),(h/2)-(scanH/2),scanW,scanH,0,0,scanW,scanH); }) }, initVideo(constrains){ let _this = this; if(navigator.mediaDevices.getUserMedia){ //最新标准API navigator.mediaDevices.getUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError); } else if (navigator.webkitGetUserMedia){ //webkit内核浏览器 navigator.webkitGetUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError); } else if (navigator.mozGetUserMedia){ //Firefox浏览器 navagator.mozGetUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError); } else if (navigator.getUserMedia){ //旧版API navigator.getUserMedia(constrains).then(_this.videoSuccess).catch(_this.videoError); } }, videoSuccess(stream){ let video = this.$refs.video, _this = this; //将视频流设置为video元素的源 video.srcObject = stream; //播放视频 video.play(); video.oncanplay = function () { // 摄像头分辨率 console.log('摄像头分辨率'); console.log(video.videoWidth + 'x' + video.videoHeight); // 视口分辨率 console.log('视口分辨率'); console.log(window.innerWidth + 'x' + window.innerHeight); _this.cameraWidth = video.videoWidth; _this.cameraHeight = video.videoHeight; // 发送图片进行识别 _this.readImg(); }; }, videoError(error){ console.log("访问用户媒体设备失败:",error.name,error.message); }, readImg(){ let video = this.$refs.video, canvas1 = this.$refs.canvas1, context = canvas1.getContext("2d"), canvas2 = this.$refs.canvas2, context2 = canvas2.getContext("2d"), _this = this; let timer = setInterval(function () { _this.handleImageSize(video,canvas1,canvas2); // 扫码条形码 let imgUri = canvas2.toDataURL(); _this.readBarcode(imgUri,timer); // 扫码二维码 let imageData = context2.getImageData(0, 0, _this.scanWidth, _this.scanHeight); _this.readQrcode(imageData.data,timer); },500) }, readBarcode(imgBase64,timer){ let _this = this; Quagga.decodeSingle({ decoder: { readers: ["code_128_reader"] }, // locate为true程序会自动寻找条码,找不到不会出结果。会对一些清晰度不高的图片有影响 locate: false, src: imgBase64 }, function(result){ if (result){ if(result.codeResult){ // 扫描成功后清除定时器,停止扫描 // clearInterval(timer); _this.$emit('ondata',{ type: 'barcode', result: result.codeResult.code}); }else { console.log("正在扫条形码...not detected"); } }else { console.log("正在扫条形码...not detected"); } }); }, readQrcode(data,timer){ let _this = this; let code = jsQR(data, _this.scanWidth, _this.scanHeight, { // 只支持白底黑码,默认支持白底黑码和黑底白码。 inversionAttempts: "dontInvert", }); if (code){ // clearInterval(timer); _this.$emit('ondata',{ type: 'qrcode', result: code.data}); //_this.$refs.audio.play(); }else { console.log('正在扫二维码...'); } } }, mounted(){ // 检测浏览器是否支持getUserMedia if (navigator.mediaDevices.getUserMedia || navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia){ //调用用户媒体设备,访问摄像头 this.initVideo({ video:{ width: window.innerWidth*2, facingMode: { // 强制后置摄像头,pc端做测试请注释掉,不然会报错 // exact: "environment" } } }); } else { alert("你的浏览器不支持访问用户媒体设备"); } Common.getOrientationChange(function (res) { console.log(res); if (res){ window.location.reload(); } }); }, template: `<div id="scanner"> <div class="model"> <div class="scanner-view"> <div class="scanner-view-arrow arrow1"></div> <div class="scanner-view-arrow arrow2"></div> <div class="scanner-view-arrow arrow3"></div> <div class="scanner-view-arrow arrow4"></div> <div class="scanner-line"></div> </div> <div class="scanner-text">放入框内,自动扫描</div> </div> <audio id="audio" ref="audio" src="./css/success.mp3" style="width: 0px;height: 0px"></audio> <video class="video-view" ref="video" autoplay playsinline="true" webkit-playsinline="true"></video> <canvas ref="canvas1" :width="canvas1Width" :height="canvas1Height" style="display: none"></canvas> <canvas ref="canvas2" :width="scanWidth" :height="scanHeight" style="display: none"></canvas> </div>` }); #app { /*width: 100%;*/ } .left, .right { width: 50%; float: left; } .left { background: #f7f7f7; height: 100vh; } .stockSearch-title { background: #fff; border: 1px solid #dcdfe6; color: #606266; font-weight: 600; height: 40px; line-height: 40px; position: relative; text-align: center; } .stockSearch-title a { color: #79bbff; font-size: 14px; position: absolute; left: 20px; } .stockSearch-title .iconLeft-1 { font-size: 14px; font-weight: 600; } .stockSearch-result { background: #fff; box-sizing: border-box; width: 92%; min-height: 500px; margin: 20px auto; padding: 12px 20px 15px 20px; } .stockSearch-result .caption{ color: #4db3a2; font-weight: 600; padding: 10px 0; } .table .is-waiting{ color: #c0c4cc; font-size: 16px; text-align: center; padding: 20px; } .table .hide { display: none; } .table table { background: #fff; border: 1px solid #dddddd; border-spacing: 0; border-collapse: collapse; color: #606266; font-size: 14px; width:100%; } .td-stock-title { font-weight: 600; padding: 8px; text-align: center; } .table tr:nth-of-type(5){ color: #428bca; } .table td { border: 1px solid #ddd; } .table td >div div { padding: 4px 8px; } .table td >div div:nth-child(odd) { background: #f9f9f9; } .table-num { /*color: #303133;*/ font-weight: 600; } .table-current { color: #fff; background: #64b1f1; font-size: 12px; padding: .2em .3em; border-radius: 4px; } /* 右侧扫码 */ #scanner { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; position: relative; } .model{ box-sizing: border-box; width: 50vw; height: 100vh; position: relative; z-index: 88; border-top: calc((100vh - 30vw)/2) solid rgba(0,0,0,.2); border-bottom: calc((100vh - 30vw)/2) solid rgba(0,0,0,.2); border-right: 10vw solid rgba(0,0,0,.2); border-left: 10vw solid rgba(0,0,0,.2); } .scanner-view{ box-sizing: border-box; width: 100%; height: 100%; position: relative; border: 1px solid rgba(255,255,255,.3); z-index: 89; } .scanner-line{ position: absolute; width: 100%; height: 1px; background: #49FF46; /*background: radial-gradient(ellipse,#71e52c,#001800);*/ border-radius: 20px; z-index: 90; animation: myScan 1s infinite alternate; } @keyframes myScan{ from { top: 0; } to { top: 30vw; } } .scanner-view-arrow{ position: absolute; width: 5vw; height: 5vw; border: 2px solid #09bb07; } .scanner-view-arrow.arrow1{ top: -1px; left: -1px; z-index: 99; border-right: none; border-bottom: none; } .scanner-view-arrow.arrow2{ top: -1px; right: -1px; z-index: 99; border-left: none; border-bottom: none; } .scanner-view-arrow.arrow3{ bottom: -1px; left: -1px; z-index: 99; border-right: none; border-top: none; } .scanner-view-arrow.arrow4{ bottom: -1px; right: -1px; z-index: 99; border-left: none; border-top: none; } .scanner-text { color: #909399; font-size: 20px; height: 40px; line-height: 40px; text-align: center; margin-top: 20px; } .video-view{ position: absolute; width: 50vw; height: 100vh; object-fit: cover; top: 0; left: 0; z-index: 80; }