前端每日实战:165# 视频演示如何用 Vue 创做一个算术训练程序(内含 3 个视频)

图片描述

效果预览

按下右侧的“点击预览”按钮能够在当前页面预览,点击连接能够全屏预览。javascript

https://codepen.io/comehope/pen/dwzRyQcss

可交互视频

此视频是能够交互的,你能够随时暂停视频,编辑视频中的代码。html

请用 chrome, safari, edge 打开观看。前端

第 1 部分:
https://scrimba.com/p/pEgDAM/ca6wWSkvue

第 2 部分:
https://scrimba.com/p/pEgDAM/c7Zy2AZjava

第 3 部分:
https://scrimba.com/p/pEgDAM/c9R2Gsygit

源代码下载

每日前端实战系列的所有源代码请从 github 下载:github

https://github.com/comehope/front-end-daily-challengesajax

代码解读

本项目能够训练加、减、乘、除四则运算。好比训练加法时,界面给出 2 个数值表示 2 个加数,小朋友心算出结果后大声说出,而后点击“?”按钮查看结果,根据对照的结果,若是计算正确(或错误),就点击绿勾(或红叉),而后再开始下一道测验。界面中还会显示已经作过几道题,正确率是多少。为了加强趣味性,加入了音效,答对时会响起小猫甜美的叫声,答错时响起的是小猫失望的叫声。chrome

页面用纯 css 布局,程序逻辑用 vue 框架编写,用 howler.js 库播放音效。整个应用分红 4 个步骤实现:静态页面布局、加法的程序逻辑、四则运算的程序逻辑、音效处理。

1、页面布局

先建立 dom 结构,整个文档分红 4 部分,.choose-type 是一组多选一按钮,用于选择四则运算的类型,.score 是成绩统计数据,.expression 是一个算式,它也是游戏的主体部分,.judgment 用于判断答题是否正确:

<div id="app">
    <div class="choose-type"></div>
    <div class="score"></div>
    <div class="expression"></div>
    <div class="judgment"></div>
</div>

.choose-type 一共包含 4 个 input[type=radio] 控件,命名为 arithmetic-type,加、减、乘、除 4 种运算类型的值分别为 一、二、三、4,每一个控件后跟随一个对应的label,最终咱们将把 input 控件隐藏起来,而让用户操做 label

<div id="app">
    <div class="choose-type">
        <div class="choose-type">
        <input type="radio" id="addition" name="arithmetic-type" value="1">
        <label for="addition">addition</label>
        <input type="radio" id="subtraction" name="arithmetic-type" value="2">
        <label for="subtraction">subtraction</label>
        <input type="radio" id="multiplication" name="arithmetic-type" value="3">
        <label for="multiplication">multiplication</label>
        <input type="radio" id="division" name="arithmetic-type" value="4">
        <label for="division">division</label>
    </div>
    <!-- 略 -->
</div>

.score 包含 2 个数据,一个是已经作过的题目数,一个是正确率:

<div id="app">
    <!-- 略 -->
    <div class="score">
        <span>ROUND 15</span>
        <span>SCORE 88%</span>
    </div>
    <!-- 略 -->
</div>

.expression 把一个表达式的各部分拆开,以便能修饰表达式各部分的样式。.number 表示等式左边的 2 个运算数,.operation 表示运算符和等号,.show 是一个问号,同时它也是一个按钮,小心算出结果后,点击它,就显示出 .result 元素,展现运算结果:

<div id="app">
    <!-- 略 -->
    <div class="expression">
        <span class="number">10</span>
        <span class="operation">+</span>
        <span class="number">20</span>
        <span class="operation">=</span>
        <span class="button show">?</span>
        <span class="result">30</span>
    </div>
    <!-- 略 -->
</div>

.judgment 包含 2 个按钮,分别是表示正确的绿勾和表示错误的红叉,显示在结果的下方:

<div id="app">
    <!-- 略 -->
    <div class="judgment">
        <span class="button right">✔</span>
        <span class="button wrong">✘</span>
    </div>
</div>

至此,完整的 dom 结构以下:

