0前端经验,被任天堂逼着写了个网页

最近Nintendo switch sports 发售了,肥肠好玩!五一长假漫漫,恰好逢上日元大跌,我按捺不住退掉了之前预订但还没发货的实体游戏卡带,准备切到日服eshop 直接购买电子版进行游玩。

上一次在eshop 购买游戏还是上一次,那时候巴西区打折,我切过去购买了一些精品独立游戏,理所当然还有余额没花完以及金币若干。余额没花完不让切服,这是南美区的老传统了,但我身经百战,以我多年穿梭在美、日、美、港、美的丰富经验,凭感觉熟练打开on sale 页面,挑选了一个余额 + 金币能cover 住的小游戏,进行付款。

打完收功后还是无法切服,定睛一看,余额还剩0.01BRL,此时我还没意识到问题的严重性,以为是之前操作有误,继续挑选了一个完全用金币就能cover 的小游戏,进行一个款的付。结束后返回账户页面查看,顿时吐出一口老血,怎么还是剩那个0.01啊啊啊啊!!!

仔细研究了下问题出在金币这儿,和日、美、港不同,在巴西1金币并不等同于1最小货币单位,而是1金币 = 0.05BRL,以我当前还剩0.01BRL 余额来举例,若用金币搭配购买游戏,需要购买价格中小数点最末尾为1(0.01)或6(0.01 + 0.05 = 0.06)的游戏才可能耗尽余额。

但事已至此,余额和金币不够再买任何游戏了,弹尽粮绝,想要从巴西区逃离,只能从某宝购买一张50 or 100BRL 的点卡,来配合花完那个0.01,但如此一来情况就变得复杂了,购买多个游戏,意味着余额与金币瞬息万变,怎么才能恰好花完?只能拿个小本本边买边算。

我整理了一下已知的规则,用java 写了段代码,来辅助计算:

  1. 1金币 = 0.05BRL
  2. 游戏售价 1BRL 送1金币,不满1BRL 的也送1金币
  3. 优先使用余额支付游戏,不足的地方用金币来补齐
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class Eshop {
private BigDecimal balance;
private int coin;
private boolean clean;
private boolean exit;

public Eshop(BigDecimal balance, int coin) {
this.balance = balance;
this.coin = coin;
this.clean = false;
this.exit = false;
}

public void buy(BigDecimal price) {
BigDecimal tempBalance = balance.subtract(price);
int goodCoin = price.setScale(0, BigDecimal.ROUND_UP).intValue();

if (tempBalance.compareTo(new BigDecimal("0")) < 0) {
int needCoin = tempBalance.abs().divide(new BigDecimal("0.05"), 0, BigDecimal.ROUND_UP).intValue();
if (needCoin > coin) {
printResult(true);
return;
}
// 此处计算规则有误 应该先算出抵扣金币后的游戏价格 重新计算金币数
balance = balance.add(new BigDecimal(needCoin).multiply(new BigDecimal("0.05"))).subtract(price);
coin -= needCoin;
} else {
balance = tempBalance;
coin += goodCoin;
}

clean = balance.compareTo(new BigDecimal("0")) == 0;
printResult(false);
}

private void printResult(boolean isExit) {
if (isExit) {
exit = true;
System.out.println("余额不足!");
} else {
String template = "此次购买后余额为["+balance+"],金币为["+coin+"],是否清零["+clean+"]";
exit = clean;
if (!exit) {
BigDecimal high = balance.add(new BigDecimal(coin).multiply(new BigDecimal("0.05")));
String extend = " 可购买价格区间["+balance+"~"+high+"]";
template += extend;
}
System.out.println(template);
}
}

public boolean isExit() {
return exit;
}

public static void process(Scanner sc) {
System.out.println("*** start ***");
// System.out.println("请输入余额:");
// double balance = sc.nextDouble();
// System.out.println("请输入余币:");
// int coin = sc.nextInt();
try {
Eshop eshop = new Eshop(new BigDecimal("100.01"), 9);
while (!eshop.isExit()) {
System.out.println("请输入游戏价格:");
String input = sc.nextLine();
BigDecimal price = new BigDecimal(input);
if (price.compareTo(new BigDecimal("0")) <= 0)
process(sc);
eshop.buy(price);
}
} catch (Exception e) {
process(sc);
}
process(sc);
}

public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
process(sc);
}
}

