从0开始,手把手教你用Vue开发一个答题App

项目演示

项目演示

项目源码

项目源码

配套讲解视频

配套讲解视频第一节
配套讲解视频第二节

微信小程序版

微信小程序版实战教程

教程说明

本教程适合对 Vue 基础知识有一点了解,但不懂得综合运用,还未曾使用 Vue 从头开发过一个小型 App 的读者。本教程不对所有的 Vue 知识点进行讲解,而是手把手一步步从 0 到 1,做出一个完整的小项目。目前网上的教程不是只有零散的知识点讲解;就是抛出一个开源的大项目,初级读者下载下来后,运行起来都很费劲,更谈不上理解这个项目是如何一步步开发出来的了。本教程试图弥补这个空白。

1. 项目初始化

1.1 使用 Vue CLI 创建项目

如果你还没有安装 VueCLI,请执行下面的命令安装或是升级:

npm install --global @vue/cli

在命令行中输入以下命令创建 Vue 项目:

vue create vue-quiz
Vue CLI v4.3.1
? Please pick a preset:
> default (babel, eslint)
  Manually select features

default:默认勾选 babel、eslint,回车之后直接进入装包

manually:自定义勾选特性配置,选择完毕之后,才会进入装包

选择第 1 种 default.

安装结束,命令提示你项目创建成功,按照命令行的提示在终端中分别输入:

# 进入你的项目目录
cd vue-quiz

# 启动开发服务
npm run serve

启动成功,命令行中输出项目的 http 访问地址。 打开浏览器,输入其中任何一个地址进行访问

image-20200707121732592

如果能看到该页面,恭喜你,项目创建成功了。

1.2 初始目录结构

项目创建好以后,下面我们来了解一下初始目录结构:

image-20200707122944401

1.3 调整初始目录结构,实现游戏设置页面

默认生成的目录结构不满足我们的开发需求,所以需要做一些自定义改动。

这里主要处理下面的内容:

  • 删除初始化的默认文件
  • 新增调整我们需要的目录结构

删除默认示例文件:

  • src/components/HelloWorld.vue
  • src/assets/logo.png

修改 package.json,添加项目依赖:

 "dependencies": {
    "axios": "^0.19.2",
    "bootstrap": "^4.4.1",
    "bootstrap-vue": "^2.5.0",
    "core-js": "^3.6.5",
    "vue": "^2.6.11",
    "vue-router": "^3.1.5"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.4.0",
    "@vue/cli-plugin-eslint": "~4.4.0",
    "@vue/cli-plugin-router": "~4.4.0",
    "@vue/cli-service": "~4.4.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "vue-template-compiler": "^2.6.11"
  },

然后运行 yarn install, 安装依赖。

修改项目入口文件 main.js,引入 bootstrap-vue。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.config.productionTip = false

Vue.use(BootstrapVue)

const state = { questions: []}

new Vue({
router,
data: state,
render: h => h(App)
}).$mount('#app')

定义一个 state 对象来共享答题数据(答题页面和结果页面共享)

const state = { questions: [] }

src 目录下新增 eventBus.js 消息总线,用来在组件间传递消息,代码如下:

import Vue from 'vue'
const EventBus = new Vue()
export default EventBus

修改 App.vue,css 样式略,请参考源码。

<template>
  <div id="app" class="bg-light">
    <Navbar></Navbar>
    <b-alert :show="dismissCountdown" dismissible variant="danger" @dismissed="dismissCountdown = 0">
      {{ errorMessage }}
    </b-alert>
    <div class="d-flex justify-content-center">
      <b-card no-body id="main-card" class="col-sm-12 col-lg-4 px-0">
        <router-view></router-view>
      </b-card>
    </div>
  </div>
</template>

<script>
import EventBus from './eventBus'
import Navbar from './components/Navbar'

export default {
name: 'app',
components: {
Navbar
},
data() {
return {
errorMessage: '',
dismissSecs: 5,
dismissCountdown: 0
}
},
methods: {
showAlert(error) {
this.errorMessage = error
this.dismissCountdown = this.dismissSecs
}
},
mounted() {
EventBus.$on('alert-error', (error) => {
this.showAlert(error)
})
},
beforeDestroy() {
EventBus.$off('alert-error')
}
}
</script>

新增 components/Navbar.vue,定义导航部分。

image-20200707125506858

<template>
    <b-navbar id="navbar" class="custom-info" type="dark" sticky>
      <b-navbar-brand id="nav-logo" :to="{name:'home'}">Vue-Quiz</b-navbar-brand>
  <span class="hljs-tag">&lt;<span class="hljs-name">b-navbar-nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ml-auto"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">b-nav-item</span> <span class="hljs-attr">:to</span>=<span class="hljs-string">"{ name: 'home' }"</span>&gt;</span>New Game <span class="hljs-tag">&lt;/<span class="hljs-name">b-nav-item</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">b-nav-item</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">target</span>=<span class="hljs-string">"_blank"</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">b-nav-item</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">b-navbar-nav</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">b-navbar</span>&gt;</span>

</template>

