年会开发日记02 - 红包雨

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

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

📱 先说说手机端部分

手机端截图

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

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

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

生成红包金额代码片段
// 红包金额分配
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上找了一个开源的红包下落的构造函数,然后按照公司的需求改写了一下,直接贴上来吧:

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片段

.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