vue3答题(单选、多选)页面+功能实现

302 阅读13分钟

step1 - 基本答题选择框结构

image.png

<template>
  <div class="chooseBox">
    <div class="row">
      <!-- 每一个选项由三部分组成 -->
      <div class="label">A:</div>
      <div class="text">选项一</div>
      <div class="icon">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path
            d="M7 7L17 17"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
          <path
            d="M7 17L17 7"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path
            d="M5 12L10 17L20 7"
            stroke="#09511C"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </div>
    </div>
    <div class="row">
      <div class="label">B:</div>
      <div class="text">选项二</div>
      <div class="icon">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path
            d="M7 7L17 17"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
          <path
            d="M7 17L17 7"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path
            d="M5 12L10 17L20 7"
            stroke="#09511C"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.chooseBox {
  .row {
    margin-bottom: 10px;
    user-select: none;
    cursor: pointer;
    border-radius: 34px;
    background: #f2f1f1;
    padding: 6px 25px;
    display: flex;
    align-items: center;
    letter-spacing: 2px;
    overflow: hidden;
    .label {
      height: 100%;
      display: flex;
      align-items: center;
    }
    .text {
      flex: 1;
    }
    .icon {
      height: 24px;
      display: flex;
      align-items: center;
      margin-left: 20px;
    }
  }
}
</style>

step2 - 优化html里的数据结构

image.png

<template>
  <div class="chooseBox">
    <div class="row" v-for="(item, index) in chooses"> //遍历数据
      <div class="label">{{ String.fromCharCode(index + 65) }}:</div> //巧用
      <div class="text">{{ item }}</div>
      <div class="icon">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path
            d="M7 7L17 17"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
          <path
            d="M7 17L17 7"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
        >
          <path
            d="M5 12L10 17L20 7"
            stroke="#09511C"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref } from "vue";
const chooses = ref(["选项一", "选项二", "选项三", "选项四"]); //数据
</script>

step3 - 封装为chooseBox组件

子组件在上基础更改part

<script setup>
    import { ref } from "vue";
    const props = defineProps({
      chooses: {
        type: Array,
        default: [],
      },
    });
</script>

父组件

<template>
  <div class="chooses">
    <myChooseBox :chooses="chooses"></myChooseBox>
  </div>
</template>
<script setup>
    import { ref } from "vue";
    import myChooseBox from "../components/myChooseBox.vue";
    const chooses = ref(["选项一", "选项二", "选项三", "选项四"]);
</script>

step4 - 答题页面基本实现

image.png

按钮组件文章:vue3封装按钮子组件 - 掘金 (juejin.cn)]

父组件

<template>
  <div class="topic">
    <div class="TIndex">第{{ curBigIndex }}关</div>
    <div class="main">
      <div class="top">
        <div class="type">{{ curType == 0 ? "单选" : "多选" }}</div>
        <div class="num">
          <span class="current">{{ currentIndex + 1 }}</span
          >/{{ currentAll }}
        </div>
      </div>
      <div class="title">{{ currentTitle }}</div>
      <div class="chooses">
        <myChooseBox :chooses="chooses" ref="choose"></myChooseBox>
      </div>
      <div class="btn">
        <myButton :isActive="true">提交</myButton>
      </div>
    </div>
  </div>
</template>
    <script setup>
import myButton from "../components/myButton.vue";
import { ref } from "vue";
import myChooseBox from "../components/myChooseBox.vue";
const curBigIndex = ref("一"); //当前关卡数
const curType = ref(0); // 当前答题类型 0-单选、1-多选
const currentIndex = ref(0); //当前答题数
const currentAll = ref("2"); // 当前答题总数
const currentTitle = ref("第一题");
const chooses = ref(["选项一", "选项二", "选项三", "选项四"]);
</script>
<style lang="scss">
.topic {
  height: 100%;
  width: 100%;
  .TIndex {
    font-size: 24px;
  }
  .main {
    border-radius: 20px;
    background: #fff;
    padding: 16px;
    .top {
      display: flex;
      align-items: center;
      .type {
        width: 34px;
        height: 20px;
        line-height: 20px;
        border-radius: 2px;
        text-align: center;
        margin-right: 20px;
      }
      .num {
        color: #979faf;
        text-align: center;
        font-size: 24px;
        .current {
          color: #1050d4;
          margin-right: 2px;
        }
      }
    }
  }
  .title {
    margin-top: 24px;
    margin-bottom: 20px;
    color: #141925;
    font-size: 18px;
    font-weight: 500;
    text-align: center;
  }
  .btn {
    margin-top: 30px;
  }
}
</style>

