SlotMachine - 滚动抽奖
SlotMachine 是一个基于Canvas和Vue 3的老虎机风格滚动抽奖组件,提供多滚轴同步或异步滚动,支持自定义符号、权重配置和中奖规则。
📦 导入
<script setup>
import { SlotMachine } from '@randbox/vue'
// 或者导入类型
import type { SlotMachineProps, SlotMachineResult } from '@randbox/vue'
</script>🚀 基础用法
<template>
<SlotMachine
:reels="reels"
@result="handleResult"
/>
</template>
<script setup>
import { ref } from 'vue'
import { SlotMachine } from '@randbox/vue'
const reels = ref([
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '7️⃣'],
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '7️⃣'],
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '7️⃣']
])
const handleResult = (result) => {
console.log('老虎机结果:', result)
if (result.isJackpot) {
alert('🎉 恭喜中大奖!')
}
}
</script>🎯 高级用法
带权重的滚动抽奖
<template>
<SlotMachine
:reels="reels"
:weights="weights"
:animation-duration="4000"
@result="handleResult"
/>
</template>
<script setup>
import { ref } from 'vue'
const reels = ref([
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '7️⃣'],
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '7️⃣'],
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '7️⃣']
])
// 为每个滚轴设置不同的权重
const weights = ref([
[20, 18, 15, 12, 10, 8, 5, 2], // 第一个滚轴权重
[20, 18, 15, 12, 10, 8, 5, 2], // 第二个滚轴权重
[20, 18, 15, 12, 10, 8, 5, 2] // 第三个滚轴权重
])
const handleResult = (result) => {
console.log('权重抽奖结果:', result)
if (result.combination === '💎💎💎') {
alert('💎 钻石大奖!')
}
}
</script>数字老虎机
<template>
<SlotMachine
:reels="numberReels"
:animation-duration="3000"
@result="handleResult"
/>
</template>
<script setup>
import { ref } from 'vue'
const numberReels = ref([
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
])
const checkJackpot = (results) => {
// 检查是否为顺子或三个相同数字
const [a, b, c] = results.map(r => parseInt(r))
return a === b && b === c // 三个相同数字
}
const handleResult = (result) => {
const isWin = checkJackpot(result.results)
console.log('数字老虎机:', { ...result, isWin })
}
</script>五轴老虎机
<template>
<SlotMachine
:reels="reels"
:weights="weights"
:animation-duration="5000"
@result="handleResult"
/>
</template>
<script setup>
import { ref, computed } from 'vue'
const symbols = ['A', 'K', 'Q', 'J', '10', '9', '8', '7']
const reels = ref(Array(5).fill(symbols))
// 为五轴设置不同的权重分布
const weights = computed(() =>
reels.value.map((_, index) => {
// 中间轴比较容易出好符号
const bonus = index === 2 ? 1.2 : 1.0
return symbols.map((_, i) => (symbols.length - i) * bonus)
})
)
const checkWinningLines = (results) => {
const lines = [
results, // 中间线
// 可以添加更多支付线逻辑
]
return lines.some(line => {
const first = line[0]
return line.every(symbol => symbol === first)
})
}
const handleResult = (result) => {
const hasWin = checkWinningLines(result.results)
console.log('五轴结果:', { ...result, hasWin })
}
</script>完整配置示例
<template>
<div class="slot-machine-container">
<!-- 统计信息 -->
<div class="stats-panel">
<div>总旋转次数: {{ totalSpins }} | 中奖次数: {{ wins }} | 中奖率: {{ winRate }}%</div>
</div>
<SlotMachine
:reels="reels"
:weights="weights"
:animation-duration="3500"
:button-text="isSpinning ? '旋转中...' : '🎰 开始旋转'"
class="casino-slot"
:style="{ border: '3px solid gold', borderRadius: '15px' }"
:disabled="isSpinning"
@game-start="handleGameStart"
@game-end="handleGameEnd"
@result="handleResult"
/>
<!-- 历史记录 -->
<div v-if="history.length > 0" class="history">
<h3>最近旋转记录:</h3>
<div
v-for="(result, index) in history"
:key="index"
class="history-item"
:class="{ 'jackpot': result.isJackpot }"
>
<strong>{{ result.combination }}</strong>
<span v-if="result.isJackpot" class="jackpot-text">🎉 中奖!</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { SlotMachine } from '@randbox/vue'
const isSpinning = ref(false)
const totalSpins = ref(0)
const wins = ref(0)
const history = ref([])
const reels = ref([
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '⭐'],
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '⭐'],
['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '⭐']
])
const weights = ref([
[25, 20, 15, 12, 10, 8, 5, 5],
[25, 20, 15, 12, 10, 8, 5, 5],
[25, 20, 15, 12, 10, 8, 5, 5]
])
const winRate = computed(() =>
totalSpins.value > 0 ? ((wins.value / totalSpins.value) * 100).toFixed(1) : '0.0'
)
const handleGameStart = () => {
isSpinning.value = true
console.log('开始旋转...')
}
const handleGameEnd = (result) => {
isSpinning.value = false
totalSpins.value++
if (result.isJackpot) {
wins.value++
}
history.value = [result, ...history.value.slice(0, 9)]
console.log('旋转结束:', result)
}
const handleResult = (result) => {
console.log('实时结果:', result)
}
</script>
<style scoped>
.slot-machine-container {
padding: 20px;
}
.stats-panel {
margin-bottom: 20px;
text-align: center;
padding: 15px;
background-color: #f8f9fa;
border-radius: 10px;
}
.casino-slot {
background: linear-gradient(145deg, #2c3e50, #34495e);
border: 3px solid #f39c12;
border-radius: 20px;
box-shadow:
0 10px 30px rgba(0,0,0,0.3),
inset 0 2px 10px rgba(255,255,255,0.1);
}
.casino-slot:hover {
transform: scale(1.02);
transition: transform 0.3s ease;
}
.history {
margin-top: 20px;
}
.history-item {
padding: 8px;
margin: 5px 0;
border-radius: 5px;
border: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.history-item.jackpot {
background-color: #fff3cd;
border: 2px solid #ffc107;
}
.jackpot-text {
color: #d4a853;
margin-left: 10px;
}
</style>渐进式奖池
<template>
<div>
<div class="jackpot-info">
<h2>累积奖池: ${{ jackpotAmount.toLocaleString() }}</h2>
<div v-if="lastWin" class="last-win">
上次中奖: {{ lastWin.type }} - ${{ lastWin.amount }}
</div>
</div>
<SlotMachine
:reels="reels"
@result="handleResult"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const jackpotAmount = ref(1000)
const lastWin = ref(null)
const reels = ref([
['💎', '⭐', '🔔', '🍒', '🍇', '🍊', '🍎'],
['💎', '⭐', '🔔', '🍒', '🍇', '🍊', '🍎'],
['💎', '⭐', '🔔', '🍒', '🍇', '🍊', '🍎']
])
const handleResult = (result) => {
// 每次游戏增加奖池
jackpotAmount.value += 10
if (result.combination === '💎💎💎') {
// 中超级大奖
lastWin.value = { type: 'jackpot', amount: jackpotAmount.value }
jackpotAmount.value = 1000 // 重置奖池
alert(`🎉 超级大奖!奖金: $${lastWin.value.amount}`)
} else if (result.isJackpot) {
const amount = 100
lastWin.value = { type: 'normal', amount }
alert(`🎉 中奖!奖金: $${amount}`)
}
}
</script>
<style scoped>
.jackpot-info {
text-align: center;
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
}
.last-win {
color: #90EE90;
margin-top: 10px;
}
</style>多支付线老虎机
<template>
<SlotMachine
:reels="reels"
@result="handleResult"
/>
<div v-if="winInfo.length > 0" class="win-info">
<h3>中奖信息:</h3>
<div
v-for="(win, index) in winInfo"
:key="index"
class="win-line"
>
支付线{{ win.line }}: {{ win.symbol }} x{{ win.count }} = {{ win.payout }}分
</div>
<div class="total-payout">总奖金: {{ totalPayout }}分</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const symbols = ['A', 'K', 'Q', 'J', '10', '9']
const reels = ref(Array(5).fill(symbols))
const winInfo = ref([])
// 定义支付线
const paylines = [
[1, 1, 1, 1, 1], // 中线
[0, 0, 0, 0, 0], // 上线
[2, 2, 2, 2, 2], // 下线
[0, 1, 2, 1, 0], // V形
[2, 1, 0, 1, 2], // 倒V形
]
const totalPayout = computed(() =>
winInfo.value.reduce((sum, win) => sum + win.payout, 0)
)
const calculatePayout = (symbol, count) => {
const payouts = {
'A': [0, 0, 50, 200, 1000],
'K': [0, 0, 25, 100, 500],
'Q': [0, 0, 20, 80, 400],
'J': [0, 0, 15, 60, 300],
'10': [0, 0, 10, 40, 200],
'9': [0, 0, 5, 20, 100],
}
return payouts[symbol]?.[count] || 0
}
const checkPaylines = (results) => {
const wins = []
paylines.forEach((line, lineIndex) => {
const lineSymbols = line.map((row, col) => {
return results[col] // 简化版本
})
// 检查支付线是否中奖
const firstSymbol = lineSymbols[0]
let matchCount = 1
for (let i = 1; i < lineSymbols.length; i++) {
if (lineSymbols[i] === firstSymbol) {
matchCount++
} else {
break
}
}
if (matchCount >= 3) {
wins.push({
line: lineIndex + 1,
symbol: firstSymbol,
count: matchCount,
payout: calculatePayout(firstSymbol, matchCount)
})
}
})
return wins
}
const handleResult = (result) => {
const wins = checkPaylines(result.results)
winInfo.value = wins
if (totalPayout.value > 0) {
alert(`中奖!总奖金: ${totalPayout.value}`)
}
}
</script>
<style scoped>
.win-info {
margin-top: 20px;
padding: 15px;
background-color: #d4edda;
border-radius: 8px;
}
.win-line {
margin: 5px 0;
padding: 5px;
background-color: #c3e6cb;
border-radius: 4px;
}
.total-payout {
margin-top: 10px;
font-weight: bold;
font-size: 1.2em;
color: #155724;
}
</style>📋 API参考
Props
| 属性 | 类型 | 默认值 | 描述 |
|---|---|---|---|
reels | string[][] | 必需 | 滚轴配置,每个子数组代表一个滚轴的符号 |
weights | number[][] | undefined | 权重配置,控制每个滚轴上符号的出现概率 |
animationDuration | number | 3000 | 动画持续时间(毫秒) |
buttonText | string | '开始旋转' | 操作按钮文字 |
class | string | '' | CSS类名 |
style | Record<string, any> | {} | 内联样式 |
disabled | boolean | false | 是否禁用 |
Events
| 事件名 | 参数 | 描述 |
|---|---|---|
result | (result: SlotMachineResult) | 结果回调 |
game-start | () | 游戏开始回调 |
game-end | (result: SlotMachineResult) | 游戏结束回调 |
SlotMachineResult
interface SlotMachineResult {
results: string[] // 每个滚轴的结果
isJackpot: boolean // 是否中大奖
combination: string // 组合字符串(如 "🍎🍎🍎")
}🎨 样式定制
霓虹灯效果
<style scoped>
.neon-slot {
border: 2px solid #ff6b6b;
border-radius: 15px;
box-shadow:
0 0 20px #ff6b6b,
inset 0 0 20px rgba(255, 107, 107, 0.1);
animation: neonGlow 2s ease-in-out infinite alternate;
}
@keyframes neonGlow {
from {
box-shadow:
0 0 20px #ff6b6b,
inset 0 0 20px rgba(255, 107, 107, 0.1);
}
to {
box-shadow:
0 0 30px #ff6b6b,
0 0 40px #ff6b6b,
inset 0 0 30px rgba(255, 107, 107, 0.2);
}
}
</style>响应式布局
<template>
<div class="responsive-container">
<SlotMachine
:reels="reels"
class="responsive-slot"
@result="handleResult"
/>
</div>
</template>
<style scoped>
.responsive-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.responsive-slot {
width: 100%;
height: auto;
}
@media (max-width: 768px) {
.responsive-container {
padding: 10px;
}
.responsive-slot {
font-size: 0.8em;
}
}
</style>🔧 高级功能
中奖规则定制
<script setup>
import { ref } from 'vue'
const symbols = ['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '⭐']
const reels = ref([symbols, symbols, symbols])
// 自定义中奖规则
const checkWinning = (results) => {
const [a, b, c] = results
// 规则1: 三个相同
if (a === b && b === c) return { type: 'triple', multiplier: 10 }
// 规则2: 两个相同
if (a === b || b === c || a === c) return { type: 'double', multiplier: 2 }
// 规则3: 特殊组合
if (results.includes('💎') && results.includes('⭐')) {
return { type: 'special', multiplier: 5 }
}
return { type: 'none', multiplier: 0 }
}
const handleResult = (result) => {
const winInfo = checkWinning(result.results)
console.log('中奖信息:', winInfo)
if (winInfo.multiplier > 0) {
alert(`中奖类型: ${winInfo.type}, 倍数: ${winInfo.multiplier}x`)
}
}
</script>与Vuex/Pinia状态管理集成
<template>
<SlotMachine
:reels="gameStore.reels"
:disabled="gameStore.isSpinning"
@result="gameStore.recordResult"
/>
<div class="game-stats">
<div>总旋转: {{ gameStore.totalSpins }}</div>
<div>中奖率: {{ gameStore.winRate }}%</div>
</div>
</template>
<script setup>
import { useGameStore } from '@/stores/game'
const gameStore = useGameStore()
</script>// stores/game.ts
import { defineStore } from 'pinia'
export const useGameStore = defineStore('slotGame', {
state: () => ({
reels: [
['🍎', '🍊', '🍋'],
['🍎', '🍊', '🍋'],
['🍎', '🍊', '🍋']
],
totalSpins: 0,
totalWins: 0,
isSpinning: false,
results: []
}),
getters: {
winRate: (state) =>
state.totalSpins > 0 ? ((state.totalWins / state.totalSpins) * 100).toFixed(1) : '0'
},
actions: {
recordResult(result) {
this.totalSpins++
if (result.isJackpot) {
this.totalWins++
}
this.results.unshift(result)
}
}
})🎯 最佳实践
1. 符号设计建议
<script setup>
// 使用易区分的符号
const goodSymbols = ['🍎', '🍊', '🍋', '🍒', '🍇', '🔔', '💎', '⭐']
// 避免相似符号
const badSymbols = ['😀', '😃', '😄', '😁'] // 太相似,难以区分
</script>2. 权重平衡
<script setup>
// 平衡的权重分配
const balancedWeights = ref([
[30, 25, 20, 15, 5, 3, 1, 1], // 常见符号权重高,稀有符号权重低
[30, 25, 20, 15, 5, 3, 1, 1],
[30, 25, 20, 15, 5, 3, 1, 1]
])
</script>3. 性能优化
<script setup>
import { ref, computed, shallowRef } from 'vue'
// 使用shallowRef优化大数组
const reels = shallowRef([...])
// 使用computed缓存复杂计算
const processedReels = computed(() =>
generateReels(complexity.value)
)
</script>🐛 常见问题
Q: 如何实现真正的随机性?
A: 组件使用RandBox的Mersenne Twister算法,提供高质量的随机数生成。
Q: 滚轴数量有限制吗?
A: 理论上没有限制,但建议不超过7个滚轴以保证性能和用户体验。
Q: 可以实现异步滚动吗?
A: 是的,组件内部已实现异步滚动效果,每个滚轴会在不同时间停止。
Q: 如何调整中奖概率?
A: 通过weights属性调整每个符号的权重,权重越高出现概率越大。
Q: 如何与Vue Router集成?
A: 可以在路由组件中正常使用,或者通过路由参数动态配置滚轴。
🔗 相关链接
最后更新于: