前言:新年接到公司的第一个需求就是要写一个H5,里面类似电商那种扫描枪之类的,用H5扫条形码拿到其中的信息,初步实现是想搞简单一点,直接调H5的相机,把拍到的照片传给后端做解析,但是这样做效果并不好,后来还是决定让前端来做扫描。
H5直接调用摄像头
- 代码如下(类名是Tailwind CSS)
html
<div class="flex relative w-27 h-27">
<input type="file" accept="image/*" capture="user" class="w-27 h-27 z-10 opacity-0">
<van-icon class="cursor-pointer scanCode" size="27" style="font-weight: bold;" name="scan" @click="scanCode" />
</div>
-
主要实现功能的部分还是下面这一段
html<input type="file" accept="image/*" capture="user" class="w-27 h-27 z-10 opacity-0">```
-
解释:通过将其设为0的透明度,配合van-icon的那个scan图标,将其叠加在图标上面,可以实现点击图标,拉起H5的相机。
- 这样做的效果可以实现拍照,但是并不能实现二维码或者是条形码Bar Code的信息扫描
- 拍照之后需要将拍到的照片发给后端进行解析
- 虽然效果不好,但是这样做的话前端是实现是最简单的。
通过三方库实现条形码扫描
- 这里我有用到两个三方库,一个是
html5-qrcode
,而另外一个是@zxing/library
。 html5-qrcode
的效果整体还是不如@zxing/library
,而且相关实现的代码,也是@zxing/library
比较多,去网上搜的话,所以这里我还是用@zxing/library
用于实现该功能
开发环境:Vue3.3 + Vite5 + Vant + zxing/library
安装zxing/library
npm install @zxing/library --save
pnpm install @zxing/library --save
yarn add @zxing/library
- 参考代码:
html
<template>
<div class="page-scan">
<!--返回-->
<van-nav-bar :title="$t('title.scanBarCode')" fixed @click-left="clickIndexLeft()" class="scan-index-bar">
<template #left>
<van-icon name="arrow-left" size="18" color="#fff" />
<span style="color: #fff"> {{ $t('text.cancel') }} </span>
</template>
</van-nav-bar>
<!-- 扫码区域 -->
<video ref="video" id="video" class="scan-video" autoplay></video>
<!-- 提示语 -->
<div v-show="tipShow" class="scan-tip"> {{ tipMsg }} </div>
<!-- 掃碼Text -->
<div v-show="!tipShow" class="scan-tip"> {{ scanText }} </div>
</div>
</template>
<script lang="ts" setup>
import { ref, onUnmounted, onBeforeMount } from 'vue';
import { BrowserMultiFormatReader } from '@zxing/library';
import { useI18n } from 'vue-i18n';
import router from '@/router';
const { t } = useI18n();
const scanText = ref()
const decodeFromInputVideoFunc = (firstDeviceId) => {
codeReader.value.reset(); // 重置
scanText.value = '';
codeReader.value.decodeFromInputVideoDeviceContinuously(firstDeviceId, 'video', (result: any, err: string) => {
tipMsg.value = t('hint.tryToScan');
scanText.value = '';
if (result) {
console.log('扫描结果', result);
scanText.value = result.text;
if (scanText.value) {
tipShow.value = false;
// 这部分接下去的代码根据需要,读者自行编写了
// this.$store.commit('app/SET_SCANTEXT', result.text);
// console.log('已扫描的小票列表', this.$store.getters.scanTextArr);
}
}
if (err && !(err)) {
tipMsg.value = t('hint.scanFail');
setTimeout(() => {
tipShow.value = false;
}, 2000)
console.error(err);
}
});
}
const tipMsg = ref(t('hint.callingCamera'))
const tipShow = ref(false)
const codeReader: any = ref(null);
const openScan = async () => {
console.log('codeReader', codeReader.value)
codeReader.value.getVideoInputDevices().then((videoInputDevices: any) => {
tipShow.value = true;
tipMsg.value = t('hint.callingCamera');
// 默认获取第一个摄像头设备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;
}
}
decodeFromInputVideoFunc(firstDeviceId);
}).catch((err: string) => {
tipShow.value = false;
console.error(err);
});
}
const openScanTwo = async () => {
codeReader.value = await new BrowserMultiFormatReader();
codeReader.value.getVideoInputDevices().then((videoInputDevices) => {
tipShow.value = true;
tipMsg.value = t('hint.callingCamera');
// 默认获取第一个摄像头设备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;
}
}
decodeFromInputVideoFunc(firstDeviceId);
}).catch((err: string) => {
tipShow.value = false;
console.error(err);
});
}
const clickIndexLeft = () => { // 返回上一页
codeReader.value = null;
router.back();
}
onBeforeMount(() => {
codeReader.value = new BrowserMultiFormatReader();
openScan();
})
onUnmounted(() => {
// codeReader.value.reset();
console.log("销毁组件");
})
</script>
<style lang="scss" scoped>
.scan-index-bar {
background-image: linear-gradient(-45deg, #42a5ff, #59cfff);
}
.van-nav-bar__title {
color: #fff !important;
}
.scan-video {
display: flex;
flex: 1;
}
.scan-tip {
margin: 10px 0;
width: 100%;
text-align: center;
color: white;
font-size: 5vw;
}
.page-scan {
display: flex;
flex-direction: column;
overflow-y: hidden;
background-color: #363636;
}
</style>
上面这里有用到i18n的国际化,这里补上相关国际化的文本
javascript
hint: {
inputWaybillNum: '請輸入運單號',
inputGoodsSearch: '請輸入商品名稱/代碼',
copySuc: '複製成功',
copyFail: '複製失敗',
addFailCaseByLeftFull: '新增失敗,倉庫暫無剩餘了',
tryToScan: '正在嘗試掃描...',
scanFail: '掃描識別失敗',
callingCamera: '正在調用攝像頭...',
ScanResults: '掃描結果'
}
实现环境,重点部分
- 上面这段代码只能在localhost或者是https环境下执行,但是一般我们本地调试的话,都是通过内网,电脑和手机连同一个WiFi或者局域网,实现手机真机调试H5。
- 局域网一般都是http协议为多,所以要调试的话需要对浏览器进行安全性白名单放行和调整。
- 解决方案也不难,可以参考一下https://blog.csdn.net/qq_40905132/article/details/126520190
- 简单来说就是浏览器输入: chrome://flags/ ,然后Ctrl + F 查:Insecure origins treated as secure
- 查到之后在输入框把要白名单的http链接输入之后,按右边的按钮改为Enabled就行了
另外一种方案
- 第二种方案可以用html5-qrcode来实现,但是实现效果貌似没有第一种好。
- 首先安装html5-qrcode
npm install html5-qrcode --save
pnpm install html5-qrcode --save
yarn add html5-qrcode
- 然后是参考代码
html
<template>
<div class="container">
<div id="reader"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { Html5Qrcode } from 'html5-qrcode';
import { Html5QrcodeResult, CameraDevice } from '../../../type';
let cameraId = ref('');
let devicesInfo = ref<any>('');
let html5QrCode: any = ref<any>(null);
const router = useRouter();
onMounted(() => {
getCameras();
});
onUnmounted(() => {
stop();
});
const getCameras = () => {
Html5Qrcode.getCameras()
.then((devices: CameraDevice[]) => {
console.log('摄像头信息', devices);
if (devices && devices.length) {
// 如果有2个摄像头,1为前置的
if (devices.length > 1) {
cameraId.value = devices[1].id;
} else {
cameraId.value = devices[0].id;
}
devicesInfo.value = devices;
// start开始扫描
start();
}
})
.catch((err) => {
// handle err
console.log('获取设备信息失败', err); // 获取设备信息失败
});
};
const start = () => {
html5QrCode = new Html5Qrcode('reader');
html5QrCode
.start(
cameraId.value, // retreived in the previous step.
{
fps: 10, // 设置每秒多少帧
qrbox: { width: 250, height: 250 }, // 设置取景范围
// scannable, rest shaded.
},
(decodedText: string, decodedResult: Html5QrcodeResult) => {
console.log('扫描的结果', decodedText, decodedResult);
},
(errorMessage: any) => {
console.log('暂无额扫描结果', errorMessage);
}
)
.catch((err: any) => {
console.log(`Unable to start scanning, error: ${err}`);
});
};
const stop = () => {
html5QrCode
.stop()
.then((ignore: any) => {
// QR Code scanning is stopped.
console.log('QR Code scanning stopped.', ignore);
})
.catch((err: any) => {
// Stop failed, handle it.
console.log('Unable to stop scanning.', err);
});
};
</script>
<style lang="scss" scoped>
.container {
position: relative;
height: 100%;
width: 100%;
max-width: 100%;
background: rgba($color: #000000, $alpha: 0.48);
}
#reader {
top: 50%;
left: 0;
transform: translateY(-50%);
}
</style>
目录