step5 - 优化答题页面里的数据结构

<template>
  <div class="topic">
    <div class="TIndex">第{{ curData.bigIndex }}关</div>
    <div class="main">
      <div class="top">
        <div class="type">{{ currentData.type == 0 ? "单选" : "多选" }}</div>
        <div class="num">
          <span class="current">{{ currentIndex + 1 }}</span
          >/{{ curData.topics.length }}
        </div>
      </div>
      <div class="title">{{ currentData.title }}</div>
      <div class="chooses">
        <myChooseBox :chooses="currentData.chooses" ref="choose"></myChooseBox>
      </div>
      <div class="btn">
        <myButton :isActive="true">提交</myButton>
      </div>
    </div>
  </div>
</template>
    <script setup>
import myButton from "../components/myButton.vue";
import { ref, onMounted } from "vue";
import myChooseBox from "../components/myChooseBox.vue";
const data = ref([
  {
    bigIndex: "一",
    topics: [
      {
        type: 0,
        title: "第一题",
        chooses: ["选项一", "选项二", "选项三", "选项四"],
        right: [2],
        analysis: "解析1",
      },
      {
        type: 1,
        title: "第二题",
        chooses: ["选项一", "选项二", "选项三", "选项四"],
        right: [2, 3],
        analysis: "解析2",
      },
    ],
  },
  {
    bigIndex: "二",
    topics: [
      {
        type: 1,
        title: "第一题",
        chooses: ["选项一", "选项二", "选项三", "选项四"],
        right: [1, 2],
        analysis: "解析1",
      },
      {
        type: 0,
        title: "第二题",
        chooses: ["选项一", "选项二", "选项三", "选项四"],
        right: [3],
        analysis: "解析2",
      },
    ],
  },
]);
const curIndex = ref(0); //当前关卡数
const currentIndex = ref(0); //当前答题数
const curData = ref(data.value[curIndex.value]); //当前关卡数据
const currentData = ref(curData.value.topics[currentIndex.value]); //当前题数据
</script>

step6 - 为选项添加点击选中样式

1717551449672.gif

子组件

<template> // 先将图标相关代码注释
  <div class="chooseBox">
    <div
      class="row"
      v-for="(item, index) in chooses"
      @click="select(index)" 
      :class="computedClass(index)"
    >
      <div class="label">{{ String.fromCharCode(index + 65) }}:</div>
      <div class="text">{{ item }}</div>
     </div>
  </div>
</template>
<script setup>
    import { ref } from "vue";
    const selected = ref([]); // 被选择的数组
    const props = defineProps({
      chooses: {
        type: Array,
        default: [],
      },
      curType: { // 新增传参
        type: Number,
        default: 0, //0单选,1多选
      },
    });
    defineExpose({
      // 暴露出去的子方法,用于重置选中样式
      replay() {
        selected.value = [];
      },
    });
    function select(index) { //
      if (props.curType == 0) {
        // 单选
        //不能直接设置index 因为首先会是空
        selected.value.length >= 1
          ? (selected.value[0] = index)
          : selected.value.push(index);
      } else if (props.curType == 1) {
        if (selected.value.indexOf(index) > -1) {
          //已经存在了,点击又加入了,所以要去掉
          selected.value = selected.value.filter((item) => {
            return item != index;
          });
        } else {
          selected.value.push(index); //不存在则直接push
        }
      }
    }
    function computedClass(index) { //
      let list = [];
      if (selected.value.indexOf(index) > -1) { 
      // 为selected中有的选项加上样式
        list.push("selected");
      }
      return list.join(" ");
    }
</script> 
<style lang="scss" scoped>
    .chooseBox {
      .row {
        margin-bottom: 10px;
        user-select: none;
        cursor: pointer;
        border-radius: 34px;
        background: #f2f1f1;
        padding: 6px 25px;
        display: flex;
        align-items: center;
        letter-spacing: 2px;
        overflow: hidden;
        .label {
          height: 100%;
          display: flex;
          align-items: center;
        }
        .text {
          flex: 1;
        }
        .icon {
          height: 24px;
          display: flex;
          align-items: center;
          margin-left: 20px;
        }
        &.selected { //选中样式
          background: rgba(19, 82, 214, 0.2);
          color: #1352d6;
        }
      }
}

父组件

