场景
做一个h5活动页,需要实现九宫格旋转抽奖效果。
参考:Vue3实现九宫格抽奖效果前言 好久没有写文章了,上一次发文还是年终总结,眨眼间又是一年,每每想多总结却是坚持不来,难 - 掘金
跟上述参考链接的不同点是:我从当前位置开始旋转。
逻辑
九宫格旋转
九宫格如下排列:
0 | 1 | 2 |
---|---|---|
3 | 4 | 5 |
6 | 7 | 8 |
其中4为空,不会转到。九宫格只会在外面转,顺时针的顺序为:
[0,1,2,5,8,7,6,3]
一圈8格。如果想要多转几圈,则加8N
假设规定一定要走32步。每次计算的总步数=到达目标的步数+32。从非0开始时,距离0的步数视为【已走步数】。
-
首次点击时,指针从0开始
-
假设抽到5,则共走32+3=35步,会走到5号
-
假设第二次抽到6:
- 共走:6+32=38步
- 从0开始走38步即正确位置
- 将当前位置5赋值为【已走步数】,这步是从当前开始的关键
- 则从当前位置只需再走33步
- 以此类推
转一圈其实是调用一次方法。
先加速后减速
配置一个速度(等待时间)。当执行的步数> 2/3总步数,则增加等待时间。
代码
基本数据
const state = reactive({
prizeList: [
{
name: '手机',
pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e',
},
{
name: '手表',
pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG',
},
{
name: '苹果',
pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG',
},
{
name: '棒棒糖',
pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG',
},
{
name: '娃娃',
pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG',
},
{
name: '木马',
pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG',
},
{
name: '德芙',
pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG',
},
{
name: '玫瑰',
pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG',
},
], // 后台配置的奖品数据
currentIndex: 0, // 当前位置(下标)
isRunning: false, // 是否正在抽奖
speed: 10, // 抽奖等待时间
timerIns: null, // 定时器实例
currentRunCount: 0, // 已跑次数
totalRunCount: 32, // 规定一定要跑的次数 8的倍数
prizeId: 0, // 中奖id
})
模拟抽奖
// 获取随机数
const getRandomNum = () => {
return prizeSort.value[Math.floor(Math.random() * prizeSort.value.length)]
}
核心逻辑
// 奖品高亮顺序
const prizeSort = ref([0, 1, 2, 5, 8, 7, 6, 3])
// 要执行总步数
const totalRunStep = computed(() => {
return state.totalRunCount + prizeSort.value.indexOf(state.prizeId)
})
// 在中间塞一个4号,不会转到
onMounted(() => {
state.prizeList.splice(4, 0, startBtn.value)
})
// 获取随机数
const getRandomNum = () => {
return prizeSort.value[Math.floor(Math.random() * prizeSort.value.length)]
}
// 点击开始
const start = () => {
if (state.isRunning) return
state.isRunning = true
state.currentRunCount = state.currentIndex // 从当前位置开始
state.speed = 50
// 模拟接口延时返回
setTimeout(() => {
const prizeId = getRandomNum()
console.log('中奖ID>>>', prizeId, state.prizeList[prizeId])
state.prizeId = prizeId
state.isRunning = false
console.log(totalRunStep.value, '总步数')
// 拿到结果后再开始转
startRun()
}, 200)
}
const startRun = () => {
stopRun()
// 已走步数超过总步数
if (state.currentRunCount > totalRunStep.value) {
state.isRunning = false
return
}
// 计算当前显示的奖品索引
state.currentIndex = prizeSort.value[state.currentRunCount % 8]
// 如果当前步数超过了 2/3,则速度慢下来 : 只要效果是先快后慢即可。
if (state.currentRunCount > Math.floor((state.totalRunCount * 2) / 3)) {
state.speed = state.speed + Math.floor(state.currentRunCount / 3)
}
state.timerIns = setTimeout(() => {
state.currentRunCount++
startRun()
}, state.speed)
}
const stopRun = () => {
state.timerIns && clearTimeout(state.timerIns)
}
总代码
<template>
<div>
<div id="app" v-cloak>
<div class="container">
<div
:class="['item', { active: state.currentIndex === index }]"
v-for="(item, index) in state.prizeList"
@click="start"
>
<img :src="item.pic" alt="" />
<p v-if="index !== 4">{{ item.name }}</p>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, computed } from 'vue'
const state = reactive({
prizeList: [
{
name: '手机',
pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e',
},
{
name: '手表',
pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG',
},
{
name: '苹果',
pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG',
},
{
name: '棒棒糖',
pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG',
},
{
name: '娃娃',
pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG',
},
{
name: '木马',
pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG',
},
{
name: '德芙',
pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG',
},
{
name: '玫瑰',
pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG',
},
], // 后台配置的奖品数据
currentIndex: 0, // 当前位置(下标)
isRunning: false, // 是否正在抽奖
speed: 10, // 抽奖等待时间
timerIns: null, // 定时器实例
currentRunCount: 0, // 已跑次数
totalRunCount: 32, // 规定一定要跑的次数 8的倍数
prizeId: 0, // 中奖id
})
const startBtn = ref({
name: '开始按钮',
pic: 'https://img2.baidu.com/it/u=1497996119,382735686&fm=253',
})
// 奖品高亮顺序
const prizeSort = ref([0, 1, 2, 5, 8, 7, 6, 3])
// 要执行总步数
const totalRunStep = computed(() => {
return state.totalRunCount + prizeSort.value.indexOf(state.prizeId)
})
onMounted(() => {
state.prizeList.splice(4, 0, startBtn.value)
})
// 获取随机数
const getRandomNum = () => {
return prizeSort.value[Math.floor(Math.random() * prizeSort.value.length)]
}
const start = () => {
if (state.isRunning) return
state.isRunning = true
state.currentRunCount = state.currentIndex // 从当前位置开始
state.speed = 50
// 模拟接口延时返回
setTimeout(() => {
const prizeId = getRandomNum()
console.log('中奖ID>>>', prizeId, state.prizeList[prizeId])
state.prizeId = prizeId
state.isRunning = false
console.log(totalRunStep.value, '总步数')
startRun()
}, 200)
}
const startRun = () => {
stopRun()
// 已走步数超过总步数
if (state.currentRunCount > totalRunStep.value) {
state.isRunning = false
return
}
// 计算当前显示的奖品索引
state.currentIndex = prizeSort.value[state.currentRunCount % 8]
// 如果当前步数超过了 2/3,则速度慢下来
if (state.currentRunCount > Math.floor((state.totalRunCount * 2) / 3)) {
state.speed = state.speed + Math.floor(state.currentRunCount / 3)
}
state.timerIns = setTimeout(() => {
state.currentRunCount++
startRun()
}, state.speed)
}
const stopRun = () => {
state.timerIns && clearTimeout(state.timerIns)
}
</script>
<style lang="less" scoped>
[v-cloak] {
display: none;
}
.container {
width: 450px;
height: 450px;
background: #98d3fc;
border: 1px solid #98d3fc;
margin: 100px auto;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
align-items: center;
}
.item {
width: 140px;
height: 140px;
border: 2px solid #fff;
position: relative;
}
.item:nth-of-type(5) {
cursor: pointer;
}
.item img {
width: 100%;
height: 100%;
}
.item p {
width: 100%;
height: 20px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 12px;
text-align: center;
line-height: 20px;
position: absolute;
left: 0;
bottom: 0;
}
.active {
border: 2px solid red;
box-shadow: 2px 2px 30px #fff;
}
</style>