在手机端,调用摄像头需要在 HTTPS 或 localhost 下访问,还需要用户事先进行授权
效果图:

<template>
<div ref="wrapperRefRef">
<div class="take-picture" v-if="imgUrl">
<img :src="imgUrl" alt="">
</div>
<video ref="videoRef" class="take-picture" v-else></video>
<div class="wrapper_btm" v-if="imgUrl">
<span class="open_album" @click="closePicture">取消</span>
<van-button type="primary" @click="refreshPicture" class="take-picture-btn">返回</van-button>
<span class="close_picture" @click="submitResult">确定</span>
</div>
<div class="wrapper_btm" v-else>
<input type="file" accept="image/*" ref="galleryRef" @change="handleFileChange" style="display: none;"/>
<span class="open_album" @click="openGallery">相册</span>
<van-button type="primary" @click="handleShoot" class="take-picture-btn">拍照</van-button>
<span class="close_picture" @click="closePicture">关闭</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, defineEmits, defineProps, watch } from "vue";
import { showToast, showNotify} from 'vant';
import { compressImage } from '@/utils/compressImage';
// import { emit } from "process";
const emit = defineEmits(["closePicture", "pictureResult"])
const videoRef = ref<HTMLVideoElement | null>(null);
const imgUrl = ref('');
const imgResult = ref<File | null>(null);
const props = defineProps({
show: {
type: Boolean,
required: true,
default: false,
},
});
// 获取媒体流,检测是否支持getUserMedia拍照
const getMediaStream = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: { exact: 'environment' }
},
audio: false,
});
if (videoRef.value) {
videoRef.value.srcObject = stream;
videoRef.value.play();
}
} catch (error) {
console.error('无法获取媒体流', error);
showNotify({ type: 'danger', message: '无法获取媒体流' });
}
};
// 点击拍照
const handleShoot = () => {
if (!videoRef.value) return;
// 设置canvas画布
const canvas = document.createElement("canvas");
canvas.width = videoRef.value.videoWidth;
canvas.height = videoRef.value.videoHeight;
// 获取canvas上下文对象
const ctx = canvas.getContext("2d");
// 截图操作
ctx?.drawImage(videoRef.value, 0, 0, canvas.width, canvas.height);
// 转为文件流
const img = canvas.toDataURL("image/jpeg");
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], 'takePicture.jpg', { type: 'image/jpeg' });
imgResult.value = newFile;
}
}, 'image/jpeg', 1); // 0.8 为压缩质量
imgUrl.value = img;
}
const galleryRef = ref<HTMLInputElement | null>(null);
// 打开相册
const openGallery = () => {
galleryRef.value?.click();
}
// 相册文件选择
const handleFileChange = (e: any) => {
// 调用压缩图片方法
compressImage(e.target.files[0], (file) => {
emit("pictureResult", file);
// 关闭相机
closePicture();
});
};
// 重置相机
const refreshPicture = () => {
if (videoRef.value) {
videoRef.value.srcObject = null;
}
imgUrl.value = '';
getMediaStream();
}
// 关闭相机
const closePicture = () => {
if (videoRef.value) {
videoRef.value.srcObject = null;
}
imgUrl.value = '';
emit("closePicture", false);
}
// 提交拍照
const submitResult = () => {
emit("pictureResult", imgResult.value);
closePicture();
}
onMounted(() => {
getMediaStream();
watch(() => props.show, (newValue, oldValue) => {
console.log('newValue', newValue)
if (newValue) {
getMediaStream();
}
});
});
</script>
<style scoped>
.take-picture{
width: 100%;
height: 80vh;
position: absolute;
top: 0;
left: 0;
/* background: rgba(255,255,255,.5); */
}
.wrapper_btm{
display: flex;
width: 100%;
height: 20vh;
background: rgb(10, 10, 10);
flex-wrap: wrap;
align-content: center;
justify-content: center;
/* align-items: center; */
position: absolute;
bottom: 0;
left: 0;
}
.take-picture-btn{
width: 4rem !important;
height: 4rem !important;
border-radius: 50% !important;
background: #fff !important;
border: none !important;
color: #000;
}
.open_album{
font-size: 1rem;
position: absolute;
top: 50%;
left: 25%;
/* left: 50%; */
transform: translateY(-50%);
color: #fff;
}
.close_picture{
font-size: 1rem;
position: absolute;
top: 50%;
right: 25%;
/* left: 50%; */
transform: translateY(-50%);
color: #fff;
}
</style>
使用拍照组件
<template>
<van-overlay :show="show" @click="show = false">
<takePicture @closePicture="show = false" :show="show" @pictureResult="handlePictureResult" />
</van-overlay>
</template>
<script lang="ts" setup>
import { ref, reactive, defineEmits, defineProps, getCurrentInstance } from 'vue';
const show = ref(false);
// 使用takePicture组件的拍照返回结果
const handlePictureResult = (result: File) => {
console.log('拍照返回结果', result);
// 上传图片
const img1 = document.createElement('img');
img1.src = URL.createObjectURL(result);
img1.style.width = '100%';
img1.style.height = '100%';
document.body.appendChild(img1);
}
</script>
compressImage.ts文件压缩
// 压缩图片质量
export const compressImage = (file: File, callback: (file: File) => void) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
const img = new Image();
img.src = e.target?.result as string;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const MAX_WIDTH = 800;
const MAX_HEIGHT = 800;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
ctx?.drawImage(img, 0, 0, width, height);
// const newFile = new File([canvas.toDataURL('image/jpeg', 0.8).split(',')[1]], file.name, { type: 'image/jpeg' });
// callback(newFile);
canvas.toBlob((blob) => {
if (blob) {
const newFile = new File([blob], file.name, { type: 'image/jpeg' });
callback(newFile);
}
}, 'image/jpeg', 0.9); // 0.8 为压缩质量
};
};
};