<script>
export default {
name: 'Navbar'
}
</script>

<style scoped>

</style>

src 目录下新增 router/index.js,定义首页路由。

import Vue from 'vue'
import VueRouter from 'vue-router'
import MainMenu from '../views/MainMenu.vue'

Vue.use(VueRouter)

const routes = [
{
name: 'home',
path: '/',
component: MainMenu
}
]

const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})

export default router

src 下新增 views/MainMenu.vue,MainMenu 主要包含 GameForm 组件。

<template>
<div>
  <b-card-header class="custom-info text-white font-weight-bold">New Game</b-card-header>
  <b-card-body class="h-100">
    <GameForm @form-submitted="handleFormSubmitted"></GameForm>
  </b-card-body>
</div>
</template>

<script>
import GameForm from '../components/GameForm'

export default {
name: 'MainMenu',
components: {
GameForm
},
methods: {
/** Triggered by custom 'form-submitted' event from GameForm child component.
* Parses formData, and route pushes to 'quiz' with formData as query
* @public
*/

handleFormSubmitted(formData) {
const query = formData
query.difficulty = query.difficulty.toLowerCase()
this.$router.push({ name: 'quiz', query: query })
}
}
}
</script>

新增 src/components/GameForm.vue,实现游戏初始设置。

image-20200707125814786

<template>
  <div>
    <LoadingIcon v-if="loading"></LoadingIcon>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-else</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">b-form</span> @<span class="hljs-attr">submit</span>=<span class="hljs-string">"onSubmit"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">b-form-group</span> 
      <span class="hljs-attr">id</span>=<span class="hljs-string">"input-group-number-of-questions"</span>
      <span class="hljs-attr">label</span>=<span class="hljs-string">"Select a number"</span>
      <span class="hljs-attr">label-for</span>=<span class="hljs-string">"input-number-of-questions"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"text-left"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">b-form-input</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"input-number-of-questions"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.number"</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span>
        <span class="hljs-attr">:min</span>=<span class="hljs-string">"minQuestions"</span>
        <span class="hljs-attr">:max</span>=<span class="hljs-string">"maxQuestions"</span>
        <span class="hljs-attr">required</span> 
        <span class="hljs-attr">:placeholder</span>=<span class="hljs-string">"`Between ${minQuestions} and ${maxQuestions}`"</span>
      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">b-form-input</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">b-form-group</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">b-form-group</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"input-group-category"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">b-form-select</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"input-category"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.category"</span>
        <span class="hljs-attr">:options</span>=<span class="hljs-string">"categories"</span>
      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">b-form-select</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">b-form-group</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">b-form-group</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"input-group-difficulty"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">b-form-select</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"input-difficulty"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.difficulty"</span>
        <span class="hljs-attr">:options</span>=<span class="hljs-string">"difficulties"</span>
      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">b-form-select</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">b-form-group</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">b-form-group</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"input-group-type"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">b-form-select</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"input-type"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"form.type"</span>
        <span class="hljs-attr">:options</span>=<span class="hljs-string">"types"</span>
      &gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">b-form-select</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">b-form-group</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">b-button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"custom-success"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">b-button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">b-form</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

</div>
</template>

<script>
import LoadingIcon from './LoadingIcon'
import axios from 'axios'

export default {
components: {
LoadingIcon
},
data() {
return {
// Form data, tied to respective inputs
form: {
number: '',
category: '',
difficulty: '',
type: ''
},
// Used for form dropdowns and number input
categories: [{ text: 'Category', value: '' }],
difficulties: [{ text: 'Difficulty', value: '' }, 'Easy', 'Medium', 'Hard'],
types: [
{ text: 'Type', value: '' },
{ text: 'Multiple Choice', value: 'multiple' },
{ text: 'True or False', value: 'boolean'}
],
minQuestions: 10,
maxQuestions: 20,
// Used for displaying ajax loading animation OR form
loading: true
}
},
created() {
this.fetchCategories()
},
methods: {
fetchCategories() {
axios.get('https://opentdb.com/api_category.php')
.then(resp => resp.data)
.then(resp => {
resp.trivia_categories.forEach(category => {
this.categories.push({text: category.name, value: <span class="hljs-subst">${category.id}</span>})
});
this.loading = false;
})
},
onSubmit(evt) {
evt.preventDefault()
/** Triggered on form submit. Passes form data
* @event form-submitted
* @type {number|string}
* @property {object}
*/

this.$emit('form-submitted', this.form)
}
}
}
</script>

GameForm 组件,主要通过 axios 发起获取全部题目分类请求:

axios.get('https://opentdb.com/api_category.php')

新增 src/components/LoadingIcon.vue,在异步请求数据未返回时,渲染等待图标。

<template>
  <div id="loading-icon" class="h-100 d-flex justify-content-center align-items-center">
    <img src="@/assets/ajax-loader.gif" alt="Loading Icon">
  </div>
</template>

<script>
export default {
name: 'LoadingIcon'
}
</script>

新增 src/assets/ajax-loader.gif 等待动画文件,请参考项目源码。