<div id="app">
    <div class="choose-type">
        <input type="radio" id="addition" name="arithmetic-type" value="1">
        <label for="addition">addition</label>
        <input type="radio" id="subtraction" name="arithmetic-type" value="2">
        <label for="subtraction">subtraction</label>
        <input type="radio" id="multiplication" name="arithmetic-type" value="3">
        <label for="multiplication">multiplication</label>
        <input type="radio" id="division" name="arithmetic-type" value="4">
        <label for="division">division</label>
    </div>
    <div class="score">
        <span>ROUND 15</span>
        <span>SCORE 88%</span>
    </div>
    <div class="expression">
        <span class="number">10</span>
        <span class="operation">+</span>
        <span class="number">20</span>
        <span class="operation">=</span>
        <span class="button show">?</span>
        <span class="result">30</span>
    </div>
    <div class="judgment">
        <span class="button right">✔</span>
        <span class="button wrong">✘</span>
    </div>
</div>

接下来用 css 布局。
居中显示:

body{
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(lightyellow, tan);
}

设置应用的容器样式,黑色渐变背景,子元素纵向排列,尺寸用相对单位 vwem,以便在窗口缩放后能自适应新窗口尺寸:

#app {
    width: 66vmin;
    display: flex;
    flex-direction: column;
    align-items: center;
    box-shadow: 0 1em 4em rgba(0, 0, 0, 0.5);
    border-radius: 2em;
    padding: 8em 5em;
    background: linear-gradient(black, dimgray, black);
    font-family: sans-serif;
    font-size: 1vw;
    user-select: none;
}

布局 .choose-type 区域。隐藏 input 控件,设置 label 为天蓝色:

.choose-type input[name=arithmetic-type] {
    position: absolute;
    visibility: hidden;
}

.choose-type label {
    font-size: 2.5em;
    color: skyblue;
    margin: 0.3em;
    letter-spacing: 0.02em;
}

label 之间加入分隔线:

.choose-type label {
    position: relative;
}

.choose-type label:not(:first-of-type)::before {
    content: '|';
    position: absolute;
    color: skyblue;
    left: -0.5em;
    filter: opacity(0.6);
}

设置 label 在鼠标悬停时变色,当 input 控件被选中时对应的 label 会变色、首字母变大写并显示下划线,为了使视觉效果切换平滑,设置了缓动时间。这里没有使用 text-decoration: underline 设置下划线,是由于用 border 才有缓动效果:

.choose-type label {
    transition: 0.3s;
}

.choose-type label:hover {
    color: deepskyblue;
    cursor: pointer;
}

.choose-type input[name=arithmetic-type]:checked + label {
    text-transform: capitalize;
    color: deepskyblue;
    border-style: solid;
    border-width: 0 0 0.1em 0;
}

.score 区域用银色字,2 组数据之间留出一些间隔:

.score{
    font-size: 2em;
    color: silver;
    margin: 1em 0 2em 0;
    width: 45%;
    display: flex;
    justify-content: space-between;
}

.expression 区域用大字号,各元素用不一样的颜色区分:

.expression {
    font-size: 12em;
    display: flex;
    align-items: center;
}

.expression span {
    margin: 0 0.05em;
}

.expression .number{
    color: orange;
}

.expression .operation{
    color: skyblue;
}

.expression .result{
    color: gold;
}

.show 是等号右边的问号,它同时也是一个按钮,在这里把按钮的样式 .button 独立出来,由于后面还会用到 .button 样式:

.expression .show {
    color: skyblue;
    font-size: 0.8em;
    line-height: 1em;
    width: 1.5em;
    text-align: center;
}

.button {
    background-color: #222;
    border: 1px solid #555;
    padding: 0.1em;
}

.button:hover {
    background-color: #333;
    cursor: pointer;
}

.button:active {
    background-color: #222;
}

设置 .judgment 区域 2 个按钮的样式,它们还共享了 .button 样式:

.judgment {
    font-size: 8em;
    align-self: flex-end;
}

.judgment .wrong {
    color: orangered;
}

.judgment .right {
    color: lightgreen;
}

至此,静态页面布局完成,完整的 css 代码以下:

body{
    margin: 0;
    height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(lightyellow, tan);
}

#app {
    width: 66vw;
    display: flex;
    flex-direction: column;
    align-items: center;
    box-shadow: 0 1em 4em rgba(0, 0, 0, 0.5);
    border-radius: 2em;
    padding: 8em 5em;
    background: linear-gradient(black, dimgray, black);
    font-family: sans-serif;
    font-size: 1vw;
    user-select: none;
}

