Jelajahi Sumber

Merge branch 'master' of http://59.110.91.129:3000/zhangmengying/AIClass into wanzi

丸子 3 bulan lalu
induk
melakukan
33637508a6

+ 15 - 4
src/api/blockly/blockly.js

@@ -322,7 +322,7 @@ const availableBlocks = {
   play_sound: {
     jsonConfig: {
       "type": "play_sound",
-      "message0": "%1 声音",
+      "message0": "%1 声音: %2",
       "args0": [
         {
           "type": "field_image",
@@ -330,12 +330,19 @@ const availableBlocks = {
           "width": 20,
           "height": 20,
           "alt": "声音"
+        },
+        {
+          "type": "field_dropdown",
+          "name": "SOUND",
+          "options": [
+            ["打招呼", "打招呼"],
+          ]
         }
       ],
       "previousStatement": null,
       "nextStatement": null,
       "colour": 160,
-      "tooltip": "自动触发特效提示音,并根据当前方格类型执行相应操作",
+      "tooltip": "播放指定的声音特效",
       "helpUrl": ""
     },
     isGeneral: false
@@ -407,7 +414,9 @@ const availableGenerators = {
     return `await pause(${seconds});\n`;
   },
   play_sound: function(block) {
-    return 'await playSound();\n';
+    const sound = block.getFieldValue('SOUND');
+    return `await playSound('${sound}');
+`;
   },
   construct: function(block) {
     return 'await construct();\n';
@@ -458,7 +467,9 @@ const availablePythonGenerators = {
     return `pause(${seconds})\n`;
   },
   play_sound: function(block) {
-    return 'play_sound()\n';
+    const sound = block.getFieldValue('SOUND');
+    return `play_sound('${sound}')
+`;
   },
   construct: function(block) {
     return 'construct()\n';

+ 4 - 0
src/api/blockly/music.js

@@ -96,6 +96,10 @@ export const onMusicEnded = (state) => {
 
 // 播放mp3文件
 export const playMp3 = async function(audioUrl) {
+    if (!audioUrl) {
+        console.error('音频URL为空');
+        return;
+    }
     try {
         // 创建音频对象,播放声音特效
         const soundEffect = new Audio(audioUrl);

+ 150 - 45
src/components/ImageUpload/index.vue

@@ -1,13 +1,13 @@
 <template>
   <div class="image-upload-container">
     <input
-      type="file"
-      ref="fileInput"
-      accept="image/*"
-      style="display: none"
-      @change="handleFileSelect"
+        type="file"
+        ref="fileInput"
+        accept="image/*"
+        style="display: none"
+        @change="handleFileSelect"
     />
-    
+
     <div class="combined-upload-btn">
       <span class="upload-part" @click="triggerFileSelect" :title="'上传参考图'">
         上传参考图
@@ -25,22 +25,22 @@
         <button class="delete-btn" @click="removeImage">×</button>
         <!-- 使用el-image组件实现预览功能 -->
         <el-image
-          :src="previewImage"
-          :preview-src-list="[previewImage]"
-          fit="cover"
-          show-progress
-          class="preview-image"
+            :src="previewImage"
+            :preview-src-list="[previewImage]"
+            fit="cover"
+            show-progress
+            class="preview-image"
         >
           <template
-            #toolbar="{ actions,  reset, activeIndex }"
+              #toolbar="{ actions,  reset, activeIndex }"
           >
             <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
             <el-icon
-              @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
+                @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })">
               <ZoomIn />
             </el-icon>
             <el-icon
-              @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
+                @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })">
               <RefreshRight />
             </el-icon>
             <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
@@ -53,10 +53,12 @@
 
     <!-- 相机弹窗 -->
     <el-dialog
-      v-model="cameraVisible"
-      title="相机拍照"
-      width="700px"
-      @close="closeCamera"
+        v-model="cameraVisible"
+        title="相机拍照"
+        :width="isSmallScreen ? '100%' : '90vw'"
+        :fullscreen="isSmallScreen"
+        @close="closeCamera"
+        class="camera-dialog"
     >
       <div class="camera-container">
         <div v-if="!isCapturing" class="camera-view">
@@ -79,7 +81,7 @@
 </template>
 
 <script setup>
-import { ref, watch, nextTick } from 'vue';
+import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
 import { Picture, ZoomOut, ZoomIn, RefreshRight, RefreshLeft, Refresh, Download, Camera } from '@element-plus/icons-vue';
 import { Message } from '@/utils/message/Message.js';
 // 导入axios用于文件上传
@@ -109,6 +111,21 @@ const stream = ref(null); // 媒体流
 const capturedImage = ref(''); // 拍摄的照片
 const isCapturing = ref(false); // 是否正在拍摄中
 const videoRef = ref(null); // 视频元素引用
+const isSmallScreen = ref(false); // 是否为小屏设备
+
+// 监听窗口大小变化
+const handleResize = () => {
+  isSmallScreen.value = window.innerWidth < 768;
+};
+
+onMounted(() => {
+  handleResize();
+  window.addEventListener('resize', handleResize);
+});
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize);
+});
 
 // 监听modelValue变化,更新预览图
 watch(() => props.modelValue, (newValue) => {
@@ -150,10 +167,10 @@ const uploadFile = async (file) => {
     // 创建FormData对象
     const formData = new FormData();
     formData.append('file', file);
-    
+
     // 构建上传地址
     const uploadUrl = import.meta.env.VITE_BASE_URL + '/infra/file/upload';
-    
+
     // 发送文件上传请求
     const response = await axios.post(uploadUrl, formData, {
       headers: {
@@ -162,13 +179,13 @@ const uploadFile = async (file) => {
       },
       timeout: 60000
     });
-    
+
     let imageUrl = '';
     if (response.data?.data) {
       // 去除反引号和可能的空格
       imageUrl = response.data.data.replace(/[`\s]/g, '');
     }
-    
+
     // 更新预览图
     previewImage.value = imageUrl;
     // 触发更新事件
@@ -219,10 +236,38 @@ const download = (index) => {
 // 打开相机弹窗
 const openCamera = async () => {
   try {
-    // 获取用户媒体设备权限
-    stream.value = await navigator.mediaDevices.getUserMedia({
-      video: true
-    });
+    // 获取相机容器的尺寸
+    const containerWidth = window.innerWidth * (isSmallScreen.value ? 0.9 : 0.8);
+    const containerHeight = window.innerHeight * (isSmallScreen.value ? 0.7 : 0.6);
+    
+    // 定义相机视频的最大分辨率
+    const videoConstraints = {
+      width: { ideal: containerWidth, max: containerWidth * 1.2 },
+      height: { ideal: containerHeight, max: containerHeight * 1.2 },
+      frameRate: { ideal: 30, max: 60 }
+    };
+    
+    // 尝试优先使用后摄像头(不带exact,提高兼容性)
+    try {
+      stream.value = await navigator.mediaDevices.getUserMedia({
+        video: { ...videoConstraints, facingMode: 'environment' }
+      });
+    } catch (backCameraError) {
+      console.log('后摄像头不可用,尝试使用前摄像头:', backCameraError);
+      // 后摄像头不可用,尝试使用前摄像头
+      try {
+        stream.value = await navigator.mediaDevices.getUserMedia({
+          video: { ...videoConstraints, facingMode: 'user' }
+        });
+      } catch (frontCameraError) {
+        console.log('前摄像头也不可用,尝试默认设置:', frontCameraError);
+        // 前摄像头也不可用,使用默认设置
+        stream.value = await navigator.mediaDevices.getUserMedia({ 
+          video: videoConstraints 
+        });
+      }
+    }
+
     // 先显示弹窗,让DOM渲染完成
     cameraVisible.value = true;
     isCapturing.value = false;
@@ -257,14 +302,20 @@ const takePhoto = () => {
   canvas.width = video.videoWidth;
   canvas.height = video.videoHeight;
   const context = canvas.getContext('2d');
-  
-  // 应用镜像效果
-  context.save();
-  context.translate(canvas.width, 0); // 平移到右侧
-  context.scale(-1, 1); // 水平翻转
-  context.drawImage(video, 0, 0, canvas.width, canvas.height);
-  context.restore();
-  
+
+  // 应用镜像效果(仅对前摄像头)
+  const isFrontCamera = stream.value?.getVideoTracks()[0]?.getSettings().facingMode === 'user';
+
+  if (isFrontCamera) {
+    context.save();
+    context.translate(canvas.width, 0); // 平移到右侧
+    context.scale(-1, 1); // 水平翻转
+    context.drawImage(video, 0, 0, canvas.width, canvas.height);
+    context.restore();
+  } else {
+    context.drawImage(video, 0, 0, canvas.width, canvas.height);
+  }
+
   capturedImage.value = canvas.toDataURL('image/jpeg');
   isCapturing.value = true;
 };
@@ -273,16 +324,16 @@ const takePhoto = () => {
 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();
@@ -303,7 +354,7 @@ const confirmAndUpload = async () => {
 
       // 调用共用的上传函数
       await uploadFile(file);
-      
+
       // 关闭相机弹窗
       closeCamera();
     } catch (error) {
@@ -399,7 +450,7 @@ const confirmAndUpload = async () => {
 
 .preview-image {
   width: rpx(90);
-   max-height: rpx(150);
+  max-height: rpx(150);
   object-fit: contain;
   border-radius: rpx(3);
 }
@@ -438,26 +489,80 @@ const confirmAndUpload = async () => {
 }
 
 // 相机弹窗样式
+.camera-dialog .el-dialog__body {
+  padding: 0;
+  overflow: hidden;
+}
+
 .camera-container {
   display: flex;
   justify-content: center;
   align-items: center;
-  padding: 20px 0;
+  padding: 0;
+  max-width: 100%;
+  max-height: 100%;
+  height: calc(100vh - 150px);
+  overflow: hidden;
+}
+
+.camera-view {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }
 
 .camera-view video {
   max-width: 100%;
-  height: auto;
+  max-height: 100%;
+  object-fit: contain;
   border: 1px solid #ddd;
   border-radius: 5px;
-  transform: scaleX(-1);
+}
+
+.captured-image-view {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  display: flex;
+  justify-content: center;
+  align-items: center;
 }
 
 .captured-image-view img {
   max-width: 100%;
-  height: auto;
+  max-height: 100%;
+  object-fit: contain;
   border: 1px solid #ddd;
   border-radius: 5px;
-  transform: scaleX(-1);
+}
+
+// 小屏设备样式
+@media (max-width: 768px) {
+  .camera-container {
+    padding: 10px 0;
+    height: calc(100vh - 150px);
+    overflow: hidden;
+  }
+
+  .camera-view video {
+    width: 100%;
+    height: 100%;
+    max-width: none;
+    max-height: none;
+    object-fit: contain;
+    border-radius: 0;
+    border: none;
+  }
+
+  .captured-image-view img {
+    max-width: 100%;
+    max-height: 100%;
+    object-fit: contain;
+    border-radius: 0;
+    border: none;
+  }
 }
 </style>

+ 35 - 6
src/components/ai/image/ImageToImage.vue

@@ -653,15 +653,27 @@ const download = (event, activeIndex) => {
   display: flex;
   padding: rpx(10);
   gap: rpx(5);
+  align-items: center;
+  align-content: center;
+  flex-wrap: nowrap;
+  min-height: rpx(30);
+  
+  > * {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: rpx(20);
+    box-sizing: border-box;
+  }
+  
   .speech-btn {
     padding: rpx(5) rpx(10);
     background: #fff;
     border: 1px solid #ffce1b;
     border-radius: rpx(5);
     cursor: pointer;
-    display: flex;
-    align-items: center;
     gap: rpx(4);
+    height: 100%;
     &.recording {
       background: #ffeeba;
       border-color: #ffc107;
@@ -679,24 +691,41 @@ const download = (event, activeIndex) => {
       transition: transform 0.3s ease;
     }
   }
+  
   // 终止按钮样式
   .stop-btn {
     cursor: pointer;
-    display: flex;
-    align-items: center;
+    height: 100%;
     img {
       width: rpx(20);
       height: rpx(20);
     }
   }
+  
+  // 确保图片上传组件垂直居中
+  :deep(.el-upload) {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  
+  // 确保语音输入组件垂直居中
+  :deep(.voice-input-container) {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
 }
+
 .input-section input {
   flex: 1;
   padding: rpx(5);
   font-size: rpx(7);
   border: 1px solid #ccc;
   border-radius: rpx(5);
+  min-width: rpx(50);
 }
+
 .input-section button {
   padding: rpx(5) rpx(15);
   background: linear-gradient(
@@ -709,8 +738,8 @@ const download = (event, activeIndex) => {
   font-size: rpx(7);
   border-radius: rpx(5);
   cursor: pointer;
-    box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
-
+  box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
+  white-space: nowrap;
 }
 
 .image-upload-section {

+ 35 - 6
src/components/ai/video/ImageToVideo.vue

@@ -534,14 +534,26 @@ const inProgressTimerFun = () => {
   display: flex;
   padding: rpx(10);
   gap: rpx(5);
+  align-items: center;
+  align-content: center;
+  flex-wrap: nowrap;
+  min-height: rpx(30);
+  
+  > * {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: rpx(20);
+    box-sizing: border-box;
+  }
+  
   .speech-btn {
     padding: rpx(5) rpx(10);
     background: #fff;
     border: 1px solid #ffce1b;
     border-radius: rpx(5);
     cursor: pointer;
-    display: flex;
-    align-items: center;
+    height: 100%;
     &.recording {
       background: #ffeeba;
       border-color: #ffc107;
@@ -555,24 +567,41 @@ const inProgressTimerFun = () => {
       color: #666;
     }
   }
+  
   // 终止按钮样式
   .stop-btn {
     cursor: pointer;
-    display: flex;
-    align-items: center;
+    height: 100%;
     img {
       width: rpx(20);
       height: rpx(20);
     }
   }
+  
+  // 确保图片上传组件垂直居中
+  :deep(.el-upload) {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  
+  // 确保语音输入组件垂直居中
+  :deep(.voice-input-container) {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
 }
+
 .input-section input {
   flex: 1;
   padding: rpx(5);
   font-size: rpx(7);
   border: 1px solid #ccc;
   border-radius: rpx(5);
+  min-width: rpx(50);
 }
+
 .input-section button {
   padding: rpx(5) rpx(15);
   background: linear-gradient(
@@ -585,8 +614,8 @@ const inProgressTimerFun = () => {
   font-size: rpx(7);
   border-radius: rpx(5);
   cursor: pointer;
-    box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
-
+  box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
+  white-space: nowrap;
 }
 
 .image-upload-section {

+ 36 - 4
src/components/ai/vision/VisionThink.vue

@@ -498,16 +498,28 @@ const imageList = ref([]) // image 列表
 
 .input-section {
   display: flex;
+  align-items: center;
+  align-content: center;
   padding: rpx(10);
   gap: rpx(5);
+  flex-wrap: nowrap;
+  min-height: rpx(30);
+  
+  // 确保所有子组件容器也垂直居中
+  > * {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: rpx(20);
+    box-sizing: border-box;
+  }
+  
   .speech-btn {
     padding: rpx(5) rpx(10);
     background: #fff;
     border: 1px solid #ffce1b;
     border-radius: rpx(5);
     cursor: pointer;
-    display: flex;
-    align-items: center;
     &.recording {
       background: #ffeeba;
       border-color: #ffc107;
@@ -521,24 +533,28 @@ const imageList = ref([]) // image 列表
       color: #666;
     }
   }
+  
   // 终止按钮样式
   .stop-btn {
     cursor: pointer;
-    display: flex;
-    align-items: center;
     img {
       width: rpx(20);
       height: rpx(20);
     }
   }
 }
+
 .input-section input {
   flex: 1;
+  min-width: rpx(50);
   padding: rpx(5);
   font-size: rpx(7);
   border: 1px solid #ccc;
   border-radius: rpx(5);
+  height: rpx(20);
+  box-sizing: border-box;
 }
+
 .input-section button {
   padding: rpx(5) rpx(15);
   background: linear-gradient(
@@ -552,7 +568,23 @@ const imageList = ref([]) // image 列表
   border-radius: rpx(5);
   cursor: pointer;
   box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
+  white-space: nowrap;
+}
 
+// 图片上传组件的容器样式
+.input-section :deep(.el-upload) {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: rpx(20);
+}
+
+// 语音输入组件的容器样式
+.input-section :deep(.voice-input-container) {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: rpx(20);
 }
 
 .image-upload-section {

+ 11 - 4
src/components/blockly/MapGame.vue

@@ -300,6 +300,11 @@ const CONFIG = {
   }
 };
 
+//可播放的mp3类型(需要同步修改blockly.js)
+const BLOCKLY_PLAY_MP3_TYPE_DICT = {
+  "打招呼": passMp3,
+}
+
 // 路由和游戏状态
 const currentGameData = ref(null);
 const playerInitialDirection = ref(0); // 人物初始朝向
@@ -509,6 +514,7 @@ onMounted(async () => {
 });
 
 //================初始化=====================
+
 // 动态生成工具箱XML
 function generateToolboxXml() {
   let toolboxXml = `
@@ -1318,19 +1324,20 @@ window.pause = async function(seconds) {
 }
 
 // 声音函数
-window.playSound = async function() {
+window.playSound = async function(mp3FileName) {
   if (shouldStopExecution || isColliding.value || isSliding.value) {
     return;
   }
 
+  // 播放声音
   if(processingSpecialTasksDisappearing()){
     //延迟,确保声音播放完成
-    await playMp3(passMp3);
+    await playMp3(BLOCKLY_PLAY_MP3_TYPE_DICT[mp3FileName]);
     await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
     return
   }
 
-  //延迟,确保声音播放完成
+  //延迟,确保声音播放完成(错误)
   await playMp3(failureMp3);
   await new Promise(resolve => setTimeout(resolve, CONFIG.DELAY.PALY_MP3_TIMES));
 };
@@ -2465,7 +2472,7 @@ onUnmounted(() => {
   cursor: pointer;
   transition: all 0.3s ease;
   // 防止文字换行
-  white-space: nowrap; 
+  white-space: nowrap;
   // 超出部分省略号显示
   overflow: hidden;
   // 超出部分省略号显示