移动端浏览器的扫描二维码实现(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的底层实现过程。
实际项目开发中更推荐方式一,兼容性与稳定性会更好些。
相关文章:
移动端浏览器的扫描二维码实现(vue-qrcode-reader与jsQR方式)
1. 实现功能 类似扫一扫的功能,自动识别到画面中的二维码并进行识别,也可以选择从相册中上传。 2. 涉及到的一些插件介绍 vue-qrcode-reader 一组用于检测和解码二维码的Vue.js组件 jsQR 一个纯粹的javascript二维码阅读库,该库接收原始…...
android中调用onnxruntime框架
创建空白项目 安装Android Studio及创建空白项目参考:【安卓Java原生开发学习记录】一、安卓开发环境的搭建与HelloWorld(详细图文解释)_安卓原生开发-CSDN博客 切记:build configuration language 一定选择Groovy!官…...
【机器学习】与【数据挖掘】技术下【C++】驱动的【嵌入式】智能系统优化
目录 一、嵌入式系统简介 二、C在嵌入式系统中的优势 三、机器学习在嵌入式系统中的挑战 四、C实现机器学习模型的基本步骤 五、实例分析:使用C在嵌入式系统中实现手写数字识别 1. 数据准备 2. 模型训练与压缩 3. 模型部署 六、优化与分析 1. 模型优化 模…...
Apollo9.0 PNC源码学习之Control模块(二)
前面文章:Apollo9.0 PNC源码学习之Control模块(一) 本文将对具体控制器以及原理做一个剖析 1 PID控制器 1.1 PID理论基础 如下图所示,PID各参数(Kp,Ki,Kd)的作用: 任何闭环控制系统的首要任务是要稳、准、快的响…...
直线度测量仪发展历程!
直线度测量仪的发展历程可以概括为以下几个关键阶段: 拉钢丝法: 早期直线度测量的简单直观方法,利用钢丝受重力自然下垂的原理来测量直线度误差。 随着机械设备的大型化和测量精度要求的提高,该方法逐渐无法满足要求,正…...
09-spring的bean创建流程(一)
文章目录 spring中bean的创建流程finishBeanFactoryInitialization(beanFactory)beanFactory.preInstantiateSingletons();getMergedLocalBeanDefinition(beanName);流程实现FactoryBean接口,里面的对象实例化过程 spring中bean的创建流程 finishBeanFactoryInitialization(be…...
spring中基于setting和构造器的注入方式
Spring中可以通过setting和构造器两种方式进行依赖注入。 1.基于setting的注入方式(Setter Injection): 实现方式:在类中添加对应的属性以及对应的setter方法,在配置文件中使用<property>元素进行注入。 示例代码…...
爬虫基本原理?介绍|实现|问题解决
爬虫基本原理: 模拟用户行为: 网络爬虫(Web Crawler)是一种自动化的程序,它模拟人类用户访问网站的方式,通过发送HTTP/HTTPS请求到服务器以获取网页内容。 请求与响应: 爬虫首先构建并发送带有…...
DevOps的原理及应用详解(六)
本系列文章简介: 在当今快速变化的商业环境中,企业对于软件交付的速度、质量和安全性要求日益提高。传统的软件开发和运维模式已经难以满足这些需求,因此,DevOps(Development和Operations的组合)应运而生&a…...
手撸 串口交互命令行 及 AT应用层协议解析框架
在嵌入式系统开发中,命令行接口(CLI)和AT命令解析是常见的需求。CLI提供了方便的调试接口,而AT命令则常用于模块间的通信控制。本文将介绍如何手动实现一个串口交互的命令行及AT应用层协议解析框架,适用于FreeRTOS系统…...
Redis几种部署模式介绍
Redis 提供了几种不同的部署模式,以满足不同的使用场景和可用性需求。这些模式包括单机模式、主从复制、哨兵模式和集群模式。下面我将简要介绍每种模式的特点和用途: 单机模式: 描述:单个 Redis 服务器实例运行在一台机器上&…...
【STM32HAL库学习】定时器功能、时钟以及各种模式理解
一、文章目的 记录自己从学习了定时器理论->代码实现使用定时->查询数据手册,加深了对定时器的理解以及该过程遇到了的一些不清楚的知识。 上图为参考手册里通用定时器框图,关于定时器各种情况的工作都在上面了,在理论学习和实际应用后…...
3588麒麟系统硬解码实战
目录 安装rockchip-mpp deb 查找头文件 .pro文件添加 检查库是否已安装 error: stdlib.h: No such file or directory ffmpeg 查找ffmpeg路径: 查找FFmpeg库和头文件的位置 使用pkg-config工具查找FFmpeg路径 ok的ffmpeg配置: ffmpeg查看是否支持libx264 ffmpeg …...
十二 nginx中location重写和匹配规则
十二 location匹配规则 ^~ ~ ~* !~ !~* /a / 内部服务跳转 十三 nginx地址重写rewrite if rewrite set return 13.1 if 应用环境 server location -x 文件是否可执行 $args $document_rot $host $limit_rate $remote_addr $server_name $document_uri if …...
python的视频处理FFmpeg库使用
FFmpeg 是一个强大的多媒体处理工具,用于录制、转换和流式传输音频和视频。它支持几乎所有的音频和视频格式,并且可以在各种平台上运行。FFmpeg 在 Python 中的使用可以通过调用其命令行工具或使用专门的库如 ffmpeg-python。以下是详细介绍如何在 Python 中使用 FFmpeg,包括…...
接口测试时, 数据Mock为何如此重要?
一、为什么要mock 工作中遇到以下问题,我们可以使用mock解决: 1、无法控制第三方系统某接口的返回,返回的数据不满足要求 2、某依赖系统还未开发完成,就需要对被测系统进行测试 3、有些系统不支持重复请求,或有访问…...
未授权与绕过漏洞
1、Laravel Framework 11 - Credential Leakage(CVE-2024-29291)认证泄漏 导航这个路径storage/logs/laravel.log搜索以下信息: PDO->__construct(mysql:host 2、 Flowise 1.6.5 - Authentication Bypass(CVE-2024-31621&am…...
云原生周刊:Kubernetes 十周年 | 2024.6.11
开源项目推荐 Kubernetes Goat Kubernetes Goat 是一个故意设计成有漏洞的 Kubernetes 集群环境,旨在通过交互式实践场地来学习并练习 Kubernetes 安全性。 kube-state-metrics (KSM) kube-state-metrics 是一个用于收集 Kubernetes 集群状态信息的开源项目&…...
ClickHouse内幕(1)数据存储与过滤机制
本文主要讲述ClickHouse中的数据存储结构,包括文件组织结构和索引结构,以及建立在其基础上的数据过滤机制,从Part裁剪到Mark裁剪,最后到基于SIMD的行过滤机制。 数据过滤机制实质上是构建在数据存储格式之上的算法,所…...
1.Mongodb 介绍及部署
MongoDB 是一个开源的文档导向数据库,采用NoSQL(非关系型数据库)的设计理念。MongoDB是一个基于分布式文件存储的数据库。 分布式文件存储是一种将文件数据分布式的存储在多台计算机上。MongoDB是一款强大的文档导向数据库,适合处…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
今日科技热点速览
🔥 今日科技热点速览 🎮 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售,主打更强图形性能与沉浸式体验,支持多模态交互,受到全球玩家热捧 。 🤖 人工智能持续突破 DeepSeek-R1&…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
