Просмотр исходного кода

Merge branch 'lll_ai_speaking' of FanYanPeng/EAIFrontend into host-nju

LiuLuLin 1 год назад
Родитель
Сommit
fe1559b71e

+ 6 - 0
package-lock.json

@@ -21,6 +21,7 @@
         "mitt": "^3.0.1",
         "pinia": "^2.1.7",
         "pinia-plugin-persistedstate": "^3.2.1",
+        "spark-md5": "^3.0.2",
         "vue": "^3.4.29",
         "vue-demi": "^0.14.10",
         "vue-router": "^4.3.3",
@@ -3952,6 +3953,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/spark-md5": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/spark-md5/-/spark-md5-3.0.2.tgz",
+      "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="
+    },
     "node_modules/ssf": {
       "version": "0.11.2",
       "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz",

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "mitt": "^3.0.1",
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",
+    "spark-md5": "^3.0.2",
     "vue": "^3.4.29",
     "vue-demi": "^0.14.10",
     "vue-router": "^4.3.3",

+ 83 - 0
src/apis/file.ts

@@ -1,4 +1,7 @@
+
+import { ElMessage } from 'element-plus';
 import axiosInstance from "@/apis/axios.config";
+import SparkMD5 from 'spark-md5'
 
 const FILE_PREFIX = "/api/file"
 
@@ -13,6 +16,86 @@ const fileApis = {
             timeout:300000 // 添加超时时间等待
         });
     },
+    async uploadChunkFile(file, userId, assignmentId, duration) {
+        const chunkCount = Math.ceil(file.size / chunk_size)
+        // 创建文件hash
+        const md5 = await createFileMd5(file)
+        // 创建文件分片
+        const chunkList = createChunkFileList(file)
+        // 创建分片对象
+        const chunkListObj = createChunksFileObj(chunkList, file, md5, chunkCount)
+        const promises =  uploadChunkFile(chunkListObj, userId, assignmentId, duration)
+        await Promise.all(promises)
+    }
+
+   
+}
+
+
+const createFileMd5 = (file) =>{
+    const reader = new FileReader()
+    return new Promise((resolve, reject) => {
+        reader.onload = (e) =>{
+            const md5 = SparkMD5.ArrayBuffer.hash(e.target.result)
+            resolve(md5)
+        }
+        reader.onerror = (error) => {
+            reject(error)
+        }
+        reader.readAsArrayBuffer(file)
+    })
+
+}
+const chunk_size = 5 * 1024 * 1024
+ // 创建文件分片
+ const createChunkFileList = (file) => {
+    let current = 0
+    const chunkList = []
+    while(current < file.size){
+        chunkList.push(file.slice(current, Math.min(current + chunk_size, file.size)))
+        current += chunk_size
+    }
+    return chunkList
 }
 
+const createChunksFileObj = (chunkList, file, md5, chunkCount) => {
+    return chunkList.map(((chunk, index) => {
+        return {
+            file: chunk,
+            hash: md5,
+            name: file.name,
+            index:index,
+            chunkCount
+        }
+    }))
+    
+}
+
+const uploadChunkFile = (chunkListObj, userId, assignmentId, duration) => {
+
+    return chunkListObj.map(async(chunk) =>{
+        try{
+            const formData = new FormData();
+            formData.append("file", chunk.file)
+            formData.append("fileName", chunk.name)
+            formData.append('fileMd5', chunk.hash)
+            formData.append('chunkCount', chunk.chunkCount)
+            formData.append('currentIndex', chunk.index)
+            formData.append('userId', userId)
+            formData.append('assignmentId', assignmentId)
+            formData.append('duration', duration)
+            return  axiosInstance.post(`${FILE_PREFIX}/large`, formData, {
+                headers: {
+                    'Content-Type': 'multipart/form-data'
+                }
+            });
+            
+        } catch (error) {
+            return Promise.reject(error)
+        }
+    })
+}
+
+
+
 export default fileApis

+ 56 - 21
src/views/AiSpeakingPage/SpeakingPage.vue

@@ -13,12 +13,14 @@
           <div class="end-time">截止时间:{{ formatDate(aiSpeakingAssignmentInfo.endTime) }}</div>
 
         </div>
-            <div class="data-item">
-              <span class="data-label">作业描述 </span>
-              <span class="data-value">{{ aiSpeakingAssignmentInfo.description !== null ? aiSpeakingAssignmentInfo.description : '未设置' }}</span>
-            </div>
-            <div class="data-item">
-              <span v-if="aiSpeakingAssignmentInfo.descriptionFile" class="data-label">作业附件</span>
+        <div class="data-item">
+          <span class="data-label">作业描述</span>
+          <div class="scrollable-description">
+            {{ aiSpeakingAssignmentInfo.description !== null ? aiSpeakingAssignmentInfo.description : '未设置' }}
+          </div>
+        </div>
+            <div class="data-item" v-if="aiSpeakingAssignmentInfo.descriptionFile">
+              <span class="data-label">作业附件</span>
               <el-icon  @click="openEditor(aiSpeakingAssignmentInfo.descriptionFile, aiSpeakingAssignmentInfo.assignmentName)"><Document /></el-icon>
             </div>
             <div class="data-item">
