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,实现了丝滑的抽奖动画,这也是九宫格抽奖最基础也最重要的两个点。希望这篇分享能帮到需要的小伙伴~

相关推荐
_AaronWong25 分钟前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
cxxcode25 分钟前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户54330814419434 分钟前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo38 分钟前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
JohnYan42 分钟前
工作笔记-CodeBuddy应用探索
javascript·ai编程·aiops
恋猫de小郭1 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木1 小时前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮1 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati1 小时前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉1 小时前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain