vue-h5:在h5中实现相机拍照加上身份证人相框和国徽框

方案1:排出来照片太糊了,效果不好

1.基础功能

参考:
https://blog.csdn.net/weixin_45148022/article/details/135696629

https://juejin.cn/post/7327353533618978842?searchId=20241101133433B2BB37A081FD6A02DA60

https://www.freesion.com/article/67641324321/

https://github.com/AlexKratky/vue-camera-lib

效果:在这里插入图片描述

调用组件的

主要组件方法:openCamera,closeCamera

Upload.vue组件

<template><div id="cameraContainer"><div ref="takePhotoDiv" class="take-photo" style="display: none"><video ref="video"  id="video-fix" :width="width" :height="height" autoplay   webkit-playsinline playsinline></video><div class="frame-container"><div class="mask" >
<!--                  头像页图标--><img v-if="props.currPhotoType=='head'" class="img-head" src="../assets/image/idcard1.svg">
<!--                  国徽页图标--><img v-if="props.currPhotoType=='mark'" class="img-mark" src="../assets/image/idcard2.svg"><div class="tips">请将{{props.currPhotoType=='head'?'身份证人像面':'身份证国徽面'}}完全置于取景框内</div></div></div></div>
<!--      拍照按钮--><div id="captureButton"  @click="takePhoto"><div class="cap-inner"></div></div></div><canvas ref="canvas" style="display: none"></canvas><img ref="photo" id="photo" alt="入职文件" style="display: none" /></template>
<script setup lang="ts">
import { showToast } from "vant/lib/toast";
import { nextTick, onMounted, ref,inject } from "vue";
import {base64ToBlob, base64ToFile, putFile} from "@/common/services/OSSFile.ts";
import {FileUploadType} from "@/common/enum/FileUploadType.ts";
import {ElLoading} from "element-plus";
const props=defineProps({currPhotoType:String
})
const emit=defineEmits(['okUploadImg'])
const video = ref<HTMLVideoElement | null>(null);
// const frame = ref<HTMLDivElement | null>(null);
const photo = ref<HTMLImageElement | null>(null);
const canvas = ref<HTMLCanvasElement | null>(null);
const mediaStream = ref<any>();
const takePhotoDiv = ref<HTMLDivElement | null>(null);const width=ref()
const height=ref()
onMounted(()=>{//设置摄像头宽高width.value=window.innerHeightheight.value=window.innerWidth})const getVideoMedia = () => {if (video.value) {// ----------兼容性代码------------// 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象if (navigator.mediaDevices === undefined) {navigator.mediaDevices = {};}// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。if (navigator.mediaDevices.getUserMedia === undefined) {navigator.mediaDevices.getUserMedia = function (constraints) {// 首先,如果有 getUserMedia 的话,就获得它var getUserMedia =navigator.webkitGetUserMedia || navigator.mozGetUserMedia;// 一些浏览器根本没实现它 - 那么就返回一个 error 到 promise 的 reject 来保持一个统一的接口if (!getUserMedia) {return Promise.reject(new Error("getUserMedia is not implemented in this browser"),);}// 否则,为老的 navigator.getUserMedia 方法包裹一个 Promisereturn new Promise(function (resolve, reject) {getUserMedia.call(navigator, constraints, resolve, reject);});};}// ----------兼容性代码------------// 获取用户媒体设备权限navigator.mediaDevices// 强制使用后置摄像头.getUserMedia({ video: { facingMode: { exact: "environment" } }, audio: false })//前置// .getUserMedia({ video: true, audio: false }).then((stream) => {// if (video.value) {//     video.value.srcObject = stream;//     mediaStream.value = stream;// }//兼容性写法if ("srcObject" in video.value) {video.value.srcObject = stream;} else {// 防止在新的浏览器里使用它,应为它已经不再支持了video.value.src = window.URL.createObjectURL(stream);}video.value.onloadedmetadata = function (e) {video.value.play();};}).catch((error) => {console.error("获取相机权限失败:", error);showToast('获取相机权限失败');});}
}const takePhoto = () => {nextTick(async () => {console.log(video.value)if (canvas.value && video.value && photo.value) {const context = canvas.value.getContext("2d");// 设置画布尺寸与取景框相同canvas.value.width = video.value.videoWidth;canvas.value.height = video.value.videoHeight;// 绘制取景框内的画面到画布if (context) {context.drawImage(video.value, 0, 0);// 将画布内容转为图片并显示photo.value.src = canvas.value.toDataURL();photo.value.style.display = "block";// 关闭videoconsole.log('video', video.value);video.value.pause();// 关闭摄像头mediaStream.value?.getTracks().forEach((track: any) => track.stop());video.value=null}}console.log(photo.value)// console.log(photo.value.src)   将文件流传给后台上传,下列代码根据实际情况自定let file:any=photo.value.srclet idtype=props.currPhotoType=='head'?FileUploadType.BIZ_TYPE_IDCARD2:FileUploadType.BIZ_TYPE_IDCARD1//文件名:时间戳+1000以内的随机数let  fileName=new Date().getTime()+ Math.floor(Math.random()*1000)+'.jpg'const loadingInstance = ElLoading.service({ fullscreen: true, background: 'rgba(0,0,0,0.1)', text: '请求中...' });let data = await putFile(fileName,idtype, base64ToFile(file,fileName));if(data){loadingInstance.close()sendValue({file:file,type:props.currPhotoType,url:data})showToast('上传成功!')emit('okUploadImg',{status:1})}else{loadingInstance.close()showToast('上传失败!')emit('okUploadImg',{status:2})}})
}const passValue:any = inject("getIdFile")
//3.孙组件在函数中调用爷爷传递过来的函数,并在()中传递要传递的数据
const sendValue = (file) => {passValue(file)
}
//4.调用这个函数(也可以使用点击事件等方式触发)//关闭相机
const closeCamera=()=>{// 关闭摄像头mediaStream.value?.getTracks().forEach((track: any) => track.stop());video.value=null
}
//dakai相机
const openCamera=()=>{console.log('打开相机')//打开相机if (takePhotoDiv.value) {takePhotoDiv.value.style.display = 'block'getVideoMedia()}
}defineExpose({openCamera,closeCamera
})
</script>
<style scoped lang="less"></style>
#cameraContainer {position: relative;//width: 324px;//height: 216px;width:100vw;height: 100vh;background: #000;overflow: hidden;.take-photo{//height:85.6*6px;//width: 53.98*6px;height: 70%;width: 90%;overflow: hidden;background: #000;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%) ;}#video-fix{position: absolute;top: 50%;left: 50%;//transform: translate(-50%, -50%) rotate(90deg);transform: translate(-50%, -50%);}
}#video {object-fit: cover;}.frame-container {position: absolute;top: 0;left: 0;width: 100%;height: 100%;
}.mask {position: absolute;height:85.6*5px;width: 53.98*5px;border: 1px solid #fdfdfd;border-radius: 5px;top: 50%;left: 50%;transform: translate(-50%, -50%);.img-head{position: absolute;bottom: 4.5%;right: 13.7%;height: 28%;width: 53%;transform: rotate(90deg);}.img-mark{position: absolute;top:7%;right: 9%;width: 37%;height: 22.5%;transform: rotate(90deg);}.tips{position: absolute;left: -50%;top: 50%;color: #fff;transform: rotate(90deg);font-size: 14px;background: #555657;border-radius: 5px;}}#frame {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);width: 200px;height: 90px;z-index: 10;background-color: transparent;
}#photo {display: none;}
#captureButton{width: 100px;height: 100px;border-radius: 50%;background: #ffffff;position: absolute;bottom: 50px;left: 50%;transform: translateX(-50%);display: flex;justify-content: center;align-items: center;.cap-inner{background: #fff;width: 85%;height: 85%;border-radius: 50%;border: 3px solid #000;}
}

base64转文件流

/*** @description: Base64 转 File* @param {string} base64 base64格式的字符串* @param {string} fileName 文件名* @return {File}*/
export const base64ToFile = (base64: string, fileName: string): File => {const arr: string[] = base64.split(',');const type = (arr[0].match(/:(.*?);/) as string[])[1];const bstr = atob(arr[1]);let n = bstr.length;const u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}return new File([u8arr], fileName, { type });
};

