前一阵子接触 Vue 和 MVVM 思想, 相比起 MVC 思想有质的改变, 前端开发不再深陷于繁琐的 DOM 操作,而是专注于数据(model层)和用户视图(view层), 将业务逻辑与 DOM 操作分离开来, 实现快速开发和高效代码复用. 另外作为扫雷骨灰级玩家, 闲来无事用周六下午的时间做了个扫雷自娱自乐
.
引入的库:
<!-- jQuery -->
<script src="static/lib/jquery.min.js"></script>
<!-- Vue -->
<script src="static/lib/vue.min-2.6.10.js"></script>
<!-- <script src="static/lib/vue-2.6.10.js"></script> -->
<!-- bootstrap -->
<link rel="stylesheet" href="static/lib/bootstrap-3.3.7-dist/css/bootstrap.min.css">
需要说明的是, 日常项目开发中, 使用 Vue 框架时并不提倡使用 jQuery, 这会使得项目开发重回 MVC 模式. 但在本例中 jQuery 只是为了对界面尺寸进行实时调整, 并没有参与到业务逻辑中, 纯 JavaScript 代码也可以实现, 这里使用 jQuery 只是为了省事(有些大材小用了), 即以下代码:
$(document).ready(e => {
// 界面尺寸调整
$(window).resize(e => {
let w = $(".mine-field").outerWidth() / vm.m;
$(".mine-field span").css({
"font-size": w - 5,
});
$(".mine-field span,.mine-field img").css({
"width": w ,
"height": w ,
});
});
});
// 禁用右键快捷菜单
$(document).bind("contextmenu",false);
素材的准备, 有些选择困难症了, 这几张破图改来改去反而花费了不少时间:
样式:
.mine-field {
margin: 0 auto;
padding: 0;
line-height: 0;
}
.mine-field span {
background-size: cover;
background-repeat: no-repeat;
margin: 0;
padding: 0;
line-height: 0;
}
.mine-field span.block {background-image: url("static/images/block.jpg") !important;}
.mine-field span.mine {background-image: url("static/images/mine.jpg");}
.mine-field span.flag {background-image: url("static/images/flag.jpg") !important;}
.mine-field span.bflag {background-image: url("static/images/badflag.jpg") !important;}
.mine-field span.boom {background-image: url("static/images/boom.jpg") !important;}
下面进入正题, 在 div#app 中放置扫雷界面和控制器:
<div class="mine-field" v-for="(mapi,i) in map">
<span v-for="(mapij,j) in mapi" class="" :cnt="mapij.cnt"
@mouseup.stop="open($event,i,j)" @dblclick.stop="clear($event,i,j)"
:style="{
backgroundImage: 'url(\'static/images/' + mapij.cnt.mapijcnt() + '.jpg\')',
}"
:class="{
'empty': mapij.mine == 0,
'mine': mapij.mine == 1,
'block': mapij.open == 0,
'flag': mapij.flag == 1,
'boom': mapij.boom == 1,
'bflag': mapij.bflag == 1,
}">
<img src="static/images/alpha.png">
</span>
</div>
<div class="btn-group timer">
<span class="btn btn-lg timer glyphicon glyphicon-time"></span>
<span class="btn btn-lg timer">{{ timer }}</span>
</div>
<form class="form-inline btn-group controler">
<label class="btn btn-default">m</label>
<input class="btn btn-default" type="number" v-model="m">
<label class="btn btn-default">n</label>
<input class="btn btn-default" type="number" v-model="n">
<label class="btn btn-default">x</label>
<input class="btn btn-default" type="number" v-model="x">
<label class="btn btn-default" @click="init">重置</label>
<span class="btn btn-default glyphicon glyphicon-repeat" @click="init"></span>
</form>
<span class="btn-group pull-right timer">
<span class="btn btn-lg timer glyphicon glyphicon-certificate"></span>
<span class="btn btn-lg timer">{{ cnt }}</span>
</span>
使用 Vue 提供的 class 和 style 绑定 将每个块的视图与属性绑定起来.
初始化:
cnt: 每个块周围的地雷数,
m,n: 行数, 列数
x: 地雷数的均值, 本例采用了不定地雷数的模式, 每个块有地雷的概率为 r = x / ( m × n )
init(){
this.map = [];
this.cnt = 0;
this.stimer = -1;
this.clock = 0;
this.timer = 0;
if(this.m <= 1 || this.n <= 1 || this.x <= 1 ) this.m = this.n = this.x = 10;
if(this.m * this.n <= this.x) this.x = this.m * this.n - 1;
r = this.x / (this.m * this.n);
for( let i = 0 ; i < this.m ; i++ ){
this.map.push([]);
for( let j = 0 ; j < this.n ; j++ ){
let res = r < Math.random();
let mine = 0;
if(!res) {
mine = 1;
this.cnt += 1;
}
this.map[i].push({
mine: mine,
open: 0,
boom: 0,
cnt: -1,
flag: 0,
bflag:0,
});
}
}
// 计算每个块周围地雷个数
for( let i = 0 ; i < this.m ; i++ ){
for( let j = 0 ; j < this.n ; j++ ){
this.map[i][j].cnt = this.count(i,j);
}
}
this.cntempty = this.m * this.n - this.cnt;
},
count(x,y){
if(this.map[x][y]==1) return -1;
let cnt = 0;
for( let i = x-1 ; i <= x+1 ; i++ ){
for( let j = y-1 ; j <= y+1 ; j++ ){
if(i==x&&j==y) continue;
if(i < this.m && i >= 0 && j < this.n && j >= 0){
cnt += this.map[i][j].mine;
}
}
}
return cnt;
},
当单击块时执行open:
右键时执行插旗/取消插旗
左键时打开地雷块, 若周围8个都没有雷,则将周围的块全部打开
cntempty记录剩余地雷数
如果碰巧第一个打开的是地雷, 则会自动重置地图
open(e,x,y){
let el = this.map[x][y];
if(e.which == 3) return this.flag(e,x,y);// 右键
if(e.which != 1) return;// 非左键
if(el.flag == 1) return;// 已插旗
if(el.mine == 1 && this.stimer == -1){
this.init();
return this.open(e,x,y);
}
this.settimer(this.stimer == -1);
if(el.open == 0){
el.open = 1;
if(el.mine == 1){
el.boom = 1;
return this.over(false);
}else{
this.cntempty--;
}
if(el.cnt == 0){
for( let i = x-1 ; i <= x+1 ; i++ ){
for( let j = y-1 ; j <= y+1 ; j++ ){
if(i==x&&j==y) continue;
if(i < this.m && i >= 0 && j < this.n && j >= 0){
this.open(e,i,j);
}
}
}
}
if(this.cntempty==0) this.over(true);
}
},
flag(e,x,y){
let el = this.map[x][y];
if(el.open == 0){ // 未开
el.flag = (el.flag + 1) % 2;
}
},
双击时执行clear: 当周围标记块数与地雷数相等时,自动清除周围未标记的块
clear(e,x,y){
let el = this.map[x][y];
let cntflag = 0;
for( let i = x-1 ; i <= x+1 ; i++ ){
for( let j = y-1 ; j <= y+1 ; j++ ){
if(i==x&&j==y) continue;
if(i < this.m && i >= 0 && j < this.n && j >= 0)
cntflag += this.map[i][j].flag;
}
}
if(cntflag == el.cnt)
for( let i = x-1 ; i <= x+1 ; i++ ){
for( let j = y-1 ; j <= y+1 ; j++ ){
if(i==x&&j==y) continue;
if(i < this.m && i >= 0 && j < this.n && j >= 0 && el.flag == 0){
this.open(e,i,j);
}
}
}
},
下面看看效果图:
动图
失败演示: