年会开发日记02 - 红包雨

鸽了6个月了,终于想起来这篇还没有写完…

年会预备了一个红包雨的活动,虽然到最后也没有用上,但是笔记还是要写的。
整个流程是关注公众号,然后回复 抢红包 返回一个授权地址获取用户授权信息,然后跳转到活动h5页面,抢红包的同时会在大荧幕上实时展示排名数据。

📱 先说说手机端部分

手机端截图

手机端的部分其实很简单,从url上边获取用户的 openID,然后请求接口返回用户信息,这个时候也会返回活动是否开始的状态

  • 活动并没有开始,那么就进入手动进入的页面,用户手动点击按钮进入 活动入口页
  • 活动已经开始则直接进入 预开始页
  • 缺失openID,提示授权失败返回授权页面重新获取授权。

参与用等待大荧幕指令,在大银幕倒数时就可以点击开始按钮进入同步的倒计时页面,同时会请求后端数据,这个时候后端会返回给我该用户被分配到的金额(哈,你没看错,后台已经按照设置的总金额给每个用户分配好了红包金额),然后按照分配到的金额数来判断是否会刷出 18888 等大红包,怕用户错过这个大红包还做了特殊的样式(金色红包),剩下的金额则按照设置的持续时间数来生成具体的红包雨,一般来说会在收到数据的同时生成完红包雨数据,倒计时结束之后就开始下落。

生成红包金额代码片段
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
// 红包金额分配
rainBuild() {
let total = this.info.amount; // 总金额
let lucky = []; // 红包雨数组
// 分配188红包
if (total > 18800) {
total -= 18800;
lucky.push({ amount: 18800 });
}
// 分配88红包
if (total > 8800) {
total -= 8800;
lucky.push({ amount: 8800 });
}
const timer = 10; // 拆分成10组
let data = []; // 临时红包数据
// 红包雨数量限制
const length = Math.max(
Math.ceil(((this.info.duration / 1000) * 4) / timer),
1
);
// 平均每组金额
const amount = total / timer;
// 循环生成每组红包数据
for (let i = 0; i < timer; i++) {
let temp = this.randomAlloc(amount, length, 0, amount); // 传入数据为 金额,数量,最小金额,最大金额
data.push(...temp);
}
// 打乱红包数据
if (lucky.length) {
lucky.forEach(item => {
let random = Math.floor(Math.random() * (data.length - 10));
data.splice(random, 0, item);
});
}
this.data = data;
}
// 金额生成
randomAlloc(total, length, min, max) {
// 首先要判断是否符合 min 和 max 条件
if (min * length > total || max * length < total) {
throw Error(`没法满足最最少 ${min} 最大 ${max} 的条件`);
}
const result = [];
let restValue = total;
let restLength = length;
for (let i = 1; i < length; i++) {
restLength--;
// 这一次要发的数量必须保证剩下的要足最小量
// 同进要保证剩下的不能大于需要的最大量
const restMin = restLength * min;
const restMax = restLength * max;
// 可发的量
const usable = restValue - restMin;
// 最少要发的量
const minValue = Math.max(min, restValue - restMax);
// 以 minValue 为最左,max 为中线来进行随机,即随机范围是 (max - minValue) * 2
// 如果这个范围大于 usable - minValue,取 usable - minValue
const limit = Math.min(usable - minValue, (max - minValue) * 2);
// 随机部分加上最少要发的部分就是应该发的,但是如果大于 max,最大取到 max
const amount = Math.min(
max,
minValue + Math.floor(limit * Math.random())
);
result.push({ amount: amount });
restValue -= amount;
}
result[length - 1] = { amount: Math.floor(restValue) };
return result;
},

