uni-lattice-lottery
v1.0.0
Published
uni-app cli工具下的九宫格,大转盘,老虎机抽奖组件
Readme
uni-lattice-lottery
uni-app(Vue 3)抽奖组件集合:九宫格 / 列表 / 大转盘 / 老虎机 四种常见样式,开箱即用。
适用于 uni-app CLI / HBuilderX 工程,跨端兼容(H5、微信小程序、支付宝小程序、App 等)。组件全部使用 view、image、text 等 uni-app 内置元素与 rpx 单位实现,不依赖任何浏览器专属 DOM API。
目录
安装
npm install uni-lattice-lottery或在 uni-app 项目内直接将 packages/lib 目录复制到工程的 components/ 或 uni_modules/ 下。
组件零运行时依赖,
dependencies为空,体积小、引入即用。
快速上手
以九宫格为例:
<template>
<view class="page">
<LotteryGrid
ref="lotteryRef"
:list="prizeList"
btn-text="开始抽奖"
@submit="onSubmit"
@start="onStart"
@end="onEnd"
/>
</view>
</template>
<script>
import LotteryGrid from 'uni-lattice-lottery/lib/LotteryGrid.vue'
export default {
components: { LotteryGrid },
data() {
return {
prizeList: [
{ id: 1, label: '一等奖', image: '/static/prize1.png' },
{ id: 2, label: '二等奖', image: '/static/prize2.png' },
{ id: 3, label: '三等奖', image: '/static/prize3.png' },
{ id: 4, label: '谢谢参与', image: '/static/prize4.png' },
{ id: 5, label: '优惠券', image: '/static/prize5.png' },
{ id: 6, label: '再来一次', image: '/static/prize6.png' },
{ id: 7, label: '积分 100', image: '/static/prize7.png' },
{ id: 8, label: '神秘大奖', image: '/static/prize8.png' }
]
}
},
methods: {
onSubmit() {
// 1. 请求服务端获取中奖序号(推荐做法,避免前端伪造)
const luckyIndex = Math.floor(Math.random() * this.prizeList.length)
// 2. 调用组件 ref 触发动画
this.$refs.lotteryRef.go(luckyIndex)
},
onStart() {
console.log('抽奖开始')
},
onEnd({ index, prize }) {
uni.showToast({ title: `恭喜获得:${prize.label}`, icon: 'none' })
}
}
}
</script>核心流程:
- 点击按钮 → 组件触发
submit事件。 - 业务侧 → 在
submit中向服务端请求真实中奖序号。 - 调用
ref.go(index)→ 组件播放抽奖动画。 - 动画结束 → 组件触发
end事件,回传{ index, prize }。
通用约定
奖品列表数据结构
四个组件共享同一份 list 结构(字段全部可选,但建议至少提供 label 或 image):
[
{
id: 1, // 业务标识,可选
label: '一等奖', // 文案
image: 'xxx.png' // 图片地址
// 其它自定义字段也会原样回传到 end 事件的 prize 中
}
]通用事件
| 事件名 | 触发时机 | 回调参数 |
| -------- | ------------------------- | --------------------------------------------------------- |
| submit | 用户点击「抽奖」按钮 | 无(Turntable / SlotMachine 中 SlotMachine 无此事件)|
| start | 调用 go() 抽奖动画启动 | 无 |
| end | 抽奖动画结束 | 单奖品组件:{ index, prize };老虎机:Array<{index, prize}> |
通过 ref 调用方法
所有组件都暴露 go(index) 方法,业务方通过 this.$refs.xxx.go(index) 触发动画:
- LotteryGrid / LotteryList / Turntable:
go(index: number),参数为单个中奖序号 - SlotMachine:
go(indexes: number[]),参数为长度等于colCount的序号数组
动画进行中重复调用
go()会被忽略并打印 warning,无需业务方加锁。
组件 API
LotteryGrid 九宫格
经典九宫格抽奖:3×3 布局,中间是抽奖按钮,外圈 8 个奖品按顺序高亮。
Props
| 字段 | 类型 | 默认值 | 说明 |
| ------------- | ------ | -------- | ------------------------------------------------------------------- |
| list | Array | [] | 奖品列表,长度不足 8 会自动补「谢谢参与」,超过 8 会截断到 8 |
| btnText | String | '抽奖' | 中央按钮文案 |
| circleTimes | Number | 3 | 抽奖前先空转的圈数,越大动画越久 |
| velocity | String | 'speed'| 速度模式:'speed' 启停时有缓动;其它任意值表示匀速 |
Events
| 名称 | 参数 |
| -------- | ------------------ |
| submit | — |
| start | — |
| end | { index, prize } |
Methods
go(index: number):序号从 0 开始,0..7。
示例
<LotteryGrid
ref="grid"
:list="prizeList"
:circle-times="4"
velocity="speed"
btn-text="立即抽奖"
@submit="onSubmit"
@end="onEnd"
/>LotteryList 列表抽奖
平铺列表样式,奖品从左到右、从上到下顺序高亮。底部独立按钮。
Props
| 字段 | 类型 | 默认值 | 说明 |
| ------------- | ------ | -------- | --------------------- |
| list | Array | [] | 奖品列表,长度任意 |
| btnText | String | '抽奖' | 按钮文案 |
| circleTimes | Number | 3 | 空转圈数 |
| velocity | String | 'speed'| 速度模式,同九宫格 |
Events / Methods
与 LotteryGrid 完全一致。
示例
<LotteryList
ref="listRef"
:list="prizeList"
:circle-times="2"
@submit="onSubmit"
@end="onEnd"
/>Turntable 大转盘
经典圆盘抽奖:使用 CSS transform: rotate() + cubic-bezier 缓动,动画时长固定 6 秒。
转盘背景图与按钮图由业务侧提供,组件只负责旋转与定位。
Props
| 字段 | 类型 | 默认值 | 说明 |
| ---------- | ------- | ------ | -------------------------------------------------------------------- |
| list | Array | [] | 奖品列表,长度任意;扇区角度 = 360 / list.length |
| tableBg | String | '' | 必填,转盘背景图地址 |
| tableBtn | String | '' | 必填,中央按钮图地址 |
| skew | Boolean | false| 初始角度是否偏半个扇区(适合扇区切割线在 12 点方向的转盘图) |
Events
| 名称 | 参数 | 说明 |
| -------- | ------------------ | --------------------- |
| submit | — | 点击中央按钮 |
| start | — | 旋转开始 |
| end | { index, prize } | 6 秒动画结束后触发 |
Methods
go(index: number):序号从 0 开始。
示例
<Turntable
ref="turntableRef"
:list="prizeList"
table-bg="/static/turntable-bg.png"
table-btn="/static/turntable-btn.png"
:skew="true"
@submit="onSubmit"
@end="onEnd"
/>背景图制作建议:把 N 个扇区均分画在一张正方形图上。组件只负责旋转,最终定位由
360 / list.length自动计算。如果扇区中线不是从 12 点开始而是切割线从 12 点开始,请把skew设为true。
SlotMachine 老虎机
多列滚动老虎机:每列独立旋转,按列依次启动、依次停止。
此组件没有内置按钮,业务方需要在外部放置按钮并调用
ref.go(indexes)。
Props
| 字段 | 类型 | 默认值 | 说明 |
| ------------- | ------ | ------ | -------------------------------------------------------------------- |
| list | Array | [] | 单列奖品池 |
| colCount | Number | 3 | 列数 |
| concatCount | Number | 4 | 内部列表拼接份数(最少 2),数值越大滚动距离越长,动画感越强 |
| moveTime | Number | 4 | 每列动画时长(秒),列与列启动间隔为 0.5 秒 |
Events
| 名称 | 参数 | 说明 |
| -------- | ----------------------------- | ---------------------------- |
| start | — | 第一列开始滚动 |
| end | Array<{ index, prize }> | 所有列停下后触发,长度 = colCount |
注意:老虎机不触发
submit事件,因为没有内置按钮。
Methods
go(indexes: number[]):参数长度必须等于colCount,每个元素是该列的中奖序号(0..list.length-1)。
示例
<template>
<view>
<SlotMachine
ref="slotRef"
:list="symbolList"
:col-count="3"
:concat-count="6"
:move-time="3"
@end="onSlotEnd"
/>
<button @click="draw">抽奖</button>
</view>
</template>
<script>
import SlotMachine from 'uni-lattice-lottery/lib/SlotMachine.vue'
export default {
components: { SlotMachine },
data() {
return {
symbolList: [
{ id: 1, label: '🍒', image: '/static/cherry.png' },
{ id: 2, label: '🍋', image: '/static/lemon.png' },
{ id: 3, label: '🍇', image: '/static/grape.png' },
{ id: 4, label: '⭐', image: '/static/star.png' }
]
}
},
methods: {
draw() {
// 三列分别落到序号 2、2、2(即三个一样:🍇🍇🍇 中奖)
this.$refs.slotRef.go([2, 2, 2])
},
onSlotEnd(results) {
const labels = results.map((r) => r.prize.label).join(' ')
uni.showToast({ title: `结果:${labels}`, icon: 'none' })
}
}
}
</script>LotteryEngine 引擎说明
LotteryGrid 和 LotteryList 共享同一个核心引擎 lib/common/lottery-engine.js,负责:
- 列表预处理(九宫格自动补足 / 截断为 8 项)
- 动画速度曲线(
speed模式启动加速 / 减速缓动) - 帧驱动的
tick(高亮序号迁移) - 命中目标后回调
onend
如果需要自行实现样式或与现有 UI 深度结合,可以直接使用引擎:
import LotteryEngine from 'uni-lattice-lottery/lib/common/lottery-engine.js'
const engine = new LotteryEngine({
type: 'grid', // 'grid' 或 'list'
list: prizeList,
circleTimes: 3,
velocity: 'speed',
onstart: () => {},
ontick: (list, listIndex) => {
// listIndex:当前高亮的奖品下标
},
onend: ({ index, prize }) => {
// 命中时回调
}
})
// 触发动画
engine.go(2)
// 动态更新奖品池
engine.updateList(newList)
// 销毁(组件 unmount 时调用)
engine.dispose()工具函数
lib/common/utils.js 提供以下导出:
| 函数 | 说明 |
| ----------------- | ------------------------------------------ |
| getRandom(a, b) | 返回 [a, b] 之间的整数 |
| getCode() | 生成 6 位字母数字混合短码(用作实例命名) |
| isArray(v) | 数组判断 |
| padGridList(l) | 把列表填充 / 截断到 8 项 |
| isValidIndex(i, len) | 校验下标是否落在 [0, len) 内 |
常见问题
1. 中奖序号应该由谁决定?
强烈建议由服务端决定。前端只负责动画展示。常见时序:
点击按钮 → @submit 事件 → 调用后端接口 → 拿到 luckyIndex → ref.go(luckyIndex) → @end 显示结果如果中奖序号在前端硬编码或随机生成,用户可以通过抓包 / 调试改写概率。
2. 为什么动画进行中再点没反应?
组件内部维护 isMoving / drawing / going 状态,动画期间忽略重复调用并打印 warning,是有意行为,避免动画错乱。如需自定义按钮禁用样式,可以监听 start / end 事件自行控制。
3. 九宫格奖品不到 8 个 / 超过 8 个怎么办?
组件内部使用 padGridList:
- 不足 8 个 → 自动追加
{ label: '谢谢参与' }项 - 超过 8 个 → 自动截断到前 8 项
4. 大转盘最终落在哪个角度?
组件根据 360 / list.length 计算单扇区角度,每次至少旋转 6 圈再加上目标偏移;动画时长固定 6 秒。如发现最终落点和预期不符,多半是 背景图扇区与序号方向不对应:默认序号 0 是从 12 点位置 顺时针 第一个扇区。把 skew 设为 true 可以补偿"切割线在 12 点"的背景图。
5. 老虎机能否每列用不同的奖品池?
当前版本所有列共享同一个 list。如果需要每列不同的奖品池,可以在外层包一层组件分别渲染多个 SlotMachine(每个 colCount=1)并各自调用 go([index])。
6. 跨端兼容性
| 端 | 状态 | | ------------- | -------- | | H5 | ✅ | | 微信小程序 | ✅ | | 支付宝小程序 | ✅ | | 抖音小程序 | ✅ | | App(Vue) | ✅ |
组件未使用
document/window等浏览器专属 API,CSS 动画基于transform,全端可用。