@@ -702,22 +704,38 @@ const saveHomework = async () => {
         const videoBlob = new Blob([...recordedChunks.value], { type: "video/webm" });
         const fileName = `user_${userId}_${assignmentId}_${Date.now()}`;
         let renameFile =new File([videoBlob], fileName +'.webm', { type: 'video/webm' })
-        try {
-          doubaoApis
-          .saveSigment(userId, assignmentId,renameFile, duration)
-          .then(res =>{
-            recordedChunks.value = [];
-            isHomeworkSaved.value = true;
-            hasHomeworkSaved.value = true;
-            isHomeworkStarted.value = false;
-            ElMessage.success("视频片段暂存成功");
-            loadingInstance.close();
-          })
+        try{
+          fileApis.uploadChunkFile(renameFile, userId, assignmentId, duration)
+            .then(res => {
+                recordedChunks.value = [];
+                isHomeworkSaved.value = true;
+                hasHomeworkSaved.value = true;
+                isHomeworkStarted.value = false;
+                ElMessage.success("视频片段暂存成功");
+                loadingInstance.close();
+            })
         } catch (error) {
           console.error("视频片段上传失败:", error);
           ElMessage.error("视频片段上传失败");
           loadingInstance.close();
         }
+
+        // try {
+        //   doubaoApis
+        //   .saveSigment(userId, assignmentId,renameFile, duration)
+        //   .then(res =>{
+        //     recordedChunks.value = [];
+        //     isHomeworkSaved.value = true;
+        //     hasHomeworkSaved.value = true;
+        //     isHomeworkStarted.value = false;
+        //     ElMessage.success("视频片段暂存成功");
+        //     loadingInstance.close();
+        //   })
+        // } catch (error) {
+        //   console.error("视频片段上传失败:", error);
+        //   ElMessage.error("视频片段上传失败");
+        //   loadingInstance.close();
+        // }
       };
     } else {
       ElMessage.error('状态出错啦')
@@ -795,9 +813,6 @@ const resetHomework = async() => {
     return;
   }
   if (isHomeworkStarted.value) {
-    if (mediaRecorder.value && mediaRecorder.value.state === "recording") {
-      mediaRecorder.value.stop();
-    }
     // 停止录制时长计时器
     if (recordingTimer) {
       clearInterval(recordingTimer);
@@ -807,6 +822,10 @@ const resetHomework = async() => {
     if (cameraPreview.value) {
       cameraPreview.value.srcObject = null;
     }
+     // 关闭摄像头和麦克风
+     if (mediaRecorder.value && mediaRecorder.value.stream) {
+              mediaRecorder.value.stream.getTracks().forEach((track) => track.stop());
+      }
   }
 
    // 显示加载状态
@@ -823,6 +842,7 @@ const resetHomework = async() => {
       isHomeworkSaved.value = false;
       isHomeworkStarted.value = false;
       initRecognitionValue()
+      recordingDuration.value = 0;
       ElMessage.success("作业内容已清空");
       getHistory()
 
@@ -1124,6 +1144,20 @@ onMounted(() => {
     color: #777;
   }
 
+
+  .scrollable-description {
+  max-height: 12em; /* 10行高度(假设1em=1行) */
+  line-height: 1.3em; /* 行高设定 */
+  overflow-y: auto; /* 垂直滚动 */
+  white-space: pre-wrap; /* 保留换行符 */
+  color: #777;
+  margin-top: 10px;
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* IE/Edge */
+}
+
+
+
 .homework-button-group {
   display: flex;
   flex-direction: column;
@@ -1369,7 +1403,7 @@ progress {
 }
 
 .wechat-audio-button {
-  margin-left: 20px;
+  margin-left: 25px;
   display: flex;
   align-items: center;
   background-color: #90caf9;
@@ -1378,6 +1412,7 @@ progress {
   padding: 8px 15px;
   cursor: pointer;
   transition: background-color 0.3s;
+  margin-bottom:8px;
 }
 
 .wechat-audio-button:hover {

+ 1 - 1
src/views/HomeworkPage/component/HomeworkDetail.vue

@@ -132,7 +132,7 @@ const fetchData = () => {
 
 const handleBack = () => {
    // 跳转到目标页面
-   router.go(-1)
+   router.push(`/home/courseDetail/${data.courseId}`)
 }
 
 onMounted(() => {