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

takePicture.vue(自定义拍照组件)
<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 为压缩质量
};
};
};
非vue版本代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义拍照上传</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #000;
color: #fff;
overflow: hidden;
}
.container {
position: relative;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
.take-picture {
width: 100%;
height: 80vh;
object-fit: cover;
background-color: #222;
}
.wrapper_btm {
display: flex;
width: 100%;
height: 20vh;
background: rgb(10, 10, 10);
flex-wrap: wrap;
align-content: center;
justify-content: center;
position: absolute;
bottom: 0;
left: 0;
}
.take-picture-btn {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: #fff;
border: none;
color: #000;
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.open_album, .close_picture {
font-size: 1rem;
position: absolute;
top: 50%;
transform: translateY(-50%);
color: #fff;
cursor: pointer;
}
.open_album {
left: 25%;
}
.close_picture {
right: 25%;
}
/* Toast 通知样式 */
.toast-container {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
max-width: 90%;
}
.toast {
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 10px;
text-align: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeIn 0.3s, fadeOut 0.3s 2.7s forwards;
}
.toast.danger {
background-color: rgba(231, 76, 60, 0.9);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* 按钮样式 */
.van-button {
display: inline-block;
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #07c;
color: white;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.van-button:active {
background-color: #005ba3;
}
.van-button.primary {
background-color: #07c;
}
</style>
</head>
<body>
<div class="container">
<div id="image-container" class="take-picture" style="display: none;">
<img id="captured-image" alt="Captured image" style="width: 100%; height: 100%; object-fit: contain;">
</div>
<video id="video" class="take-picture" autoplay playsinline></video>
<div id="result-buttons" class="wrapper_btm" style="display: none;">
<span class="open_album" onclick="closePicture()">取消</span>
<button type="button" onclick="refreshPicture()" class="take-picture-btn">返回</button>
<span class="close_picture" onclick="submitResult()">确定</span>
</div>
<div id="camera-buttons" class="wrapper_btm">
<input type="file" id="gallery-input" accept="image/*" style="display: none;">
<span class="open_album" onclick="openGallery()">相册</span>
<button onclick="handleShoot()" class="take-picture-btn">拍照</button>
<span class="close_picture" onclick="closePicture()">关闭</span>
</div>
</div>
<div id="toast-container" class="toast-container"></div>
<script>
// 全局变量
let videoElement = null;
let imageContainer = null;
let capturedImage = null;
let galleryInput = null;
let cameraButtons = null;
let resultButtons = null;
let imgUrl = '';
let imgResult = null;
let stream = null;
// 初始化函数
function init() {
videoElement = document.getElementById('video');
imageContainer = document.getElementById('image-container');
capturedImage = document.getElementById('captured-image');
galleryInput = document.getElementById('gallery-input');
cameraButtons = document.getElementById('camera-buttons');
resultButtons = document.getElementById('result-buttons');
// 添加文件选择事件监听
galleryInput.addEventListener('change', handleFileChange);
// 初始化相机
getMediaStream();
}
// 获取媒体流
async function getMediaStream() {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
audio: false,
});
videoElement.srcObject = stream;
} catch (error) {
console.error('无法获取媒体流', error);
showNotify('无法获取媒体流', 'danger');
}
}
// 拍照功能
function handleShoot() {
if (!videoElement) return;
// 创建canvas进行截图
const canvas = document.createElement("canvas");
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
// 获取canvas上下文
const ctx = canvas.getContext("2d");
// 截图操作
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
// 转为DataURL
const imgDataUrl = canvas.toDataURL("image/jpeg");
// 转为Blob对象
canvas.toBlob((blob) => {
if (blob) {
imgResult = new File([blob], 'takePicture.jpg', { type: 'image/jpeg' });
}
}, 'image/jpeg', 0.8);
// 显示图片
capturedImage.src = imgDataUrl;
imgUrl = imgDataUrl;
// 切换显示
videoElement.style.display = 'none';
imageContainer.style.display = 'block';
cameraButtons.style.display = 'none';
resultButtons.style.display = 'flex';
// 停止视频流
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
}
// 打开相册
function openGallery() {
galleryInput.click();
}
// 处理文件选择
function handleFileChange(e) {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
compressImage(file, (compressedFile) => {
// 这里应该触发一个事件,但简化处理直接显示
capturedImage.src = URL.createObjectURL(compressedFile);
imgResult = compressedFile;
// 切换显示
videoElement.style.display = 'none';
imageContainer.style.display = 'block';
cameraButtons.style.display = 'none';
resultButtons.style.display = 'flex';
// 关闭相机
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
});
}
}
// 图片压缩函数
function compressImage(file, callback) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(event) {
const img = new Image();
img.src = event.target.result;
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置最大尺寸
let width = img.width;
let height = img.height;
const maxWidth = 1200;
const maxHeight = 1200;
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(function(blob) {
const compressedFile = new File([blob], file.name, {
type: 'image/jpeg',
lastModified: Date.now()
});
callback(compressedFile);
}, 'image/jpeg', 0.7);
};
};
}
// 重置相机
function refreshPicture() {
// 清除图片
capturedImage.src = '';
imgUrl = '';
// 切换显示
imageContainer.style.display = 'none';
videoElement.style.display = 'block';
resultButtons.style.display = 'none';
cameraButtons.style.display = 'flex';
// 重新获取媒体流
getMediaStream();
}
// 关闭相机
function closePicture() {
// 停止媒体流
if (stream) {
stream.getTracks().forEach(track => track.stop());
}
// 清除图片
capturedImage.src = '';
imgUrl = '';
// 切换显示
imageContainer.style.display = 'none';
videoElement.style.display = 'block';
resultButtons.style.display = 'none';
cameraButtons.style.display = 'flex';
// 在实际应用中,这里应该触发一个关闭事件
console.log("相机已关闭");
}
// 提交结果
function submitResult() {
if (imgResult) {
// 在实际应用中,这里应该触发一个结果事件
console.log("图片已提交", imgResult);
showToast('图片已保存');
} else {
showNotify('没有可提交的图片', 'danger');
}
closePicture();
}
// 显示Toast
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
const container = document.getElementById('toast-container');
container.appendChild(toast);
// 3秒后移除toast
setTimeout(() => {
container.removeChild(toast);
}, 3000);
}
// 显示通知
function showNotify(message, type = '') {
const toast = document.createElement('div');
toast.className = type ? `toast ${type}` : 'toast';
toast.textContent = message;
const container = document.getElementById('toast-container');
container.appendChild(toast);
// 3秒后移除通知
setTimeout(() => {
container.removeChild(toast);
}, 3000);
}
// 页面加载完成后初始化
window.addEventListener('load', init);
</script>
</body>
</html>

896






