H5 中 van-popup
的使用以及题目的切换
在移动端开发中,弹窗组件是一个常见的需求。vant
是一个轻量、可靠的移动端 Vue 组件库,其中的 van-popup
组件可以方便地实现弹窗效果。本文将介绍如何使用 van-popup
实现题目详情的弹窗展示,并实现题目的切换功能。
关键点总结
-
引入
van-popup
组件:- 在 Vue 项目中引入
vant
组件库,并使用van-popup
组件实现弹窗效果。 -
import { createApp } from 'vue' import Vant from 'vant' const app = createApp(App) app.use(Vant) app.mount('#app')
- 在 Vue 项目中引入
-
弹窗内容的条件渲染:
- 根据不同的题目类型(如互动题和练习题),在弹窗中显示不同的内容。
-
题目详情的展示:
- 使用
computed
属性计算当前题目的详情,并在弹窗中展示题目的相关信息。
- 使用
-
题目的切换:
- 通过按钮实现题目的上一题和下一题的切换,并更新当前题目的索引。
代码示例
以下是实现上述功能的关键代码片段:
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
}
数据结构
关键点解析
-
引入
van-popup
组件:- 在模板中使用
<van-popup>
标签,并通过v-model:show
绑定弹窗的显示状态。 - 设置
position="bottom"
和round
属性,使弹窗从底部弹出并带有圆角。
- 在模板中使用
-
弹窗内容的条件渲染:
- 使用
v-if
和v-else-if
根据type
属性的值渲染不同的内容。 - 当
type
为interactive
时,显示互动题的详情;当type
为practice
时,显示练习题的详情。
- 使用
-
题目详情的展示:
- 使用
computed
属性计算当前题目的详情,并在弹窗中展示题目的相关信息。 - 通过
currentQuestion
计算属性获取当前题目的详细信息。
- 使用
-
题目的切换:
- 通过按钮实现题目的上一题和下一题的切换,并更新当前题目的索引。
- 使用
isFirstQuestion
和isLastQuestion
计算属性判断当前题目是否为第一题或最后一题,以控制按钮的显示和隐藏。
大致效果
通过以上关键点的实现,我们可以在移动端应用中使用 van-popup
组件实现题目详情的弹窗展示,并实现题目的切换功能。希望本文对您有所帮助!