浏览代码

AI实验室
1、新增相机拍照上传功能

liyanbo 5 月之前
父节点
当前提交
ac5f28fac8
共有 4 个文件被更改,包括 211 次插入269 次删除
  1. 211 26
      src/components/ImageUpload/index.vue
  2. 0 2
      src/router/index.js
  3. 0 239
      src/views/CameraDemo.vue
  4. 0 2
      src/views/block/MapGame.vue

+ 211 - 26
src/components/ImageUpload/index.vue

@@ -7,20 +7,21 @@
       style="display: none"
       @change="handleFileSelect"
     />
-    <button 
-      class="upload-btn"
-      @click="triggerFileSelect"
-      :disabled="isUploading"
-      title="上传参考图"
-    >
-    上传参考图
-      <el-icon><Picture /></el-icon>
-    </button>
+    
+    <div class="combined-upload-btn">
+      <span class="upload-part" @click="triggerFileSelect" :title="'上传参考图'">
+        上传参考图
+        <el-icon><Picture /></el-icon>
+      </span>
+      <span class="separator">|</span>
+      <span class="camera-part" @click="openCamera" :title="'使用相机拍照'">
+        <el-icon><Camera /></el-icon>
+      </span>
+    </div>
 
     <!-- 预览图显示区域 - 使用el-image组件 -->
     <div v-if="previewImage && previewImage !== '/src/assets/images/default-preview.png'" class="preview-container">
       <div class="image-wrapper">
-        <!-- 删除按钮 - 确保显示 -->
         <button class="delete-btn" @click="removeImage">×</button>
         <!-- 使用el-image组件实现预览功能 -->
         <el-image
@@ -49,12 +50,37 @@
         </el-image>
       </div>
     </div>
+
+    <!-- 相机弹窗 -->
+    <el-dialog
+      v-model="cameraVisible"
+      title="相机拍照"
+      width="700px"
+      @close="closeCamera"
+    >
+      <div class="camera-container">
+        <div v-if="!isCapturing" class="camera-view">
+          <video ref="videoRef" autoplay muted playsinline></video>
+        </div>
+        <div v-else class="captured-image-view">
+          <img :src="capturedImage" alt="已拍摄照片" />
+        </div>
+      </div>
+      <template #footer>
+        <div class="camera-actions">
+          <el-button @click="closeCamera">关闭</el-button>
+          <el-button v-if="!isCapturing" @click="takePhoto">拍照</el-button>
+          <el-button v-else @click="retakePhoto">重拍</el-button>
+          <el-button v-if="isCapturing" type="primary" @click="confirmAndUpload">确认并上传</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
-import { ref } from 'vue';
-import { Picture, ZoomOut, ZoomIn, RefreshRight, RefreshLeft, Refresh, Download } from '@element-plus/icons-vue';
+import { ref, watch, nextTick } from 'vue';
+import { Picture, ZoomOut, ZoomIn, RefreshRight, RefreshLeft, Refresh, Download, Camera } from '@element-plus/icons-vue';
 import { Message } from '@/utils/message/Message.js';
 // 导入axios用于文件上传
 import axios from 'axios';
@@ -77,6 +103,20 @@ const isUploading = ref(false);
 // 预览图片URL
 const previewImage = ref('');
 