自测一下

写完这个,我简单测了一下,感觉可行。但是一想到刚买oculus quest2,我玩节奏光剑也是运动,何苦又花钱折腾去买switch sports 呢。于是我转身折腾VR 去了。

第二天早上躺床上刷微博,看到王晓光在夸NS sports 里面的足球多么多么好玩、多么多么有创意,我又开始心动了,但一联想到还困在巴西服呢,就生气。

会不会有人和我一样只有一个账号被困在巴西服出不去?我是不是可以把昨天的java 代码做成一个静态页面,给有需要的人使用?这个念头突然开始在我脑中盘旋。说干就干,我一跃而起,打开电脑。

我从来没正儿八经地写过前端页面,但是javascript 和java 名字都差不多,我把这段代码搬过去用作计算逻辑,用html 写两个输入框代替控制台,这不就成了吗?

正所谓人靠衣装,佛靠金装,页面也得漂漂亮亮的才能显出我技术高超,于是第一件事,我先点开饿了么 element-ui 页面,按照教程引入了一下组件,copy 了一下输入框代码,又copy 了两个按钮代码。看教程示例代码,使用的vue 框架,于是又打开vue 的官方文档,引入了一下vue ,开始学着用vue 来实现我想要的效果。

研究了一个白天,磕磕碰碰,算是把页面功能都实现好了,但是布局好丑啊,于是又打开阮一峰的博客,研究css 怎么写,于是一晚上又过去了。中途还意识到页面在移动端和pc 端显示效果不一样,一开始以为是ui 框架的问题,又去翻了mint ui、cube-ui 的文档,简单重构了一下,发现不是那么回事。。。前端水太深了。。。

最终解决完移动端问题,弄好布局样式,已经是凌晨2点,掏出switch 亲自试验了一下,逃出了巴西服,当然也发现了之前计算规则里遗漏的小bug,顺手修复了。

充了100的点卡

清零完毕,最终按着这个游戏清单购买,成功逃出巴西服

之前做mhgu 百科小程序,租用了腾讯云的一台服务器,本来想的是把这个页面直接丢上去,配了下nginx 配置,没成功。此时太困了,干脆把这个页面丢到了后端项目的静态资源里,简单写了个接口,打包发版。地址为 https://mhgu.top/eshop

算是大功告成了。中间还是遇到了一些有意思的问题,比如js 如何处理浮点数精度、js 里对象引用与深拷贝浅拷贝,统统通过面向搜索引擎编程解决。下面是整个页面的源码(省略了css部分):

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0, user-scalable=no">
<!-- import Vue before Element -->
<script src="js.package/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui@2.15.7/lib/index.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.7/lib/theme-chalk/index.css">
<link rel="shortcut icon" type="image/x-icon" href="pic/monster/jsz.webp">
<title>巴西eshop余额清零计算器</title>
</head>

<style>
...
</style>

<body id="box">

<div id="app">
<div id="title">

</div>
<div id=top>
<div id="left"></div>
<div id="cen">
<div class="goods">
<div v-for="(good, index) in goods" :key="index" class="input-box">
<div class="input-box">
<p class="p-class"><i class="el-icon-s-goods"></i> 游戏{{index + 1}}价格(BRL):</p>
<el-input class="input-class" class="top-in" v-model="good.price" oninput="value=value.replace(/[^\d\.]/g,'')" maxLength='9' placeholder="请输入" :disabled="good.init"></el-input>
</div>
<div>
<el-button class="clear" type="warning" plain @click="resetOne" v-if="good.clear"><i class="el-icon-delete"></i> 清除</el-button>
</div>
</div>
</div>