.choose-type input[name=arithmetic-type] {
    position: absolute;
    visibility: hidden;
}

.choose-type label {
    font-size: 2.5em;
    color: skyblue;
    margin: 0.3em;
    letter-spacing: 0.02em;
    position: relative;
    transition: 0.3s;
}

.choose-type label:not(:first-of-type)::before {
    content: '|';
    position: absolute;
    color: skyblue;
    left: -0.5em;
    filter: opacity(0.6);
}

.choose-type label:hover {
    color: deepskyblue;
    cursor: pointer;
}

.choose-type input[name=arithmetic-type]:checked + label {
    text-transform: capitalize;
    color: deepskyblue;
    border-style: solid;
    border-width: 0 0 0.1em 0;
}

.score{
    font-size: 2em;
    color: silver;
    margin: 1em 0 2em 0;
    width: 45%;
    display: flex;
    justify-content: space-between;
}

.expression {
    font-size: 12em;
    display: flex;
    align-items: center;
}

.expression span {
    margin: 0 0.05em;
}

.expression .number{
    color: orange;
}

.expression .operation{
    color: skyblue;
}

.expression .result{
    color: gold;
}

.expression .show {
    color: skyblue;
    font-size: 0.8em;
    line-height: 1em;
    width: 1.5em;
    text-align: center;
}

.judgment {
    font-size: 8em;
    align-self: flex-end;
}

.judgment .wrong {
    color: orangered;
}

.judgment .right {
    color: lightgreen;
}

.button {
    background-color: #222;
    border: 1px solid #555;
    padding: 0.1em;
}

.button:hover {
    background-color: #333;
    cursor: pointer;
}

.button:active {
    background-color: #222;
}

2、加法的程序逻辑

咱们先用加法把流程跑通,再把加法扩展为四则运算。

引入 vue 框架:

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>

建立一个 Vue 对象:

let vm = new Vue({
    el: '#app',
})

定义数据,round 存储题目数,round.all 表示总共答过了多少道题,round.right 表示答对了多少道题;numbers 数组包含 2 个元素,用于存储等式左边的 2 个运算数,用数组是为了便于后面使用解构语法:

let vm = new Vue({
    ///...略
    data: {
        round: {all: 0, right: 0},
        numbers: [0, 0],
    }
    ///...略
})

定义计算属性,operation 是操做符,目前是加号,result 是计算结果,等于 2 个运算数相加,score 是正确率,开始作第一题时正确率显示为 100%,后续根据实际答对的题数计算正确率:

let vm = new Vue({
    ///...略
    computed: {
        operation: function() {
            return '+'
        },
        result: function() {
            return this.numbers[0] + this.numbers[1]
        },
        score: function() {
            return this.round.all == 1
                ? 100
                : Math.round(this.round.right / (this.round.all - 1) * 100)
        }
    },
    ///...略
})

把数据绑定到 html 模板中:

<div id="app">
    <!-- 略 -->
    <div class="score">
        <span>ROUND {{round.all - 1}}</span>
        <span>SCORE {{score}}%</span>
    </div>
    <div class="expression">
        <span class="number">{{numbers[0]}}</span>
        <span class="operation">{{operation}}</span>
        <span class="number">{{numbers[1]}}</span>
        <span class="operation">=</span>
        <span class="button show">?</span>
        <span class="result">{{result}}</span>
    </div>
    <!-- 略 -->
</div>

至此,页面中的数据都是动态获取的了。

等式右边的问号和结果不该同时显示出来,在用户思考时应显示问号,思考结束后应隐藏问号显示结果。为此,增长一个 isThinking 变量,用于标志用户所处的状态,默认为 true,即进入游戏时,用户开始思考第 1 道题目:

let vm = new Vue({
    ///...略
    data: {
        round: {all: 0, right: 0},
        numbers: [0, 0],
        isThinking: true,
    },
    ///...略
})

isThinking 绑定到 html 模板中,用户思考时只显示问号 .show,不然显示结果 .result 和判断结果正确与否的按钮 .judgment,此处请注意,对于占据同一个视觉位置的元素,用 v-show=false,即 display: none 隐藏,对于占据独立视觉位置的元素,用 visibility: hidden 隐藏:

