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

本文介绍如何使用React.js开发一个九宫格抽奖组件。从定义组件接口、页面布局、样式设置,到实现动画逻辑和处理概率问题,详细讲解了组件的完整实现过程。同时,解决了抽奖过程中可能出现的动画停顿和多次点击导致的紊乱问题。

定义一下组件接口 🥡

  • 奖项数据是必不可少的 「九宫格抛弃抽奖按钮占去一个外,还剩下8个」
  • 抽奖运动的时间
  • 抽奖完成之后的回调
  • 是否需要自定义概率
type Tuple8<TItem> = [TItem, ...TItem[]] & { length: 8 };
type CallbackType = (arg: LDataType) => void;

// props type
interface LType {data: Tuple8<LDataType>;time?: number;useCustomProbability?: boolean;callback?: CallbackType;
} 

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

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

先简单画一下页面吧 🏂

页面结构

奖励项只有八个,我们怎么完成九宫格的布局呢 ?

嘿嘿,我们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 (<div className="lottery">{realViewData.map((item) => {return (<divclassName={classNames({'lottery-item': true,'is-btn': item.id === '__btn__'})}onClick={}key={item.id}>{item.name}</div>);})}</div>);
}; 

搞点小样式

先写好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;}
} 

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

@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)}
} 

蛙,页面出来啦!!!

完善一下基本逻辑 🥊

结构样式处理完了,接下来该处理动画了

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

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

 const [prizeActiveState, setPrizeActiveState] = useState<any>(props.data.reduce((pre, cur) => ({...pre,['active' + cur.id]: false,}),{},),); 

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

// active: item.id !== '__btn__' && prizeActiveState[`active${item.id}`]

<divclassName={classNames({'lottery-item': true,'is-btn': item.id === '__btn__',active:item.id !== '__btn__' && prizeActiveState[`active${item.id}`],})}onClick={() => start(item.id)}key={item.id}>{item.name}</div> 

样式

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

开始跑动画咯

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

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

解释:当前path中每项的值是真正的奖项数据在其原数组的索引位置。

即:第一次 0 号位置的奖项, 然后是 1 号,再是 2 号,接下来是 4 号

根据这个信息,我们就可以定位到这个奖项所对应的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,};}}, {}),); 

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

 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 (<div className="lottery">{realViewData.map((item) => {return (<divclassName={classNames({'lottery-item': true,'is-btn': item.id === '__btn__',active:item.id !== '__btn__' && prizeActiveState[`active${item.id}`],})}onClick={() => start(item.id)}key={item.id}>{item.name}</div>);})}</div>); 

喜大普奔,终于跑起来了

不过有两个问题

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);} 

处理一下概率问题 ⚽️

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

正规

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

很简单:

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

来加入逻辑中

 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);}; 

这样一个正规的抽奖组件就完成了

自定义概率

比如 一个数据 【苹果🍎,香蕉🍌,梨🍐】

要求随机抽,并且抽中苹果🍎的概率要达到80%,其他各10%。这要啷个搞么

其实也是很简单

构造一个临时数组

【苹果🍎_80,香蕉🍌_10,梨🍐*10】=> 利用Math.random()*100去随机取一下

其实就是小学概率问题「一个球,扔到个区间的概率各是多少」

来写一下逻辑

 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)];}; 

加入到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 的概率调整为 50%,即只能抽中代金券 1 和 2

const data = [{id: 1,name: '代金券1',probability: 0.5,},{id: 2,name: '代金券2',probability: 0.5,},{id: 3,name: '代金券3',}// ....
] 

效果

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

此资源包括抽奖相关所有的配置,中奖概率配置、奖品表、抽奖记录表通用存储过程算法。 可以指定抽多少次以后在按照正常概率来计算,中奖个数,如果奖品全部被抽完就永远抽不中,中奖率中奖最大范围有关,数字越大概率越低,反正越高。由于涉及到一些敏感表数据,只提供主要的中奖表,如用户流水账号信息这些表不提供。 规则: --中奖率公式:中奖率 = 奖项数字范围 ÷ 摇奖数字范围 ÷ 中奖数字范围 --1、摇奖数字范围最小值最大值定义了产生奖项数字的范围。 --2、中奖数字范围是从1到中奖范围的最大值,此范围内产生中奖号码。 /* 比如说,现在有三个选择一等、二等、三等。 可以设置“摇奖数字选项”为 1-30 一等 奖项数字范围1-10 二等 奖项数字范围 11-20 三等 奖项数字范围 21-30 所设置的奖项数字范围必须在 “摇奖数字选项”范围内,而且不得交叉、重叠。 如果我其中一个设置1-12 另外一个设置10-20 那么就重叠了。 而且为了方便调整中奖率,建议把所有的 奖项数字范围全部设置等距离。如现在有十二个选项,那么依次可以设置成为以下: 1-10、11-20、21-30、...、111-120 那么自然 “摇奖数字范围”就是1-120 现在需要调整中奖率的大小,中奖号码不用改了,全部设置成为1. 然后去调整中奖数字范围。 比如说 一等 中奖数字范围 1-30 二等 中奖数字范围 1-20 三等 中奖数字范围 1-10 那么具体这样设置下来,中奖率会有多高呢?算一下便知道。 一等:10/(30*30)=1/90 九十分之一。 二等:10/(30*20)=1/60 六十分之一。 三等:10/(30*10)=1/30 三十分之一。 所以设置的时候就把中奖号码都设置成1,只需要调整中奖数字范围便可。 */
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值