vue3+h5实现自定义拍照功能

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

效果图:
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/72d14669688c443b840031284b03b4cb.png
在这里插入图片描述
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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值