前言:新年接到公司的第一个需求就是要写一个H5,里面类似电商那种扫描枪之类的,用H5扫条形码拿到其中的信息,初步实现是想搞简单一点,直接调H5的相机,把拍到的照片传给后端做解析,但是这样做效果并不好,后来还是决定让前端来做扫描。
H5直接调用摄像头
1 2 3 4
| <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>
|
- 主要实现功能的部分还是下面这一段
1
| <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
1 2 3 4 5
| npm install @zxing/library --save
pnpm install @zxing/library --save
yarn add @zxing/library
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
| <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> <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; } } 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'); 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'); 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(() => { 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的国际化,这里补上相关国际化的文本
1 2 3 4 5 6 7 8 9 10 11
| 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
1 2 3 4 5
| npm install html5-qrcode --save
pnpm install html5-qrcode --save
yarn add html5-qrcode
|
- 然后是参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| <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) { if (devices.length > 1) { cameraId.value = devices[1].id; } else { cameraId.value = devices[0].id; } devicesInfo.value = devices; start(); } }) .catch((err) => { console.log('获取设备信息失败', err); }); }; const start = () => { html5QrCode = new Html5Qrcode('reader'); html5QrCode .start( cameraId.value, { fps: 10, qrbox: { width: 250, height: 250 }, }, (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) => { console.log('QR Code scanning stopped.', ignore); }) .catch((err: any) => { 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>
|