فهرست منبع

新增听力题和阅读题下的简答题

dusenyao 1 سال پیش
والد
کامیت
5616f64b16

+ 18 - 0
src/api/exercise.js

@@ -15,6 +15,24 @@ export function CreateExercise(data) {
 }
 
 /**
+ * 得到练习题信息
+ * @param {object} data
+ * @param {string} data.exercise_id 练习题id
+ * @returns {object} data
+ * @returns {string} data.exercise 练习题信息
+ */
+export function GetExerciseInfo(data) {
+  return http.post(`/TeachingServer/ExerciseManager/GetExerciseInfo`, data);
+}
+
+/**
+ * 更新练习题
+ */
+export function UpdateExercise(data) {
+  return http.post(`/TeachingServer/ExerciseManager/UpdateExercise`, data);
+}
+
+/**
  * 删除练习题
  * @param {object} data
  * @param {string} data.exercise_id 练习题id

+ 19 - 0
src/components/common/RichText.vue

@@ -101,6 +101,25 @@ export default {
         menubar: false, // 菜单栏
         branding: false, // 品牌
         statusbar: false, // 状态栏
+        font_formats:
+          'Andale Mono=andale mono,monospace;' +
+          'Arial=arial,helvetica,sans-serif;' +
+          'Arial Black=arial black,sans-serif;' +
+          'Book Antiqua=book antiqua,palatino,serif;' +
+          'Comic Sans MS=comic sans ms,sans-serif;' +
+          'Courier New=courier new,courier,monospace;' +
+          'Georgia=georgia,palatino,serif;' +
+          'Helvetica=helvetica,arial,sans-serif;' +
+          'Impact=impact,sans-serif;' +
+          'League=League;' +
+          'Symbol=symbol;' +
+          'Tahoma=tahoma,arial,helvetica,sans-serif;' +
+          'Terminal=terminal,monaco,monospace;' +
+          'Times New Roman=times new roman,times,serif;' +
+          'Trebuchet MS=trebuchet ms,geneva,sans-serif;' +
+          'Verdana=verdana,geneva,sans-serif;' +
+          'Webdings=webdings;' +
+          'Wingdings=wingdings,zapf dingbats',
         // 字数限制
         ax_wordlimit_num: this.wordlimitNum,
         ax_wordlimit_callback(editor) {

+ 7 - 3
src/views/exercise_questions/create/components/exercises/ListenFillQuestion.vue

@@ -119,7 +119,11 @@
           </el-radio>
         </el-form-item>
         <el-form-item label-width="45px">
-          <el-input v-model="data.property.score" type="number" />
+          <el-input-number
+            v-model="data.property.score"
+            :min="0"
+            :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
+          />
         </el-form-item>
       </el-form>
     </template>
@@ -132,7 +136,7 @@ import QuestionMixin from '../common/QuestionMixin.js';
 
 import { getRandomNumber } from '@/utils';
 import { addTone } from '@/views/exercise_questions/data/common';
-import { fillData, handleToneValue } from '@/views/exercise_questions/data/listenFill';
+import { listenFillData, handleToneValue } from '@/views/exercise_questions/data/listenFill';
 
 export default {
   name: 'ListenFillQuestion',
@@ -147,7 +151,7 @@ export default {
         top: 0,
         left: 0,
       },
-      data: JSON.parse(JSON.stringify(fillData)),
+      data: JSON.parse(JSON.stringify(listenFillData)),
     };
   },
   created() {

+ 7 - 3
src/views/exercise_questions/create/components/exercises/ListenJudgeQuestion.vue

@@ -117,7 +117,11 @@
           </el-radio>
         </el-form-item>
         <el-form-item label-width="45px">
-          <el-input v-model="data.property.score" type="number" />
+          <el-input-number
+            v-model="data.property.score"
+            :min="0"
+            :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
+          />
         </el-form-item>
       </el-form>
     </template>
@@ -130,7 +134,7 @@ import QuestionMixin from '../common/QuestionMixin.js';
 
 import { changeOptionType } from '@/views/exercise_questions/data/common';
 import {
-  getJudgeData,
+  getListenJudgeData,
   option_type_list,
   option_type_value_list,
   getOption,
@@ -146,7 +150,7 @@ export default {
     return {
       option_type_list,
       changeOptionType,
-      data: getJudgeData(),
+      data: getListenJudgeData(),
     };
   },
   computed: {

+ 7 - 3
src/views/exercise_questions/create/components/exercises/ListenSelectQuestion.vue

@@ -121,7 +121,11 @@
           </el-radio>
         </el-form-item>
         <el-form-item label-width="45px">
-          <el-input v-model="data.property.score" type="number" />
+          <el-input-number
+            v-model="data.property.score"
+            :min="0"
+            :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
+          />
         </el-form-item>
       </el-form>
     </template>
@@ -133,7 +137,7 @@ import UploadAudio from '../common/UploadAudio.vue';
 import QuestionMixin from '../common/QuestionMixin.js';
 
 import { selectTypeList, scoreTypeList, changeOptionType } from '@/views/exercise_questions/data/common';
-import { getSelectData, getOption } from '@/views/exercise_questions/data/listenSelect';
+import { getListenSelectData, getOption } from '@/views/exercise_questions/data/listenSelect';
 
 export default {
   name: 'ListenSelectQuestion',
@@ -145,7 +149,7 @@ export default {
     return {
       selectTypeList,
       changeOptionType,
-      data: getSelectData(),
+      data: getListenSelectData(),
     };
   },
   methods: {

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

@@ -127,6 +127,7 @@ import SelectQuestion from '@/views/exercise_questions/create/components/exercis
 import JudgeQuestion from '@/views/exercise_questions/create/components/exercises/JudgeQuestion.vue';
 import MatchingQuestion from '@/views/exercise_questions/create/components/exercises/MatchingQuestion.vue';
 import FillQuestion from '@/views/exercise_questions/create/components/exercises/FillQuestion.vue';
+import ShortAnswerQuestion from './ShortAnswerQuestion.vue';
 
 export default {
   name: 'ReadQuestion',
@@ -151,6 +152,7 @@ export default {
         judge: JudgeQuestion,
         matching: MatchingQuestion,
         fill: FillQuestion,
+        short_answer: ShortAnswerQuestion,
       },
     };
   },

+ 148 - 0
src/views/exercise_questions/create/components/exercises/ShortAnswerQuestion.vue

@@ -0,0 +1,148 @@
+<template>
+  <QuestionBase>
+    <template #content>
+      <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="输入题干" />
+
+        <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"
+        />
+
+        <el-input
+          v-if="isEnable(data.property.is_enable_reference_answer)"
+          v-model="data.reference_answer"
+          type="textarea"
+          rows="3"
+          placeholder="输入参考答案"
+        />
+      </div>
+    </template>
+
+    <template #property>
+      <el-form :model="data.property">
+        <el-form-item label="题干">
+          <el-radio
+            v-for="{ value, label } in stemTypeList"
+            :key="value"
+            v-model="data.property.stem_type"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label="题号">
+          <el-input v-model="data.property.question_number" />
+        </el-form-item>
+        <el-form-item label-width="45px">
+          <el-radio
+            v-for="{ value, label } in questionNumberTypeList"
+            :key="value"
+            v-model="data.other.question_number_type"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+
+        <el-form-item label="描述">
+          <el-radio
+            v-for="{ value, label } in switchOption"
+            :key="value"
+            v-model="data.property.is_enable_description"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+
+        <el-form-item label="听力">
+          <el-radio
+            v-for="{ value, label } in switchOption"
+            :key="value"
+            v-model="data.property.is_enable_listening"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+
+        <el-form-item label="参考答案">
+          <el-radio
+            v-for="{ value, label } in switchOption"
+            :key="value"
+            v-model="data.property.is_enable_reference_answer"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+
+        <el-form-item label="分值">
+          <el-radio
+            v-for="{ value, label } in scoreTypeList"
+            :key="value"
+            v-model="data.property.score_type"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label-width="45px">
+          <el-input-number
+            v-model="data.property.score"
+            :min="0"
+            :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
+          />
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import QuestionMixin from '../common/QuestionMixin.js';
+import UploadAudio from '../common/UploadAudio.vue';
+
+import { shortAnswerData } from '@/views/exercise_questions/data/shortAnswer';
+
+export default {
+  name: 'ReadAloudQuestion',
+  components: {
+    UploadAudio,
+  },
+  mixins: [QuestionMixin],
+  data() {
+    return {
+      data: JSON.parse(JSON.stringify(shortAnswerData)),
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+.stem {
+  border-bottom-width: 0 !important;
+}
+</style>

+ 47 - 2
src/views/exercise_questions/create/index.vue

@@ -2,7 +2,11 @@
   <div class="exercise">
     <div class="list">
       <el-button icon="el-icon-back" @click="back">返回练习管理</el-button>
-      <div class="list-title">课上练习</div>
+      <div class="list-title">
+        <el-input v-if="isEditExercise" v-model="exercise.name" @keyup.enter.native="UpdateExerciseName" />
+        <span v-else class="nowrap-ellipsis">{{ exercise.name }}</span>
+        <SvgIcon icon-class="edit" class-name="edit" @click="editExercise" />
+      </div>
       <div class="exercise-list">
         <ul>
           <li v-for="(item, i) in index_list" :key="i" :class="['exercise-item', { active: i === curIndex }]">
@@ -56,7 +60,14 @@
 
 <script>
 import { exerciseNames } from '../data/common';
-import { AddQuestionToExercise, DeleteQuestion, GetExerciseQuestionIndexList, GetQuestionInfo } from '@/api/exercise';
+import {
+  AddQuestionToExercise,
+  DeleteQuestion,
+  GetExerciseQuestionIndexList,
+  GetQuestionInfo,
+  GetExerciseInfo,
+  UpdateExercise,
+} from '@/api/exercise';
 
 import CreateMain from './components/create.vue';
 import SelectPreview from '@/views/exercise_questions/preview/SelectPreview.vue';
@@ -110,6 +121,8 @@ export default {
 
     return {
       id, // 练习id
+      exercise: { name: '' }, // 练习信息
+      isEditExercise: false, // 是否编辑练习
       curIndex: 0, // 当前练习索引
       index_list: [], // 练习列表
       exerciseNames, // 练习名称
@@ -150,6 +163,7 @@ export default {
   },
   created() {
     this.getExerciseQuestionIndexList(true);
+    this.getExerciseInfo();
   },
   methods: {
     // 返回练习管理
@@ -200,6 +214,26 @@ export default {
           console.log(err);
         });
     },
+    getExerciseInfo() {
+      GetExerciseInfo({ exercise_id: this.id }).then(({ exercise }) => {
+        this.exercise = exercise;
+      });
+    },
+    editExercise() {
+      if (this.isEditExercise) {
+        return this.UpdateExerciseName();
+      }
+      this.isEditExercise = !this.isEditExercise;
+    },
+    UpdateExerciseName() {
+      UpdateExercise(this.exercise)
+        .then(() => {
+          this.isEditExercise = false;
+        })
+        .catch(() => {
+          this.$message.error('修改失败');
+        });
+    },
     /**
      * 获取题目信息
      */