<template> //只展示有修改部分
    <div class="chooses">
        <myChooseBox
          :chooses="currentData.chooses"
          :curType="currentData.type" //新增传参
          ref="choose" // 
        ></myChooseBox>
    </div>
    <div class="btn">
       <myButton :isActive="true" @click="next">下一题</myButton>
    </div>
</template>
<script setup> //只展示有修改部分
    const choose = ref(null); //组件
    function next() {
      //如果没到达最后一题
      if (currentIndex.value < curData.value.topics.length - 1) {
        currentIndex.value++;
        currentData.value = curData.value.topics[currentIndex.value];
        choose.value.replay(); //调用子组件传递的方法
      } else {
        console.log(2);
      }
    }
</script>

step7 - 判断正误 + 按钮文本变化

1717553579140.gif

子组件

<template> //只展示有修改部分
   <div class="icon">
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      v-if="wrong.indexOf(index) > -1" //在错误数组中-显示错误图标
    >
      <path
        d="M7 7L17 17"
        stroke="#A10A10"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      />
      <path
        d="M7 17L17 7"
        stroke="#A10A10"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      />
    </svg>
    <svg
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      v-if="right.indexOf(index) > -1" //在正确数组中-显示正确图标
    >
      <path
        d="M5 12L10 17L20 7"
        stroke="#09511C"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      />
    </svg>
  </div>
</template>
<script setup> //只展示有修改部分
    const wrong = ref([]); // 新增数组
    const right = ref([]); // 新增数组
    const props = defineProps({
      chooses: {
        type: Array,
        default: [],
      },
      curType: {
        type: Number,
        default: 0,
      },
      curRights: {
        // 新增传参
        type: Array,
        default: [],
      },
    });
    defineExpose({
        check() {
            //核对方法 - 添加样式+返回true or false
            if (selected.value.length == 0) {
              return false;
            }
            selected.value.forEach((item, index) => {
              if (props.curRights.indexOf(item) > -1) {
                //如果在正确选项里
                right.value.push(item);
              } else {
                wrong.value.push(item);
              }
            });
            if (
              //如果多选数量不匹配
              props.topicType == 1 &&
              selected.value.length < props.curRights.length
            ) {
              return false;
            }
            if (wrong.value.length == 0) {
              return true;
            } else {
              return false;
            }
        },
        replay() {
            selected.value = [];
            right.value = []; //
            wrong.value = []; //
        },
    });
    function computedClass(index) {
        let list = [];
        if (selected.value.indexOf(index) > -1) {
        list.push("selected");
        }
        if (wrong.value.indexOf(index) > -1) { // 正确样式
        //
        list.push("wrong");
        }
        if (right.value.indexOf(index) > -1) { // 错误样式
        //
        list.push("right");
        }
        return list.join(" ");
    }
});
</script>
<style lang="scss" scoped>
.chooseBox {
    .row {
        &.wrong {
          background: rgba(255, 76, 76, 0.2);
          color: #a10a10;
        }
        &.right {
          background: rgba(50, 169, 83, 0.2);
          color: #09521c;
        }
    }
}
</style>

父组件

<template> //只展示有修改部分
    <div class="chooses">
        <myChooseBox
          :chooses="currentData.chooses"
          :curType="currentData.type"
          :curRights="currentData.right"
          ref="choose"
        ></myChooseBox>
        </div>
    <div class="btn">
        <BigButton @click="submit" :active="true" v-show="isAnsRight == -1"
          >提交</BigButton
        >
        <BigButton @click="replay" :active="true" v-show="isAnsRight == 0"
          >再答一次</BigButton
        >
        <myButton :isActive="true" @click="next" v-show="isAnsRight == 1"
          >下一题</myButton
        >
    </div>
</template>
<script setup>
    const isAnsRight = ref(-1); //添加判断正误标志-对应按钮变化
    function next() {
      //如果没到达最后一题
      if (currentIndex.value < curData.value.topics.length - 1) {
        currentIndex.value++;
        currentData.value = curData.value.topics[currentIndex.value];
        isAnsRight.value = -1; //改变按钮
        choose.value.replay(); //重置
      } else {
        console.log(2);
      }
    }
    function submit() { //做完一道题进行批改
      if (choose.value.check()) { //调用子组件方法,如果全部正确
        isAnsRight.value = 1;
      } else {
        isAnsRight.value = 0;
      }
    }
    function replay() { //重置
      isAnsRight.value = -1;
      choose.value.replay();
    }
</script>

step8 - 答对查看解析 (解析的显示与隐藏)

