Ma Jiang Mgmt Tool 麻将积分小工具

鉴于每次打牌的时候小伙伴都要拿个电脑来计分,还得开一个很麻烦的软件(so called Numbers on Mac),作为一个搬砖的程序猿自然是不能忍受这种繁琐的行为,于是有了下面这个项目。

Demo 地址:https://rvtea.github.io/MaJiangCounter/

项目地址:https://github.com/Rvtea/MaJiangCounter

引子

原因上面已经说了,麻将瘾太大,忍不住,然后每次看小伙伴计分又很麻烦,这个程序的内在逻辑又很单一,于是结合vue.js来写一个的想法就跃然代码上了。

example

基本原理

这一部分其实还是蛮值得说一说,毕竟撰写这部分学到了不少东西。我们从整个UI结构上来分开说。

输入人名部分

在第一版的成品里其实是不提供这个功能的,毕竟最开始是为了给一起玩儿的小伙伴用的,所以名字都写死了,然而这显然不利于推广使用。于是在第二版更新了UI界面,加入了添加人名的部分。需要解决输入、确认、展示的功能,考虑再三决定放弃名字修改功能,毕竟实在不行就重新刷新嘛,一般这时候都是刚开始进入,不存在初始数据问题。

输入完毕的展示费了点劲儿,这里是通过读取确认按钮之后重写对应div内的html实现的,如果有更好的思路烦请PR,不胜感激。

1
2
3
4
5
6
<div class="three wide column">
<label class="ui mini action input" id="newPlayer_1">
<input type="text" placeholder="10 chars" v-model="newPlayer_1" size="10" maxlength="10">
<button class="mini ui compact basic button" v-on:click="inputPlayer($event)"><i class="checkmark icon"></i></button>
</label>
</div>

以及对应的js代码

1
2
3
4
5
6
7
8
9
10
11
12
inputPlayer: function($event) {
let wholePlayer = [this.newPlayer_1, this.newPlayer_2, this.newPlayer_3, this.newPlayer_4];
let currentPlayer = '#' + event.target.parentElement.parentElement.id;
let currentId = parseInt(currentPlayer.substring(11)) - 1;
if (wholePlayer[currentId] != '') {
$(currentPlayer).text(wholePlayer[currentId]); // avoid using html()
$(currentPlayer).css("font-size", "24px");
$(currentPlayer).css("font-weight", "bold")
} else {
alert("The player name is not allowed empty.");
}
}

输入分数部分

这其实是最麻烦的部分,涉及到几个点。第一是如何优雅地实现既能友好地告知用户输入数字,同时要让用户不必计算总和,只需要自己计算每局的每个人得分即可。第二个则是如何避免其他符号的输入,能通过点击的方式来实现当前局谁赢了谁输了。

输入数字

构想这一部分的设计的时候,我想起来很多时候打开手机端的一些SPA,点击输入的时候,会触发数字键盘。通过万能的stackoverflow和自己不断地测试实现,我找到了办法。如果感兴趣的可以去Mobile Input Types感受一下。另外鉴于输赢不会太大,我在input属性里对长度做了限制。同时因为没有浮点数的存在,纯数字键盘的触发完美地实现了告知用户只需要数字输入即可的目的。

但这只解决了一部分问题,我们还需要确定赢家的归属。因为我们采用的积分规则是输的为正分,赢家为负分。那么如果能输入一个负号在数字前面,不就解决问题了吗?这么想当然是挺好,然而现实情况中我们需要面对很多问题:

  • 数字键盘首先就不能提供负号(但是加号可以通过<input type="number" pattern="[0-9]*">提供)
  • 用户需要计算总分,尤其是对于数学加减法不太好的用户而言,实在痛苦
  • 用户的输入边界情况需要处理,比如其他符号输入

这显然增加了这个设计带来的代码处理的逻辑,作为偷懒成性的我怎么会使用这种设计呢哈哈哈。我机智地想到了通过一个按钮来标识。同时按下按钮会使得输入框无法输入,从而避免了误输入和求和。于是乎代码就有了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="ui form">
<div class="four inline fields">
<div class="field">
<input type="radio" name="win" checked="" tabindex="0" class="hidden">
<input type="tel" placeholder="Number" disabled="" v-model="newRound_1" size="5" maxlength="5" class="numberbox">
</div>
<div class="field">
<input type="radio" name="win" tabindex="1" class="hidden">
<input type="tel" placeholder="Number" v-model="newRound_2" size="5" maxlength="5" class="numberbox">
</div>
<div class="field">
<input type="radio" name="win" tabindex="2" class="hidden">
<input type="tel" placeholder="Number" v-model="newRound_3" size="5" maxlength="5" class="numberbox">
</div>
<div class="field">
<input type="radio" name="win" tabindex="3" class="hidden">
<input type="tel" placeholder="Number" v-model="newRound_4" size="5" maxlength="5" class="numberbox">
</div>
</div>
</div>


