H5 中 van-popup 的使用以及题目的切换

发布于:2024-12-20 ⋅ 阅读:(182) ⋅ 点赞:(0)

H5 中 van-popup 的使用以及题目的切换

在移动端开发中,弹窗组件是一个常见的需求。vant 是一个轻量、可靠的移动端 Vue 组件库,其中的 van-popup 组件可以方便地实现弹窗效果。本文将介绍如何使用 van-popup 实现题目详情的弹窗展示,并实现题目的切换功能。

关键点总结

  1. 引入 van-popup 组件

    • 在 Vue 项目中引入 vant 组件库,并使用 van-popup 组件实现弹窗效果。
    • import { createApp } from 'vue'
      import Vant from 'vant'
      
      const app = createApp(App)
      app.use(Vant)
      app.mount('#app')
  2. 弹窗内容的条件渲染

    • 根据不同的题目类型(如互动题和练习题),在弹窗中显示不同的内容。
  3. 题目详情的展示

    • 使用 computed 属性计算当前题目的详情,并在弹窗中展示题目的相关信息。
  4. 题目的切换

    • 通过按钮实现题目的上一题和下一题的切换,并更新当前题目的索引。
代码示例

以下是实现上述功能的关键代码片段:

questions.vue---子组件

<template>
  <van-popup v-model:show="localVisible" position="bottom" round :style="{ height: '80%' }" @close="close">
    <div v-if="type === 'interactive'">
      <div class="picker-header">
        <div class="picker-title">
          题目详情
          <button @click="close" class="close-button">X</button>
        </div>
        <div class="picker-info">
          <div class="left-info">
            <span class="number">第{{ currentQuestion.serial_number }}题</span>
            <span class="status">{{ getStatusText(currentQuestion.status) }}</span>
          </div>
          <div class="right-info">
            <button v-if="!isFirstQuestion" @click="prevQuestion">
              <van-icon name="arrow-left" />
            </button>
            <span>{{ currentQuestion.serial_number }}/{{ questions.length }}</span>
            <button v-if="!isLastQuestion" @click="nextQuestion">
              <van-icon name="arrow" />
            </button>
          </div>
        </div>
      </div>
      <div class="picker-content">
        <div class="section-title">课件页面</div>
        <iframe :src="currentQuestion.previewUrl" frameborder="0"></iframe>
        <div class="use-duration">
          我的用时:
          <span class="time-number">{{ formattedDuration.minutes }}</span>分 <span class="time-number">{{
            formattedDuration.seconds }}</span>秒
        </div>
      </div>
    </div>
    <div v-else-if="type === 'practice'">
      //  其他内容
    </div>
  </van-popup>
</template>

<script setup>
import { defineProps, defineEmits, computed, ref, watch } from 'vue'
import { Popup } from 'vant'

const props = defineProps({
  visible: Boolean,
  questions: {
    type: Array,
    required: true,
  },
  currentQuestionIndex: {
    type: Number,
    required: true,
  },
  type: {
    type: String,
    required: true,
  },
})

const emits = defineEmits(['close', 'changeQuestion'])

const localVisible = ref(props.visible)

watch(
  () => props.visible,
  newVal => {
    localVisible.value = newVal
  },
)

const currentQuestion = computed(() => {
  const question = props.questions[props.currentQuestionIndex] || {}
  if (props.type === 'practice' && !question.serial_number) {
    question.serial_number = props.currentQuestionIndex + 1
  }
  return question
})

const getStatusText = status => {
  switch (status) {
    case 1:
      return '正确'
    case 2:
      return '错误'
    case 3:
      return '半对半错'
    default:
      return '未作答'
  }
}

const formatDuration = duration => {
  const minutes = String(Math.floor(duration / 60)).padStart(2, '0')
  const seconds = String(duration % 60).padStart(2, '0')
  return { minutes, seconds }
}

const formattedDuration = computed(() => formatDuration(currentQuestion.value.use_duration))

const isFirstQuestion = computed(() => props.currentQuestionIndex === 0)
const isLastQuestion = computed(() => props.currentQuestionIndex === props.questions.length - 1)

const prevQuestion = () => {
  if (!isFirstQuestion.value) {
    emits('changeQuestion', props.currentQuestionIndex - 1)
  }
}

const nextQuestion = () => {
  if (!isLastQuestion.value) {
    emits('changeQuestion', props.currentQuestionIndex + 1)
  }
}

const close = () => {
  emits('close')
}
</script>

<style lang="less" scoped>
.picker-header {
  padding: 10px;
}

