625 lines
14 KiB
Vue
625 lines
14 KiB
Vue
<template>
|
||
<div class="lucky-draw-container">
|
||
<!-- 背景图 -->
|
||
<div class="background">
|
||
<img
|
||
v-lazy="backgroundImage"
|
||
alt="抽奖背景"
|
||
@error="handleImageError"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 抽奖标题 -->
|
||
<h2 class="title">新春抽奖活动</h2>
|
||
|
||
<!-- 抽奖轮盘 -->
|
||
<div class="lottery-wheel-container">
|
||
<div
|
||
class="lottery-wheel"
|
||
:class="{ 'rotating': spinning }"
|
||
:style="wheelStyle"
|
||
>
|
||
<div
|
||
v-for="(prize, index) in prizes"
|
||
:key="prize.id"
|
||
class="prize-segment"
|
||
:style="getSegmentStyle(index)"
|
||
>
|
||
<span class="prize-name">{{ prize.name }}</span>
|
||
<span class="prize-content">{{ prize.prize }}</span>
|
||
</div>
|
||
<div class="wheel-center">
|
||
<div
|
||
class="draw-button"
|
||
@click="handleDrawClick"
|
||
:disabled="spinning || !userInfoComplete"
|
||
>
|
||
<span class="draw-text">{{ spinning ? '抽奖中...' : '开始抽奖' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 抽奖信息 -->
|
||
<div class="lottery-info">
|
||
<p class="remaining-draws">剩余抽奖次数: {{ remainingDraws }}</p>
|
||
<p class="rules-link" @click="showRules = !showRules">查看活动规则 ▼</p>
|
||
</div>
|
||
|
||
<!-- 活动规则弹窗 -->
|
||
<uni-popup v-model="showRules" position="center" :style="{ width: '90%' }">
|
||
<div class="rules-content">
|
||
<h3>活动规则</h3>
|
||
<div class="rules-list">
|
||
<p>1. 活动时间:2026年2月1日-2026年2月15日</p>
|
||
<p>2. 每位用户仅有一次抽奖机会</p>
|
||
<p>3. 中奖后请在7天内领取奖品,逾期失效</p>
|
||
<p>4. 实物奖品将在活动结束后7个工作日内发放</p>
|
||
<p>5. 本活动最终解释权归主办方所有</p>
|
||
</div>
|
||
<button class="primary-button" @click="showRules = false">我知道了</button>
|
||
</div>
|
||
</uni-popup>
|
||
|
||
<!-- 用户信息表单 -->
|
||
<uni-popup v-model="showInfoForm" position="center" :style="{ width: '90%' }">
|
||
<div class="form-content">
|
||
<h3>填写信息参与抽奖</h3>
|
||
<form @submit.prevent="onSubmit">
|
||
<div class="form-item">
|
||
<label class="form-label">姓名</label>
|
||
<input
|
||
v-model="userInfo.name"
|
||
name="name"
|
||
type="text"
|
||
placeholder="请输入姓名"
|
||
class="form-input"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="form-item">
|
||
<label class="form-label">手机号</label>
|
||
<input
|
||
v-model="userInfo.phone"
|
||
name="phone"
|
||
type="tel"
|
||
placeholder="请输入手机号"
|
||
class="form-input"
|
||
pattern="^1[3-9]\d{9}$"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="form-item">
|
||
<label class="form-label">收货地址</label>
|
||
<textarea
|
||
v-model="userInfo.address"
|
||
name="address"
|
||
placeholder="请输入收货地址"
|
||
class="form-textarea"
|
||
rows="3"
|
||
required
|
||
></textarea>
|
||
</div>
|
||
<div style="margin: 16px;">
|
||
<button type="submit" class="primary-button" block>提交</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</uni-popup>
|
||
|
||
<!-- 抽奖结果弹窗 -->
|
||
<uni-popup v-model="showResult" position="center" :style="{ width: '90%' }">
|
||
<div class="result-content">
|
||
<div v-if="lotteryResult.won" class="win-result">
|
||
<div class="success-icon">✓</div>
|
||
<h3 class="result-title">恭喜中奖!</h3>
|
||
<p class="result-prize">{{ lotteryResult.prize.name }}:{{ lotteryResult.prize.prize }}</p>
|
||
<button class="primary-button" block @click="handleClaimPrize">领取奖品</button>
|
||
</div>
|
||
<div v-else class="lose-result">
|
||
<div class="cross-icon">✗</div>
|
||
<h3 class="result-title">很遗憾</h3>
|
||
<p class="result-message">未中奖,感谢参与!</p>
|
||
<button class="default-button" block @click="showResult = false">关闭</button>
|
||
</div>
|
||
</div>
|
||
</uni-popup>
|
||
|
||
<!-- 领取成功弹窗 -->
|
||
<uni-popup v-model="showClaimSuccess" position="center" :style="{ width: '90%' }">
|
||
<div class="success-content">
|
||
<div class="success-icon">✓</div>
|
||
<h3>领取成功!</h3>
|
||
<p>您的奖品将在7个工作日内发放,请耐心等待。</p>
|
||
<button class="primary-button" block @click="showClaimSuccess = false">确定</button>
|
||
</div>
|
||
</uni-popup>
|
||
|
||
<!-- 加载动画 -->
|
||
<div v-if="loading" class="loading-overlay">
|
||
<div class="custom-loading"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, watch } from 'vue';
|
||
import { submitLotteryInfo, drawLottery, claimPrize } from '../api/lottery';
|
||
|
||
// 响应式数据
|
||
const spinning = ref(false);
|
||
const loading = ref(false);
|
||
const showRules = ref(false);
|
||
const showInfoForm = ref(false);
|
||
const showResult = ref(false);
|
||
const showClaimSuccess = ref(false);
|
||
const backgroundImage = ref('https://placeholder.pics/svg/640x1136/FDF5E6/CD853F/抽奖背景');
|
||
const remainingDraws = ref(1); // 默认每人1次抽奖机会
|
||
const rotationAngle = ref(0);
|
||
|
||
// 奖项设置
|
||
const prizes = ref([
|
||
{ id: 1, name: '一等奖', prize: 'iPhone 15' },
|
||
{ id: 2, name: '二等奖', prize: 'AirPods Pro' },
|
||
{ id: 3, name: '三等奖', prize: '京东购物卡200元' },
|
||
{ id: 4, name: '四等奖', prize: '品牌充电宝' },
|
||
{ id: 5, name: '参与奖', prize: '新春福袋' }
|
||
]);
|
||
|
||
// 用户信息
|
||
const userInfo = reactive({
|
||
name: '',
|
||
phone: '',
|
||
address: ''
|
||
});
|
||
|
||
// 抽奖结果
|
||
const lotteryResult = reactive({
|
||
won: false,
|
||
prize: null
|
||
});
|
||
|
||
// 计算属性
|
||
const userInfoComplete = computed(() => {
|
||
return userInfo.name && userInfo.phone && userInfo.address;
|
||
});
|
||
|
||
const wheelStyle = computed(() => {
|
||
return {
|
||
transform: `rotate(${rotationAngle.value}deg)`
|
||
};
|
||
});
|
||
|
||
// 方法
|
||
const getSegmentStyle = (index) => {
|
||
const segments = prizes.value.length;
|
||
const angle = 360 / segments;
|
||
const rotate = angle * index;
|
||
const isEven = index % 2 === 0;
|
||
|
||
return {
|
||
background: isEven ? '#ffd700' : '#ffed4e',
|
||
transform: `rotate(${rotate}deg)`
|
||
};
|
||
};
|
||
|
||
const handleDrawClick = () => {
|
||
if (!userInfoComplete.value) {
|
||
showInfoForm.value = true;
|
||
return;
|
||
}
|
||
|
||
if (remainingDraws.value <= 0) {
|
||
uni.uni.showToast({title: '抽奖次数已用完', duration: 2000});
|
||
return;
|
||
}
|
||
|
||
executeDraw();
|
||
};
|
||
|
||
const onSubmit = async () => {
|
||
loading.value = true;
|
||
|
||
try {
|
||
const result = await submitLotteryInfo(userInfo);
|
||
if (result.code === 200) {
|
||
uni.uni.showToast({title: '信息提交成功', duration: 2000});
|
||
showInfoForm.value = false;
|
||
executeDraw();
|
||
} else {
|
||
uni.uni.showToast({title: '信息提交失败,请重试', duration: 2000});
|
||
}
|
||
} catch (error) {
|
||
console.error('提交信息失败:', error);
|
||
uni.uni.showToast({title: '网络异常,请稍后重试', duration: 2000});
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const executeDraw = async () => {
|
||
spinning.value = true;
|
||
|
||
try {
|
||
// 先旋转几圈
|
||
const initialRotation = rotationAngle.value;
|
||
const extraRotations = 5;
|
||
const finalRotation = initialRotation + extraRotations * 360;
|
||
|
||
// 调用抽奖API
|
||
const result = await drawLottery('user123');
|
||
|
||
if (result.code === 200) {
|
||
if (result.data.won) {
|
||
// 计算中奖位置
|
||
const winningIndex = prizes.value.findIndex(p => p.id === result.data.prize.id);
|
||
const segments = prizes.value.length;
|
||
const anglePerSegment = 360 / segments;
|
||
const offsetAngle = 90 - (winningIndex * anglePerSegment) - (anglePerSegment / 2);
|
||
|
||
// 最终旋转角度
|
||
rotationAngle.value = finalRotation + offsetAngle;
|
||
} else {
|
||
// 未中奖,随机停止
|
||
const randomOffset = Math.random() * 360;
|
||
rotationAngle.value = finalRotation + randomOffset;
|
||
}
|
||
|
||
// 更新剩余抽奖次数
|
||
remainingDraws.value = result.data.remainingDraws;
|
||
|
||
// 更新抽奖结果
|
||
lotteryResult.won = result.data.won;
|
||
lotteryResult.prize = result.data.prize;
|
||
|
||
// 显示结果
|
||
setTimeout(() => {
|
||
spinning.value = false;
|
||
showResult.value = true;
|
||
}, 3000);
|
||
} else {
|
||
spinning.value = false;
|
||
uni.uni.showToast({title: '抽奖失败,请重试', duration: 2000});
|
||
}
|
||
} catch (error) {
|
||
console.error('抽奖失败:', error);
|
||
spinning.value = false;
|
||
uni.uni.showToast({title: '网络异常,请稍后重试', duration: 2000});
|
||
}
|
||
};
|
||
|
||
const handleClaimPrize = async () => {
|
||
if (!lotteryResult.prize) return;
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
const result = await claimPrize(lotteryResult.prize.id);
|
||
if (result.code === 200) {
|
||
showResult.value = false;
|
||
showClaimSuccess.value = true;
|
||
} else {
|
||
uni.uni.showToast({title: '领取失败,请重试', duration: 2000});
|
||
}
|
||
} catch (error) {
|
||
console.error('领取奖品失败:', error);
|
||
uni.uni.showToast({title: '网络异常,请稍后重试', duration: 2000});
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const selectKeyword = (word) => {
|
||
keyword.value = word;
|
||
};
|
||
|
||
// 处理图片加载错误
|
||
const handleImageError = (event) => {
|
||
event.target.src = 'https://placeholder.pics/svg/640x1136/FDF5E6/CD853F/抽奖背景';
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.lucky-draw-container {
|
||
position: relative;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.background {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: -1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.background img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.title {
|
||
font-size: 32px;
|
||
color: #e67e22;
|
||
margin-bottom: 30px;
|
||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.lottery-wheel-container {
|
||
position: relative;
|
||
width: 300px;
|
||
height: 300px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.lottery-wheel {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
border: 5px solid #e67e22;
|
||
overflow: hidden;
|
||
transition: transform 3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
transform-origin: center center;
|
||
}
|
||
|
||
.prize-segment {
|
||
position: absolute;
|
||
width: 50%;
|
||
height: 50%;
|
||
top: 0;
|
||
left: 50%;
|
||
transform-origin: left bottom;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: flex-start;
|
||
padding-left: 20px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.prize-name {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #8b4513;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.prize-content {
|
||
font-size: 12px;
|
||
color: #8b4513;
|
||
}
|
||
|
||
.wheel-center {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 50%;
|
||
background: #fff;
|
||
border: 3px solid #e67e22;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.draw-button {
|
||
width: 70px;
|
||
height: 70px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #e67e22, #d35400);
|
||
color: white;
|
||
border: none;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.draw-button:hover:not(:disabled) {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 4px 12px rgba(230, 126, 34, 0.4);
|
||
}
|
||
|
||
.draw-button:disabled {
|
||
background: #ddd;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.lottery-info {
|
||
margin-top: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.remaining-draws {
|
||
font-size: 18px;
|
||
color: #e67e22;
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.rules-link {
|
||
font-size: 14px;
|
||
color: #3498db;
|
||
cursor: pointer;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.rules-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.rules-content h3 {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
color: #e67e22;
|
||
}
|
||
|
||
.rules-list p {
|
||
margin-bottom: 10px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.form-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.form-content h3 {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
color: #e67e22;
|
||
}
|
||
|
||
.result-content {
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.result-title {
|
||
font-size: 24px;
|
||
margin: 20px 0;
|
||
color: #e67e22;
|
||
}
|
||
|
||
.result-prize {
|
||
font-size: 18px;
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.result-message {
|
||
font-size: 18px;
|
||
color: #999;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.success-content {
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
.success-content h3 {
|
||
font-size: 24px;
|
||
margin: 20px 0;
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.success-content p {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin-bottom: 20px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.loading-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 999;
|
||
}
|
||
|
||
/* 自定义图标样式 */
|
||
.success-icon {
|
||
font-size: 64px;
|
||
color: #f39c12;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.cross-icon {
|
||
font-size: 64px;
|
||
color: #999;
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
/* 自定义按钮样式 */
|
||
.primary-button {
|
||
background-color: #07c160;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 10px 0;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.default-button {
|
||
background-color: #f7f8fa;
|
||
color: #323233;
|
||
border: 1px solid #ebedf0;
|
||
border-radius: 4px;
|
||
padding: 10px 0;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* 自定义加载动画 */
|
||
.custom-loading {
|
||
width: 48px;
|
||
height: 48px;
|
||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||
border-radius: 50%;
|
||
border-top-color: #fff;
|
||
animation: spin 1s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 480px) {
|
||
.title {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.lottery-wheel-container {
|
||
width: 280px;
|
||
height: 280px;
|
||
}
|
||
|
||
.prize-segment {
|
||
padding-left: 15px;
|
||
}
|
||
|
||
.prize-name {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.prize-content {
|
||
font-size: 10px;
|
||
}
|
||
}
|
||
</style>
|