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