1717595410797.gif

父组件

<template>
    <div class="analysis">
        <div class="answer">
          <div>答案:{{ rightAnswer }}</div>
          <div class="more" @click="more">
            查看解析
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="15"
              height="9"
              viewBox="0 0 15 9"
              fill="none"
              v-if="analysisOnShow"
            >
              <path
                fill-rule="evenodd"
                clip-rule="evenodd"
                d="M0.292893 6.47356C-0.0976311 6.86408 -0.0976311 7.49725 0.292893 7.88777C0.683417 8.2783 1.31658 8.27829 1.70711 7.88777L7.18213 2.41275L12.6569 7.88747C13.0474 8.278 13.6805 8.278 14.0711 7.88747C14.4616 7.49695 14.4616 6.86379 14.0711 6.47326L7.89754 0.29973C7.85652 0.258715 7.81283 0.222007 7.76702 0.189607C7.37605 -0.0940027 6.82606 -0.0596046 6.47365 0.292801L0.292893 6.47356Z"
                fill="#1352D6"
                fill-opacity="0.4"
              />
            </svg>
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="15"
              height="9"
              viewBox="0 0 15 9"
              fill="none"
              v-else
            >
              <path
                fill-rule="evenodd"
                clip-rule="evenodd"
                d="M0.292889 1.70711C-0.0976295 1.3166 -0.0976295 0.683426 0.292889 0.292923C0.683407 -0.097641 1.31657 -0.097641 1.7071 0.292923L7.18214 5.76796L12.6568 0.293228C13.0474 -0.0973358 13.6805 -0.0973358 14.0711 0.293228C14.4616 0.683731 14.4616 1.31691 14.0711 1.70741L7.89753 7.88094C7.85675 7.92171 7.81332 7.95821 7.7678 7.99049C7.37678 8.27473 6.82627 8.24049 6.47364 7.88789L0.292889 1.70711Z"
                fill="#1352D6"
                fill-opacity="0.4"
              />
            </svg>
          </div>
        </div>
        <div class="text" v-if="analysisOnShow" ref="analysis"> /这里有v-if
          <div>解析</div>
          <div v-html="currentData.analysis"></div>
        </div>
    </div>
</template>
<script setup>
    const analysis = ref(null);
    const analysisOnShow = ref(false);
    const rightAnswer = computed(() => {
      return currentData.value.right
        .map((item) => {
          return String.fromCharCode(item + 65);
        })
        .join("、");
    });
    function more() {
      if (!analysisOnShow.value) {
        analysisOnShow.value = true;
      } else {
        analysisOnShow.value = false;
      }
    }
</script>
<style lang="scss">
.analysis {
  margin-top: 16px;
  overflow: hidden;
  .answer {
    color: #263961;
    display: flex;
    justify-content: space-between;
    .more {
      cursor: pointer;
      user-select: none;
    }
  }
  .text {
    width: 100%;
    color: #5473b2;
    white-space: wrap;
    font-size: 16px;
    letter-spacing: 2px;
  }
}
</style>

step9 - 优化解析过渡动画

1717596141438.gif

<template> // 只展示修改部分 不要v-if
    <div class="text" ref="analysis">
      <div>解析</div>
      <div v-html="currentData.analysis"></div>
    </div>
</template>
 <script setup>
    function showAnalysis() {
      analysisOnShow.value = true;
      let height =
        analysis.value.children[0].clientHeight +
        analysis.value.children[1].clientHeight;
      analysis.value.animate(
        [
          {
            marginTop: "12px",
            height: "0px",
          },
          {
            marginTop: "12px",
            height: height + "px",
          },
        ],
        {
          duration: 100,
          fill: "forwards",
        }
      );
    }
    function hideAnalysis() {
      analysisOnShow.value = false;
      let height =
        analysis.value.children[0].clientHeight +
        analysis.value.children[1].clientHeight;
      analysis.value.animate(
        [
          {
            marginTop: "12px",
            height: height + "px",
          },
          {
            marginTop: "0px",
            height: 0 + "px",
          },
        ],
        {
          duration: 100,
          fill: "forwards",
        }
      );
    }
    function more() {
      if (analysis.value.clientHeight == 0) {
        showAnalysis();
      } else {
        hideAnalysis();
      }
    } 
 </script>
 <style lang="scss">
     .text {
        width: 100%;
        color: #5473b2;
        white-space: wrap;
        font-size: 16px;
        letter-spacing: 2px;
        height: 0; //初始时height设置为0 由height来决定显示与隐藏
      }
 </style>