<div id="app">
    <!-- 略 -->
    <div class="expression">
        <!-- 略 -->
        <span class="button show" v-show="isThinking">?</span>
        <span class="result" v-show="!isThinking">{{result}}</span>
    </div>
    <div class="judgment" :style="{visibility: isThinking ? 'hidden' : 'visible'}">
        <!-- 略 -->
    </div>
</div>

接下来生成随机运算数。建立一个 next() 方法用于开始下一个题目,那么在页面载入后就应执行这个方法初始化第 1 道题目:

let vm = new Vue({
    ///...略
    methods: {
        next: function() {

        },
    },
})

window.onload = vm.next

next() 方法一方面要负责初始化运算数,还要把答过的题目数加1,这里独立出来一个 newRound() 方法是为了方便后面复用它:

let vm = new Vue({
    ///...略
    methods: {
        newRound: function() {
            this.numbers = this.getNumbers()
            this.isThinking = true
        },
        next: function() {
            this.newRound()
            this.round.all++
        },
    },
})

getNumbers() 方法用于生成 2 个随机数,它调用 getRandomNumber() 方法来生成一个随机数,其中 level 参数表示随机数的取值范围,level 为 1 时,生成的随机数介于 1 ~ 9 之间,level 为 2 时,生成的随机数介于 10 ~ 99 之间。为了增长一点加法的难度,咱们把 level 设置为 2:

let vm = new Vue({
    ///...略
    methods: {
        getRandomNumber: function(level) {
            let min = Math.pow(10, level - 1)
            let max = Math.pow(10, level)
            return min + Math.floor(Math.random() * (max - min))
        },
        getNumbers: function() {
            let level = 2
            let a = this.getRandomNumber(level)
            let b = this.getRandomNumber(level)
            return [a, b]
        },
        newRound: function() {
            this.numbers = this.getNumbers()
            this.isThinking = true
        },
        next: function() {
            this.newRound()
            this.round.all++
        },
    },
})

此时,每刷新一次页面,运算数就会跟着刷新,由于每次页面加载都会运行 vm.next() 方法生成新的随机数。
接下来咱们来处理按钮事件,页面中一共有 3 个按钮:问号按钮 .show 被点击后应显示结果;绿勾按钮 .right 被点击后应给答对题的数目加 1,而后进入下一道题;红叉按钮 .wrong 被点击后直接进入下一道题,因此咱们在程序中增长 3 个方法,getResult()answerRight()answerWrong 分别对应上面的 3 个点击事件:

let vm = new Vue({
    ///...略
    methods: {
        ///...略
        getResult: function() {
            this.isThinking = false
        },
        answerRight: function() {
            this.round.right++
            this.next()
        },
        answerWrong: function() {
            this.next()
        },
    },
})

把事件绑定到 html 模板:

<div id="app">
    <!-- 略 -->
    <div class="expression">
        <!-- 略 -->
        <span class="button show" v-show="isThinking" @click="getResult">?</span>
        <!-- 略 -->
    </div>
    <div class="judgment" :style="{visibility: isThinking ? 'hidden' : 'visible'}">
        <span class="button right" @click="answerRight">✔</span>
        <span class="button wrong" @click="answerWrong">✘</span>
    </div>
</div>

至此,加法程序就所有完成了,能够一道又一道题一直作下去。
此时的 html 代码以下:

<div id="app">
    <div class="choose-type">
        <!-- 没有改变 -->
    </div>
    <div class="score">
        <span>ROUND {{round.all - 1}}</span>
        <span>SCORE {{score}}%</span>
    </div>
    <div class="expression">
        <span class="number">{{numbers[0]}}</span>
        <span class="operation">{{operation}}</span>
        <span class="number">{{numbers[1]}}</span>
        <span class="operation">=</span>
        <span class="button show" v-show="isThinking" @click="getResult">?</span>
        <span class="result" v-show="!isThinking">{{result}}</span>
    </div>
    <div class="judgment" :style="{visibility: isThinking ? 'hidden' : 'visible'}">
        <span class="button right" @click="answerRight">✔</span>
        <span class="button wrong" @click="answerWrong">✘</span>
    </div>
</div>

此时的 javascript 代码以下:

