Ver código fonte

完成 练习题-判断题 编辑与预览

dusenyao 1 ano atrás
pai
commit
566df90eb4

+ 53 - 31
package-lock.json

@@ -1359,9 +1359,9 @@
       }
     },
     "@eslint/js": {
-      "version": "8.51.0",
-      "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.51.0.tgz",
-      "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==",
+      "version": "8.52.0",
+      "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.52.0.tgz",
+      "integrity": "sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==",
       "dev": true
     },
     "@gar/promisify": {
@@ -1386,12 +1386,12 @@
       }
     },
     "@humanwhocodes/config-array": {
-      "version": "0.11.11",
-      "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
-      "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
+      "version": "0.11.13",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+      "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
       "dev": true,
       "requires": {
-        "@humanwhocodes/object-schema": "^1.2.1",
+        "@humanwhocodes/object-schema": "^2.0.1",
         "debug": "^4.1.1",
         "minimatch": "^3.0.5"
       }
@@ -1403,9 +1403,9 @@
       "dev": true
     },
     "@humanwhocodes/object-schema": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+      "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
       "dev": true
     },
     "@jest/schemas": {
@@ -2091,6 +2091,12 @@
       "integrity": "sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ==",
       "dev": true
     },
+    "@ungap/structured-clone": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+      "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+      "dev": true
+    },
     "@vue/babel-helper-vue-jsx-merge-props": {
       "version": "1.4.0",
       "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz",
@@ -4005,9 +4011,9 @@
       "dev": true
     },
     "css-functions-list": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmmirror.com/css-functions-list/-/css-functions-list-3.2.0.tgz",
-      "integrity": "sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==",
+      "version": "3.2.1",
+      "resolved": "https://registry.npmmirror.com/css-functions-list/-/css-functions-list-3.2.1.tgz",
+      "integrity": "sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==",
       "dev": true
     },
     "css-loader": {
@@ -4830,18 +4836,19 @@
       "dev": true
     },
     "eslint": {
-      "version": "8.51.0",
-      "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.51.0.tgz",
-      "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==",
+      "version": "8.52.0",
+      "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.52.0.tgz",
+      "integrity": "sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==",
       "dev": true,
       "requires": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
         "@eslint/eslintrc": "^2.1.2",
-        "@eslint/js": "8.51.0",
-        "@humanwhocodes/config-array": "^0.11.11",
+        "@eslint/js": "8.52.0",
+        "@humanwhocodes/config-array": "^0.11.13",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
         "ajv": "^6.12.4",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
@@ -9348,9 +9355,9 @@
       "dev": true
     },
     "sass": {
-      "version": "1.69.3",
-      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.3.tgz",
-      "integrity": "sha512-X99+a2iGdXkdWn1akFPs0ZmelUzyAQfvqYc2P/MPTrJRuIRoTffGzT9W9nFqG00S+c8hXzVmgxhUuHFdrwxkhQ==",
+      "version": "1.69.4",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.69.4.tgz",
+      "integrity": "sha512-+qEreVhqAy8o++aQfCJwp0sklr2xyEzkm9Pp/Igu9wNPoe7EZEQ8X/MBvvXggI2ql607cxKg/RKOwDj6pp2XDA==",
       "dev": true,
       "requires": {
         "chokidar": ">=3.0.0 <4.0.0",
@@ -10021,9 +10028,9 @@
       }
     },
     "stylelint": {
-      "version": "15.10.3",
-      "resolved": "https://registry.npmmirror.com/stylelint/-/stylelint-15.10.3.tgz",
-      "integrity": "sha512-aBQMMxYvFzJJwkmg+BUUg3YfPyeuCuKo2f+LOw7yYbU8AZMblibwzp9OV4srHVeQldxvSFdz0/Xu8blq2AesiA==",
+      "version": "15.11.0",
+      "resolved": "https://registry.npmmirror.com/stylelint/-/stylelint-15.11.0.tgz",
+      "integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==",
       "dev": true,
       "requires": {
         "@csstools/css-parser-algorithms": "^2.3.1",
@@ -10033,12 +10040,12 @@
         "balanced-match": "^2.0.0",
         "colord": "^2.9.3",
         "cosmiconfig": "^8.2.0",
-        "css-functions-list": "^3.2.0",
+        "css-functions-list": "^3.2.1",
         "css-tree": "^2.3.1",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.1",
         "fastest-levenshtein": "^1.0.16",
-        "file-entry-cache": "^6.0.1",
+        "file-entry-cache": "^7.0.0",
         "global-modules": "^2.0.0",
         "globby": "^11.1.0",
         "globjoin": "^0.1.4",
@@ -10047,13 +10054,13 @@
         "import-lazy": "^4.0.0",
         "imurmurhash": "^0.1.4",
         "is-plain-object": "^5.0.0",
-        "known-css-properties": "^0.28.0",
+        "known-css-properties": "^0.29.0",
         "mathml-tag-names": "^2.1.3",
         "meow": "^10.1.5",
         "micromatch": "^4.0.5",
         "normalize-path": "^3.0.0",
         "picocolors": "^1.0.0",
-        "postcss": "^8.4.27",
+        "postcss": "^8.4.28",
         "postcss-resolve-nested-selector": "^0.1.1",
         "postcss-safe-parser": "^6.0.0",
         "postcss-selector-parser": "^6.0.13",
@@ -10096,6 +10103,21 @@
             "source-map-js": "^1.0.1"
           }
         },