step10 - 完善解析part显示与否逻辑

1717596830172.gif

<template> //只展示修改部分
 <div class="analysis" v-if="isAnsRight == 1"> // 只需要添加v-if判断
        <div class="answer">
          <div>答案:{{ rightAnswer }}</div>
          <div class="more" @click="more">
            查看解析
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="15"
              height="9"
              viewBox="0 0 15 9"
              fill="none"
              v-if="analysisOnShow"
            >
              <path
                fill-rule="evenodd"
                clip-rule="evenodd"
                d="M0.292893 6.47356C-0.0976311 6.86408 -0.0976311 7.49725 0.292893 7.88777C0.683417 8.2783 1.31658 8.27829 1.70711 7.88777L7.18213 2.41275L12.6569 7.88747C13.0474 8.278 13.6805 8.278 14.0711 7.88747C14.4616 7.49695 14.4616 6.86379 14.0711 6.47326L7.89754 0.29973C7.85652 0.258715 7.81283 0.222007 7.76702 0.189607C7.37605 -0.0940027 6.82606 -0.0596046 6.47365 0.292801L0.292893 6.47356Z"
                fill="#1352D6"
                fill-opacity="0.4"
              />
            </svg>
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="15"
              height="9"
              viewBox="0 0 15 9"
              fill="none"
              v-else
            >
              <path
                fill-rule="evenodd"
                clip-rule="evenodd"
                d="M0.292889 1.70711C-0.0976295 1.3166 -0.0976295 0.683426 0.292889 0.292923C0.683407 -0.097641 1.31657 -0.097641 1.7071 0.292923L7.18214 5.76796L12.6568 0.293228C13.0474 -0.0973358 13.6805 -0.0973358 14.0711 0.293228C14.4616 0.683731 14.4616 1.31691 14.0711 1.70741L7.89753 7.88094C7.85675 7.92171 7.81332 7.95821 7.7678 7.99049C7.37678 8.27473 6.82627 8.24049 6.47364 7.88789L0.292889 1.70711Z"
                fill="#1352D6"
                fill-opacity="0.4"
              />
            </svg>
          </div>
        </div>
        <div class="text" ref="analysis">
          <div>解析</div>
          <div v-html="currentData.analysis"></div>
        </div>
      </div>
</template>

step11 - 解决bug-提交答案后选项应禁选

image.png

1717655027875.gif

新建状态管理store

step1 - 新建文件夹

image.png

step2 - 在main.js中引入

import store from './store';

step3 - store/index.js中写入

import { createStore } from 'vuex';
const store = createStore({
	state() {
		return {
			is_op : 1, //默认选项是可以点击的
		};
	},
	mutations: {
		setIsOp(state,binding){
			state.is_op = binding;
		}
	},
	actions: {},
	getters: {},
	modules: {},
});
export default store;

子组件

<script setup> //只展示修改部分
    import { useStore } from "vuex";
    const store = useStore();
    function computedClass(index) {
      let list = [];
      if (selected.value.indexOf(index) > -1) {
        list.push("selected");
      }
      if (wrong.value.indexOf(index) > -1) {
        list.push("wrong");
      }
      if (right.value.indexOf(index) > -1) {
        list.push("right");
      }
      if (!store.state.is_op) list.push("not_op"); //新增样式的判断
      return list.join(" "); 
    }
</script>
<style lang="scss" scoped> //只展示修改部分
.chooseBox {
    .row {
        &.not_op {
          pointer-events: none;
        }
    }
}
</style>

父组件

<script setup> //只展示修改部分
    import { useStore } from "vuex";
    const store = useStore();
    function next() {
      store.commit("setIsOp", 1); // 状态变化 
      if (currentIndex.value < curData.value.topics.length - 1) {
        currentIndex.value++;
        currentData.value = curData.value.topics[currentIndex.value];
        isAnsRight.value = -1; 
        choose.value.replay();
      } else {
        console.log(2);
      }
    }
    function submit() {
      //做完一道题进行批改
      if (choose.value.check()) {
        isAnsRight.value = 1;
      } else {
        isAnsRight.value = 0;
      }
      store.commit("setIsOp", 0); // 状态变化
    }
    function replay() {
      store.commit("setIsOp", 1); // 状态变化
      isAnsRight.value = -1;
      choose.value.replay();
    }
</script>

step12 - 成功!完整代码!快来试试吧!

子组件 - myChooseBox