1
2
3
4
5
6
7
8
9
10
11
12
13
14
$('input:radio').change(
function() {
let minus_index = checkedIndex();
var box = $('input.numberbox');
for (let i = 0; i < box.length; i++) {
if (i == minus_index) {
$(box[i]).attr("disabled", true);
} else {
$(box[i]).attr("disabled", false);
}
}
}
);

上面代码虽然不多,但是排版的实现和radio按钮的change事件的应用,对我来说都是全新的使用,而disabled值的设定也是非常有学习意义。

展示过往局分部分

展示这一部分其实非常简单哈哈哈,看上去可能是最复杂的逻辑,但是vue.js真的太方便了,直接调用一个循环就完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="roundsInfo" class="ui sixteen column grid" v-if="rounds.length > 0" v-for="(round, index) in rounds">
<div class="middle aligned center aligned two wide column">{{ index+1 }}</div>
<div class="middle aligned center aligned three wide column">
{{round[0]}}
</div>
<div class="middle aligned center aligned three wide column">
{{round[1]}}
</div>
<div class="middle aligned center aligned three wide column">
{{round[2]}}
</div>
<div class="middle aligned center aligned three wide column">
{{round[3]}}
</div>
<div class="two wide column">
<button class="fluid ui icon button" v-on:click="removeRound(index)">
<i class="remove icon"></i>
</button>
</div>
</div>

这背后的工作不少,首先我们需要处理边界case——一局都没开始的时候的显示。所以使用了

1
2
3
4
5
6
7
``` html
<div class="two wide column">
<button class="fluid ui icon button" v-on:click="addNewRound()">
<i class="plus icon"></i>
</button>
</div>

通过这个方法,我们可以把每一局的数据newRound添加到全局的rounds数组来存储。而newRound中只有三个输家的数据,于是需要求和并乘以-1来得到对应的负数作为赢家的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// computed attrs
newRound: function() {
let initData = [
this.newRound_1,
this.newRound_2,
this.newRound_3,
this.newRound_4
];
var finalData = initData.map((x) => {
if (x == '')
return 0;
else
return parseInt(x.trim());
});
let minus_index = checkedIndex();
let sumZero = finalData.reduce((a, b) => a + b, 0);
finalData[minus_index] = -1 * sumZero;
return finalData;
}
// methods
addNewRound: function() {
this.rounds.push(this.newRound);
this.newRound_1 = '';
this.newRound_2 = '';
this.newRound_3 = '';
this.newRound_4 = '';
}

添加数据完成之后,加入添加错了怎么办呢?显然修改功能会非常复杂——不仅要考虑处理输入的问题,还需要考虑数字求和等等问题,那不如我们让用户删掉然后重新输入一遍吧。毕竟修改那么麻烦…(懒惰如我╭(╯^╰)╮)

注意,在第二版里加入了confirm对话框来确保不是误删除,也算是了解了一把confirm函数。

1
2
3
4
5
removeRound: function(index) {
if (confirm("Are you sure to delete this row?")) {
this.rounds.splice(index, 1);
}
}

计算部分

终于到了算分的逻辑了!不少用户可能会困惑为什么会有一个Percent的部分。这个属于一个我们自己玩儿的时候规定的规则,赢得最多的那位在下次吃饭的时候不必出钱,其他人按照比例分摊。所以如果你没有这个规则,那么无视这一列就好了。
至于求和那个,一行代码就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
sumData: function() {
var sumData = [0, 0, 0, 0];
this.rounds.map((x) => {
sumData = sumData.map((s, index) => (s + x[index]));
});
return sumData;
},
sumPercentage: function() {
var sumPer = this.sumData;
if (!arraysEqual(sumPer, [0, 0, 0, 0])) {
var minValue = Math.min.apply(null, sumPer); // might have a few of them
let sumAll = 0;
var newArray = sumPer.map((x) => {
if (x != minValue) {
x = x - minValue;
} else {
x = 0;
}
sumAll += x;
return x;
});
var finalPer = newArray.map((x) => {
return ((x / sumAll) * 100).toFixed(2);
});
return finalPer;
} else {
return [0, 0, 0, 0];
}
}

这时候你也许以为我们的目标已经达到了…然而我们还应该统计一下每个人胡牌的局数,如何显示呢?再在底下增加一列?一来没必要,二来数字混在一起容易看错。结合Semantic UI,我想到可以显示在玩家的名字旁边,用颜色高亮,既显眼也比较好看。

1
2
3
4
5
6
7
8
9
winRoundInfo: function() {
var initial = [0, 0, 0, 0];
this.rounds.map((x) => {
x.map((y, idx) => {
if (y < 0) initial[idx]++;
});
});
return initial;
}

至此,大功告成。

结语

有需求才会有动力,而码代码是为了更好地懒惰。

通过这个小应用,巩固了vue.js和semantic-ui的学习,练习了简单的ES6语法和函数式计算代码,收获颇丰。

又是一个周五,让我们愉快地打麻将吧;-)