VUE3简单实现九宫格点击抽奖

前言

分享一个基于 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 个:

  1. 错误的停止条件 :很多人会用 step>指定数值 作为停止条件之一,但step被限制在 0-7 循环,永远不会超过指定数值,导致停止条件永远不成立。
  2. 递归传参丢失 :把中奖下标作为递归参数传递,容易导致下标丢失或错乱,本次改为全局变量存储中奖下标,逻辑更稳定。

二、本次的「必停」核心逻辑(重点)

本次采用 「速度达标 + 位置匹配」 的停止规则,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-rowgrid-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>标签中,无嵌套,直接修改对应的backgroundborder-radiuscolor即可,比如修改奖品底色、高亮色、按钮色等。

总结

这个九宫格抽奖组件做到了 极简、无 BUG、高颜值、易复用,所有核心逻辑都做了优化和修复,代码注释清晰,新手也能轻松理解。组件无任何第三方依赖,可直接集成到 Vue3 项目中,也可以根据需求扩展更多功能(比如抽奖次数限制、奖品概率配置、动画效果增强等)。

核心的核心:解决了「无限转圈」的 BUG,实现了丝滑的抽奖动画,这也是九宫格抽奖最基础也最重要的两个点。希望这篇分享能帮到需要的小伙伴~

相关推荐
华玥作者2 小时前
[特殊字符] VitePress 对接 Algolia AI 问答(DocSearch + AI Search)完整实战(下)
前端·人工智能·ai
Mr Xu_2 小时前
告别冗长 switch-case:Vue 项目中基于映射表的优雅路由数据匹配方案
前端·javascript·vue.js
前端摸鱼匠2 小时前
Vue 3 的toRefs保持响应性:讲解toRefs在解构响应式对象时的作用
前端·javascript·vue.js·前端框架·ecmascript
sleeppingfrog3 小时前
zebra通过zpl语言实现中文打印(二)
javascript
lang201509283 小时前
JSR-340 :高性能Web开发新标准
java·前端·servlet
好家伙VCC3 小时前
### WebRTC技术:实时通信的革新与实现####webRTC(Web Real-TimeComm
java·前端·python·webrtc
未来之窗软件服务4 小时前
未来之窗昭和仙君(六十五)Vue与跨地区多部门开发—东方仙盟练气
前端·javascript·vue.js·仙盟创梦ide·东方仙盟·昭和仙君
baidu_247438614 小时前
Android ViewModel定时任务
android·开发语言·javascript
嘿起屁儿整4 小时前
面试点(网络层面)
前端·网络
VT.馒头4 小时前
【力扣】2721. 并行执行异步函数
前端·javascript·算法·leetcode·typescript