+// 相机相关状态变量
+const cameraVisible = ref(false); // 相机弹窗显示状态
+const stream = ref(null); // 媒体流
+const capturedImage = ref(''); // 拍摄的照片
+const isCapturing = ref(false); // 是否正在拍摄中
+const videoRef = ref(null); // 视频元素引用
+
+// 监听modelValue变化,更新预览图
+watch(() => props.modelValue, (newValue) => {
+  if (newValue && newValue !== previewImage.value) {
+    previewImage.value = newValue;
+  }
+}, { immediate: true });
+
 // 触发文件选择
 const triggerFileSelect = () => {
   fileInput.value?.click();
@@ -100,13 +140,17 @@ const handleFileSelect = async (event) => {
     return;
   }
 
+  await uploadFile(file);
+};
+
+// 抽取文件上传逻辑为单独的函数,供文件选择和相机拍摄共用
+const uploadFile = async (file) => {
   try {
     isUploading.value = true;
     // 创建FormData对象
     const formData = new FormData();
     formData.append('file', file);
     
-    
     // 构建上传地址
     const uploadUrl = import.meta.env.VITE_BASE_URL + '/infra/file/upload';
     
@@ -119,7 +163,6 @@ const handleFileSelect = async (event) => {
       timeout: 60000
     });
     
-    
     let imageUrl = '';
     if (response.data?.data) {
       // 去除反引号和可能的空格
@@ -138,8 +181,8 @@ const handleFileSelect = async (event) => {
     isUploading.value = false;
   } finally {
     // 清空input值,允许重复选择同一文件
-    if (event.target) {
-      event.target.value = '';
+    if (fileInput.value) {
+      fileInput.value.value = '';
     }
   }
 };
@@ -152,7 +195,8 @@ const clearPreview = () => {
 
 // 暴露方法给父组件
 defineExpose({
-  clearPreview
+  clearPreview,
+  previewImage
 });
 
 // 移除图片
@@ -171,6 +215,96 @@ const download = (index) => {
   link.click();
   document.body.removeChild(link);
 };
+
+// 打开相机弹窗
+const openCamera = async () => {
+  try {
+    // 获取用户媒体设备权限
+    stream.value = await navigator.mediaDevices.getUserMedia({
+      video: true
+    });
+    // 先显示弹窗,让DOM渲染完成
+    cameraVisible.value = true;
+    isCapturing.value = false;
+
+    // 使用nextTick确保DOM已经更新,再设置视频源
+    nextTick(() => {
+      if (videoRef.value) {
+        videoRef.value.srcObject = stream.value;
+      }
+    });
+  } catch (error) {
+    console.error('获取相机权限失败:', error);
+    Message().error('无法访问相机,请确保已授予相机权限!', true);
+  }
+};
+
+// 关闭相机弹窗
+const closeCamera = () => {
+  if (stream.value) {
+    stream.value.getTracks().forEach(track => track.stop());
+    stream.value = null;
+  }
+  cameraVisible.value = false;
+  capturedImage.value = '';
+  isCapturing.value = false;
+};
+
+// 拍照功能
+const takePhoto = () => {
+  const canvas = document.createElement('canvas');
+  const video = videoRef.value;
+  canvas.width = video.videoWidth;
+  canvas.height = video.videoHeight;
+  const context = canvas.getContext('2d');
+  context.drawImage(video, 0, 0, canvas.width, canvas.height);
+  capturedImage.value = canvas.toDataURL('image/jpeg');
+  isCapturing.value = true;
+};
+
+// 重拍功能 - 修复不显示相机实况的问题
+const retakePhoto = async () => {
+  capturedImage.value = '';
+  isCapturing.value = false;
+  
+  // 确保视频元素已经渲染且媒体流存在
+  await nextTick();
+  
+  if (videoRef.value && stream.value) {
+    // 确保视频元素的srcObject正确设置为媒体流
+    if (videoRef.value.srcObject !== stream.value) {
+      videoRef.value.srcObject = stream.value;
+    }
+    
+    // 尝试重新播放视频(在某些浏览器中可能需要)
+    try {
+      await videoRef.value.play();
+    } catch (error) {
+      console.warn('自动播放视频失败:', error);
+    }
+  }
+};
+
+// 确认并上传照片
+const confirmAndUpload = async () => {
+  if (capturedImage.value) {
+    try {
+      // 创建Blob对象
+      const response = await fetch(capturedImage.value);
+      const blob = await response.blob();
+      const file = new File([blob], `camera_photo_${Date.now()}.jpg`, { type: 'image/jpeg' });
+
+      // 调用共用的上传函数
+      await uploadFile(file);
+      
+      // 关闭相机弹窗
+      closeCamera();
+    } catch (error) {
+      console.error('相机拍摄图片上传失败:', error);
+      Message().error('相机拍摄图片上传失败,请重试!', true);
+    }
+  }
+};
 </script>
 
 <style scoped lang="scss">
@@ -185,29 +319,59 @@ const download = (index) => {
   display: inline-block;
 }
 
-.upload-btn {
-  padding: rpx(5) rpx(10);
+// 合并按钮样式
+.combined-upload-btn {
+  display: inline-flex;
+  align-items: center;
   background: #fff;
   border: 1px solid #ffce1b;
   box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
   border-radius: rpx(5);
-  cursor: pointer;
+  overflow: hidden;
+  font-size: rpx(6);
+  cursor: default;
+}
+
+.upload-part {
+  padding: rpx(5) rpx(10);
   display: flex;
   align-items: center;
   gap: rpx(3);
-  font-size: rpx(6);
+  cursor: pointer;
+  transition: background-color 0.2s;
 
-  .el-icon {
-    font-size: rpx(8);
-    color: #666;
+  &:hover {
+    background-color: rgba(255, 206, 27, 0.1);
   }
 
-  &:disabled {
+  &.disabled {
     opacity: 0.6;
     cursor: not-allowed;
   }
 }
 
+.separator {
+  color: #ccc;
+  font-size: rpx(8);
+}
+
+.camera-part {
+  padding: rpx(5) rpx(10);
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  transition: background-color 0.2s;
+
+  &:hover {
+    background-color: rgba(255, 206, 27, 0.1);
+  }
+}
+
+.el-icon {
+  font-size: rpx(8);
+  color: #666;
+}
+
 // 预览图样式
 .preview-container {
   position: absolute;
@@ -233,7 +397,6 @@ const download = (index) => {
   border-radius: rpx(3);
 }
 
-// 删除按钮样式 - 确保正确显示
 .delete-btn {
   position: absolute;
   top: rpx(3);
@@ -266,4 +429,26 @@ const download = (index) => {
 .delete-btn:hover {
   opacity: 1;
 }
+
+// 相机弹窗样式
+.camera-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 20px 0;
+}
+
+.camera-view video {
+  max-width: 100%;
+  height: auto;
+  border: 1px solid #ddd;
+  border-radius: 5px;
+}
+
+.captured-image-view img {
+  max-width: 100%;
+  height: auto;
+  border: 1px solid #ddd;
+  border-radius: 5px;
+}
 </style>

+ 0 - 2
src/router/index.js

@@ -5,8 +5,6 @@ import App from '../App.vue'
 const routes = [
 
   { path: '/', component: () => import('../views/Login.vue') },
-  // 相机Demo页面
-  { path: '/camera-demo', component: () => import('../views/CameraDemo.vue') },
   { path: '/login', component: () => import('../views/Login.vue') },
   // 免登录
   { path: '/quick-login', component: () => import('../views/QuickLogin.vue') },

+ 0 - 239
src/views/CameraDemo.vue

@@ -1,239 +0,0 @@
-<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>

+ 0 - 2
src/views/block/MapGame.vue

@@ -11,7 +11,6 @@
     <div class="content">
       <!-- 地图显示区域 -->
       <div class="map-section">
-        <h2>游戏地图</h2>
         <div class="map-container">
           <!-- 地图背景 -->
           <div class="map-background">
@@ -84,7 +83,6 @@
         </div>
 
         <div class="workspace-section">
-          <h2>工作区</h2>
           <div class="controls">
             <button id="runCode" @click="runCode">运行代码</button>
             <button @click="clearWorkspace">清空工作区</button>