介绍
提供了几个组合式函数,封装了九宫格、幸运大转盘、老虎机等抽奖的主要逻辑。布局与样式需自行实现,可拷贝案例代码再进行修改。
引入
js
import { useLuckyGrid, useLuckyWheel, useSlotMachine } from 'sard-uniapp'代码演示
九宫格
遍历 useLuckyGrid 返回的 grids 计算属性来渲染九宫格;activeIndex 计算属性用于判断当前活动的奖品。
点击按钮后调用 play 方法开始动画;
在确定抽中的奖品下标时调用 stop 方法开始减速动画;
在动画完全停止后调用 complete 回调函数展示抽中的奖品。
vue
<template>
<view class="grid-box">
<view v-for="item in grids" :key="item" class="grid-item">
<view
v-if="item > -1"
:class="['prize-item', { active: item === activeIndex }]"
>
<view class="prize-icon">
<sar-icon family="cake" :name="prizes[item]?.icon" />
</view>
<view class="prize-name">{{ prizes[item]?.name }}</view>
</view>
<view v-else class="play-btn" @click="onPlay()">点我抽奖</view>
</view>
</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon family="cake" :name="winningPrize.icon" />
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">“{{ winningPrize.name }}”</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="ts">
import { useLuckyGrid } from 'sard-uniapp'
import { onMounted, ref } from 'vue'
import { getPrizesApi, getPrizeApi, type Prize } from './utils'
const prizes = ref<Prize[]>([])
const winningPrize = ref<Prize>()
const dialogVisible = ref(false)
const { grids, activeIndex, play, stop, pause } = useLuckyGrid({
complete: (index) => {
winningPrize.value = prizes.value[index]
dialogVisible.value = true
},
})
const onPlay = () => {
play()
getPrizeApi(8)
.then((prize) => {
stop(prizes.value.findIndex((item) => item.id === prize.id))
})
.catch(() => pause())
}
onMounted(() => {
getPrizesApi(8).then((res) => {
prizes.value = res
})
})
</script>
<style lang="scss" scoped>
.grid-box {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
width: 660rpx;
height: 660rpx;
margin: 0 auto;
padding: 10rpx;
border: 10rpx solid #ffddcf;
border-radius: 32rpx;
background-color: #fffbef;
}
.grid-item {
box-sizing: border-box;
width: 33.3333%;
padding: 10rpx;
}
.prize-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
background-color: #fff0cb;
border: 2rpx solid rgba(0, 0, 0, 0.02);
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.1);
&.active {
background-color: #ffd166;
}
}
.prize-icon {
font-size: 72rpx;
line-height: 1;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 20rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 0;
}
.dialog-prize-icon {
font-size: 160rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>vue
<template>
<view class="grid-box">
<view v-for="item in grids" :key="item" class="grid-item">
<view
v-if="item > -1"
:class="['prize-item', { active: item === activeIndex }]"
>
<view class="prize-icon">
<sar-icon family="cake" :name="prizes[item]?.icon" />
</view>
<view class="prize-name">{{ prizes[item]?.name }}</view>
</view>
<view v-else class="play-btn" @click="onPlay()">点我抽奖</view>
</view>
</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon family="cake" :name="winningPrize.icon" />
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">“{{ winningPrize.name }}”</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="js">
import { useLuckyGrid } from "sard-uniapp";
import { onMounted, ref } from "vue";
import { getPrizesApi, getPrizeApi } from "./utils";
const prizes = ref([]);
const winningPrize = ref();
const dialogVisible = ref(false);
const { grids, activeIndex, play, stop, pause } = useLuckyGrid({
complete: (index) => {
winningPrize.value = prizes.value[index];
dialogVisible.value = true;
}
});
const onPlay = () => {
play();
getPrizeApi(8).then((prize) => {
stop(prizes.value.findIndex((item) => item.id === prize.id));
}).catch(() => pause());
};
onMounted(() => {
getPrizesApi(8).then((res) => {
prizes.value = res;
});
});
</script>
<style lang="scss" scoped>
.grid-box {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
width: 660rpx;
height: 660rpx;
margin: 0 auto;
padding: 10rpx;
border: 10rpx solid #ffddcf;
border-radius: 32rpx;
background-color: #fffbef;
}
.grid-item {
box-sizing: border-box;
width: 33.3333%;
padding: 10rpx;
}
.prize-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
background-color: #fff0cb;
border: 2rpx solid rgba(0, 0, 0, 0.02);
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.1);
&.active {
background-color: #ffd166;
}
}
.prize-icon {
font-size: 72rpx;
line-height: 1;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 20rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 0;
}
.dialog-prize-icon {
font-size: 160rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>utils.ts
ts
import { random } from 'sard-uniapp'
export function getPrizes() {
return [
{ id: 1, name: '华夫饼', icon: 'huafubing' },
{ id: 2, name: '芝士蛋糕', icon: 'zhishidangao' },
{ id: 3, name: '曲奇饼干', icon: 'quqibinggan' },
{ id: 4, name: '布丁', icon: 'buding' },
{ id: 5, name: '汉堡', icon: 'hanbao' },
{ id: 6, name: '吐司', icon: 'tusi' },
{ id: 7, name: '提拉米苏', icon: 'tilamisu' },
{ id: 8, name: '三明治', icon: 'sanmingzhi' },
{ id: 9, name: '泡芙', icon: 'paofu' },
{ id: 10, name: '千层蛋糕', icon: 'qiancengdangao' },
{ id: 11, name: '蛋挞', icon: 'danta' },
{ id: 12, name: '虎皮卷', icon: 'hupijuan' },
{ id: 13, name: '果冻', icon: 'guodong' },
{ id: 14, name: '蛋黄酥', icon: 'danhuangsu' },
{ id: 15, name: '巧克力蛋卷', icon: 'qiaokelidanjuan' },
{ id: 16, name: '苏打饼干', icon: 'sudabinggan' },
{ id: 17, name: '奥利奥', icon: 'aoliao' },
{ id: 18, name: '巧克力', icon: 'qiaokeli' },
]
}
export interface Prize {
id: number
name: string
icon: string
}
export const getPrizesApi = async (count: number) => {
return new Promise<Prize[]>((resolve) =>
setTimeout(resolve, 150, getPrizes().slice(0, count)),
)
}
export const getPrizeApi = async (count: number) => {
return new Promise<Prize>((resolve) =>
setTimeout(resolve, 150, getPrizes()[random(0, count - 1)]),
)
}
export const getMultiPrizeApi = async (columns: number[]) => {
return new Promise<Prize[]>((resolve) =>
setTimeout(
resolve,
150,
columns.map((column) => getPrizes()[random(0, column - 1)]),
),
)
}js
import { random } from "sard-uniapp";
function getPrizes() {
return [
{ id: 1, name: "\u534E\u592B\u997C", icon: "huafubing" },
{ id: 2, name: "\u829D\u58EB\u86CB\u7CD5", icon: "zhishidangao" },
{ id: 3, name: "\u66F2\u5947\u997C\u5E72", icon: "quqibinggan" },
{ id: 4, name: "\u5E03\u4E01", icon: "buding" },
{ id: 5, name: "\u6C49\u5821", icon: "hanbao" },
{ id: 6, name: "\u5410\u53F8", icon: "tusi" },
{ id: 7, name: "\u63D0\u62C9\u7C73\u82CF", icon: "tilamisu" },
{ id: 8, name: "\u4E09\u660E\u6CBB", icon: "sanmingzhi" },
{ id: 9, name: "\u6CE1\u8299", icon: "paofu" },
{ id: 10, name: "\u5343\u5C42\u86CB\u7CD5", icon: "qiancengdangao" },
{ id: 11, name: "\u86CB\u631E", icon: "danta" },
{ id: 12, name: "\u864E\u76AE\u5377", icon: "hupijuan" },
{ id: 13, name: "\u679C\u51BB", icon: "guodong" },
{ id: 14, name: "\u86CB\u9EC4\u9165", icon: "danhuangsu" },
{ id: 15, name: "\u5DE7\u514B\u529B\u86CB\u5377", icon: "qiaokelidanjuan" },
{ id: 16, name: "\u82CF\u6253\u997C\u5E72", icon: "sudabinggan" },
{ id: 17, name: "\u5965\u5229\u5965", icon: "aoliao" },
{ id: 18, name: "\u5DE7\u514B\u529B", icon: "qiaokeli" }
];
}
const getPrizesApi = async (count) => {
return new Promise(
(resolve) => setTimeout(resolve, 150, getPrizes().slice(0, count))
);
};
const getPrizeApi = async (count) => {
return new Promise(
(resolve) => setTimeout(resolve, 150, getPrizes()[random(0, count - 1)])
);
};
const getMultiPrizeApi = async (columns) => {
return new Promise(
(resolve) => setTimeout(
resolve,
150,
columns.map((column) => getPrizes()[random(0, column - 1)])
)
);
};
export {
getMultiPrizeApi,
getPrizeApi,
getPrizes,
getPrizesApi
};自定义宫格行列
useLuckyGrid 默认会生成 3x3 个格子,也可以自定义行列数。
宫格的外边缘格子下标从左上角以 0 开始逆时针递增,内部的格子下标从左到右、从上到下以 -1 开始递减。
centerSize 计算属性可用于获取中间格子的行列数。
vue
<template>
<view class="grid-box">
<view
v-for="item in grids"
:key="item"
class="grid-item"
:style="{ width: 100 / column + '%' }"
>
<view
class="grid-item-inner"
:style="{
width: item === -1 ? centerSize.column * 100 + '%' : '',
height: item === -1 ? centerSize.row * 100 + '%' : '',
}"
>
<view
v-if="item > -1"
:class="['prize-item', { active: item === activeIndex }]"
>
<view class="prize-icon">
<sar-icon family="cake" :name="prizes[item]?.icon" />
</view>
<view class="prize-name">{{ prizes[item]?.name }}</view>
</view>
<view v-else-if="item === -1" class="play-btn" @click="onPlay()">
点我抽奖
</view>
</view>
</view>
</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon family="cake" :name="winningPrize.icon" />
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">“{{ winningPrize.name }}”</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="ts">
import { useLuckyGrid, getGridPrizeCount } from 'sard-uniapp'
import { computed, ref } from 'vue'
import { getPrizeApi, getPrizes, type Prize } from './utils'
const row = ref(4)
const column = ref(5)
const prizes = computed(() => {
return getPrizes().slice(0, getGridPrizeCount(row.value, column.value))
})
const winningPrize = ref<Prize>()
const dialogVisible = ref(false)
const { grids, activeIndex, centerSize, play, stop, pause } = useLuckyGrid({
row,
column,
complete: (index) => {
winningPrize.value = prizes.value[index]
dialogVisible.value = true
},
})
const onPlay = () => {
play()
getPrizeApi(getGridPrizeCount(row.value, column.value))
.then((prize) => {
stop(prizes.value.findIndex((item) => item.id === prize.id))
})
.catch(() => pause())
}
</script>
<style lang="scss" scoped>
.grid-box {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
width: 660rpx;
height: 600rpx;
margin: 0 auto;
padding: 10rpx;
border: 10rpx solid #ffddcf;
border-radius: 32rpx;
background-color: #fffbef;
}
.grid-item {
position: relative;
box-sizing: border-box;
width: 33.3333%;
}
.grid-item-inner {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 10rpx;
}
.prize-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
background-color: #fff0cb;
border: 2rpx solid rgba(0, 0, 0, 0.02);
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.1);
&.active {
background-color: #ffd166;
}
}
.prize-icon {
font-size: 48rpx;
line-height: 1;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 12rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 0;
}
.dialog-prize-icon {
font-size: 160rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>vue
<template>
<view class="grid-box">
<view
v-for="item in grids"
:key="item"
class="grid-item"
:style="{ width: 100 / column + '%' }"
>
<view
class="grid-item-inner"
:style="{
width: item === -1 ? centerSize.column * 100 + '%' : '',
height: item === -1 ? centerSize.row * 100 + '%' : '',
}"
>
<view
v-if="item > -1"
:class="['prize-item', { active: item === activeIndex }]"
>
<view class="prize-icon">
<sar-icon family="cake" :name="prizes[item]?.icon" />
</view>
<view class="prize-name">{{ prizes[item]?.name }}</view>
</view>
<view v-else-if="item === -1" class="play-btn" @click="onPlay()">
点我抽奖
</view>
</view>
</view>
</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon family="cake" :name="winningPrize.icon" />
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">“{{ winningPrize.name }}”</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="js">
import { useLuckyGrid, getGridPrizeCount } from "sard-uniapp";
import { computed, ref } from "vue";
import { getPrizeApi, getPrizes } from "./utils";
const row = ref(4);
const column = ref(5);
const prizes = computed(() => {
return getPrizes().slice(0, getGridPrizeCount(row.value, column.value));
});
const winningPrize = ref();
const dialogVisible = ref(false);
const { grids, activeIndex, centerSize, play, stop, pause } = useLuckyGrid({
row,
column,
complete: (index) => {
winningPrize.value = prizes.value[index];
dialogVisible.value = true;
}
});
const onPlay = () => {
play();
getPrizeApi(getGridPrizeCount(row.value, column.value)).then((prize) => {
stop(prizes.value.findIndex((item) => item.id === prize.id));
}).catch(() => pause());
};
</script>
<style lang="scss" scoped>
.grid-box {
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
width: 660rpx;
height: 600rpx;
margin: 0 auto;
padding: 10rpx;
border: 10rpx solid #ffddcf;
border-radius: 32rpx;
background-color: #fffbef;
}
.grid-item {
position: relative;
box-sizing: border-box;
width: 33.3333%;
}
.grid-item-inner {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 10rpx;
}
.prize-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
background-color: #fff0cb;
border: 2rpx solid rgba(0, 0, 0, 0.02);
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.1);
&.active {
background-color: #ffd166;
}
}
.prize-icon {
font-size: 48rpx;
line-height: 1;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 12rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 24rpx;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: inset 0 -6rpx 0 rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 0;
}
.dialog-prize-icon {
font-size: 160rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>大转盘
大转盘与宫格抽奖逻辑类似。
count 选项用于指定奖品数量;
useLuckyWheel 函数返回的 sectorDegrees 计算属性为每个奖品所占的角度;
degrees 计算属性为整个大转盘当前旋转的角度。
vue
<template>
<view class="disc">
<view
class="sectors"
:style="{
backgroundImage: repeatingConicGradient,
transform: `rotate(${degrees}deg)`,
}"
>
<view
v-for="(prize, index) in prizes"
:key="prize.id"
class="sector"
:class="{ 'sector-event': index % 2 === 0 }"
:style="{
transform: `rotate(${-index * sectorDegrees}deg)`,
}"
>
<view class="sector-half">
<view class="prize-icon">
<sar-icon family="cake" :name="prize.icon" />
</view>
<view class="prize-name">{{ prize.name }}</view>
</view>
</view>
</view>
<view class="play-btn" @click="onPlay">
<text>点我抽奖</text>
<view class="play-btn-arrow"></view>
</view>
</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon family="cake" :name="winningPrize.icon" />
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">“{{ winningPrize.name }}”</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="ts">
import { useLuckyWheel } from 'sard-uniapp'
import { computed, onMounted, ref } from 'vue'
import { getPrizesApi, getPrizeApi, type Prize } from './utils'
const prizes = ref<Prize[]>([])
const winningPrize = ref<Prize>()
const dialogVisible = ref(false)
const { degrees, sectorDegrees, play, stop, pause } = useLuckyWheel({
count: computed(() => prizes.value.length),
complete: (index) => {
winningPrize.value = prizes.value[index]
dialogVisible.value = true
},
})
const repeatingConicGradient = computed(() => {
const angle = sectorDegrees.value
return (
`repeating-conic-gradient(` +
`var(--sector-bg) ${-angle / 2}deg, var(--sector-bg) ${angle / 2}deg,` +
`transparent ${angle / 2 + 0.5}deg, transparent ${angle / 2 + angle}deg,` +
`var(--sector-bg) ${angle / 2 + angle + 0.5}deg)`
)
})
const onPlay = () => {
play()
getPrizeApi(8)
.then((prize) => {
stop(prizes.value.findIndex((item) => item.id === prize.id))
})
.catch(() => pause())
}
onMounted(() => {
getPrizesApi(8).then((res) => {
prizes.value = res
})
})
</script>
<style lang="scss" scoped>
.disc {
position: relative;
box-sizing: border-box;
width: 660rpx;
height: 660rpx;
margin: 0 auto;
border: 10rpx solid #ffddcf;
border-radius: 50%;
overflow: hidden;
--sector-bg: #fff0cb;
}
.sectors {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #fffbef;
}
.sector {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.sector-half {
box-sizing: border-box;
width: 100%;
height: 50%;
display: flex;
flex-direction: column;
align-items: center;
}
.prize-icon {
margin-top: 20rpx;
font-size: 72rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 20rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
width: 160rpx;
height: 160rpx;
border: 8rpx solid #fff;
border-radius: 50%;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.play-btn-arrow {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%) scaleX(0.4);
&::before {
content: '';
display: flex;
width: 60rpx;
height: 60rpx;
background-color: #f02020;
transform: rotate(45deg);
}
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 0;
}
.dialog-prize-icon {
font-size: 160rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>vue
<template>
<view class="disc">
<view
class="sectors"
:style="{
backgroundImage: repeatingConicGradient,
transform: `rotate(${degrees}deg)`,
}"
>
<view
v-for="(prize, index) in prizes"
:key="prize.id"
class="sector"
:class="{ 'sector-event': index % 2 === 0 }"
:style="{
transform: `rotate(${-index * sectorDegrees}deg)`,
}"
>
<view class="sector-half">
<view class="prize-icon">
<sar-icon family="cake" :name="prize.icon" />
</view>
<view class="prize-name">{{ prize.name }}</view>
</view>
</view>
</view>
<view class="play-btn" @click="onPlay">
<text>点我抽奖</text>
<view class="play-btn-arrow"></view>
</view>
</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon family="cake" :name="winningPrize.icon" />
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">“{{ winningPrize.name }}”</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="js">
import { useLuckyWheel } from "sard-uniapp";
import { computed, onMounted, ref } from "vue";
import { getPrizesApi, getPrizeApi } from "./utils";
const prizes = ref([]);
const winningPrize = ref();
const dialogVisible = ref(false);
const { degrees, sectorDegrees, play, stop, pause } = useLuckyWheel({
count: computed(() => prizes.value.length),
complete: (index) => {
winningPrize.value = prizes.value[index];
dialogVisible.value = true;
}
});
const repeatingConicGradient = computed(() => {
const angle = sectorDegrees.value;
return `repeating-conic-gradient(var(--sector-bg) ${-angle / 2}deg, var(--sector-bg) ${angle / 2}deg,transparent ${angle / 2 + 0.5}deg, transparent ${angle / 2 + angle}deg,var(--sector-bg) ${angle / 2 + angle + 0.5}deg)`;
});
const onPlay = () => {
play();
getPrizeApi(8).then((prize) => {
stop(prizes.value.findIndex((item) => item.id === prize.id));
}).catch(() => pause());
};
onMounted(() => {
getPrizesApi(8).then((res) => {
prizes.value = res;
});
});
</script>
<style lang="scss" scoped>
.disc {
position: relative;
box-sizing: border-box;
width: 660rpx;
height: 660rpx;
margin: 0 auto;
border: 10rpx solid #ffddcf;
border-radius: 50%;
overflow: hidden;
--sector-bg: #fff0cb;
}
.sectors {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #fffbef;
}
.sector {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.sector-half {
box-sizing: border-box;
width: 100%;
height: 50%;
display: flex;
flex-direction: column;
align-items: center;
}
.prize-icon {
margin-top: 20rpx;
font-size: 72rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 20rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
width: 160rpx;
height: 160rpx;
border: 8rpx solid #fff;
border-radius: 50%;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.play-btn-arrow {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%) scaleX(0.4);
&::before {
content: '';
display: flex;
width: 60rpx;
height: 60rpx;
background-color: #f02020;
transform: rotate(45deg);
}
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 0;
}
.dialog-prize-icon {
font-size: 160rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
line-height: 32rpx;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>老虎机
老虎机可以一次性抽取多个奖品。
columns 选项用于指定列数以及每列的奖品数量;
offset 计算属性保存着每一列的百分比偏移量,每一列奖品容器的高度需和奖品高度一致;
为了实现首位相接的效果,还需在渲染的奖品列表的末尾加上第一个奖品。
vue
<template>
<view class="reels">
<view v-for="(translateY, index) in offset" :key="index" class="reel-box">
<view class="reel" :style="{ transform: `translateY(${translateY}%)` }">
<view
v-for="(prize, index) in renderedPrizes"
:key="index"
class="prize-item"
>
<view class="prize-icon">
<sar-icon family="cake" :name="prize.icon" />
</view>
<view class="prize-name">{{ prize.name }}</view>
</view>
</view>
</view>
</view>
<view class="play-btn" @click="onPlay()">点我抽奖</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize.length > 0" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon
v-for="(prize, index) in winningPrize"
:key="index"
family="cake"
:name="prize.icon"
/>
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">
“{{ winningPrize.map((prize) => prize.name).join('、') }}”
</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="ts">
import { useSlotMachine } from 'sard-uniapp'
import { computed, onMounted, ref } from 'vue'
import { getPrizesApi, getMultiPrizeApi, type Prize } from './utils'
const prizes = ref<Prize[]>([])
const renderedPrizes = computed(() => {
return prizes.value.length === 0 ? [] : [...prizes.value, prizes.value[0]]
})
const winningPrize = ref<Prize[]>([])
const dialogVisible = ref(false)
const columns = ref<number[]>([])
const { play, stop, pause, offset } = useSlotMachine({
columns,
complete: (indexes) => {
winningPrize.value = indexes.map((index) => prizes.value[index])
dialogVisible.value = true
},
})
const onPlay = () => {
play()
getMultiPrizeApi([8, 8, 8])
.then((multiPrizes) => {
stop(
multiPrizes.map((prize) =>
prizes.value.findIndex((item) => item.id === prize.id),
),
)
})
.catch(() => pause())
}
onMounted(() => {
getPrizesApi(8).then((res) => {
columns.value = [8, 8, 8]
prizes.value = res
})
})
</script>
<style scoped lang="scss">
.reels {
box-sizing: border-box;
display: flex;
height: 240rpx;
gap: 20rpx;
padding: 0 20rpx;
border: 10rpx solid #ffddcf;
border-radius: 32rpx;
background-color: #fffbef;
}
.reel-box {
flex: 1;
height: 100%;
overflow: hidden;
background-color: #fff0cb;
}
.reel {
display: flex;
flex-direction: column-reverse;
height: 100%;
}
.prize-item {
flex: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.prize-icon {
font-size: 72rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 20rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
display: flex;
justify-content: center;
align-items: center;
height: 100rpx;
margin-top: 20rpx;
border-radius: 24rpx;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: inset 0 -8rpx 0 rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 32rpx;
}
.dialog-prize-icon {
display: flex;
gap: 32rpx;
font-size: 96rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
text-align: center;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>vue
<template>
<view class="reels">
<view v-for="(translateY, index) in offset" :key="index" class="reel-box">
<view class="reel" :style="{ transform: `translateY(${translateY}%)` }">
<view
v-for="(prize, index) in renderedPrizes"
:key="index"
class="prize-item"
>
<view class="prize-icon">
<sar-icon family="cake" :name="prize.icon" />
</view>
<view class="prize-name">{{ prize.name }}</view>
</view>
</view>
</view>
</view>
<view class="play-btn" @click="onPlay()">点我抽奖</view>
<sar-dialog
v-model:visible="dialogVisible"
:show-cancel="false"
confirm-text="收下"
>
<view v-if="winningPrize.length > 0" class="dialog-prize">
<view class="dialog-prize-icon">
<sar-icon
v-for="(prize, index) in winningPrize"
:key="index"
family="cake"
:name="prize.icon"
/>
</view>
<view class="dialog-prize-title">
<text>恭喜你抽中</text>
<text class="dialog-prize-name">
“{{ winningPrize.map((prize) => prize.name).join('、') }}”
</text>
</view>
</view>
</sar-dialog>
</template>
<script setup lang="js">
import { useSlotMachine } from "sard-uniapp";
import { computed, onMounted, ref } from "vue";
import { getPrizesApi, getMultiPrizeApi } from "./utils";
const prizes = ref([]);
const renderedPrizes = computed(() => {
return prizes.value.length === 0 ? [] : [...prizes.value, prizes.value[0]];
});
const winningPrize = ref([]);
const dialogVisible = ref(false);
const columns = ref([]);
const { play, stop, pause, offset } = useSlotMachine({
columns,
complete: (indexes) => {
winningPrize.value = indexes.map((index) => prizes.value[index]);
dialogVisible.value = true;
}
});
const onPlay = () => {
play();
getMultiPrizeApi([8, 8, 8]).then((multiPrizes) => {
stop(
multiPrizes.map(
(prize) => prizes.value.findIndex((item) => item.id === prize.id)
)
);
}).catch(() => pause());
};
onMounted(() => {
getPrizesApi(8).then((res) => {
columns.value = [8, 8, 8];
prizes.value = res;
});
});
</script>
<style scoped lang="scss">
.reels {
box-sizing: border-box;
display: flex;
height: 240rpx;
gap: 20rpx;
padding: 0 20rpx;
border: 10rpx solid #ffddcf;
border-radius: 32rpx;
background-color: #fffbef;
}
.reel-box {
flex: 1;
height: 100%;
overflow: hidden;
background-color: #fff0cb;
}
.reel {
display: flex;
flex-direction: column-reverse;
height: 100%;
}
.prize-item {
flex: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.prize-icon {
font-size: 72rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.prize-name {
margin-top: 20rpx;
font-weight: 500;
font-size: 22rpx;
color: #eb7e50;
line-height: 32rpx;
}
.play-btn {
display: flex;
justify-content: center;
align-items: center;
height: 100rpx;
margin-top: 20rpx;
border-radius: 24rpx;
font-weight: 500;
color: #fff;
background-color: #f02020;
box-shadow: inset 0 -8rpx 0 rgba(0, 0, 0, 0.2);
cursor: pointer;
}
.dialog-prize {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 60rpx 32rpx;
}
.dialog-prize-icon {
display: flex;
gap: 32rpx;
font-size: 96rpx;
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.2);
}
.dialog-prize-title {
margin-top: 32rpx;
font-size: 32rpx;
text-align: center;
}
.dialog-prize-name {
color: #eb7e50;
font-weight: 600;
}
</style>API
useLuckyGrid
ts
function useLuckyGrid(options?: UseLuckyGridOptions): UseLuckyGridReturnUseLuckyGridOptions
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| row | 宫格行数 | number | Ref<number> | 3 |
| column | 宫格列数 | number | Ref<number> | 3 |
| minSpeed | 最小加速度 | number | Ref<number> | 0.1 |
| maxSpeed | 最大加速度 | number | Ref<number> | 0.4 |
| accelTime | 加速时间,单位毫秒 | number | Ref<number> | 2500 |
| decelTime | 减速时间,单位毫秒 | number | Ref<number> | 2500 |
| easeIn | 加速缓动公式 | (progress: number) => number | (k) => k \* k |
| easeOut | 减速缓动公式 | (progress: number) => number | (k) => k \* (2 - k) |
| startDelay | 加速运动前的等待时间,单位毫秒 | number | Ref<number> | 0 |
| endDelay | 减速运动后的等待时间,单位毫秒 | number | Ref<number> | 500 |
| complete | 完成抽奖动画后的回调 | (index: number) => void | - |
UseLuckyGridReturn
| 属性 | 描述 | 类型 |
|---|---|---|
| play | 调用后开始抽奖 | () => void |
| stop | 调用后开始减速动画 | (index?: number) => void |
| pause | 调用后暂停动画 | () => void |
| reset | 调用后重置动画 | () => void |
| playing | 用于判断当前是否正在动画 | ComputedRef<boolean> |
| activeIndex | 用于判断当前活动的下标 | ComputedRef<number | undefined> |
| grids | 用于渲染成宫格,并根据下标判断奖品和非奖品位置 | ComputedRef<number[]> |
| centerSize | 用于渲染中间非奖品格子的行列数 | ComputedRef\<{ row: number; column: number; }> |
useLuckyWheel
ts
function useLuckyWheel(options?: UseLuckyWheelOptions): UseLuckyWheelReturnUseLuckyWheelOptions
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| count | 奖品个数 | number | Ref<number> | 8 |
| minSpeed | 最小加速度 | number | Ref<number> | 0.01 |
| maxSpeed | 最大加速度 | number | Ref<number> | 0.4 |
| accelTime | 加速时间,单位毫秒 | number | Ref<number> | 2500 |
| decelTime | 减速时间,单位毫秒 | number | Ref<number> | 2500 |
| easeIn | 加速缓动公式 | (progress: number) => number | (k) => k \* k |
| easeOut | 减速缓动公式 | (progress: number) => number | (k) => k \* (2 - k) |
| startDelay | 加速运动前的等待时间,单位毫秒 | number | Ref<number> | 0 |
| endDelay | 减速运动后的等待时间,单位毫秒 | number | Ref<number> | 300 |
| complete | 完成抽奖动画后的回调 | (index: number) => void | - |
UseLuckyWheelReturn
| 属性 | 描述 | 类型 |
|---|---|---|
| play | 调用后开始抽奖 | () => void |
| stop | 调用后开始减速动画 | (index?: number) => void |
| pause | 调用后暂停动画 | () => void |
| reset | 调用后重置动画 | () => void |
| playing | 用于判断当前是否正在动画 | ComputedRef<boolean> |
| sectorDegrees | 每个扇形奖品所占的角度,单位 deg | ComputedRef<number> |
| degrees | 当前转盘渲染的角度,单位 deg | ComputedRef<number> |
useSlotMachine
ts
function useSlotMachine(options: UseSlotMachineOptions): UseSlotMachineReturnUseSlotMachineOptions
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| columns | 奖品个数 | number[] | unknown[][] | Ref<number[] | unknown[][]> | [] |
| staggerDelay | 列间交错延迟时间,单位毫秒 | number | Ref<number> | 600 |
| minSpeed | 最小加速度 | number | Ref<number> | 0.01 |
| maxSpeed | 最大加速度 | number | Ref<number> | 0.3 |
| accelTime | 加速时间,单位毫秒 | number | Ref<number> | 2500 |
| decelTime | 减速时间,单位毫秒 | number | Ref<number> | 2500 |
| easeIn | 加速缓动公式 | (progress: number) => number | (k) => k \* k |
| easeOut | 减速缓动公式 | (progress: number) => number | (k) => k \* (2 - k) |
| startDelay | 加速运动前的等待时间,单位毫秒 | number | Ref<number> | 0 |
| endDelay | 减速运动后的等待时间,单位毫秒 | number | Ref<number> | 300 |
| complete | 完成抽奖动画后的回调 | (index: number[]) => void | - |
UseSlotMachineReturn
| 属性 | 描述 | 类型 |
|---|---|---|
| play | 调用后开始抽奖 | () => void |
| stop | 调用后开始减速动画 | (index?: number[]) => void |
| pause | 调用后暂停动画 | () => void |
| reset | 调用后重置动画 | () => void |
| playing | 用于判断当前是否正在动画 | ComputedRef<boolean> |
| offset | 每一列的当前偏移量 | ComputedRef<number[]> |