let vm = new Vue({
    el: '#app',

    data: {
        round: {all: 0, right: 0},
        numbers: [0, 0],
        isThinking: true,
    },

    computed: {
        operation: function() {
            return '+'
        },
        result: function() {
            return this.numbers[0] + this.numbers[1]
        },
        score: function() {
            return this.round.all == 1
                ? 100
                : Math.round(this.round.right / (this.round.all - 1) * 100)
        }
    },
    
    methods: {
        getRandomNumber: function(level) {
            let min = Math.pow(10, level - 1)
            let max = Math.pow(10, level)
            return min + Math.floor(Math.random() * (max - min))
        },
        getNumbers: function() {
            let level = 2
            let a = this.getRandomNumber(level)
            let b = this.getRandomNumber(level)
            return [a, b]
        },
        newRound: function() {
            this.numbers = this.getNumbers()
            this.isThinking = true
        },
        next: function() {
            this.newRound()
            this.round.all++
        },
        getResult: function() {
            this.isThinking = false
        },
        answerRight: function() {
            this.round.right++
            this.next()
        },
        answerWrong: function() {
            this.next()
        },
    },
})

window.onload = vm.next

3、四则运算的程序逻辑

咱们先来评估一下四种运算在这个程序里会在哪些方面有差别。首先,运算符不一样,加、减、乘、除的运算符分别是“+”、“-”、“×”、“÷”;第二是运算函数不一样,这个不用多说。根据这 2 点,咱们定义一个枚举对象 ARITHMETIC_TYPE,用它存储四种运算的差别,每一个枚举对象有 2 个属性,operation 表明操做符,f() 函数是运算逻辑。另外,咱们再声明一个变量 arithmeticType,用于存储用户当前选择的运算类型:

let vm = new Vue({
    ///...略
    data: {
        ///...略
        ARITHMETIC_TYPE: {
            ADDITION: 1,
            SUBTRACTION: 2,
            MULTIPLICATION: 3,
            DIVISION: 4,
            properties: {
                1: {operation: '+', f: ([x, y]) => x + y},
                2: {operation: '-', f: ([x, y]) => x - y},
                3: {operation: '×', f: ([x, y]) => x * y},
                4: {operation: '÷', f: ([x, y]) => x / y}
            }
        },
        arithmeticType: 1,
    },
})

改造计算属性中关于运算符和计算结果的函数:

let vm = new Vue({
    ///...略
    computed: {
        ///...略
        operation: function() {
            // return '+'
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].operation
        },
        result: function() {
            // return this.numbers[0] + this.numbers[1]
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].f(this.numbers)
        },
        ///...略
    },
})

由于上面 2 个计算属性都用到了 arithmeticType 变量,因此当用户选择运算类型时,这 2 个计算属性的值会自动更新。另外,为了让 ui 逻辑更严密,咱们令 arithmeticType 的值改变时,开始一个新题目:

let vm = new Vue({
    ///...略
    watch: {
        arithmeticType: function() {
            this.newRound()
        }
    }
})

而后,把 arithmeticType 变量绑定到 html 模板中的 input 控件上:

<div id="app">
    <div class="choose-type">
        <input type="radio" id="addition" name="arithmetic-type" value="1" v-model="arithmeticType">
        <label for="addition">addition</label>
        <input type="radio" id="subtraction" name="arithmetic-type" value="2" v-model="arithmeticType">
        <label for="subtraction">subtraction</label>
        <input type="radio" id="multiplication" name="arithmetic-type" value="3" v-model="arithmeticType">
        <label for="multiplication">multiplication</label>
        <input type="radio" id="division" name="arithmetic-type" value="4" v-model="arithmeticType">
        <label for="division">division</label>
    </div>
    <!-- 略 -->
</div>

至此,当选择不一样的运算类型时,表达式的运算符和计算结果都会自动更新为匹配的值,好比选择乘法时,运算符就变为乘号,运算结果为 2 个运算数的乘积。
不过,此时的最明显的问题是,除法的运算数由于是随机生成的,商常常是无限小数,为了更合理,咱们规定这里的除法只作整除运算。再延伸一下,对于减法,为了不差为负数,也规定被减数不小于减数。
解决这个问题的办法是在 ARITHMETIC_TYPE 枚举中添加一个 gen() 函数,用于存储生成运算数的逻辑,gen() 函数接收一个包含 2 个随机数的数组做为参数,对于加法和乘法,直接返回数组自己,减法的 gen() 函数为 gen: ([a, b]) => a >= b ? [a, b] : [b, a],除法的 gen() 函数为 gen: ([a, b]) => [a * b, b],通过如此处理的运算数,就能够实现上面规定的逻辑了。改造后的 ARITHMETIC_TYPE 以下:

