本文书写✍️耗时 30min
建议阅读时长 10min 🏄🏽♂️
type Tuple8 = [TItem, ...TItem[]] & { length: 8 };
type CallbackType = (arg: LDataType) => void;
// props type
interface LType {
data: Tuple8;
time?: number;
useCustomProbability?: boolean;
callback?: CallbackType;
}
至于奖项数组,每一个奖项的属性则定义为
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 (
{realViewData.map((item) => {
return (
{item.name}
);
})}
);
};
先写好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)
}
}
蛙,页面出来啦!!!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wcZ5ajwe-1669304260237)(]
)
结构样式处理完了,接下来该处理动画了
动画的处理方案,我们采用最传统方式。按照九宫格的顺时针方向不断的给小盒子设置一个active样式类,令其高亮「古老的干掉他人仅留自己」
const [prizeActiveState, setPrizeActiveState] = useState(
props.data.reduce(
(pre, cur) => ({
...pre,
['active' + cur.id]: false,
}),
{},
),
);
active: item.id !== ‘btn’ && prizeActiveState[active${item.id}
]
start(item.id)}
key={item.id}
>
{item.name}
样式
.active {
background-color: rgb(227, 248, 121);
}
先别着急跑,有个小问题。我们需要根据九宫格的转动方向先定义好转动的路径
// 顺时针
const path = [0, 1, 2, 4, 7, 6, 5, 3];
解释:当前path中每项的值是真正的奖项数据在其原数组的索引位置。
即:第一次 0 号位置的奖项, 然后是 1 号,再是 2 号,接下来是 4 号
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h7efk9BE-1669304235843)(]
)
根据这个信息,我们就可以定位到这个奖项所对应的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 (
{realViewData.map((item) => {
return (
start(item.id)}
key={item.id}
>
{item.name}
);
})}
);
喜大普奔,终于跑起来了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mGiio0PT-1669304280105)(]
)
不过有两个问题
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);
};
这样一个正规的抽奖组件就完成了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eo8ZdukR-1669304302870)(]
)
比如 一个数据 【苹果🍎,香蕉🍌,梨🍐】
要求随机抽,并且抽中苹果🍎的概率要达到80%,其他各10%。这要啷个搞么
其实也是很简单
构造一个临时数组
【苹果🍎80,香蕉🍌10,梨🍐*10】=> 利用Math.random()*100
去随机取一下
其实就是小学概率问题「扔一个球,仍到三个区间的概率各是多少」
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aipgsPRW-1669304319408)(]
)
来写一下逻辑
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',
}
// ....
]
效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLTzDzk9-1669304337343)(]
)
打完收工!!!
虽然自己一直的业务方向是用户增长、营销等领域,但是最近有将近一年半的时间都在搞效率平台相关的开发了。最近正好闲下来,把以前的东西归纳整理一下。写个单独的组件库出来 「目前刚开始,均在alpha阶段」