Browse Source

排序 竖排

natasha 1 year ago
parent
commit
fe6f715b63

+ 56 - 41
package-lock.json

@@ -2492,6 +2492,49 @@
         "webpack-merge": "^5.7.3",
         "webpack-virtual-modules": "^0.4.2",
         "whatwg-fetch": "^3.6.2"
+      },
+      "dependencies": {
+        "@vue/vue-loader-v15": {
+          "version": "npm:vue-loader@15.11.1",
+          "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.11.1.tgz",
+          "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==",
+          "dev": true,
+          "requires": {
+            "@vue/component-compiler-utils": "^3.1.0",
+            "hash-sum": "^1.0.2",
+            "loader-utils": "^1.1.0",
+            "vue-hot-reload-api": "^2.3.0",
+            "vue-style-loader": "^4.1.0"
+          },
+          "dependencies": {
+            "hash-sum": {
+              "version": "1.0.2",
+              "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
+              "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
+              "dev": true
+            }
+          }
+        },
+        "json5": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz",
+          "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+          "dev": true,
+          "requires": {
+            "minimist": "^1.2.0"
+          }
+        },
+        "loader-utils": {
+          "version": "1.4.2",
+          "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.2.tgz",
+          "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
+          "dev": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^1.0.1"
+          }
+        }
       }
     },
     "@vue/cli-shared-utils": {
@@ -2679,47 +2722,6 @@
       "integrity": "sha512-RoorRB50WehYbsiWu497q8egZBYlrvOo9KBUG41uth4O023Cbs+7POLm9uw2CAiViBAIhvpw1Y4w4i+MZxOfXw==",
       "dev": true
     },
-    "@vue/vue-loader-v15": {
-      "version": "npm:vue-loader@15.11.1",
-      "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-15.11.1.tgz",
-      "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==",
-      "dev": true,
-      "requires": {
-        "@vue/component-compiler-utils": "^3.1.0",
-        "hash-sum": "^1.0.2",
-        "loader-utils": "^1.1.0",
-        "vue-hot-reload-api": "^2.3.0",
-        "vue-style-loader": "^4.1.0"
-      },
-      "dependencies": {
-        "hash-sum": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz",
-          "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==",
-          "dev": true
-        },
-        "json5": {
-          "version": "1.0.2",
-          "resolved": "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz",
-          "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
-          "dev": true,
-          "requires": {
-            "minimist": "^1.2.0"
-          }
-        },
-        "loader-utils": {
-          "version": "1.4.2",
-          "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-1.4.2.tgz",
-          "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
-          "dev": true,
-          "requires": {
-            "big.js": "^5.2.2",
-            "emojis-list": "^3.0.0",
-            "json5": "^1.0.1"
-          }
-        }
-      }
-    },
     "@vue/web-component-wrapper": {
       "version": "1.3.0",
       "resolved": "https://registry.npmmirror.com/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz",
@@ -10083,6 +10085,11 @@
         "websocket-driver": "^0.7.4"
       }
     },
+    "sortablejs": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.10.2.tgz",
+      "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
+    },
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmmirror.com/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -11858,6 +11865,14 @@
       "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
       "dev": true
     },