let vm = new Vue({
    ///...略
    data: {
        ///...略
        ARITHMETIC_TYPE: {
            ADDITION: 1,
            SUBTRACTION: 2,
            MULTIPLICATION: 3,
            DIVISION: 4,
            pproperties: {
                1: {operation: '+', f: (arr) => arr, gen: ([a, b]) => [a, b]},
                2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a]},
                3: {operation: '×', f: (arr) => arr, gen: ([a, b]) => [a, b]},
                4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b]}
            }
        },
        ///...略
    },
    ///...略
})

而后,在 getNumbers() 中调用 gen() 方法:

let vm = new Vue({
    ///...略
    methods: {
        ///...略
        getNumbers: function() {
            let level = 2
            let a = this.getRandomNumber(2)
            let b = this.getRandomNumber(2)
            // return [a, b]
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
        },
        ///...略
    },
    ///...略
})

至此,减法能够保证差不为负数,除法也能够保证商是整数了。
接下来,咱们来配置训练难度。对大多数人来讲,2 个二位数的加减法不是很难,可是 2 个二位数的乘除法的难度就大多了。在生成随机数时,由于定义了 level=2,因此取值范围固定是 11 ~ 99,咱们但愿可以灵活配置每一个运算数的取值范围,为此,咱们须要再为 ARITHMETIC_TYPE 枚举中增长一个 level 属性,用于表示随机数的取值范围,它是一个包含 2 个元素的数组,分别表示 2 个运算数的取值范围,改造后的 ARITHMETIC_TYPE 以下:

let vm = new Vue({
    ///...略
    data: {
        ///...略
        ARITHMETIC_TYPE: {
            ADDITION: 1,
            SUBTRACTION: 2,
            MULTIPLICATION: 3,
            DIVISION: 4,
            properties: {
                1: {operation: '+', f: ([x, y]) => x + y, gen: (arr) => arr, level: [3, 2]},
                2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a], level: [3, 2]},
                3: {operation: '×', f: ([x, y]) => x * y, gen: (arr) => arr, level: [2, 1]},
                4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b], level: [2, 1]}
            }
        },
        ///...略
    },
    ///...略
})

而后,把 getNumbers() 函数的 level 变量的值改成从枚举 ARITHMETIC_TYPE 中取值:

let vm = new Vue({
    ///...略
    methods: {
        getNumbers: function() {
            let level = this.ARITHMETIC_TYPE.properties[this.arithmeticType].level
            let a = this.getRandomNumber(level[0])
            let b = this.getRandomNumber(level[1])
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
        },
        ///...略
    },
    ///...略
})

如今运行程序能够看到,加减法的 2 个运算数分别是 3 位数和 2 位数,而乘除法的 2 个运算数则分别是 2 位数和 1 位数,你也能够根据本身的须要来调整训练难度。
至此,四则运算的程序逻辑所有完成,此时的 javascript 代码以下:

let vm = new Vue({
    el: '#app',

    data: {
        round: {all: 0, right: 0},
        numbers: [0, 0],
        isThinking: true,
        ARITHMETIC_TYPE: {
            ADDITION: 1,
            SUBTRACTION: 2,
            MULTIPLICATION: 3,
            DIVISION: 4,
            properties: {
                1: {operation: '+', f: ([x, y]) => x + y, gen: (arr) => arr, level: 2},
                2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a], level: 2},
                3: {operation: '×', f: ([x, y]) => x * y, gen: (arr) => arr, level: 1},
                4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b], level: 1}
            }
        },
        arithmeticType: 1,
    },

    computed: {
        operation: function() {
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].operation
        },
        result: function() {
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].f(this.numbers)
        },
        score: function() {
            return this.round.all == 1
                ? 100
                : Math.round(this.round.right / (this.round.all - 1) * 100)
        }
    },
    
    methods: {
        getRandomNumber: function(level) {
            let min = Math.pow(10, level - 1)
            let max = Math.pow(10, level)
            return min + Math.floor(Math.random() * (max - min))
        },
        getNumbers: function() {
            let level = this.ARITHMETIC_TYPE.properties[this.arithmeticType].level
            let a = this.getRandomNumber(level[0])
            let b = this.getRandomNumber(level[1])
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
        },
        newRound: function() {
            this.numbers = this.getNumbers()
            this.isThinking = true
        },
        next: function() {
            this.newRound()
            this.round.all++
        },
        getResult: function() {
            this.isThinking = false
        },
        answerRight: function() {
            this.round.right++
            this.next()
        },
        answerWrong: function() {
            this.next()
        },
    },

    watch: {
        arithmeticType: function() {
            this.newRound()
        }
    }
})

