先看看效果,如有需要直接拿
说明:
扫码页面手写
线上为https,微信可用,苹果浏览器可用
本地手机调试http用QQ浏览器
封装代码
<template>
<div class="scaner" ref="scaner">
<i class="close" @click="closeScaner()"> </i>
<div class="banner" v-if="showBanner">
<i class="close_icon" @click="() => (showBanner = false)"></i>
<p class="text">若当前浏览器无法扫码,请切换其他浏览器尝试</p>
</div>
<div class="cover">
<p class="line"></p>
<span class="square top left"></span>
<span class="square top right"></span>
<span class="square bottom right"></span>
<span class="square bottom left"></span>
<p class="tips">请将二维码放入框内</p>
</div>
<video
v-show="showPlay"
class="source"
ref="video"
:width="videoWH.width"
:height="videoWH.height"
controls
></video>
<canvas v-show="!showPlay" ref="canvas" />
<button v-show="showPlay" @click="run">开始</button>
</div>
</template>
<script>
import jsQR from 'jsqr'
export default {
name: 'Scaner',
props: {
// 使用后置相机
useBackCamera: {
type: Boolean,
default: true
},
// 扫描识别后停止
stopOnScaned: {
type: Boolean,
default: true
},
drawOnfound: {
type: Boolean,
default: true
},
// 线条颜色
lineColor: {
type: String,
default: '#03C03C'
},
// 线条宽度
lineWidth: {
type: Number,
default: 2
},
// 视频宽度
videoWidth: {
type: Number,
default: document.documentElement.clientWidth || document.body.clientWidth
},
// 视频高度
videoHeight: {
type: Number,
default: document.documentElement.clientHeight - 48 || document.body.clientHeight - 48
},
responsive: {
type: Boolean,
default: false
}
},
data() {
return {
showPlay: false,
showBanner: true,
containerWidth: null,
active: false
}
},
computed: {
videoWH() {
if (this.containerWidth) {
const width = this.containerWidth
const height = width * 0.75
return {
width, height }
}
return {
width: this.videoWidth, height: this.videoHeight }
}
},
watch: {
active: {
immediate: true,
handler(active) {
if (!active) {
this.fullStop()
}
}
}
},
methods: {
// 关闭摄像机
closeScaner() {
this.$emit('closeScaner', false)
},
// 画线
drawLine(begin, end) {
this.canvas.beginPath()
this.canvas.moveTo(begin.x, begin.y)
this.canvas.lineTo(end.x, end.y)
this.canvas.lineWidth = this.lineWidth
this.canvas.strokeStyle = this.lineColor
this.canvas.stroke()
},
// 画框
drawBox(location) {
if (this.drawOnfound) {
this.drawLine(location.topLeftCorner, location.topRightCorner)
this.drawLine(location.topRightCorner, location.bottomRightCorner)
this.drawLine(location.bottomRightCorner, location.bottomLeftCorner)
this.drawLine(location.bottomLeftCorner, location.topLeftCorner)
}
},
tick() {
if (this.$refs.video && this.$refs.video.readyState === this.$refs.video.HAVE_ENOUGH_DATA) {
this.$refs.canvas.height = this.videoWH.height
this.$refs.canvas.width = this.videoWH.width
this.canvas.drawImage(this.$refs.video, 0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
const imageData = this.canvas.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height)
let code = false
try {
code = jsQR(imageData.data, imageData.width, imageData.height)
} catch (e) {
console.error(e)
}
if (code) {
this.drawBox(code.location)
this.found(code.data)
}
}
this.run()
},
// 初始化
setup() {
if (this.responsive) {
this.$nextTick(() => {
this.containerWidth = this.$refs.scaner.clientWidth
})
}
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
this.previousCode = null
this.parity = 0
this.active = true
this.canvas = this.$refs.canvas.getContext('2d')
const facingMode = this.useBackCamera ? {
exact: 'environment' } : 'user'
const handleSuccess = stream => {
if (this.$refs.video.srcObject !== undefined) {
this.$refs.video.srcObject = stream
} else if (window.videoEl.mozSrcObject !== undefined) {
this.$refs.video.mozSrcObject = stream
} else if (window.URL.createObjectURL) {
this.$refs.video.src = window.URL.createObjectURL(stream)
} else if (window.webkitURL) {
this.$refs.video.src = window.webkitURL.createObjectURL(stream)
} else {
this.$refs.video.src = stream
}
this.$refs.video.playsInline = true
const playPromise = this.$refs.video.play()
playPromise.catch(() => (this.showPlay = true))
playPromise.then(this.run)
}
navigator.mediaDevices
.getUserMedia({
video: {
facingMode } })
.then(handleSuccess)
.catch(() => {
navigator.mediaDevices
.getUserMedia({
video: true })
.then(handleSuccess)
.catch(error => {
this.$emit('error-captured', error)
})
})
}
},
run() {
if (this.active) {
requestAnimationFrame(this.tick)
}
},
found(code) {
if (this.previousCode !== code) {
this.previousCode = code
} else if (this.previousCode === code) {
this.parity += 1
}
if (this.parity > 2) {
this.active = this.stopOnScanned ? false : true
this.parity = 0
this.$emit('code-scanned', code)
}
},
// 完全停止
fullStop() {
if (this.$refs.video && this.$refs.video.srcObject) {
this.$refs.video.srcObject.getTracks().forEach(t => t.stop())
}
}
},
mounted() {
this.setup()
},
beforeDestroy() {
this.fullStop()
}
}
</script>
<style lang="css" scoped>
.close {
display: inline-block;
height: 80px;
width: 80px;
background: url('@/assets/image/close.png') no-repeat center;
background-size: auto 100%;
position: absolute;
left: 2px;
top: 5px;
}
.scaner {
background: #000000;
position: fixed;
top: 90px;
left: 0;
width: 100%;
height: 100%;
height: -webkit-calc(100%);
height: -moz-calc(100%);
height: -ms-calc(100%);
height: -o-calc(100%);
height: calc(100%);
}
.scaner .banner {
width: 340px;
position: absolute;
top: 16px;
left: 50%;
margin-left: -170px;
background: #fa74a2;
border-radius: 8px;
box-sizing: border-box;
padding: 12px;
opacity: 0.9;
box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.2);
}
.scaner .banner .text {
padding: 0;
margin: 0;
color: #ffffff;
font-size: 12px;
text-align: justify;
text-align-last: left;
}
.scaner .banner .close_icon {
display: inline-block;
height: 24px;
width: 24px;
background: url('@/assets/image/close.png') no-repeat center;
background-size: auto 100%;
position: absolute;
right: 2px;
top: -3px;
}
.scaner .cover {
height: 300px;
width: 320px;
position: absolute;
top: 30%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
/* border: 0.5px solid #999999; */
z-index: 1111;
}
.scaner .cover .line {
width: 300px;
height: 1px;
margin-left: 10px;
background: #5f68e8;
background: linear-gradient(to right, transparent, #5f68e8, #0165ff, #5f68e8, transparent);
position: absolute;
-webkit-animation: scan 1.75s infinite linear;
-moz-animation: scan 1.75s infinite linear;
-ms-animation: scan 1.75s infinite linear;
-o-animation: scan 1.75s infinite linear;
animation: scan 1.75s infinite linear;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
-ms-animation-fill-mode: both;
-o-animation-fill-mode: both;
animation-fill-mode: both;
border-radius: 1px;
}
.scaner .cover .square {
display: inline-block;
height: 20px;
width: 20px;
position: absolute;
}
.scaner .cover .square.top {
top: 0;
border-top: 1px solid #5f68e8;
}
.scaner .cover .square.left {
left: 0;
border-left: 1px solid #5f68e8;
}
.scaner .cover .square.bottom {
bottom: 0;
border-bottom: 1px solid #5f68e8;
}
.scaner .cover .square.right {
right: 0;
border-right: 1px solid #5f68e8;
}
.scaner .cover .tips {
position: absolute;
bottom: -70px;
width: 100%;
text-align: center;
font-size: 13px;
color: #ffffff;
opacity: 0.8;
}
@-webkit-keyframes scan {
0% {
top: 0;
}
25% {
top: 50px;
}
50% {
top: 100px;
}
75% {
top: 150px;
}
100% {
top: 200px;
}
}
@-moz-keyframes scan {
0% {
top: 0;
}
25% {
top: 50px;
}
50% {
top: 100px;
}
75% {
top: 150px;
}
100% {
top: 200px;
}
}
@-o-keyframes scan {
0% {
top: 0;
}
25% {
top: 50px;
}
50% {
top: 100px;
}
75% {
top: 150px;
}
100% {
top: 200px;
}
}
@keyframes scan {
0% {
top: 0;
}
25% {
top: 50px;
}
50% {
top: 100px;
}
75% {
top: 150px;
}
100% {
top: 200px;
}
}
</style>
使用
<!-- 扫码组件 -->
<div v-if="showSaner">
<Scaner
@code-scanned="codeScanned"
@error-captured="errorCaptured"
@closeScaner="closeScaner"
:stop-on-scanned="true"
:draw-on-found="true"
:responsive="false"
/>
</div>
// 扫码结果
codeScanned(code) {
setTimeout(() => {
this.formObj.apply_code = code
this.showSaner = false
Toast.success('扫码成功!')
}, 200)
},
// 捕获异常
errorCaptured(error) {
switch (error.name) {
case 'NotAllowedError':
this.errorMessage = 'Camera permission denied.'
break
case 'NotFoundError':
this.errorMessage = 'There is no connected camera.'
break
case 'NotSupportedError':
this.errorMessage = 'Seems like this page is served in non-secure context.'
break
case 'NotReadableError':
this.errorMessage = "Couldn't access your camera. Is it already in use?"
break
case 'OverconstrainedError':
this.errorMessage = "Constraints don't match any installed camera."
break
default:
this.errorMessage = 'UNKNOWN ERROR: ' + error.message
}
console.error(this.errorMessage)
alert('相机调用失败')
}
// 关闭扫码
closeScaner(is) {
// this.showNavbar = true
this.showSaner = is
},