+    "vuedraggable": {
+      "version": "2.24.3",
+      "resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-2.24.3.tgz",
+      "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
+      "requires": {
+        "sortablejs": "1.10.2"
+      }
+    },
     "vuex": {
       "version": "3.6.2",
       "resolved": "https://registry.npmmirror.com/vuex/-/vuex-3.6.2.tgz",

+ 1 - 0
package.json

@@ -23,6 +23,7 @@
     "vue": "^2.6.14",
     "vue-i18n": "^8.28.2",
     "vue-router": "^3.6.5",
+    "vuedraggable": "^2.24.3",
     "vuex": "^3.6.2"
   },
   "devDependencies": {

+ 5 - 0
src/icons/svg/draggable.svg

@@ -0,0 +1,5 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="draggable">
+<path id="Vector" d="M7.0835 5.83398C7.77385 5.83398 8.3335 5.27434 8.3335 4.58398C8.3335 3.89363 7.77385 3.33398 7.0835 3.33398C6.39314 3.33398 5.8335 3.89363 5.8335 4.58398C5.8335 5.27434 6.39314 5.83398 7.0835 5.83398ZM7.0835 11.2507C7.77385 11.2507 8.3335 10.691 8.3335 10.0007C8.3335 9.31032 7.77385 8.75065 7.0835 8.75065C6.39314 8.75065 5.8335 9.31032 5.8335 10.0007C5.8335 10.691 6.39314 11.2507 7.0835 11.2507ZM8.3335 15.4173C8.3335 16.1077 7.77385 16.6673 7.0835 16.6673C6.39314 16.6673 5.8335 16.1077 5.8335 15.4173C5.8335 14.727 6.39314 14.1673 7.0835 14.1673C7.77385 14.1673 8.3335 14.727 8.3335 15.4173ZM12.9168 5.83398C13.6072 5.83398 14.1668 5.27434 14.1668 4.58398C14.1668 3.89363 13.6072 3.33398 12.9168 3.33398C12.2265 3.33398 11.6668 3.89363 11.6668 4.58398C11.6668 5.27434 12.2265 5.83398 12.9168 5.83398ZM14.1668 10.0007C14.1668 10.691 13.6072 11.2507 12.9168 11.2507C12.2265 11.2507 11.6668 10.691 11.6668 10.0007C11.6668 9.31032 12.2265 8.75065 12.9168 8.75065C13.6072 8.75065 14.1668 9.31032 14.1668 10.0007ZM12.9168 16.6673C13.6072 16.6673 14.1668 16.1077 14.1668 15.4173C14.1668 14.727 13.6072 14.1673 12.9168 14.1673C12.2265 14.1673 11.6668 14.727 11.6668 15.4173C11.6668 16.1077 12.2265 16.6673 12.9168 16.6673Z" fill="currentColor"/>
+</g>
+</svg>

+ 3 - 0
src/views/exercise_questions/create/components/create.vue

@@ -54,6 +54,7 @@ import TalkPicture from './exercises/TalkPicture.vue';
 import ChooseToneQuestion from './exercises/ChooseToneQuestion.vue';
 import RepeatQuestion from './exercises/RepeatQuestion.vue';
 import ReadQuestion from './exercises/ReadQuestion.vue';
+import SortQuestion from './exercises/SortQuestion.vue';
 
 export default {
   name: 'CreateMain',
@@ -71,6 +72,7 @@ export default {
     ChooseToneQuestion,
     RepeatQuestion,
     ReadQuestion,
+    SortQuestion,
   },
   provide() {
     return {
@@ -106,6 +108,7 @@ export default {
         choose_tone: ChooseToneQuestion,
         repeat: RepeatQuestion,
         read: ReadQuestion,
+        sort: SortQuestion,
       },
     };
   },

+ 141 - 0
src/views/exercise_questions/create/components/exercises/SortQuestion.vue

@@ -0,0 +1,141 @@
+<!-- 排序题 -->
+<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="输入描述"
+        />
+      </div>
+
+      <div class="content">
+        <ul>
+          <li v-for="(item, i) in data.option_list" :key="i" class="content-item">
+            <div class="option-content">
+              <RichText v-model="item.content" placeholder="输入内容" :inline="true" />
+            </div>
+            <SvgIcon icon-class="delete" class="delete pointer" @click="deleteOption(i)" />
+          </li>
+        </ul>
+      </div>
+
+      <div class="footer">
+        <span class="add-option" @click="addOption">
+          <SvgIcon icon-class="add-circle" size="14" /> <span>增加选项</span>
+        </span>
+      </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 sortTypeList"
+            :key="value"
+            v-model="data.property.layout_type"
+            :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"
+            :disabled="scoreTypeList[1].value === value && data.property.layout_type === sortTypeList[0].value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label-width="45px">
+          <el-input v-model="data.property.score" type="number" />
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import QuestionMixin from '../common/QuestionMixin.js';
+
+import { getOption, sortTypeList, getSortDataTemplate } from '@/views/exercise_questions/data/sort';
+
+export default {
+  name: 'SortQuestion',
+  components: {},
+  mixins: [QuestionMixin],
+  data() {
+    return {
+      sortTypeList,
+      data: getSortDataTemplate(),
+    };
+  },
+  watch: {
+    'data.option_list': {
+      handler(val) {
+        if (!val) return;
+        this.data.answer.answer_list = val.map(({ mark }) => {
+          return mark;
+        });
+      },
+      deep: true,
+    },
+  },
+  methods: {
+    addOption() {
+      this.data.option_list.push(getOption());
+    },
+  },
+};
+</script>

+ 3 - 0
src/views/exercise_questions/create/index.vue

@@ -71,6 +71,7 @@ import TalkPictruePreview from '../preview/TalkPictruePreview.vue';
 import ChooseTonePreview from '../preview/ChooseTonePreview.vue';
 import RepeatPreview from '../preview/RepeatPreview.vue';
 import ReadPreview from '../preview/ReadPreview.vue';
+import SortPreview from '../preview/SortPreview.vue';
 
 export default {
   name: 'CreateExercise',
@@ -88,6 +89,7 @@ export default {
     ChooseTonePreview,
     RepeatPreview,
     ReadPreview,
+    SortPreview,
   },
   provide() {
     return {
@@ -119,6 +121,7 @@ export default {
         choose_tone: ChooseTonePreview,
         repeat: RepeatPreview,
         read: ReadPreview,
+        sort: SortPreview,
       },
     };
   },

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

@@ -9,7 +9,7 @@ export const questionTypeOption = [
       { label: '选择题', value: 'select' },
       { label: '判断题', value: 'judge' },
       { label: '填空题', value: 'fill' },
-      { label: '排序题', value: 'sort', disabled: true },
+      { label: '排序题', value: 'sort' },
       { label: '连线题', value: 'matching' },
       { label: '选择声调', value: 'choose_tone' },
     ],

+ 35 - 0
src/views/exercise_questions/data/sort.js

@@ -0,0 +1,35 @@
+import { stemTypeList, scoreTypeList, questionNumberTypeList, switchOption } from './common';
+import { getRandomNumber } from '@/utils/index';
+
+export function getOption(content = '') {
+  return { content, mark: getRandomNumber() };
+}
+export const sortTypeList = [
+  { value: 'horizontal', label: '横排' },
+  { value: 'vertical', label: '竖排' },
+];
+// 选择题数据模板
+export function getSortDataTemplate() {
+  let option_list = [getOption(), getOption(), getOption()];
+  let answer_list = option_list.map(({ mark }) => mark);
+  return {
+    type: 'sort', // 题型
+    stem: '', // 题干
+    description: '', // 描述
+    option_list, // 选项
+    answer: { answer_list, score: 0, score_type: scoreTypeList[0].value }, // 答案
+    // 题型属性
+    property: {
+      stem_type: stemTypeList[0].value, // 题干类型
+      question_number: '1', // 题号
+      is_enable_description: switchOption[1].value, // 描述
+      score: 1, // 分值
+      score_type: scoreTypeList[0].value, // 分值类型
+      layout_type: sortTypeList[0].value,
+    },
+    // 其他属性
+    other: {
+      question_number_type: questionNumberTypeList[0].value, // 题号类型
+    },
+  };
+}

+ 131 - 0
src/views/exercise_questions/preview/SortPreview.vue

@@ -0,0 +1,131 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="sort-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>
+    <draggable
+      v-model="move_list"
+      animation="300"
+      :options="{
+        group: { name: 'itxst', pull: 'clone' },
+      }"
+      :sort="task_model == 'ANSWER' ? false : true"
+      class="content-box"
+      @start="onStart($event)"
+      @end="onEnd($event)"
+    >
+      <transition-group>
+        <div
+          v-for="(itemNode, indexNode) in move_list"
+          :key="indexNode"
+          :class="['drag-item', itemNode.correct == 'correct' ? 'correct' : 'error']"
+        >
+          <SvgIcon class="drag-icon" :size="20" icon-class="draggable" />
+          <span class="drag-content" v-html="sanitizeHTML(itemNode.content)"></span>
+        </div>
+      </transition-group>
+    </draggable>
+  </div>
+</template>
+
+<script>
+import Draggable from 'vuedraggable';
+import PreviewMixin from './components/PreviewMixin';
+
+export default {
+  name: 'SortPreview',
+  components: { Draggable },
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      task_model: '', // 答题模式
+      answer_list: [], // 存储用户答题
+      move_list: [], // 移动后的数组
+      drag: false,
+    };
+  },
+  watch: {
+    move_list: {
+      handler(val) {
+        if (!val) return;
+        this.answer_list = val.map(({ mark }) => {
+          return mark;
+        });
+        console.log(this.answer_list);
+      },
+      deep: true,
+    },
+  },
+  created() {
+    console.log(this.data);
+    this.handleData();
+  },
+  mounted() {},
+  methods: {
+    // 初始化数据
+    handleData() {
+      this.move_list = this.shuffle(JSON.parse(JSON.stringify(this.data.option_list)));
+    },
+    // 随机打乱数组顺序
+    shuffle(arr) {
+      for (let i = arr.length - 1; i > 0; i--) {
+        const j = Math.floor(Math.random() * (i + 1));
+        [arr[i], arr[j]] = [arr[j], arr[i]];
+      }
+      return arr;
+    },
+    onStart() {
+      this.drag = true;
+    },
+    // 拖拽结束事件
+    onEnd() {
+      this.drag = false;
+      this.changeuserAnswerJudge();
+      this.$forceUpdate();
+    },
+    // 判断对错
+    changeuserAnswerJudge() {
+      let flag = true;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.sort-preview {
+  @include preview;
+
+  .content-box {
+    .drag-item {
+      display: flex;
+      column-gap: 16px;
+      align-items: center;
+      margin-bottom: 8px;
+      cursor: move;
+    }
+
+    .drag-icon {
+      width: 20px;
+      height: 20px;
+      color: #000;
+    }
+
+    .drag-content {
+      flex: 1;
+      max-width: 800px;
+      padding: 8px 16px;
+      background: #f9f8f9;
+      border-radius: 4px;
+
+      :deep p {
+        margin: 0;
+      }
+    }
+  }
+}
+</style>