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;width: 33%;height: 240rpx;align-items: center;justify-content: center;border-radius: 8rpx;overflow: hidden;}.prize-item:nth-of-type(4) {position: relative;left: 66%;}.prize-item:nth-of-type(5) {position: relative;left: 33%;top: 240rpx;}.prize-item:nth-of-type(6) {position: relative;left: -33%;top: 240rpx;}.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 {background: rgb(110, 149, 183);}.prize-item-ani {background: rgb(78, 78, 78);}
结果如图:

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-height: 20%;align-items: center;justify-content:center;background-color: #fff;padding: 20rpx;border-radius: 10rpx;animation: showTips 2s forwards; /* 应用动画,持续2秒,停留在结束状态 */}@keyframes showTips {from { opacity: 0; } /* 初始透明度 */to { opacity: 1; } /* 目标透明度 */}

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,isRef} from'vue'const prizeArr = ref([]); //列表const prizeRes = ref({}); //结果const finalResult = ref({}); //最终结果onMounted(() => {getPrizeList();//获取数据//指定结果prizeRes.value = {id: 4,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 = [{id: 1,name: '奖品1', //名称thumb: '/static/prize/01.png', //图片probability:0.02//中奖概率}, {id: 2,name: '奖品2',thumb: '/static/prize/03.png',probability:5}, {id: 3,name: '奖品3',thumb: '/static/prize/02.png',probability:6}, {id: 4,name: '奖品4',thumb: '/static/prize/01.png',probability:10}, {id: 5,name: '奖品5',thumb: '/static/prize/03.png',probability:200}, {id: 6,name: '奖品6',thumb: '',probability:66}, {id: 7,name: '奖品7',thumb: '',probability:80}, {id: 8,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{width: 100vw;height: 100vh;display: flex;flex-direction: column;justify-content: center;align-items: center;transition:transform 0.3s;.res-box{width: 100%;height: 50rpx;text-align: center;position: absolute;top: 10%;left: 0;}}</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,watch} from'vue'const props = defineProps({prizeArr: {type: Array,required: true,}, //奖品数据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.probability, 0); //使用高阶函数 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 {width: 100vw;height: 100vh;justify-content: center;align-items: center;overflow: hidden;}.prize-draw-box {width: 90%;align-items: center;justify-content: center;}.prize-item {float: left;width: 33%;height: 240rpx;align-items: center;justify-content: center;border-radius: 8rpx;overflow: hidden;}.prize-item:nth-of-type(4) {position: relative;left: 66%;}.prize-item:nth-of-type(5) {position: relative;left: 33%;top: 240rpx;}.prize-item:nth-of-type(6) {position: relative;left: -33%;top: 240rpx;}.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 {background: rgb(110, 149, 183);}.prize-item-ani {background: rgb(78, 78, 78);}.pi-block-btn {width: 90%;height: 90%;background: rgb(255, 148, 61);align-items: center;justify-content:center;border-radius: 10rpx;.pi-b-text {color: white;text-align: center;font-size: 1.2rem;line-height: 40rpx;}}.pi-block {width: 90%;height: 90%;align-items: center;justify-content:center;border-radius: 10rpx;.pi-b-text {color: white;text-align: center;font-size: 1rem;line-height: 40rpx;}.pi-b-img {width: 50%;height: 50%;}}/*** 弹框* */.tips-box{position: absolute;width: 100vw;height: 100vh;align-items: center;justify-content:center;background: rgba(255, 255, 255, 0.2);.tb-mask{position: absolute;width: 100%;height: 100%;}.tb-block{position: relative;width:50%;min-height: 20%;align-items: center;justify-content:center;background-color: #fff;padding: 20rpx;border-radius: 10rpx;animation: showTips 2s forwards; /* 应用动画,持续2秒,停留在结束状态 */}@keyframes showTips {from { opacity: 0; } /* 初始透明度 */to { opacity: 1; } /* 目标透明度 */}.tb-b-tips{width: 100%;font-size: 1.2rem;font-weight: bold;text-align: center;}.tb-b-img{width:60%;height: 60%;}.tb-b-text{width: 100%;text-align: center;font-size: 1rem;}}</style>
夜雨聆风