Przeglądaj źródła

练习题 选择题 上传音频

dusenyao 1 rok temu
rodzic
commit
6a64fd740d

+ 13 - 9
src/App.vue

@@ -31,15 +31,19 @@ export default {
       // console.error('未捕获的 Promise.reject 错误:', reason);
     });
 
-    const config = getConfig();
-    if (config) {
-      const link = document.querySelector("link[rel*='icon']") || document.createElement('link');
-      link.type = 'image/x-icon';
-      link.rel = 'shortcut icon';
-      link.href = config.title_icon_url;
-      document.getElementsByTagName('head')[0].appendChild(link);
-    }
+    this.setTitleIcon();
   },
-  methods: {}
+  methods: {
+    setTitleIcon() {
+      const config = getConfig();
+      if (config) {
+        const link = document.querySelector("link[rel*='icon']") || document.createElement('link');
+        link.type = 'image/x-icon';
+        link.rel = 'shortcut icon';
+        link.href = config.title_icon_url;
+        document.getElementsByTagName('head')[0].appendChild(link);
+      }
+    }
+  }
 };
 </script>

+ 36 - 0
src/api/app.js

@@ -14,3 +14,39 @@ export function GetVerificationCodeImage() {
 export function GetLogo(data) {
   return http.post(`${process.env.VUE_APP_FileServer}?MethodName=sys_config_manager-GetLogo`, data);
 }
+
+/**
+ * 得到文件 ID 与 URL 的映射
+ */
+export function GetFileURLMap(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=file_store_manager-GetFileURLMap`, data);
+}
+
+/**
+ * 得到文件存储信息
+ */
+export function GetFileStoreInfo(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=file_store_manager-GetFileStoreInfo`, data);
+}
+
+/**
+ * 上传文件
+ * @param {String} SecurityLevel 保密级别
+ * @param {object} file 文件对象
+ */
+export function fileUpload(SecurityLevel, file) {
+  const formData = new FormData();
+  formData.append(file.filename, file.file, file.file.name);
+
+  return http.postForm('/GCLSFileServer/WebFileUpload', formData, {
+    params: {
+      SecurityLevel
+    },
+    transformRequest: [
+      (data) => {
+        return data;
+      }
+    ],
+    timeout: 0
+  });
+}

+ 10 - 0
src/icons/svg/audio.svg

@@ -0,0 +1,10 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_955_16891)">
+<path d="M5.50251 8.33366L8.3335 6.01738V13.9832L5.50251 11.667H2.50016V8.33366H5.50251ZM1.66683 13.3337H4.90757L9.31966 16.9435C9.39408 17.0044 9.48733 17.0377 9.5835 17.0377C9.81358 17.0377 10.0002 16.8512 10.0002 16.621V3.37957C10.0002 3.28339 9.96691 3.19016 9.906 3.11572C9.76025 2.93762 9.49775 2.91137 9.31966 3.05709L4.90757 6.66697H1.66683C1.2066 6.66697 0.833496 7.04006 0.833496 7.5003V12.5003C0.833496 12.9606 1.2066 13.3337 1.66683 13.3337ZM19.1668 10.0002C19.1668 12.7436 17.9617 15.2055 16.052 16.8854L14.8706 15.7039C16.48 14.3283 17.5002 12.2834 17.5002 10.0002C17.5002 7.71704 16.48 5.67215 14.8706 4.29655L16.052 3.11507C17.9617 4.79498 19.1668 7.25687 19.1668 10.0002ZM15.0002 10.0002C15.0002 8.40716 14.2552 6.98814 13.0946 6.07252L11.9037 7.26344C12.7679 7.8657 13.3335 8.86691 13.3335 10.0002C13.3335 11.1336 12.7679 12.1347 11.9037 12.737L13.0946 13.9279C14.2552 13.0123 15.0002 11.5932 15.0002 10.0002Z" fill="white"/>
+</g>
+<defs>
+<clipPath id="clip0_955_16891">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 15 - 0
src/icons/svg/upload.svg

