<template>
|
<view class="lottery-container">
|
<view class="lottery-title">抽奖转盘</view>
|
|
<!-- 转盘容器 -->
|
<view class="wheel-container">
|
|
|
<!-- 转盘旋转包装器 -->
|
<view class="wheel-wrapper" :style="{ transform: `rotate(${rotateAngle}deg)` }"
|
:class="{ 'spinning': isSpinning }">
|
<!-- 使用Canvas绘制转盘 -->
|
<canvas canvas-id="wheelCanvas" class="wheel-canvas" :style="{
|
width: canvasSize + 'px',
|
height: canvasSize + 'px'
|
}"></canvas>
|
</view>
|
|
<!-- 中心抽奖按钮 -->
|
<view class="center-button" @tap="startLottery" :class="{ 'disabled': isSpinning }">
|
<text class="button-text">{{ isSpinning ? '抽奖中...' : '抽奖' }}</text>
|
</view>
|
</view>
|
|
<!-- 中奖结果显示 -->
|
<view class="result-container" v-if="showResult">
|
<view class="result-box">
|
<text class="result-title">{{ winResult.isWin ? '恭喜中奖!' : '很遗憾,未中奖' }}</text>
|
<view v-if="winResult.isWin" class="win-info">
|
<image :src="winResult.prize.image" class="result-image" />
|
<text class="result-prize">{{ winResult.prize.name }}</text>
|
</view>
|
<view class="result-actions">
|
<button class="btn-again" @tap="resetLottery">再次抽奖</button>
|
</view>
|
</view>
|
</view>
|
</view>
|
</template>
|
|
<script>
|
export default {
|
data() {
|
return {
|
// 奖品配置
|
prizes: [{
|
"id": 1,
|
"name": "新鲜苹果优惠卷",
|
"image": "https://img95.699pic.com/xsj/3b/cn/2o.jpg!/fh/300",
|
"probability": 0.15
|
},
|
{
|
"id": 2,
|
"name": "有机香蕉优惠卷",
|
"image": "https://img95.699pic.com/xsj/3b/cn/2o.jpg!/fh/300",
|
"probability": 0.12
|
},
|
{
|
"id": 3,
|
"name": "新鲜橙子优惠卷",
|
"image": "https://img95.699pic.com/xsj/3b/cn/2o.jpg!/fh/300",
|
"probability": 0.18
|
},
|
{
|
"id": 4,
|
"name": "精选葡萄优惠卷",
|
"image": "https://img95.699pic.com/xsj/3b/cn/2o.jpg!/fh/300",
|
"probability": 0.10
|
},
|
{
|
"id": 5,
|
"name": "新鲜草莓优惠卷",
|
"image": "https://img95.699pic.com/xsj/3b/cn/2o.jpg!/fh/300",
|
"probability": 0.08
|
},
|
{
|
"id": 6,
|
"name": "有机西红柿优惠卷",
|
"image": "https://img95.699pic.com/xsj/3b/cn/2o.jpg!/fh/300",
|
"probability": 0.20
|
},
|
{
|
"id": 7,
|
"name": "新鲜黄瓜优惠卷",
|
"image": "https://img95.699pic.com/xsj/3b/cn/2o.jpg!/fh/300",
|
"probability": 0.17
|
}
|
],
|
// 扇区颜色
|
sectorColors: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#f0932b', '#eb4d4b', '#6c5ce7'],
|
// 转盘状态
|
isSpinning: false,
|
rotateAngle: 0,
|
// 结果显示
|
showResult: false,
|
winResult: {
|
isWin: false,
|
prize: null
|
},
|
canvasContext: null,
|
canvasSize: 300 // 统一的尺寸
|
}
|
},
|
computed: {
|
// 计算每个扇区的角度
|
sectorAngle() {
|
return 360 / this.prizes.length;
|
}
|
},
|
mounted() {
|
this.$nextTick(() => {
|
this.initCanvas();
|
});
|
},
|
methods: {
|
// 初始化Canvas
|
initCanvas() {
|
try {
|
this.canvasContext = uni.createCanvasContext('wheelCanvas', this);
|
|
if (!this.canvasContext) {
|
console.error('Canvas上下文创建失败');
|
return;
|
}
|
|
// 延迟绘制,确保Canvas完全初始化
|
setTimeout(() => {
|
this.drawWheel();
|
}, 100);
|
} catch (error) {
|
console.error('Canvas初始化失败:', error);
|
}
|
},
|
|
// 绘制转盘
|
drawWheel() {
|
const ctx = this.canvasContext;
|
const centerX = this.canvasSize / 2; // 使用动态计算的中心点
|
const centerY = this.canvasSize / 2;
|
const radius = this.canvasSize * 0.45; // 转盘半径 - 基于canvas尺寸计算
|
|
// 设置canvas实际像素大小
|
ctx.width = this.canvasSize;
|
ctx.height = this.canvasSize;
|
|
ctx.clearRect(0, 0, this.canvasSize, this.canvasSize);
|
|
// 绘制背景圆
|
ctx.beginPath();
|
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
ctx.fillStyle = '#fff';
|
ctx.fill();
|
ctx.strokeStyle = '#ddd';
|
ctx.lineWidth = 2;
|
ctx.stroke();
|
|
// 绘制每个扇区
|
this.prizes.forEach((prize, index) => {
|
const startAngle = (index * this.sectorAngle) * Math.PI / 180;
|
const endAngle = ((index + 1) * this.sectorAngle) * Math.PI / 180;
|
|
// 绘制扇区
|
ctx.beginPath();
|
ctx.moveTo(centerX, centerY);
|
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
|
ctx.closePath();
|
ctx.fillStyle = this.sectorColors[index % this.sectorColors.length];
|
ctx.fill();
|
ctx.strokeStyle = '#fff';
|
ctx.lineWidth = 2;
|
ctx.stroke();
|
|
// 计算文字位置(外侧)
|
const textAngle = startAngle + (endAngle - startAngle) / 2;
|
const textRadius = radius * 0.8;
|
const textX = centerX + Math.cos(textAngle) * textRadius;
|
const textY = centerY + Math.sin(textAngle) * textRadius;
|
|
// 绘制奖品名称(垂直于圆心)
|
ctx.save();
|
ctx.translate(textX, textY);
|
ctx.rotate(textAngle + Math.PI / 2);
|
ctx.fillStyle = '#fff';
|
ctx.font = '12px Arial';
|
ctx.textAlign = 'center';
|
ctx.shadowColor = 'rgba(0,0,0,0.8)';
|
ctx.shadowBlur = 1;
|
ctx.fillText(prize.name, 0, 0);
|
ctx.restore();
|
|
// 绘制图片(内侧)
|
const imgRadius = radius * 0.5;
|
const imgX = centerX + Math.cos(textAngle) * imgRadius;
|
const imgY = centerY + Math.sin(textAngle) * imgRadius;
|
const imgSize = 30; // 图片大小
|
|
// 绘制奖品图片
|
this.drawPrizeImage(ctx, prize, imgX, imgY, imgSize, textAngle);
|
});
|
|
ctx.draw();
|
},
|
|
// 绘制奖品图片
|
drawPrizeImage(ctx, prize, imgX, imgY, imgSize, angle) {
|
if (!prize.image) {
|
// 如果没有图片,显示礼物emoji
|
ctx.save();
|
ctx.translate(imgX, imgY);
|
ctx.rotate(angle + Math.PI / 2); // 旋转到正确方向
|
ctx.font = '20px Arial';
|
ctx.textAlign = 'center';
|
ctx.textBaseline = 'middle';
|
ctx.fillStyle = '#fff';
|
ctx.shadowColor = 'rgba(0,0,0,0.5)';
|
ctx.shadowBlur = 2;
|
ctx.fillText('🎁', 0, 0);
|
ctx.restore();
|
return;
|
}
|
|
// 使用uni.getImageInfo加载图片
|
uni.getImageInfo({
|
src: prize.image,
|
success: (res) => {
|
if (res && res.path) {
|
try {
|
ctx.save();
|
// 移动到图片中心点
|
ctx.translate(imgX, imgY);
|
// 旋转图片,使其头部朝向扇形外侧方向
|
ctx.rotate(angle + Math.PI / 2);
|
|
// 调整图片尺寸,使其更适合扇形区域(长方形)
|
const imgWidth = imgSize * 1.2; // 稍微增加宽度
|
const imgHeight = imgSize * 0.8; // 减少高度,形成长方形
|
|
// 直接绘制图片,不添加任何背景或边框
|
ctx.drawImage(res.path, -imgWidth / 2, -imgHeight / 2, imgWidth, imgHeight);
|
ctx.restore();
|
ctx.draw(true); // 保留之前的绘制内容
|
} catch (error) {
|
console.error('绘制图片失败:', error);
|
this.drawPlaceholderIcon(ctx, imgX, imgY, angle);
|
}
|
} else {
|
this.drawPlaceholderIcon(ctx, imgX, imgY, angle);
|
}
|
},
|
fail: (err) => {
|
console.error('加载图片失败:', err);
|
this.drawPlaceholderIcon(ctx, imgX, imgY, angle);
|
}
|
});
|
},
|
|
// 绘制占位符图标
|
drawPlaceholderIcon(ctx, imgX, imgY, angle) {
|
ctx.save();
|
ctx.translate(imgX, imgY);
|
ctx.rotate(angle + Math.PI / 2); // 旋转到正确方向
|
ctx.font = '20px Arial';
|
ctx.textAlign = 'center';
|
ctx.textBaseline = 'middle';
|
ctx.fillStyle = '#fff';
|
ctx.shadowColor = 'rgba(0,0,0,0.5)';
|
ctx.shadowBlur = 2;
|
ctx.fillText('🎁', 0, 0);
|
ctx.restore();
|
ctx.draw(true);
|
},
|
|
// 开始抽奖
|
startLottery() {
|
if (this.isSpinning) return;
|
|
this.isSpinning = true;
|
this.showResult = false;
|
|
// 随机中奖逻辑
|
const winPrize = this.getRandomPrize();
|
const isWin = winPrize !== null;
|
|
// 计算目标角度
|
let targetAngle;
|
if (isWin) {
|
const prizeIndex = this.prizes.findIndex(p => p.id === winPrize.id);
|
// 指向中奖扇区的中心
|
targetAngle = 360 - (prizeIndex * this.sectorAngle + this.sectorAngle / 2);
|
} else {
|
// 随机角度,不指向任何奖品
|
targetAngle = Math.random() * 360;
|
}
|
|
// 增加多圈旋转效果 - 使用固定的旋转圈数
|
const extraRotations = 1440; // 4圈
|
const finalAngle = extraRotations + targetAngle;
|
|
// 直接设置最终角度,避免累积
|
this.rotateAngle = finalAngle;
|
|
// 3秒后显示结果
|
setTimeout(() => {
|
this.isSpinning = false;
|
this.winResult = {
|
isWin: isWin,
|
prize: winPrize
|
};
|
this.showResult = true;
|
}, 3000);
|
},
|
|
// 随机中奖算法
|
getRandomPrize() {
|
const random = Math.random();
|
let totalProbability = 0;
|
|
// 计算总中奖概率
|
const winProbability = this.prizes.reduce((sum, prize) => sum + prize.probability, 0);
|
|
// 30%的概率不中奖
|
if (random > winProbability) {
|
return null;
|
}
|
|
// 根据概率选择奖品
|
for (let prize of this.prizes) {
|
totalProbability += prize.probability;
|
if (random <= totalProbability) {
|
return prize;
|
}
|
}
|
|
return null;
|
},
|
|
// 重置抽奖
|
resetLottery() {
|
this.showResult = false;
|
this.isSpinning = false;
|
this.winResult = {
|
isWin: false,
|
prize: null
|
};
|
// 重置转盘角度到0
|
this.rotateAngle = 0;
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.lottery-container {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
padding: 40rpx;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
min-height: 100vh;
|
}
|
|
.lottery-title {
|
font-size: 48rpx;
|
font-weight: bold;
|
color: #fff;
|
margin-bottom: 60rpx;
|
text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.3);
|
}
|
|
.wheel-container {
|
position: relative;
|
width: 300px;
|
height: 300px;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
margin: 0 auto;
|
}
|
|
.wheel-wrapper {
|
position: relative;
|
width: 300px;
|
height: 300px;
|
display: flex;
|
justify-content: center;
|
align-items: center;
|
transition: transform 3s cubic-bezier(0.23, 1, 0.32, 1);
|
}
|
|
.wheel-wrapper.spinning {
|
transition: transform 3s cubic-bezier(0.23, 1, 0.32, 1);
|
}
|
|
.wheel-canvas {
|
width: 300px;
|
height: 300px;
|
border-radius: 50%;
|
box-shadow: 0 0 30rpx rgba(0, 0, 0, 0.3);
|
position: relative;
|
z-index: 1;
|
}
|
|
.pointer {
|
position: absolute;
|
top: 10px;
|
left: 50%;
|
transform: translateX(-50%);
|
width: 0;
|
height: 0;
|
border-left: 10px solid transparent;
|
border-right: 10px solid transparent;
|
border-top: 30px solid #ff6b6b;
|
z-index: 15;
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
}
|
|
.center-button {
|
position: absolute;
|
top: 50%;
|
left: 50%;
|
transform: translate(-50%, -50%);
|
width: 80px;
|
height: 80px;
|
background: linear-gradient(45deg, #ff6b6b, #ee5a52);
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
box-shadow: 0 6rpx 20rpx rgba(255, 107, 107, 0.4);
|
z-index: 20;
|
touch-action: manipulation;
|
pointer-events: auto;
|
}
|
|
.center-button.disabled {
|
background: #ccc;
|
box-shadow: none;
|
}
|
|
.button-text {
|
color: #fff;
|
font-size: 28rpx;
|
font-weight: bold;
|
text-align: center;
|
pointer-events: none;
|
}
|
|
.result-container {
|
position: fixed;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
background: rgba(0, 0, 0, 0.7);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
z-index: 1000;
|
}
|
|
.result-box {
|
background: #fff;
|
border-radius: 20rpx;
|
padding: 60rpx;
|
text-align: center;
|
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
|
max-width: 500rpx;
|
}
|
|
.result-title {
|
font-size: 36rpx;
|
font-weight: bold;
|
color: #333;
|
margin-bottom: 30rpx;
|
}
|
|
.win-info {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
margin-bottom: 40rpx;
|
}
|
|
.result-image {
|
width: 120rpx;
|
height: 120rpx;
|
border-radius: 50%;
|
margin-bottom: 20rpx;
|
}
|
|
.result-prize {
|
font-size: 32rpx;
|
color: #ff6b6b;
|
font-weight: bold;
|
}
|
|
.btn-again {
|
background: linear-gradient(45deg, #4ecdc4, #44a08d);
|
color: #fff;
|
border: none;
|
border-radius: 50rpx;
|
padding: 20rpx 60rpx;
|
font-size: 30rpx;
|
font-weight: bold;
|
}
|
</style>
|