1.4 运行项目

yarn run serve

image-20200707130702456

2. 答题页面开发

image-20200708083506391

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 router

2.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:<span class="hljs-comment">//opentdb.com/api.php?amount=${this.number}
if (this.category) url += &amp;category=${<span class="hljs-keyword">this</span>.category}
if (this.difficulty) url += &amp;difficulty=${<span class="hljs-keyword">this</span>.difficulty}
if (this.type) url += &amp;type=${<span class="hljs-keyword">this</span>.type}

  axios.<span class="hljs-keyword">get</span>(url)
    .then(resp =&gt; resp.<span class="hljs-keyword">data</span>)
    .then(resp =&gt; {
      <span class="hljs-keyword">if</span> (resp.response_code === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">this</span>.setQuestions(resp)
      } <span class="hljs-keyword">else</span> {
        EventBus.$emit(<span class="hljs-string">'alert-error'</span>, <span class="hljs-string">'Bad game settings. Try another combination.'</span>)
        <span class="hljs-keyword">this</span>.$router.replace({ name: <span class="hljs-string">'home'</span> })
      }
    })
},
<span class="hljs-comment">/** Takes return data from API call and transforms to required object setup. 
 * Stores return in $root.$data.state.
 * <span class="hljs-doctag">@public</span>
 */</span>
setQuestions(resp) {
  resp.results.forEach(qst =&gt; {
    <span class="hljs-keyword">const</span> answers = <span class="hljs-keyword">this</span>.shuffleArray([qst.correct_answer, ...qst.incorrect_answers])
    <span class="hljs-keyword">const</span> question = {
      questionData: qst,
      answers: answers,
      userAnswer: <span class="hljs-literal">null</span>,
      correct: <span class="hljs-literal">null</span>
    }
    <span class="hljs-keyword">this</span>.questions.push(question)
  })
  <span class="hljs-keyword">this</span>.$root.$<span class="hljs-keyword">data</span>.state = <span class="hljs-keyword">this</span>.questions
  <span class="hljs-keyword">this</span>.currentQuestion = <span class="hljs-keyword">this</span>.questions[<span class="hljs-number">0</span>]
  <span class="hljs-keyword">this</span>.loading = <span class="hljs-literal">false</span>
},
<span class="hljs-comment">/** Called on submit.
 * Checks if answer is correct and sets the user answer.
 * Invokes nextQuestion().
 * <span class="hljs-doctag">@public</span>
 */</span>
onAnswerSubmit(answer) {
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.currentQuestion.questionData.correct_answer === answer) {
    <span class="hljs-keyword">this</span>.currentQuestion.correct = <span class="hljs-literal">true</span>
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">this</span>.currentQuestion.correct = <span class="hljs-literal">false</span>
  }
  <span class="hljs-keyword">this</span>.currentQuestion.userAnswer = answer
  <span class="hljs-keyword">this</span>.nextQuestion()
},
<span class="hljs-comment">/** Filters all unanswered questions, 
 * checks if any questions are left unanswered, 
 * updates currentQuestion if so, 
 * or routes to "result" if not.
 * <span class="hljs-doctag">@public</span>
 */</span>
nextQuestion() {
  <span class="hljs-keyword">const</span> unansweredQuestions = <span class="hljs-keyword">this</span>.questions.filter(q =&gt; !q.userAnswer)
  <span class="hljs-keyword">if</span> (unansweredQuestions.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">this</span>.currentQuestion = unansweredQuestions[<span class="hljs-number">0</span>]
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">this</span>.$router.replace({ name: <span class="hljs-string">'result'</span> })
  }
}

}
}
</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>
<span class="hljs-tag">&lt;<span class="hljs-name">b-card-body</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"pt-0"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">hr</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">b-form</span> @<span class="hljs-attr">submit</span>=<span class="hljs-string">"onSubmit"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">b-form-group</span>
      <span class="hljs-attr">label</span>=<span class="hljs-string">"Select an answer:"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"text-left"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">b-form-radio</span> 
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"(ans, index) of question.answers"</span> 
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"index"</span> 
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"answer"</span> 
        <span class="hljs-attr">:value</span>=<span class="hljs-string">"ans"</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-html</span>=<span class="hljs-string">"ans"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">b-form-radio</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">b-form-group</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">b-button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"custom-success"</span>&gt;</span>Submit<span class="hljs-tag">&lt;/<span class="hljs-name">b-button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">b-form</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">b-card-body</span>&gt;</span>

</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

image-20200708083544511

<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

启动成功:

image-20200708083506391

如果能看到该页面,恭喜你,项目到此成功了。

2.3 至此项目目录结构

如果你走丢,请下载源码进行对比:

image-20200708084009828

3 实现最终结果展示页面

image-20200708084853745

再次修改 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 项目结构

image-20200708085222248

项目总结

很感谢您和豆约翰走到了这里,至此我们一个小型的 Vue 项目,全部开发完毕,下一期,豆约翰会带大家见识一个中型的项目,咱们循序渐进,一起加油。

最后

为了将来还能找到我