Quellcode durchsuchen

富文本和对话题修改

dusenyao vor 1 Jahr
Ursprung
Commit
86e123a2b7

+ 84 - 17
src/components/common/RichText.vue

@@ -1,15 +1,24 @@
 <template>
-  <Editor
-    v-bind="$attrs"
-    :id="id"
-    ref="richText"
-    model-events="change keyup undo redo setContent"
-    :value="value"
-    :class="['rich-text', isBorder ? 'is-border' : '']"
-    :init="init"
-    v-on="$listeners"
-    @onBlur="handleRichTextBlur"
-  />
+  <div class="rich-wrapper">
+    <Editor
+      v-bind="$attrs"
+      :id="id"
+      ref="richText"
+      model-events="change keyup undo redo setContent"
+      :value="value"
+      :class="['rich-text', isBorder ? 'is-border' : '']"
+      :init="init"
+      v-on="$listeners"
+      @onBlur="handleRichTextBlur"
+    />
+    <div v-show="isShow" :style="contentmenu" class="contentmenu">
+      <SvgIcon icon-class="slice" size="16" @click="setFill" />
+      <span class="button" @click="setFill">设为填空</span>
+      <span class="line"></span>
+      <SvgIcon icon-class="close-circle" size="16" @click="deleteFill" />
+      <span class="button" @click="deleteFill">删除填空</span>
+    </div>
+  </div>
 </template>
 
 <script>