+        "file-entry-cache": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-7.0.1.tgz",
+          "integrity": "sha512-uLfFktPmRetVCbHe5UPuekWrQ6hENufnA46qEGbfACkK5drjTTdQYUragRgMjHldcbYG+nslUerqMPjbBSHXjQ==",
+          "dev": true,
+          "requires": {
+            "flat-cache": "^3.1.1"
+          }
+        },
+        "known-css-properties": {
+          "version": "0.29.0",
+          "resolved": "https://registry.npmmirror.com/known-css-properties/-/known-css-properties-0.29.0.tgz",
+          "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
+          "dev": true
+        },
         "mdn-data": {
           "version": "2.0.30",
           "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz",
@@ -10928,9 +10950,9 @@
       "dev": true
     },
     "tinymce": {
-      "version": "5.10.7",
-      "resolved": "https://registry.npmmirror.com/tinymce/-/tinymce-5.10.7.tgz",
-      "integrity": "sha512-9UUjaO0R7FxcFo0oxnd1lMs7H+D0Eh+dDVo5hKbVe1a+VB0nit97vOqlinj+YwgoBDt6/DSCUoWqAYlLI8BLYA=="
+      "version": "5.10.8",
+      "resolved": "https://registry.npmmirror.com/tinymce/-/tinymce-5.10.8.tgz",
+      "integrity": "sha512-iyoo3VGMAJhLMDdblAefKvYgBRk9kQi58GTwAmoieqsyggGsKZWlQl/YY6nTILFHUCA1FhYu0HdmM5YYjs17UQ=="
     },
     "titleize": {
       "version": "3.0.0",

+ 4 - 4
package.json

@@ -15,7 +15,7 @@
     "js-cookie": "^3.0.5",
     "md5": "^2.3.0",
     "nprogress": "^0.2.0",
-    "tinymce": "^5.10.7",
+    "tinymce": "^5.10.8",
     "vue": "^2.6.14",
     "vue-i18n": "^8.28.2",
     "vue-router": "^3.6.5",
@@ -32,14 +32,14 @@
     "@vue/preload-webpack-plugin": "^2.0.0",
     "babel-plugin-dynamic-import-node": "^2.3.3",
     "compression-webpack-plugin": "^6.1.1",
-    "eslint": "^8.51.0",
+    "eslint": "^8.52.0",
     "eslint-plugin-prettier": "^5.0.1",
     "eslint-plugin-vue": "^9.17.0",
     "postcss-html": "^1.5.0",
     "prettier": "^3.0.3",
-    "sass": "^1.69.3",
+    "sass": "^1.69.4",
     "sass-loader": "^13.3.2",
-    "stylelint": "^15.10.3",
+    "stylelint": "^15.11.0",
     "stylelint-config-recess-order": "^4.3.0",
     "stylelint-config-recommended-scss": "^13.0.0",
     "stylelint-config-recommended-vue": "^1.5.0",

+ 11 - 2
src/api/exercise.js

@@ -58,6 +58,15 @@ export function GetExerciseQuestionIndexList(data) {
  * @param {object} data
  * @param {string} data.question_id 题目id
  */
-export function GetQuestion(data) {
-  return http.post(`/TeachingServer/ExerciseManager/GetQuestion`, data);
+export function GetQuestionInfo(data) {
+  return http.post(`/TeachingServer/ExerciseManager/GetQuestionInfo`, data);
+}
+
+/**
+ * 删除题目
+ * @param {object} data
+ * @param {string} data.question_id 题目id
+ */
+export function DeleteQuestion(data) {
+  return http.post(`/TeachingServer/ExerciseManager/DeleteQuestion`, data);
 }

+ 5 - 0
src/icons/svg/check-mark.svg

@@ -0,0 +1,5 @@
+<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" clip-rule="evenodd"
+    d="M3.76258 5.71248L9.06588 0.40918L9.77299 1.11629L3.76258 7.12669L0.227051 3.59116L0.934158 2.88405L3.76258 5.71248Z"
+    fill="currentColor" />
+</svg>

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

@@ -0,0 +1,5 @@
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path
+    d="M5 10C2.23857 10 0 7.7614 0 5C0 2.23857 2.23857 0 5 0C7.7614 0 10 2.23857 10 5C10 7.7614 7.7614 10 5 10ZM5 9C7.20915 9 9 7.20915 9 5C9 2.79086 7.20915 1 5 1C2.79086 1 1 2.79086 1 5C1 7.20915 2.79086 9 5 9Z"
+    fill="currentColor" />
+</svg>

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

@@ -0,0 +1,5 @@
+<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path fill-rule="evenodd" clip-rule="evenodd"
+    d="M3.29282 3.99993L0.11084 0.817947L0.817947 0.11084L3.99993 3.29282L7.18191 0.11084L7.88901 0.817947L4.70703 3.99993L7.88901 7.18191L7.18191 7.88902L3.99993 4.70703L0.817947 7.88901L0.11084 7.18191L3.29282 3.99993Z"
+    fill="currentColor" />
+</svg>

+ 1 - 1
src/icons/svg/practice.svg

@@ -1,5 +1,5 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
   <path
     d="M12.6666 14.6666H3.33331C2.22875 14.6666 1.33331 13.7712 1.33331 12.6666V1.99992C1.33331 1.63173 1.63179 1.33325 1.99998 1.33325H11.3333C11.7015 1.33325 12 1.63173 12 1.99992V9.99992H14.6666V12.6666C14.6666 13.7712 13.7712 14.6666 12.6666 14.6666ZM12 11.3333V12.6666C12 13.0348 12.2984 13.3333 12.6666 13.3333C13.0348 13.3333 13.3333 13.0348 13.3333 12.6666V11.3333H12ZM10.6666 13.3333V2.66659H2.66665V12.6666C2.66665 13.0348 2.96513 13.3333 3.33331 13.3333H10.6666ZM3.99998 4.66659H9.33331V5.99992H3.99998V4.66659ZM3.99998 7.33325H9.33331V8.66659H3.99998V7.33325ZM3.99998 9.99992H7.33331V11.3333H3.99998V9.99992Z"
-    fill="#4E5969" />
+    fill="currentColor" />
 </svg>

+ 3 - 0
src/styles/element-variables.scss

@@ -1,5 +1,8 @@
 @use './variables.scss' as *;
 
+// base
+$--color-primary: $main-color;
+
 // table
 $--table-header-background-color: $fill-color;
 $--table-border: 1px solid $border-color;

+ 24 - 17
src/styles/mixin.scss

@@ -1,29 +1,36 @@
 @use './variables.scss' as *;
 
-@mixin question-content {
-  width: 100%;
+// 预览
+@mixin preview {
+  display: flex;
+  flex-direction: column;
+  row-gap: 24px;
+  padding: 64px;
+  margin: 16px 0;
   background-color: #fff;
-  border: $border;
   border-radius: 8px;
 
-  &-container {
+  // 题干
+  .stem {
     display: flex;
-    width: 100%;
-    padding: 24px;
+    align-items: center;
+    font-size: 18px;
+    font-weight: bold;
+    color: #34343a;
 
-    &-content {
-      flex: 1;
+    .question-number {
+      margin-right: 4px;
     }
 
-    .setting {
-      &::before {
-        display: inline-block;
-        width: 2px;
-        height: 100%;
-        margin: 0 24px;
-        content: '';
-        background-color: $border-color;
-      }
+    :deep p {
+      margin: 0;
     }
   }
+
+  // 描述
+  .description {
+    padding: 12px 24px;
+    background-color: #f9f8f9;
+    border-radius: 16px;
+  }
 }

+ 91 - 18
src/views/exercise_questions/create/components/common/QuestionBase.vue

@@ -1,32 +1,20 @@
 <template>
   <div class="question">
-    <SelectQuestionType :type="type" />
     <div class="question-container">
       <div class="question-content">
         <slot name="content"></slot>
       </div>
-      <div v-if="isSetting" class="setting">
-        <slot name="setting"></slot>
+      <div v-if="isSetting" class="property">
+        <slot name="property"></slot>
       </div>
     </div>
   </div>
 </template>
 
 <script>
-import SelectQuestionType from './SelectQuestionType.vue';
-
 export default {
   name: 'QuestionBase',
-  components: {
-    SelectQuestionType,
-  },
   inject: ['isSetUp'],
-  props: {
-    type: {
-      type: Array,
-      required: true,
-    },
-  },
   data() {
     return {};
   },
@@ -40,13 +28,11 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-@use '~@/styles/mixin.scss' as *;
-
 .question {
   width: 100%;
   background-color: #fff;
   border: $border;
-  border-radius: 8px;
+  border-radius: 0 0 8px 8px;
 
   &-container {
     display: flex;
@@ -55,9 +41,90 @@ export default {
 
     .question-content {
       flex: 1;
+
+      .stem {
+        padding-bottom: 8px;
+        margin-bottom: 8px;
+        border-bottom: 1px solid $border-color;
+
+        .el-textarea {
+          margin-top: 8px;
+        }
+      }
+
+      .content {
+        > ul {
+          display: flex;
+          flex-direction: column;
+          row-gap: 8px;
+          width: 100%;
+
+          .content-item {
+            display: flex;
+            column-gap: 4px;
+            align-items: center;
+
+            .question-number {
+              min-width: 40px;
+              height: 32px;
+              padding: 4px 8px;
+              color: $text-color;
+              cursor: default;
+              background-color: $fill-color;
+              border-radius: 2px;
+            }
+
+            .option-content {
+              display: flex;
+              flex: 1;
+              align-items: center;
+              padding-left: 16px;
+              background-color: $fill-color;
+
+              .rich-text {
+                flex: 1;
+                min-height: 32px;
+
+                :deep &.mce-content-body {
+                  padding-top: 4px;
+                }
+
+                :deep &:not(.mce-edit-focus) {
+                  p {
+                    margin: 0;
+                  }
+                }
+
+                :deep &.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before {
+                  top: 6px;
+                }
+              }
+            }
+
+            .delete {
+              margin-left: 12px;
+            }
+          }
+        }
+      }
+
+      .footer {
+        display: flex;
+        justify-content: center;
+        margin-top: 12px;
+        color: $main-color;
+
+        .add-option {
+          display: flex;
+          column-gap: 8px;
+          align-items: center;
+          font-size: 14px;
+          cursor: pointer;
+        }
+      }
     }
 
-    .setting {
+    .property {
       display: flex;
 
       &::before {
@@ -69,6 +136,12 @@ export default {
         background-color: $border-color;
       }
 
+      .el-form {
+        .el-input {
+          width: 250px;
+        }
+      }
+
       :deep .el-form-item.el-form-item--small {
         display: flex;
       }

+ 13 - 5
src/views/exercise_questions/create/components/common/SelectQuestionType.vue

@@ -2,7 +2,7 @@
   <div class="question-type">
     <el-cascader
       v-model="typeArr"
-      :options="options"
+      :options="questionTypeOption"
       :show-all-levels="false"
       :props="{ expandTrigger: 'hover' }"
       @change="handleChange"
@@ -24,7 +24,7 @@ export default {
   components: {
     IntelligentRecognition,
   },
-  inject: ['curSaveDate', 'recognition'],
+  inject: ['curSaveDate', 'recognition', 'updateCurQuestionType'],
   props: {
     type: {
       type: Array,
@@ -35,7 +35,7 @@ export default {
     return {
       dialogVisible: false, // 智能识别弹窗
       typeArr: this.type,
-      options: questionTypeOption,
+      questionTypeOption,
     };
   },
   computed: {
@@ -43,9 +43,14 @@ export default {
       return this.curSaveDate();
     },
   },
+  watch: {
+    type(val) {
+      this.typeArr = val;
+    },
+  },
   methods: {
     handleChange(val) {
-      console.log(val);
+      this.updateCurQuestionType(val);
     },
 
     showRecognition() {
@@ -62,7 +67,10 @@ export default {
   justify-content: space-between;
   width: 100%;
   padding: 4px 8px;
-  border-bottom: $border;
+  background-color: #fff;
+  border: $border;
+  border-bottom-width: 0;
+  border-radius: 8px 8px 0 0;
 
   .el-cascader {
     width: 100px;

+ 16 - 12
src/views/exercise_questions/create/components/create.vue

@@ -26,8 +26,11 @@
       </div>
     </div>
     <div class="create-content">
+      <SelectQuestionType v-if="indexList.length > 0" :type="exerciseTypeList[indexList[curIndex].type]" />
       <template v-for="({ id }, i) in indexList">
-        <component :is="curExercisePage" v-if="i === curIndex" ref="exercise" :key="id" :question-id="id" />
+        <KeepAlive :key="id">
+          <component :is="curExercisePage" v-if="i === curIndex" ref="exercise" :question-id="id" />
+        </KeepAlive>
       </template>
     </div>
   </main>
@@ -36,14 +39,18 @@
 <script>
 import { SaveQuestion } from '@/api/exercise';
 import { timeFormatConversion } from '@/utils/transform';
-import { scoreTypeList } from '@/views/exercise_questions/data/common';
+import { scoreTypeList, exerciseTypeList } from '@/views/exercise_questions/data/common';
 
+import SelectQuestionType from './common/SelectQuestionType.vue';
 import SelectQuestion from './exercises/SelectQuestion.vue';
+import JudgeQuestion from './exercises/JudgeQuestion.vue';
 
 export default {
   name: 'CreateMain',
   components: {
     SelectQuestion,
+    JudgeQuestion,
+    SelectQuestionType,
   },
   provide() {
     return {
@@ -65,8 +72,10 @@ export default {
     return {
       timer: '',
       curSaveDate: '',
+      exerciseTypeList,
       exerciseComponents: {
         select: SelectQuestion,
+        judge: JudgeQuestion,
       },
     };
   },
@@ -110,7 +119,7 @@ export default {
      * 删除
      */
     deleteQuestion() {
-      this.$confirm('是否删除当前练习题', '提示', {
+      this.$confirm('是否删除当前题', '提示', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning',
@@ -125,20 +134,15 @@ export default {
      */
     dataConversion() {
       const data = this.$refs.exercise[0].data;
-      if (data.setting.score_type === scoreTypeList[0].value) {
-        data.answer.score = data.setting.score;
-      }
-      if (data.setting.score_type === scoreTypeList[1].value) {
-        data.answer.item_score = data.setting.score;
-      }
+      const { score, score_type, select_type } = data.property;
+      data.answer.score = score;
+      data.answer.score_type = score_type;
 
       return {
         question_id: this.indexList[this.curIndex].id,
         type: data.type,
-        additional_type: data.setting.select_type,
-        file_id_list: [],
+        additional_type: select_type || '',
         content: JSON.stringify(data),
-        answer: JSON.stringify(data.answer),
       };
     },
     /**

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

@@ -0,0 +1,259 @@
+<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="输入题干" />
+
+        <UploadAudio
+          v-show="data.property.is_enable_listening"
+          :file-id="data.file_id_list?.[0]"
+          @upload="upload"
+          @deleteFile="deleteFile"
+        />
+      </div>
+
+      <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>
+            <div class="option-content">
+              <RichText v-model="item.content" placeholder="输入内容" :inline="true" />
+            </div>
+            <div class="option-type">
+              <div
+                v-for="option_type in data.property.option_type_list"
+                :key="option_type"
+                :class="[
+                  'option-type-item',
+                  {
+                    active: data.answer.select_list.find(
+                      (li) => li.mark === item.mark && li.option_type === option_type,
+                    ),
+                  },
+                ]"
+                @click="selectOptionAnswer(option_type, item.mark)"
+              >
+                <SvgIcon
+                  v-if="option_type === option_type_list[0].value"
+                  icon-class="check-mark"
+                  width="10"
+                  height="7"
+                />
+                <SvgIcon v-if="option_type === option_type_list[1].value" icon-class="cross" size="8" />
+                <SvgIcon v-if="option_type === option_type_list[2].value" icon-class="circle" size="10" />
+              </div>
+            </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_listening"
+            :label="value"
+          >
+            {{ label }}
+          </el-radio>
+        </el-form-item>
+        <el-form-item label="选项">
+          <el-checkbox-group v-model="optionTypeList">
+            <el-checkbox v-for="{ label, value } in option_type_list" :key="value" :label="label" />
+          </el-checkbox-group>
+        </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 v-model="data.property.score" type="number" />
+        </el-form-item>
+      </el-form>
+    </template>
+  </QuestionBase>
+</template>
+
+<script>
+import RichText from '@/components/common/RichText.vue';
+import UploadAudio from '../common/UploadAudio.vue';
+import QuestionBase from '../common/QuestionBase.vue';
+
+import {
+  stemTypeList,
+  questionNumberTypeList,
+  switchOption,
+  scoreTypeList,
+  computedQuestionNumber,
+  changeOptionType,
+} from '@/views/exercise_questions/data/common';
+import { judgeData, option_type_list, option_type_value_list, getOption } from '@/views/exercise_questions/data/judge';
+
+export default {
+  name: 'JudgeQuestion',
+  components: {
+    QuestionBase,
+    RichText,
+    UploadAudio,
+  },
+  props: {
+    questionId: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      stemTypeList,
+      option_type_list,
+      questionNumberTypeList,
+      switchOption,
+      scoreTypeList,
+      computedQuestionNumber,
+      changeOptionType,
+      data: JSON.parse(JSON.stringify(judgeData)),
+    };
+  },
+  computed: {
+    optionTypeList: {
+      get() {
+        return this.data.property.option_type_list
+          .map((item) => {
+            let type = this.option_type_list.find(({ value }) => value === item);
+            return type ? type.label : '';
+          })
+          .filter((word) => word && word.length > 0);
+      },
+      set(val) {
+        this.data.property.option_type_list = val
+          .map((item) => {
+            let type = this.option_type_list.find(({ label }) => label === item);
+            return type ? type.value : '';
+          })
+          .filter((word) => word && word.length > 0)
+          .sort((a, b) => option_type_value_list.indexOf(a) - option_type_value_list.indexOf(b));
+      },
+    },
+  },
+  methods: {
+    upload(file_id) {
+      this.data.file_id_list.push(file_id);
+    },
+    deleteFile(file_id) {
+      let index = this.data.file_id_list.indexOf(file_id);
+      if (index !== -1) {
+        this.data.file_id_list.splice(index, 1);
+      }
+    },
+    /**
+     * 设置题目内容
+     * @param {object} param
+     * @param {string} param.content 题目内容
+     */
+    setQuestion({ content }) {
+      this.data = JSON.parse(content);
+    },
+    deleteOption(i) {
+      this.data.option_list.splice(i, 1);
+    },
+    addOption() {
+      this.data.option_list.push(getOption());
+    },
+    /**
+     * 选择选项答案
+     * @param {String} option_type 选项类型
+     * @param {String} mark 选项标记
+     */
+    selectOptionAnswer(option_type, mark) {
+      const index = this.data.answer.select_list.findIndex((item) => item.mark === mark);
+      if (index === -1) {
+        this.data.answer.select_list.push({ option_type, mark });
+      } else {
+        this.data.answer.select_list[index].option_type = option_type;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.content {
+  &-item {
+    .option-type {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+      height: 32px;
+      padding: 8px 16px;
+      background-color: $fill-color;
+
+      &-item {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 16px;
+        height: 16px;
+        color: #4e5969;
+        cursor: pointer;
+        background-color: #d7d7d7;
+        border-radius: 2px;
+
+        &.active {
+          color: #fff;
+          background-color: $main-color;
+        }
+      }
+    }
+  }
+}
+</style>

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

@@ -1,10 +1,10 @@
 <!-- 选择题 -->
 <template>
-  <QuestionBase :type="selectType">
+  <QuestionBase>
     <template #content>
       <div class="stem">
         <el-input
-          v-if="data.setting.stem_type === stemTypeList[0].value"
+          v-if="data.property.stem_type === stemTypeList[0].value"
           v-model="data.stem"
           rows="3"
           resize="none"
@@ -12,11 +12,11 @@
           placeholder="输入题干"
         />
 
-        <RichText v-if="data.setting.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="data.setting.is_describe"
-          v-model="data.describe"
+          v-show="data.property.is_enable_description"
+          v-model="data.description"
           rows="3"
           resize="none"
           type="textarea"
@@ -24,19 +24,21 @@
         />
 
         <UploadAudio
-          v-show="data.setting.is_hear"
+          v-show="data.property.is_enable_listening"
           :file-id="data.file_id_list?.[0]"
           @upload="upload"
           @deleteFile="deleteFile"
         />
       </div>
+
       <div class="content">
         <ul>
-          <li v-for="(item, i) in data.options" :key="i" class="content-item">
-            <span class="question-number">{{ computedQuestionNumber(i) }}.</span>
+          <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>
             <div class="option-content">
               <span :class="['checkbox', { active: isAnswer(item.mark) }]" @click="selectAnswer(item.mark)"></span>
-              <!-- <el-input v-model="item.content" placeholder="输入内容" /> -->
               <RichText v-model="item.content" placeholder="输入内容" :inline="true" />
             </div>
             <SvgIcon icon-class="delete" class="delete pointer" @click="deleteOption(i)" />
@@ -51,26 +53,26 @@
       </div>
     </template>
 
-    <template #setting>
-      <el-form :model="data.setting">
+    <template #property>
+      <el-form :model="data.property">
         <el-form-item label="题干">
           <el-radio
             v-for="{ value, label } in stemTypeList"
             :key="value"
-            v-model="data.setting.stem_type"
+            v-model="data.property.stem_type"
             :label="value"
           >
             {{ label }}
           </el-radio>
         </el-form-item>
         <el-form-item label="题号">
-          <el-input v-model="data.setting.question_number" />
+          <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.setting.question_number_type"
+            v-model="data.other.question_number_type"
             :label="value"
           >
             {{ label }}
@@ -80,7 +82,7 @@
           <el-radio
             v-for="{ value, label } in switchOption"
             :key="value"
-            v-model="data.setting.is_describe"
+            v-model="data.property.is_enable_description"
             :label="value"
           >
             {{ label }}
@@ -90,7 +92,7 @@
           <el-radio
             v-for="{ value, label } in selectTypeList"
             :key="value"
-            v-model="data.setting.select_type"
+            v-model="data.property.select_type"
             :label="value"
             @input="changeSelectType"
           >
@@ -98,7 +100,12 @@
           </el-radio>
         </el-form-item>
         <el-form-item label="听力">
-          <el-radio v-for="{ value, label } in switchOption" :key="value" v-model="data.setting.is_hear" :label="value">
+          <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>
@@ -106,14 +113,15 @@
           <el-radio
             v-for="{ value, label } in scoreTypeList"
             :key="value"
-            v-model="data.setting.score_type"
+            v-model="data.property.score_type"
             :label="value"
+            :disabled="scoreTypeList[1].value === value && data.property.select_type === selectTypeList[0].value"
           >
             {{ label }}
           </el-radio>
         </el-form-item>
         <el-form-item label-width="45px">
-          <el-input v-model="data.setting.score" type="number" />
+          <el-input v-model="data.property.score" type="number" />
         </el-form-item>
       </el-form>
     </template>
@@ -130,11 +138,11 @@ import {
   selectTypeList,
   switchOption,
   scoreTypeList,
-  computeOptionMethods,
+  computedQuestionNumber,
+  changeOptionType,
   questionNumberTypeList,
 } from '@/views/exercise_questions/data/common';
-import { selectData, selectType } from '@/views/exercise_questions/data/select';
-import { GetQuestion } from '@/api/exercise';
+import { selectData } from '@/views/exercise_questions/data/select';
 import { getRandomNumber } from '@/utils/index';
 
 export default {
@@ -152,18 +160,16 @@ export default {
   },
   data() {
     return {
-      selectType,
       stemTypeList,
       selectTypeList,
       switchOption,
       scoreTypeList,
       questionNumberTypeList,
+      changeOptionType,
+      computedQuestionNumber,
       data: JSON.parse(JSON.stringify(selectData)),
     };
   },
-  created() {
-    this.getQuestion();
-  },
   methods: {
     upload(file_id) {
       this.data.file_id_list.push(file_id);
@@ -186,7 +192,7 @@ export default {
 
       if (arr.length > 0) {
         this.data.stem = arr[0];
-        this.data.options = arr.slice(1).map((content) => {
+        this.data.option_list = arr.slice(1).map((content) => {
           return {
             mark: getRandomNumber(),
             content,
@@ -194,36 +200,35 @@ export default {
         });
       }
     },
-
-    getQuestion() {
-      GetQuestion({ question_id: this.questionId })
-        .then(({ file_list, question }) => {
-          if (question.content) {
-            this.data = JSON.parse(question.content);
-          }
-        })
-        .catch((err) => {
-          console.log(err);
-        });
+    /**
+     * 设置题目内容
+     * @param {object} param
+     * @param {string} param.content 题目内容
+     */
+    setQuestion({ content }) {
+      this.data = JSON.parse(content);
     },
     deleteOption(i) {
-      this.data.options.splice(i, 1);
+      this.data.option_list.splice(i, 1);
     },
     changeSelectType(val) {
-      console.log(val);
-      if (this.data.setting.select_type === selectTypeList[0].value && this.data.answer.select_list.length > 1) {
+      if (val === selectTypeList[0].value && this.data.answer.select_list.length > 1) {
         this.data.answer.select_list = [this.data.answer.select_list[0]];
       }
+      // 当多选题切换到单选题时,分值类型切换为总分
+      if (val === selectTypeList[0].value && this.data.property.score_type === scoreTypeList[1].value) {
+        this.data.property.score_type = scoreTypeList[0].value;
+      }
     },
     isAnswer(mark) {
       return this.data.answer.select_list.indexOf(mark) !== -1;
     },
     selectAnswer(mark) {
       let index = this.data.answer.select_list.indexOf(mark);
-      if (this.data.setting.select_type === selectTypeList[0].value) {
+      if (this.data.property.select_type === selectTypeList[0].value) {
         this.data.answer.select_list = [mark];
       }
-      if (this.data.setting.select_type === selectTypeList[1].value) {
+      if (this.data.property.select_type === selectTypeList[1].value) {
         if (index === -1) {
           this.data.answer.select_list.push(mark);
         } else {
@@ -232,129 +237,40 @@ export default {
       }
     },
     addOption() {
-      this.data.options.push({
+      this.data.option_list.push({
         mark: getRandomNumber(),
         content: '',
       });
     },
-    computedQuestionNumber(i) {
-      const computationMethod = computeOptionMethods[this.data.option_type];
-      if (computationMethod) {
-        return computationMethod(i);
-      }
-      return '';
-    },
   },
 };
 </script>
 
 <style lang="scss" scoped>
-.stem {
-  padding-bottom: 8px;
-  margin-bottom: 8px;
-  border-bottom: 1px solid $border-color;
-
-  .el-textarea {
-    margin-top: 8px;
-  }
-}
-
 .content {
-  > ul {
-    display: flex;
-    flex-direction: column;
-    row-gap: 8px;
-    width: 100%;
-
-    .content-item {
-      display: flex;
-      column-gap: 4px;
+  .option-content {
+    .checkbox {
+      display: inline-flex;
       align-items: center;
+      justify-content: center;
+      width: 16px;
+      height: 16px;
+      margin-right: 8px;
+      cursor: pointer;
+      border: 1px solid #333;
+      border-radius: 50%;
 
-      .question-number {
-        min-width: 40px;
-        height: 32px;
-        padding: 4px 8px;
-        color: $text-color;
-        background-color: $fill-color;
-        border-radius: 2px;
-      }
-
-      .option-content {
-        display: flex;
-        flex: 1;
-        align-items: center;
-        padding-left: 16px;
-        background-color: $fill-color;
-
-        .checkbox {
-          display: inline-flex;
-          align-items: center;
-          justify-content: center;
-          width: 16px;
-          height: 16px;
-          margin-right: 8px;
-          cursor: pointer;
-          border: 1px solid #333;
+      &.active {
+        &::before {
+          display: inline-block;
+          width: 6px;
+          height: 6px;
+          content: '';
+          background-color: #333;
           border-radius: 50%;
-
-          &.active {
-            &::before {
-              display: inline-block;
-              width: 6px;
-              height: 6px;
-              content: '';
-              background-color: #333;
-              border-radius: 50%;
-            }
-          }
-        }
-
-        .rich-text {
-          flex: 1;
-          min-height: 32px;
-
-          :deep &.mce-content-body {
-            padding-top: 4px;
-          }
-
-          :deep &:not(.mce-edit-focus) {
-            p {
-              margin: 0;
-            }
-          }
-
-          :deep &.mce-content-body[data-mce-placeholder]:not(.mce-visualblocks)::before {
-            top: 6px;
-          }
         }
       }
-
-      .delete {
-        margin-left: 12px;
-      }
     }
   }
 }
-
-.footer {
-  display: flex;
-  justify-content: center;
-  margin-top: 12px;
-  color: $main-color;
-
-  .add-option {
-    display: flex;
-    column-gap: 8px;
-    align-items: center;
-    font-size: 14px;
-    cursor: pointer;
-  }
-}
-
-.el-form {
-  .el-input {
-    width: 250px;
-  }
-}
 </style>

+ 53 - 15
src/views/exercise_questions/create/index.vue

@@ -10,7 +10,7 @@
             <span class="item-name nowrap-ellipsis" @click="selectExerciseItem(i)">
               {{ i + 1 }}. {{ exerciseNames[item.type] }}
             </span>
-            <SvgIcon icon-class="copy" class="pointer" :size="10" @click="copy(i, item.type)" />
+            <SvgIcon icon-class="copy" class="pointer" :size="10" @click="copy(i)" />
           </li>
         </ul>
       </div>
@@ -56,20 +56,23 @@
 
 <script>
 import { exerciseNames } from '../data/common';
-import { AddQuestionToExercise, GetExerciseQuestionIndexList } from '@/api/exercise';
+import { AddQuestionToExercise, DeleteQuestion, GetExerciseQuestionIndexList, GetQuestionInfo } from '@/api/exercise';
 
 import CreateMain from './components/create.vue';
 import SelectPreview from '@/views/exercise_questions/preview/SelectPreview.vue';
+import JudgePreview from '@/views/exercise_questions/preview/JudgePreview.vue';
 
 export default {
   name: 'CreateExercise',
   components: {
     CreateMain,
     SelectPreview,
+    JudgePreview,
   },
   provide() {
     return {
       isSetUp: () => this.isSetUp,
+      updateCurQuestionType: this.updateCurQuestionType,
     };
   },
   data() {
@@ -83,7 +86,7 @@ export default {
       isSetUp: false, // 设置
       preview: false, // 预览显示
       previewData: {}, // 预览数据
-      previewComponents: { select: SelectPreview },
+      previewComponents: { select: SelectPreview, judge: JudgePreview },
     };
   },
   computed: {
@@ -92,18 +95,20 @@ export default {
       return this.previewComponents[this.index_list[this.curIndex].type];
     },
   },
+  watch: {
+    curIndex() {
+      if (this.index_list.length === 0) return;
+      this.getQuestionInfo();
+    },
+  },
   created() {
-    this.getExerciseQuestionIndexList();
+    this.getExerciseQuestionIndexList(true);
   },
   methods: {
     // 返回练习管理
     back() {
       this.$router.push('/personal_question');
     },
-    // 新建练习
-    createExercise() {
-      this.index_list.push({ type: 'select' });
-    },
     /**
      * 添加题目到练习
      * @param {string} type
@@ -121,17 +126,34 @@ export default {
         .then(() => {
           this.getExerciseQuestionIndexList();
         })
-        .catch((err) => {
-          console.log(err);
+        .catch(() => {
+          this.$message.error('添加失败');
         });
     },
     /**
      * 获取练习题目索引列表
      */
-    getExerciseQuestionIndexList() {
+    getExerciseQuestionIndexList(init) {
       GetExerciseQuestionIndexList({ exercise_id: this.id })
         .then(({ index_list }) => {
           this.index_list = index_list;
+
+          if (!init) return;
+          this.getQuestionInfo();
+        })
+        .catch((err) => {
+          console.log(err);
+        });
+    },
+    getQuestionInfo() {
+      if (this.index_list.length === 0) return;
+      GetQuestionInfo({ question_id: this.index_list[this.curIndex].id })
+        .then(({ question }) => {
+          if (question.content) {
+            this.$nextTick(() => {
+              this.$refs.createMain.$refs.exercise?.[0].setQuestion(question);
+            });
+          }
         })
         .catch((err) => {
           console.log(err);
@@ -140,10 +162,8 @@ export default {
     /**
      * 复制练习
      * @param {Number} index 练习索引
-     * @param {String} type 练习类型
      */
-    copy(index, type) {
-      // this.index_list.splice(index + 1, 0, { type });
+    copy(index) {
       // TODO
     },
     /**
@@ -168,7 +188,25 @@ export default {
       this.getPreviewData();
     },
     // 删除练习
-    deleteQuestion() {},
+    deleteQuestion() {
+      DeleteQuestion({ question_id: this.index_list[this.curIndex].id })
+        .then(() => {
+          this.curIndex = Math.max(0, this.curIndex - 1);
+          this.getExerciseQuestionIndexList();
+          this.$message.success('删除成功');
+        })
+        .catch(() => {
+          this.$message.error('删除失败');
+        });
+    },
+    /**
+     * 修改当前题目类型
+     * @param {Array} arr
+     */
+    updateCurQuestionType(arr) {
+      let type = arr[arr.length - 1];
+      this.index_list[this.curIndex].type = type;
+    },
   },
 };
 </script>

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

@@ -1,8 +1,5 @@
 import { digitToChinese } from '@/utils/transform';
 
-// 练习名称
-export const exerciseNames = { select: '选择题', fill: '填空题', judge: '判断题' };
-
 // 题型选项
 export const questionTypeOption = [
   {
@@ -54,6 +51,33 @@ export const questionTypeOption = [
   },
 ];
 
+// 练习名称
+export const exerciseNames = questionTypeOption
+  .map(({ label, value, children }) => {
+    if (children) return children;
+    return { label, value };
+  })
+  .flat()
+  .reduce((obj, { label, value }) => {
+    obj[value] = label;
+    return obj;
+  }, {});
+
+// 题型类型列表
+export const exerciseTypeList = questionTypeOption
+  .map(({ value, children }) => {
+    if (children) {
+      return children.map(({ value: children_value }) => {
+        return { [children_value]: [value, children_value] };
+      });
+    }
+    return { [value]: value };
+  })
+  .flat()
+  .reduce((obj, item) => {
+    return { ...obj, ...item };
+  }, {});
+
 // 选项类型
 export const optionTypeList = [
   { value: 'letter', label: '字母' },
@@ -68,6 +92,29 @@ export const computeOptionMethods = {
   [optionTypeList[2].value]: (i) => digitToChinese(i + 1),
 };
 
+/**
+ * 改变选项类型
+ * @param {object} data 数据
+ */
+export function changeOptionType(data) {
+  let index = optionTypeList.findIndex(({ value }) => value === data.option_number_show_mode);
+  data.option_number_show_mode = optionTypeList[index + 1]?.value || optionTypeList[0].value;
+}
+
+/**
+ * 计算选项题号
+ * @param {Number} i 序号
+ * @param {String} option_number_show_mode 选项类型
+ * @returns String 题号
+ */
+export function computedQuestionNumber(i, option_number_show_mode) {
+  const computationMethod = computeOptionMethods[option_number_show_mode];
+  if (computationMethod) {
+    return computationMethod(i);
+  }
+  return '';
+}
+
 // 题干类型
 export const stemTypeList = [
   { value: 'text', label: '纯文本' },

+ 30 - 10
src/views/exercise_questions/data/judge.js

@@ -1,18 +1,38 @@
-import { questionTypeOption, optionTypeList } from './common';
+import { optionTypeList, stemTypeList, questionNumberTypeList, scoreTypeList } from './common';
 import { getRandomNumber } from '@/utils/index';
 
+// 选项类型列表
+export const option_type_list = [
+  { value: 'right', label: '是' },
+  { value: 'error', label: '非' },
+  { value: 'incertitude', label: '不确定' },
+];
+
+export const option_type_value_list = option_type_list.map(({ value }) => value);
+
+export function getOption() {
+  return { content: '', mark: getRandomNumber() };
+}
+
 // 判断题数据模板
 export const judgeData = {
   type: 'judge', // 题型
   stem: '', // 题干
-  option_type: optionTypeList[0].value, // 选项类型
-  option_list: [
-    { content: '', mark: getRandomNumber() },
-    { content: '', mark: getRandomNumber() },
-    { content: '', mark: getRandomNumber() },
-  ], // 选项
+  option_number_show_mode: optionTypeList[0].value, // 选项类型
+  option_list: [getOption(), getOption(), getOption()], // 选项
   file_id_list: [], // 文件 id 列表
+  answer: { select_list: [], score: 0, score_type: 0 }, // 答案
+  // 题型属性
+  property: {
+    stem_type: stemTypeList[0].value, // 题干类型
+    question_number: 1, // 题号
+    option_type_list: [option_type_list[0].value, option_type_list[1].value], // 选项类型列表
+    is_enable_listening: true, // 是否听力
+    score: 1, // 分值
+    score_type: scoreTypeList[0].value, // 分值类型
+  },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
 };
-
-// 判断题类型
-export const selectType = [questionTypeOption[0].value, questionTypeOption[0].children[1].value];

+ 13 - 20
src/views/exercise_questions/data/select.js

@@ -1,38 +1,31 @@
-import {
-  optionTypeList,
-  stemTypeList,
-  selectTypeList,
-  scoreTypeList,
-  questionTypeOption,
-  questionNumberTypeList,
-} from './common';
+import { optionTypeList, stemTypeList, selectTypeList, scoreTypeList, questionNumberTypeList } from './common';
 import { getRandomNumber } from '@/utils/index';
 
 // 选择题数据模板
 export const selectData = {
   type: 'select', // 题型
   stem: '', // 题干
-  option_type: optionTypeList[0].value, // 选项类型
-  describe: '', // 描述
-  options: [
+  option_number_show_mode: optionTypeList[0].value, // 选项类型
+  description: '', // 描述
+  option_list: [
     { content: '', mark: getRandomNumber() },
     { content: '', mark: getRandomNumber() },
     { content: '', mark: getRandomNumber() },
   ], // 选项
   file_id_list: [], // 文件 id 列表
-  answer: { select_list: [], score: 0, item_score: 0 }, // 答案
-  // 题型设置
-  setting: {
+  answer: { select_list: [], score: 0, score_type: 0 }, // 答案
+  // 题型属性
+  property: {
     stem_type: stemTypeList[0].value, // 题干类型
     question_number: 1, // 题号
-    question_number_type: questionNumberTypeList[0].value, // 题号类型
-    is_describe: false, // 描述
+    is_enable_description: false, // 描述
     select_type: selectTypeList[0].value, // 选择类型
-    is_hear: true, // 是否听力
+    is_enable_listening: true, // 是否听力
     score: 1, // 分值
     score_type: scoreTypeList[0].value, // 分值类型
   },
+  // 其他属性
+  other: {
+    question_number_type: questionNumberTypeList[0].value, // 题号类型
+  },
 };
-
-// 选择题类型
-export const selectType = [questionTypeOption[0].value, questionTypeOption[0].children[0].value];

+ 132 - 0
src/views/exercise_questions/preview/JudgePreview.vue

@@ -0,0 +1,132 @@
+<template>
+  <div class="judge-preview">
+    <div class="stem">
+      <span class="question-number">{{ data.property.question_number }}.</span>
+      <span v-html="data.stem"></span>
+    </div>
+    <AudioPlay
+      v-if="data.property.is_enable_listening && data.file_id_list.length > 0"
+      :file-id="data.file_id_list[0]"
+    />
+
+    <ul class="option-list">
+      <li
+        v-for="{ content, mark } in data.option_list"
+        :key="mark"
+        :class="['option-item', { active: isAnswer(mark) }]"
+      >
+        <div class="option-content" v-html="content"></div>
+        <div class="option-type">
+          <div
+            v-for="option_type in data.property.option_type_list"
+            :key="option_type"
+            :class="[
+              'option-type-item',
+              {
+                active: isAnswer(mark, option_type),
+              },
+            ]"
+            @click="selectAnswer(mark, option_type)"
+          >
+            <SvgIcon v-if="option_type === option_type_list[0].value" icon-class="check-mark" width="17" height="12" />
+            <SvgIcon v-if="option_type === option_type_list[1].value" icon-class="cross" size="12" />
+            <SvgIcon v-if="option_type === option_type_list[2].value" icon-class="circle" size="20" />
+          </div>
+        </div>
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+import { option_type_list } from '@/views/exercise_questions/data/judge';
+
+import AudioPlay from '@/views/exercise_questions/create/components/common/AudioPlay.vue';
+
+export default {
+  name: 'JudgePreview',
+  components: {
+    AudioPlay,
+  },
+  props: {
+    data: {
+      type: Object,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      answer: [],
+      option_type_list,
+    };
+  },
+  methods: {
+    isAnswer(mark, option_type) {
+      return this.answer.some((li) => li.mark === mark && li.option_type === option_type);
+    },
+
+    selectAnswer(mark, option_type) {
+      const index = this.answer.findIndex((li) => li.mark === mark);
+      if (index === -1) {
+        this.answer.push({ mark, option_type });
+      } else {
+        this.answer[index].option_type = option_type;
+      }
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.judge-preview {
+  @include preview;
+
+  .option-list {
+    display: flex;
+    flex-direction: column;
+    row-gap: 16px;
+
+    .option-item {
+      display: flex;
+      column-gap: 16px;
+
+      .option-content {
+        flex: 1;
+        padding: 12px 24px;
+        color: #706f78;
+        background-color: #f9f8f9;
+        border-radius: 40px;
+      }
+
+      .option-type {
+        display: flex;
+        column-gap: 8px;
+        align-items: center;
+
+        &-item {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 48px;
+          height: 48px;
+          color: #000;
+          cursor: pointer;
+          background-color: #f9f8f9;
+          border-radius: 50%;
+
+          &.active {
+            color: #fff;
+            background-color: #504f57;
+          }
+        }
+      }
+
+      :deep p {
+        margin: 0;
+      }
+    }
+  }
+}
+</style>

+ 13 - 36
src/views/exercise_questions/preview/SelectPreview.vue

@@ -1,20 +1,23 @@
 <template>
   <div class="select-preview">
     <div class="stem">
-      <span class="question-number">{{ data.setting.question_number }}.</span>
+      <span class="question-number">{{ data.property.question_number }}.</span>
       <span v-html="data.stem"></span>
     </div>
-    <div v-if="data.setting.is_describe" class="describe">{{ data.describe }}</div>
-    <AudioPlay v-if="data.setting.is_hear && data.file_id_list.length > 0" :file-id="data.file_id_list[0]" />
+    <div v-if="data.property.is_enable_description" class="description">{{ data.description }}</div>
+    <AudioPlay
+      v-if="data.property.is_enable_listening && data.file_id_list.length > 0"
+      :file-id="data.file_id_list[0]"
+    />
     <ul class="option-list">
       <li
-        v-for="({ content, mark }, i) in data.options"
+        v-for="({ content, mark }, i) in data.option_list"
         :key="mark"
         :class="['option-item', { active: isAnswer(mark) }]"
         @click="selectAnswer(mark)"
       >
         <span class="selectionbox"></span>
-        <span>{{ computeOptionMethods[data.option_type](i) }}. </span>
+        <span>{{ computeOptionMethods[data.option_number_show_mode](i) }}. </span>
         <span v-html="content"></span>
       </li>
     </ul>
@@ -49,10 +52,10 @@ export default {
     },
     selectAnswer(mark) {
       const index = this.answer.indexOf(mark);
-      if (this.data.setting.select_type === selectTypeList[0].value) {
+      if (this.data.property.select_type === selectTypeList[0].value) {
         this.answer = [mark];
       }
-      if (this.data.setting.select_type === selectTypeList[1].value) {
+      if (this.data.property.select_type === selectTypeList[1].value) {
         if (index === -1) {
           this.answer.push(mark);
         } else {
@@ -65,36 +68,10 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.select-preview {
-  display: flex;
-  flex-direction: column;
-  row-gap: 24px;
-  padding: 64px;
-  margin: 16px 0;
-  background-color: #fff;
-  border-radius: 8px;
-
-  .stem {
-    display: flex;
-    align-items: center;
-    font-size: 18px;
-    font-weight: bold;
-    color: #34343a;
-
-    .question-number {
-      margin-right: 4px;
-    }
+@use '@/styles/mixin.scss' as *;
 
-    :deep p {
-      margin: 0;
-    }
-  }
-
-  .describe {
-    padding: 12px 24px;
-    background-color: #f9f8f9;
-    border-radius: 16px;
-  }
+.select-preview {
+  @include preview;
 
   .option-list {
     display: flex;

+ 40 - 0
src/views/home/personal_question/components/ExerciseLink.vue

@@ -0,0 +1,40 @@
+<!-- 练习题链接 -->
+<template>
+  <el-dialog :visible="visible" title="练习题链接" width="400px" @close="dialogClose">
+    <div class="exercise-link">sdfsf</div>
+
+    <div slot="footer" class="footer">
+      <el-button @click="dialogClose">取消</el-button>
+      <el-button v-show="linkType === 0" type="primary" @click="copy">复制</el-button>
+      <el-button v-show="linkType === 1" type="primary">下载二维码</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'ExerciseLink',
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    linkType: {
+      type: Number,
+      required: true,
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {
+    copy() {},
+
+    dialogClose() {
+      this.$emit('update:visible', false);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 30 - 4
src/views/home/personal_question/components/ShareDialog.vue

@@ -46,14 +46,24 @@
       <span>访问人数</span>
       <el-input v-model="visitors_number" placeholder="请输入访问人数" />
       <span>可直接输入人数</span>
-      <span>作答模式</span>
-      <el-radio-group v-model="answer_mode" disabled>
+      <span :class="{ disabled: access_permission === accessPermissions[0].type }">作答模式</span>
+      <el-radio-group
+        v-model="answer_mode"
+        :disabled="access_permission === accessPermissions[0].type"
+        :class="{ disabled: access_permission === accessPermissions[0].type }"
+      >
         <el-radio v-for="{ type, name } in answerModes" :key="type" :label="type">{{ name }}</el-radio>
       </el-radio-group>
     </div>
 
     <div slot="footer" class="footer">
-      <el-button type="primary" @click="dialogClose">发送</el-button>
+      <template v-if="send_mode === sendModes[0].type">
+        <el-button type="primary" @click="dialogClose">发送</el-button>
+      </template>
+      <template v-if="send_mode === sendModes[1].type">
+        <el-button @click="generateLink(1)">生成二维码</el-button>
+        <el-button type="primary" @click="generateLink(2)">生成链接</el-button>
+      </template>
     </div>
   </el-dialog>
 </template>
@@ -103,6 +113,9 @@ export default {
     };
   },
   methods: {
+    generateLink(type) {
+      this.$emit('generateLink', type);
+    },
     dialogClose() {
       this.$emit('update:dialogVisible');
     },
@@ -142,6 +155,10 @@ export default {
   column-gap: 16px;
   align-items: center;
   margin-top: 16px;
+
+  .disabled {
+    opacity: 0.5;
+  }
 }
 
 .footer {
@@ -149,10 +166,19 @@ export default {
   align-items: center;
   justify-content: center;
 
-  :deep .el-button {
+  .el-button {
     width: 200px;
     height: 34px;
     border-radius: 20px;
+
+    + .el-button {
+      margin-left: 16px;
+    }
+
+    &--default {
+      color: $main-color;
+      background-color: #f3f7ff;
+    }
   }
 }
 </style>

+ 25 - 7
src/views/home/personal_question/index.vue

@@ -57,7 +57,8 @@
     </HomeCommon>
 
     <CreateExercise :dialog-visible.sync="dialogVisible" />
-    <ShareDialog :dialog-visible.sync="visibleShare" />
+    <ShareDialog :dialog-visible.sync="visibleShare" @generateLink="generateLink" />
+    <ExerciseLink :visible.sync="visibleLink" :link-type="linkType" />
   </div>
 </template>
 
@@ -67,6 +68,7 @@ import { PageQueryExerciseList, DeleteExercise } from '@/api/exercise';
 import HomeCommon from '../common.vue';
 import CreateExercise from './components/CreateExercise.vue';
 import ShareDialog from './components/ShareDialog.vue';
+import ExerciseLink from './components/ExerciseLink.vue';
 
 export default {
   name: 'PersonalQuestion',
@@ -74,6 +76,7 @@ export default {
     HomeCommon,
     CreateExercise,
     ShareDialog,
+    ExerciseLink,
   },
   data() {
     return {
@@ -96,6 +99,8 @@ export default {
       total_count: 0, // 总条数
       dialogVisible: false,
       visibleShare: false, // 分享弹窗
+      visibleLink: false, // 练习题链接弹窗
+      linkType: 1, // 链接类型
     };
   },
   methods: {
@@ -109,14 +114,22 @@ export default {
       });
     },
     deleteExercise(exercise_id) {
-      DeleteExercise({ exercise_id })
+      this.$confirm('是否删除当前练习题', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
         .then(() => {
-          this.$message.success('删除成功');
-          this.getPageList();
+          DeleteExercise({ exercise_id })
+            .then(() => {
+              this.$message.success('删除成功');
+              this.getPageList();
+            })
+            .catch(() => {
+              this.$message.error('删除失败');
+            });
         })
-        .catch(() => {
-          this.$message.error('删除失败');
-        });
+        .catch(() => {});
     },
     share() {
       this.visibleShare = true;
@@ -124,6 +137,11 @@ export default {
     createExercise() {
       this.dialogVisible = true;
     },
+    generateLink(type) {
+      this.linkType = type;
+      this.visibleShare = false;
+      this.visibleLink = true;
+    },
   },
 };
 </script>