|
|
@@ -0,0 +1,239 @@
|
|
|
+<template>
|
|
|
+ <div class="camera-container">
|
|
|
+ <h1>相机Demo</h1>
|
|
|
+
|
|
|
+ <div class="camera-section">
|
|
|
+ <!-- 视频元素,用于捕获相机流但不直接显示 -->
|
|
|
+ <video ref="videoElement" autoplay playsinline style="display: none;"></video>
|
|
|
+
|
|
|
+ <!-- Canvas元素,用于渲染相机画面 -->
|
|
|
+ <canvas ref="canvasElement" class="camera-canvas"></canvas>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="controls">
|
|
|
+ <button @click="startCamera" v-if="!isCameraActive">启动相机</button>
|
|
|
+ <button @click="stopCamera" v-if="isCameraActive">停止相机</button>
|
|
|
+ <button @click="takePhoto" v-if="isCameraActive">拍照</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="photo-preview" v-if="capturedImage">
|
|
|
+ <h3>拍摄预览</h3>
|
|
|
+ <img :src="capturedImage" alt="拍摄的照片" class="preview-image">
|
|
|
+ <button @click="savePhoto">保存照片</button>
|
|
|
+ <button @click="clearPhoto">清除</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="status" v-if="statusMessage">
|
|
|
+ {{ statusMessage }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, onUnmounted } from 'vue';
|
|
|
+
|
|
|
+// 引用DOM元素
|
|
|
+const videoElement = ref(null);
|
|
|
+const canvasElement = ref(null);
|
|
|
+const canvasContext = ref(null);
|
|
|
+
|
|
|
+// 状态变量
|
|
|
+const isCameraActive = ref(false);
|
|
|
+const capturedImage = ref(null);
|
|
|
+const statusMessage = ref('');
|
|
|
+let animationId = null;
|
|
|
+
|
|
|
+// 组件挂载时初始化canvas
|
|
|
+onMounted(() => {
|
|
|
+ if (canvasElement.value) {
|
|
|
+ canvasContext.value = canvasElement.value.getContext('2d');
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// 启动相机
|
|
|
+const startCamera = async () => {
|
|
|
+ try {
|
|
|
+ statusMessage.value = '正在请求相机权限...';
|
|
|
+
|
|
|
+ // 请求相机权限并获取视频流
|
|
|
+ const stream = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: {
|
|
|
+ width: { ideal: 1280 },
|
|
|
+ height: { ideal: 720 }
|
|
|
+ },
|
|
|
+ audio: false
|
|
|
+ });
|
|
|
+
|
|
|
+ // 将视频流设置到视频元素
|
|
|
+ if (videoElement.value) {
|
|
|
+ videoElement.value.srcObject = stream;
|
|
|
+ isCameraActive.value = true;
|
|
|
+ statusMessage.value = '相机已启动';
|
|
|
+
|
|
|
+ // 等待视频元素准备就绪
|
|
|
+ videoElement.value.onloadedmetadata = () => {
|
|
|
+ // 设置canvas尺寸与视频尺寸匹配
|
|
|
+ if (canvasElement.value && canvasContext.value) {
|
|
|
+ canvasElement.value.width = videoElement.value.videoWidth;
|
|
|
+ canvasElement.value.height = videoElement.value.videoHeight;
|
|
|
+ // 开始渲染视频到canvas
|
|
|
+ startRendering();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('相机访问错误:', error);
|
|
|
+ statusMessage.value = `相机访问失败: ${error.message}`;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 渲染视频到canvas
|
|
|
+const startRendering = () => {
|
|
|
+ if (!videoElement.value || !canvasElement.value || !canvasContext.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const render = () => {
|
|
|
+ if (videoElement.value && canvasContext.value) {
|
|
|
+ // 将视频帧绘制到canvas
|
|
|
+ canvasContext.value.drawImage(
|
|
|
+ videoElement.value,
|
|
|
+ 0, 0,
|
|
|
+ canvasElement.value.width,
|
|
|
+ canvasElement.value.height
|
|
|
+ );
|
|
|
+
|
|
|
+ // 继续下一帧渲染
|
|
|
+ animationId = requestAnimationFrame(render);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 开始渲染循环
|
|
|
+ animationId = requestAnimationFrame(render);
|
|
|
+};
|
|
|
+
|
|
|
+// 停止相机
|
|
|
+const stopCamera = () => {
|
|
|
+ if (videoElement.value && videoElement.value.srcObject) {
|
|
|
+ // 停止渲染循环
|
|
|
+ if (animationId) {
|
|
|
+ cancelAnimationFrame(animationId);
|
|
|
+ animationId = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 停止所有视频轨道
|
|
|
+ const tracks = videoElement.value.srcObject.getTracks();
|
|
|
+ tracks.forEach(track => track.stop());
|
|
|
+ videoElement.value.srcObject = null;
|
|
|
+
|
|
|
+ isCameraActive.value = false;
|
|
|
+ statusMessage.value = '相机已停止';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 拍照功能
|
|
|
+const takePhoto = () => {
|
|
|
+ if (canvasElement.value && canvasContext.value) {
|
|
|
+ // 将canvas内容转换为图片URL
|
|
|
+ capturedImage.value = canvasElement.value.toDataURL('image/png');
|
|
|
+ statusMessage.value = '已拍照';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 保存照片功能
|
|
|
+const savePhoto = () => {
|
|
|
+ if (capturedImage.value) {
|
|
|
+ // 创建下载链接
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = capturedImage.value;
|
|
|
+ // 设置文件名,使用时间戳确保唯一性
|
|
|
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
|
+ link.download = `camera-photo-${timestamp}.png`;
|
|
|
+ // 触发下载
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ statusMessage.value = '照片已保存';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 清除照片预览
|
|
|
+const clearPhoto = () => {
|
|
|
+ capturedImage.value = null;
|
|
|
+ statusMessage.value = '';
|
|
|
+};
|
|
|
+
|
|
|
+// 组件卸载时清理资源
|
|
|
+onUnmounted(() => {
|
|
|
+ stopCamera();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.camera-container {
|
|
|
+ max-width: 800px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 20px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-section {
|
|
|
+ margin: 20px 0;
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.camera-canvas {
|
|
|
+ border: 2px solid #ccc;
|
|
|
+ border-radius: 8px;
|
|
|
+ max-width: 100%;
|
|
|
+ background-color: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.controls {
|
|
|
+ margin: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+button {
|
|
|
+ padding: 10px 20px;
|
|
|
+ margin: 0 10px;
|
|
|
+ font-size: 16px;
|
|
|
+ background-color: #4CAF50;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+button:hover {
|
|
|
+ background-color: #45a049;
|
|
|
+}
|
|
|
+
|
|
|
+button:active {
|
|
|
+ background-color: #3e8e41;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-preview {
|
|
|
+ margin-top: 30px;
|
|
|
+ padding: 20px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 8px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-image {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 400px;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.status {
|
|
|
+ margin-top: 20px;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f0f0f0;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+</style>
|