之前写过一个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;
}