乐于分享
好东西不私藏

uniapp+vue3+ts:小程序九宫格抽奖组件

uniapp+vue3+ts:小程序九宫格抽奖组件

小程序九宫格抽奖组件,可随机结果,亦可指定结果。每次都可以随机打乱奖品排序。

视频演示

已关注

关注

重播 分享

一、功能

✔封奖品随机排序

✔根据概率随机获取奖品

✔指定奖品

✔封装成为组件

二、代码分析

1、九宫格布局

使用相对定位,纯css样式布局:

<viewclass="prize-item prize-row"v-for="(item, index) in prizes":key="index"><view:class="['pi-block','prize-column',index == chooseId?'prize-item-ani':'prize-item-nor']"><imageclass="pi-b-img":src="item.thumb"mode="widthFix"v-if="item.thumb !=''" /><textclass="pi-b-text">{{ item.name }} </text></view></view>
.prize-item {float: left;width33%;height240rpx;align-items: center;justify-content: center;border-radius8rpx;overflow: hidden;	}.prize-item:nth-of-type(4) {position: relative;left66%;	}.prize-item:nth-of-type(5) {position: relative;left33%;top240rpx;	}.prize-item:nth-of-type(6) {position: relative;left: -33%;top240rpx;	}.prize-item:nth-of-type(8) {position: relative;left: -33%;top: -240rpx;	}.prize-item:nth-of-type(9) {cursor: pointer;position: relative;left: -33%;top: -240rpx;	}.prize-item-nor {backgroundrgb(110149183);	}.prize-item-ani {backgroundrgb(787878);	}

结果如图:

2、抽奖动画设计

预设动画选中标识、滚动事件对象、动画运动参数等

const chooseId = ref(-1); //选中的ID 滚动动画标识const stopIdx = ref(0); //停止的下标const aniTimer = ref(null); //时间对象-定时器const speed = ref(props.aniSpeed); //滚动速度const startOrStop = ref(false); //开始抽奖还是停止const stopTimer = ref(null); //要求停止的时间对象 定时器const aniRunNum = ref(0); //动画运动圈数const aniRunNumStop = ref(3); //停止圈数判断不同的下标可到达停止的圈数

动画开始和动画停止、结果预处理等

//开始constprizeDrawStart = async () => {preRandomResult(); //结果预处理		startOrStop.value = true//按钮标识awaitnextTick(); //等待DOM更新完成		aniTimer.value = setInterval(() => {if (chooseId.value == 8) {				chooseId.value = -1;				aniRunNum.value++;			}			chooseId.value++		}, speed.value)prizeDrawStop(); //停止	}//停止constprizeDrawStop = () => {//定时器 监听到达条件		stopTimer.value = setInterval(() =>prizDrawStop(), 400);	}//停止操作constprizDrawStop = () => {if (aniRunNum.value >= aniRunNumStop.value) {// console.log("++",aniRunNum.value,aniRunNumStop.value)//最终停止if (chooseId.value == stopIdx.value) {//重置定时器timerClear();				startOrStop.value = false//启停标识				aniRunNum.value = 0//重置圈数// choosePrize.value = Array.from(prizes.value)[stopIdx.value]; //结果赋值				isShowTips.value = true;			}		}	}//预处理结果constpreRandomResult = () => {//是否存在指定结果if(Object.keys(choosePrize.value).length == 0){ //未存在指定结果		 choosePrize.value = randomResult();		}//已知结果,处理滚动时的条件nextTick(() => {const id = choosePrize.value.id//根据存在的id赋值const idx = prizes.value.findIndex(item => item.id == id); //获取结果对应的下标			stopIdx.value = idx; //最后停止的下标//停止的下标判断,为让视觉效果好点if (stopIdx.value >= 4) {//结果下标结果 大于4 即4、5、6、7 的在到达第3圈后才可停止 				aniRunNumStop.value = 3;else {//结果小于于4 即 0、1、2、3 的到达第4圈后才可停止 				aniRunNumStop.value = 4;			}// console.log("id:", id, "  stop:", stopIdx.value, " data:", prizes);		})	}

3、结果弹框

使用css动画控制弹框透明的,实现缓慢出现的效果:

