zq 2 ماه پیش
والد
کامیت
3855a4f59b

+ 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>

+ 179 - 0
src/views/book/courseware/create/components/question/judge/Judge.vue

@@ -0,0 +1,179 @@
+<template>
+  <ModuleBase :type="data.type">
+    <template #content>
+      <ul class="option-list">
+        <li v-for="(item, i) in data.option_list" :key="i" class="option-item">
+          <span class="serial-number">{{ convertNumberToLetter(i) }}.</span>
+          <div class="option-content">
+            <RichText v-model="item.content" placeholder="输入内容" :inline="true" />
+          </div>
+
+          <div
+            v-for="option_type in incertitudeList"
+            :key="option_type"
+            :class="[
+              'option-type-item',
+              {
+                active: data.answer.answer_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>
+          <span class="delete" @click="deleteOption">
+            <SvgIcon icon-class="delete-2" width="12" height="12" />
+          </span>
+        </li>
+      </ul>
+      <div class="add">
+        <SvgIcon icon-class="add-circle" width="14" height="14" />
+        <span class="add-button" @click="addOption">增加选项</span>
+      </div>
+    </template>
+  </ModuleBase>
+</template>
+
+<script>
+import ModuleMixin from '../../common/ModuleMixin';
+import { getJudgeData, getOption, option_type_list, isEnable } from '@/views/book/courseware/data/judge';
+
+export default {
+  name: 'JudgePage',
+  components: {},
+  mixins: [ModuleMixin],
+  data() {
+    return {
+      data: getJudgeData(),
+      option_type_list,
+      isEnable,
+    };
+  },
+  computed: {
+    incertitudeList() {
+      let _option_type_list = this.data.property.option_type_list;
+      if (isEnable(this.data.property.is_view_incertitude)) {
+        return _option_type_list;
+      }
+      // 返回不包含第三个元素的新数组
+      return [..._option_type_list.slice(0, 2), ..._option_type_list.slice(3)];
+    },
+  },
+  methods: {
+    // 将数字转换为小写字母
+    convertNumberToLetter(number) {
+      return String.fromCharCode(97 + number);
+    },
+    /**
+     * 选择选项答案
+     * @param {String} option_type 选项类型
+     * @param {String} mark 选项标记
+     */
+    selectOptionAnswer(option_type, mark) {
+      const index = this.data.answer.answer_list.findIndex((item) => item.mark === mark);
+      if (index === -1) {
+        this.data.answer.answer_list.push({ option_type, mark });
+      } else {
+        this.data.answer.answer_list[index].option_type = option_type;
+      }
+    },
+    /**
+     * @description 添加选项
+     */
+    addOption() {
+      this.data.option_list.push(getOption());
+    },
+    /**
+     * @description 删除选项
+     * @param {number} index 选项索引
+     */
+    deleteOption(index) {
+      this.data.option_list.splice(index, 1);
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+.option-list {
+  display: flex;
+  flex-direction: column;
+  row-gap: 8px;
+
+  .option-item {
+    display: flex;
+    column-gap: 8px;
+    align-items: center;
+    min-height: 36px;
+
+    .serial-number {
+      width: 40px;
+      height: 36px;
+      padding: 4px 8px;
+      color: $text-color;
+      background-color: $fill-color;
+      border-radius: 2px;
+    }
+
+    .option-content {
+      display: flex;
+      flex: 1;
+      column-gap: 6px;
+      align-items: center;
+      padding: 0 12px;
+      overflow: hidden;
+      background-color: $fill-color;
+    }
+
+    .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: $font-light-color;
+        cursor: pointer;
+        background-color: #d7d7d7;
+        border-radius: 2px;
+
+        &.active {
+          color: #fff;
+          background-color: $main-color;
+        }
+      }
+    }
+
+    .delete {
+      margin-left: 8px;
+      cursor: pointer;
+    }
+  }
+}
+
+.add {
+  display: flex;
+  column-gap: 6px;
+  align-items: center;
+  justify-content: center;
+  margin-top: 12px;
+
+  .svg-icon {
+    cursor: pointer;
+  }
+
+  .add-button {
+    font-size: 14px;
+    color: $main-color;
+    cursor: pointer;
+  }
+}
+</style>

+ 40 - 0
src/views/book/courseware/create/components/question/judge/JudgeSetting.vue

@@ -0,0 +1,40 @@
+<template>
+  <div>
+    <el-form :model="property" label-width="72px" label-position="left">
+      <SerailNumber :property="property" />
+      <el-form-item label="不确定">
+        <el-radio-group v-model="property.is_view_incertitude">
+          <el-radio v-for="{ value, label } in switchOption" :key="value" :label="value" :value="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
+import { getJudgeProperty, switchOption } from '@/views/book/courseware/data/judge';
+
+export default {
+  name: 'JudgeSetting',
+  mixins: [SettingMixin],
+  data() {
+    return {
+      property: getJudgeProperty(),
+      switchOption,
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.el-form {
+  @include setting-base;
+}
+</style>

+ 12 - 0
src/views/book/courseware/data/bookType.js

@@ -49,6 +49,8 @@ import Article from '../create/components/question/article/Article.vue';
 import ArticleSetting from '../create/components/question/article/ArticleSetting.vue';
 import Math from '../create/components/question/math/Math.vue';
 import MathSetting from '../create/components/question/math/MathSetting.vue';
+import Judge from '../create/components/question/judge/Judge.vue';
+import JudgeSetting from '../create/components/question/judge/JudgeSetting.vue';
 
 // 预览组件页面列表
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
@@ -77,6 +79,8 @@ import OtherWordPreview from '../preview/components/other_word/OtherWordPreview.
 import ArticlePreview from '../preview/components/article/index.vue';
 import MathPreview from '../preview/components/math/MathPreview.vue';
 
+import JudgePreview from '../preview/components/judge/JudgePreview.vue';
+
 export const bookTypeOption = [
   {
     value: 'base',
@@ -289,6 +293,14 @@ export const bookTypeOption = [
         set: MathSetting,
         preview: MathPreview,
       },
+      {
+        value: 'judge',
+        label: '判断组件',
+        icon: '',
+        component: Judge,
+        set: JudgeSetting,
+        preview: JudgePreview,
+      },
     ],
   },
 ];

+ 47 - 0
src/views/book/courseware/data/judge.js

@@ -0,0 +1,47 @@
+import {
+  displayList,
+  serialNumberTypeList,
+  serialNumberPositionList,
+  switchOption,
+  isEnable,
+} from '@/views/book/courseware/data/common';
+import { getRandomNumber } from '@/utils';
+
+export { switchOption, isEnable };
+
+// 选项类型列表
+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 getJudgeProperty() {
+  return {
+    serial_number: 1,
+    sn_type: serialNumberTypeList[0].value,
+    sn_position: serialNumberPositionList[0].value,
+    sn_display_mode: displayList[0].value,
+    option_type_list: [option_type_list[0].value, option_type_list[1].value, option_type_list[2].value],
+    is_view_incertitude: switchOption[0].value,
+  };
+}
+
+export function getOption() {
+  return {
+    content: '',
+    mark: getRandomNumber(),
+  };
+}
+
+export function getJudgeData() {
+  return {
+    type: 'judge',
+    title: '判断',
+    property: getJudgeProperty(),
+    option_list: [getOption(), getOption(), getOption(), getOption()],
+    answer: {
+      answer_list: [],
+    },
+  };
+}

+ 182 - 0
src/views/book/courseware/preview/components/judge/JudgePreview.vue

@@ -0,0 +1,182 @@
+<template>
+  <div class="judge-preview" :style="getAreaStyle()">
+    <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
+    <div class="main">
+      <ul class="option-list">
+        <li
+          v-for="({ content, mark }, i) in data.option_list"
+          :key="mark"
+          :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
+          :class="['option-item', { active: isAnswer(mark) }]"
+        >
+          <div :class="['option-content', computedIsJudgeRight(mark)]">
+            <span class="serial-number">{{ convertNumberToLetter(i) }}.</span>
+            <div class="rich-text" v-html="sanitizeHTML(content)"></div>
+          </div>
+          <div class="option-type">
+            <div
+              v-for="option_type in incertitudeList"
+              :key="option_type"
+              :style="{ cursor: disabled ? 'not-allowed' : 'pointer' }"
+              :class="[
+                'option-type-item',
+                {
+                  active: isAnswer(mark, option_type),
+                },
+                computedIsShowRightAnswer(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="16" />
+            </div>
+          </div>
+        </li>
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+import { getJudgeData, option_type_list, isEnable } from '@/views/book/courseware/data/judge';
+import PreviewMixin from '../common/PreviewMixin';
+
+export default {
+  name: 'JudgePreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      data: getJudgeData(),
+      option_type_list,
+      isEnable,
+    };
+  },
+  computed: {
+    incertitudeList() {
+      let _option_type_list = this.data.property.option_type_list;
+      if (isEnable(this.data.property.is_view_incertitude)) {
+        return _option_type_list;
+      }
+      // 返回不包含第三个元素的新数组
+      return [..._option_type_list.slice(0, 2), ..._option_type_list.slice(3)];
+    },
+  },
+  methods: {
+    // 将数字转换为小写字母
+    convertNumberToLetter(number) {
+      return String.fromCharCode(97 + number);
+    },
+
+    isAnswer(mark, option_type) {
+      return this.answer.answer_list.some((li) => li.mark === mark && li.option_type === option_type);
+    },
+
+    //选择答案
+    selectAnswer(mark, option_type) {
+      if (this.disabled) return;
+      const index = this.answer.answer_list.findIndex((li) => li.mark === mark);
+      if (index === -1) {
+        this.answer.answer_list.push({ mark, option_type });
+      } else {
+        this.answer.answer_list[index].option_type = option_type;
+      }
+    },
+    // 计算判断题小题题目样式
+    computedIsJudgeRight(mark) {
+      if (!this.isJudgingRightWrong) return '';
+      let selectOption = this.answer.answer_list.find((item) => item.mark === mark); // 查找是否已选中的选项
+      if (!selectOption) return 'wrong';
+      return this.data.answer.answer_list.find((item) => item.mark === mark).option_type === selectOption.option_type
+        ? 'right'
+        : 'wrong';
+    },
+    // 计算是否显示正确答案的样式
+    computedIsShowRightAnswer(mark, option_type) {
+      if (!this.isShowRightAnswer) return '';
+      let selectOption = this.answer.answer_list.find((item) => item.mark === mark); // 查找是否已选中的选项
+      // 是否是正确的选项类型
+      let isCorrectType = this.data.answer.answer_list.find((item) => item.mark === mark)?.option_type === option_type;
+      if (!selectOption) {
+        return isCorrectType ? 'answer-right' : '';
+      }
+      return isCorrectType && !(selectOption.option_type === option_type) ? 'answer-right' : '';
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.judge-preview {
+  @include preview-base;
+
+  .option-list {
+    display: flex;
+    flex-direction: column;
+    row-gap: 16px;
+
+    .option-item {
+      display: flex;
+      column-gap: 16px;
+
+      .option-content {
+        display: flex;
+        flex: 1;
+        column-gap: 8px;
+        align-items: center;
+        padding: 12px 24px;
+        background-color: $content-color;
+        border-radius: 40px;
+
+        &.right {
+          background-color: $right-bc-color;
+        }
+
+        &.wrong {
+          box-shadow: 0 0 0 1px $error-color;
+        }
+
+        .serial-number {
+          font-size: 16pt;
+          color: #000;
+        }
+      }
+
+      .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: $content-color;
+          border-radius: 50%;
+
+          &.active {
+            color: #fff;
+            background-color: $light-main-color;
+          }
+
+          &.answer-right {
+            color: $right-color;
+            border: 1px solid $right-color;
+          }
+        }
+      }
+    }
+  }
+}
+</style>