使用的是开源项目Html5Qrcode,自带一套UI,开箱即用,有点丑。还提供核心方法,包括获取摄像头权限,获取设备列表,获取设备能力,扫码解码,基于上述方法,整了个javascript版和React版,可以实现跨平台,跨浏览器无差别扫码,识别效率与正确率还OK。当然还有些付费的项目,像DynamSoft做的就比较炫酷。
javascript版,演示地址
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>二维码/条形码扫描 - 迦楠</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui"/>
<meta name="description" content="二维码/条形码扫描 - QRCode/BarCode Scan"/>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css" rel="stylesheet">
<script src="html5-qrcode.min.js"></script>
</head>
<body>
<div class="container-fluid d-flex flex-column align-items-center p-3">
<div class="card" style="width: 20rem;">
<div id="reader" class="card-img-top" style="min-height: 300px; background-color: rgba(0,0,0,0.48)"></div>
<div class="card-body">
<label for="barcodes" class="form-label">扫码结果</label>
<input type="text" id="barcodes" class="form-control mb-3"/>
<label for="cameras" class="form-label">摄像头</label>
<select class="form-control mb-3" id="cameras"></select>
</div>
<div class="mb-3 text-center">
<button type="button" id="scan" class="btn btn-primary btn-block">SCAN</button>
<button type="button" id="stop" class="btn btn-primary btn-block">STOP</button>
</div>
</div>
<div>Powered By <a href="https://github.com/mebjas/html5-qrcode" target="_blank"><strong>Html5QRCode</strong></a> </div>
</div>
<script>
const codeScan = {
cameraId: null,
html5QrCode: null,
getCameras() {
Html5Qrcode.getCameras()
.then((devices) => {
if (devices && devices.length) {
devices.forEach((camera) => {
const option = document.createElement("option");
option.value = camera.id;
option.label = camera.label;
document.querySelector('#cameras').append(option);
})
// 如果有1个摄像头,直接开始扫描,多个摄像头选择后手动开始扫描
if (devices.length === 1) {
this.cameraId = devices[0].id
this.start()
}
}
})
.catch((err) => {
console.log(err) // 获取设备信息失败
})
},
start() {
this.html5QrCode = new Html5Qrcode('reader')
this.html5QrCode
.start(
this.cameraId, // retreived in the previous step.
{
fps: 10, // 扫码的帧数
qrbox: 220 // 扫码的框
},
(decodedText, decodedResult) => {
console.log(decodedText, decodedResult);
if (decodedText) {
/// decodedText, decodedResult 返回的数据
document.querySelector('#barcodes').value = decodedText + "\r\n";
this.stop()
}
},
(errorMessage) => {
console.log(errorMessage)
})
.catch((err) => {
console.log(`Unable to start scanning, error: ${err}`)
})
},
stop() {
this.html5QrCode
.stop()
.then((ignore) => {
// 隐藏扫码的dom
console.log('QR Code scanning stopped.')
})
.catch(() => {
console.log('Unable to stop scanning.')
})
},
};
codeScan.getCameras();
document.querySelector('#cameras').onchange = function () {
codeScan.stop();
}
document.querySelector('#scan').onclick = async function () {
codeScan.cameraId = document.querySelector('#cameras').value;
codeScan.start();
};
document.querySelector('#stop').onclick = () => {
codeScan.stop();
}
document.querySelector('#barcodes').onkeydown = (e) => {
console.log(e.keyCode);
}
</script>
</body>
</html>
React组件版
import tool from "@/services/tool";
import { CameraFilled } from "@ant-design/icons";
import { Button, Card, Modal, Select, Space } from "antd";
import type { Html5QrcodeCameraScanConfig } from "html5-qrcode";
import { Html5Qrcode, Html5QrcodeScannerState } from "html5-qrcode";
import React, { useEffect, useState } from "react";
const Html5QRCode = (props: {
trigger?: React.ReactNode;
onClose?: () => void;
onScanSuccess: (codeText: string) => void;
onScanFailure?: (error: any) => void;
autoStartOnOneCamera?: boolean;
autoStopWhenScanSuccess?: boolean;
cameraConfig?: Html5QrcodeCameraScanConfig;
}) => {
const [open, setOpen] = useState<boolean>(false);
const [codeScan, setCodeScan] = useState<Html5Qrcode | undefined>(undefined);
const [devices, setDevices] = useState<API.OptionItem[]>([]);
const [cameraId, setCameraId] = useState<string>("");
const stop = () => {
if (codeScan?.getState() === Html5QrcodeScannerState.SCANNING) {
codeScan
?.stop()
.then((ignore) => {
// 隐藏扫码的dom
console.log("QR Code scanning stopped.");
})
.catch(() => {
console.log("Unable to stop scanning.");
});
}
};
const close = () => {
setOpen(false);
props?.onClose?.();
};
const start = (camera_id: string) => {
codeScan
?.start(
camera_id,
{ fps: 10, qrbox: 180, ...props?.cameraConfig },
(decodedText) => {
if (decodedText) {
if (props?.autoStopWhenScanSuccess ?? true) {
stop();
close();
}
props.onScanSuccess(decodedText);
}
},
(errorMessage) => {
// console.log(errorMessage);
props?.onScanFailure?.(errorMessage);
}
)
.catch((err) => {
console.log(`Unable to start scanning, error: ${err}`);
});
};
useEffect(() => {
// 如果有1个摄像头,直接开始扫描,多个摄像头选择后手动开始扫描
if (open && devices) {
let deviceId = undefined;
if (devices.length === 1) {
deviceId = devices[0].value;
} else if (tool.local.get("cameraId")) {
deviceId = tool.local.get("cameraId")!;
}
if (deviceId) {
setCameraId(deviceId);
if (props?.autoStartOnOneCamera ?? true) {
start(deviceId);
}
}
}
}, [devices, open]);
return (
<>
<span
onClick={() => {
setOpen(true);
}}
style={{ cursor: "pointer" }}
>
{props?.trigger ?? <CameraFilled />}
</span>
<Modal
title={"扫码"}
width={"20rem"}
open={open}
afterOpenChange={(state) => {
if (state) {
Html5Qrcode.getCameras()
.then((cameraDevices) => {
if (cameraDevices && cameraDevices.length) {
setDevices(
cameraDevices.map((item) => ({
label: item.label,
value: item.id,
}))
);
if (tool.local.get("cameraId")) {
setCameraId(tool.local.get("cameraId")!);
}
}
})
.catch((err: any) => {
console.log(err); // 获取设备信息失败
});
setCodeScan(new Html5Qrcode("reader"));
} else {
console.log("cancel");
stop();
setCodeScan(undefined);
setDevices([]);
setCameraId("");
}
}}
onCancel={() => close()}
destroyOnClose={true}
maskClosable={false}
footer={false}
>
<Card
cover={
<div
id={"reader"}
style={{ minHeight: 200, backgroundColor: "rgba(0,0,0,0.48)" }}
/>
}
>
<Card.Meta
title={
<Select
style={{ width: "100%" }}
options={devices}
value={cameraId}
onChange={(value) => {
tool.local.set("cameraId", value);
setCameraId(value);
stop();
start(value);
}}
placeholder={"选择相机"}
/>
}
description={
<Space style={{ width: "100%", justifyContent: "center" }}>
<Button onClick={() => start(cameraId)} disabled={!cameraId}>
开始
</Button>
<Button onClick={() => stop()} disabled={!cameraId}>
结束
</Button>
</Space>
}
/>
</Card>
</Modal>
</>
);
};
export default Html5QRCode;