发布时间:2024-01-25 16:30
为了练习Vue,写了一个小项目,主要内容是答题。
cli3.x
,选择router
即可element
,详见在vue中使用elementUIaxios
import Vue from \'vue\'
import VueRouter from \'vue-router\'
Vue.use(VueRouter)
const Main = () => import(\'@/views/Main\')
const Quiz = () => import(\'@/views/quiz/Quiz\')
const Start = () => import(\'@/views/quiz/Start\')
const routes = [
{
path: \'/\',
redirect: \'/index\',
component: Main,
children: [
{
path: \'/index\',
component: Start
},
{
path: \'/quiz\',
component: Quiz
},
]
},
]
const router = new VueRouter({
mode: \'history\',
base: process.env.BASE_URL,
routes
})
export default router
非常简单的路由设计,其中的路由的加载方式为懒加载。
<div>
<h3>你好,请选择答题范围h3>
<el-checkbox :indeterminate=\"isIndeterminate\" v-model=\"checkAll\" @change=\"handleCheckAllChange\">全选el-checkbox>
<div style=\"margin: 15px 0;\">div>
<el-checkbox-group v-model=\"checkedCities\" @change=\"handleCheckedCitiesChange\">
<el-checkbox @change=\"isDisabled\" v-for=\"city in cities\" :label=\"city\" :key=\"city\">{{ city }}el-checkbox>
el-checkbox-group>
<el-button type=\"primary\" @click=\"start\" :disabled=\"btnDisabled\" style=\"margin-top: 2rem;\">
开始答题
el-button>
div>
export default {
name: \"Start\",
data() {
return {
checkAll: false,
btnDisabled: false,
checkedCities: [\'文科\', \'理科\'],
cities: [\'文科\', \'理科\', \'娱乐\', \'生活\', \'文艺\', \'流行\'],
isIndeterminate: true
};
},
created() {
this.checkedCities = localStorage.checkedWords ? localStorage.checkedWords.split(\',\') : this.checkedCities
},
methods: {
isDisabled() {
this.btnDisabled = this.checkedCities.length === 0
},
handleCheckAllChange(val) {
this.checkedCities = val ? this.cities : [];
this.isIndeterminate = false;
this.isDisabled()
},
handleCheckedCitiesChange(value) {
let checkedCount = value.length;
this.checkAll = checkedCount === this.cities.length;
this.isIndeterminate = checkedCount > 0 && checkedCount < this.cities.length;
},
start() {
this.$set(localStorage, \'checkedWords\', this.checkedCities)
this.$router.push(\'/quiz\')
this.$emit(\'checked\', this.checkedCities, true)
}
}
}
created
函数,在创建组件完成后,检查浏览器缓存localStorage
中是否已经存在选择的题目范围。如果有则直接使用,没有就使用默认选择的。
localStorage
中,是以,
隔开的字符串形式,所以要用split
函数来分割一下。用法详见字符串方法isDisabled
函数是用来判断选中题目种类的个数是否为0,如果是则按钮不可点击。start
函数按钮点击后触发,目的是跳转路由至/quiz
答题页,并将题目种类数组传给父组件Main
Quiz
,即子传父,父传其他子,也可以用过vuex
实现。<el-container style=\"height: 40rem; border: 1px solid #eee;\">
<el-header style=\"background:linear-gradient(to right,#cfd9df,#e2ebf0); height:5rem;\">
<div>
<h2 style=\"margin: 1rem 0 0 40%\">Vue答题h2>
<span style=\"float: right;\">--<a style=\"color: deepskyblue;\"
href=\"https://blog.csdn.net/qq_44888570\">zeda> 制作span>
div>
el-header>
<el-container style=\"height: 100%;\">
<el-aside width=\"15rem\" style=\"background-color: #ccc;height: 100%; padding: 2rem;\">
<h2>历史排行榜h2>
<ol>
<li v-for=\"item in scores\">{{ item }}li>
ol>
el-aside>
<el-main>
<router-view @checked=\"setInfo\" @score=\"setScore\" :info=\"info\">router-view>
el-main>
el-container>
el-container>
布局采用ElementUI
中如下布局
<el-container>
<el-header>Headerel-header>
<el-container>
<el-aside width=\"200px\">Asideel-aside>
<el-main>Mainel-main>
el-container>
el-container>
export default {
name: \"Main\",
data() {
return {
info: [],
scores: localStorage.scores ? localStorage.scores.split(\',\') : []
}
},
methods: {
setInfo(data) {
this.info = data
},
setScore(data) {
this.scores.push(data)
this.sortArr(this.scores)
if (this.scores.length > 10) {
this.scores.splice(10)
}
localStorage.scores = this.scores
},
sortArr(arr) {
return arr.sort((x, y) => y - x)
}
}
}
data
中的info
数组,就是前面Start
组件传过来的数据data
中的scores
,每次打完题之后,Quiz
数组都会将分数传过来。然后将这些分数压入数组,存储到localStorage
中setInfo
函数捕获子组件Start
传过来的数据,并赋值给该组件setScore
函数捕获子组件Quiz
传过来的数据,并压入分数数组,且要排序。如果分数数组的长度超过10,则只截取分数最高的十项sort
函数为数组排序,用法详见数组方法 <div>
<div style=\"height: 5rem;\">
<h2 style=\"float: left;\">score:{{ score }}h2>
<h2 style=\"float: left;margin-left: 5rem;\">hp:<i class=\"el-icon-s-opportunity\" v-for=\"value in hp\">i>h2>
div>
<div>
<span>科目范围:{{ info }}span>
<span style=\"margin-left: 5rem;\">本题属:{{ currentQuiz.school }} <i
class=\"el-icon-arrow-right\">i> {{ currentQuiz.type }}span>
div>
<h2>{{ currentQuiz.quiz }}h2>
<div style=\"width: 30rem;\">
<el-progress :percentage=\"progress.percentage\" :format=\"format\" :stroke-width=\"10\"
:color=\"progress.customColors\">el-progress>
div>
<div>
<el-radio v-for=\"(item,index) in currentQuiz.options\" v-model=\"answer\"
style=\"margin-top: 2rem\"
:label=\"currentQuiz._id + index\"
:class=\"{\'isAnswer\' : isAnswer[index]}\"
border>{{ item }}
el-radio>
div>
<el-button type=\"primary\" @click=\"nextBtn\" :disabled=\"btnDisabled\" style=\"margin-top: 2rem\">nextel-button>
div>
<template>
<div>
<div style=\"height: 5rem;\">
<h2 style=\"float: left;\">score:{{ score }}</h2>
<h2 style=\"float: left;margin-left: 5rem;\">hp:<i class=\"el-icon-s-opportunity\" v-for=\"value in hp\"></i></h2>
</div>
<div>
<span>科目范围:{{ info }}</span>
<span style=\"margin-left: 5rem;\">本题属:{{ currentQuiz.school }} <i
class=\"el-icon-arrow-right\"></i> {{ currentQuiz.type }}</span>
</div>
<h2>{{ currentQuiz.quiz }}</h2>
<div style=\"width: 30rem;\">
<el-progress :percentage=\"progress.percentage\" :format=\"format\" :stroke-width=\"10\"
:color=\"progress.customColors\"></el-progress>
</div>
<div>
<el-radio v-for=\"(item,index) in currentQuiz.options\" v-model=\"answer\"
style=\"margin-top: 2rem\"
:label=\"currentQuiz._id + index\"
:class=\"{\'isAnswer\' : isAnswer[index]}\"
border>{{ item }}
</el-radio>
</div>
<el-button type=\"primary\" @click=\"nextBtn\" :disabled=\"btnDisabled\" style=\"margin-top: 2rem\">next</el-button>
</div>
</template>
<script>
export default {
name: \"Quiz\",
props: {
info: {
type: Array
}
},
data() {
return {
hp: [1, 1, 1],
quizzes: [],
currentQuiz: {},
answer: 0,
score: 0,
btnDisabled: false,
isAnswer: [false, false, false, false],
progress: {
percentage: 100,
cdTimer: null,
customColors: [
{color: \'#f56c6c\', percentage: 30},
{color: \'#e6a23c\', percentage: 60},
{color: \'#5cb87a\', percentage: 100}
],
}
}
},
async created() {
if (this.info.length === 0) {
this.$message({
type: \'error\',
message: \'请先选择答题范围\'
})
return this.$router.replace(\'/index\')
}
await this.fetch()
this.filter()
this.renderQuiz()
},
methods: {
async fetch() {
const res = await this.$http.get(\'/quizzes.json\')
this.quizzes = res.data
},
format(per) {
return `${Math.round(per * 0.1)}s`
},
filter() {
//一共六种题目,如果全选则不需要筛选
if (this.info.length === 6) {
return
}
const newList = this.quizzes.filter(item => {
return this.info.includes(item.school)
})
this.quizzes = newList
},
randomQuiz() {
const currentIndex = Math.round(this.quizzes.length * Math.random())
this.currentQuiz = this.quizzes[currentIndex]
this.quizzes.splice(currentIndex, 1)
},
renderQuiz() {
this.randomQuiz()
this.killProgress()
},
killProgress() {
this.progress.cdTimer && clearInterval(this.progress.cdTimer)
this.progress.percentage = 100
this.answer = 0
this.progress.cdTimer = setInterval(() => {
if (--this.progress.percentage <= 0) {
this.nextBtn()
}
}, 100)
},
//点击next或者到时间
checkAnswer() {
return new Promise(resolve => {
//如果答案正确,则直接下一题,不正确1s延迟后跳转,并且标记出正确答案
if (this.currentQuiz.answer - 1 + \'\' === this.answer[this.answer.length - 1]) {
this.score++
resolve(true)
} else {
this.$set(this.isAnswer, this.currentQuiz.answer - 1, true)
setTimeout(() => {
this.$set(this.isAnswer, this.currentQuiz.answer - 1, false)
this.hp.pop()
resolve(this.hp.length > 0)
}, 1000)
}
})
},
async nextBtn() {
this.btnDisabled = true
clearInterval(this.progress.cdTimer)
if (await this.checkAnswer()) {
this.renderQuiz()
this.btnDisabled = false
} else {
this.$emit(\'score\', this.score, true)
this.$router.push(\'/index\')
}
}
}
}
</script>
<style scoped>
.isAnswer {
border: 2px solid #0f0;
}
</style>
created
函数
info
,即题目种类数组是否为空,如果是空则返回到首页,重新选择。fetch
函数用来发送axios
请求,这里的axios
被笔者挂载到Vue 的原型上,这样便可全局使用。也可以只在该组件中引入axios
async && await
用法详见轻松理解 async 与 awaitmain.js
文件中书写import axios from \"axios\"
Vue.prototype.$http = axios
filter
函数用来过滤题目列表,如果题目列表的长度为6,也就意味着全选,则不用筛选,直接返回。randomQuiz
函数用于随机出题,出题之后,为了防止重复,直接在题目列表中删除此题killProgress
用于管理进度条计时器
answer
值,因为answer
只有1,2,3,4
,所以恢复为0是可以的;恢复进度条百分比nextBtn
函数中完成。也就是说,进度条走完和点击下一题按钮的效果相同checkAnswer
函数用于判定答案的对与错
Promise
用法详见Promise详解1,2,3,4
。咱们选择的答案是0,1,2,3
,所以要在题目正确答案-1或者再咱们的答案+1,都是可以的。label
值不直接绑定索引,而是题目的_id
再加索引值。这是因为vue读取缓存的机制,这道题的选项的label
值如果绑定了1,2,3,4
,下道题也是1,2,3,4
,这样vue会直接将缓存中的四个选项捞出来,而不是重新创建。这就意味着,咱们上一题的选中效果,切换到下一题的时候,依旧存在。this.$set
赋值,可见vue响应式详解true
代表答对了,而是代表hp
还是有的,也就是说可以继续出题;false
代表hp
用完了,不能再继续出题nextBtn
函数,一旦点击按钮或者进度条结束,就把按钮变为不可点击,这是为了屏蔽用户的无效操作,而且多次点击有可能导致计时器的混乱。一旦点击按钮或者进度条结束,就要判定是否继续出题;如果继续,则需要把按钮恢复可点击,调用出题函数;如果结束了,则把分数传给父组件Main
,并且跳转路由至/index
this.hp
为何是一个数组呢?这个数组是用来渲染那个灯泡图标的。isAnswer
,配合选项v-for
渲染来绑定样式类。初始该数组里有4个false
,一旦进度条结束或点击按钮,则会将正确的那个选项绑定样式类。什么?你说选正确了就不用绑定了是吧?不错,但是正确没有延迟1s切换下一题,用户也就看不到这个效果了。.isAnswer {
border: 2px solid #0f0;
}
总的来说,这个项目难度不大,但是一些基础琐碎的知识挺多的,适合练手。
链接:https://pan.baidu.com/s/1HKvGly1H2lpQCkxfm2Onlw
提取码:z1ed