Vant之手机端上传图片只允许拍照上传
1.开发拍照上传页面 - andImgCapture.vue,包含镜头翻转功能
<template> <div> <van-button icon="plus" type="primary" :disabled="disabled" @click.stop="clickCamera" ref="uploadBtn" >拍照上传</van-button > <div class="camera-content" v-show="cameraVisible"> <van-button icon="cross" class="close-photo" type="primary" @click.stop="stopVideoStream" ></van-button> <video class="camera-video" ref="videoElement" playsinline webkit-playsinline ></video> <van-button icon="photograph" class="take-photo" @click.stop="capturePhoto" ></van-button> <!-- 你可以使用适当的图标来表示切换摄像头 --> <van-button icon="share" class="switch-camera" @click.stop="switchCamera" ></van-button> <canvas ref="canvasElement" style="display: none"></canvas> </div> <div class="captured-image-content" v-if="photoVisible"> <van-button class="cancel-photo" @click="cancelPhoto">取消</van-button> <van-button class="confirm-photo" @click="confirmPhoto">完成</van-button> <img class="captured-image" v-if="capturedImage" :src="capturedImage" alt="Captured Image" /> </div> </div> </template> <script setup> import { onBeforeUnmount, onMounted, ref, toRefs, defineAsyncComponent, computed, watch, nextTick, reactive, } from "vue"; import { Toast } from "vant"; const props = defineProps({ disabled: { type: Boolean, default: false, }, afterRead: { type: Function, default: () => { return true; }, }, beforeRead: { type: Function, default: () => {}, }, }); const emit = defineEmits(["clickUpload"]); const cameraVisible = ref(false); const photoVisible = ref(false); const capturedImage = ref(null); const videoElement = ref(null); const canvasElement = ref(null); const file = reactive({ content: "", file: {}, }); watch(() => cameraVisible.value, newVal => { if (newVal) { document.getElementsByTagName("body")[0].style.backgroundColor = "#000000"; } else { document.getElementsByTagName("body")[0].style.backgroundColor = "#ffffff"; } }); onBeforeUnmount(() => { stopVideoStream(); }); const clickCamera = async (e) => { emit("clickUpload", e); // 开启摄像头前检测是否触发了阻止默认事件 if (e.defaultPrevented) { return; } else { setupCamera(); } }; const uploadBtn = ref(null); // 默认使用后置摄像头 const facingMode = ref(sessionStorage.facingMode ?? "environment"); const switchCamera = () => { facingMode.value = facingMode.value === "environment" ? "user" : "environment"; // 当点击切换按钮时,如果当前是后置摄像头,则切换到前置摄像头,反之亦然。 sessionStorage.facingMode = facingMode.value; // 重新设置摄像头以应用更改 stopVideoStream(); nextTick(() => { uploadBtn.value.$el.click(); }); }; const setupCamera = async () => { try { initFile(); navigator.mediaDevices .getUserMedia({ video: { facingMode: facingMode.value }, }) .then((stream) => { const videoElementObj = videoElement.value; videoElementObj.srcObject = stream; cameraVisible.value = true; if ("srcObject" in videoElementObj) { videoElementObj.srcObject = stream; } else { // 防止在新的浏览器里使用它,因为它已经不再支持了 videoElementObj.src = window.URL.createObjectURL(stream); } videoElementObj.onloadedmetadata = function (e) { videoElementObj.play(); }; }) .catch((err) => { Toast.fail("访问用户媒体设备失败:", err.message); }); } catch (error) { cameraVisible.value = false; console.log("获取摄像头失败:", error); Toast.fail("获取摄像头失败"); } }; const capturePhoto = () => { const videoElementObj = videoElement.value; const canvasElementObj = canvasElement.value; const context = canvasElementObj.getContext("2d"); canvasElementObj.width = videoElementObj.videoWidth; canvasElementObj.height = videoElementObj.videoHeight; context.drawImage( videoElementObj, 0, 0, canvasElementObj.width, canvasElementObj.height ); capturedImage.value = canvasElementObj.toDataURL(); photoVisible.value = true; }; const cancelPhoto = () => { photoVisible.value = false; capturedImage.value = ""; }; const confirmPhoto = () => { stopVideoStream(); file.content = capturedImage.value; file.file = base64ToFile(capturedImage.value, "photo" + Date.now()); const passBeforeRead = props.beforeRead(file); if (passBeforeRead || passBeforeRead === undefined) { props.afterRead(file); } }; const initFile = () => { file.content = ""; file.file = ""; capturedImage.value = ""; }; const stopVideoStream = () => { photoVisible.value = false; cameraVisible.value = false; if (videoElement.value && videoElement.value.srcObject) { const tracks = videoElement.value.srcObject.getTracks(); tracks.forEach((track) => { track.stop(); }); } }; const base64ToFile = (dataurl, fileName) => { let arr = dataurl.split(","); let mime = arr[0].match(/:(.*?);/)[1]; // suffix是该文件的后缀 let suffix = mime.split("/")[1]; // atob 对经过 base-64 编码的字符串进行解码 let bstr = atob(arr[1]); // n 是解码后的长度 let n = bstr.length; // Uint8Array 数组类型表示一个 8 位无符号整型数组 初始值都是 数子0 let u8arr = new Uint8Array(n); // charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数 while (n--) { u8arr[n] = bstr.charCodeAt(n); } // new File返回File对象 第一个参数是 ArraryBuffer 或 Bolb 或Arrary 第二个参数是文件名 // 第三个参数是 要放到文件中的内容的 MIME 类型 return new File([u8arr], `${fileName}.${suffix}`, { type: mime, }); }; </script> <style lang="less" scoped> .camera-content { width: 100%; height: 100vh; position: fixed; top: 0; left: 0; background: #000000; z-index: 99; } .close-photo { position: absolute; left: 0; z-index: 100; border: unset; background: #000000; } .camera-video { width: 100vw; height: auto; position: absolute; top: 50%; left: 0px; transform: translateY(-55%); } .take-photo { width: 70px; height: 70px; position: absolute; z-index: 100; bottom: 40px; left: 20%; // transform: translateX(-50%); border-radius: 50%; font-size: 28px; background: #ffffff; } .switch-camera { width: 70px; height: 70px; position: absolute; z-index: 100; bottom: 40px; right: 20%; // transform: translateX(-50%); border-radius: 50%; font-size: 28px; background: #ffffff; } .captured-image-content { width: 100%; height: 100vh; display: flex; align-items: center; position: fixed; top: 0; left: 0; z-index: 9999; background: #000000; } .cancel-photo { color: #ffffff; border: unset; background: none; top: 0; left: 0; position: fixed; } .confirm-photo { color: #0570db; border: unset; background: none; top: 0; right: 0; position: fixed; } .captured-image { width: 100%; height: auto; } </style>
ps:代码使用ref绑定上传按钮到uploadBtn,使用uploadBtn.value.$el.click()触发按钮点击事件。
由于是Vant按钮组件,所以需要使用$el来获取原生DOM。
nextTick函数可以在DOM更新完成后执行回调。
假如使用ref绑定组件到orgPicker,那么可以使用orgPicker.value.show()来显示组件。
const orgPicker = ref(null); const goToSelectNodePeople = () => { orgPicker.value.show(); };
2.编写androidOrIos.js
export const checkMobileModel = () => { let u = navigator.userAgent; let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); if (isAndroid) { return 'Android' } if (isIOS) { return 'IOS' } return '' }
3.导入两个文件
import AndImgCapture from "@/components/andImgCapture/andImgCapture.vue";
import { checkMobileModel } from "@/utils/androidOrIos";
4.在主页面实现拍照上传基本功能
<AndImgCapture v-if="checkMobileModel() === 'Android'" :after-read="(file) => uploaderAfter(file, 'promotionPhoto')" @click-upload="openUpload($event, item)" > </AndImgCapture> <van-uploader v-else :multiple="true" :preview-image="false" :max-size="50000 * 1024" :after-read="(file) => uploaderAfter(file, 'promotionPhoto')" @click-upload="openUpload($event, item)" @oversize="onOversize" capture="camera" accept="image/*" > <van-button icon="plus" type="primary">拍照上传</van-button> </van-uploader>
即可。