<template>
|
|
<view class="page">
|
|
|
|
<navbar leftClick
|
|
color="#fff"
|
|
bgColor="#667eea"
|
|
@leftClick="$utils.navigateBack" />
|
|
<view class="turntable-container">
|
|
|
|
<!-- 头部标题 -->
|
|
<view class="header">
|
|
<text class="title">幸运大转盘</text>
|
|
<text class="subtitle">转一转,好运来!</text>
|
|
</view>
|
|
|
|
<!-- 积分余额显示 -->
|
|
<view class="points-display">
|
|
<view class="points-info">
|
|
<text class="points-label">当前积分:</text>
|
|
<text class="points-value">{{ userPoints }}</text>
|
|
</view>
|
|
<view class="cost-info">
|
|
<text class="cost-label">每次消耗:</text>
|
|
<text class="cost-value">{{ drawCost }}积分</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 转盘区域 -->
|
|
<view class="turntable-wrapper" v-if="prizes.length > 0">
|
|
<view class="turntable" :class="{ 'spinning': isSpinning }" :style="{ transform: `rotate(${rotateAngle}deg)` }">
|
|
<!-- 使用纯CSS创建转盘 -->
|
|
<view class="wheel-bg">
|
|
<!-- 8个扇形区域 -->
|
|
<view
|
|
v-for="(prize, index) in prizes"
|
|
:key="index"
|
|
class="wheel-sector"
|
|
:style="{
|
|
backgroundColor: sectorColors[index % sectorColors.length],
|
|
transform: `rotate(${index * sectorAngle}deg)`
|
|
}"
|
|
>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 奖品文字覆盖层 -->
|
|
<view class="prizes-overlay">
|
|
<view
|
|
v-for="(prize, index) in prizes"
|
|
:key="index"
|
|
class="prize-item"
|
|
:style="prizeStyles[index]"
|
|
>
|
|
<view class="prize-content">
|
|
<text class="prize-icon">{{ getIcon(prize.type) }}</text>
|
|
<text class="prize-name">{{ prize.title }}</text>
|
|
<text class="prize-value" v-if="prize.price">¥{{ prize.price }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 中心指针 -->
|
|
<view class="pointer">
|
|
<view class="pointer-triangle"></view>
|
|
</view>
|
|
|
|
<!-- 中心按钮 -->
|
|
<view class="center-button" @click="startSpin" :class="{ disabled: isSpinning || userPoints < drawCost }">
|
|
<text class="button-text">{{ getButtonText() }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 加载中状态 -->
|
|
<view v-else class="loading-wrapper">
|
|
<uv-loading-icon mode="spinner" color="#fff" size="60"></uv-loading-icon>
|
|
<text class="loading-text">加载中...</text>
|
|
</view>
|
|
|
|
<!-- 抽奖结果弹窗 -->
|
|
<view class="result-modal" v-if="showResult" @click="closeResult">
|
|
<view class="modal-content" @click.stop>
|
|
<text class="result-title">🎉 恭喜您 🎉</text>
|
|
<view class="result-prize">
|
|
<!-- <text class="result-icon">{{ getIcon(currentPrize.type) }}</text> -->
|
|
|
|
<image :src="currentPrize.img" mode="widthFix" style="width: 100%;"></image>
|
|
|
|
<text class="result-name">{{ currentPrize.title }}</text>
|
|
<text class="result-value" v-if="currentPrize.price">¥{{ currentPrize.price }}</text>
|
|
</view>
|
|
<view class="points-change">
|
|
<text class="change-text">{{ getPointsChangeText() }}</text>
|
|
</view>
|
|
<view class="result-actions">
|
|
<button class="confirm-btn" @click="closeResult">确定</button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 积分提示 -->
|
|
<view class="spin-info">
|
|
<text>消耗{{ drawCost }}积分即可抽奖,快来试试手气吧!</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { mapState } from 'vuex'
|
|
export default {
|
|
computed: {
|
|
...mapState(['userInfo']),
|
|
// 获取用户当前积分
|
|
userPoints() {
|
|
return this.userInfo?.integerPrice || 0
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
// 奖品配置(从接口获取)
|
|
prizes: [],
|
|
sectorColors: [
|
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4',
|
|
'#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'
|
|
],
|
|
isSpinning: false, // 是否正在旋转
|
|
rotateAngle: 0, // 旋转角度
|
|
showResult: false, // 显示结果弹窗
|
|
currentPrize: null, // 当前中奖奖品
|
|
sectorAngle: 0, // 每个扇形的角度
|
|
drawCost: 5, // 抽奖消耗积分
|
|
pointsChange: null, // 积分变化
|
|
// 预计算奖品位置样式
|
|
prizeStyles: []
|
|
}
|
|
},
|
|
onLoad() {
|
|
this.getLuckDrawList()
|
|
},
|
|
onShow() {
|
|
// 刷新用户信息以获取最新积分
|
|
if (uni.getStorageSync('token')) {
|
|
this.$store.commit('getUserInfo')
|
|
}
|
|
},
|
|
methods: {
|
|
// 获取抽奖列表
|
|
getLuckDrawList() {
|
|
this.$api('getLuckDrawList', {}, res => {
|
|
if (res.code == 200) {
|
|
this.prizes = res.result || []
|
|
|
|
// 计算每个扇形的角度
|
|
if (this.prizes.length > 0) {
|
|
this.sectorAngle = 360 / this.prizes.length
|
|
this.calculatePrizeStyles()
|
|
}
|
|
} else {
|
|
uni.showToast({
|
|
title: res.message || '获取抽奖信息失败',
|
|
icon: 'none'
|
|
})
|
|
}
|
|
})
|
|
},
|
|
|
|
// 获取按钮文字
|
|
getButtonText() {
|
|
if (this.isSpinning) {
|
|
return '抽奖中...'
|
|
}
|
|
if (this.userPoints < this.drawCost) {
|
|
return '积分不足'
|
|
}
|
|
return '开始抽奖'
|
|
},
|
|
|
|
// 根据奖品类型获取图标
|
|
getIcon(type) {
|
|
const iconMap = {
|
|
'0': '💰',
|
|
'points': '⭐',
|
|
'gift': '🎁',
|
|
'coupon': '🎫',
|
|
'thanks': '🤝'
|
|
}
|
|
return iconMap[type] || '🎁'
|
|
},
|
|
|
|
// 计算奖品位置样式
|
|
calculatePrizeStyles() {
|
|
this.prizeStyles = this.prizes.map((prize, index) => {
|
|
// 计算奖品在圆形中的位置
|
|
const angle = (index * this.sectorAngle + this.sectorAngle / 2) * Math.PI / 180; // 转换为弧度
|
|
const radius = 150; // 奖品距离中心的距离
|
|
const x = Math.cos(angle - Math.PI/2) * radius; // 减去90度,因为我们希望0度在顶部
|
|
const y = Math.sin(angle - Math.PI/2) * radius;
|
|
|
|
return `left: calc(50% + ${x}rpx); top: calc(50% + ${y}rpx); transform: translate(-50%, -50%);`
|
|
})
|
|
},
|
|
|
|
// 开始抽奖
|
|
startSpin() {
|
|
// 检查是否可以抽奖
|
|
if (this.isSpinning) {
|
|
return
|
|
}
|
|
|
|
// 检查积分是否足够
|
|
if (this.userPoints < this.drawCost) {
|
|
uni.showModal({
|
|
title: '积分不足',
|
|
content: `抽奖需要消耗${this.drawCost}积分,您当前积分为${this.userPoints}`,
|
|
showCancel: true,
|
|
cancelText: '取消',
|
|
confirmText: '去赚积分',
|
|
success: (res) => {
|
|
if (res.confirm) {
|
|
// 跳转到积分获取页面或任务页面
|
|
uni.navigateBack()
|
|
}
|
|
}
|
|
})
|
|
return
|
|
}
|
|
|
|
this.isSpinning = true
|
|
|
|
// 调用抽奖接口
|
|
this.$api('luckDraw', {}, res => {
|
|
if (res.code == 200) {
|
|
const result = res.result
|
|
this.currentPrize = result.gift
|
|
this.pointsChange = result.remainingIntegral || null
|
|
|
|
// 更新用户信息(积分变化)
|
|
this.$store.commit('getUserInfo')
|
|
|
|
// 找到中奖奖品的索引
|
|
const prizeIndex = this.prizes.findIndex(prize => prize.id == result.gift.id)
|
|
|
|
if (prizeIndex !== -1) {
|
|
// 计算旋转角度
|
|
const targetAngle = 360 - (prizeIndex * this.sectorAngle + this.sectorAngle / 2)
|
|
const spinRounds = 5 // 转5圈
|
|
const finalAngle = this.rotateAngle + spinRounds * 360 + targetAngle
|
|
|
|
this.rotateAngle = finalAngle
|
|
|
|
// 动画结束后显示结果
|
|
setTimeout(() => {
|
|
this.isSpinning = false
|
|
this.showResult = true
|
|
this.handlePrizeResult()
|
|
}, 3000)
|
|
} else {
|
|
this.isSpinning = false
|
|
uni.showToast({
|
|
title: '抽奖异常,请重试',
|
|
icon: 'none'
|
|
})
|
|
}
|
|
} else {
|
|
this.isSpinning = false
|
|
uni.showToast({
|
|
title: res.message || '抽奖失败,请重试',
|
|
icon: 'none'
|
|
})
|
|
}
|
|
})
|
|
},
|
|
|
|
// 处理中奖结果
|
|
handlePrizeResult() {
|
|
if (this.currentPrize) {
|
|
console.log(`中奖信息:`, this.currentPrize)
|
|
// 根据奖品类型显示不同提示
|
|
if (this.currentPrize.type === 'money') {
|
|
console.log(`获得现金奖励: ${this.currentPrize.prizeValue}`)
|
|
} else if (this.currentPrize.type === 'points') {
|
|
console.log(`获得积分奖励: ${this.currentPrize.prizeValue}`)
|
|
} else if (this.currentPrize.type === 'gift') {
|
|
console.log(`获得礼品: ${this.currentPrize.prizeName}`)
|
|
}
|
|
}
|
|
},
|
|
|
|
// 关闭结果弹窗
|
|
closeResult() {
|
|
this.showResult = false
|
|
this.pointsChange = null
|
|
// 刷新抽奖信息
|
|
this.getLuckDrawList()
|
|
},
|
|
|
|
// 积分变化提示文字
|
|
getPointsChangeText() {
|
|
let text = `消耗 ${this.drawCost} 积分`
|
|
|
|
if (this.currentPrize && this.currentPrize.type === 'points') {
|
|
const gained = parseInt(this.currentPrize.prizeValue)
|
|
const net = gained - this.drawCost
|
|
if (net > 0) {
|
|
text += `,获得 ${gained} 积分,净收益 +${net} 积分`
|
|
} else if (net < 0) {
|
|
text += `,获得 ${gained} 积分,净损失 ${Math.abs(net)} 积分`
|
|
} else {
|
|
text += `,获得 ${gained} 积分,收支平衡`
|
|
}
|
|
}
|
|
|
|
return text
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.turntable-container {
|
|
min-height: 100vh;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
padding: 40rpx;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 40rpx;
|
|
color: white;
|
|
|
|
.title {
|
|
font-size: 48rpx;
|
|
font-weight: bold;
|
|
display: block;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 28rpx;
|
|
opacity: 0.9;
|
|
}
|
|
}
|
|
|
|
.points-display {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 20rpx;
|
|
padding: 30rpx;
|
|
margin-bottom: 40rpx;
|
|
backdrop-filter: blur(10rpx);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
max-width: 600rpx;
|
|
|
|
.points-info, .cost-info {
|
|
display: flex;
|
|
align-items: center;
|
|
color: white;
|
|
|
|
.points-label, .cost-label {
|
|
font-size: 28rpx;
|
|
margin-right: 10rpx;
|
|
}
|
|
|
|
.points-value {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #FFD700;
|
|
}
|
|
|
|
.cost-value {
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
color: #FF6B6B;
|
|
}
|
|
}
|
|
}
|
|
|
|
.turntable-wrapper {
|
|
position: relative;
|
|
width: 600rpx;
|
|
height: 600rpx;
|
|
margin-bottom: 60rpx;
|
|
}
|
|
|
|
.loading-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 600rpx;
|
|
|
|
.loading-text {
|
|
color: white;
|
|
font-size: 28rpx;
|
|
margin-top: 20rpx;
|
|
}
|
|
}
|
|
|
|
.turntable {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
position: relative;
|
|
transition: transform 3s cubic-bezier(0.23, 1, 0.32, 1);
|
|
box-shadow: 0 0 40rpx rgba(0, 0, 0, 0.3);
|
|
overflow: hidden;
|
|
|
|
&.spinning {
|
|
transition-duration: 3s;
|
|
}
|
|
}
|
|
|
|
.wheel-bg {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.wheel-sector {
|
|
position: absolute;
|
|
width: 50%;
|
|
height: 50%;
|
|
top: 0%;
|
|
left: 50%;
|
|
transform-origin: 0% 100%;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: inherit;
|
|
clip-path: polygon(0% 100%, 50% 0%, 100% 100%);
|
|
}
|
|
|
|
// 添加边框线
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
border-right: 2rpx solid rgba(255,255,255,0.3);
|
|
transform-origin: 0% 100%;
|
|
}
|
|
}
|
|
|
|
.prizes-overlay {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 990;
|
|
}
|
|
|
|
.prize-item {
|
|
position: absolute;
|
|
width: 120rpx;
|
|
height: 80rpx;
|
|
z-index: 10;
|
|
}
|
|
|
|
.prize-content {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
color: white;
|
|
text-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.8);
|
|
|
|
.prize-icon {
|
|
font-size: 32rpx;
|
|
display: block;
|
|
margin-bottom: 4rpx;
|
|
}
|
|
|
|
.prize-name {
|
|
font-size: 18rpx;
|
|
display: block;
|
|
font-weight: bold;
|
|
margin-bottom: 2rpx;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.prize-value {
|
|
font-size: 20rpx;
|
|
display: block;
|
|
font-weight: bold;
|
|
line-height: 1.2;
|
|
}
|
|
}
|
|
|
|
.pointer {
|
|
position: absolute;
|
|
top: -20rpx;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 100;
|
|
|
|
.pointer-triangle {
|
|
width: 0;
|
|
height: 0;
|
|
border-left: 20rpx solid transparent;
|
|
border-right: 20rpx solid transparent;
|
|
border-top: 60rpx solid #FF4757;
|
|
filter: drop-shadow(0 4rpx 8rpx rgba(0, 0, 0, 0.3));
|
|
}
|
|
}
|
|
|
|
.center-button {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 160rpx;
|
|
height: 160rpx;
|
|
border-radius: 50%;
|
|
background: linear-gradient(145deg, #FF6B6B, #FF4757);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 8rpx 20rpx rgba(255, 71, 87, 0.4);
|
|
z-index: 50;
|
|
transition: transform 0.2s;
|
|
|
|
&:active:not(.disabled) {
|
|
transform: translate(-50%, -50%) scale(0.95);
|
|
}
|
|
|
|
&.disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
background: linear-gradient(145deg, #999, #777);
|
|
}
|
|
|
|
.button-text {
|
|
color: white;
|
|
font-size: 24rpx;
|
|
font-weight: bold;
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
.result-modal {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
|
|
.modal-content {
|
|
background: white;
|
|
border-radius: 20rpx;
|
|
padding: 60rpx 40rpx;
|
|
text-align: center;
|
|
box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, 0.3);
|
|
min-width: 500rpx;
|
|
|
|
.result-title {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
color: #FF6B6B;
|
|
margin-bottom: 40rpx;
|
|
}
|
|
|
|
.result-prize {
|
|
margin-bottom: 30rpx;
|
|
|
|
.result-icon {
|
|
font-size: 60rpx;
|
|
display: block;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.result-name {
|
|
font-size: 32rpx;
|
|
color: #333;
|
|
display: block;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.result-value {
|
|
font-size: 36rpx;
|
|
color: #FF6B6B;
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
|
|
.points-change {
|
|
margin-bottom: 40rpx;
|
|
padding: 20rpx;
|
|
background: #f5f5f5;
|
|
border-radius: 10rpx;
|
|
|
|
.change-text {
|
|
font-size: 24rpx;
|
|
color: #666;
|
|
line-height: 1.4;
|
|
}
|
|
}
|
|
|
|
.confirm-btn {
|
|
background: linear-gradient(45deg, #FF6B6B, #FF4757);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 40rpx;
|
|
padding: 20rpx 60rpx;
|
|
font-size: 28rpx;
|
|
font-weight: bold;
|
|
}
|
|
}
|
|
}
|
|
|
|
.spin-info {
|
|
text-align: center;
|
|
color: white;
|
|
font-size: 28rpx;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
padding: 20rpx 40rpx;
|
|
border-radius: 40rpx;
|
|
backdrop-filter: blur(10rpx);
|
|
}
|
|
</style>
|