记录前端菜鸟的日常——实现类似学习通的答题界面

一、总体介绍

1.先来看一下答题界面

题目前会展示当前题目类型和题目分数以及当前题数,左下角可以查看当前答题情况。后端返回的数据是一个大数组,每一项包含题目、题目类型、所有答案选项(最多四个)、题目code、题目分值、正确答案。

只有最后一页和作答情况面板里展示交卷按钮,第一页的上一题按钮不可点,未作答完就给出提示,最后提交试卷展示分数。并且支持左右滑动切换题目。

2.先来缕一下整体思路

从后端拿到的数据questionList我们分为三个小数组,并给每一题都配置一个checked属性,记录当前题目有没有作答。

在第一张图里展示的题目就是用的questionList,第二张图使用的是三个小数组,小数组点击通过遍历唯一值questionCode定位到大数组中。(第一张图不使用小数组:a.需要展示当前题号/总题目数 b.后端返回的数据就是单选=>多选=>判断排列的 c.三个小数组每次都得从0开始,作答情况面板不好实现点击定位。)

响应式变量stemNum来记录当前作答题目是questionList中的哪一道,radio数组来记录当前的作答情况。

除此之外就是左右滑动、点击提交加防抖节流等等细节问题。

二、代码

1.先来看一下模版

复制代码
 <div class="answerExam" @touchstart.passive="handleTouchStart" @touchmove.passive="handleTouchMove"
      @touchend.passive="handleTouchEnd">
      <div class="topic-con">
        <div class="stem">
          <div>
            <span class="type">{{
              questionList[stemNum]?.questionType == '1'
                ? "单选题"
                : questionList[stemNum]?.questionType == '2'
                  ? "多选题"
                  : "判断题"
            }}</span>
            <span class="score">
              {{ questionList[stemNum]?.questionType == '2' ? `每题至少两个答案,全部选对为${questionList[stemNum]?.questionScore}分` :
                `每题${questionList[stemNum]?.questionScore}分` }}
            </span>
          </div>
          <div>
            <span class="num">{{ stemNum + 1 }}</span><span class="sum">/{{ questionList.length }}</span>
          </div>
        </div>
        <div class="single" v-if="questionList[stemNum]?.questionType == '1'">
          <p>
            {{ questionList[stemNum].questionTitle }}
          </p>
          <van-radio-group style="padding: 12px;" @change="select()" v-model="radio[stemNum]">
            <van-radio name="A">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">A</span>
              </template>
              {{ questionList[stemNum].optionA }}</van-radio>
            <van-radio name="B">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">B</span>
              </template>
              {{ questionList[stemNum].optionB }}
            </van-radio>
            <van-radio name="C">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">C</span>
              </template>
              {{ questionList[stemNum].optionC }}
            </van-radio>
            <van-radio name="D">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">D</span>
              </template>
              {{ questionList[stemNum].optionD }}
            </van-radio>
          </van-radio-group>
        </div>
        <div class="multiple" v-if="questionList[stemNum]?.questionType == '2'">
          <p>{{ questionList[stemNum].questionTitle }}</p>
          <van-checkbox-group @change="select()" v-model="radio[stemNum]">
            <van-checkbox name="A">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">A</span>
              </template>
              {{ questionList[stemNum].optionA }}
            </van-checkbox>
            <van-checkbox name="B">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">B</span>
              </template>
              {{ questionList[stemNum].optionB }}
            </van-checkbox>
            <van-checkbox name="C">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">C</span>
              </template>
              {{ questionList[stemNum].optionC }}
            </van-checkbox>
            <van-checkbox name="D">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'">D</span>
              </template>
              {{ questionList[stemNum].optionD }}
            </van-checkbox>
          </van-checkbox-group>
        </div>
        <div class="judge" v-if="questionList[stemNum]?.questionType == '3'">
          <p>{{ questionList[stemNum].questionTitle }}</p>
          <van-radio-group v-model="radio[stemNum]" @change="select()">
            <van-radio name="A">
              <template #icon="props">
                <span :class="props.checked ? 'checked' : 'nochecked'"></span>
              </template>
              {{ questionList[stemNum].optionA }}
            </van-radio>
            <van-radio name="B">
              <template #icon="props">
                <span :class="props.checked ? 'checked1' : 'nochecked1'"></span>
              </template>
              {{ questionList[stemNum].optionB }}
            </van-radio>
          </van-radio-group>
        </div>
      </div>
      <div class="botm-con">
        <div class="condition" @click="response">
          <van-icon name="description" size="20px" />作答情况
        </div>
        <van-button round class="prev-btn" @click="goPrev" :disabled="stemNum === 0"
          :class="{ disabled: stemNum === 0 }">
          上一题
        </van-button>
        <!-- 下一题/交卷按钮 -->
        <van-button v-if="stemNum < questionList.length - 1" round type="info" @click="goNext">
          下一题
        </van-button>

        <van-button v-else round type="info" @click="handin">
          交卷
        </van-button>
      </div>
      <van-action-sheet v-model:show="show" title="作答情况">
        <div class="content">
          <div class="sin-c">
            <p>单选题</p>
            <div :class="item.checked ? 'card' : 'nocard'" v-for="(item, index) in singglequest" :key="index"
              @click="clickstem(item.questionCode)">
              {{ index + 1 }}
            </div>
          </div>
          <div class="sin-c" v-if="multiplequest.length !== 0">
            <p>多选题</p>
            <div class="card-con">
              <div :class="item1.checked ? 'card' : 'nocard'" v-for="(item1, index1) in multiplequest" :key="index1"
                @click="clickstem(item1.questionCode)">
                {{ index1 + 1 }}
              </div>
            </div>
          </div>
          <div class="sin-c" v-if="judgequest.length !== 0">
            <p>判断题</p>
            <div class="card-con">
              <div :class="item2.checked ? 'card' : 'nocard'" v-for="(item2, index2) in judgequest" :key="index2"
                @click="clickstem(item2.questionCode)">
                {{ index2 + 1 }}
              </div>
            </div>
          </div>
          <div class="zuoda">
            <van-button round class="cardBtn" @click="handin">交卷</van-button>
          </div>
        </div>
      </van-action-sheet>
      <van-dialog v-model:show="showScore" confirmButtonColor='#E12D22' @confirm="scoreConfirm" @cancel="scoreCancel"
        confirmButtonText="确定">
        <div class="score-con">
          <img class="jiaojuan" src="../../assets/scoreIcon.png" alt="">
          <span class="jjspan">交卷成功</span>
          <div><span class="scorenum">{{ examScore }}</span> <span>分</span></div>
        </div>
      </van-dialog>
    </div>