<template>
  <div class="chooseBox">
    <div
      class="row"
      v-for="(item, index) in chooses"
      @click="select(index)"
      :class="computedClass(index)"
    >
      <div class="label">{{ String.fromCharCode(index + 65) }}:</div>
      <div class="text">{{ item }}</div>
      <div class="icon">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          v-if="wrong.indexOf(index) > -1"
        >
          <path
            d="M7 7L17 17"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
          <path
            d="M7 17L17 7"
            stroke="#A10A10"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="24"
          height="24"
          viewBox="0 0 24 24"
          fill="none"
          v-if="right.indexOf(index) > -1"
        >
          <path
            d="M5 12L10 17L20 7"
            stroke="#09511C"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          />
        </svg>
      </div>
    </div>
  </div>
</template>
<script setup>
    import { ref } from "vue";
    import { useStore } from "vuex";
    const store = useStore();

    const selected = ref([]);
    const wrong = ref([]);
    const right = ref([]);
    const props = defineProps({
      chooses: {
        type: Array,
        default: [],
      },
      curType: {
        type: Number,
        default: 0,
      },
      curRights: {
        type: Array,
        default: [],
      },
    });
    defineExpose({
      check() {
        //核对方法 - 添加样式+返回true or false
        if (selected.value.length == 0) {
          return false;
        }
        selected.value.forEach((item, index) => {
          if (props.curRights.indexOf(item) > -1) {
            //如果在正确选项里
            right.value.push(item);
          } else {
            wrong.value.push(item);
          }
        });
        if (
          //如果多选数量不匹配
          props.topicType == 1 &&
          selected.value.length < props.curRights.length
        ) {
          return false;
        }
        if (wrong.value.length == 0) {
          return true;
        } else {
          return false;
        }
      },
      replay() {
        selected.value = [];
        right.value = [];
        wrong.value = [];
      },
    });
    function select(index) {
      if (props.curType == 0) {
        selected.value.length >= 1
          ? (selected.value[0] = index)
          : selected.value.push(index);
      } else if (props.curType == 1) {
        if (selected.value.indexOf(index) > -1) {
          selected.value = selected.value.filter((item) => {
            return item != index;
          });
        } else {
          selected.value.push(index);
        }
      }
    }
    function computedClass(index) {
      let list = [];
      if (selected.value.indexOf(index) > -1) {
        list.push("selected");
      }
      if (wrong.value.indexOf(index) > -1) {
        list.push("wrong");
      }
      if (right.value.indexOf(index) > -1) {
        list.push("right");
      }
      if (!store.state.is_op) list.push("not_op");
      return list.join(" ");
    }
</script>
<style lang="scss" scoped>
    .chooseBox {
      .row {
        margin-bottom: 10px;
        user-select: none;
        cursor: pointer;
        border-radius: 34px;
        background: #f2f1f1;
        padding: 6px 25px;
        display: flex;
        align-items: center;
        letter-spacing: 2px;
        overflow: hidden;
        .label {
          height: 100%;
          display: flex;
          align-items: center;
        }
        .text {
          flex: 1;
        }
        .icon {
          height: 24px;
          display: flex;
          align-items: center;
          margin-left: 20px;
        }
        &.selected {
          background: rgba(19, 82, 214, 0.2);
          color: #1352d6;
        }
        &.wrong {
          background: rgba(255, 76, 76, 0.2);
          color: #a10a10;
        }
        &.right {
          background: rgba(50, 169, 83, 0.2);
          color: #09521c;
        }
        &.not_op {
          pointer-events: none;
        }
      }
    }
</style>

子组件 - myButton

<template>
  <div class="bigButton" @click="onclick" :class="isActive ? 'active' : ''">
    <slot></slot>
  </div>
</template>
<script setup>
    const emits = defineEmits(["click"]);
    const props = defineProps({
      isActive: {
        type: Boolean,
        default: false,
      },
    });
    function onclick() {
      if (!props.isActive) {
        return;
      }
      emits("click");
    }
</script>

<style lang="scss" scoped>
    .bigButton {
      user-select: none;
      cursor: not-allowed;
      border-radius: 54px;
      background-color: #789be3;
      width: 100%;
      height: 55px;
      line-height: 55px;
      color: #fff;
      text-align: center;
      font-size: 20px;
      font-weight: 400;
      &.active {
        background: #1352d6;
        box-shadow: 0px 0px 7.6px 3px #b2bed6;
        cursor: pointer;
        &:hover {
          background-color: #5380e1;
        }
      }
    }
