Html5利用设备摄像头扫描条形码/二维码

使用的是开源项目Html5Qrcode,自带一套UI,开箱即用,有点丑。还提供核心方法,包括获取摄像头权限,获取设备列表,获取设备能力,扫码解码,基于上述方法,整了个javascript版和React版,可以实现跨平台,跨浏览器无差别扫码,识别效率与正确率还OK。当然还有些付费的项目,像DynamSoft做的就比较炫酷。

javascript版手机端网页效果图
React组件手机端网页效果图

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;

Leave a Comment

Your email address will not be published.

*

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据