6F - 60 秒算數遊戲 : codepan
Core tech
<aside> 💡 Vue.component, props,emit(value), window.setInterval(), window.clearInterval(), switch...case if
</aside>
總覽
JS
Vue.component
以 Vue 實體為根目錄,將組件以類似積木、樂高的概念堆疊,最大特色在於重複使用,組件中的 data 值為獨立擁護的資料,在 data 的設計上會是「函數」的回傳,不同於根目錄的「物件」
data
选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:data: function () {
return {
count: 0
}
}
寫法 (以此為例)
HTML:
Tips: HTML 建立新的標簽名稱為註冊的元件<start-component>, script 的部分為 start-component 的內容,以字串模組來串接 (type="text/x-template" , id 為JS # 代的名稱)
<start-component>...</start-component>
<script type="text/x-template" id="startComponent">
<!-- content of components -->
</script>
JS:
Tips:註冊一個 start-component 的組件 id 名稱為 startComponent,獨立的 data 內容會以韓式的方式回傳 (return {...}),在組建中也可以觸發自行的行為
Vue.component('start-component', {
template: '#startComponent',
data: {
return {
value:0
},
methods: {
// 要觸發的函式
},
})
Vue.component 的觀念與元件間的溝通
Tips: 組件中存在父子關係,圖為互相呼叫參數的方式
寫法:
props: 將父組件定義的值丟到子組件使用(用於渲染HTML)。
<!-- 於父組件上綁定 -->
<play-component v-if="pageState === 'play'" :pnum1="num1" :pnum2="num2"
:poperator="operator" :pseconds="seconds" :pscore='score'></play-component>
<!-- 於子組件上顯示 -->
<script type="text/x-template" id="playComponent">
<div class="d-flex flex-column justify-content-between" style="height:350px;">
<div class="d-flex justify-content-around" style="width:100%;">
<div class="play-box">
<p class="play-title">60 SECONDS CHALLENGE</p>
<p class="play-subtitle">SCORE</p>
<span style="padding-left:5px;">{{ pscore }}</span>
</div>
<div class="play-time-box">
00:{{ pseconds }}
</div>
</div>
<div class="d-flex justify-content-around" style="max-width:100%;">
<div class="operand">
<span>{{ pnum1 }}</span>
<span style="color:white">{{ poperator }}</span>
<span>{{ pnum2 }}</span>
...
</script>
// 子組件
Vue.component('play-component', {
template: '#playComponent',
data: function () {
return {
panswer: 0
}
},
props: ['pnum1','pnum2','poperator','pseconds','pscore'],
// ...以下先省略... //
})
// 父組件
const vue = new Vue({
el:'#app',
data:{
pageState:'start',
num1:0,
num2:0,
result:[],
operatorArr:['+','-','x','÷'],
operator:'',
seconds:60,
score:0,
timeID:0,
},
})
emit: 將子組件收到的行為或參數回傳父組件使用
HTML: 這邊的@click 與 props的功能類似,行為上的綁定
<!-- 子組件:行為 @keyup.enter 到方法後emit一個變數 'key-enter' 準備給父組件使用 -->
<script type="text/x-template" id="playComponent">
<!-- 中間省略 -->
<div class="answer">
<input type="text" v-model="panswer" onkeyup="value=value.replace(/[^\\-?\\d.]/g,'')"
@keyup.enter="getCaulate()">
</div>
</div>
</div>
</script>
<!-- 父組件:用@方法綁定 $emit的內容,針對此內容在父組件上動作 -->
<play-component
@key-enter='caculation' @key-update-ans="compareAns">
</play-component>
JS: 回傳的內容可能是使用者行為(如點擊或enter)或參數,用@click 事件處理來回傳,寫法為 this.$emit('子物命名',參數)
// 子組件:丟出一個行為與參數
Vue.component('play-component', {
template: '#playComponent',
/* 中間省略 */
methods: {
getCaulate: function() {
this.$emit('key-enter');
this.$emit('key-update-ans', parseFloat(this.panswer))
},
}
})
// 父組件:html上已經用@綁定 @key-update-ans=compareAns,直接做函式計算即可
compareAns(answerValue) {
const vm = this
let currentAns = answerValue
// 以下省略
},
此專案的設計流程
點擊 enter 後會進入遊戲畫面
<!-- 用v-if 判斷現在屬於哪個頁面,顯示不同畫面 -->
<start-component v-if="pageState === 'start'" @to-play="changePlay()"></start-component>
// 子組件:getSate 針對使用者行為丟出一個 to-play
Vue.component('start-component', {
template: '#startComponent',
methods: {
getState: function () {
// 透過 $emit 將自訂的 to-play Event 丟給父元件
this.$emit('to-play');
}
},
})
// 父組件:接收到之後去使用changePlay函式,將頁面的狀態改寫 'play'
methods:{
changePlay() {
const vm = this;
vm.pageState = 'play'
},
進入 play 頁面時,開始倒數計時,並計算第一組答案(this.caculation)
watch: {
pageState: function() {
// 60秒倒數計時同時寫入 timeID 判斷秒數為零時不扣值
const vm = this
if (vm.pageState === 'play') {
vm.timeID = window.setInterval( function()
{ vm.seconds = vm.seconds - 1 }, 1000)
}
// 先寫入第一次的計算式
this.caculation()
}
}
watch: {
seconds: function(value) {
const vm = this;
// 當秒數為 0 時,停止計秒同時轉換畫面
if ( value === 0 ) {
window.clearInterval(vm.timeID);
this.pageState = 'scoreBoard'
}
},
}
隨機產生數字的函式
doRamdom(min,max) {
return Math.floor(Math.random()*(max-min+1))+min;
},
計算隨機數字並符合條件規則
caculation() {
const vm = this
// 特定秒數取得亂數數字
switch(true) {
case vm.seconds <= 60 && vm.seconds >= 41:
vm.num1 = vm.doRamdom(1,9)
vm.num2 = vm.doRamdom(1,9)
break;
case vm.seconds <= 40 && vm.seconds >= 21:
vm.num1 = vm.doRamdom(10,99)
vm.num2 = vm.doRamdom(10,99)
break;
case vm.seconds <= 20:
vm.num1 = vm.doRamdom(100,999)
vm.num2 = vm.doRamdom(100,999)
break;
}
vm.operator = vm.operatorArr[vm.doRamdom(0,3)]
switch(vm.operator) {
case '+':
vm.result.push(vm.num1 + vm.num2);
break;
case '-':
vm.result.push(vm.num1 - vm.num2);
break;
case 'x':
vm.result.push(vm.num1 * vm.num2);
break;
case '÷':
let resultNum = (vm.num1/vm.num2).toFixed(1)
vm.result.push(parseFloat(resultNum));
break;
}
}
計算隨機數字並符合條件規則(處理除法小數點問題)
caculation() {
const vm = this
// 特定秒數取得亂數數字
switch(true) {
case vm.seconds <= 60 && vm.seconds >= 41:
vm.num1 = vm.doRamdom(1,9)
vm.num2 = vm.doRamdom(1,9)
break;
case vm.seconds <= 40 && vm.seconds >= 21:
vm.num1 = vm.doRamdom(10,99)
vm.num2 = vm.doRamdom(10,99)
break;
case vm.seconds <= 20:
vm.num1 = vm.doRamdom(100,999)
vm.num2 = vm.doRamdom(100,999)
break;
}
vm.operator = vm.operatorArr[vm.doRamdom(0,3)]
switch(vm.operator) {
case '+':
vm.result.push(vm.num1 + vm.num2);
break;
case '-':
vm.result.push(vm.num1 - vm.num2);
break;
case 'x':
vm.result.push(vm.num1 * vm.num2);
break;
case '÷':
let resultNum = (vm.num1/vm.num2).toFixed(1)
vm.result.push(parseFloat(resultNum));
break;
}
}
比較使用者輸入的答案是否正確:
<!-- 父組件: -->
<play-component v-if="pageState === 'play'" :pnum1="num1" :pnum2="num2"
:poperator="operator" :pseconds="seconds" :pscore='score'
@key-enter='caculation' @key-update-ans="compareAns"
</play-component>
<!-- 子組件: @keyup.enter 呼叫getCaulate()函式-->
<div class="answer">
<input type="text" v-model="panswer" onkeyup="value=value.replace(/[^\\-?\\d.]/g,'')"
@keyup.enter="getCaulate()">
</div>
// 子組件: getCaulate emit 一個key-update-ans 代一個 data 中的 panswer
Vue.component('play-component', {
template: '#playComponent',
data: function () {
return {
panswer: 0
}
},
props: ['pnum1','pnum2','poperator','pseconds','pscore'],
methods: {
getCaulate: function() {
this.$emit('key-enter');
this.$emit('key-update-ans', parseFloat(this.panswer))
},
}
})
// 父組件: 比較子組件帶來的參數與先前寫入計算結果的陣列值,判斷秒數依規則給分
compareAns(answerValue) {
const vm = this
let currentAns = answerValue
// 抓倒數第二筆的資料(因為下一個資料會同時寫入)
let currentResult = vm.result[vm.result.length - 2]
// 計算答案準備拿來與 input 比較
if ( vm.seconds <= 20 && answerValue === currentResult ) {
vm.score += 5;
}
else if ( answerValue !== currentResult && vm.score > 0) {
vm.score -= 1;
}
else if ( answerValue === currentResult) {
vm.score += 1;
}
else {
return;
}
},
時間到自動轉跳結束頁面並顯示分數
<!-- 父組件: -->
<score-component v-if="pageState === 'scoreBoard'" @to-play="changePlay()"
:pscore='score'></score-component>
<!-- 子組件: -->
<script type="text/x-template" id="scoreboardComponent">
<div class="d-flex flex-column align-items-center justify-content-around score-box">
<div class="d-flex flex-column align-items-center">
<p class="title">60 SECONDS CHALLENGE</p>
<p class="subtitle">
<span style="color:white">- </span>YOUR FINAL SCORE<span style="color:white"> -</span>
</p>
<p class="result">{{ pscore }}</p>
</div>
<div class="button-box" @click="getState()">
<p>RESTART</p>
</div>
</div>
</script>
// 子組件: props 分數顯示在HTML,在呼叫一次 getState 綁定使用者行為
Vue.component('score-component',{
template: '#scoreboardComponent',
props: ['pscore'],
methods: {
getState: function () {
// 透過 $emit 將自訂的 to-play Event 丟給父元件
this.$emit('to-play');
}
},
})
// 父組件: changePlay 將所有的資料初始,讓遊戲重新進行
changePlay() {
const vm = this;
vm.pageState = 'play'
vm.score = 0
vm.seconds = 60
vm.result = []
},