@@ -0,0 +1,15 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_956_3987)">
+<g clip-path="url(#clip1_956_3987)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.0004 1.72363L11.4955 5.21873L10.5527 6.16154L8.66742 4.27627L8.66742 11.0472H7.33409L7.33409 4.27556L5.44812 6.16154L4.50531 5.21873L8.0004 1.72363ZM3.33333 12.9998V11.6664H2V14.3331H14V11.6664H12.6667V12.9998H3.33333Z" fill="#4E5969"/>
+</g>
+</g>
+<defs>
+<clipPath id="clip0_956_3987">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+<clipPath id="clip1_956_3987">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 7 - 0
src/utils/http.js

@@ -110,6 +110,13 @@ export const http = {
     };
     return service.post(url, data, config);
   },
+  postForm: (url, data = {}, config = {}) => {
+    config.params = {
+      ...config.params,
+      ...getRequestParams()
+    };
+    return service.postForm(url, data, config);
+  },
   put: (url, data, config) => service.put(url, data, config),
   delete: (url, data, config) => service.delete(url, data, config)
 };

+ 58 - 0
src/views/exercise_questions/create/components/common/AudioPlay.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="audio-wrapper">
+    <span class="audio-play" @click="playAudio">
+      <SvgIcon size="20" icon-class="audio" />
+    </span>
+    <audio ref="audio" :src="url" preload="metadata"></audio>
+  </div>
+</template>
+
+<script>
+import { GetFileURLMap } from '@/api/app';
+
+export default {
+  name: 'AudioPlay',
+  props: {
+    fileId: {
+      type: String,
+      required: true
+    }
+  },
+  data() {
+    return {
+      url: ''
+    };
+  },
+  watch: {
+    fileId: {
+      handler(val) {
+        if (!val) return;
+        GetFileURLMap({ file_id_list: [val] }).then(({ url_map }) => {
+          this.url = url_map[val];
+        });
+      },
+      immediate: true
+    }
+  },
+  methods: {
+    playAudio() {
+      this.$refs.audio.play();
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.audio-wrapper {
+  .audio-play {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    cursor: pointer;
+    background-color: $main-color;
+    border-radius: 50%;
+  }
+}
+</style>

+ 104 - 6
src/views/exercise_questions/create/components/common/UploadAudio.vue

@@ -1,23 +1,121 @@
 <template>
-  <div class="upload-audio">
-    <el-upload :limit="1">
-      <span>上传音频</span>
+  <div class="upload-wrapper">
+    <el-upload
+      ref="upload"
+      :limit="1"
+      action="no"
+      :show-file-list="false"
+      :before-upload="beforeUpload"
+      :http-request="upload"
+    >
+      <div class="upload-audio">
+        <SvgIcon icon-class="upload" />
+        <span>上传音频</span>
+      </div>
     </el-upload>
+    <div v-show="file_url.length > 0" class="file-wrapper">
+      <div class="file-name">{{ file_name }}</div>
+      <SvgIcon icon-class="delete" class-name="delete pointer" @click="deleteFile" />
+    </div>
   </div>
 </template>
 
 <script>
+import { fileUpload, GetFileStoreInfo } from '@/api/app';
+
 export default {
   name: 'UploadAudio',
+  props: {
+    fileId: {
+      type: String,
+      default: ''
+    }
+  },
   data() {
-    return {};
+    return {
+      file_id: '',
+      file_url: '',
+      file_name: ''
+    };
+  },
+  watch: {
+    fileId: {
+      handler(val) {
+        if (!val) return;
+        GetFileStoreInfo({ file_id: val }).then(({ file_id, file_url, file_name }) => {
+          this.file_id = file_id;
+          this.file_url = file_url;
+          this.file_name = file_name;
+        });
+      },
+      immediate: true
+    }
   },
-  methods: {}
+  methods: {
+    beforeUpload(file) {
+      if (this.file_id.length > 0) return;
+      const fileName = file.name;
+      const suffix = fileName.slice(fileName.lastIndexOf('.') + 1, fileName.length).toLowerCase();
+      if (!['mp3', 'wav', 'aac', 'm4a'].includes(suffix)) {
+        this.$message.error('音频格式不正确');
+        return false;
+      }
+    },
+    upload(file) {
+      fileUpload('Mid', file).then(({ file_info_list }) => {
+        if (file_info_list.length > 0) {
+          const { file_id, file_name, file_url } = file_info_list[0];
+          this.file_id = file_id;
+          this.file_url = file_url;
+          this.file_name = file_name;
+          this.$emit('upload', file_id);
+        }
+      });
+    },
+    deleteFile() {
+      this.$confirm('是否删除当前音频文件?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      })
+        .then(() => {
+          this.$emit('deleteFile', this.file_id);
+          this.file_id = '';
+          this.file_url = '';
+          this.file_name = '';
+          this.$refs.upload.clearFiles();
+        })
+        .catch(() => {});
+    }
+  }
 };
 </script>
 
 <style lang="scss" scoped>
-.upload-audio {
+.upload-wrapper {
   display: flex;
+  column-gap: 12px;
+  align-items: center;
+  margin-top: 8px;
+
+  .upload-audio {
+    display: flex;
+    column-gap: 12px;
+    align-items: center;
+    width: 233px;
+    padding: 4px 12px;
+    background-color: $fill-color;
+  }
+
+  .file-wrapper {
+    display: flex;
+    column-gap: 12px;
+    align-items: center;
+
+    .file-name {
+      padding: 4px 12px;
+      background-color: $fill-color;
+    }
+  }
 }
 </style>

+ 20 - 1
src/views/exercise_questions/create/components/exercises/SelectQuestion.vue

@@ -22,6 +22,13 @@
           type="textarea"
           placeholder="输入描述"
         />
+
+        <UploadAudio
+          v-show="data.setting.is_hear"
+          :file-id="data.file_id_list?.[0]"
+          @upload="upload"
+          @deleteFile="deleteFile"
+        />
       </div>
       <div class="content">
         <ul>
@@ -116,6 +123,7 @@
 <script>
 import QuestionBase from '../common/QuestionBase.vue';
 import RichText from '@/components/common/RichText.vue';
+import UploadAudio from '../common/UploadAudio.vue';
 
 import {
   stemTypeList,
@@ -133,7 +141,8 @@ export default {
   name: 'SelectQuestion',
   components: {
     QuestionBase,
-    RichText
+    RichText,
+    UploadAudio
   },
   props: {
     questionId: {
@@ -156,6 +165,16 @@ export default {
     this.getQuestion();
   },
   methods: {
+    upload(file_id) {
+      this.data.file_id_list.push(file_id);
+    },
+    deleteFile(file_id) {
+      let index = this.data.file_id_list.indexOf(file_id);
+      if (index !== -1) {
+        this.data.file_id_list.splice(index, 1);
+      }
+    },
+
     getQuestion() {
       GetQuestion({ question_id: this.questionId })
         .then(({ file_list, question }) => {

+ 6 - 0
src/views/exercise_questions/preview/SelectPreview.vue

@@ -5,6 +5,7 @@
       <span v-html="data.stem"></span>
     </div>
     <div v-if="data.setting.is_describe" class="describe">{{ data.describe }}</div>
+    <AudioPlay v-if="data.setting.is_hear && data.file_id_list.length > 0" :file-id="data.file_id_list[0]" />
     <ul class="option-list">
       <li
         v-for="({ content, mark }, i) in list"
@@ -23,8 +24,13 @@
 <script>
 import { computeOptionMethods, selectTypeList } from '@/views/exercise_questions/data/common';
 
+import AudioPlay from '@/views/exercise_questions/create/components/common/AudioPlay.vue';
+
 export default {
   name: 'SelectPreview',
+  components: {
+    AudioPlay
+  },
   props: {
     data: {
       type: Object,