</style>

父组件

<template>
  <div class="topic">
    <div class="TIndex">第{{ curData.bigIndex }}关</div>
    <div class="main">
      <div class="top">
        <div class="type">{{ currentData.type == 0 ? "单选" : "多选" }}</div>
        <div class="num">
          <span class="current">{{ currentIndex + 1 }}</span
          >/{{ curData.topics.length }}
        </div>
      </div>
      <div class="title">{{ currentData.title }}</div>
      <div class="chooses">
        <myChooseBox
          :chooses="currentData.chooses"
          :curType="currentData.type"
          :curRights="currentData.right"
          ref="choose"
        ></myChooseBox>
      </div>
      <div class="analysis" v-if="isAnsRight == 1">
        <div class="answer">
          <div>答案:{{ rightAnswer }}</div>
          <div class="more" @click="more">
            查看解析
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="15"
              height="9"
              viewBox="0 0 15 9"
              fill="none"
              v-if="analysisOnShow"
            >
              <path
                fill-rule="evenodd"
                clip-rule="evenodd"
                d="M0.292893 6.47356C-0.0976311 6.86408 -0.0976311 7.49725 0.292893 7.88777C0.683417 8.2783 1.31658 8.27829 1.70711 7.88777L7.18213 2.41275L12.6569 7.88747C13.0474 8.278 13.6805 8.278 14.0711 7.88747C14.4616 7.49695 14.4616 6.86379 14.0711 6.47326L7.89754 0.29973C7.85652 0.258715 7.81283 0.222007 7.76702 0.189607C7.37605 -0.0940027 6.82606 -0.0596046 6.47365 0.292801L0.292893 6.47356Z"
                fill="#1352D6"
                fill-opacity="0.4"
              />
            </svg>
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="15"
              height="9"
              viewBox="0 0 15 9"
              fill="none"
              v-else
            >
              <path
                fill-rule="evenodd"
                clip-rule="evenodd"
                d="M0.292889 1.70711C-0.0976295 1.3166 -0.0976295 0.683426 0.292889 0.292923C0.683407 -0.097641 1.31657 -0.097641 1.7071 0.292923L7.18214 5.76796L12.6568 0.293228C13.0474 -0.0973358 13.6805 -0.0973358 14.0711 0.293228C14.4616 0.683731 14.4616 1.31691 14.0711 1.70741L7.89753 7.88094C7.85675 7.92171 7.81332 7.95821 7.7678 7.99049C7.37678 8.27473 6.82627 8.24049 6.47364 7.88789L0.292889 1.70711Z"
                fill="#1352D6"
                fill-opacity="0.4"
              />
            </svg>
          </div>
        </div>
        <div class="text" ref="analysis">
          <div>解析</div>
          <div v-html="currentData.analysis"></div>
        </div>
      </div>
      <div class="btn">
        <BigButton @click="submit" :active="true" v-show="isAnsRight == -1"
          >提交</BigButton
        >
        <BigButton @click="replay" :active="true" v-show="isAnsRight == 0"
          >再答一次</BigButton
        >
        <myButton :isActive="true" @click="next" v-show="isAnsRight == 1"
          >下一题</myButton
        >
      </div>
    </div>
  </div>
