Explorar el Código

完成练习题需求,修改练习题问题

dusenyao hace 1 año
padre
commit
b3d4e099fd
Se han modificado 29 ficheros con 359 adiciones y 111 borrados
  1. 6 0
      src/App.vue
  2. 29 12
      src/api/app.js
  3. 101 0
      src/components/GlobalProgress.vue
  4. 2 2
      src/components/common/RichText.vue
  5. 3 0
      src/icons/svg/edit.svg
  6. 31 2
      src/store/modules/app.js
  7. 8 3
      src/store/mutation-types.js
  8. 15 0
      src/utils/common.js
  9. 1 1
      src/views/exercise_questions/create/components/common/UploadAudio.vue
  10. 0 2
      src/views/exercise_questions/create/components/exercises/ChineseQuestion.vue
  11. 4 44
      src/views/exercise_questions/create/components/exercises/ChooseToneQuestion.vue
  12. 17 1
      src/views/exercise_questions/create/components/exercises/FillQuestion.vue
  13. 2 2
      src/views/exercise_questions/create/components/exercises/JudgeQuestion.vue
  14. 1 1
      src/views/exercise_questions/create/components/exercises/MatchingQuestion.vue
  15. 27 18
      src/views/exercise_questions/create/components/exercises/ReadAloudQuestion.vue
  16. 6 6
      src/views/exercise_questions/create/components/exercises/ReadQuestion.vue
  17. 2 2
      src/views/exercise_questions/create/components/exercises/RepeatQuestion.vue
  18. 2 2
      src/views/exercise_questions/create/components/exercises/SelectQuestion.vue
  19. 0 2
      src/views/exercise_questions/create/components/exercises/WriteQuestion.vue
  20. 53 3
      src/views/exercise_questions/data/common.js
  21. 20 0
      src/views/exercise_questions/data/fill.js
  22. 2 2
      src/views/exercise_questions/data/read.js
  23. 2 0
      src/views/exercise_questions/data/readAloud.js
  24. 1 1
      src/views/exercise_questions/preview/ChooseTonePreview.vue
  25. 20 2
      src/views/exercise_questions/preview/FillPreview.vue
  26. 1 1
      src/views/exercise_questions/preview/JudgePreview.vue
  27. 1 0
      src/views/exercise_questions/preview/ReadAloudPreview.vue
  28. 1 1
      src/views/exercise_questions/preview/RepeatPreview.vue
  29. 1 1
      src/views/exercise_questions/preview/SelectPreview.vue

+ 6 - 0
src/App.vue

@@ -1,14 +1,20 @@
 <template>
   <div id="app">
     <RouterView />
+    <GlobalProgress />
   </div>
 </template>
 
 <script>
 import { getConfig } from '@/utils/auth';
 
