67. Moving between Questions
npm create vue@latest
이번 강의에서는 퀴즈 애플리케이션의 진행 상태를 추적하고, 사용자가 현재 답변하고 있는 질문만 표시하는 방법을 배웁니다.Questions Answered
라는 데이터 속성을 사용하여 현재 활성화된 질문의 인덱스를 추적합니다.
사용자가 질문에 답변하면, 이 속성의 값이 증가하여 다음 질문으로 넘어갑니다.Questions
컴포넌트에서는 v-show
지시어를 사용하여 현재 인덱스와 일치하는 질문만 표시하도록 설정합니다.
답변을 클릭할 때마다 selectAnswer
함수가 실행되며, 이 함수는 답변의 정확성을 검사하고, 정답 수를 추적합니다.
또한, 진행 상태 표시줄을 동적으로 업데이트하여 사용자가 몇 개의 질문에 답변했는지 시각적으로 표시합니다.
이러한 과정을 통해 퀴즈 애플리케이션의 기본적인 기능을 구현합니다.
src/main.ts
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
src/App.vue
<script setup lang="ts">
import {ref} from "vue";
import Questions from "@/components/Questions.vue";
import Result from "@/components/Result.vue";
const questionsAnswered = ref(0);
const totalCorrect = ref(0);
const questions = ref([
{
q: 'What is 2 + 2?',
answers: [
{
text: '4',
is_correct: true
},
{
text: '3',
is_correct: false
},
{
text: 'Fish',
is_correct: false
},
{
text: '5',
is_correct: false
}
]
},
{
q: 'How many letters are in the word "Banana"?',
answers: [
{
text: '5',
is_correct: false
},
{
text: '7',
is_correct: false
},
{
text: '6',
is_correct: true
},
{
text: '12',
is_correct: false
}
]
},
{
q: 'Find the missing letter: C_ke',
answers: [
{
text: 'e',
is_correct: false
},
{
text: 'a',
is_correct: true
},
{
text: 'i',
is_correct: false
}
]
},
])
const results = ref([
{
min: 0,
max: 2,
title: "Try again!",
desc: "Do a little more studying and you may succeed!"
},
{
min: 3,
max: 3,
title: "Wow, you're a genius!",
desc: "Studying has definitely paid off for you!"
}
])
const questionAnswered = (is_correct: boolean) => {
if (is_correct) {
totalCorrect.value++;
}
questionsAnswered.value++;
}
</script>
<template>
<div class="ctr">
<Questions
v-if="questionsAnswered < questions.length"
:questions="questions"
:questionsAnswered="questionsAnswered"
@question-answered="questionAnswered"
/>
<Result v-else />
<button type="button" class="reset-btn">Reset</button>
</div>
</template>
src/components/Questions.vue
<script setup lang="ts">
const props = withDefaults(
defineProps<{
questions: Array<{
q: string
answers: Array<{
text: string
is_correct: boolean
}>
}>
questionsAnswered: number
}>(),
{}
)
const emits = defineEmits(['question-answered'])
const selectAnswer = (is_correct: boolean) => {
emits('question-answered', is_correct)
}
</script>
<template>
<div class="questions-ctr">
<div class="progress">
<div class="bar" :style="{width: `${questionsAnswered / questions.length * 100}%`}"></div>
<div class="status">
{{ questionsAnswered }} out of {{ questions.length }} questions answered
</div>
</div>
<div
class="single-question"
v-for="(question, qi) in questions"
:key="question.q"
v-show="questionsAnswered === qi"
>
<div class="question">{{ question.q }}</div>
<div class="answers">
<div
class="answer"
v-for="answer in question.answers"
:key="answer.text"
@click.prevent="selectAnswer(answer.is_correct)"
>
{{ answer.text }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>
src/components/Result.vue
<script setup lang="ts">
</script>
<template>
<div class="result">
<div class="title">You got sample result 1!</div>
<div class="desc">
Enter a short description here about the result.
</div>
</div>
</template>
<style scoped>
</style>
src/assets/main.css
* {
box-sizing: border-box;
}
body {
font-size: 20px;
font-family: sans-serif;
padding-top: 20px;
background: #e6ecf1;
}
.ctr {
margin: 0 auto;
max-width: 600px;
width: 100%;
box-sizing: border-box;
position: relative;
}
.questions-ctr {
position: relative;
width: 100%;
}
.question {
width: 100%;
padding: 20px;
font-size: 32px;
font-weight: bold;
text-align: center;
background-color: #00ca8c;
color: #fff;
box-sizing: border-box;
}
.single-question {
position: relative;
width: 100%;
}
.answer {
border: 1px solid #8e959f;
padding: 20px;
font-size: 18px;
width: 100%;
background-color: #fff;
transition: 0.2s linear all;
}
.answer span {
display: inline-block;
margin-left: 5px;
font-size: 0.75em;
font-style: italic;
}
.progress {
height: 50px;
margin-top: 10px;
background-color: #ddd;
position: relative;
}
.bar {
height: 50px;
background-color: #ff6372;
transition: all 0.3s linear;
}
.status {
position: absolute;
top: 15px;
left: 0;
text-align: center;
color: #fff;
width: 100%;
}
.answer:not(.is-answered) {
cursor: pointer;
}
.answer:not(.is-answered):hover {
background-color: #8ce200;
border-color: #8ce200;
color: #fff;
}
.title {
width: 100%;
padding: 20px;
font-size: 32px;
font-weight: bold;
text-align: center;
background-color: #12cbc4;
color: #fff;
box-sizing: border-box;
}
.desc {
border: 1px solid #8e959f;
padding: 20px;
font-size: 18px;
width: 100%;
background-color: #fff;
transition: 0.4s linear all;
text-align: center;
}
.fade-enter-from {
opacity: 0;
}
.fade-enter-active {
transition: all 0.3s linear;
}
.fade-leave-active {
transition: all 0.3s linear;
opacity: 0;
position: absolute;
}
.fade-leave-to {
opacity: 0;
}
.reset-btn {
background-color: #ff6372;
border: 0;
font-size: 22px;
color: #fff;
padding: 10px 25px;
margin: 10px auto;
display: block;
}
.result{
width: 100%;
}
.reset-btn:active, .reset-btn:focus, .reset-btn:hover{
border: 0;
outline: 0;
}