前言
分享一个基于 Vue3 + setup 语法糖 开发的极简九宫格抽奖组件,无任何第三方依赖,代码简洁易懂、无冗余,修复了抽奖无限转圈的核心 BUG,实现了「点击抽奖→先快后慢减速转动→精准停止→中奖弹窗」的完整业务逻辑,同时做了轻量精致的样式美化,不花哨、颜值在线,可直接 CV 复用在项目中,新手也能轻松看懂和修改。
实现功能清单
✅ 基于 Vue3 setup 语法糖开发,代码极致精简✅ v-for 循环渲染奖品,无重复 DOM,简洁易维护✅ 完美解决「抽奖无限转圈停不下来」的核心 BUG,100% 必停✅ 经典抽奖动画:点击后高速转动 → 平滑减速 → 缓慢停止,体验极佳✅ 九宫格布局规整,抽奖按钮固定居中无错位✅ 按钮禁用防重复点击,避免重复触发抽奖逻辑✅ 中奖弹窗展示奖品信息,弹窗关闭自动重置状态✅ 组件销毁时清除定时器,防止内存泄漏✅ 轻量样式美化,配色协调、圆角柔和,无多余花哨样式
完整可运行代码
vue
<template>
<div class="prize-container">
<h1>九宫格点击抽奖</h1>
<div class="prize">
<!-- v-for循环渲染8个奖品项 -->
<div class="option" v-for="(item, index) in prizeList" :key="index" :class="{ active: activeIndex === item.id }">
<img :src="item.img" alt="" />
<p>{{ item.name }}</p>
</div>
<!-- 中间抽奖按钮 -->
<div class="btn-box">
<button @click="handleDraw" :disabled="isDrawing">点击抽奖</button>
</div>
</div>
<!-- 中奖弹窗 -->
<div class="modal" v-show="showModal">
<div class="modal-content">
<h3>恭喜您</h3>
<img :src="winPrize.img" alt="" />
<p>{{ winPrize.name }}</p>
<button @click="closeModal">确定</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
// 引入奖品图片(替换成自己的图片路径即可)
import img1 from '/src/img/539ba7d39e9f442d4b09079fbdd0b95a.png'
import img2 from '/src/img/5437b2ff108c891d4a0347b921e39044.png'
import img3 from '/src/img/3eda0d5c8223469332f939ebfd176c7b.png'
import img4 from '/src/img/69fd41c17118d8ab0c738d890c948066.png'
import img5 from '/src/img/10001.png'
import img6 from '/src/img/aba5af2a02e5b7f48ac9f59fcc2e2932.png'
import img7 from '/src/img/b93a239cafe4b630b7438dfa263f00fa.png'
import img8 from '/src/img/c0a591d1d9eee307d5cfda25aeef8339.png'
// 奖品列表配置 - id对应奖品标识,img对应图片,name对应奖品名称
const prizeList = [
{ id: 0, img: img1, name: '一等奖' },
{ id: 1, img: img2, name: '未中奖' },
{ id: 2, img: img3, name: '四等奖' },
{ id: 3, img: img4, name: '再接再厉' },
{ id: 4, img: img5, name: '二等奖' },
{ id: 5, img: img6, name: '三等奖' },
{ id: 6, img: img7, name: '特等奖' },
{ id: 7, img: img8, name: '再来一次' }
]
// 响应式变量 - 状态管理
const activeIndex = ref(-1) // 当前高亮的奖品id,-1为默认无高亮
const isDrawing = ref(false) // 是否正在抽奖,控制按钮禁用状态
const showModal = ref(false) // 是否显示中奖弹窗
const winPrize = ref({}) // 中奖奖品的信息
// 普通变量 - 抽奖核心配置
let timer = null // 定时器标识,用于控制抽奖转动
let step = 0 // 转动步骤,控制奖品高亮切换
let speed = 100 // 转动速度(单位:毫秒),数值越小转动越快
let winIndex = 0 // 中奖奖品的下标,全局变量避免递归传参丢失
// 抽奖转动的顺时针顺序 - 对应九宫格奖品的id顺序,固定不可乱
const rollOrder = [0, 1, 2, 4, 7, 6, 5, 3]
// 点击抽奖触发的主方法
const handleDraw = () => {
// 重置所有抽奖状态
step = 0
speed = 100
activeIndex.value = rollOrder[step]
isDrawing.value = true
// 随机生成中奖下标(0-7),可在此处修改为指定中奖逻辑
winIndex = Math.floor(Math.random() * 8)
// 启动抽奖转动逻辑
startRoll()
}
// 抽奖转动的核心逻辑
const startRoll = () => {
// 清除上一次的定时器,防止定时器叠加
clearTimeout(timer)
// 切换下一个奖品高亮
step++
// 步骤循环:超过数组长度则重置为0,实现无限循环转动
if (step >= rollOrder.length) step = 0
activeIndex.value = rollOrder[step]
// 先快后慢的核心减速逻辑:速度小于320时,每次增加20,达到320后匀速转动
if (speed < 320) speed += 20
// 停止抽奖的核心条件:速度达标(足够慢) + 高亮的奖品是中奖奖品
if (speed >= 320 && activeIndex.value === winIndex) {
clearTimeout(timer) // 清除定时器,停止转动
winPrize.value = prizeList.find(item => item.id === winIndex) // 获取中奖信息
showModal.value = true // 显示中奖弹窗
isDrawing.value = false // 解锁抽奖按钮
return // 终止方法,避免继续转动
}
// 继续转动,递归调用自身
timer = setTimeout(startRoll, speed)
}
// 关闭中奖弹窗的方法
const closeModal = () => {
showModal.value = false
activeIndex.value = -1
step = 0
}
// 组件销毁时清除定时器,防止内存泄漏
onUnmounted(() => clearTimeout(timer))
</script>
<style scoped>
/* 外层容器样式 */
.prize-container {
width: 100%;
text-align: center;
padding-top: 50px;
}
/* 九宫格核心布局 - grid实现完美3*3布局 */
.prize {
width: 370px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, 120px);
grid-template-rows: repeat(3, 120px);
gap: 5px;
}
/* 奖品项和按钮的公共样式 */
.option, .btn-box {
width: 120px;
height: 120px;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
/* 奖品项基础样式 */
.option {
background: #f8b88b;
}
/* 奖品项悬浮微动效 */
.option:hover {
transform: scale(1.02);
}
.option img {
width: 60px;
height: 60px;
object-fit: contain;
margin-bottom: 8px;
}
.option p {
color: #333;
font-size: 14px;
margin: 0;
font-weight: 500;
}
/* 奖品高亮样式 - 中奖转动时的选中态 */
.option.active {
background: #ff4747 !important;
color: #fff;
}
.option.active p {
color: #fff;
}
/* 抽奖按钮容器 - 固定在九宫格第二行第二列(正中间) */
.btn-box {
grid-row: 2;
grid-column: 2;
}
.btn-box button {
width: 100px;
height: 40px;
border: none;
border-radius: 20px;
background: linear-gradient(90deg, #ff5555, #f83f3f);
color: #fff;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.btn-box button:hover:not(:disabled) {
filter: brightness(1.1);
}
.btn-box button:disabled {
background: #e0e0e0;
color: #999;
cursor: not-allowed;
}
/* 弹窗遮罩层 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
/* 弹窗内容区样式 */
.modal-content {
width: 300px;
padding: 25px 20px;
background: #fff;
border-radius: 16px;
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
align-items: center;
}
.modal-content h3 {
margin: 0 0 10px;
color: #ff5555;
font-size: 20px;
}
.modal-content img {
width: 80px;
height: 80px;
margin: 10px 0;
}
.modal-content p {
font-size: 18px;
color: #ff5555;
margin: 10px 0 25px;
font-weight: 500;
}
.modal-content button {
width: 100px;
height: 36px;
border: none;
border-radius: 18px;
background: #ff5555;
color: #fff;
cursor: pointer;
transition: all 0.2s;
}
.modal-content button:hover {
filter: brightness(1.1);
}
</style>
核心知识点解析 & 易错点修复
一、为什么抽奖会「无限转圈停不下来」?(核心 BUG 修复)
很多小伙伴写九宫格抽奖都会遇到这个问题,本次代码彻底解决该问题,核心原因有 2 个:
- 错误的停止条件 :很多人会用
step>指定数值作为停止条件之一,但step被限制在0-7循环,永远不会超过指定数值,导致停止条件永远不成立。 - 递归传参丢失 :把中奖下标作为递归参数传递,容易导致下标丢失或错乱,本次改为全局变量存储中奖下标,逻辑更稳定。
二、本次的「必停」核心逻辑(重点)
本次采用 「速度达标 + 位置匹配」 的停止规则,100% 能触发停止,无任何例外:
javascript
运行
if (speed >= 320 && activeIndex.value === winIndex) { ... }
- 速度从
100开始,每次增加20,一定会涨到 320,这是必然结果; - 速度达到
320后,会保持匀速缓慢转动,早晚会转到中奖奖品的位置; - 两个条件同时满足后,立即执行停止逻辑,完美解决无限转圈问题。
三、先快后慢的抽奖动画实现
抽奖的丝滑减速是用户体验的核心,实现逻辑非常简单:
javascript
运行
if (speed < 320) speed += 20
speed是转动的间隔毫秒数,数值越小,转动越快;- 初始速度为
100(最快),每次转动时速度加20,转动越来越慢; - 当速度达到
320后,不再增加,保持匀速慢转,直到匹配中奖位置。
四、九宫格完美居中布局
本次采用 CSS Grid 布局 实现九宫格,相比 flex 布局更适合这种固定行列的场景,一行代码实现 3 列 3 行,按钮通过grid-row和grid-column固定在正中间,永不偏移:
css
.prize {
display: grid;
grid-template-columns: repeat(3, 120px);
grid-template-rows: repeat(3, 120px);
}
.btn-box {
grid-row: 2;
grid-column: 2;
}
易修改的配置项(新手友好)
不用改动核心逻辑,仅修改几个数值 / 配置,就能适配自己的需求,所有可修改项都整理好了,直接改就行:
✅ 1. 调整抽奖转动速度 / 圈数
在 startRoll 方法中,修改以下 2 个数值即可:
javascript
运行
speed += 20 // 数值越小 → 减速越慢、抽奖转动的圈数越多;数值越大 → 减速越快、圈数越少,推荐值:15-25
speed < 320 // 数值越小 → 抽奖越早停止;数值越大 → 抽奖越晚停止,推荐值:300-350
✅ 2. 修改奖品配置
直接修改 prizeList 数组即可,替换成自己的奖品图片、名称、id:
javascript
运行
const prizeList = [
{ id: 0, img: 你的图片路径, name: '奖品名称' },
...
]
✅ 3. 修改中奖规则(非随机)
默认是随机中奖,如需指定中奖奖品(比如必中一等奖),修改 handleDraw 中的 winIndex 赋值即可:
javascript
运行
// 随机中奖(默认)
winIndex = Math.floor(Math.random() * 8)
// 指定中奖:比如必中一等奖(id=0)
winIndex = 0
✅ 4. 修改样式配色 / 圆角
所有样式都集中在<style>标签中,无嵌套,直接修改对应的background、border-radius、color即可,比如修改奖品底色、高亮色、按钮色等。
总结
这个九宫格抽奖组件做到了 极简、无 BUG、高颜值、易复用,所有核心逻辑都做了优化和修复,代码注释清晰,新手也能轻松理解。组件无任何第三方依赖,可直接集成到 Vue3 项目中,也可以根据需求扩展更多功能(比如抽奖次数限制、奖品概率配置、动画效果增强等)。
核心的核心:解决了「无限转圈」的 BUG,实现了丝滑的抽奖动画,这也是九宫格抽奖最基础也最重要的两个点。希望这篇分享能帮到需要的小伙伴~