在 Vue 中使用 XZing 处理二维码
XZing 是一个开源的条码处理库,我们这里简单介绍他的 js 版本,仓库地址:https://github.com/zxing-js/library
我们主要使用 XZing 的三个功能:
- 单张图片解码
- 视频动态解码
- 绘制二维码
安装与引入
使用 npm 安装:
npm i @zxing/browser
在 Vue 中可以直接导入对应的组件:
import { BrowserMultiFormatReader } from '@zxing/browser'
通过图片解码
官方文档中已经列举了所有的扫描方法,通过图片解码常用到以下两个:
decodeFromImageElement()
:需要传入一个 HTML 元素,可以是 id ,也可以是 Vue 的 ref 属性decodeFromImageUrl()
:需要传入一个 URL,可以是一个从设备中选择的文件,也可以是一个网址
对于第一种方式不再赘述,从设备选择图片可以使用 FileReader,下面给出一个 Demo 代码:
<script setup>
import { ref } from 'vue'
import { BrowserMultiFormatReader } from '@zxing/browser'
const codeReader = new BrowserMultiFormatReader()
const resultFromDevice = ref('暂未获取到信息')
const resultFromElement = ref('暂未获取到信息')
const selectedImage = ref(null)
const imgElement = ref(null)
const scanningImgFromDevice = () => {
if (!selectedImage.value) {
resultFromDevice.value = '请先选择一张图片。'
return
}
codeReader
.decodeFromImageUrl(selectedImage.value)
.then(decodeResult => {
console.log(decodeResult)
resultFromDevice.value = decodeResult.text
})
.catch(err => {
console.error(err)
resultFromDevice.value = '解码失败,请检查图片。'
})
}
const scanningImgElement = () => {
codeReader
.decodeFromImageElement(imgElement.value)
.then(decodeResult => {
console.log(decodeResult)
resultFromElement.value = decodeResult.text
})
.catch(err => {
console.error(err)
resultFromElement.value = '解码失败,请检查图片。'
})
}
const resetScanning = () => {
resultFromDevice.value = '暂未获取到信息'
resultFromElement.value = '暂未获取到信息'
selectedImage.value = null
console.log('Reset.')
}
const handleFileChange = event => {
const file = event.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = e => {
selectedImage.value = e.target.result // 获取图片的 base64 数据
}
reader.readAsDataURL(file) // 读取图片为 base64 格式
}
}
</script>
<template>
<h1>通过图片扫描 1D/2D 码</h1>
<h2>扫描 HTML 元素:</h2>
<div class="options-box">
<div class="option-item" @click="resetScanning">重置</div>
<div class="option-item" @click="scanningImgElement">扫描下方示例图片</div>
</div>
<h3>示例图片</h3>
<div class="demo-box">
<div class="left-show">
<img
ref="imgElement"
src="../assets/img/test-qr-code.png"
style="width: 150px; height: 150px"
/>
</div>
<div class="divider"></div>
<div class="right-result">
<label>结果:</label>
<div>{{ resultFromElement }}</div>
</div>
</div>
<h2>从设备获取:</h2>
<div class="options-box">
<div class="option-item" @click="resetScanning">重置</div>
<div class="option-item" @click="scanningImgFromDevice">
扫描从设备获取的图片
</div>
</div>
<input type="file" accept="image/*" @change="handleFileChange" />
<div class="demo-box">
<div class="left-show">
<img
v-if="selectedImage"
:src="selectedImage"
alt="选择的图片"
style="width: 150px; height: 150px"
/>
</div>
<div class="divider"></div>
<div class="right-result">
<label>结果:</label>
<div>{{ resultFromDevice }}</div>
</div>
</div>
</template>
<style scoped>
h1 {
padding: 10px 0;
font-size: 24px;
font-weight: bold;
text-align: center;
}
h2 {
padding: 10px 0 0 0;
font-size: 20px;
font-weight: bold;
text-align: center;
}
.demo-box {
border: 1px solid #000;
border-radius: 20px;
overflow: hidden;
width: 90%;
height: 150px;
padding: 10px;
display: flex;
justify-content: flex-start;
}
.divider {
border: 1px solid #000;
margin: 5px;
}
.left-show {
width: 150px;
display: flex;
justify-content: center;
align-items: center;
}
.options-box {
width: 90%;
margin-top: 10px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.option-item {
height: 20px;
padding: 10px 20px;
margin: 5px 10px;
background-color: #f68512;
color: white;
font-weight: bold;
border-radius: 10px;
cursor: pointer;
text-align: center;
}
</style>
通过视频解码
通过视频解码需要指定摄像头设备,需要先通过 listVideoInputDevices()
获取所有摄像头设备,然后向 decodeFromVideoDevice
或其他 API 中传入这个 ID 即可,如果想要在页面上显示,可以再指定一个 HTML 元素。
下面给出一个 Demo 代码:
<script setup>
import { ref, onMounted } from 'vue'
import { BrowserMultiFormatReader } from '@zxing/browser'
const video = ref(null)
const codeReader = new BrowserMultiFormatReader()
const videoInputDevices = ref([])
const selectedDeviceId = ref(null)
const result = ref('暂未获取到信息')
let controls
onMounted(async () => {
await getVideoInputDevices()
})
const getVideoInputDevices = async () => {
try {
// 请求摄像头权限
const devices = await BrowserMultiFormatReader.listVideoInputDevices()
videoInputDevices.value = devices
if (devices.length > 0) {
selectedDeviceId.value = devices[0].deviceId // 选择第一个设备
}
} catch (err) {
console.error(err)
result.value = '无法获取视频输入设备,请检查权限设置。'
}
}
const startScanning = async () => {
controls = await codeReader.decodeFromVideoDevice(
selectedDeviceId.value,
video.value,
(resultData, err) => {
if (resultData) {
result.value = resultData.text
console.log(resultData)
}
if (err) {
// 检查错误信息是否包含 "NotFoundException"
if (err.name === 'NotFoundException2') {
// result.value = '未找到二维码,请检查图片或二维码质量。'
} else {
result.value = '解码失败: ' + err.message // 其他错误信息
console.error(err)
}
}
},
)
console.log(
`Started continuous decode from camera with id ${selectedDeviceId.value}`,
)
}
const resetScanning = () => {
controls.stop()
result.value = ''
console.log('Reset.')
}
</script>
<template>
<div style="display: flex; flex-direction: column; align-items: center">
<h1>通过视频扫描 1D/2D 码</h1>
<div class="options-box">
<div class="option-item" @click="startScanning">开始</div>
<div class="option-item" @click="resetScanning">重置</div>
</div>
<video
ref="video"
style="border: 1px solid gray; width: 300px; height: 400px"
></video>
<div v-if="videoInputDevices.length > 0">
<label for="sourceSelect">选择摄像头:</label>
<select id="sourceSelect" v-model="selectedDeviceId">
<option
v-for="device in videoInputDevices"
:key="device.deviceId"
:value="device.deviceId"
>
{{ device.label }}
</option>
</select>
</div>
<label>结果:</label>
<div>{{ result }}</div>
</div>
</template>
<style scoped>
h1 {
padding: 10px 0;
font-size: 24px;
font-weight: bold;
text-align: center;
}
h2 {
padding: 10px 0 0 0;
font-size: 20px;
font-weight: bold;
}
.demo-box {
border: 1px solid #000;
border-radius: 20px;
overflow: hidden;
width: 90%;
height: 150px;
padding: 10px;
display: flex;
justify-content: flex-start;
}
.divider {
border: 1px solid #000;
margin: 5px;
}
.left-show {
width: 150px;
display: flex;
justify-content: center;
align-items: center;
}
.options-box {
width: 90%;
margin-top: 10px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.option-item {
height: 20px;
padding: 10px 20px;
margin: 5px 10px;
background-color: #f68512;
color: white;
font-weight: bold;
border-radius: 10px;
cursor: pointer;
text-align: center;
}
</style>
绘制二维码
这里引入 file-saver
与 html2canvas
用于保存生成的二维码,如果仅用于网页展示则不需要该插件。
安装命令:
npm i file-saver html2canvas
<script setup>
import { ref } from 'vue'
import { BrowserQRCodeSvgWriter } from '@zxing/browser'
import { saveAs } from 'file-saver'
import html2canvas from 'html2canvas' // 引入 html2canvas
const codeWriter = new BrowserQRCodeSvgWriter()
const input = ref('')
const result = ref(null)
function drawQRCode() {
if (result.value) {
while (result.value.firstChild) {
result.value.removeChild(result.value.firstChild)
}
}
codeWriter.writeToDom(result.value, input.value, 300, 300)
}
function saveQRCode(format = 'png') {
if (result.value) {
html2canvas(result.value).then(canvas => {
canvas.toBlob(
blob => {
const fileName = `zxing-qrcode-example.${format}`
saveAs(blob, fileName)
},
format === 'png' ? 'image/png' : 'image/jpeg',
)
})
}
}
function resetQRCode() {
input.value = ''
if (result.value) {
while (result.value.firstChild) {
result.value.removeChild(result.value.firstChild)
}
}
}
</script>
<template>
<h1>生成并保存 QR 码</h1>
<div class="options-box">
<div class="option-item" @click="drawQRCode">生成</div>
<div class="option-item" @click="() => saveQRCode('png')">保存为 PNG</div>
<div class="option-item" @click="() => saveQRCode('jpg')">保存为 JPG</div>
<div class="option-item" @click="resetQRCode">重置</div>
</div>
<div class="input-box">
<textarea v-model="input"></textarea>
</div>
<div ref="result" class="result"></div>
</template>
<style scoped>
textarea {
width: 80%;
height: 200px;
resize: none;
border-radius: 10px;
margin: 10px;
padding: 10px;
border: 3px solid #f68512;
font-size: 18px;
}
.input-box {
width: 100%;
display: flex;
justify-content: center;
}
h1 {
padding: 10px 0;
font-size: 24px;
font-weight: bold;
text-align: center;
}
.options-box {
width: 90%;
margin-top: 10px;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.option-item {
height: 20px;
padding: 10px 20px;
margin: 5px 10px;
background-color: #f68512;
color: white;
font-weight: bold;
border-radius: 10px;
cursor: pointer;
text-align: center;
}
</style>