转盘抽奖
效果
一个简单的转盘抽奖代码,有可配置的参数
- 指定转盘大小
- 抽奖可指定奖品的抽中概率
- 可指定抽奖时转盘的旋转圈数
- 可指定抽奖时转盘旋转的时间
代码
<template>
<view class="lottery-box">
<!-- 转盘 -->
<image
src="/static/images/circle.png"
class="turntable"
mode="scaleToFill"
:animation="turntableAnimationData"
/>
<view :animation="animationData" class="turntable-box">
<!-- 分割线,用于辅助查看转盘分割是否正确 -->
<view class="turntable-line">
<view
class="turntable-line-item"
v-for="(item, index) in list"
:key="index"
:style="{ transform: 'rotate(' + (index * width + width / 2) + 'deg)' }"
></view>
</view>
<!-- 奖品列表 -->
<view class="turntable-list">
<view
class="turntable-list-item"
:style="{
transform: 'rotate(' + index * width + 'deg)',
zIndex: index,
}"
v-for="(iteml, index) in list"
:key="index"
>
<view class="turntable-list-item-box" :style="{ transform: 'rotate(' + index + ')' }">
<div>
{{ iteml.name }}
</div>
</view>
</view>
</view>
</view>
<!-- 中间的指针/抽奖按钮 -->
<image src="/static/images/GO.png" class="btn" mode="scaleToFill" @click="playReward" />
</view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { proxy } = getCurrentInstance();
onMounted(() => {
// 获取奖品列表
width.value = 360 / list.value.length;
// 验证概率
validateProbability();
});
// 抽奖配置
const config = ref({
circles: 5, // 旋转圈数,默认5圈
duration: 1000 * 5, // 旋转时间,单位毫秒,默认5秒
size: 700, // 转盘大小,单位rpx,默认700rpx
});
// 计算转盘中心点位置(用于分隔线定位和奖品定位)
const centerPoint = computed(() => config.value.size / 2);
// 奖品列表
const list = ref([
{
name: '5折',
probability: 1,
},
{
name: '6折',
probability: 1,
},
{
name: '7折',
probability: 1,
},
{
name: '8折',
probability: 1,
},
{
name: '9折',
probability: 1,
},
{
name: '感谢参与',
isNoPrize: true,
probability: 95,
},
]);
const width = ref(0);
const animationData = ref({}); // 奖品动画
const turntableAnimationData = ref({}); // 背景圆盘动画
const btnDisabled = ref(false); // 按钮是否禁用
let runDeg = 0; // 旋转角度
// 验证概率总和是否为100
const validateProbability = () => {
const total = list.value.reduce((sum, item) => sum + item.probability, 0);
if (total !== 100) {
uni.showToast({
title: '奖品概率总和必须为100%',
icon: 'none',
});
return false;
}
return true;
};
// 根据概率获取中奖索引
const getLuckyIndex = () => {
const random = Math.random() * 100;
let currentSum = 0;
for (let i = 0; i < list.value.length; i++) {
currentSum += list.value[i].probability;
if (random <= currentSum) {
return i;
}
}
return list.value.length - 1;
};
// 创建抽奖动画
const animation = (index, duration) => {
const currentList = list.value;
const runNum = config.value.circles; // 使用配置的圈数
// 旋转角度
runDeg =
runDeg + (360 - (runDeg % 360)) + (360 * runNum - index * (360 / currentList.length)) + 1;
// 创建奖品动画
const animationRun = uni.createAnimation({
duration: duration,
timingFunction: 'ease',
});
animationRun.rotate(runDeg).step();
animationData.value = animationRun.export();
// 创建背景圆盘动画
const turntableAnimation = uni.createAnimation({
duration: duration,
timingFunction: 'ease',
});
turntableAnimation.rotate(runDeg).step();
turntableAnimationData.value = turntableAnimation.export();
};
// 发起抽奖
async function playReward() {
if (btnDisabled.value) return;
btnDisabled.value = true; // 按钮禁用
// 验证概率
if (!validateProbability()) return;
// 获取中奖索引
const index = getLuckyIndex();
const duration = config.value.duration; // 使用配置的时间
animation(index, duration);
await new Promise((resolve) => setTimeout(resolve, duration + 1000));
uni.showModal({
content: list.value[index].isNoPrize
? '抱歉,您未中奖'
: `恭喜,获得${list.value[index].name}`,
});
btnDisabled.value = false;
}
</script>
<style lang="scss" scoped>
.lottery-box {
position: relative;
width: v-bind('config.size + "rpx"');
height: v-bind('config.size + "rpx"');
}
// 转盘
.turntable {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 50%;
}
// 转盘容器
.turntable-box {
position: absolute;
left: 0;
top: 0;
z-index: 1;
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
}
/* 分隔线 开始 */
.turntable-line {
position: absolute;
left: 0;
top: 0;
width: inherit;
height: inherit;
z-index: 99;
}
.turntable-line-item {
position: absolute;
left: v-bind('centerPoint + "rpx"');
top: 0;
width: 3rpx;
height: v-bind('centerPoint + "rpx"');
background-color: rgba(228, 55, 14, 0.4);
overflow: hidden;
transform-origin: 50% v-bind('centerPoint + "rpx"');
}
/* 分隔线 结束 */
/* 奖品列表 开始 */
.turntable-list {
position: absolute;
left: 0;
top: 0;
width: inherit;
height: inherit;
z-index: 9999;
}
.turntable-list-item {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
color: #e4370e;
}
.turntable-list-item-box {
position: relative;
display: block;
padding-top: v-bind('(centerPoint * 0.15) + "rpx"');
margin: 0 auto;
text-align: center;
transform-origin: 50% v-bind('centerPoint + "rpx"');
display: flex;
flex-direction: column;
align-items: center;
color: #fb778b;
}
/* 奖品列表 结束 */
/* 抽奖按钮 开始 */
.btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: v-bind('(config.size * 0.167) + "rpx"');
height: v-bind('(config.size * 0.167) + "rpx"');
z-index: 400;
}
/* 抽奖按钮 结束 */
</style>
注意:
- 需要根据项目需求自行修改
- demo代码中用到了两张图片,一张是转盘图片一张是抽奖按钮图片
九宫格抽奖
效果
一个简单的九宫格抽奖代码,有可配置的参数
- 抽奖动画的初始速度
- 抽奖可指定奖品的抽中概率
- 可指定抽奖时旋转的圈数
- 可指定抽奖的动画时间
- 可指定是否每次都从第一个奖品开始抽奖
代码
<template>
<view class="lottery-container">
<view class="lottery-box">
<template v-for="(item, index) in 9" :key="index">
<!-- 奖品 -->
<template v-if="index !== 4">
<view class="lottery-item" :class="{ active: currentIndex === index }">
<view class="lottery-content">
<text>{{ getItemName(index) }}</text>
</view>
</view>
</template>
<!-- 中间的抽奖按钮 -->
<template v-else>
<view class="lottery-btn" @click="playReward" :class="{ 'btn-disabled': btnDisabled }">
<text>{{ btnDisabled ? '抽奖中...' : '开始' }}</text>
</view>
</template>
</template>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, computed, getCurrentInstance, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { proxy } = getCurrentInstance();
onMounted(() => {
if (list.value.length != 8) {
uni.showToast({
title: '奖品数量必须为8个',
icon: 'none',
});
return;
}
validateProbability();
// 初始化lastStopIndex为0(第一个奖品位置)
lastStopIndex.value = 0;
});
// 组件卸载时清理定时器
onUnmounted(() => {
if (timer) {
clearTimeout(timer);
timer = null;
}
});
// 抽奖配置
const config = ref({
speed: 50, // 初始速度(ms)
duration: 5000, // 动画总时间(ms)
circles: 5, // 总圈数
isInit: false, // 是否每次都从第一个奖品开始转
});
// 奖品列表 - 8个奖品,按照九宫格顺序排列(中间为开始按钮)
const list = ref([
{ name: '1元红包', probability: 0 },
{ name: '2元红包', probability: 0 },
{ name: '3元红包', probability: 0 },
{ name: '4元红包', probability: 0 },
{ name: '5元红包', probability: 0 },
{ name: '6元红包', probability: 0 },
{ name: '7元红包', probability: 0 },
{ name: '谢谢参与', probability: 100, isNoPrize: true },
]);
// 九宫格顺序路径 - 顺时针旋转,跳过中间位置
// 九宫格布局:
// [0] [1] [2]
// [3] [4] [5]
// [6] [7] [8]
const path = [0, 1, 2, 5, 8, 7, 6, 3]; // 顺时针路径
const currentIndex = ref(-1); // 当前选中索引
const btnDisabled = ref(false); // 按钮是否禁用
let timer = null; // 定时器
let currentSpeed = config.value.speed; // 当前速度
let cycles = 0; // 当前循环次数
// 记录上一次停止的位置
const lastStopIndex = ref(0);
// 验证概率总和是否为100
const validateProbability = () => {
const total = list.value.reduce((sum, item) => sum + item.probability, 0);
if (total !== 100) {
uni.showToast({
title: '奖品概率总和必须为100%',
icon: 'none',
});
return false;
}
return true;
};
// 根据概率获取中奖索引
const getLuckyIndex = () => {
const random = Math.random() * 100;
let currentSum = 0;
for (let i = 0; i < list.value.length; i++) {
currentSum += list.value[i].probability;
if (random <= currentSum) {
return i;
}
}
return list.value.length - 1;
};
// 获取实际奖品索引
const getItemIndex = (gridIndex) => {
// 九宫格位置与奖品索引的映射关系
const mapping = {
0: 0, // 左上
1: 1, // 中上
2: 2, // 右上
3: 3, // 左中
4: -1, // 中间(抽奖按钮)
5: 4, // 右中
6: 5, // 左下
7: 6, // 中下
8: 7, // 右下
};
return mapping[gridIndex];
};
// 获取奖品名称
const getItemName = (gridIndex) => {
const itemIndex = getItemIndex(gridIndex);
if (itemIndex === -1) return '';
return list.value[itemIndex]?.name || '';
};
// 动画逻辑
const runAnimation = (targetIndex) => {
let pathIndex = 0;
let startTime = Date.now();
let isPreCircle = false;
let hasReachedFirst = config.value.isInit;
const needPreCircle = !config.value.isInit && lastStopIndex.value !== 0;
const actualCircles = needPreCircle ? config.value.circles + 1 : config.value.circles;
const run = async () => {
// 如果需要预转圈且还没到第一个奖品
if (needPreCircle && !hasReachedFirst) {
currentIndex.value = path[pathIndex];
// 如果转到了第一个奖品位置
if (path[pathIndex] === 0) {
hasReachedFirst = true;
isPreCircle = false;
cycles = 0;
startTime = Date.now();
}
pathIndex = (pathIndex + 1) % path.length;
timer = setTimeout(run, config.value.speed);
return;
}
currentIndex.value = path[pathIndex];
// 计算当前圈数
const currentCircles = Math.floor(cycles / path.length);
// 如果达到目标圈数且到达目标位置,则停止
if (currentCircles >= config.value.circles && path[pathIndex] === targetIndex) {
clearTimeout(timer);
timer = null;
btnDisabled.value = false;
lastStopIndex.value = targetIndex;
// 显示中奖信息
await new Promise((resolve) => setTimeout(resolve, 500));
const itemIndex = getItemIndex(targetIndex);
uni.showModal({
content: list.value[itemIndex].isNoPrize
? '抱歉,您未中奖'
: `恭喜,获得${list.value[itemIndex].name}`,
});
return;
}
// 计算当前速度
// 使用指数函数实现渐进式减速
const totalSteps = config.value.circles * path.length;
const currentStep = cycles;
const progress = currentStep / totalSteps;
// 使用指数函数计算速度因子,让减速更加平滑
const speedFactor = Math.pow(progress, 1.5); // 指数1.5提供较为平缓的减速效果
// 初始速度到最终速度的变化范围更大,让减速效果更明显
// 最大减速到初始速度的5倍
currentSpeed = config.value.speed * (1 + speedFactor * 4);
pathIndex = (pathIndex + 1) % path.length;
cycles++;
timer = setTimeout(run, currentSpeed);
};
// 设置初始pathIndex
if (!config.value.isInit && lastStopIndex.value !== 0) {
pathIndex = path.indexOf(lastStopIndex.value);
}
run();
};
// 开始抽奖
const playReward = () => {
if (btnDisabled.value || timer !== null) return;
if (!validateProbability()) return;
btnDisabled.value = true;
cycles = 0;
currentSpeed = config.value.speed;
// 获取中奖索引
const luckyIndex = getLuckyIndex();
// 将奖品索引转换为九宫格位置索引
let targetGridIndex = -1;
for (let i = 0; i < 9; i++) {
if (getItemIndex(i) === luckyIndex) {
targetGridIndex = i;
break;
}
}
// 确保目标位置在路径数组中
if (!path.includes(targetGridIndex)) {
console.error('目标位置不在路径中');
btnDisabled.value = false;
return;
}
runAnimation(targetGridIndex);
};
</script>
<style lang="scss" scoped>
.lottery-container {
padding: 20rpx;
}
.lottery-box {
position: relative;
width: 600rpx;
height: 600rpx;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 10rpx;
background-color: #f5f5f5;
padding: 10rpx;
border-radius: 20rpx;
}
.lottery-item {
background-color: #ffffff;
border-radius: 10rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s ease;
&.active {
background-color: #ffe4e1;
transform: scale(0.95);
}
}
.lottery-content {
text-align: center;
color: #333;
font-size: 28rpx;
}
.lottery-btn {
width: 180rpx;
height: 180rpx;
background-color: #ff4444;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&.btn-disabled {
background-color: #cccccc;
pointer-events: none;
}
&:active {
transform: scale(0.8);
}
}
</style>