<div id="inits">
<div id="balance" class="input-box">
<p class="p-class"><i class="el-icon-bank-card"></i> 余额(BRL):</p>
<el-input class="input-class-last" v-model="balance" oninput="value=value.replace(/[^\d\.]/g,'')" maxLength='9' placeholder="请输入初始余额" :disabled="init"></el-input>
</div>
<div id="coin" class="input-box">
<p class="p-class"><i class="el-icon-coin"></i> 金币:</p>
<el-input class="input-class-last" v-model="coin" oninput="value=value.replace(/[^\d]/g,'')" maxLength='9' placeholder="请输入初始金币数" :disabled="init"></el-input>
</div>
</div>
</div>
<div id="right"></div>
</div>

<div id=mid>
<p v-if="msg0">可购买价格区间 {{balance}}~{{realBalance}} (BRL) 的游戏</p>
<p v-if="msg1">余额不足!</p>
<p v-if="msg2" class="msg2"> *** 恭喜余额已清零! *** </p>
</div>

<div id=bot>
<el-button type="danger" plain @click="resetAll"><i class="el-icon-refresh-left"></i> 全部重置</el-button>
<el-button type="primary" plain @click="startBuy" :disabled="cannotBuy"><i class="el-icon-shopping-cart-2"></i> {{start}}</el-button>
</div>

<div id="explain">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span><h3>使用说明</h3></span>
<el-button style="float: right; padding: 3px 0" type="text"></el-button>
</div>
<div class="text item">
- 本页面所有货币单位均为巴西雷亚尔(BRL)
</div>
<div class="text item">
- 输入eshop余额与金币数量,点击“开始”按钮开始
</div>
<div class="text item">
- 依次输入想要购买的游戏价格,并点击“购买”,将自动计算余额与金币数
</div>
<div class="text item">
- 每次购买遵循:优先使用余额,不足部分使用金币补齐,1金币=0.05BRL
</div>
<div class="text item">
- 可点击“清除”,撤销上一次购买
</div>
<div class="text item">
- 当余额为0时计算结束
</div>
<div class="text item">
- 点击“全部重置”删除所有记录
</div>

<div class="text item">
- 联系我:<a href="mailto:thbb268@live.com">thbb268@live.com</a>
</div>

</el-card>
</div>
</div>
</body>

<script>
var floatObj = function () {
/*
* 判断obj是否为一个整数
*/
function isInteger(obj) {
return Math.floor(obj) === obj
}

/*
* 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100
* @param floatNum {number} 小数
* @return {object}
* {times:100, num: 314}
*/
function toInteger(floatNum) {
var ret = {times: 1, num: 0};
if (isInteger(floatNum)) {
ret.num = floatNum;
return ret
}
var strfi = floatNum + '';
var dotPos = strfi.indexOf('.');
var len = strfi.substr(dotPos + 1).length;
var times = Math.pow(10, len);
var intNum = parseInt(floatNum * times + 0.5, 10);
ret.times = times;
ret.num = intNum;
return ret
}

/*
* 核心方法,实现加减乘除运算,确保不丢失精度
* 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
*
* @param a {number} 运算数1
* @param b {number} 运算数2
* @param op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
*
*/
function operation(a, b, op) {
var o1 = toInteger(a);
var o2 = toInteger(b);
var n1 = o1.num;
var n2 = o2.num;
var t1 = o1.times;
var t2 = o2.times;
var max = t1 > t2 ? t1 : t2;
var result = null;
switch (op) {
case 'add':
if (t1 === t2) { // 两个小数位数相同
result = n1 + n2
} else if (t1 > t2) { // o1 小数位 大于 o2
result = n1 + n2 * (t1 / t2)
} else { // o1 小数位 小于 o2
result = n1 * (t2 / t1) + n2
}
return result / max;
case 'subtract':
if (t1 === t2) {
result = n1 - n2
} else if (t1 > t2) {
result = n1 - n2 * (t1 / t2)
} else {
result = n1 * (t2 / t1) - n2
}
return result / max;
case 'multiply':
result = (n1 * n2) / (t1 * t2);
return result;
case 'divide':
result = (n1 / n2) * (t2 / t1);
return result
}
}

// 加减乘除的四个接口
function add(a, b) {
return operation(a, b, 'add')
}

function subtract(a, b) {
return operation(a, b, 'subtract')
}

function multiply(a, b) {
return operation(a, b, 'multiply')
}

function divide(a, b) {
return operation(a, b, 'divide')
}

// exports
return {
add: add,
subtract: subtract,
multiply: multiply,
divide: divide
}
}();