挨个遍历questionList,最上面展示题目类型、每题分数、当前题数,单选和判断使用van-radio-group;多选是van-checkbox-group,van-checkbox,都使用自定义图标。

上一题、下一题、交卷按钮根据当前题数stemNum和questionList.length进行比较控制显隐和能否点击。

作答弹窗遍历的是三个小数组,每一题的checked控制样式(这里有一个小问题,多选题只选择一项会被认为没有答过此题,可以根据需求来定)

2.逻辑代码

(1)大数组拆分成三个小数组

复制代码
const dividequestion = () => {
  questionList.value.forEach((item, index) => {
    if (item.questionType == "1") {
      item.checked = 0
      singglequest.value.push(item);
    } else if (item.questionType == "2") {
      item.checked = 0
      multiplequest.value.push(item);
    } else if (item.questionType == "3") {
      item.checked = 0
      judgequest.value.push(item);
    }
  });
}

(2)左右滑动和点击按钮切换题目

复制代码
const handleTouchStart = () => {
  startX.value = event.touches[0].clientX;
}
const handleTouchMove = (event) => {
  // 可以在这里实时监控滑动
}
const handleTouchEnd = () => {
  endX.value = event.changedTouches[0].clientX;
  const deltaX = endX.value - startX.value;
  if (Math.abs(deltaX) > 15) {
    if (deltaX > 0) {
      if (stemNum.value > 0) {
        stemNum.value--;
      }
      // 向右滑动
    } else {
      if (stemNum.value < questionList.value.length - 1) {
        stemNum.value++;
      }

    }
  }
}
// 导航功能
const goPrev = debounce(() => {
  if (stemNum.value > 0) {
    stemNum.value--
  }
}, 300)
const goNext = debounce(() => {
  if (stemNum.value < questionList.value.length - 1) {
    stemNum.value++
  }
}, 300)

