• 来开发一个比较规整的九宫格抽奖~


    本文书写✍️耗时 30min
    建议阅读时长 10min 🏄🏽‍♂️

    定义一下组件接口 🥡

    • 奖项数据是必不可少的 「九宫格抛弃抽奖按钮占去一个外,还剩下8个」
    • 抽奖运动的时间
    • 抽奖完成之后的回调
    • 是否需要自定义概率
    type Tuple8 = [TItem, ...TItem[]] & { length: 8 };
    type CallbackType = (arg: LDataType) => void;
    
    // props type
    interface LType {
      data: Tuple8;
      time?: number;
      useCustomProbability?: boolean;
      callback?: CallbackType;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    至于奖项数组,每一个奖项的属性则定义为

    • 奖品id 「必要」
    • 奖品描述 「必要」
    • 奖品抽中概率 「非必要」
    • 背景色、图片 「这些样式先忽略哈」
    interface LDataType {
      id: string | number;
      name: string;
      probability?: number;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    先简单画一下页面吧 🏂

    页面结构

    奖励项只有八个,我们怎么完成九宫格的布局呢 ?
    嘿嘿,我们ta们中间硬塞一个数据不就OK了么

    import classNames from 'classnames';
    import React, { useMemo } from 'react';
    
    const Lottery = (props: LType) => {
      
      const realViewData = useMemo(() => {
        return [
          ...props.data.slice(0, 4),
          {
            id: '__btn__',
            name: '抽奖',
          },
          ...props.data.slice(4),
        ];
      }, [props.data]);
      
      return (
        
    {realViewData.map((item) => { return (
    {item.name}
    ); })}
    ); };
    • 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

    搞点小样式

    先写好base scss 「九宫格先搞个默认框高 300」

    @mixin lottery-base($lottery_width:300px) {
      display: flex;
      width: $lottery_width;
      height: $lottery_width;
      flex-wrap: wrap;
      justify-content: space-between;
      align-self: space-between;
    
      .lottery-item {
        text-align: center;
        line-height: $lottery_width/3 - 10px;
        width: $lottery_width/3 - 10px;
        height: $lottery_width/3 - 10px;
        border-radius: 5px;
        background-color: rgb(222, 220, 220);
      }
    
      .is-btn {
        background-color: rgb(33, 194, 140);
        color: azure;
        cursor: pointer;
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    再利用媒体查询在不同宽度下重新赋值

    @media screen and (min-width: 1160px) {
      .lottery {
        @include lottery-base(500px)
      }
    }
    
    @media screen and (max-width: 1160px) {
      .lottery {
        @include lottery-base(420px)
      }
    }
    
    @media screen and (max-width: 820px) {
      .lottery {
        @include lottery-base(360px)
      }
    }
    
    @media screen and (max-width: 768px) {
      .lottery {
        @include lottery-base(300px)
      }
    }
    
    @media screen and (max-width: 390px) {
      .lottery {
        @include lottery-base(250px)
      }
    }
    
    
    • 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

    蛙,页面出来啦!!!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wcZ5ajwe-1669304260237)(https://img-blog.csdnimg.cn/img_convert/ac81a0c1403b173a93febe65fb422ed4.png#averageHue=#e4e3e3&clientId=u7bf87577-e992-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u52f10942&margin=[object Object]&name=截屏2022-11-24 下午6.46.17.png&originHeight=1098&origin &originalType=binary&ratio=1&rotation=0&showTitle=false&size=79765&status=done&style=none&taskId=u29bcb7ab-684e-4b5a-8d3d-9c678e04ba9&title=]
    )

    完善一下基本逻辑 🥊

    结构样式处理完了,接下来该处理动画了
    动画的处理方案,我们采用最传统方式。按照九宫格的顺时针方向不断的给小盒子设置一个active样式类,令其高亮「古老的干掉他人仅留自己」

    声明8个状态用于对应小盒子的active状态

      const [prizeActiveState, setPrizeActiveState] = useState(
        props.data.reduce(
          (pre, cur) => ({
            ...pre,
            ['active' + cur.id]: false,
          }),
          {},
        ),
      );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    将元素与状态进行绑定到一块吧

    active: item.id !== ‘btn’ && prizeActiveState[active${item.id}]

    start(item.id)} key={item.id} > {item.name}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    样式

    .active {
        background-color: rgb(227, 248, 121);
      }
    
    • 1
    • 2
    • 3

    开始跑动画咯

    先别着急跑,有个小问题。我们需要根据九宫格的转动方向先定义好转动的路径

    // 顺时针
    const path = [0, 1, 2, 4, 7, 6, 5, 3];
    
    • 1
    • 2

    解释:当前path中每项的值是真正的奖项数据在其原数组的索引位置。
    即:第一次 0 号位置的奖项, 然后是 1 号,再是 2 号,接下来是 4 号
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h7efk9BE-1669304235843)(https://img-blog.csdnimg.cn/img_convert/5675dd5fadb8d2b8d8823e0a48bae696.png#averageHue=#e9e7e7&clientId=u7bf87577-e992-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ud9914c02&margin=[object Object]&name=截屏2022-11-24 下午7.02.23.png&originHeight=1144&origin &originalType=binary&ratio=1&rotation=0&showTitle=false&size=76973&status=done&style=none&taskId=u3689c2a8-a409-4986-a29b-016d9fdcd19&title=]
    )
    根据这个信息,我们就可以定位到这个奖项所对应的active状态, 从而做干掉别人仅留自己的操作

    setPrizeActiveState(
            props.data.reduce((pre, cur) => {
              if (cur.id === props.data[path[curIndex]].id) {
                return {
                  ...pre,
                  ['active' + cur.id]: true,
                };
              } else {
                return {
                  ...pre,
                  ['active' + cur.id]: false,
                };
              }
            }, {}),
          );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    有了这些我们就可以跑一个定时器,进行轮循了

      const start = (id: string | number) => {
        if (id !== '__btn__') return;
     
        const path = [0, 1, 2, 4, 7, 6, 5, 3];
        let curIndex = 0;
        let stop = false;
    
        setTimeout(() => {
          stop = true;
        }, props.time || 3000);
    
        const intervalId = setInterval(() => {
          if (curIndex > 7) curIndex = 0;
    
          if (stop) 
            clearInterval(intervalId);
      
    
          setPrizeActiveState(
            props.data.reduce((pre, cur) => {
              if (cur.id === props.data[path[curIndex]].id) {
                return {
                  ...pre,
                  ['active' + cur.id]: true,
                };
              } else {
                return {
                  ...pre,
                  ['active' + cur.id]: false,
                };
              }
            }, {}),
          );
    
          curIndex++;
        }, 100);
      };
    
      return (
        
    {realViewData.map((item) => { return (
    start(item.id)} key={item.id} > {item.name}
    ); })}
    );
    • 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

    喜大普奔,终于跑起来了
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mGiio0PT-1669304280105)(https://img-blog.csdnimg.cn/img_convert/250b6be28bc69fc0d591637ebf6e7226.gif#averageHue=#e8e7e7&clientId=u7bf87577-e992-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ud934b016&margin=[object Object]&name=Nov-24-2022 19-22-00.gif&originHeight=496&origin &originalType=binary&ratio=1&rotation=0&showTitle=false&size=179627&status=done&style=none&taskId=uafdf7c3c-ff82-44b2-bf05-a32205a7aed&title=]
    )

    不过有两个问题

    1. 因为我们运动的时间是固定的。所以导致动画每次都会停在一个固定的位置🥲
    2. 第二个问题就因为抽奖的点击时间可以在动画过程中继续进行点击操作,导致动画紊乱的问题

    ps: 第一个问题放到概率那处理就好,先来处理比较简单的

    第二个问题比较好搞,就是加个开关呗

      const flag = useRef(true);
    
    	const start = (id: string | number) => {
         if (!flag.current) return;
    
        flag.current = false;
    
        // ...
    
        const intervalId = setInterval(() => {
          if (curIndex > 7) curIndex = 0;
    
          if (stop) {
            flag.current = true;
            clearInterval(intervalId);
          }
    
          // ...
    	
        }, 100);
      }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    处理一下概率问题 ⚽️

    好了,到这基本是大局已定了。只剩下概率问题

    正规

    想一下🤔️,不做自定义概率。只保证在8个奖项中抽中每个的概率为1/8这样怎么写呢
    很简单:

    Math.floor(Math.random() * props.data.length);
    
    • 1

    来加入逻辑中

      const start = (id: string | number) => {
        if (id !== '__btn__') return;
        if (!flag.current) return;
    
        flag.current = false;
    
        const path = [0, 1, 2, 4, 7, 6, 5, 3];
        let curIndex = 0;
        let stop = false;
    
        // +++
        const luckyRewardsIndex = Math.floor(Math.random() * path.length);
    
        setTimeout(() => {
          stop = true;
        }, props.time || 3000);
    
        const intervalId = setInterval(() => {
          if (curIndex > 7) curIndex = 0;
    
          // +++
          if (stop && curIndex === luckyRewardsIndex) {
            flag.current = true;
            clearInterval(intervalId);
    
            if (props.callback) {
              (props.callback as CallbackType)(props.data[path[curIndex]]);
            }
          }
    
          setPrizeActiveState(
            props.data.reduce((pre, cur) => {
              if (cur.id === props.data[path[curIndex]].id) {
                return {
                  ...pre,
                  ['active' + cur.id]: true,
                };
              } else {
                return {
                  ...pre,
                  ['active' + cur.id]: false,
                };
              }
            }, {}),
          );
    
          curIndex++;
        }, 100);
      };
    
    • 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

    这样一个正规的抽奖组件就完成了
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eo8ZdukR-1669304302870)(https://img-blog.csdnimg.cn/img_convert/a988283871dbc9c1c4cc1c15ac2472e0.gif#averageHue=#e9e9df&clientId=u7bf87577-e992-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ufd36d7e7&margin=[object Object]&name=Nov-24-2022 19-35-20.gif&originHeight=496&origin &originalType=binary&ratio=1&rotation=0&showTitle=false&size=163800&status=done&style=none&taskId=ub7428d44-79f5-4026-95b7-d4860fce0bd&title=]
    )

    自定义概率

    比如 一个数据 【苹果🍎,香蕉🍌,梨🍐】
    要求随机抽,并且抽中苹果🍎的概率要达到80%,其他各10%。这要啷个搞么

    其实也是很简单

    构造一个临时数组
    【苹果🍎80,香蕉🍌10,梨🍐*10】=> 利用Math.random()*100去随机取一下

    其实就是小学概率问题「扔一个球,仍到三个区间的概率各是多少」
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aipgsPRW-1669304319408)(https://img-blog.csdnimg.cn/img_convert/5bb6b109cb0bfca60ee2eb4fefc9dfc1.png#averageHue=#fdfdfd&clientId=u7bf87577-e992-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u4191e6d5&margin=[object Object]&name=截屏2022-11-24 下午7.58.32.png&originHeight=1544&origin &originalType=binary&ratio=1&rotation=0&showTitle=false&size=131587&status=done&style=none&taskId=u4833c042-85a1-4a69-8060-927224aaed6&title=]
    )

    来写一下逻辑

     const calCustomProbabilityIndex = () => {
        const handleData = props.data;
        let tempArr: number[] = [];
        const notHandleItems = [];
        let surplus = 1;
    
        for (let i = 0; i < handleData.length; i++) {
          if (handleData[i].probability === 0) continue;
          if (handleData[i].probability) {
            surplus = surplus - (handleData[i].probability as number);
            tempArr = [
              ...tempArr,
              ...Array(
                Math.floor((handleData[i].probability as number) * 100),
              ).fill(i),
            ];
          } else {
            notHandleItems.push(i);
          }
        }
    
        if (surplus > 0) {
          notHandleItems.forEach((item) => {
            tempArr = [
              ...tempArr,
              ...Array(
                Math.floor(Math.floor((surplus / notHandleItems.length) * 100)),
              ).fill(item),
            ];
          });
        }
    
        return tempArr[Math.floor(Math.random() * tempArr.length)];
      };
    
    • 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

    加入到start方法中

      const start = (id: string | number) => {
        if (id !== '__btn__') return;
        if (!flag.current) return;
    
        flag.current = false;
    
        const path = [0, 1, 2, 4, 7, 6, 5, 3];
        let curIndex = 0;
        let stop = false;
        let luckyRewardsValue: number;
    
        // ++++
        if (props.useCustomProbability) {
          // +++
          luckyRewardsValue = calCustomProbabilityIndex();
        }
    
        const luckyRewardsIndex = props.useCustomProbability
          ? path.findIndex((item) => item === luckyRewardsValue)
          : Math.floor(Math.random() * path.length);
    
        setTimeout(() => {
          stop = true;
        }, props.time || 3000);
    
        const intervalId = setInterval(() => {
          if (curIndex > 7) curIndex = 0;
    
          if (stop && curIndex === luckyRewardsIndex) {
            flag.current = true;
            clearInterval(intervalId);
    
            if (props.callback) {
              (props.callback as CallbackType)(props.data[path[curIndex]]);
            }
          }
    
          setPrizeActiveState(
            props.data.reduce((pre, cur) => {
              if (cur.id === props.data[path[curIndex]].id) {
                return {
                  ...pre,
                  ['active' + cur.id]: true,
                };
              } else {
                return {
                  ...pre,
                  ['active' + cur.id]: false,
                };
              }
            }, {}),
          );
    
          curIndex++;
        }, 100);
      };
    
    • 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

    来将代金券 1 和 2 的概率调整为 50%,即只能抽中代金券 1 和 2

    const data = [
      {
        id: 1,
        name: '代金券1',
        probability: 0.5,
      },
      {
        id: 2,
        name: '代金券2',
        probability: 0.5,
      },
      {
        id: 3,
        name: '代金券3',
      }
      // ....
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    效果
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLTzDzk9-1669304337343)(https://img-blog.csdnimg.cn/img_convert/6fe0e4f790fb559d8b2e0780c9b2ca6d.gif#averageHue=#e9e7e7&clientId=u7bf87577-e992-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u6b9b8f71&margin=[object Object]&name=Nov-24-2022 20-06-08.gif&originHeight=496&origin &originalType=binary&ratio=1&rotation=0&showTitle=false&size=171667&status=done&style=none&taskId=u1ad9bed7-8ac3-4b47-bb35-1d848cd49d7&title=]
    )
    打完收工!!!

    写在最后

    虽然自己一直的业务方向是用户增长、营销等领域,但是最近有将近一年半的时间都在搞效率平台相关的开发了。最近正好闲下来,把以前的东西归纳整理一下。写个单独的组件库出来 「目前刚开始,均在alpha阶段」

    如果有一些帮助,就赏一个star吧 🙈
    代码地址: github
    文档地址:文档

  • 相关阅读:
    MySQL-json字段的使用
    比较三种非破坏性处理数组的方法
    visual studio code中base环境切换的问题
    无涯教程-JavaScript - DB函数
    Web进阶
    什么样的研发活动可享受研发费用加计扣?
    【Linux】线程安全-生产者消费者模型
    offer
    locust 的分布式
    论语第三篇-八侑
  • 原文地址:https://blog.csdn.net/qq_41086511/article/details/128028957