const rate = 0.05

const initData = {
start: '开始',
balance: '',
coin: '',
goods: [],
realBalance: '',
msg0: false,
msg1: false,
msg2: false,
records: [],
init: false,
cannotBuy: false
}

function calRealBalance(balance, coin) {
var coinPrice = floatObj.multiply(coin, rate)
return floatObj.add(balance, coinPrice)
}

function getCoin(price) {
var temp = Math.floor(price)
if (temp == price) {
return temp
}
return temp + 1
}

var vm = new Vue({
el: '#app',
data: JSON.parse(JSON.stringify(initData)),
methods: {
startBuy: function() {
this.start = '购买'
this.init = true
this.goods.push({price: '', init: false, clear: false})
var currentIndex = this.goods.length - 1
this.records.push({index: currentIndex, balance: this.balance, coin: this.coin})
if (currentIndex > 0) {
var goodIndex = currentIndex - 1
var goodPrice = this.goods[goodIndex].price

if (!goodPrice) {
this.goods.pop()
this.records.pop()
this.$message({
message: '请输入游戏价格!',
type: 'warning'
})
return
}

var goodCoin = getCoin(goodPrice)
var tempBalance = floatObj.subtract(this.balance, goodPrice)
if (tempBalance < 0) {
tempBalance = floatObj.subtract(goodPrice, this.balance)
var needCoin = floatObj.divide(tempBalance, rate)
needCoin = getCoin(needCoin)
if (needCoin > this.coin) {
// 终止 无法计算
// this.msg1 = true
this.goods.pop()
this.records.pop()
this.$message({
message: '余额不足!',
type: 'warning'
})
return
} else {
var coinPrice = floatObj.multiply(needCoin, rate)
var needBalance = floatObj.subtract(goodPrice, coinPrice)
if (needBalance > 0) {
this.balance = floatObj.subtract(this.balance, needBalance)
} else {
needBalance = 0
}
this.coin = floatObj.subtract(this.coin, needCoin)
goodCoin = getCoin(needBalance)
this.coin = floatObj.add(this.coin, goodCoin)
this.goods[goodIndex].init = true
this.goods[goodIndex].clear = true
if (goodIndex > 0) {
this.goods[goodIndex-1].clear = false
}
}
} else {
this.balance = tempBalance
this.coin = floatObj.add(this.coin, goodCoin)
this.goods[goodIndex].init = true
this.goods[goodIndex].clear = true
if (goodIndex > 0) {
this.goods[goodIndex-1].clear = false
}
}
}

if (this.balance == 0) {
// 终止 可以计算
this.msg2 = true
this.cannotBuy = true
this.$message({
message: '恭喜你,余额已清零!',
type: 'success'
})
}
this.realBalance = calRealBalance(this.balance, this.coin)
this.msg0 = true
},
resetAll: function() {
const temp = JSON.parse(JSON.stringify(initData));
Object.assign(this.$data, temp);
},
resetOne: function() {
this.goods.pop()
this.goods.pop()
this.goods.push({price: '', init: false, clear: false})
var currentIndex = this.goods.length - 1
if (currentIndex > 0) {
this.goods[currentIndex-1].clear = true
}

var record = this.records.pop()
this.balance = record.balance
this.coin = record.coin
this.realBalance = calRealBalance(this.balance, this.coin)
this.msg2 = false
this.cannotBuy = false
}
}
})
</script>
</html>

最后,NS sports 真的好好玩哦,我手臂已经疼得抬不起来了~~~


0前端经验,被任天堂逼着写了个网页
https://honosv.github.io/2022/05/04/0前端经验,被任天堂逼着写了个网页/
作者
Nova
发布于
2022年5月4日
许可协议