前端每日實戰:165# 視頻演示如何用 Vue 創作一個算術訓練程序(內含 3 個視頻)

圖片描述

效果預覽

按下右側的“點擊預覽”按鈕可以在當前頁面預覽,點擊鏈接可以全屏預覽。

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

大功告成!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章