@@ -89,6 +98,11 @@ export default {
   },
   data() {
     return {
+      isShow: false,
+      contentmenu: {
+        top: 0,
+        left: 0,
+      },
       id: getRandomNumber(),
       init: {
         inline: this.inline,
@@ -134,6 +148,16 @@ export default {
       },
     };
   },
+  created() {
+    if (this.isFill) {
+      window.addEventListener('click', this.hideContentmenu);
+    }
+  },
+  beforeDestroy() {
+    if (this.isFill) {
+      window.removeEventListener('click', this.hideContentmenu);
+    }
+  },
   methods: {
     /**
      * 图片上传自定义逻辑函数
@@ -194,14 +218,14 @@ export default {
      */
     initInstanceCallback(editor) {
       editor.on('SetContent Undo Redo ViewUpdate keyup mousedown', () => {
-        this.$emit('hideContentmenu');
+        this.hideContentmenu();
       });
 
       editor.on('click', (e) => {
         if (e.target.classList.contains('rich-fill')) {
           editor.selection.select(e.target); // 选中填空
           let { offsetLeft, offsetTop } = e.target;
-          this.$emit('showContentmenu', {
+          this.showContentmenu({
             pixelsFromLeft: offsetLeft - 14,
             pixelsFromTop: offsetTop,
           });
@@ -218,7 +242,7 @@ export default {
         let end = editor.selection.getEnd();
         let rng = editor.selection.getRng();
         if (start !== end || rng.collapsed) {
-          this.$emit('hideContentmenu');
+          this.hideContentmenu();
           return;
         }
         if (e.offsetX < mouseX) {
@@ -252,7 +276,7 @@ export default {
         const fontSize = parseFloat(computedStyle.fontSize);
         const canvas = document.createElement('canvas');
         const context = canvas.getContext('2d');
-        context.font = `${fontSize}px ${computedStyle.fontFamily}`;
+        context.font = `${fontSize}pt ${computedStyle.fontFamily}`;
         // 计算文字距离左侧的像素位置
         const width = context.measureText(textBeforeOffset).width;
         const lineHeight = context.measureText('M').fontBoundingBoxAscent + 3.8; // 获取行高
@@ -263,7 +287,7 @@ export default {
         row = row % 1 > 0.8 ? Math.ceil(row) : Math.floor(row);
         const offsetTop = start.offsetTop; // 获取选中文本距离顶部的像素位置
         let pixelsFromTop = offsetTop + lineHeight * row; // 计算选中文本距离顶部的像素位置
-        this.$emit('showContentmenu', {
+        this.showContentmenu({
           pixelsFromLeft: mouseX,
           pixelsFromTop,
         });
@@ -303,7 +327,7 @@ export default {
       let editor = tinymce.get(this.id);
       let rng = editor.selection.getRng();
       if (!rng.collapsed) {
-        this.$emit('hideContentmenu');
+        this.hideContentmenu();
         editor.selection.collapse();
       }
     },
@@ -318,6 +342,26 @@ export default {
     handleRichTextBlur() {
       this.$emit('handleRichTextBlur');
     },
+    // 设置填空
+    setFill() {
+      this.setContent();
+      this.hideContentmenu();
+    },
+    // 删除填空
+    deleteFill() {
+      this.deleteContent();
+      this.hideContentmenu();
+    },
+    hideContentmenu() {
+      this.isShow = false;
+    },
+    showContentmenu({ pixelsFromLeft, pixelsFromTop }) {
+      this.isShow = true;
+      this.contentmenu = {
+        left: `${pixelsFromLeft + 14}px`,
+        top: `${pixelsFromTop - 18}px`,
+      };
+    },
   },
 };
 </script>
@@ -355,4 +399,27 @@ export default {
     }
   }
 }
+
+.contentmenu {
+  position: absolute;
+  z-index: 999;
+  display: flex;
+  column-gap: 4px;
+  align-items: center;
+  padding: 4px 8px;
+  font-size: 14px;
+  background-color: #fff;
+  border-radius: 2px;
+  box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 10%);
+
+  .svg-icon,
+  .button {
+    cursor: pointer;
+  }
+
+  .line {
+    min-height: 16px;
+    margin: 0 4px;
+  }
+}
 </style>

+ 12 - 10
src/views/exercise_questions/create/components/common/QuestionBase.vue

@@ -75,22 +75,24 @@ export default {
               padding-left: 16px;
               background-color: $fill-color;
 
-              .rich-text {
+              .rich-wrapper {
                 flex: 1;
                 min-height: 32px;
 
-                :deep &.mce-content-body {
-                  padding-top: 4px;
-                }
+                :deep .rich-text {
+                  &.mce-content-body {
+                    padding-top: 4px;
+                  }
 
-                :deep &:not(.mce-edit-focus) {
-                  p {
-                    margin: 0;
+                  &:not(.mce-edit-focus) {
+                    p {
+                      margin: 0;
+                    }
                   }
-                }
 
-                :deep &.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before {
-                  top: 6px;
+                  &.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before {
+                    top: 6px;
+                  }
                 }
               }
             }

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

@@ -57,6 +57,7 @@ export default {
 <style lang="scss">
 .select-question-type {
   .el-cascader-menu__wrap {
+    // height: 300px;
     height: 270px;
   }
 }

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

@@ -4,10 +4,13 @@
       <div class="stem">
         <RichText v-model="data.stem" :font-size="18" placeholder="输入题干" />
 
-        <RichText
+        <el-input
           v-if="isEnable(data.property.is_enable_description)"
           v-model="data.description"
-          placeholder="输入提示"
+          rows="3"
+          resize="none"
+          type="textarea"
+          placeholder="输入填空内容"
         />
       </div>
 

+ 254 - 4
src/views/exercise_questions/create/components/exercises/FillFormQuestion.vue

@@ -1,11 +1,50 @@
 <template>
   <QuestionBase>
     <template #content>
+      <div v-show="isMask" class="mask"></div>
       <div class="stem">
         <RichText v-model="data.stem" :font-size="18" placeholder="输入题干" />
       </div>
 
-      <div class="content"></div>
+      <div class="content">
+        <div class="option-wrapper" :style="computedOptionStyle()">
+          <template v-for="i in data.property.column_number">
+            <div :key="i" class="fill-form">
+              <!-- 头部 -->
+              <span class="form-header">
+                <span v-if="i === 1 && isEnable(data.property.is_enable_number_column)" class="header-serial-number">
+                  #
+                </span>
+                <el-input v-model="data.option_header_list[i - 1].text" placeholder="请输入" />
+              </span>
+
+              <span v-for="j in data.property.row_number" :key="j" :class="['form-item', `form-item-${i}-${j}`]">
+                <span v-if="i === 1 && isEnable(data.property.is_enable_number_column)" class="serial-number">
+                  {{ j }}
+                </span>
+                <div class="rich">
+                  <RichText
+                    ref="modelEssay"
+                    v-model="data.option_list[i - 1][j - 1].text"
+                    :is-fill="true"
+                    :toolbar="false"
+                    :font-size="12"
+                    :wordlimit-num="false"
+                    placeholder="请输入"
+                  />
+                </div>
+              </span>
+            </div>
+            <span
+              v-if="i !== data.property.column_number"
+              :key="`resize-${i}`"
+              class="resize"
+              @mousedown="resize($event, i)"
+            ></span>
+          </template>
+        </div>
+        <div class="tips">在输入框最前插入##,代表输入框中内容作为正确答案在作答时隐藏。</div>
+      </div>
     </template>
 
     <template #property>
@@ -40,6 +79,25 @@
             :step="data.property.score_type === scoreTypeList[0].value ? 1 : 0.1"
           />
         </el-form-item>
+        <el-form-item label="序号列">
+          <el-radio
+            v-for="{ value, label } in switchOption"
+            :key="value"
+            v-model="data.property.is_enable_number_column"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label="列表宽度">
+          <el-input-number v-model="data.property.form_width" :min="100" :max="3000" :step="100" />
+        </el-form-item>
+        <el-form-item label="行数">
+          <el-input-number v-model="data.property.row_number" :min="1" :max="30" />
+        </el-form-item>
+        <el-form-item label="列数">
+          <el-input-number v-model="data.property.column_number" :min="2" :max="5" />
+        </el-form-item>
       </el-form>
     </template>
   </QuestionBase>
@@ -48,18 +106,210 @@
 <script>
 import QuestionMixin from '../common/QuestionMixin.js';
 
-import { getFillFormData } from '@/views/exercise_questions/data/fillForm';
+import { getRandomNumber } from '@/utils';
+import { getFillFormData, getOption } from '@/views/exercise_questions/data/fillForm';
 
 export default {
   name: 'FillFormQuestion',
   mixins: [QuestionMixin],
   data() {
     return {
+      isMask: false,
       data: getFillFormData(),
     };
   },
-  methods: {},
+  computed: {
+    optionList() {
+      return JSON.parse(JSON.stringify(this.data.option_list));
+    },
+  },
+  watch: {
+    'data.property.row_number': {
+      handler(val) {
+        this.data.option_list = this.data.option_list.map((item) => {
+          const arr = [];
+          for (let i = 0; i < val; i++) {
+            arr.push(item[i] || getOption());
+          }
+          return arr;
+        });
+      },
+      immediate: true,
+    },
+    'data.property.column_number': {
+      handler(val) {
+        if (val > this.data.option_header_list.length) {
+          let width = 0;
+          this.data.option_header_list.forEach((item) => {
+            // 平均分配宽度,最小宽度为10
+            let w = Math.max((item.width / val) * this.data.option_header_list.length, 10);
+            width += item.width - w;
+            item.width = w;
+          });
+          this.data.option_header_list.push({ mark: getRandomNumber(), text: '', width });
+        } else if (val < this.data.option_header_list.length) {
+          this.data.option_header_list.pop();
+        }
+
+        if (val > this.data.option_list.length) {
+          this.data.option_list.push(Array.from({ length: this.data.property.row_number }, () => getOption()));
+        } else if (val < this.data.option_list.length) {
+          this.data.option_list.pop();
+        }
+      },
+      immediate: true,
+    },
+    optionList: {
+      handler(newVal, oldVal) {
+        if (newVal.length === 0 || !oldVal) return;
+        for (let i = 0; i < newVal.length; i++) {
+          for (let j = 0; j < newVal[i].length; j++) {
+            if (newVal[i][j].text !== oldVal[i][j].text) {
+              console.log('text属性发生变化:', newVal[i][j].text);
+            }
+          }
+        }
+      },
+      immediate: true,
+      deep: true,
+    },
+  },
+  methods: {
+    resize(e, i) {
+      let target = e.target;
+      let startX = e.clientX;
+      this.isMask = true;
+
+      // 鼠标拖动事件
+      document.onmousemove = (e) => {
+        const endX = e.clientX;
+        let list = this.data.option_header_list;
+        // 计算拖动的距离与总宽度的比例
+        let w = ((endX - startX) / this.data.property.form_width) * 100;
+        // 限制最小宽度为10,最大宽度为总宽度-10
+        list[i - 1].width = Math.min(Math.max(10, list[i - 1].width + w), list[i - 1].width + list[i].width - 10);
+        list[i].width = Math.min(Math.max(10, list[i].width - w), list[i - 1].width + list[i].width - 10);
+
+        startX = endX;
+      };
+
+      // 鼠标松开事件
+      document.onmouseup = () => {
+        this.isMask = false;
+        document.onmousemove = null;
+        document.onmouseup = null;
+        target.releaseCapture && target.releaseCapture(); // 当不在需要继续获得鼠标消息就要应该调用 ReleaseCapture() 释放掉
+      };
+      target.setCapture && target.setCapture(); // 该函数在属于当前线程的指定窗口里设置鼠标捕获
+      return false;
+    },
+    computedOptionStyle() {
+      let gridTemplateColumns = this.data.option_header_list.reduce((acc, { width }, i) => {
+        if (i === this.data.option_header_list.length - 1) {
+          return `${acc}${width}%`;
+        }
+        return `${acc}${width}% 2px `;
+      }, '');
+
+      return {
+        gridTemplateColumns,
+        width: `${this.data.property.form_width}px`,
+      };
+    },
+  },
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.mask {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1;
+  width: 100%;
+  height: 100%;
+  cursor: col-resize;
+}
+
+.content {
+  width: calc(100vw - 645px);
+  overflow: auto;
+
+  .option-wrapper {
+    display: grid;
+    grid-auto-flow: column;
+
+    .fill-form {
+      display: flex;
+      flex-direction: column;
+
+      .form-header {
+        display: flex;
+        align-items: center;
+        height: 40px;
+        background-color: $fill-color;
+
+        .header-serial-number {
+          padding: 8px 16px;
+          font-weight: bold;
+        }
+
+        :deep .el-input__inner {
+          font-weight: bold;
+        }
+      }
+
+      .form-item {
+        display: flex;
+        height: 55px;
+        border-bottom: $border;
+
+        .serial-number {
+          width: 40px;
+          min-width: 40px;
+          line-height: 55px;
+          text-align: center;
+        }
+
+        .rich {
+          position: relative;
+          width: 100%;
+          overflow: auto;
+        }
+
+        :deep .tox-tinymce {
+          border-width: 0;
+        }
+
+        :deep .tox .tox-sidebar-wrap {
+          border-width: 0;
+        }
+
+        :deep .tox-editor-header {
+          display: none;
+        }
+      }
+    }
+
+    .resize {
+      width: 2px;
+      height: 100%;
+      cursor: col-resize;
+      background-color: #165dff;
+      border-radius: 4px;
+    }
+  }
+
+  .tips {
+    margin-top: 8px;
+    font-size: 14px;
+    color: #999;
+  }
+}
+
+.property {
+  .el-input-number {
+    width: 200px;
+  }
+}
+</style>

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

@@ -22,16 +22,8 @@
           :toolbar="false"
           :wordlimit-num="false"
           placeholder="输入文段"
-          @showContentmenu="showContentmenu"
-          @hideContentmenu="hideContentmenu"
         />
-        <div v-show="isShow" ref="contentmenu" :style="contentmenu" class="contentmenu">
-          <SvgIcon icon-class="slice" size="16" @click="setFill" />
-          <span class="button" @click="setFill">设为填空</span>
-          <span class="line"></span>
-          <SvgIcon icon-class="close-circle" size="16" @click="deleteFill" />
-          <span class="button" @click="deleteFill">删除填空</span>
-        </div>
+
         <el-button @click="identifyText">识别</el-button>
         <div v-if="data.answer.answer_list.length > 0" class="correct-answer">
           <div class="subtitle">正确答案</div>
@@ -106,20 +98,9 @@ export default {
   mixins: [QuestionMixin],
   data() {
     return {
-      isShow: false,
-      contentmenu: {
-        top: 0,
-        left: 0,
-      },
       data: JSON.parse(JSON.stringify(fillData)),
     };
   },
-  created() {
-    window.addEventListener('click', this.hideContentmenu);
-  },
-  beforeDestroy() {
-    window.removeEventListener('click', this.hideContentmenu);
-  },
   methods: {
     // 识别文本
     identifyText() {
@@ -170,26 +151,6 @@ export default {
       }
       return arr;
     },
-    // 设置填空
-    setFill() {
-      this.$refs.modelEssay.setContent();
-      this.hideContentmenu();
-    },
-    // 删除填空
-    deleteFill() {
-      this.$refs.modelEssay.deleteContent();
-      this.hideContentmenu();
-    },
-    hideContentmenu() {
-      this.isShow = false;
-    },
-    showContentmenu({ pixelsFromLeft, pixelsFromTop }) {
-      this.isShow = true;
-      this.contentmenu = {
-        left: `${pixelsFromLeft + 14}px`,
-        top: `${pixelsFromTop - 18}px`,
-      };
-    },
     handleTone(value, i) {
       if (!/^[a-zA-Z0-9\s]+$/.test(value)) return;
       this.data.answer.answer_list[i].value = value
@@ -237,28 +198,5 @@ export default {
       }
     }
   }
-
-  .contentmenu {
-    position: absolute;
-    z-index: 999;
-    display: flex;
-    column-gap: 4px;
-    align-items: center;
-    padding: 4px 8px;
-    font-size: 14px;
-    background-color: #fff;
-    border-radius: 2px;
-    box-shadow: 0 1px 10px 0 rgba(0, 0, 0, 10%);
-
-    .svg-icon,
-    .button {
-      cursor: pointer;
-    }
-
-    .line {
-      min-height: 16px;
-      margin: 0 4px;
-    }
-  }
 }
 </style>

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

@@ -16,7 +16,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) }}
+                {{ i + 1 }}
               </span>
               <RichText v-model="li.content" placeholder="输入内容" :inline="true" />
               <span v-if="data.property.column_number > j + 1" class="horizontal-line"></span>
@@ -163,25 +163,27 @@ export default {
       column-gap: 4px;
       align-items: center;
 
-      .rich-text {
+      .rich-wrapper {
         flex: 1;
         min-height: 32px;
         background-color: $fill-color;
 
-        :deep &.mce-content-body {
-          padding-top: 4px;
-          padding-left: 12px;
-        }
+        :deep .rich-text {
+          &.mce-content-body {
+            padding-top: 4px;
+            padding-left: 12px;
+          }
 
-        :deep &:not(.mce-edit-focus) {
-          p {
-            margin: 0;
+          &:not(.mce-edit-focus) {
+            p {
+              margin: 0;
+            }
           }
-        }
 
-        :deep &.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before {
-          top: 6px;
-          left: 12px;
+          &.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before {
+            top: 6px;
+            left: 12px;
+          }
         }
       }
     }

+ 23 - 7
src/views/exercise_questions/data/fillForm.js

@@ -1,4 +1,9 @@
-import { optionTypeList, stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } from './common';
+import { getRandomNumber } from '@/utils';
+import { stemTypeList, questionNumberTypeList, scoreTypeList, switchOption } from './common';
+
+export function getOption() {
+  return { mark: getRandomNumber(), text: '', content_list: [] };
+}
 
 /**
  * 获取填表题数据模板(防止 mark 重复)
@@ -8,18 +13,29 @@ export function getFillFormData() {
   return {
     type: 'fill_form', // 题型
     stem: '', // 题干
-    option_number_show_mode: optionTypeList[0].value, // 选项类型
-    option_list: [], // 选项列表
-    answer: { answer_list: [], score: 1, score_type: scoreTypeList[0].value }, // 答案
+    option_header_list: [
+      { mark: getRandomNumber(), text: '', width: 50 },
+      { mark: getRandomNumber(), text: '', width: 50 },
+    ], // 选项头部
+    option_list: [
+      [getOption(), getOption()],
+      [getOption(), getOption()],
+    ], // 选项列表
+    answer: {
+      score: 1,
+      score_type: scoreTypeList[0].value,
+      answer_list: [],
+    }, // 答案
     // 题型属性
     property: {
       stem_type: stemTypeList[1].value, // 题干类型
       question_number: '1', // 题号
       score: 1, // 分值
       score_type: scoreTypeList[0].value, // 分值类型
-      order_number_column: switchOption[0].value, // 序号列
-      row_number: 1, // 行数
-      column_number: 2, // 列数
+      is_enable_number_column: switchOption[0].value, // 是否启用序号列
+      form_width: 800, // 表格宽度 100 - 3000
+      row_number: 2, // 行数 1 - 30
+      column_number: 2, // 列数 2 - 5
     },
     // 其他属性
     other: {

+ 80 - 5
src/views/exercise_questions/preview/DialoguePreview.vue

@@ -5,11 +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 rich-text"
-      v-html="sanitizeHTML(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>
 
     <div class="content">
       <div v-for="(item, i) in optionList" :key="i" class="option-list">
@@ -31,6 +31,7 @@
                   :disabled="disabled"
                   :class="[...computedAnswerClass(item.mark, li.mark, li.content)]"
                   :style="[{ width: Math.max(80, li.content.length * 16) + 'px' }]"
+                  @focus="handleInputFocus(i, j)"
                 />
                 <span
                   v-show="computedAnswerText(item.mark, li.mark, li.content).length > 0"
@@ -83,8 +84,18 @@ export default {
     return {
       optionList: [],
       file_map_list: [],
+      inputFocus: false,
+      focusPostion: {
+        i: -1,
+        j: -1,
+      },
     };
   },
+  computed: {
+    descriptionList() {
+      return this.data.description.split(/\s+/).filter((item) => item);
+    },
+  },
   watch: {
     'data.option_list': {
       handler(val) {
@@ -141,8 +152,54 @@ export default {
       });
     },
   },
+  created() {
+    document.addEventListener('click', this.handleBlur);
+    document.addEventListener('keydown', this.handleBlurTab);
+  },
+  beforeDestroy() {
+    document.removeEventListener('click', this.handleBlur);
+    document.removeEventListener('keydown', this.handleBlurTab);
+  },
   methods: {
     /**
+     * 处理点击失焦
+     * @param {MouseEvent} e
+     */
+    handleBlur(e) {
+      if (e.target.tagName === 'INPUT') return;
+      this.inputFocus = false;
+    },
+    /**
+     * 处理 tab 键失焦
+     * @param {KeyboardEvent} e
+     */
+    handleBlurTab(e) {
+      if (e.key !== 'Tab') return;
+      this.inputFocus = false;
+    },
+    /**
+     * 处理输入框聚焦
+     * @param {number} i
+     * @param {number} j
+     */
+    handleInputFocus(i, j) {
+      this.inputFocus = true;
+      this.focusPostion = {
+        i,
+        j,
+      };
+    },
+    /**
+     * 选中描述
+     * @param {string} text 描述文本
+     */
+    selectedDescription(text) {
+      if (!this.inputFocus) return;
+      const { i, j } = this.focusPostion;
+      this.optionList[i].content_list[j].content = text;
+      this.inputFocus = false;
+    },
+    /**
      * 计算答题对错选项字体颜色
      * @param {string} optionMark 选项标识
      * @param {string} mark 选项标识
@@ -193,6 +250,24 @@ export default {
 .dialogue-preview {
   @include preview;
 
+  .description {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 4px 8px;
+
+    &-item {
+      padding: 4px 8px;
+      cursor: pointer;
+      background-color: #e9edf7;
+      border-radius: 4px;
+
+      &:hover,
+      &:active {
+        color: $light-main-color;
+      }
+    }
+  }
+
   .content {
     display: flex;
     flex-direction: column;

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

@@ -46,8 +46,8 @@ export default {
   mixins: [PreviewMixin],
   data() {
     return {
-      inputFocus: false,
       modelEssay: [],
+      inputFocus: false,
       focusPostion: {
         i: -1,
         j: -1,
@@ -56,7 +56,7 @@ export default {
   },
   computed: {
     descriptionList() {
-      return this.data.description.split(/\s+/);
+      return this.data.description.split(/\s+/).filter((item) => item);
     },
   },
   watch: {
@@ -111,14 +111,27 @@ export default {
     document.removeEventListener('keydown', this.handleBlurTab);
   },
   methods: {
+    /**
+     * 处理点击失焦
+     * @param {MouseEvent} e
+     */
     handleBlur(e) {
       if (e.target.tagName === 'INPUT') return;
       this.inputFocus = false;
     },
+    /**
+     * 处理 tab 键失焦
+     * @param {KeyboardEvent} e
+     */
     handleBlurTab(e) {
-      if (e.keyCode !== 9 || e.key !== 'Tab') return;
+      if (e.key !== 'Tab') return;
       this.inputFocus = false;
     },
+    /**
+     * 处理输入框聚焦
+     * @param {number} i
+     * @param {number} j
+     */
     handleInputFocus(i, j) {
       this.inputFocus = true;
       this.focusPostion = {
@@ -126,6 +139,10 @@ export default {
         j,
       };
     },
+    /**
+     * 选中描述
+     * @param {string} text 描述文本
+     */
     selectedDescription(text) {
       if (!this.inputFocus) return;
       const { i, j } = this.focusPostion;

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

@@ -62,7 +62,7 @@ export default {
   },
   computed: {
     descriptionList() {
-      return this.data.description.split(/\s+/);
+      return this.data.description.split(/\s+/).filter((item) => item);
     },
   },
   watch: {