前言:新年接到公司的第一个需求就是要写一个H5,里面类似电商那种扫描枪之类的,用H5扫条形码拿到其中的信息,初步实现是想搞简单一点,直接调H5的相机,把拍到的照片传给后端做解析,但是这样做效果并不好,后来还是决定让前端来做扫描。

H5直接调用摄像头

  • 代码如下(类名是Tailwind CSS)
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的信息扫描
    • 拍照之后需要将拍到的照片发给后端进行解析
    • 虽然效果不好,但是这样做的话前端是实现是最简单的。

通过三方库实现条形码扫描

  1. 这里我有用到两个三方库,一个是html5-qrcode,而另外一个是@zxing/library
  2. 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>
<!-- 掃碼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的国际化,这里补上相关国际化的文本

1
2
3
4
5
6
7
8
9
10
11
hint: {
inputWaybillNum: '請輸入運單號',
inputGoodsSearch: '請輸入商品名稱/代碼',
copySuc: '複製成功',
copyFail: '複製失敗',
addFailCaseByLeftFull: '新增失敗,倉庫暫無剩餘了',
tryToScan: '正在嘗試掃描...',
scanFail: '掃描識別失敗',
callingCamera: '正在調用攝像頭...',
ScanResults: '掃描結果'
}

实现环境,重点部分

  1. 上面这段代码只能在localhost或者是https环境下执行,但是一般我们本地调试的话,都是通过内网,电脑和手机连同一个WiFi或者局域网,实现手机真机调试H5。
  2. 局域网一般都是http协议为多,所以要调试的话需要对浏览器进行安全性白名单放行和调整。
  3. 解决方案也不难,可以参考一下https://blog.csdn.net/qq_40905132/article/details/126520190
  4. 简单来说就是浏览器输入: chrome://flags/ ,然后Ctrl + F 查:Insecure origins treated as secure
  5. 查到之后在输入框把要白名单的http链接输入之后,按右边的按钮改为Enabled就行了

另外一种方案

  1. 第二种方案可以用html5-qrcode来实现,但是实现效果貌似没有第一种好。
  2. 首先安装html5-qrcode
1
2
3
4
5
npm install html5-qrcode --save

pnpm install html5-qrcode --save

yarn add html5-qrcode
  1. 然后是参考代码
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) {
// 如果有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>