.picker-title {
  font-size: 18px;
  font-weight: bold;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
  color: #000;
  margin-top: 10px;
  display: flex;
  width: 100%;

  .close-button {
    background: none;
    border: none;
    font-size: 16px;
    margin-left: auto;
    color: #a9aeb8;
    cursor: pointer;
  }
}

.picker-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 10px 0 10px;
}

.left-info {
  display: flex;
  flex-direction: row;

  .number {
    margin-right: 20px;
    font-size: 16px;
    font-weight: 500;
  }

  .status {
    font-size: 16px;
    font-weight: 500;
    color: #1f70ff;
  }
}

.right-info {
  display: flex;
  position: absolute;
  right: 10px;
  color: #a9aeb8;

  .right-icon {
    width: 28px;
    height: 28px;
  }
}

.right-info button {
  background: none;
  border: none;
  font-size: 16px;
  cursor: pointer;
  margin: 0 5px;
}

.picker-content {
  padding: 10px 20px 0 20px;
}

.section-title {
  font-size: 16px;
  font-family: PingFangSC-Regular, PingFang SC;
  font-weight: 400;
  color: #2b2f38;
}

iframe {
  width: 100%;
  height: 300px;
  border: none;
  margin-bottom: 10px;
}

.use-duration {
  font-size: 16px;
  color: #2b2f38;
}

.time-number {
  font-weight: bold;
  color: #0074fc;
  font-size: 24px;
}

.van-popup {
  height: 50%;
  z-index: 99999;
}

.practice-content {
  padding: 0px 20px 0 20px;
}
</style>

courseDetail.vue---父组件

// template关键代码
<div v-for="(item, index) in period.interactive_performance.list" :key="index" :class="[
              'performance-item',
              getStatusClass(item.status),
              { selected: selectedQuestion === index },
            ]" @click="selectQuestion(index, period.interactive_performance.list, 'interactive')">
              <span :class="getQuestionTextClass(item.status, selectedQuestion === index)">{{
                item.serial_number
                }}</span>
            </div>

<div v-for="(item, index) in period.practice_detail.list" :key="index" :class="[
              'practice-item',
              getStatusClass(item.status),
              { selected: selectedPracticeQuestion === index },
            ]" @click="selectPracticeQuestion(index, period.practice_detail.list, 'practice')">
              <div class="question-number">
                <span>{{ index + 1 }}</span>
              </div>
</div>

<QuestionDetail :visible="showQuestionDetail" :questions="currentQuestions" :type="currentType"
      :currentQuestionIndex="currentQuestionIndex" @close="closeQuestionDetail" @changeQuestion="changeQuestion" />

// script关键代码
const selectQuestion = (index, questions, type) => {
  selectedQuestion.value = index
  currentQuestions.value = questions
  currentType.value = type
  currentQuestionIndex.value = index
  showQuestionDetail.value = true
}

const selectPracticeQuestion = (index, questions, type) => {
  selectedPracticeQuestion.value = index
  currentQuestions.value = questions
  currentQuestionIndex.value = index
  // 设置 serial_number 属性
  currentQuestions.value.forEach((question, idx) => {
    question.serial_number = idx + 1
  })
  currentType.value = type
  showQuestionDetail.value = true
}
const changeQuestion = index => {
  currentQuestionIndex.value = index
}

数据结构

关键点解析
  1. 引入 van-popup 组件

    • 在模板中使用 <van-popup> 标签,并通过 v-model:show 绑定弹窗的显示状态。
    • 设置 position="bottom" 和 round 属性,使弹窗从底部弹出并带有圆角。
  2. 弹窗内容的条件渲染

    • 使用 v-if 和 v-else-if 根据 type 属性的值渲染不同的内容。
    • 当 type 为 interactive 时,显示互动题的详情;当 type 为 practice 时,显示练习题的详情。
  3. 题目详情的展示

    • 使用 computed 属性计算当前题目的详情,并在弹窗中展示题目的相关信息。
    • 通过 currentQuestion 计算属性获取当前题目的详细信息。
  4. 题目的切换

    • 通过按钮实现题目的上一题和下一题的切换,并更新当前题目的索引。
    • 使用 isFirstQuestion 和 isLastQuestion 计算属性判断当前题目是否为第一题或最后一题,以控制按钮的显示和隐藏。

大致效果

通过以上关键点的实现,我们可以在移动端应用中使用 van-popup 组件实现题目详情的弹窗展示,并实现题目的切换功能。希望本文对您有所帮助!


网站公告

今日签到

点亮在社区的每一天
去签到