下落的具体代码我从Github上找了一个开源的红包下落的构造函数,然后按照公司的需求改写了一下,直接贴上来吧:

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
function luckyMoney(options) {
this.el = options.el; // 容器对象
this.rain = []; // 红包雨数组
this.speed = options.speed; // 红包落下的速度
this.density = options.density; // 红包下落的密度
this.callback = options.callback; // 回调
}
// 创建红包
luckyMoney.prototype.create = function(id, amount) {
const el = this.el, // 容器
lucky = document.createElement("span"); // 创建一个span元素
let flag = true; // 标志为true
lucky.setAttribute("amount", amount); // 设置红包金额
lucky.setAttribute("title", (amount / 100).toFixed(2)); // 设置红包金额
lucky.className = "luckyMoney"; // 设置类名 luckyMoney
if (amount > 1000) lucky.setAttribute("lucky", "lucky");
lucky.style.left =
Math.random() * (el.clientWidth * 0.5) + el.clientWidth * 0.2 + "px"; // 设置 left 初始位置
lucky.style.top = -el.clientHeight / 10 + "px"; // 设置 top 初始位置
el.appendChild(lucky); // 把虚拟节点添加到容器内
this.rain.push(lucky); // 把红包元素添加到红包雨数组内
this.move(lucky); // 开始移动红包元素
// 打开红包
var handler = e => {
// 如果flag为真 -> 红包未点开过
if (flag === true) {
e.target.className = "luckyMoney opened"; // 添加已打开类名 opened
this.callback(e); // 回调
flag = false; // 标志改为false
} else {
return; // 跳出
}
};
// 添加触摸事件
document.addEventListener("touchstart", function(e) {
// 如果被点击的元素类名为 luckyMoney
if (e.target.className === "luckyMoney") {
handler(e); // 触发红包打开事件
} else if (e.target.getAttribute("amount") === "0") {
e.target.className = "luckyMoney luckyMoneyNone"; // 如果红包金额为 0修改类名为 luckyMoneyNone
} else {
return false; // 其它直接返回
}
});
};
// 红包开始下落
luckyMoney.prototype.start = function(data) {
let i = 0; // 计数器
// 按照红包密度时间创建红包
this.timer = setInterval(() => {
// 如果没有超过红包总数
if (i < data.length) {
const id = data[i].id, // 红包ID
amount = data[i].amount; // 包红金额
this.create(id, amount); // 创建红包对象
i++; // 计数器+1
}
}, this.density);
};
// 红包下落停止
luckyMoney.prototype.stop = function() {
clearInterval(this.timer); // 清除计时器
// 清除所有红包的移动计时器
this.rain.forEach(rain => {
clearInterval(rain.timer);
});
};
// 红包移动
luckyMoney.prototype.move = function(rain) {
const el = this.el; // 容器
let diffY = Math.random() / 2 + 0.4, // 垂直上的轻微偏移
diffX = Math.random() / 2; // 水平上的轻微偏移
const amount = rain.getAttribute("amount");
// 特殊红包大于10元重置为缓落
if (amount > 1000) {
diffY = 0.4;
diffX = Math.random() / 10;
}
// 红包移动按照设置的速率
rain.timer = setInterval(() => {
// 如果y轴偏移吵过1.5
if (diffY > 1.5) {
// 设置红包雨的 left 值
rain.style.left =
parseInt(rain.style.left) +
parseInt((diffX * rain.clientHeight) / 30) +
"px";
} else {
// 设置红包雨的 left 值
rain.style.left =
parseInt(rain.style.left) -
parseInt((diffX * rain.clientHeight) / 30) +
"px";
}
// 设置红包雨的 top 值
rain.style.top =
parseInt(rain.style.top) +
parseInt((diffY * rain.clientHeight) / 20) +
"px";
const position = {
top: parseInt(rain.style.top),
left: parseInt(rain.style.left)
};
if (
position.top > el.clientHeight ||
position.left > el.clientWidth ||
position.left < -100
) {
// 超出屏幕过后,清除定时器,删除红包
clearInterval(rain.timer);
el.removeChild(rain);
}
}, this.speed);
};
// 时间停止时清除剩余红包
luckyMoney.prototype.clear = function() {
const el = this.el, // 容器
redItem = el.childNodes;
for (let i = redItem.length - 1; i > -1; i--) {
el.removeChild(redItem[i]);
}
};
export default luckyMoney;

每次点开红包之后会收集金额到 tempMoneytotalMoney中,并且提交到后台然后清空 tempMoney(这边做了1秒的节流操作),这样大银幕就能展示当前轮次的 Top5 用户。

手机端的大部分内容就是这样了,主要是下落这块比较麻烦,其它的都是一些样式的问题,稍微调试一下就行了。

🏮大荧幕部分

大银幕截图

大银幕这块的话就容易很多了,本来是考虑用 WebSocket 来实时传输红包排名的,但是发现还不如轮询简单,所以还是我这边做了1秒间隔的轮询。
获取到数据之后跟新排名数据,界面就会实时刷新了,这边在展示用户排名时做了动画,每次用户新进和排名更替都会进行左右移动,并不只是简单的修改了展示的数据。

用到了transform的偏移量来修改展示的位置,这样就可以通过修改元素次序来打到用户排名更替补间动画了

Stylus片段

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
.rank-box
width 90%
height 300px
max-width 1600px
display flex
justify-content space-around
position absolute
left 50%
bottom 5vh
z-index 5
transform translateX(-50%)
.lucky-box
width 180px
height 260px
line-height 25px
font-size 18px
background url('~assets/img/luckyBox.png') center no-repeat
background-size 100% 100%
padding 20px
border-radius 15px
border #d5b06e 3px solid
box-shadow 10px 10px 32px rgba(black, 0.45)
display flex
justify-content center
align-items center
flex-direction column
position absolute
left 50%
bottom 0
transform translateX(800px)
opacity 1
transition all 0.5s
box-sizing border-box
&:nth-child(1)
transform translateX(-350%)
z-index 10
&:nth-child(2)
transform translateX(-200%)
z-index 9
&:nth-child(3)
transform translateX(-50%)
z-index 8
&:nth-child(4)
transform translateX(100%)
z-index 7
&:nth-child(5)
transform translateX(250%)
z-index 6
&:nth-child(5)~.lucky-box
transform translateX(800px)
z-index 5
opacity 0
.avatar
width (@width / 2)
height @width
border #d5b06e 4px solid
border-radius 50%
margin 0 auto 10px
display block