</template>
<script setup>
    import myButton from "../components/myButton.vue";
    import { ref, onMounted, computed } from "vue";
    import { useStore } from "vuex";
    const store = useStore();

    import myChooseBox from "../components/myChooseBox.vue";
    const rightAnswer = computed(() => {
      return currentData.value.right
        .map((item) => {
          return String.fromCharCode(item + 65);
        })
        .join("、");
    });
    const analysis = ref(null);
    const analysisOnShow = ref(false);

    const isAnsRight = ref(-1);
    //添加判断正误标志-对应按钮变化
    const data = ref([
      {
        bigIndex: "一",
        topics: [
          {
            type: 0,
            title: "第一题",
            chooses: ["选项一", "选项二", "选项三", "选项四"],
            right: [2],
            analysis:
              "最后,江湖儿女的生活方式与贾宝玉和林黛玉也有很大差别。江湖儿女生活在动荡的江湖中,他们经常面临生死考验,生活节奏快、压力大。而贾宝玉和林黛玉则生活在安宁的豪门之中,生活节奏慢、物质生活丰富。这样的生活方式差异使得江湖儿女很难适应贾宝玉和林黛玉的生活环境。综上所述,江湖儿女不会爱上贾宝玉或林黛玉,主要是因为他们的性格、生活方式和价值观与江湖儿女相差甚远。最后,江湖儿女的生活方式与贾宝玉和林黛玉也有很大差别。江湖儿女生活在动荡的江湖中,他们经常面临生死考验,生活节奏快、压力大。而贾宝玉和林黛玉则生活在安宁的豪门之中,生活节奏慢、物质生活丰富。这样的生活方式差异使得江湖儿女很难适应贾宝玉和林黛玉的生活环境。综上所述,江湖儿女不会爱上贾宝玉或林黛玉,主要是因为他们的性格、生活方式和价值观与江湖儿女相差甚远",
          },
          {
            type: 1,
            title: "第二题",
            chooses: ["选项一", "选项二", "选项三", "选项四"],
            right: [2, 3],
            analysis: "解析",
          },
        ],
      },
      {
        bigIndex: "二",
        topics: [
          {
            type: 1,
            title: "第一题",
            chooses: ["选项一", "选项二", "选项三", "选项四"],
            right: [1, 2],
            analysis: "解析1",
          },
          {
            type: 0,
            title: "第二题",
            chooses: ["选项一", "选项二", "选项三", "选项四"],
            right: [3],
            analysis: "解析2",
          },
        ],
      },
    ]);
    const choose = ref(null); //组件
    const curIndex = ref(0); //当前关卡数
    const currentIndex = ref(0); //当前答题数
    const curData = ref(data.value[curIndex.value]); //当前关卡数据
    const currentData = ref(curData.value.topics[currentIndex.value]); //当前题数据
    function next() {
      store.commit("setIsOp", 1);
      //如果没到达最后一题
      if (currentIndex.value < curData.value.topics.length - 1) {
        currentIndex.value++;
        currentData.value = curData.value.topics[currentIndex.value];
        isAnsRight.value = -1; //改变按钮
        choose.value.replay();
      } else {
        console.log(2);
      }
    }
    function submit() {
      //做完一道题进行批改
      if (choose.value.check()) {
        isAnsRight.value = 1;
      } else {
        isAnsRight.value = 0;
      }
      store.commit("setIsOp", 0);
    }
    function replay() {
      store.commit("setIsOp", 1);
      isAnsRight.value = -1;
      choose.value.replay();
    }
    function showAnalysis() {
      analysisOnShow.value = true;
      let height =
        analysis.value.children[0].clientHeight +
        analysis.value.children[1].clientHeight;
      analysis.value.animate(
        [
          {
            marginTop: "12px",
            height: "0px",
          },
          {
            marginTop: "12px",
            height: height + "px",
          },
        ],
        {
          duration: 100,
          fill: "forwards",
        }
      );
    }
    function hideAnalysis() {
      analysisOnShow.value = false;
      let height =
        analysis.value.children[0].clientHeight +
        analysis.value.children[1].clientHeight;
      analysis.value.animate(
        [
          {
            marginTop: "12px",
            height: height + "px",
          },
          {
            marginTop: "0px",
            height: 0 + "px",
          },
        ],
        {
          duration: 100,
          fill: "forwards",
        }
      );
    }
    function more() {
      if (analysis.value.clientHeight == 0) {
        showAnalysis();
      } else {
        hideAnalysis();
      }
    }
</script>
<style lang="scss">
    .topic {
      height: 100%;
      width: 100%;
      .TIndex {
        font-size: 24px;
      }
      .main {
        border-radius: 20px;
        background: #fff;
        padding: 16px;
        .top {
          display: flex;
          align-items: center;
          .type {
            width: 34px;
            height: 20px;
            line-height: 20px;
            border-radius: 2px;
            text-align: center;
            margin-right: 20px;
          }
          .num {
            color: #979faf;
            text-align: center;
            font-size: 24px;
            .current {
              color: #1050d4;
              margin-right: 2px;
            }
          }
        }
        .analysis {
          margin-top: 16px;
          overflow: hidden;
          .answer {
            color: #263961;
            display: flex;
            justify-content: space-between;
            .more {
              cursor: pointer;
              user-select: none;
            }
          }
          .text {
            width: 100%;
            color: #5473b2;
            white-space: wrap;
            font-size: 16px;
            letter-spacing: 2px;
            height: 0;
          }
        }
      }
      .title {
        margin-top: 24px;
        margin-bottom: 20px;
        color: #141925;
        font-size: 18px;
        font-weight: 500;
        text-align: center;
      }
      .btn {
        margin-top: 30px;
      }
    }
</style>