首页前端开发VUE从0开始,手把手教你用Vue.js开发一个答题App(下)

从0开始,手把手教你用Vue.js开发一个答题App(下)

时间2023-07-09 07:17:02发布访客分类VUE浏览1031
导读: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 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://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
从0开始,手把手教你用Vue.js开发一个答题App(上) Vue生命周期解析(Vue2、Vue3)

游客 回复需填写必要信息