记录起止滑动的坐标,相减超过一定范围(这里定的是15px)才判断为切换题目,并且需要注意边界问题,然后就可以直接改变stemNum进行切换了。

(3)当选择了某个选项触发的事件

复制代码
const select = () => {
  getnoSelect()
}
const getnoSelect = () => {
  questionList.value.forEach((item, index) => {
    if (radio.value[index] && item.questionType == '1') {
      singglequest.value.forEach((item1, index1) => {
        if (item1.questionCode == item.questionCode) {
          item1.checked = 1
        }
      })
    } else if (radio.value[index] && item.questionType == '2' && radio.value[index].length > 1) {
      multiplequest.value.forEach((item2, index2) => {
        if (item2.questionCode == item.questionCode) {
          item2.checked = 1
        }
      })
    } else if (radio.value[index] && item.questionType == '3') {
      judgequest.value.forEach((item3, index3) => {
        if (item3.questionCode == item.questionCode) {
          item3.checked = 1
        }
      })
    } else {
      multiplequest.value.forEach((item2, index2) => {
        if (item2.questionCode == item.questionCode) {
          item2.checked = 0
        }
      })
    }
  });
  console.log(questionList.value, multiplequest.value, judgequest.value, '选择情况')
}

这个函数是先遍历的questionList,然后判断是否有值、再看类型,再遍历对应的小数组将用户当前选择的那项设置为被选择了(多选只选一项不算),不过我试过了,就算不再遍历里面的小数组直接修改当前item.checked也是对的(引用数据类型指向同一地址),代码如下:

复制代码
questionList.value.forEach((item, index) => {
    if (radio.value[index] && item.questionType == '1') {
      // singglequest.value.forEach((item1, index1) => {
      //   if (item1.questionCode == item.questionCode) {
      item.checked = 1
      console.log(item, 'item1被改变喽')
      // }
      // })
    } else if (radio.value[index] && item.questionType == '2' && radio.value[index].length > 1) {
      // multiplequest.value.forEach((item2, index2) => {
      //   if (item2.questionCode == item.questionCode) {
      item.checked = 1
      console.log(item, 'item2被改变喽')
      //   }
      // })
    } else if (radio.value[index] && item.questionType == '3') {
      // judgequest.value.forEach((item3, index3) => {
      //   if (item3.questionCode == item.questionCode) {
      item.checked = 1
      console.log(item, 'item3被改变喽')
      //   }
      // })
    } else {
      // multiplequest.value.forEach((item2, index2) => {
      //   if (item2.questionCode == item.questionCode) {
      item.checked = 0
      console.log(item, 'item2只选了一个')
      //   }
      // })
    }
  });

在点击弹窗出来的时候也要调用一次这个函数,保证数据实时有效

(4)点击弹窗面板上的题号跳转

复制代码
const clickstem = (code) => {
  questionList.value.forEach((item, index) => {
    if (item.questionCode == code) {
      stemNum.value = index
    }
  })
  show.value = false
}

这里用的是唯一值questionCode找到大数组中的所在位置,然后赋值给stemNum

(5)交卷

这里就只展示一部分内容,我采用的结构是每一道题都将questionCode、abcd四个选项(选则1,否则0)这种结构传给后端

复制代码
studentAnswerList.value = []
  for (let i = 0; i < questionList.value.length; i++) {
    let list = {
      questionCode: questionList.value[i].questionCode,
      selectedA: "0",
      selectedB: "0",
      selectedC: "0",
      selectedD: "0",
    };
    if (radio.value[i]) {
      if (questionList.value[i].questionType == '2') {
        radio.value[i].forEach((item, index) => {
          list[`selected${item}`] = "1";
        });
      } else {
        list[`selected${radio.value[i]}`] = "1";
      }
      studentAnswerList.value.push(list);
    } else {
      studentAnswerList.value.push(list);
    }
  }

0: "A"

1: "A"

10: (2) ['A', 'B']

radio数组输出是这种结构的

样式这里就不再放了哈,大家根据自己的项目去设置

完结~

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习