window.onload = vm.next

4、音效处理

引入 howler 库:

<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.1.1/howler.min.js"></script>

声明变量 sound,它有 2 个属性 rightwrong,分别表明回答正确和错误时的音效,属性值是一个 Howl 对象,在构造函数中指定音频文件的 url:

let vm = new Vue({
    ///...略
    data: {
        ///...略
        sound: {
            right: new Howl({src: ['https://freesound.org/data/previews/203/203121_777645-lq.mp3']}),
            wrong: new Howl({src: ['https://freesound.org/data/previews/415/415209_5121236-lq.mp3']})
        },
    },
    ///...略
})

answerRight() 方法和 answerWrong() 方法中分别调用播放声音的 play() 方法便可:

let vm = new Vue({
    ///...略
    methods: {
        ///...略
        answerRight: function() {
            this.round.right++
            this.sound.right.play()
            this.next()
        },
        answerWrong: function() {
            this.sound.wrong.play()
            this.next()
        },
    ///...略
})

如今,当点击绿勾时,就会响起小猫甜美的叫声;当点击红叉时,响起的是小猫失望的叫声。
至此,程序所有开发完成,最终的 javascript 代码以下:

let vm = new Vue({
    el: '#app',

    data: {
        round: {all: 0, right: 0},
        numbers: [0, 0],
        isThinking: true,
        ARITHMETIC_TYPE: {
            ADDITION: 1,
            SUBTRACTION: 2,
            MULTIPLICATION: 3,
            DIVISION: 4,
            properties: {
                1: {operation: '+', f: ([x, y]) => x + y, gen: (arr) => arr, level: [3, 2]},
                2: {operation: '-', f: ([x, y]) => x - y, gen: ([a, b]) => a >= b ? [a, b] : [b, a], level: [3, 2]},
                3: {operation: '×', f: ([x, y]) => x * y, gen: (arr) => arr, level: [2, 1]},
                4: {operation: '÷', f: ([x, y]) => x / y, gen: ([a, b]) => [a * b, b], level: [2, 1]}
            }
        },
        arithmeticType: 1,
        sound: {
            right: new Howl({src: ['https://freesound.org/data/previews/203/203121_777645-lq.mp3']}),
            wrong: new Howl({src: ['https://freesound.org/data/previews/415/415209_5121236-lq.mp3']})
        },
    },

    computed: {
        operation: function() {
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].operation
        },
        result: function() {
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].f(this.numbers)
        },
        score: function() {
            return this.round.all == 1
                ? 100
                : Math.round(this.round.right / (this.round.all - 1) * 100)
        }
    },
    
    methods: {
        getRandomNumber: function(level) {
            let min = Math.pow(10, level - 1)
            let max = Math.pow(10, level)
            return min + Math.floor(Math.random() * (max - min))
        },
        getNumbers: function() {
            let level = this.ARITHMETIC_TYPE.properties[this.arithmeticType].level
            let a = this.getRandomNumber(level[0])
            let b = this.getRandomNumber(level[1])
            return this.ARITHMETIC_TYPE.properties[this.arithmeticType].gen([a, b])
        },
        newRound: function() {
            this.numbers = this.getNumbers()
            this.isThinking = true
        },
        next: function() {
            this.newRound()
            this.round.all++
        },
        getResult: function() {
            this.isThinking = false
        },
        answerRight: function() {
            this.round.right++
            this.sound.right.play()
            this.next()
        },
        answerWrong: function() {
            this.sound.wrong.play()
            this.next()
        },
    },

    watch: {
        arithmeticType: function() {
            this.newRound()
        }
    }
})

window.onload = vm.next

大功告成!

相关文章
相关标签/搜索