移动端扫码封装barcode.vue扫描组件
<template>
<div class="scan">
<div id="bcid">
<div style="height: 40%"></div>
<p class="tip">...载入中...</p>
</div>
</div>
</template>
<script type='text/ecmascript-6'>
import { Toast } from 'vant';
let scan = null;
let ws = null;
let view = null;
export default {
props: {
success: {
type: Function,
default () {
return null;
},
},
},
data () {
return {
codeUrl: "",
};
},
mounted () {
//await this.waitplus();
this.$nextTick(() => this.startRecognize());
},
beforeDestroy () {
console.log("barcode unmount");
this.closeScan();
},
methods: {
async waitplus () {
return new Promise((resolve, reject) => {
setInterval(() => {
if (!window.plus) return;
plus.screen.lockOrientation('portrait-primary');//竖屏
resolve();
}, 10);
setTimeout(() => {
reject();
}, 60000);
})
},
// 创建扫描控件
startRecognize () {
let that = this;
if (!window.plus) return;
plus.screen.lockOrientation('portrait-primary');
ws = plus.webview.currentWebview();
scan = new plus.barcode.Barcode('bcid', [window.plus.barcode.QR], {
position: 'absolute',
scanbarColor: '#2E49C0',
frameColor: '#2E49C0'
});
scan.onmarked = onmarked;
this.startScan();
function onmarked (type, result, file) {
switch (type) {
case plus.barcode.QR:
type = "QR";
break;
case plus.barcode.EAN13:
type = "EAN13";
break;
case plus.barcode.EAN8:
type = "EAN8";
break;
default:
type = "其它" + type;
break;
}
result = result.replace(/\n/g, "");
that.codeUrl = result;
//alert(result);
that.success(result);
}
},
// 开始扫描
startScan () {
if (!window.plus) return;
scan.start();
},
// 关闭条码识别控件
closeScan () {
if (!window.plus) return;
scan.cancel();
scan.close();
},
},
};
</script>
<style lang="less">
.scan {
height: 100%;
background: #000;
#bcid {
width: 100%;
position: absolute;
left: 0;
right: 0;
top: 1.3rem;
bottom: 0;
text-align: center;
color: #fff;
background: #000;
}
}
</style>
PC端扫码封装
<template>
<div>
<slot name="header">
<header>
<div
class="refresh"
@click="decodeOnceFromConstraintsFunc(switchPerspective)"
v-if="showToggleBtnCopy"
>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-shuaxin"></use>
</svg>
</div>
<div class="toggle" @click="toggle" v-if="showToggleBtnCopy">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-iconqiehuanjingtou"></use>
</svg>
</div>
</header>
</slot>
<div class="page" v-if="parseType === 'Camera'">
<video
id="video"
:class="{ video: !videoWidth && !videoHeight }"
:width="videoWidth"
:height="videoHeight"
autoplay
></video>
<slot>
<div
class="scanBox"
:style="{ width: scanBoxWidth, height: scanHeight }"
v-if="parseType === 'Camera' && showScanBoxCopy"
>
<div class="frame upperLeft"></div>
<div class="frame upperRight"></div>
<div class="frame lowerRight"></div>
<div class="frame lowerLeft"></div>
<div class="pointerBox">
<div class="pointer"></div>
</div>
<div v-show="tipShow" class="tip">{{ tipMsg }}</div>
</div>
</slot>
</div>
<!-- 选择图片 -->
<input
type="file"
id="choose"
accept="image/*"
v-if="parseType === 'Image' && inputId === 'choose' && showDefaultInput"
/>
<slot name="other"></slot>
</div>
</template>
<script>
import "./assets/svg/iconfont";
// BrowserQRCodeReader只读取二维码 速度要比 BrowserMultiFormatReader快
import { BrowserMultiFormatReader } from "@zxing/library";
export default {
name: "parser-code",
props: {
parseType: {
// 传入需要解码的类型 摄像头还是静态图 Camera 或 Image
type: String,
default: "Camera",
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
let result = ["Camera", "Image"].indexOf(value) !== -1;
if (!result)
throw new Error("parseType can only be 'Camera' or 'Image'");
return true;
},
},
// 如果选择使用静态图 那么可以传入一个input的id,这个input类型需要是file类型,否则采用组件默认的input
inputId: {
type: String,
default: "choose",
},
// 如果不想要默认的input框展示可以传入showDefaultInput,然后通过组件下的parseStaticImg方法传入图片路径来获取解析后的值
showDefaultInput: {
type: Boolean,
default: true,
},
showScanBox: {
type: Boolean,
default: false,
},
// 是否展示切换镜头按钮
showToggleBtn: {
type: Boolean,
default: false,
},
videoWidth: {
typeof: Number,
default: null,
},
videoHeight: {
typeof: Number,
default: null,
},
scanBoxWidth: {
typeof: Number,
default: null,
},
scanHeight: {
typeof: Number,
default: null,
},
success: {
type: Function,
default () {
return null
},
},
fail: {
type: Function,
default () {
return null
},
},
// 获取摄像头失败回调
getVideoFail: {
type: Function,
default () {
return null
},
},
},
data () {
return {
codeReader: null,
tipMsg: "扫描二维码",
tipShow: true,
video: null,
img: null,
reader: null,
back: true,
showScanBoxCopy: this.showScanBox,
showToggleBtnCopy: this.showToggleBtn,
inputDom: null,
};
},
computed: {
switchPerspective () {
return this.back
? { video: { facingMode: { exact: "environment" } } }
: { video: { facingMode: "user" } };
},
},
created () {
if (this.parseType === "Camera") {
this.openScan();
} else {
this.reader = new FileReader();
let _this = this;
this.reader.onload = function (e) {
if (!e.target.result)
throw new Error("[FileReader]:Image parsing failed");
_this.parseStaticImg(e.target.result); // 'data:image/jpeg;base64,/9j/4AAQSk...(base64编码)...'
};
}
},
mounted () {
// 防止获取不到父组件中的元素
if (this.parseType === "Image") {
this.$nextTick(() => {
this.inputDom = document.querySelector(`#${this.inputId}`);
this.inputDom &&
this.inputDom.addEventListener("change", this.selectImg(this), false);
});
}
},
beforeDestroy () {
// 页面关闭前,结束调用摄像头
this.codeReader.reset()
},
methods: {
async openScan () {
this.codeReader = await new BrowserMultiFormatReader();
this.codeReader
.getVideoInputDevices()
.then((videoInputDevices) => {
if (videoInputDevices.length > 1) {
this.showScanBoxCopy = true;
this.tipShow = true;
this.showToggleBtnCopy = true;
//this.tipMsg = "扫描装备条码";
this.decodeOnceFromConstraintsFunc(this.switchPerspective);
} else if (videoInputDevices.length > 0) {
this.showScanBoxCopy = true;
this.tipShow = true;
this.showToggleBtnCopy = true;
//this.tipMsg = "扫描装备条码";
const selectedDeviceId = videoInputDevices[0].deviceId;
this.codeReader.decodeFromInputVideoDeviceContinuously(selectedDeviceId, 'video', (result, err) => {
if (result) {
this.textContent = result.text;
this.success(result.text)
//关闭
}
if (err && !(err)) {
console.error(err);
}
});
} else {
return alert("请允许获取摄像头权限后再进行二维码扫描");
}
})
.catch((err) => {
this.showScanBoxCopy = false;
this.tipShow = false;
this.getVideoFail(err) || alert('调取用户摄像头失败');
});
},
toggle () {
//摄像头切换时,需要先重置codeReader,关闭已打开摄像头,否则操作失败
this.back = !this.back;
this.decodeOnceFromConstraintsFunc(this.switchPerspective);
},
async decodeOnceFromConstraintsFunc (constraints) {
// 摄像头读取重置
this.codeReader.decodeOnceFromConstraints(
constraints,
"video"
)
const stream = await navigator.mediaDevices.getUserMedia(constraints);
await this.codeReader.reset();
this.video = await this.codeReader.attachStreamToVideo(stream, "video");
await this.decoding(this.video)
},
async decoding (video) {
let result = this.codeReader.decodeOnce(video)
result.then((result) => {
this.tipShow = true;
this.tipMsg = "正在尝试识别...";
if (result) {
if (result.text) {
this.success(result.text)
this.tipMsg = "扫描装备条码";
this.decoding(this.video)
}
}
}, (err) => {
if (err) {
if (err.message === 'Video stream has ended before any code could be detected.') return
this.tipMsg = "识别失败";
this.fail(err);
setTimeout(() => { this.tipShow = false }, 2000)
}
});
},
// 返回一个promise对象,如果要获取解析后的值可以通过调用then方法来获取
async parseStaticImg (imgSrc) {
this.codeReader = await new BrowserMultiFormatReader();
let res = this.codeReader
.decodeFromImage(undefined, imgSrc)
.then((result) => {
this.success(result.text)
return result.text;
})
.catch((err) => {
this.fail(err);
});
return res;
},
selectImg (_this) {
return function () {
let file = this.files[0];
if (
file.type !== "image/jpeg" &&
file.type !== "image/png" &&
file.type !== "image/gif"
) {
this.value = "";
//this.outerHTML = this.outerHTML;
alert("不是有效的图片文件!");
return;
}
this.value = "";
// 以DataURL的形式读取文件: 该方法是一个异步方法,所以上面采用onload监听文件读取完成
_this.reader.readAsDataURL(file);
};
},
},
};
</script>
<style scoped>
body {
margin: 0;
}
/* 头部 */
header {
position: absolute;
top: 2%;
left: 0;
right: 0;
display: flex;
height: 40px;
justify-content: flex-end;
z-index: 999;
}
.toggle,
.refresh {
width: 30px;
height: 30px;
margin-right: 5%;
display: flex;
justify-content: center;
align-items: center;
}
.refresh {
margin-right: 2%;
}
/*vjs-fluid 自适video 长宽*/
.video {
height: 100%;
width: 100%;
/* 等比例放大,填充满父级 */
object-fit: cover;
}
.tip {
position: absolute;
left: 50%;
top: 120%;
transform: translate(-50%, 0);
white-space: nowrap;
color: rgba(222, 220, 222, 1);
font-size: 16px;
}
/* common */
.page {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
bottom: 0px;
}
.scanBox {
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%, -50%);
/* 扫描框变大,相机离太远不易得出结果,所以选择放大扫描框 */
height: 40%;
width: 85%;
}
.frame {
position: absolute;
width: 15px;
height: 15px;
border: 3px solid transparent;
background: transparent;
}
.upperLeft {
top: 0;
left: 0;
border-left-color: rgba(66, 133, 244, 1);
border-top-color: rgba(66, 133, 244, 1);
}
.upperRight {
top: 0;
right: 0;
border-right-color: rgba(66, 133, 244, 1);
border-top-color: rgba(66, 133, 244, 1);
}
.lowerRight {
bottom: 0;
right: 0;
border-bottom-color: rgba(66, 133, 244, 1);
border-right-color: rgba(66, 133, 244, 1);
}
.lowerLeft {
bottom: 0;
left: 0;
border-left-color: rgba(66, 133, 244, 1);
border-bottom-color: rgba(66, 133, 244, 1);
}
.pointerBox {
position: absolute;
top: 0;
left: 0;
width: 98%;
height: 100%;
overflow: hidden;
}
.pointer {
height: 3px;
background-image: linear-gradient(
to right,
transparent 0%,
rgba(66, 133, 244, 1) 50%,
transparent 100%
);
transform: translateY(-3px);
animation: move 3.5s linear infinite;
}
@keyframes move {
0% {
transform: translateY(-3px);
}
100% {
/* scanBox变高扫描线运动范围增大 */
transform: translateY(calc(45vh - 3px));
}
}
.icon {
width: 3em;
height: 3em;
vertical-align: -0.15em;
fill: white;
overflow: hidden;
}
</style>
使用组件
<template>
<div>
<!-- PC端扫码 -->
<!-- <ParserCode v-if="!showShare" :success="success" /> -->
<!-- 移动端扫码 -->
<barcode v-if="!showShare" :success="success" />
<van-share-sheet
v-model="showShare"
:options="options"
safe-area-inset-bottom
get-container="body"
@select="clickOptions"
/>
</div>
</template>