移动端浏览器的扫描二维码实现(vue-qrcode-reader与jsQR方式)

1. 实现功能

类似扫一扫的功能,自动识别到画面中的二维码并进行识别,也可以选择从相册中上传。
在这里插入图片描述
在这里插入图片描述

2. 涉及到的一些插件介绍

vue-qrcode-reader
一组用于检测和解码二维码的Vue.js组件

jsQR
一个纯粹的javascript二维码阅读库,该库接收原始图像,并将定位、提取和解析在其中找到的任何二维码。

zxing-wasm
ZXing-C++ WebAssembly 作为带有类型的 ES/CJS 模块。读/写 web、node、bun 和 deno 中的条形码。

2. vite项目配置本地开发使用https访问

安装basicSsl

pnpm i @vitejs/plugin-basic-ssl
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import basicSsl from '@vitejs/plugin-basic-ssl'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(), basicSsl()],server: {host: '0.0.0.0',https: true},resolve: {alias: {'@': path.resolve(__dirname, 'src')}}
})

3. 方式一:vue-qrcode-reader实现

npm install vue-qrcode-reader

🔍vue-qrcode-reader Api 介绍

<template><div><qrcode-streamclass="qrcode-wrap":torch="torch"v-memo="[torch]":constraints="selectedConstraints":track="paintBoundingBox"@error="onError"@detect="onDetect"@camera-on="onCameraReady"><div v-if="isSupportTorch" class="torch-wrap"><div class="torch" @click="() => (torch = !torch)"><div class="flash-light" v-if="torch"><MdiFlashlightOff style="width: 36px; height: 36px" /></div><div class="flash-light" v-else><MdiFlashlight style="width: 36px; height: 36px" /></div>{{ torch ? "关闭闪光灯" : "打开闪光灯" }}</div></div><div class="photo-wrap"><div class="photo" @click="handleOpenFile"><el-icon size="20"><PictureFilled /></el-icon></div><div class="color-[#fff]">相册</div></div></qrcode-stream></div>
</template>
<script setup lang="ts">
// https://gruhn.github.io/vue-qrcode-reader/api/QrcodeStream.html
import { QrcodeStream } from "vue-qrcode-reader";
import { PictureFilled } from "@element-plus/icons-vue";
import MdiFlashlight from "~icons/mdi/flashlight";
import MdiFlashlightOff from "~icons/mdi/flashlight-off";
import { ElMessage } from "element-plus";
import { fileOpen } from "browser-fs-access";
import _ from "lodash";const error = ref("");
const cameraIsReady = ref(false);
const isSupportTorch = ref(false); // 是否支持闪光灯
const torch = ref(false); // 闪光灯状态
// 相机配置选项: 'user'|'environment' (默认:environment)
const selectedConstraints = ref({ facingMode: "environment" });// 检测到二维码后绘制画布类型
function paintBoundingBox(detectedCodes: any, ctx: CanvasRenderingContext2D) {for (const detectedCode of detectedCodes) {const {boundingBox: { x, y, width, height },} = detectedCode;ctx.lineWidth = 2;ctx.strokeStyle = "#007bff";// 绘制边框矩形ctx.strokeRect(x, y, width, height);}
}async function onCameraReady(capabilities: any) {// NOTE: on iOS we can't invoke `enumerateDevices` before the user has given// camera access permission. `QrcodeStream` internally takes care of// requesting the permissions. The `camera-on` event should guarantee that this// has happened.try {isSupportTorch.value = !!capabilities.torch;cameraIsReady.value = true;error.value = "";} catch (error) {onError(error);cameraIsReady.value = false;}
}
// 错误提示
function onError(err: any) {error.value = `[${err.name}]: `;if (err.name === "NotAllowedError") {error.value += "you need to grant camera access permission";} else if (err.name === "NotFoundError") {error.value += "no camera on this device";} else if (err.name === "NotSupportedError") {error.value += "secure context required (HTTPS, localhost)";} else if (err.name === "NotReadableError") {error.value += "is the camera already in use?";} else if (err.name === "OverconstrainedError") {error.value += "installed cameras are not suitable";} else if (err.name === "StreamApiNotSupportedError") {error.value += "Stream API is not supported in this browser";} else if (err.name === "InsecureContextError") {error.value +="Camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP.";} else {error.value += err.message;}ElMessage.warning(error.value);
}
// 用户摄像头的流后
function onDetect(detectedCodes: any) {if (detectedCodes.length > 0) {onDecode(detectedCodes[0]?.rawValue);}
}const emit = defineEmits(["on-success"]);// 解码(交给父组件处理:进行网络请求)
function onDecode(text: string) {emit("on-success", text);
}
// 文件转成base64
const processFile = async (file: any) => {let reader = new FileReader();reader.readAsDataURL(file);reader.onload = (e) => {let base64String = e.target?.result as string ||'';// 此处可对该base64进行获取赋值传入后端, 如果有直接上传文件的接口就可以直接传文件onDecode(base64String)};
};
// 打开图片选择
async function handleOpenFile() {try {const file = await fileOpen({ mimeTypes: ["image/*"] }).catch(() => null);if (!file) return;// 计算文件的大小const fileSizeMb = _.round(file.size / 1024 / 1024, 2);// 文件大小不能超过 10MBconst limitSizeMb = 10;if (fileSizeMb > limitSizeMb) {return ElMessage.warning(`图片大小限制 ${limitSizeMb}MB`);}processFile(file)} catch (error) {console.log(`[log] - handleOpenUploadIcon - error:`, error);}
}
</script>
<style scoped>
.qrcode-wrap {position: fixed !important;top: 0;right: 0;bottom: 0;left: 0;width: 100vw;height: 100vh;z-index: 1 !important;background: rgba(0, 0, 0, 0.5);
}
.torch-wrap {width: 18.75rem;height: 12.5rem;position: fixed !important;left: 50%;top: 50%;transform: translate(-50%, -30%);z-index: 20;
}.torch {position: fixed;bottom: -6.25rem;left: 50%;transform: translateX(-50%);z-index: 20;color: #fff;display: flex;flex-direction: column;align-items: center;
}
.photo-wrap {position: fixed;bottom: 2.875rem;left: 2.875rem;display: flex;flex-direction: column;align-items: center;gap: 6px;
}.photo {height: 3.125rem;width: 3.125rem;background-color: rgba(250, 250, 250, 0.8);border-radius: 50%;display: grid;place-items: center;cursor: pointer;
}
</style>

browser-fs-access 这个包需要提前下载

pnpm i browser-fs-access

如果需要在离线的环境解析二维码,则需要使用zxing-wasm,在上面文件的js部分添加以下代码:

// 该文件由zxing-wasm项目构建而来
import wasmFile from './zxing_reader.wasm?url';// !为了离线加载
// https://github.com/gruhn/vue-qrcode-reader/issues/354
setZXingModuleOverrides({locateFile: (path: string, prefix: any) => {if (path.endsWith('.wasm')) {return wasmFile;}return prefix + path;},
});

✨ 该部分完整Demo代码在该文件夹下:Demo (二维码实现之vue-qrcode-reader)

4. 方式二:jsQR实现

🔍Canvas的基本介绍与使用

<template><div><div class="canvasBox"><div class="box"><div class="line"></div><div class="angle"></div></div><div v-if="isUseTorch" class="box2"><div class="track" @click="openTrack"><div class="flash-light" v-if="trackStatus"><MdiFlashlightOff style="width: 36px; height: 36px" /></div><div class="flash-light" v-else><MdiFlashlight style="width: 36px; height: 36px" /></div>{{ trackStatus ? "关闭闪光灯" : "打开闪光灯" }}</div></div><div class="photo-wrap"><div class="photo" @click="handleClickFile"><el-icon size="20"><inputclass="hide_file"ref="fileRef"type="file"accept="image/*;"@change="getFile"/><PictureFilled /></el-icon></div><div class="color-[#fff]">相册</div></div></div></div>
</template><script setup lang="ts">
// https://github.com/cozmo/jsQR
import jsQR from "jsqr";
import { PictureFilled } from "@element-plus/icons-vue";
import MdiFlashlight from "~icons/mdi/flashlight";
import MdiFlashlightOff from "~icons/mdi/flashlight-off";
import { ElMessage } from "element-plus";
import _ from "lodash";const props = withDefaults(defineProps<{// environment 后摄像头  user 前摄像头exact?: "environment" | "user";//  whole 全屏  half 半屏size?: "whole" | "half";// 清晰度: fasle 正常  true 高清definition?: boolean;}>(),{exact: "environment",size: "whole",definition: false,}
);
const video = ref();
const canvas2d = ref();
const canvasWidth = ref(520);
const canvasHeight = ref(500);
const c = ref();
const track = ref();
const isUseTorch = ref(false);
const trackStatus = ref(false);
const fileRef = ref();onMounted(() => {const windowWidth = window.screen.availWidth;const windowHeight = window.screen.availHeight;canvasWidth.value = windowWidth;canvasHeight.value = windowHeight;nextTick(() => {video.value = document.createElement("video");c.value = document.createElement("canvas");c.value.id = "c";c.value.width = canvasWidth.value;c.value.height = canvasHeight.value;c.value.style.width = "100%";document.querySelector(".canvasBox")?.append(c.value);openScan();});
});onUnmounted(() => {closeCamera();
});
// 开始扫描
async function openScan() {try {let width = canvasHeight.value;width = props.size === "whole" ? width : width * 0.5;width = props.definition ? width * 1.6 : width;let height = canvasWidth.value;height = props.definition ? height * 1.6 : height;const videoParam = {audio: false,video: {facingMode: { exact: props.exact }, //强制使用摄像头类型width,height,},};// 获取用户摄像头的视频流const stream = await navigator.mediaDevices.getUserMedia(videoParam);if (stream) {video.value.srcObject = stream;video.value.setAttribute("playsinline", true); //内联播放video.value.play();requestAnimationFrame(tick);// 返回所有的媒体内容流的轨道列表track.value = stream.getVideoTracks()?.[0];setTimeout(() => {// 检测摄像头是否支持闪光灯isUseTorch.value = track.value.getCapabilities().torch || null;}, 500);}} catch (error) {ElMessage.warning("设备不支持,请检查是否允许摄像头权限!");console.log("获取本地设备(摄像头)---失败", error);}
}
function closeCamera() {if (video.value.srcObject) {video.value.srcObject.getTracks().forEach((track: any) => {track.stop();});}
}
function tick() {if (video.value.readyState === video.value.HAVE_ENOUGH_DATA) {canvasHeight.value = video.value.videoHeight;canvasWidth.value = video.value.videoWidth;c.value.width = canvasWidth.value;c.value.height = canvasHeight.value;if (canvas2d.value === undefined) {canvas2d.value = c.value.getContext("2d");}canvas2d.value.drawImage(video.value,0,0,canvasWidth.value,canvasHeight.value);const imageData = canvas2d.value.getImageData(0,0,canvasWidth.value,canvasHeight.value);// 解析二维码数据const code = jsQR(imageData.data, imageData.width, imageData.height, {inversionAttempts: "dontInvert",});if (!_.isEmpty(code)) {drawLine(code.location.topLeftCorner,code.location.topRightCorner,"#FF3B58");drawLine(code.location.topRightCorner,code.location.bottomRightCorner,"#FF3B58");drawLine(code.location.bottomRightCorner,code.location.bottomLeftCorner,"#FF3B58");drawLine(code.location.bottomLeftCorner,code.location.topLeftCorner,"#FF3B58");if (code.data) {getData(code.data);}}}requestAnimationFrame(tick);
}
function drawLine(begin: any, end: any, color: string) {canvas2d.value.beginPath();canvas2d.value.moveTo(begin.x, begin.y);canvas2d.value.lineTo(end.x, end.y);canvas2d.value.lineWidth = 4;canvas2d.value.strokeStyle = color;canvas2d.value.stroke();
}
const emit = defineEmits(["on-success"]);function getData(data: string) {emit("on-success", data);closeCamera();
}function openTrack() {trackStatus.value = !trackStatus.value;track.value.applyConstraints({advanced: [{ torch: trackStatus.value }],});
}
const handleClickFile = () => {fileRef.value.click();
};
const getFile = (e: any) => {const file = e.target.files[0];emit("on-success", file);closeCamera();
};
</script><style scoped>
.flash-light {display: grid;place-content: center;margin-bottom: 6px;
}.photo-wrap {position: fixed;bottom: 2.875rem;left: 2.875rem;display: flex;flex-direction: column;align-items: center;gap: 6px;
}.photo {height: 3.125rem;width: 3.125rem;background-color: rgba(250, 250, 250, 0.8);border-radius: 50%;display: grid;place-items: center;cursor: pointer;
}.hide_file {display: none;
}page {background-color: #333333;
}.canvasBox {width: 100vw;position: relative;top: 0;bottom: 0;left: 0;right: 0;background-image: linear-gradient(0deg,transparent 24%,rgba(32, 255, 77, 0.1) 25%,rgba(32, 255, 77, 0.1) 26%,transparent 27%,transparent 74%,rgba(32, 255, 77, 0.1) 75%,rgba(32, 255, 77, 0.1) 76%,transparent 77%,transparent),linear-gradient(90deg,transparent 24%,rgba(32, 255, 77, 0.1) 25%,rgba(32, 255, 77, 0.1) 26%,transparent 27%,transparent 74%,rgba(32, 255, 77, 0.1) 75%,rgba(32, 255, 77, 0.1) 76%,transparent 77%,transparent);background-size: 3rem 3rem;background-position: -1rem -1rem;z-index: 10;background-color: #1110;
}.box {width: 11.9375rem;height: 11.9375rem;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -80%);overflow: hidden;border: 0.1rem solid rgba(0, 255, 51, 0.2);z-index: 11;
}.line {height: calc(100% - 2px);width: 100%;background: linear-gradient(180deg, rgba(0, 255, 51, 0) 43%, #00ff33 211%);border-bottom: 3px solid #00ff33;transform: translateY(-100%);animation: radar-beam 2s infinite alternate;animation-timing-function: cubic-bezier(0.53, 0, 0.43, 0.99);animation-delay: 1.4s;
}.box:after,
.box:before,
.angle:after,
.angle:before {content: "";display: block;position: absolute;width: 3vw;height: 3vw;z-index: 12;border: 0.2rem solid transparent;
}.box:after,
.box:before {top: 0;border-top-color: #00ff33;
}.angle:after,
.angle:before {bottom: 0;border-bottom-color: #00ff33;
}.box:before,
.angle:before {left: 0;border-left-color: #00ff33;
}.box:after,
.angle:after {right: 0;border-right-color: #00ff33;
}@keyframes radar-beam {0% {transform: translateY(-100%);}100% {transform: translateY(0);}
}.box2 {width: 18.75rem;height: 12.5rem;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -80%);z-index: 20;
}.track {position: absolute;bottom: -6.25rem;left: 50%;transform: translateX(-50%);z-index: 20;color: #fff;display: flex;flex-direction: column;align-items: center;
}
</style>

✨该部分完整Demo代码在该文件夹下:Demo (二维码实现之jsQR)

5. 总结

方式一结合vue-qrcode-reader的封装性更强且识别二维码速度更快。
方式二接近了vue-qrcode-reader的底层实现过程。
实际项目开发中更推荐方式一,兼容性与稳定性会更好些

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/346788.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Activity->Activity中动态添加Fragment->add和replace方式添加的区别

XML文件 Activity布局文件R.layout.activity_main <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:id"id/root_ll"android:orientation"v…

操作系统期末复习(大题)

1. 进程调度 周转时间作业完成时刻-作业到达时刻 带权周转时间周转时间/服务时间 平均周转时间各个作业周转时间之和/作业个数 操作系统&#xff1a;周转时间和其他时间_系统为作业提供的时间-CSDN博客 2. 进程调度 3. 调度算法 4. 临界区互斥访问问题 即证明是否满足互斥&a…

mac环境基于llama3和metaGPT自动开发2048游戏

1.准备虚拟环境 conda create -n metagpt python3.9 && conda activate metagpt 2.安装metagpt pip install --upgrade metagpt 3.初始化配置文件 metagpt --init-config 4. 安装llama3 5. 修改配置文件 6.让metegpt自动开发2048游戏 7.经过多轮迭代&#xff0c;最终…

抖音 根据sec uid获取个人详细信息(性别、年龄、属地、关注数、粉丝数、简介等)

本文带来用户的secuid获取用户信息以及其他基本信息&#xff1a; 话不多说看效果&#xff1a; 第一步输入用户sec_uid 根据secuid获取到用户基本信息&#xff1a; 可以支持接口批量转换&#xff0c;欢迎咨询

国内著名的四个“大模型”

关于您提到的国内四大模型&#xff0c;这里为您详细介绍&#xff1a; 文心大模型&#xff1a;文心大模型是百度自主研发的产业级知识增强大模型。它以创新性的知识增强技术为核心&#xff0c;从单模态大模型发展到跨模态&#xff0c;从通用基础大模型到跨领域、跨行业&#xff…

05 Linux 内核启动流程

1、阅读 Linux 内核源码 学习 Linux 有两种路线&#xff1a; 1&#xff09;按照 Linux 启动流程&#xff0c;梳理每个子系统。 2&#xff09;把 Linux 所有用到的子系统学会&#xff0c;再组合起来。 博主选择第一种方式&#xff0c;可以快速上手&#xff0c;知道自己在学什…

5.mongodb 备份与恢复

mongodb备份工具介绍&#xff1a; 1.mongoexport(备份)/mongoimport(恢复) mongoexport是MongoDB提供的一个工具&#xff0c;用于将数据从MongoDB实例导出到JSON或CSV格式的文件中&#xff0c;这个工具对于数据迁移、数据备份或者在不同的数据库之间同步数据非常有用 2.mongodu…

鱼泡-伙伴匹配系统

第一次直播 项目介绍 帮助找到志同道合的伙伴 需求分析 标签分类 主动搜索 组队 创建队伍加入队伍根据标签查询队伍 前端项目初始化 项目初始化文件夹中一定不要带空格&#xff01; 使用npm&#xff08;node包管理器&#xff09;来安装 Vite 脚手架&#xff1a; vue cl…

USB (5)

USB是一个poll的总线。并且数据传输是对齐到time-line的。 对于比如鼠标这样的设备&#xff0c;主机会最快125us poll一次&#xff0c;看是否有输入。这也就是interrupt transfer类型。 对于isochronous transfer类型&#xff0c;数据是在固定的时隙传输的&#xff0c;但不保证…

Python魔法之旅-魔法方法(22)

目录 一、概述 1、定义 2、作用 二、应用场景 1、构造和析构 2、操作符重载 3、字符串和表示 4、容器管理 5、可调用对象 6、上下文管理 7、属性访问和描述符 8、迭代器和生成器 9、数值类型 10、复制和序列化 11、自定义元类行为 12、自定义类行为 13、类型检…

怎么改图片分辨率的dpi数值?简单调整图片dpi的方法

图片分辨率的dpi是目前使用图片时比较常见的要求之一&#xff0c;在网上上传图片时比如证件照类型&#xff0c;都经常会对图片dpi数值有要求。在使用图片的时候&#xff0c;如果dpi的数值不满足用户使用&#xff0c;那么就会无法正常上传使用&#xff0c;那么修改图片api具体该…

42.vue-element-admin界面上的search字段配置

vue-element-admin界面上的search字段&#xff08;下图红色部分&#xff09;是可配置&#xff0c;使用*.vue里的search关键字进行配置。 一、配置方法 1.如果这个字段要放到search区域&#xff0c;则&#xff1a; search: {hidden: false}, 2.如果这个字段不要放到search区域…

实验七、创建小型实验拓扑《计算机网络》

早检到底是谁发明出来的。 一、实验目的 完成本实验后&#xff0c;您将能够&#xff1a; • 设计逻辑网络。 • 配置物理实验拓扑。 • 配置 LAN 逻辑拓扑。 • 验证 LAN 连通性。 二、实验任务 在本实验中&#xff0c;将要求您连接网络设备并配置主机实现基本的网络…

贪心算法-数组跳跃游戏(mid)

目录 一、问题描述 二、解题思路 1.回溯法 2.贪心算法 三、代码实现 1.回溯法实现 2.贪心算法实现 四、刷题链接 一、问题描述 二、解题思路 1.回溯法 使用递归的方式&#xff0c;找到所有可能的走步方式&#xff0c;并记录递归深度&#xff08;也就是走步次数&#x…

【AI法官】人工智能判官在线判案?

概述 AI法官是一款为用户提供专业法律分析和判决建议的智能体应用。用户只需简要描述案情&#xff0c;AI法官便会利用其强大的法律知识和逻辑推理能力&#xff0c;快速且准确地梳理出判决结果。该应用的目标是为用户提供高效、准确、合法的判决建议。 角色任务 任务描述 作为…

小程序 UI 风格魅力非凡

小程序 UI 风格魅力非凡

Oracle的优化器

sql优化第一步&#xff1a;搞懂Oracle中的SQL的执行过程 从图中我们可以看出SQL语句在Oracle中经历了以下的几个步骤&#xff1a; 语法检查&#xff1a;检查SQL拼写是否正确&#xff0c;如果不正确&#xff0c;Oracle会报语法错误。 语义检查&#xff1a;检查SQL中的访问对象…

海南聚广众达电子商务咨询有限公司打造一站式电商服务

在数字经济的浪潮中&#xff0c;电商行业蓬勃发展&#xff0c;各种平台和服务商如雨后春笋般涌现。其中&#xff0c;海南聚广众达电子商务咨询有限公司凭借其专业的团队和丰富的经验&#xff0c;在抖音电商服务领域独树一帜&#xff0c;成为业界的佼佼者。 海南聚广众达电子商…

判断对称树

leetcode - 101 - 对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false提示&a…

注册小程序

每个小程序都需要在 app.js 中调用 App 方法注册小程序实例&#xff0c;绑定生命周期回调函数、错误监听和页面不存在监听函数等。 详细的参数含义和使用请参考 App 参考文档 。 整个小程序只有一个 App 实例&#xff0c;是全部页面共享的。开发者可以通过 getApp 方法获取到全…