@@ -300,7 +334,18 @@ export default {
     border-right: 1px solid $border-color;
 
     &-title {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
       font-weight: bold;
+
+      :first-child {
+        flex: 1;
+      }
+
+      .edit {
+        cursor: pointer;
+      }
     }
 
     .exercise-list {

+ 1 - 1
src/views/exercise_questions/data/listenFill.js

@@ -21,7 +21,7 @@ export function handleToneValue(valItem) {
 }
 
 // 听后填空题数据模板
-export const fillData = {
+export const listenFillData = {
   type: 'listen_fill', // 题型
   stem: '', // 题干
   file_id_list: [], // 文件 id 列表

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

@@ -18,9 +18,9 @@ export function getOption(content = '') {
  * 获取听后判断题数据模板(防止 mark 重复)
  * @returns {object} 判断题数据模板
  */
-export function getJudgeData() {
+export function getListenJudgeData() {
   return {
-    type: 'judge', // 题型
+    type: 'listen_judge', // 题型
     stem: '', // 题干
     option_number_show_mode: optionTypeList[0].value, // 选项类型
     option_list: [getOption(), getOption(), getOption()], // 选项

+ 1 - 1
src/views/exercise_questions/data/listenSelect.js

@@ -15,7 +15,7 @@ export function getOption(content = '') {
 /**
  * 获取听后选择题数据模板(防止 mark 重复)
  */
-export function getSelectData() {
+export function getListenSelectData() {
   return {
     type: 'listen_select', // 题型
     stem: '', // 题干

+ 1 - 0
src/views/exercise_questions/data/read.js

@@ -6,6 +6,7 @@ export const questionTypeOption = [
   { label: '判断题', value: 'judge' },
   { label: '填空题', value: 'fill' },
   { label: '连线题', value: 'matching' },
+  { label: '简答题', value: 'short_answer' },
 ];
 
 // 题型类型列表

+ 28 - 0
src/views/exercise_questions/data/shortAnswer.js

@@ -0,0 +1,28 @@
+import { stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } from './common';
+
+// 简答题数据模板
+export const shortAnswerData = {
+  type: 'short_answer', // 题型
+  stem: '', // 题干
+  description: '', // 描述
+  reference_answer: '', // 参考答案
+  file_id_list: [], // 文件 id 列表
+  answer: {
+    score: 0,
+    score_type: scoreTypeList[0].value,
+  }, // 答案
+  // 题型属性
+  property: {
+    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, // 分值类型
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
+};

+ 69 - 3
src/views/exercise_questions/preview/FillPreview.vue

@@ -5,7 +5,11 @@
       <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>
+    <div v-if="isEnable(data.property.is_enable_description)" class="description">
+      <span v-for="(text, i) in descriptionList" :key="i" class="description-item" @click="selectedDescription(text)">
+        {{ text }}
+      </span>
+    </div>
 
     <AudioPlay
       v-if="isEnable(data.property.is_enable_listening) && data.file_id_list.length > 0"
@@ -16,7 +20,13 @@
       <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" @blur="handleTone(li.content, i, j)" />
+          <el-input
+            v-if="li.type === 'input'"
+            :key="j"
+            v-model="li.content"
+            @focus="handleInputFocus(i, j)"
+            @blur="handleTone(li.content, i, j)"
+          />
         </template>
       </p>
     </div>
@@ -33,7 +43,18 @@ export default {
   name: 'FillPreview',
   mixins: [PreviewMixin],
   data() {
-    return {};
+    return {
+      inputFocus: false,
+      focusPostion: {
+        i: -1,
+        j: -1,
+      },
+    };
+  },
+  computed: {
+    descriptionList() {
+      return this.data.description.split(/\s+/);
+    },
   },
   watch: {
     'data.model_essay': {
@@ -58,7 +79,37 @@ export default {
       immediate: true,
     },
   },
+  created() {
+    document.addEventListener('click', this.handleBlur);
+    document.addEventListener('keydown', this.handleBlurTab);
+  },
+  beforeDestroy() {
+    document.removeEventListener('click', this.handleBlur);
+    document.removeEventListener('keydown', this.handleBlurTab);
+  },
   methods: {
+    handleBlur(e) {
+      if (e.target.tagName === 'INPUT') return;
+      this.inputFocus = false;
+    },
+    handleBlurTab(e) {
+      if (e.keyCode !== 9 || e.key !== 'Tab') return;
+      this.inputFocus = false;
+    },
+    handleInputFocus(i, j) {
+      this.inputFocus = true;
+      this.focusPostion = {
+        i,
+        j,
+      };
+    },
+    selectedDescription(text) {
+      console.log(2);
+      if (!this.inputFocus) return;
+      const { i, j } = this.focusPostion;
+      this.data.model_essay[i][j].content = text;
+      this.inputFocus = false;
+    },
     handleTone(value, i, j) {
       if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
       this.data.model_essay[i][j].content = value
@@ -83,6 +134,21 @@ export default {
 .fill-preview {
   @include preview;
 
+  .description {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 4px 24px;
+
+    &-item {
+      cursor: pointer;
+
+      &:hover,
+      &:active {
+        color: $light-main-color;
+      }
+    }
+  }
+
   .fill-wrapper {
     .el-input {
       width: 120px;

+ 3 - 3
src/views/exercise_questions/preview/MatchingPreview.vue

@@ -422,13 +422,13 @@ export default {
           width: 14px;
           height: 14px;
           cursor: pointer;
-          border: 4px solid #306eff;
+          border: 4px solid $light-main-color;
           border-radius: 50%;
 
           &.focus {
-            background-color: #306eff;
+            background-color: $light-main-color;
             border-color: #fff;
-            outline: 2px solid #306eff;
+            outline: 2px solid $light-main-color;
           }
         }
       }

+ 2 - 0
src/views/exercise_questions/preview/ReadPreview.vue

@@ -29,6 +29,7 @@ import SelectPreview from '@/views/exercise_questions/preview/SelectPreview.vue'
 import JudgePreview from '@/views/exercise_questions/preview/JudgePreview.vue';
 import MatchingPreview from '@/views/exercise_questions/preview/MatchingPreview.vue';
 import FillPreview from '../preview/FillPreview.vue';
+import ShortAnswerPreview from './ShortAnswerPreview.vue';
 
 export default {
   name: 'ReadPreview',
@@ -46,6 +47,7 @@ export default {
         judge: JudgePreview,
         matching: MatchingPreview,
         fill: FillPreview,
+        short_answer: ShortAnswerPreview,
       },
     };
   },

+ 50 - 0
src/views/exercise_questions/preview/ShortAnswerPreview.vue

@@ -0,0 +1,50 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="readaloud-preview">
+    <div class="stem">
+      <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>
+
+    <AudioPlay
+      v-if="isEnable(data.property.is_enable_listening) && data.file_id_list.length > 0"
+      :file-id="data.file_id_list[0]"
+    />
+
+    <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>
+</template>
+
+<script>
+import PreviewMixin from './components/PreviewMixin';
+import SoundRecordPreview from './components/common/SoundRecordPreview.vue';
+
+export default {
+  name: 'ShortAnswerPreview',
+  components: {
+    SoundRecordPreview,
+  },
+  mixins: [PreviewMixin],
+  created() {
+    this.$set(this.answer.answer_list, 0, { text: '', voice_file_id: '' });
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.readaloud-preview {
+  @include preview;
+
+  .reference-answer {
+    padding: 12px 24px;
+    background-color: $content-color;
+    border-radius: 8px;
+  }
+}
+</style>

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

@@ -195,7 +195,7 @@ export default {
 
     .drag-item-active {
       .drag-content {
-        border-color: #306eff;
+        border-color: $light-main-color;
       }
     }
   }

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

@@ -104,7 +104,7 @@ export default {
     cursor: pointer;
 
     &.sample-show {
-      color: #306eff;
+      color: $light-main-color;
     }
   }