21

图片描述

效果预览

按下右侧的“点击预览”按钮可以在当前页面预览,点击链接可以全屏预览。

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

可交互视频

此视频是可以交互的,你可以随时暂停视频,编辑视频中的代码。

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

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

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

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

源代码下载

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

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

代码解读

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

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

一、页面布局

先创建 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 种运算类型的值分别为 1、2、3、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;
}

二、加法的程序逻辑

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

引入 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

三、四则运算的程序逻辑

我们先来评估一下四种运算在这个程序里会在哪些方面有差异。首先,运算符不同,加、减、乘、除的运算符分别是“+”、“-”、“×”、“÷”;第二是运算函数不同,这个不用多说。根据这 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

四、音效处理

引入 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

大功告成!


comehope
9.4k 声望14.4k 粉丝

💯累计分享170+个项目💯