调用组件:

<script setup lang="ts">
import {onMounted, ref} from "vue";
import Upload from "@/components/Upload.vue";const props=defineProps({currPhotoType:String
})
const _show=ref(false)
const uploadRef=ref()const goBack =()=> {// window.history.back() // 删掉van-popup打开时添加的history_show.value = false//关闭相机uploadRef.value.closeCamera()
}const openModal=()=>{_show.value=truesetTimeout(()=>{//打开相机uploadRef.value.openCamera()},500)}
onMounted(()=>{})const  okUpload=(e)=>{if(e.status==1){//上传成功,关闭弹框,关闭相机goBack()}if(e.status==2){//上传失败,关闭弹框,关闭相机goBack()}
}defineExpose({openModal
})
</script><template>
<!--全屏弹框组件--><!--  @close="selectProjectCloseHandler"   @open="selectProjectOpenHandler"--><van-popup v-model:show="_show"    :overlay="false"  position="bottom" :style="{ width: '100%', height: '100%'}"><div class="header"><van-nav-bar class="title" left-arrow title="身份证头像页上传" :safe-area-inset-top="true" :fixed="true"@click-left="goBack" /></div><div style="color: red">{{props}}</div><Upload  ref="uploadRef" @okUploadImg="okUpload" :currPhotoType="props.currPhotoType"></Upload></van-popup>
</template><style scoped lang="less"></style>

2.问题及方案

2.1 ios游览器打开video相机默认是全屏的

安卓可以正常用video打开相机,ios有问题,打开时全屏的。

在iOS端的Web控件上使用video标签播放视频时,视频会自动全屏播放。

解决方案
ios端video标签必须加webkit-playsinline、playsinline属性。

android端部分视频也会存在自动全屏问题,添加webkit-playsinline属性。

 <video ref="video"  id="video-fix" :width="width" :height="height" autoplay   webkit-playsinline playsinline></video>

2.2 拍出来的图片角度有问题

拍出来图片是顺时针旋转了90度,所以需要在canvas中给图片转正
下面是一个旋转的demo

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>
<script type="text/javascript">function drawBeauty(beauty){var mycv = document.getElementById("cv");  var myctx = mycv.getContext("2d");myctx.translate(beauty.width / 2, beauty.height / 2);//调整这里90*3 旋转至正确角度myctx.rotate(((90+90*3) * Math.PI) / 180);myctx.drawImage(beauty, -beauty.width / 2, -beauty.height / 2);}function load(){var beauty = new Image();  //获取本题图片beauty.src = "./asset/WechatIMG134.jpg"; if(beauty.complete){drawBeauty(beauty);}else{beauty.onload = function(){drawBeauty(beauty);};beauty.onerror = function(){window.alert('美女加载失败,请重试');};};   }//loadif (document.all) {window.attachEvent('onload', load);  }else {  window.addEventListener('load', load, false);}</script><canvas id="cv"  width="600" height="300" style="border:1px solid #ccc;margin:20px auto;display: block;">当前浏览器不支持canvas<!-- 如果浏览器支持canvas,则canvas标签里的内容不会显示出来 -->
</canvas>
</body>
</html>

参考:
https://blog.csdn.net/qq_30100043/article/details/106355667
https://www.cnblogs.com/html5test/archive/2012/03/01/2375558.html
https://jelly.jd.com/article/6006b1045b6c6a01506c87e6
https://www.cnblogs.com/Joe-and-Joan/p/10957818.html

2.3 拍出来的照片默认是640*480 ,照片不清晰

https://stackoverflow.com/questions/15849724/capture-high-resolution-video-image-html5

简而言之:video宽高要设置成 4:3或16:9才行,这里我设置成了1280*720

<video ref="video"  id="video-fix" width="1280" height="720" autoplay   webkit-playsinline playsinline></video>
<canvas ref="canvas" style="display: none" width="1280" height="720"></canvas>
   var constraints = {audio: false,video: {width: { min: 1280, max: 1560 }, height: { min: 720, max: 1440 },facingMode: { exact: "environment" }//设置后置,注释掉就是前置}};navigator.mediaDevices.getUserMedia(constraints).then(gotStream).catch(handleError)

2.4 本地local能打开电脑前置,不是最终效果

localhost 只能调起电脑的前置摄像头,无法在手机本地调试。这是因为浏览器的安全限制,必须使用 https 才可以。所以需要让运维升级测试环境为https。所在在使用后置摄像头调试时非常麻烦,建议将需要调试的参数都设置成变量再逐一调试。

2.5 部分手机打开相机默认是放大的(手动设置焦距)

设置焦距:
根据下面这篇文章设置,电脑是不生效的,建议先在手机测试
https://www.dynamsoft.com/codepool/camera-zoom-control-on-web.html

https://github.com/webrtc/samples/blob/gh-pages/src/content/getusermedia/pan-tilt-zoom/js/main.js

2.6 ios的css样式bug

安卓:正常显示box-shadow
在这里插入图片描述
ios:显示异常
在这里插入图片描述

原因:ios下给textarea设置默认边框样式
解决办法: -webkit-appearance: none; 去掉Ios默认的样式

  #capture-rectangle {margin: (400/75)*1rem auto 0;width: (700/75)*1rem;  // 这里写上我们需要裁切的宽height: (450/75)*1rem;// 这里写上我们需要裁切的高border: 1px solid #fff;border-radius: (20/75)*1rem;z-index: 2;-webkit-appearance: none;//解决ios的cssbugbox-shadow:0 0 0 (2000/75)*1rem rgba(0, 0, 0, 0.7); // 外层阴影-webkit-box-shadow: 0 0 0 (2000/75)*1rem rgba(0, 0, 0, 0.7); // 外层阴影}

经过不断调试后,发现并不是上面的这个原因。真正的原因是ios的游览器识别不到video实时的offsetHeight的值,所以在识别不到的时候,手动设置一下遮罩层的高度就可以了。

  videoHeight.value=video.value.offsetHeightif(video.value.offsetHeight<400){//解决ios不能获取到实时的offsetHeight的问题videoHeight.value=600}

在这里插入图片描述

方案2: 最终方案

本方案参考: https://juejin.cn/post/6955036353931247629
本文博主是用react写的,有源码但是本地启动不了,在方案1的基础上对其代码进行简单改写为vue3版本。

效果:(如果需要加人像国徽框,自行添加样式)
在这里插入图片描述
注:贴一张cavans的drawImage()的参数示例图
在这里插入图片描述

最终方案代码如下:

getUserMediaStream.ts

//访问用户媒体设备的兼容方法
function getUserMedia(constrains) {const navigator: any = window.navigator;if (navigator.mediaDevices?.getUserMedia) {//最新标准APIreturn navigator.mediaDevices.getUserMedia(constrains);} else if (navigator.webkitGetUserMedia) {//webkit内核浏览器return navigator.webkitGetUserMedia(constrains);} else if (navigator.mozGetUserMedia) {//Firefox浏览器return navigator.mozGetUserMedia(constrains);} else if (navigator.getUserMedia) {//旧版APIreturn navigator.getUserMedia(constrains);}
}//成功的回调函数
function success(stream, video) {return new Promise<void>((resolve, reject) => {video.srcObject = stream;//播放视频video.onloadedmetadata = function (e) {video.play();};resolve();});
}function getUserMediaStream(videoNode) {//调用用户媒体设备,访问摄像头return getUserMedia({audio: false,// video: { facingMode: { exact: 'environment' } },//后置// video: true,//默认前置video: { facingMode: { exact: 'environment', width: 1920, height: 1080 } },//设置成后置}).then(res => {return success(res, videoNode);}).catch(error => {console.log('访问用户媒体设备失败:', error.name, error.message);return Promise.reject();});
}export { getUserMediaStream };

PhotoUpload.vue组件

<script setup lang="ts">const props=defineProps({currPhotoType:String
})
const emit=defineEmits(['okUploadImg'])const video = ref<HTMLVideoElement | null>(null);
const videoHeight=ref()
const rectangle = ref<HTMLDivElement | null>(null);
const _canvas = ref<HTMLCanvasElement | null>(null);
const showTips=ref(true)onMounted(()=>{setTimeout(()=>{//3秒后关闭提示showTips.value=false},5000)
})/**获取video中对应的真实size*/
function getRealSize() {const { videoHeight: vh, videoWidth: vw, offsetHeight: oh, offsetWidth: ow } = video.value;console.log(vh,vw,oh,ow)return {getHeight: height => {return (vh / oh) * height;},getWidth: width => {return (vw / ow) * width;},};
}const currImg=ref()
const getPhoto=async()=>{const { getHeight, getWidth } = getRealSize();if (!rectangle.value) {return;}/** 获取框的位置 */const { left, top, width, height } = rectangle.value.getBoundingClientRect();console.log(left, top,width,height,window.scrollX,window.scrollY)const context = _canvas.value.getContext('2d');_canvas.value.width = width*3;//这里截出来的图比列太小了,做了一个放大3倍的操作_canvas.value.height = height*3;context?.drawImage(video.value,getWidth(left + window.scrollX),getHeight(top + window.scrollY),getWidth(width),getHeight(height),0,0,width*3,height*3,);const base64 = _canvas.value.toDataURL('image/jpeg');currImg.value=base64// 这里都是些上传逻辑,根据实际情况写,如果需要将这个base64的文件转为文件流我下面贴了转换函数
}
const passValue:any = inject("getIdFile")
const passLocalValue:any = inject("getLocalFile")
//3.孙组件在函数中调用爷爷传递过来的函数,并在()中传递要传递的数据
const sendValue = (file) => {passValue(file)
}
const sendLocalValue = (file) => {passLocalValue(file)
}
//选择本地文件
const  selectLocal=()=>{sendLocalValue({type:props.currPhotoType})
}//关闭相机
const closeCamera=()=>{console.log('关闭相机')console.log( video.value)// 关闭摄像头video.value.srcObject?.getTracks().forEach((track: any) => track.stop());// video.value=null}
//dakai相机
const openCamera=()=>{console.log('打开相机')//打开相机// rectangle.value = document.getElementById('capture-rectangle');getUserMediaStream(video.value).then(() => {setTimeout(()=>{videoHeight.value=video.value.offsetHeightif(video.value.offsetHeight<400){//解决ios不能获取到实时的offsetHeight的问题videoHeight.value=600}if (navigator.mediaDevices.getSupportedConstraints().zoom) {// showToast("The browser   support zoom.");}else{showToast("当前游览器不支持调节焦距");}},200)}).catch(() => {showToast('无法调起后置摄像头,请点击相册,手动上传身份证!')});
}const currFoucs=ref(1)//焦点参数const changeFoucs=async (num)=>{try {currFoucs.value=num//改变焦距const videoTracks = video.value.srcObject.getVideoTracks();let track = videoTracks[0];const constraints = {advanced: [{zoom: num}]};await track.applyConstraints(constraints);}catch (e) {showToast('当前游览器不支持此倍数的焦距!')}
}//调试参数
const width=ref(1600)
const height=ref(1000)
const position=ref(700)
const shadow=ref(1500)defineExpose({openCamera,closeCamera
})
</script>
<template><div class="container"><canvas ref="_canvas" style="display: none" ></canvas><video ref="video"  id="video-box" autoplay  muted  webkit-playsinline playsinline style="width: 100%;"></video><!--    boxShadow:`0 0 0 ${(shadow/75)*1}rem rgba(0, 0, 0, 0.7)`--><div class="shadow-layer" :style="{height:videoHeight +'px'}"><div ref="rectangle" id="capture-rectangle"  :style="{width:(width/75)*1+'rem',height:(height/75)*1+'rem',margin: `${(position/75)*1}rem auto 0`,boxShadow:`0 0 0 ${(shadow/75)*1}rem rgba(0, 0, 0, 0.7)`}"></div><div class="hold-tips">请将{{props.currPhotoType=='head'?'身份证人像面':'身份证国徽面'}}完全置于取景框内</div></div><div class="footer"><div class="foucs-list"><div class="foucs-one" :class="currFoucs==2? 'active':''" @click="changeFoucs(2)">x2.0</div><div class="foucs-one" :class="currFoucs==1.5? 'active':''" @click="changeFoucs(1.5)">x1.5</div><div class="foucs-one" :class="currFoucs==1? 'active':''" @click="changeFoucs(1)">x1.0</div>
<!--        <div class="foucs-one" :class="currFoucs==0.5? 'active':''" @click="changeFoucs(0.5)">x0.5</div>--></div><div v-if="showTips" class="tips-list">温馨提示:选择拍照上传时建议将 <span style="text-decoration: underline">手机锁定竖屏</span>  ,相机无法启动请 <span style="text-decoration: underline">刷新页面</span> 或将浏览器设置-隐私安全中阻止弹框等功能关闭。</div><div class="left" ><van-icon @click="selectLocal" name="photo-o" style="color: #fff"/></div><div class="mid"><div id="captureButton"  @click="getPhoto" ><div class="cap-inner"></div></div></div><div class="right"></div></div><!--调试代码--><div class="config" v-if="false">设置框的宽高<div >
<!--        w:<input type="text" v-model="width"><br/>-->
<!--        h:<input type="text" v-model="height"><br/>-->
<!--        position:<input type="text" v-model="position"><br/>-->
<!--        shadow:<input type="text" v-model="shadow"><br/>-->
<!--        shadowlayerheight:<input type="text" v-model="videoHeight"><br/>-->
<!--        focus:<input type="number" min="0" max="2" id="zoomInput" value="0" @change="setZoom">--></div><button @click="getPhoto">获取图片</button><div>获取到的图:<div><img :src="currImg"></div></div></div></div>
</template>
.container{background: #000000;width: 100%;//min-height: 100%;height: 100%;position: relative;#video-box{position: absolute;//top: 46px;top: 0;left: 0;}.shadow-layer {position: absolute;//top: 46px;left: 0;top: 0;width: 100%;//z-index: 1;overflow: hidden;#capture-rectangle {margin: (400/75)*1rem auto 0;width: (700/75)*1rem;  // 这里写上我们需要裁切的宽height: (450/75)*1rem;// 这里写上我们需要裁切的高border: 1px solid #fff;border-radius: (20/75)*1rem;z-index: 2;-webkit-appearance: none;//解决ios的cssbugbox-shadow:0 0 0 (1500/75)*1rem rgba(0, 0, 0, 0.7); // 外层阴影-webkit-box-shadow: 0 0 0 (1500/75)*1rem rgba(0, 0, 0, 0.7); // 外层阴影}.hold-tips{color: #e1e1e1;font-size: 12px;display: flex;align-items: center;justify-content: center;margin: 5px auto 0;border-radius: 5px;}}.config{position: absolute;top: 400px;left: 0;z-index: 20;width: 100%;height: 100px;color: #d21818;}.footer{position: relative;display: flex;position: fixed;bottom: 0;width: 100%;height: 20vh;justify-content: space-around;align-items: center;.foucs-list{z-index: 320;position: absolute;right: 0;color: #fff;text-align: left;right: 20px;bottom: 22vh;font-size: 14px;div{margin: 5px 0;padding: 2px 6px;}.active{background: #fff;color: #000;border-radius: 10px;}}.left{font-size: 30px;}.mid{#captureButton{width: 80px;height: 80px;border-radius: 50%;background: #ffffff;position: absolute;bottom: 50px;left: 50%;transform: translateX(-50%);display: flex;justify-content: center;align-items: center;.cap-inner{background: #fff;width: 85%;height: 85%;border-radius: 50%;border: 3px solid #000;}}}.tips-list{position: absolute;font-size: 12px;color:#fff;top: 0;text-align: left;padding: 0 20px;}}
}
/*** @description: Base64 转 File* @param {string} base64 base64格式的字符串* @param {string} fileName 文件名* @return {File}*/
export const base64ToFile = (base64: string, fileName: string): File => {const arr: string[] = base64.split(',');const type = (arr[0].match(/:(.*?);/) as string[])[1];const bstr = atob(arr[1]);let n = bstr.length;const u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}return new File([u8arr], fileName, { type });
};/*** @description: Base64 转 Blob* @param {string} base64 base64格式的字符串* @return {Blob}*/
export const base64ToBlob = (base64: string): Blob => {const arr: string[] = base64.split(',');const type = (arr[0].match(/:(.*?);/) as string[])[1];console.log('arr',arr);const bstr = atob(arr[1]);let n = bstr.length;const u8arr = new Uint8Array(n);while (n--) {u8arr[n] = bstr.charCodeAt(n);}return new Blob([u8arr], { type });
};

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

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

相关文章

初识GIS

文章目录 一、什么叫地理信息1、定义2、主要特点3、分类 二、什么叫GIS1、定义2、GIS对空间信息的储存2.1、矢量数据模型2.2、栅格数据模型 3、离散栅格和连续栅格的区别 三、坐标系统1、为什么要存在坐标系统&#xff1f;2、地理坐标系2.1、定义与特点2.2、分类 3、投影坐标系…

Android 开发指南:初学者入门

Android 是全球最受欢迎的移动操作系统之一&#xff0c;为开发者提供了丰富的工具和资源来创建各种类型的应用程序。本文将为你提供一个全面的入门指南&#xff0c;帮助你从零开始学习 Android 开发。 目录 1. 了解 Android 平台[1]2. 设置开发环境[2]3. 学习基础知识[3]4. 创…

【QML】QML多线程应用(WorkerScript)

1. 实现功能 QML项目中&#xff0c;点击一个按键后&#xff0c;运行一段比较耗时的程序&#xff0c;此时ui线程会卡住。如何避免ui线程卡住。 2. 单线程&#xff08;会卡住&#xff09; 2.1 界面 2.2 现象 点击delay btn后&#xff0c;执行耗时函数&#xff08;TestJs.func…

MFC1(note)

引言 在学习SDK后我们发现&#xff0c;写消息好麻烦&#xff0c;处理消息更麻烦 处理消息效率低发送消息效率低 所以把SDK中这些消息全部封装好 MFC封装了windows 的大部分API 这里说一下QT架构跨平台 MFC用得如何取决于你SDK的水平 创建 如果打开没有MFC 一般勾选以下…

封装一个省市区的筛选组件

筛选功能&#xff1a;只能单选&#xff08;如需多选需要添加show-checkbox多选框属性&#xff09;&#xff0c;选中省传递省的ID&#xff0c;选中市传递省、市的ID&#xff0c; 选中区传递省市区的ID 父组件&#xff1a; <el-form-item><div style"width: 240px;…

大模型在蓝鲸运维体系应用——蓝鲸运维开发智能助手

本文来自腾讯蓝鲸智云社区用户: CanWay 背景 1、运维转型背景 蓝鲸平台从诞生之初&#xff0c;就一直在不遗余力地推动运维转型&#xff0c;让运维团队可以通过一体化PaaS平台&#xff0c;快速编写脚本&#xff0c;编排流程&#xff0c;开发运维工具&#xff0c;从被动地提供…

独家|京东上线自营秒送,拿出二十年底牌和美团竞争

京东自营秒送开启招商&#xff0c;即时零售也要全托管&#xff1f; 作者|王迟 编辑|杨舟 据「市象」独家获悉&#xff0c;京东将在近期上线自营秒送业务&#xff0c;目前已经开始邀约制招商。「市象」获得的招商资料显示&#xff0c;和5月刚升级上线的京东秒送以POP模式不同&…

GEE 数据集——美国gNATSGO(网格化国家土壤调查地理数据库)完整覆盖了美国所有地区和岛屿领土的最佳可用土壤信息

目录 简介 代码 引用 网址推荐 知识星球 机器学习 gNATSGO&#xff08;网格化国家土壤调查地理数据库&#xff09; 简介 gNATSGO&#xff08;网格化国家土壤调查地理数据库&#xff09;数据库是一个综合数据库&#xff0c;完整覆盖了美国所有地区和岛屿领土的最佳可用土…

JavaSE常用API-日期(计算两个日期时间差-高考倒计时)

计算两个日期时间差&#xff08;高考倒计时&#xff09; JDK8之前日期、时间 Date SimpleDateFormat Calender JDK8开始日期、时间 LocalDate/LocalTime/LocalDateTime ZoneId/ZoneDateTIme Instant-时间毫秒值 DateTimeFormatter Duration/Period

15分钟学 Go 第 53 天 :社区资源与学习材料

第53天&#xff1a;社区资源与学习材料 目标 了解Go语言官方资源掌握社区重要学习平台学会利用开源项目学习构建个人知识体系 一、Go语言官方资源汇总 资源类型网址说明Go官网golang.org官方文档、下载、教程Go Blogblog.golang.org技术博客、最新特性介绍Go Playgroundpla…

删库跑路,启动!

起因&#xff1a;这是一个悲伤的故事&#xff0c;在抓logcat时 device待机自动回根目录了&#xff0c;而题主对当前路径的印象还停留在文件夹下&#xff0c;不小心在根目录执行了rm -rf * … 所以&#xff0c;这是个悲伤的故事&#xff0c;东西全没了…device也黑屏了&#xff…

如何优化Kafka消费者的性能

要优化 Kafka 消费者性能&#xff0c;你可以考虑以下策略&#xff1a; 并行消费&#xff1a;通过增加消费者组中的消费者数量来并行处理更多的消息&#xff0c;从而提升消费速度。 批量消费&#xff1a;配置 fetch.min.bytes 和 fetch.max.wait.ms 参数来控制批量消费的大小和…

开始使用 Elastic AI Assistant 进行可观察性和 Microsoft Azure OpenAI

作者&#xff1a;Jonathan Simon 按照此分步过程开始使用 Elastic AI Assistant for Observability 和 Microsoft Azure OpenAI。 最近&#xff0c;Elastic 宣布&#xff0c;AI Assistant for Observability 现已面向所有 Elastic 用户开放。AI Assistant 为 Elastic Observabi…

vue2项目启用tailwindcss - 开启class=“w-[190px] mr-[20px]“ - 修复tailwindcss无效的问题

效果图 步骤 停止编译"npm run dev"安装依赖 npm install -D tailwindcssnpm:tailwindcss/postcss7-compat postcss^7 autoprefixer^9 创建文件/src/assets/tailwindcss.css&#xff0c;写入内容&#xff1a; tailwind base; tailwind components; tailwind utiliti…

深度学习——AE、VAE

&#x1f33a;历史文章列表&#x1f33a; 机器学习——损失函数、代价函数、KL散度机器学习——特征工程、正则化、强化学习机器学习——常见算法汇总机器学习——感知机、MLP、SVM机器学习——KNN机器学习——贝叶斯机器学习——决策树机器学习——随机森林、Bagging、Boostin…

【数字图像处理+MATLAB】基于 Sobel 算子计算图像梯度并进行边缘增强:使用 imgradientxy 函数

引言 在图像处理中&#xff0c;边缘通常是图像中像素强度变化最大的地方&#xff0c;这种变化可以通过计算图像的梯度来量化。梯度是一个向量&#xff0c;它的方向指向像素强度增加最快的方向&#xff0c;它的大小&#xff08;或者说幅度&#xff09;表示像素强度增加的速度。…

建设展示型网站企业渠道用户递达

展示型网站的主要作用便是作为企业线上门户平台、信息承载形式、拓客咨询窗口、服务/产品宣传订购、其它内容/个人形式呈现等&#xff0c;网站发展多年&#xff0c;现在依然是企业线上发展的主要工具之一且有建设的必要性。 谈及整体价格&#xff0c;自制、定制开发、SAAS系统…

无桥Boost-PFC 双闭环控制MATLAB仿真

一、无桥Boost-PFC原理概述 无桥 Boost-PFC&#xff08;Power Factor Correction&#xff0c;功率因数校正&#xff09;的工作原理是通过特定的电路结构和控制策略&#xff0c;对输入电流进行校正&#xff0c;使其与输入电压同相位&#xff0c;从而提高电路的功率因数&#xf…

java访问华为网管软件iMaster NCE的北向接口时传递参数问题

上一篇文章介绍了利用《java访问华为网管软件iMaster NCE的北向接口》的一般性步骤&#xff0c;这里详细介绍其中一个读取性能数据的示例。原因是读取华为网管软件北向接口&#xff0c;完全找不到可供参考的例子。如果不需要传递什么参数&#xff0c;就能获取到结果&#xff0c…

深度学习之pytorch常见的学习率绘制

文章目录 0. Scope1. StepLR2. MultiStepLR3. ExponentialLR4. CosineAnnealingLR5. ReduceLROnPlateau6. CyclicLR7. OneCycleLR小结参考文献 https://blog.csdn.net/coldasice342/article/details/143435848 0. Scope 在深度学习中&#xff0c;学习率&#xff08;Learning R…