+import GlobalProgress from '@/components/GlobalProgress.vue';
+
 export default {
   name: 'App',
+  components: {
+    GlobalProgress,
+  },
   created() {
     // 捕获未处理的错误
     window.onerror = (msg, url, lineNo, columnNo, error) => {

+ 29 - 12
src/api/app.js

@@ -1,4 +1,6 @@
 import { http } from '@/utils/http';
+import store from '@/store';
+import { app } from '@/store/mutation-types';
 
 /**
  * @description 得到验证码图像
@@ -40,8 +42,11 @@ export function GetChildSysList_CanEnter_PC(data) {
  * 上传文件
  * @param {String} SecurityLevel 保密级别
  * @param {object} file 文件对象
+ * @param {object} option 上传选项
+ * @param {function} option.handleUploadProgress 上传进度回调
+ * @param {boolean} option.isGlobalprogress 是否使用全局进度条
  */
-export function fileUpload(SecurityLevel, file, handleUploadProgress = () => {}) {
+export function fileUpload(SecurityLevel, file, { handleUploadProgress, isGlobalprogress = false }) {
   let formData = null;
   if (file instanceof FormData) {
     formData = file;
@@ -50,18 +55,30 @@ export function fileUpload(SecurityLevel, file, handleUploadProgress = () => {})
     formData.append(file.filename, file.file, file.file.name);
   }
 
-  return http.postForm('/GCLSFileServer/WebFileUpload', formData, {
-    params: {
-      SecurityLevel,
-    },
-    transformRequest: [
-      (data) => {
-        return data;
+  let onUploadProgress = handleUploadProgress || null;
+  if (isGlobalprogress) {
+    store.commit(`app/${app.SHOW_PROGRESS}`, true);
+    onUploadProgress = ({ loaded, progress, total }) => {
+      store.commit(`app/${app.SET_UPLOAD_INFO}`, { loaded, progress, total });
+    };
+  }
+
+  const controller = new AbortController();
+  store.commit(`app/${app.SET_UPLOAD_CONTROLLER}`, controller);
+
+  return http
+    .postForm('/GCLSFileServer/WebFileUpload', formData, {
+      params: {
+        SecurityLevel,
       },
-    ],
-    onUploadProgress: handleUploadProgress,
-    timeout: 0,
-  });
+      signal: controller.signal,
+      transformRequest: [(data) => data],
+      onUploadProgress,
+      timeout: 0,
+    })
+    .finally(() => {
+      store.commit(`app/${app.SET_UPLOAD_CONTROLLER}`, null);
+    });
 }
 
 export function GetStaticResources(MethodName, data) {

+ 101 - 0
src/components/GlobalProgress.vue

@@ -0,0 +1,101 @@
+<template>
+  <div v-show="showProgress" class="global-progress">
+    <div class="progress">
+      <div class="title">上传...</div>
+      <el-progress :percentage="uploadInfo.progress" :show-text="false" />
+      <div class="upload-info">
+        <span>上传文件 {{ uploadInfo.loaded }}/{{ uploadInfo.total }}</span>
+        <span :class="[{ success: complete }]">{{ uploadText }}</span>
+      </div>
+      <div class="footer">
+        <el-button v-if="complete" type="primary" @click="uploadComplete">完成</el-button>
+        <el-button v-else @click="cancelUpload">取消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import { app } from '@/store/mutation-types';
+
+export default {
+  name: 'GlobalProgress',
+  data() {
+    return {};
+  },
+  computed: {
+    complete() {
+      return this.uploadInfo.progress === 100;
+    },
+    uploadText() {
+      if (this.complete) return '上传成功';
+      return `${this.uploadInfo.progress}%`;
+    },
+    ...mapState({
+      uploadInfo: (state) => state.app.uploadInfo,
+      uploadController: (state) => state.app.uploadController,
+      showProgress: (state) => state.app.showProgress,
+    }),
+  },
+  methods: {
+    cancelUpload() {
+      this.$store.commit(`app/${app.SHOW_PROGRESS}`, false);
+      this.uploadController?.abort();
+    },
+    uploadComplete() {
+      this.$store.commit(`app/${app.SHOW_PROGRESS}`, false);
+      this.$store.commit(`app/${app.RESET_UPLOAD_INFO}`);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.global-progress {
+  position: fixed;
+  inset: 0;
+  z-index: 3001;
+  display: flex;
+  justify-content: center;
+  color: #242634;
+  background-color: hsla(0deg, 0%, 100%, 80%);
+  transition: opacity 0.3s;
+
+  .progress {
+    display: flex;
+    flex-direction: column;
+    row-gap: 8px;
+    width: 400px;
+    height: 164px;
+    padding: 24px;
+    margin-top: 35vh;
+    background-color: #fff;
+    border-radius: 4px;
+    box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 8%);
+
+    .title {
+      font-weight: bold;
+    }
+
+    .upload-info {
+      display: flex;
+      justify-content: space-between;
+      font-size: 12px;
+
+      :first-child {
+        font-weight: bold;
+      }
+
+      span.success {
+        color: #0fbc00;
+      }
+    }
+
+    .footer {
+      margin-top: 16px;
+      text-align: center;
+    }
+  }
+}
+</style>

+ 2 - 2
src/components/common/RichText.vue

@@ -125,7 +125,7 @@ export default {
       let file = blobInfo.blob();
       const formData = new FormData();
       formData.append(file.name, file, file.name);
-      fileUpload('Mid', formData)
+      fileUpload('Mid', formData, { isGlobalprogress: true })
         .then(({ file_info_list }) => {
           if (file_info_list.length > 0) {
             success(file_info_list[0].file_url);
@@ -154,7 +154,7 @@ export default {
           let file = input.files[0];
           const formData = new FormData();
           formData.append(file.name, file, file.name);
-          fileUpload('Mid', formData)
+          fileUpload('Mid', formData, { isGlobalprogress: true })
             .then(({ file_info_list }) => {
               if (file_info_list.length > 0) {
                 callback(file_info_list[0].file_url);

+ 3 - 0
src/icons/svg/edit.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4.27614 10.5933L11.0375 3.83183L10.0947 2.88902L3.33333 9.65045V10.5933H4.27614ZM4.82843 11.9266H2V9.09819L9.62333 1.47481C9.88373 1.21445 10.3058 1.21445 10.5661 1.47481L12.4518 3.36042C12.7121 3.62077 12.7121 4.04288 12.4518 4.30323L4.82843 11.9266ZM2 13.2599H14V14.5933H2V13.2599Z" fill="black" fill-opacity="0.4"/>
+</svg>

+ 31 - 2
src/store/modules/app.js

@@ -1,9 +1,17 @@
+import { app } from '@/store/mutation-types';
 import { getConfig } from '@/utils/auth';
+import { conversionSize } from '@/utils/common';
 
 const getDefaultSate = () => {
   const config = getConfig();
   return {
-    app_id: '',
+    showProgress: false,
+    uploadController: null,
+    uploadInfo: {
+      loaded: 0,
+      progress: 0,
+      total: 0,
+    },
     config: {
       logo_image_url: config?.logo_image_url ?? '',
     },
@@ -12,7 +20,28 @@ const getDefaultSate = () => {
 
 const state = getDefaultSate();
 
-const mutations = {};
+const mutations = {
+  [app.SHOW_PROGRESS]: (state, isShow) => {
+    state.showProgress = isShow;
+  },
+  [app.RESET_UPLOAD_INFO]: (state) => {
+    state.uploadInfo = {
+      loaded: 0,
+      progress: 0,
+      total: 0,
+    };
+  },
+  [app.SET_UPLOAD_CONTROLLER]: (state, controller) => {
+    state.uploadController = controller;
+  },
+  [app.SET_UPLOAD_INFO]: (state, { loaded, progress, total }) => {
+    state.uploadInfo = {
+      loaded: conversionSize(loaded),
+      progress: parseInt(String(progress * 100).replace(/(\d+)\.\d+/, '$1')),
+      total: conversionSize(total),
+    };
+  },
+};
 
 const actions = {};
 

+ 8 - 3
src/store/mutation-types.js

@@ -1,8 +1,13 @@
-const app = {};
+const app = {
+  SET_UPLOAD_INFO: 'SET_UPLOAD_INFO', // 设置上传信息
+  SET_UPLOAD_CONTROLLER: 'SET_UPLOAD_CONTROLLER', // 设置上传控制器
+  RESET_UPLOAD_INFO: 'RESET_UPLOAD_INFO', // 重置上传信息
+  SHOW_PROGRESS: 'SHOW_PROGRESS', // 显示进度条
+};
 
 const user = {
-  RESET_STATE: 'RESET_STATE',
-  SET_USER_INFO: 'SET_USER_INFO',
+  RESET_STATE: 'RESET_STATE', // 重置用户状态
+  SET_USER_INFO: 'SET_USER_INFO', // 设置用户信息
 };
 
 export { app, user };

+ 15 - 0
src/utils/common.js

@@ -0,0 +1,15 @@
+/**
+ * 换算数据大小
+ * @param {Number} size
+ * @returns String
+ */
+export function conversionSize(size) {
+  let _size = size;
+  const units = ['B', 'KB', 'MB', 'GB'];
+  let factor = 0;
+  while (_size > 1024 && factor < units.length - 1) {
+    _size /= 1024;
+    factor += 1;
+  }
+  return `${_size.toFixed(2)}${units[factor]}`;
+}

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

@@ -77,7 +77,7 @@ export default {
       }
     },
     upload(file) {
-      fileUpload('Mid', file, this.handleUploadProgress).then(({ file_info_list }) => {
+      fileUpload('Mid', file, { handleUploadProgress: this.handleUploadProgress }).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;

+ 0 - 2
src/views/exercise_questions/create/components/exercises/ChineseQuestion.vue

@@ -135,7 +135,6 @@ import UploadAudio from '../common/UploadAudio.vue';
 import SoundRecord from '../common/SoundRecord.vue';
 import { GetStaticResources } from '@/api/app';
 
-import { changeOptionType } from '@/views/exercise_questions/data/common';
 import {
   chineseData,
   learnTypeList,
@@ -154,7 +153,6 @@ export default {
     return {
       learnTypeList,
       audioGenerationMethodList,
-      changeOptionType,
       data: JSON.parse(JSON.stringify(chineseData)),
     };
   },

+ 4 - 44
src/views/exercise_questions/create/components/exercises/ChooseToneQuestion.vue

@@ -27,8 +27,8 @@
         <label class="subtitle">内容</label>
         <ul>
           <li v-for="(item, i) in data.option_list" :key="i" class="content-item">
-            <span class="question-number" @dblclick="changeOptionType(data)">
-              {{ computedQuestionNumber(i, data.option_number_show_mode) }}.
+            <span class="question-number" title="双击切换序号类型" @dblclick="changeOptionType(data)">
+              {{ computedQuestionNumber(i, data.option_number_show_mode) }}
             </span>
             <el-input v-model="item.content" placeholder="输入内容" @blur="handleItemAnswer(item)" />
             <UploadAudio
@@ -136,7 +136,7 @@ import QuestionMixin from '@/views/exercise_questions/create/components/common/Q
 import UploadAudio from '../common/UploadAudio.vue';
 import SoundRecord from '../common/SoundRecord.vue';
 
-import { changeOptionType } from '@/views/exercise_questions/data/common';
+import { changeOptionType, addTone } from '@/views/exercise_questions/data/common';
 import {
   getOption,
   ChooseToneData,
@@ -161,19 +161,6 @@ export default {
       toneTypeList,
       data: JSON.parse(JSON.stringify(ChooseToneData)),
       matically_pinyin_obj: {}, // 存放转成声调的拼音
-      tone_data: [
-        ['ā', 'á', 'ǎ', 'à', 'a'],
-        ['ō', 'ó', 'ǒ', 'ò', 'o'],
-        ['ē', 'é', 'ě', 'è', 'e'],
-        ['ī', 'í', 'ǐ', 'ì', 'i'],
-        ['ū', 'ú', 'ǔ', 'ù', 'u'],
-        ['ǖ', 'ǘ', 'ǚ', 'ǜ', 'ü'],
-        ['Ā', 'Á', 'Â', 'À', 'A'],
-        ['Ō', 'Ó', 'Ô', 'Ò', 'O'],
-        ['Ē', 'É', 'Ê', 'È', 'E'],
-        ['Ī', 'Í', 'Î', 'Ì', 'I'],
-        ['Ū', 'Ú', 'Û', 'Ù', 'U'],
-      ],
       res_arr: [],
     };
   },
@@ -217,7 +204,7 @@ export default {
       });
       this.matically_pinyin_obj[mark] = this.res_arr
         .map((item) =>
-          item.map(({ number, con }) => (number && con ? this.addTone(Number(number), con) : number || con || '')),
+          item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
         )
         .filter((item) => item.length > 0)
         .join(',');
@@ -243,33 +230,6 @@ export default {
 
       this.res_arr.push(numList.length === 0 ? [{ con: valItem }] : numList);
     },
-    addTone(number, con) {
-      const zmList = ['a', 'o', 'e', 'i', 'u', 'v', 'A', 'O', 'E', 'I', 'U'];
-      let cons = con;
-      if (number) {
-        for (let i = 0; i < zmList.length; i++) {
-          let zm = zmList[i];
-          if (con.includes(zm)) {
-            let zm2 = this.tone_data[i][number - 1];
-            if (con.includes('iu')) {
-              zm2 = this.tone_data[4][number - 1];
-              cons = con.replace('u', zm2);
-            } else if (con.includes('ui')) {
-              zm2 = this.tone_data[3][number - 1];
-              cons = con.replace('i', zm2);
-            } else if (/yv|jv|qv|xv/.test(con)) {
-              zm2 = this.tone_data[4][number - 1];
-              cons = con.replace('v', zm2);
-            } else {
-              cons = con.replace(zm, zm2);
-            }
-
-            break;
-          }
-        }
-      }
-      return cons;
-    },
     // 答案
     handleItemAnswer(item) {
       const index = this.data.answer.answer_list.findIndex((items) => items.mark === item.mark);

+ 17 - 1
src/views/exercise_questions/create/components/exercises/FillQuestion.vue

@@ -55,6 +55,7 @@
             v-for="(item, i) in data.answer.answer_list.filter(({ type }) => type === 'any_one')"
             :key="item.mark"
             v-model="item.value"
+            @blur="handleTone(item.value, i)"
           >
             <span slot="prefix">{{ i + 1 }}.</span>
           </el-input>
@@ -130,7 +131,8 @@ import UploadAudio from '../common/UploadAudio.vue';
 import QuestionMixin from '../common/QuestionMixin.js';
 
 import { getRandomNumber } from '@/utils';
-import { fillData } from '@/views/exercise_questions/data/fill';
+import { addTone } from '@/views/exercise_questions/data/common';
+import { fillData, handleToneValue } from '@/views/exercise_questions/data/fill';
 
 export default {
   name: 'FillQuestion',
@@ -224,6 +226,20 @@ export default {
         top: `${pixelsFromTop - 18}px`,
       };
     },
+    handleTone(value, i) {
+      if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
+      this.data.answer.answer_list[i].value = value
+        .trim()
+        .split(/\s+/)
+        .map((item) => {
+          return handleToneValue(item);
+        })
+        .map((item) =>
+          item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
+        )
+        .filter((item) => item.length > 0)
+        .join(' ');
+    },
   },
 };
 </script>

+ 2 - 2
src/views/exercise_questions/create/components/exercises/JudgeQuestion.vue

@@ -24,8 +24,8 @@
       <div class="content">
         <ul>
           <li v-for="(item, i) in data.option_list" :key="i" class="content-item">
-            <span class="question-number" @dblclick="changeOptionType(data)">
-              {{ computedQuestionNumber(i, data.option_number_show_mode) }}.
+            <span class="question-number" title="双击切换序号类型" @dblclick="changeOptionType(data)">
+              {{ computedQuestionNumber(i, data.option_number_show_mode) }}
             </span>
             <div class="option-content">
               <RichText v-model="item.content" placeholder="输入内容" :inline="true" />

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

@@ -28,7 +28,7 @@
           <li v-for="(item, i) in data.option_list" :key="i" class="content-item">
             <div v-for="(li, j) in item" :key="li.mark" class="item-cell">
               <span v-if="j === 0" class="question-number">
-                {{ computedQuestionNumber(i, data.option_number_show_mode) }}.
+                {{ computedQuestionNumber(i, data.option_number_show_mode) }}
               </span>
               <RichText v-model="li.content" placeholder="输入内容" :inline="true" />
               <span v-if="data.property.column_number > j + 1" class="horizontal-line"></span>

+ 27 - 18
src/views/exercise_questions/create/components/exercises/ReadAloudQuestion.vue

@@ -1,25 +1,34 @@
 <template>
   <QuestionBase>
     <template #content>
-      <el-input
-        v-if="data.property.stem_type === stemTypeList[0].value"
-        v-model="data.stem"
-        rows="3"
-        resize="none"
-        type="textarea"
-        placeholder="输入题干"
-      />
+      <div class="stem">
+        <el-input
+          v-if="data.property.stem_type === stemTypeList[0].value"
+          v-model="data.stem"
+          rows="3"
+          resize="none"
+          type="textarea"
+          placeholder="输入题干"
+        />
+
+        <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
 
-      <RichText v-if="data.property.stem_type === stemTypeList[1].value" v-model="data.stem" placeholder="输入题干" />
+        <el-input
+          v-show="isEnable(data.property.is_enable_description)"
+          v-model="data.description"
+          rows="3"
+          resize="none"
+          type="textarea"
+          placeholder="输入文段"
+        />
 
-      <UploadAudio
-        v-show="isEnable(data.property.is_enable_listening)"
-        :file-id="data.file_id_list?.[0]"
-        @upload="upload"
-        @deleteFile="deleteFile"
-      />
+        <UploadAudio
+          v-show="isEnable(data.property.is_enable_listening)"
+          :file-id="data.file_id_list?.[0]"
+          @upload="upload"
+          @deleteFile="deleteFile"
+        />
 
-      <div class="content">
         <el-input
           v-if="isEnable(data.property.is_enable_reference_answer)"
           v-model="data.reference_answer"
@@ -118,7 +127,7 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.content {
-  margin-top: 8px;
+.stem {
+  border-bottom-width: 0 !important;
 }
 </style>

+ 6 - 6
src/views/exercise_questions/create/components/exercises/ReadQuestion.vue

@@ -32,12 +32,6 @@
         <RichText v-model="data.article" :is-border="true" :height="130" placeholder="输入文章" />
       </div>
 
-      <div class="footer">
-        <span class="add-option" @click="addQuestion">
-          <SvgIcon icon-class="add-circle" size="14" /><span>增加配题</span>
-        </span>
-      </div>
-
       <div v-for="(item, i) in data.question_list" :key="i">
         <SelectQuestionType
           :question-type-option="questionTypeOption"
@@ -58,6 +52,12 @@
           />
         </KeepAlive>
       </div>
+
+      <div class="footer">
+        <span class="add-option" @click="addQuestion">
+          <SvgIcon icon-class="add-circle" size="14" /><span>增加配题</span>
+        </span>
+      </div>
     </template>
 
     <template #property>

+ 2 - 2
src/views/exercise_questions/create/components/exercises/RepeatQuestion.vue

@@ -28,8 +28,8 @@
         <label class="title-little">内容</label>
         <ul>
           <li v-for="(item, i) in data.option_list" :key="i" class="content-item repeat-option">
-            <span class="question-number" @dblclick="changeOptionType(data)">
-              {{ computedQuestionNumber(i, data.option_number_show_mode) }}.
+            <span class="question-number" title="双击切换序号类型" @dblclick="changeOptionType(data)">
+              {{ computedQuestionNumber(i, data.option_number_show_mode) }}
             </span>
             <div class="option-content">
               <RichText v-model="item.content" placeholder="输入内容" :inline="true" />

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

@@ -34,8 +34,8 @@
       <div class="content">
         <ul>
           <li v-for="(item, i) in data.option_list" :key="i" class="content-item">
-            <span class="question-number" @dblclick="changeOptionType(data)">
-              {{ computedQuestionNumber(i, data.option_number_show_mode) }}.
+            <span class="question-number" title="双击切换序号类型" @dblclick="changeOptionType(data)">
+              {{ computedQuestionNumber(i, data.option_number_show_mode) }}
             </span>
             <div class="option-content">
               <span :class="['checkbox', { active: isAnswer(item.mark) }]" @click="selectAnswer(item.mark)"></span>

+ 0 - 2
src/views/exercise_questions/create/components/exercises/WriteQuestion.vue

@@ -124,7 +124,6 @@
 <script>
 import QuestionMixin from '../common/QuestionMixin.js';
 
-import { changeOptionType } from '@/views/exercise_questions/data/common';
 import { writeData } from '@/views/exercise_questions/data/write';
 
 export default {
@@ -132,7 +131,6 @@ export default {
   mixins: [QuestionMixin],
   data() {
     return {
-      changeOptionType,
       data: JSON.parse(JSON.stringify(writeData)),
     };
   },

+ 53 - 3
src/views/exercise_questions/data/common.js

@@ -79,13 +79,15 @@ export const optionTypeList = [
   { value: 'letter', label: '字母' },
   { value: 'number', label: '数字' },
   { value: 'chinese', label: '中文数字' },
+  { value: 'bracket_number', label: '括号数字' },
 ];
 
 // 计算选项方法
 export const computeOptionMethods = {
-  [optionTypeList[0].value]: (i) => String.fromCharCode(97 + i),
-  [optionTypeList[1].value]: (i) => i + 1,
-  [optionTypeList[2].value]: (i) => digitToChinese(i + 1),
+  [optionTypeList[0].value]: (i) => `${String.fromCharCode(97 + i)}.`,
+  [optionTypeList[1].value]: (i) => `${i + 1}.`,
+  [optionTypeList[2].value]: (i) => `${digitToChinese(i + 1)}.`,
+  [optionTypeList[3].value]: (i) => `(${i + 1})`,
 };
 
 /**
@@ -151,3 +153,51 @@ export const questionNumberTypeList = [
 ];
 
 export const svgNS = 'http://www.w3.org/2000/svg'; // SVG命名空间
+
+export const tone_data = [
+  ['ā', 'á', 'ǎ', 'à', 'a'],
+  ['ō', 'ó', 'ǒ', 'ò', 'o'],
+  ['ē', 'é', 'ě', 'è', 'e'],
+  ['ī', 'í', 'ǐ', 'ì', 'i'],
+  ['ū', 'ú', 'ǔ', 'ù', 'u'],
+  ['ǖ', 'ǘ', 'ǚ', 'ǜ', 'ü'],
+  ['Ā', 'Á', 'Â', 'À', 'A'],
+  ['Ō', 'Ó', 'Ô', 'Ò', 'O'],
+  ['Ē', 'É', 'Ê', 'È', 'E'],
+  ['Ī', 'Í', 'Î', 'Ì', 'I'],
+  ['Ū', 'Ú', 'Û', 'Ù', 'U'],
+];
+
+/**
+ * 添加声调
+ * @param {Number} number
+ * @param {String} con
+ * @returns String
+ */
+export function addTone(number, con) {
+  const zmList = ['a', 'o', 'e', 'i', 'u', 'v', 'A', 'O', 'E', 'I', 'U'];
+  let cons = con;
+  if (number) {
+    for (let i = 0; i < zmList.length; i++) {
+      let zm = zmList[i];
+      if (con.includes(zm)) {
+        let zm2 = tone_data[i][number - 1];
+        if (con.includes('iu')) {
+          zm2 = tone_data[4][number - 1];
+          cons = con.replace('u', zm2);
+        } else if (con.includes('ui')) {
+          zm2 = tone_data[3][number - 1];
+          cons = con.replace('i', zm2);
+        } else if (/yv|jv|qv|xv/.test(con)) {
+          zm2 = tone_data[4][number - 1];
+          cons = con.replace('v', zm2);
+        } else {
+          cons = con.replace(zm, zm2);
+        }
+
+        break;
+      }
+    }
+  }
+  return cons;
+}

+ 20 - 0
src/views/exercise_questions/data/fill.js

@@ -1,5 +1,25 @@
 import { stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } from './common';
 
+export function handleToneValue(valItem) {
+  let numList = [];
+  if (/[A-Za-z]+\d/g.test(valItem)) {
+    valItem.split('').forEach((item, i) => {
+      if (/\d/.test(item)) {
+        let numIndex = numList.length === 0 ? 0 : numList[numList.length - 1].index;
+        let con = valItem.substring(numIndex, i).replace(/\d/g, '');
+        numList.push({
+          number: item,
+          con,
+        });
+      }
+    });
+  } else {
+    numList = [];
+  }
+
+  return numList.length === 0 ? [{ con: valItem }] : numList;
+}
+
 // 填空题数据模板
 export const fillData = {
   type: 'fill', // 题型

+ 2 - 2
src/views/exercise_questions/data/read.js

@@ -1,4 +1,4 @@
-import { stemTypeList, scoreTypeList, questionNumberTypeList, getExerciseTypeList } from './common';
+import { stemTypeList, scoreTypeList, questionNumberTypeList, getExerciseTypeList, switchOption } from './common';
 
 // 题型类型选项
 export const questionTypeOption = [
@@ -23,7 +23,7 @@ export const readData = {
   property: {
     stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
-    is_enable_description: false, // 描述
+    is_enable_description: switchOption[1].value, // 描述
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型
   },

+ 2 - 0
src/views/exercise_questions/data/readAloud.js

@@ -4,6 +4,7 @@ import { stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } fro
 export const readAloudData = {
   type: 'read_aloud', // 题型
   stem: '', // 题干
+  description: '', // 描述
   reference_answer: '', // 参考答案
   file_id_list: [], // 文件 id 列表
   answer: {
@@ -15,6 +16,7 @@ export const readAloudData = {
     stem_type: stemTypeList[0].value, // 题干类型
     question_number: '1', // 题号
     is_enable_listening: switchOption[0].value, // 是否开启听力
+    is_enable_description: switchOption[0].value, // 是否启用描述
     is_enable_reference_answer: switchOption[0].value, // 是否开启参考答案
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型

+ 1 - 1
src/views/exercise_questions/preview/ChooseTonePreview.vue

@@ -9,7 +9,7 @@
 
     <div class="option-list">
       <li v-for="(item, i) in data.option_list" :key="i" :class="['option-item']">
-        <span>{{ computeOptionMethods[data.option_number_show_mode](i) }}. </span>
+        <span>{{ computeOptionMethods[data.option_number_show_mode](i) }} </span>
         <AudioPlay v-if="item.audio_file_id" :file-id="item.audio_file_id" />
         <div class="option-content">
           <template v-if="data.property.answer_mode === 'select'">

+ 20 - 2
src/views/exercise_questions/preview/FillPreview.vue

@@ -16,7 +16,7 @@
       <p v-for="(item, i) in data.model_essay" :key="i">
         <template v-for="(li, j) in item">
           <span v-if="li.type === 'text'" :key="j" v-html="sanitizeHTML(li.content)"></span>
-          <el-input v-if="li.type === 'input'" :key="j" v-model="li.content" />
+          <el-input v-if="li.type === 'input'" :key="j" v-model="li.content" @blur="handleTone(li.content, i, j)" />
         </template>
       </p>
     </div>
@@ -26,6 +26,9 @@
 <script>
 import PreviewMixin from './components/PreviewMixin';
 
+import { addTone } from '@/views/exercise_questions/data/common';
+import { handleToneValue } from '@/views/exercise_questions/data/fill';
+
 export default {
   name: 'FillPreview',
   mixins: [PreviewMixin],
@@ -55,7 +58,22 @@ export default {
       immediate: true,
     },
   },
-  methods: {},
+  methods: {
+    handleTone(value, i, j) {
+      if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
+      this.data.model_essay[i][j].content = value
+        .trim()
+        .split(/\s+/)
+        .map((item) => {
+          return handleToneValue(item);
+        })
+        .map((item) =>
+          item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
+        )
+        .filter((item) => item.length > 0)
+        .join(' ');
+    },
+  },
 };
 </script>
 

+ 1 - 1
src/views/exercise_questions/preview/JudgePreview.vue

@@ -17,7 +17,7 @@
         :class="['option-item', { active: isAnswer(mark) }]"
       >
         <div class="option-content">
-          <span class="serial-number">{{ computedQuestionNumber(i, data.option_number_show_mode) }}.</span>
+          <span class="serial-number">{{ computedQuestionNumber(i, data.option_number_show_mode) }}</span>
           <div v-html="sanitizeHTML(content)"></div>
         </div>
         <div class="option-type">

+ 1 - 0
src/views/exercise_questions/preview/ReadAloudPreview.vue

@@ -5,6 +5,7 @@
       <span class="question-number">{{ data.property.question_number }}.</span>
       <span v-html="sanitizeHTML(data.stem)"></span>
     </div>
+    <div v-if="isEnable(data.property.is_enable_description)" class="description">{{ data.description }}</div>
     <el-input v-model="answer.answer_list[0].text" type="textarea" :autosize="{ minRows: 6, maxRows: 36 }" />
     <SoundRecordPreview :wav-blob.sync="answer.answer_list[0].voice_file_id" />
   </div>

+ 1 - 1
src/views/exercise_questions/preview/RepeatPreview.vue

@@ -8,7 +8,7 @@
     <div v-if="isEnable(data.property.is_enable_description)" class="description">{{ data.description }}</div>
     <div class="option-list">
       <li v-for="(item, i) in answer_list" :key="i" :class="['option-item']">
-        <span>{{ computeOptionMethods[data.option_number_show_mode](i) }}. </span>
+        <span>{{ computeOptionMethods[data.option_number_show_mode](i) }} </span>
         <AudioPlay v-if="data.option_list[i].audio_file_id" :file-id="data.option_list[i].audio_file_id" />
         <div
           v-if="sanitizeHTML(data.option_list[i].content)"

+ 1 - 1
src/views/exercise_questions/preview/SelectPreview.vue

@@ -18,7 +18,7 @@
         @click="selectAnswer(mark)"
       >
         <span class="selectionbox"></span>
-        <span>{{ computeOptionMethods[data.option_number_show_mode](i) }}. </span>
+        <span>{{ computeOptionMethods[data.option_number_show_mode](i) }} </span>
         <span v-html="sanitizeHTML(content)"></span>
       </li>
     </ul>