从0开始,手把手教你用Vue.js开发一个答题App(下)
导读:1.4 运行项目yarn run serve2. 答题页面开发2.1 修改路由修改router/index.js:import Vue from 'vue' import VueRouter from 'vue-router' import...
1.4 运行项目
yarn run serve
2. 答题页面开发
2.1 修改路由
修改router/index.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
import MainMenu from '../views/MainMenu.vue'
import GameController from '../views/GameController.vue'
Vue.use(VueRouter)
const routes = [
{
name: 'home',
path: '/',
component: MainMenu
}
, {
name: 'quiz',
path: '/quiz',
component: GameController,
props: (route) =>
({
number: route.query.number,
difficulty: route.query.difficulty,
category: route.query.category,
type: route.query.type
}
)
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
}
)
export default router2.2 答题页面
新增views/GameController.vue
本页面是本项目最重要的模块,展示问题,和处理用户提交的答案,简单解析一下:
1.fetchQuestions函数通过请求远程接口获得问题列表。
2.setQuestions保存远程回应的问题列表到本地数组。
3.onAnswerSubmit处理用户提交的选项,调用nextQuestion函数返回下一问题。
template>
div class="h-100">
LoadingIcon v-if="loading">
/LoadingIcon>
Question :question="currentQuestion" @answer-submitted="onAnswerSubmit" v-else>
/Question>
/div>
/template>
script>
import EventBus from '../eventBus'
import ShuffleMixin from '../mixins/shuffleMixin'
import Question from '../components/Question'
import LoadingIcon from '../components/LoadingIcon'
import axios from 'axios'
export default {
name: 'GameController',
mixins: [ShuffleMixin],
props: {
/** Number of questions */
number: {
default: '10',
type: String,
required: true
}
,
/** Id of category. Empty string if not included in query */
category: String,
/** Difficulty of questions. Empty string if not included in query */
difficulty: String,
/** Type of questions. Empty string if not included in query */
type: String
}
,
components: {
Question,
LoadingIcon
}
,
data() {
return {
// Array of custom question objects. See setQuestions() for format
questions: [],
currentQuestion: {
}
,
// Used for displaying ajax loading animation OR form
loading: true
}
}
,
created() {
this.fetchQuestions()
}
,
methods: {
/** Invoked on created()
* Builds API URL from query string (props).
* Fetches questions from API.
* "Validates" return from API and either routes to MainMenu view, or invokes setQuestions(resp).
* @public
*/
fetchQuestions() {
let url = `https://opentdb.com/api.php?amount=${
this.number}
`
if (this.category) url += `&
category=${
this.category}
`
if (this.difficulty) url += `&
difficulty=${
this.difficulty}
`
if (this.type) url += `&
type=${
this.type}
`
axios.get(url)
.then(resp =>
resp.data)
.then(resp =>
{
if (resp.response_code === 0) {
this.setQuestions(resp)
}
else {
EventBus.$emit('alert-error', 'Bad game settings. Try another combination.')
this.$router.replace({
name: 'home' }
)
}
}
)
}
,
/** Takes return data from API call and transforms to required object setup.
* Stores return in $root.$data.state.
* @public
*/
setQuestions(resp) {
resp.results.forEach(qst =>
{
const answers = this.shuffleArray([qst.correct_answer, ...qst.incorrect_answers])
const question = {
questionData: qst,
answers: answers,
userAnswer: null,
correct: null
}
this.questions.push(question)
}
)
this.$root.$data.state = this.questions
this.currentQuestion = this.questions[0]
this.loading = false
}
,
/** Called on submit.
* Checks if answer is correct and sets the user answer.
* Invokes nextQuestion().
* @public
*/
onAnswerSubmit(answer) {
if (this.currentQuestion.questionData.correct_answer === answer) {
this.currentQuestion.correct = true
}
else {
this.currentQuestion.correct = false
}
this.currentQuestion.userAnswer = answer
this.nextQuestion()
}
,
/** Filters all unanswered questions,
* checks if any questions are left unanswered,
* updates currentQuestion if so,
* or routes to "result" if not.
* @public
*/
nextQuestion() {
const unansweredQuestions = this.questions.filter(q =>
!q.userAnswer)
if (unansweredQuestions.length >
0) {
this.currentQuestion = unansweredQuestions[0]
}
else {
this.$router.replace({
name: 'result' }
)
}
}
}
}
/script>
新增\src\mixins\shuffleMixin.js
打乱问题答案,因为远程返回的答案有规律。mixins是混入的意思,可以混入到我们的某个页面或组件中,补充页面或组件功能,便于复用。
const ShuffleMixin = {
methods: {
shuffleArray: (arr) =>
arr
.map(a =>
[Math.random(), a])
.sort((a, b) =>
a[0] - b[0])
.map(a =>
a[1])
}
}
export default ShuffleMixin新增src/components/Question.vue
template>
div>
QuestionBody :questionData="question.questionData">
/QuestionBody>
b-card-body class="pt-0">
hr>
b-form @submit="onSubmit">
b-form-group
label="Select an answer:"
class="text-left"
>
b-form-radio
v-for="(ans, index) of question.answers"
:key="index"
v-model="answer"
:value="ans"
>
div v-html="ans">
/div>
/b-form-radio>
/b-form-group>
b-button type="submit" class="custom-success">
Submit/b-button>
/b-form>
/b-card-body>
/div>
/template>
script>
import QuestionBody from './QuestionBody'
export default {
name: 'Question',
props: {
/** Question object containing questionData, possible answers, and user answer information. */
question: {
required: true,
type: Object
}
}
,
components: {
QuestionBody
}
,
data() {
return {
answer: null
}
}
,
methods: {
onSubmit(evt) {
evt.preventDefault()
if (this.answer) {
/** Triggered on form submit. Passes user answer.
* @event answer-submitted
* @type {
number|string}
* @property {
string}
*/
this.$emit('answer-submitted', this.answer)
this.answer = null
}
}
}
}
/script>
新增src/components/QuestionBody.vue
template>
div>
b-card-header :class="variant" class="d-flex justify-content-between border-bottom-0">
div>
{
{
questionData.category }
}
/div>
div class="text-capitalize">
{
{
questionData.difficulty }
}
/div>
/b-card-header>
b-card-body>
b-card-text class="font-weight-bold" v-html="questionData.question">
/b-card-text>
/b-card-body>
/div>
/template>
script>
export default {
name: 'QuestionBody',
props: {
/** Object containing question data as given by API. */
questionData: {
required: true,
type: Object
}
}
,
data() {
return {
variants: {
easy: 'custom-success', medium: 'custom-warning', hard: 'custom-danger', default: 'custom-info' }
,
variant: 'custom-info'
}
}
,
methods: {
/** Invoked on mounted().
* Sets background color of card header based on question difficulty.
* @public
*/
setVariant() {
switch (this.questionData.difficulty) {
case 'easy':
this.variant = this.variants.easy
break
case 'medium':
this.variant = this.variants.medium
break
case 'hard':
this.variant = this.variants.hard
break
default:
this.variant = this.variants.default
break
}
}
}
,
mounted() {
this.setVariant()
}
}
/script>
docs>
Simple component displaying question category, difficulty and question text.
Used on both Question component and Answer component.
/docs>
运行:
yarn run serve
启动成功:
如果能看到该页面,恭喜你,项目到此成功了。
2.3 至此项目目录结构
如果你走丢,请下载源码进行对比:
3 实现最终结果展示页面
再次修改router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import MainMenu from '../views/MainMenu.vue'
import GameController from '../views/GameController.vue'
import GameOver from '../views/GameOver'
Vue.use(VueRouter)
const routes = [
...
{
name: 'result',
path: '/result',
component: GameOver
}
]
...新增src/views/GameOver.vue:
template>
div class="h-100">
b-card-header class="custom-info text-white font-weight-bold">
Your Score: {
{
score }
}
/ {
{
maxScore }
}
/b-card-header>
Answer v-for="(question, index) of questions" :key="index" :question="question">
/Answer>
/div>
/template>
script>
import Answer from '../components/Answer'
export default {
name: 'GameOver',
components: {
Answer
}
,
data() {
return {
questions: [],
score: 0,
maxScore: 0
}
}
,
methods: {
/** Invoked on created().
* Grabs data from $root.$data.state.
* Empties $root.$data.state =>
This is done to ensure data is cleared when starting a new game.
* Invokes setScore().
* @public
*/
setQuestions() {
this.questions = this.$root.$data.state || []
this.$root.$data.state = []
this.setScore()
}
,
/** Computes maximum possible score (amount of questions * 10)
* Computes achieved score (amount of correct answers * 10)
* @public
*/
setScore() {
this.maxScore = this.questions.length * 10
this.score = this.questions.filter(q =>
q.correct).length * 10
}
}
,
created() {
this.setQuestions();
}
}
/script>
新增src\components\Answer.vue
template>
div>
b-card no-body class="answer-card rounded-0">
QuestionBody :questionData="question.questionData">
/QuestionBody>
b-card-body class="pt-0 text-left">
hr class="mt-0">
b-card-text
class="px-2"
v-html="question.questionData.correct_answer"
>
/b-card-text>
b-card-text
class="px-2"
:class="{
'custom-success': question.correct, 'custom-danger': !question.correct }
"
v-html="question.userAnswer"
>
/b-card-text>
/b-card-body>
/b-card>
/div>
/template>
script>
import QuestionBody from './QuestionBody'
export default {
name: 'Answer',
props: {
/** Question object containing questionData, possible answers, and user answer information. */
question: {
required: true,
type: Object
}
}
,
components: {
QuestionBody
}
}
/script>
style scoped>
.answer-card >
>
>
.card-header {
border-radius: 0;
}
/style>
3.1 运行项目
yarn run serve
3.2 项目结构
项目总结
很感谢您和豆约翰走到了这里,至此我们一个小型的Vue项目,全部开发完毕,下一期,豆约翰会带大家见识一个中型的项目,咱们循序渐进,一起加油。
本系列文章首发于作者的微信公众号[豆约翰],想尝鲜的朋友,请微信搜索关注。
有什么问题也可以加我微信[tiantiancode]一起讨论。
最后
为了将来还能找到我
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: 从0开始,手把手教你用Vue.js开发一个答题App(下)
本文地址: https://pptw.com/jishu/297694.html