.tb-block{position: relative;width:50%;min-height20%;align-items: center;justify-content:center;background-color#fff;padding20rpx;border-radius10rpx;animation: showTips 2s forwards; /* 应用动画,持续2秒,停留在结束状态 */			 }@keyframes showTips {from { opacity0; } /* 初始透明度 */to { opacity1; } /* 目标透明度 */		 }
如下图效果:

4、奖品随机排序

每次抽完奖时,重新打乱排序,可使每次抽奖奖品排序都不一致。

代码示例:

//随机排列constweightedShuffle = (arr)=> {const result = [...arr]; // 避免修改原数组for (let i = result.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));	      [result[i], result[j]] = [result[j], result[i]];	    }return result;	}

附上组件的使用和完整的代码,仅供参考:

组件引用:

<template><viewclass="prize-content"><viewclass="res-box"v-if="finalResult.id">{{ finalResult.name }}</view><viewclass="draw-cp"><prize-drawv-if="prizeArr.length>0":prizeArr="prizeArr":prizeRes="prizeRes":aniSpeed="100" @endDrawResult="getResult"></prize-draw></view></view></template><scriptsetup>import prizeDraw from'@/components/prize-draw/prize-draw.vue'import {		nextTick,		onMounted,		ref,		getCurrentInstance,		onUnmounted,		isReffrom'vue'const prizeArr = ref([]); //列表const prizeRes = ref({}); //结果const finalResult = ref({}); //最终结果onMounted(() => {getPrizeList();//获取数据//指定结果		prizeRes.value = {id4,name'奖品4',thumb'/static/prize/01.png',probability:10		};	})//获取结果constgetResult = (e) =>{console.log("结果:",e);		finalResult.value = e;		prizeArr.value = weightedShuffle(prizeArr.value); //再次给其随机排列	}//获取列表constgetPrizeList = ()=>{const arr = [			{id1,name'奖品1'//名称thumb'/static/prize/01.png'//图片probability:0.02//中奖概率			}, {id2,name'奖品2',thumb'/static/prize/03.png',probability:5			}, {id3,name'奖品3',thumb'/static/prize/02.png',probability:6			}, {id4,name'奖品4',thumb'/static/prize/01.png',probability:10			}, {id5,name'奖品5',thumb'/static/prize/03.png',probability:200			}, {id6,name'奖品6',thumb'',probability:66			}, {id7,name'奖品7',thumb'',probability:80			}, {id8,name'奖品8',thumb'',probability:99			}		];		prizeArr.value = weightedShuffle(arr);	}//随机排列constweightedShuffle = (arr)=> {const result = [...arr]; // 避免修改原数组for (let i = result.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));	      [result[i], result[j]] = [result[j], result[i]];	    }return result;	}onUnmounted(() => {	})</script><stylelang="scss">.prize-content{width100vw;height100vh;display: flex;flex-direction: column;justify-content: center;align-items: center;transition:transform 0.3s;.res-box{width100%;height50rpx;text-align: center;position: absolute;top10%;left0;		}	}</style>

组件源码:

<template><viewclass="prize-box prize-column"><!--主体内容--><viewclass="prize-draw-box"><viewclass="prize-item prize-row"v-for="(item, index) in prizes":key="index"><view:class="['pi-block','prize-column',index == chooseId?'prize-item-ani':'prize-item-nor']"><imageclass="pi-b-img":src="item.thumb"mode="widthFix"v-if="item.thumb !=''" /><textclass="pi-b-text">{{ item.name }} </text></view></view><viewclass="prize-item prize-row"style="cursor: pointer;" @tap="prizeDrawStart"v-if="!startOrStop"><viewclass="pi-block-btn prize-column"><textclass="pi-b-text">开始</text></view></view><viewclass="prize-item prize-row"style="cursor: pointer;"v-else><viewclass="pi-block-btn prize-column"><textclass="pi-b-text">抽奖中...</text></view></view></view><!--弹出--><viewclass="tips-box prize-column"v-show="isShowTips"v-if="choosePrize.id>0"><!--遮罩--><viewclass="tb-mask" @tap="closeTips"></view><!--内容--><viewclass="tb-block prize-column"><textclass="tb-b-tips">恭喜你获得</text><imageclass="tb-b-img":src="choosePrize.thumb"mode="widthFix"v-if="choosePrize.thumb !=''" /><textclass="tb-b-text">{{ choosePrize.name }} </text></view></view></view></template><scriptsetup>import {		nextTick,		onMounted,		ref,		getCurrentInstance,		onUnmounted,		watchfrom'vue'const props = defineProps({prizeArr: {typeArray,requiredtrue,	  }, //奖品数据prizeRes:{type:Object,default:{},	  },//结果aniSpeed:{type:Number,default:200//默认200 数字越小滚动越快	  }//滚动速度	  });const prizes = ref(props.prizeArr); //列表信息const choosePrize = ref(props.prizeRes); //选中的信息 - 结果// const pdResult = ref("恭喜您抽中了!!!");//结果const chooseId = ref(-1); //选中的ID 滚动动画标识const stopIdx = ref(0); //停止的下标const aniTimer = ref(null); //时间对象-定时器const speed = ref(props.aniSpeed); //滚动速度const startOrStop = ref(false); //开始抽奖还是停止const stopTimer = ref(null); //要求停止的时间对象 定时器const aniRunNum = ref(0); //动画运动圈数const aniRunNumStop = ref(3); //停止圈数判断不同的下标可到达停止的圈数const plist = watch(()=>props.prizeArr,(val)=>{		  prizes.value = val;	  });//监听结果变化const pRes = watch(()=>props.prizeRes,(val)=>{		  choosePrize.value = val;	  });//注册输出的事件const emits = defineEmits(['endDrawResult']); //返回抽奖结果//随机结果constrandomResult =() =>{const arrCopy = [...prizes.value]; // 避免修改原数组 首先先按照权重从小到大的排序const arr =arrCopy.sort((a,b)=>a.probability - b.probability);//计算总权重const totalWeight = arr.reduce((sum, item) => sum +item.probability0); //使用高阶函数 reduce 来累计总权重 const randomNum = Math.random()*totalWeight; //获取随机数// 根据随机数选择奖品let currentSum = 0;for (const item of arr) {		      currentSum += item.probability;if (randomNum <= currentSum) {return item;		      }		    }//如果有问题就默认返回第一个return items[0];	  }//预处理结果constpreRandomResult = () => {//是否存在指定结果if(Object.keys(choosePrize.value).length == 0){ //未存在指定结果		 choosePrize.value = randomResult();		}//已知结果,处理滚动时的条件nextTick(() => {const id = choosePrize.value.id//根据存在的id赋值const idx = prizes.value.findIndex(item => item.id == id); //获取结果对应的下标			stopIdx.value = idx; //最后停止的下标//停止的下标判断,为让视觉效果好点if (stopIdx.value >= 4) {//结果下标结果 大于4 即4、5、6、7 的在到达第3圈后才可停止 				aniRunNumStop.value = 3;else {//结果小于于4 即 0、1、2、3 的到达第4圈后才可停止 				aniRunNumStop.value = 4;			}// console.log("id:", id, "  stop:", stopIdx.value, " data:", prizes);		})	}//开始constprizeDrawStart = async () => {preRandomResult(); //结果预处理		startOrStop.value = true//按钮标识awaitnextTick(); //等待DOM更新完成		aniTimer.value = setInterval(() => {if (chooseId.value == 8) {				chooseId.value = -1;				aniRunNum.value++;			}			chooseId.value++		}, speed.value)prizeDrawStop(); //停止	}//停止constprizeDrawStop = () => {//定时器 监听到达条件		stopTimer.value = setInterval(() =>prizDrawStop(), 400);	}//停止操作constprizDrawStop = () => {if (aniRunNum.value >= aniRunNumStop.value) {// console.log("++",aniRunNum.value,aniRunNumStop.value)//最终停止if (chooseId.value == stopIdx.value) {//重置定时器timerClear();				startOrStop.value = false//启停标识				aniRunNum.value = 0//重置圈数// choosePrize.value = Array.from(prizes.value)[stopIdx.value]; //结果赋值				isShowTips.value = true;			}		}	}/**	 * 结果弹框	 * **/const isShowTips = ref(false); //是否显示//关闭弹框constcloseTips =()=>{emits("endDrawResult",choosePrize.value);restDate(); //重置timerClear(); //清空重置定时器	}//数据重置constrestDate = () => {		isShowTips.value = false//关闭弹框		stopIdx.value = 0//重置		choosePrize.value = {}; //清空结果		chooseId.value  = -1;//滚动标识	}//清空定时器consttimerClear = () =>{//重置定时器clearInterval(aniTimer.value);		aniTimer.value = null;//重置定时器clearInterval(stopTimer.value);		stopTimer.value = null;	}</script><stylelang="scss">.prize-row {display: flex;flex-direction: row;	}.prize-column {display: flex;flex-direction: column;	}.prize-box {width100vw;height100vh;justify-content: center;align-items: center;overflow: hidden;	}.prize-draw-box {width90%;align-items: center;justify-content: center;	}.prize-item {float: left;width33%;height240rpx;align-items: center;justify-content: center;border-radius8rpx;overflow: hidden;	}.prize-item:nth-of-type(4) {position: relative;left66%;	}.prize-item:nth-of-type(5) {position: relative;left33%;top240rpx;	}.prize-item:nth-of-type(6) {position: relative;left: -33%;top240rpx;	}.prize-item:nth-of-type(8) {position: relative;left: -33%;top: -240rpx;	}.prize-item:nth-of-type(9) {cursor: pointer;position: relative;left: -33%;top: -240rpx;	}.prize-item-nor {backgroundrgb(110149183);	}.prize-item-ani {backgroundrgb(787878);	}.pi-block-btn {width90%;height90%;backgroundrgb(25514861);align-items: center;justify-content:center;border-radius10rpx;.pi-b-text {color: white;text-align: center;font-size1.2rem;line-height40rpx;		}	}.pi-block {width90%;height90%;align-items: center;justify-content:center;border-radius10rpx;.pi-b-text {color: white;text-align: center;font-size1rem;line-height40rpx;		}.pi-b-img {width50%;height50%;		}	}/**	 * 弹框	 * */.tips-box{position: absolute;width100vw;height100vh;align-items: center;justify-content:center;backgroundrgba(2552552550.2);.tb-mask{position: absolute;width100%;height100%;}.tb-block{position: relative;width:50%;min-height20%;align-items: center;justify-content:center;background-color#fff;padding20rpx;border-radius10rpx;animation: showTips 2s forwards; /* 应用动画,持续2秒,停留在结束状态 */			 }@keyframes showTips {from { opacity0; } /* 初始透明度 */to { opacity1; } /* 目标透明度 */		 }.tb-b-tips{width100%;font-size1.2rem;font-weight: bold;text-align: center;}.tb-b-img{width:60%;height60%;}.tb-b-text{width100%;text-align: center;font-size1rem;}	 }</style>