Browse Source

Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into hongxilin/feature-20250710175148-抽奖

songtongtong/feature-20250717104937-众筹
no99 4 weeks ago
parent
commit
d3f27f2973
  1. 44
      CORS_README.md
  2. 1154
      package-lock.json
  3. 9
      package.json
  4. BIN
      src/assets/bg@2x.png
  5. BIN
      src/assets/loginback.png
  6. BIN
      src/assets/music.mp3
  7. BIN
      src/assets/qilin.webp
  8. BIN
      src/assets/展开.png
  9. 20
      src/router/index.js
  10. 58
      src/store/lottery.js
  11. 20
      src/stores/auth.js
  12. 2
      src/utils/request.js
  13. 102
      src/views/choujiang/Login.vue
  14. 129
      src/views/choujiang/index.vue
  15. 143
      src/views/choujiang/lottery/CardItem.vue
  16. 55
      src/views/choujiang/lottery/ControlBar.vue
  17. 300
      src/views/choujiang/lottery/Lottery3D.vue
  18. 54
      src/views/choujiang/lottery/Mascot.vue
  19. 32
      src/views/choujiang/lottery/MusicPlayer.vue
  20. 292
      src/views/choujiang/lottery/PrizePanel.vue
  21. 4
      src/views/choujiang/lottery/UserList.vue
  22. 217
      src/views/choujiang/lottery/dataManager.js
  23. 149
      src/views/choujiang/lottery/lotteryEngine.js
  24. 2
      src/views/homePage.vue
  25. 2
      src/views/zhongchou/index.vue
  26. 98
      修改完成总结.md
  27. 92
      抽奖逻辑修改说明.md
  28. 113
      测试用例.md

44
CORS_README.md

@ -0,0 +1,44 @@
# 跨域问题解决方案
## 配置说明
本项目已配置Vite代理来解决跨域问题。
### 1. Vite配置 (vite.config.js)
```javascript
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/api': {
target: 'https://dbqb.nfdxy.net',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/api/, '/devLotApi/api')
}
}
}
```
### 2. 请求配置 (src/utils/request.js)
- `baseURL: '/api'` - 设置基础URL为代理路径
### 3. API调用 (src/api/API.js)
- 使用相对路径 `/prize/list` 而不是完整的URL
- 实际请求会被代理到 `https://dbqb.nfdxy.net/devLotApi/api/prize/list`
## 工作原理
1. 前端发起请求到 `/api/prize/list`
2. Vite开发服务器拦截请求
3. 代理将请求转发到 `https://dbqb.nfdxy.net/devLotApi/api/prize/list`
4. 服务器响应通过代理返回给前端
## 注意事项
- 此配置仅在开发环境有效
- 生产环境需要在服务器端配置CORS或使用nginx代理
- 确保目标服务器允许跨域请求

1154
package-lock.json
File diff suppressed because it is too large
View File

9
package.json

@ -12,11 +12,16 @@
"@tweenjs/tween.js": "^18.6.4",
"@vitejs/plugin-vue": "^4.6.2",
"axios": "^1.10.0",
"element-plus": "^2.10.3",
"three": "^0.150.1",
"element-plus": "^2.10.4",
"pinia": "^3.0.3",
"three": "^0.178.0",
"pinia-plugin-persistedstate": "^4.4.1",
"vite": "^4.5.3",
"vue": "^3.5.17",
"vue-router": "^4.5.1"
},
"devDependencies": {
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0"
}
}

BIN
src/assets/bg@2x.png

After

Width: 3840  |  Height: 2160  |  Size: 5.6 MiB

BIN
src/assets/loginback.png

After

Width: 1920  |  Height: 1080  |  Size: 1.7 MiB

BIN
src/assets/music.mp3

BIN
src/assets/qilin.webp

BIN
src/assets/展开.png

After

Width: 40  |  Height: 34  |  Size: 2.9 KiB

20
src/router/index.js

@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth';
const routes = [
{
path: '/',
@ -26,6 +27,11 @@ const routes = [
path: '/hxlCj',
name: 'hxlCj',
component: () => import('../views/choujiang/hxl-cj/cj.vue'),
},
{
path: '/login',
name: 'login',
component: () => import('../views/choujiang/Login.vue'),
}
]
// 创建路由实例
@ -33,5 +39,19 @@ const router = createRouter({
history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
routes
})
// 添加路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore(); // 获取auth store实例
// 仅对/choujiang路由进行登录验证
if (to.path === '/choujiang') {
if (!authStore.isLoggedIn) {
// 如果未登录,重定向到登录页面
next('/login');
return;
}
}
next();
})
// 导出
export default router

58
src/store/lottery.js

@ -4,13 +4,69 @@ import { ref } from 'vue'
export const useLotteryStore = defineStore('lottery', () => {
const lotteryState = ref('idle') // idle, ready, rotating, result
const lastRevealedIdx = ref(-1)
const waitingForNextReveal = ref(false)
//设置中奖人数列表
const winners = ref([])
// 添加用户列表管理
const allUsers = ref([])
const isUsersLoaded = ref(false)
function setWinners(list) {
// 如果是数组,则添加到现有数组中;如果是单个项目,则直接添加
if (Array.isArray(list)) {
winners.value = [...winners.value, ...list]
} else {
winners.value = [...winners.value, list]
}
}
function clearWinners() {
winners.value = []
}
function setLotteryState(state) {
lotteryState.value = state
}
function setLastRevealedIdx(idx) {
lastRevealedIdx.value = idx
}
function setWaitingForNextReveal(val) {
waitingForNextReveal.value = val
}
// 设置用户列表
function setAllUsers(users) {
allUsers.value = users
isUsersLoaded.value = true
}
// 获取随机用户名称
function getRandomUserName() {
if (allUsers.value.length > 0) {
const randomIndex = Math.floor(Math.random() * allUsers.value.length)
return allUsers.value[randomIndex]
}
return ""
}
return {
lotteryState,
setLotteryState
setLotteryState,
lastRevealedIdx,
setLastRevealedIdx,
waitingForNextReveal,
setWaitingForNextReveal,
winners,
setWinners,
clearWinners,
allUsers,
isUsersLoaded,
setAllUsers,
getRandomUserName
}
})

20
src/stores/auth.js

@ -0,0 +1,20 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAuthStore = defineStore('auth', () => {
// 登录状态
const isLoggedIn = ref(false);
// 登录方法
const login = () => {
isLoggedIn.value = true;
};
// 登出方法
const logout = () => {
isLoggedIn.value = false;
};
return {
isLoggedIn,
login,
logout
};
},{persist : false});
//开启持久化)

2
src/utils/request.js

@ -20,7 +20,7 @@ const ERROR_MESSAGES = {
}
const service = axios.create({
baseURL: '', // url = base url + request url+
baseURL: '/api', // 使用代理路径作为baseURL
// timeout: 50000,
withCredentials: false // send cookies when cross-domain requests
// headers: {

102
src/views/choujiang/Login.vue

@ -0,0 +1,102 @@
<template>
<div class="login-container">
<div class="login-card">
<form @submit.prevent="handleLogin">
<div class="form-group">
<input
type="password"
id="password"
v-model="password"
placeholder="请输入密码"
/>
</div>
<button type="submit" class="login-button">进入抽奖</button>
</form>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '../../stores/auth';
const password = ref('');
const CORRECT_PASSWORD = '123456';
const router = useRouter();
const authStore = useAuthStore();
const handleLogin = () => {
if (password.value === '') {
alert('请输入密码');
return;
}
if (password.value === CORRECT_PASSWORD) {
alert('登录成功,即将跳转到抽奖页面');
//
authStore.login(); // 使Pinia
router.push('/choujiang');
} else {
alert('密码错误,请重试');
}
};
//
console.log('登录信息:', {
password: password.value,
});
// API
</script>
<style scoped>
.login-container {
background-image: url('../../assets/loginback.png'); /* 确保路径正确 */
background-position: center;
background-size: cover;
height: 100vh; /* 确保背景图片覆盖整个视口高度 */
width: 100vw; /* 确保背景图片覆盖整个视口宽度 */
position: fixed; /* 使用fixed定位确保背景图片覆盖整个页面 */
top: 0;
left: 0;
z-index: -1; /* 确保背景图片在其他内容下方 */
}
.login-card {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 300px; /* 设置固定宽度 */
height: 200px; /* 设置固定高度 */
padding: 2rem;
background: rgba(255, 255, 255, 0.8); /* 调整背景颜色为半透明白色 */
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
z-index: 1; /* 确保卡片在背景图片上方 */
}
input {
width: 100%;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
margin-bottom: 2rem;
box-sizing: border-box
}
.login-button {
width: 100%;
padding: 0.8rem;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
box-sizing: border-box
}
.login-button:hover {
background-color: #359469;
}
</style>

129
src/views/choujiang/index.vue

@ -1,9 +1,7 @@
<template>
<div class="choujiang-main">
<Lottery3D ref="lottery3DRef" />
<PrizePanel
:prizes="dataManager.state.basicData.prizes"
/>
<PrizePanel :prizes="dataManager.state.basicData.prizes" />
<ControlBar
:lottery-state="lotteryState"
:is-disabled="isDisabled"
@ -11,7 +9,16 @@
@reset="handleReset"
@export="handleExport"
/>
<MusicPlayer />
<MusicPlayer ref="musicPlayerRef" />
<Mascot />
<!-- 透明弹窗 -->
<div v-if="showPrizeExhaustedModal" class="prize-exhausted-modal">
<div class="modal-content">
<p class="modal-text">该礼品已抽取完毕请揭秘下一个礼品</p>
</div>
</div>
<!-- <UserList
:lucky-users="
dataManager.state.basicData.luckyUsers[
@ -31,6 +38,7 @@ import ControlBar from "./lottery/ControlBar.vue";
import MusicPlayer from "./lottery/MusicPlayer.vue";
import Qipao from "./lottery/Qipao.vue";
import UserList from "./lottery/UserList.vue";
import Mascot from "./lottery/Mascot.vue";
import { ref, onMounted, nextTick, computed, watch } from "vue";
import { useDataManager } from "./lottery/dataManager.js";
import { useLotteryEngine } from "./lottery/lotteryEngine.js";
@ -39,6 +47,7 @@ import { useLotteryStore } from "../../store/lottery"; // 路径根据实际情
const qipaoText = ref("");
const showQipao = ref(false);
const showPrizeExhaustedModal = ref(false);
// const lotteryState = ref('idle'); // idle, ready, rotating, result
@ -49,6 +58,16 @@ const lotteryState = computed({
set: (val) => lotteryStore.setLotteryState(val),
});
const lastRevealed = computed({
get: () => lotteryStore.lastRevealedIdx,
set: (val) => lotteryStore.setLastRevealedIdx(val),
});
const waitingForNextReveal = computed({
get: () => lotteryStore.waitingForNextReveal,
set: (val) => lotteryStore.setWaitingForNextReveal(val),
});
const isDisabled = ref(false);
watch(isDisabled, (newVal, oldVal) => {
@ -58,6 +77,7 @@ watch(isDisabled, (newVal, oldVal) => {
//
const dataManager = useDataManager();
let lottery3DRef = ref(null);
let musicPlayerRef = ref(null);
const lotteryEngine = useLotteryEngine(dataManager, {
resetCard: (...args) => lottery3DRef.value?.resetCard?.(...args),
addHighlight: (...args) => lottery3DRef.value?.addHighlight?.(...args),
@ -70,13 +90,27 @@ const lotteryEngine = useLotteryEngine(dataManager, {
onMounted(async () => {
await dataManager.getBasicData();
await dataManager.getUsers();
// dataManager window 使
window.dataManager = dataManager;
//
setTimeout(() => {
if (musicPlayerRef.value && !musicPlayerRef.value.isPlaying()) {
//
musicPlayerRef.value.toggleMusic();
}
}, 1000);
});
function showLotteryQipao() {
const luckys = dataManager.state.currentLuckys;
const prize = dataManager.state.currentPrize;
if (!luckys || luckys.length === 0) return;
const names = luckys.map((item) => item[1]).join("、");
// jwcode username
const names = luckys
.map((item) => item.username || item[1] || item.jwcode || "")
.join("、");
qipaoText.value = `恭喜${names}获得${prize?.title || ""}`;
showQipao.value = true;
setTimeout(() => {
@ -94,18 +128,36 @@ async function handleLotteryClick() {
switch (lotteryState.value) {
case "idle":
//
await lottery3DRef.value?.switchScreen?.("lottery");
console.log("lotteryState 变更前:", lotteryState.value, "-> ready");
// await new Promise((resolve) => setTimeout(resolve, 2000));
lotteryState.value = "ready";
console.log("lotteryState 变更后:", lotteryState.value);
await lottery3DRef.value?.switchScreen?.("lottery");
break;
case "ready":
if (waitingForNextReveal.value) {
console.log("waitingForNextReveal.value", waitingForNextReveal.value);
//
showPrizeExhaustedModal.value = true;
setTimeout(() => {
showPrizeExhaustedModal.value = false;
}, 1000);
break;
}
if (lastRevealed.value === -1) {
console.log("lastRevealed.value", lastRevealed.value);
break;
}
//
console.log("lotteryState 变更前:", lotteryState.value, "-> rotating");
lotteryState.value = "rotating";
console.log("lotteryState 变更后:", lotteryState.value);
//
// isRunning.value = false; //
// isRunning.value = false; //
await lottery3DRef.value?.rotateBallStart?.();
break;
@ -124,10 +176,14 @@ async function handleLotteryClick() {
case "result":
// result
await lottery3DRef.value?.switchScreen?.("lottery");
await new Promise((resolve) => setTimeout(resolve, 2000));
//
lottery3DRef.value?.changeCard1?.();
//2
lotteryState.value = "ready";
break;
@ -166,11 +222,62 @@ function handleNextPrize() {
.choujiang-main {
width: 100vw;
height: 100vh;
position: relative;
position: fixed;
top: 0;
left: 0;
overflow: hidden;
/* 添加背景图片 */
background: url('../../assets/登录.png') ;
background: url("../../assets/bg@2x.png");
background-size: 1920px 980px;
}
/* 透明弹窗样式 */
.prize-exhausted-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 20vh;
z-index: 9999;
pointer-events: none;
}
.modal-content {
background: transparent;
padding: 20px 30px;
border-radius: 10px;
animation: fadeInOut 1s ease-in-out;
}
.modal-text {
color: #ff0000;
font-size: 18px;
font-weight: bold;
text-align: center;
margin: 0;
white-space: nowrap;
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateY(-20px);
}
20% {
opacity: 1;
transform: translateY(0);
}
80% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-20px);
}
}
</style>

143
src/views/choujiang/lottery/CardItem.vue

@ -9,12 +9,12 @@
>
<!-- <div class="company">{{ company }}</div> -->
<!-- <div class="name">{{ user[1] }}</div> -->
<div class="details">{{ (user[0] || "") + "\n" + (user[2] || "") }}</div>
<div class="details">{{ displayText }}</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import { computed, ref, onMounted, onBeforeUnmount, watch } from "vue";
import { useLotteryStore } from "../../../store/lottery"; //
@ -33,46 +33,127 @@ const props = defineProps({
highlight: Boolean,
prize: Boolean,
});
//
const displayText = ref("");
const textSwitchInterval = ref(null);
// store
const allUsers = computed(() => lotteryStore.allUsers);
const isUsersLoaded = computed(() => lotteryStore.isUsersLoaded);
//
const switchText = () => {
if (isUsersLoaded.value && allUsers.value.length > 0) {
displayText.value = lotteryStore.getRandomUserName();
} else {
displayText.value = props.user[0] || "";
}
};
//
const startTextSwitch = () => {
//
if (lotteryState.value === "idle" || lotteryState.value === "ready") {
const scheduleNextSwitch = () => {
// 1-4
const randomInterval = Math.random() * 20000 + 1000; // 10000-20000
textSwitchInterval.value = setTimeout(() => {
switchText();
//
scheduleNextSwitch();
}, randomInterval);
};
scheduleNextSwitch();
}
};
//
const stopTextSwitch = () => {
if (textSwitchInterval.value) {
clearTimeout(textSwitchInterval.value);
textSwitchInterval.value = null;
}
};
//
const handleLotteryStateChange = () => {
if (lotteryState.value === "rotating" || lotteryState.value === "result") {
//
stopTextSwitch();
displayText.value = props.user[0] || "";
} else {
//
startTextSwitch();
}
};
const cardStyle = computed(() => {
//
const baseStyle = {
width: "130px",
height: "170px",
border: "1px solid rgb(255,255,255)",
};
if (props.isBold && props.showTable) {
if (lotteryState.value === "idle") {
return {
// backgroundColor: "rgba(226, 60, 38, 1)",
background: 'linear-gradient(135deg, rgba(243,153,38,0.7) 0%, rgba(207,56,35,1) 100%)',
width: "130px",
height: "170px",
...baseStyle,
background: 'linear-gradient(180deg, rgba(243,153,38,0.7) 0%, rgba(207,56,35,1) 100%)',
};
}
}
// return {
// // background: 'linear-gradient(135deg,rgba(255, 170, 22, 100) 0%, rgba(255, 170, 22, 100) 100%)',
// backgroundColor:'rgba(254, 177, 48, 100)',
// width: '130px',
// height: '170px',
// border: '1px solid rgb(255,255,255)',
// };
if (lotteryState.value === "result") {
return {
background: 'linear-gradient(135deg,rgba(255, 170, 22, 100) 0%, rgba(255, 170, 22, 100) 100%)',
// backgroundColor: "rgba(254, 177, 48, 100)",
width: "130px",
height: "170px",
border: "1px solid rgb(255,255,255)",
};
} else {
return {
backgroundColor: "rgba(254, 177, 48, 100)",
width: "130px",
height: "170px",
border: "1px solid rgb(255,255,255)",
};
// resultprizeCSS
if (lotteryState.value === "result" && props.prize) {
return baseStyle;
}
//
return {
...baseStyle,
backgroundColor: "rgba(254, 177, 48, 100)",
};
});
//
onMounted(async () => {
//
displayText.value = props.user[0] || "";
//
setTimeout(() => {
startTextSwitch();
}, 1000);
//
handleLotteryStateChange();
});
//
watch(lotteryState, () => {
handleLotteryStateChange();
});
//
watch(isUsersLoaded, (loaded) => {
if (loaded) {
//
switchText();
}
});
//
onBeforeUnmount(() => {
stopTextSwitch();
});
</script>
<style scoped>
.element {
transition: background 2s;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
/* 你的基础样式 */
}
.lightitem {
@ -82,8 +163,8 @@ const cardStyle = computed(() => {
/* 响应式高亮样式 */
}
.prize {
/* 中奖样式 */
background-color: #fc0202;
/* 中奖样式 - 使用更高优先级 */
background: linear-gradient(180deg, #F39B26 0%, #E13A26 100%) !important;
}
.company {
/* ... */
@ -93,10 +174,12 @@ const cardStyle = computed(() => {
}
.details {
font-size: 30px;
color: white;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
transition: opacity 0.3s ease;
}
</style>

55
src/views/choujiang/lottery/ControlBar.vue

@ -1,7 +1,10 @@
<template>
<div class="control-bar">
<button :disabled="isDisabled"
@click="$emit('lottery-click')">
<button
:disabled="isDisabled"
@click="$emit('lottery-click')"
class="lottery-button"
>
{{ lotteryState === 'idle' ? '进入抽奖' : lotteryState === 'ready' ? '开始抽奖' : lotteryState === 'rotating' ? '结束抽奖' : '开始抽奖' }}
</button>
<!-- <button @click="$emit('reset')">重置</button> -->
@ -27,17 +30,49 @@ defineEmits(['lottery-click', 'reset', 'export']);
gap: 24px;
z-index: 10;
}
button {
padding: 10px 24px;
font-size: 18px;
border-radius: 6px;
.lottery-button {
padding: 12px 32px;
font-size: 20px;
font-weight: 700;
border-radius: 50px;
border: none;
background: #0078ff;
background: linear-gradient(90deg, #ff8c42 0%, #ff6b35 50%, #ff5722 100%);
color: #fff;
cursor: pointer;
transition: background 0.2s;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(2, 0, 0, 0.5);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
min-width: 160px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
button:hover {
background: #005bb5;
.lottery-button:hover {
background: linear-gradient(90deg, #ff9a5a 0%, #ff7a4a 50%, #ff6b3a 100%);
box-shadow: 0 6px 20px rgba(255, 87, 34, 0.4);
transform: translateY(-2px);
}
.lottery-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(255, 87, 34, 0.3);
}
.lottery-button:disabled {
background: linear-gradient(90deg, #ccc 0%, #bbb 50%, #aaa 100%);
cursor: not-allowed;
box-shadow: none;
transform: none;
opacity: 0.6;
}
.lottery-button:disabled:hover {
background: linear-gradient(90deg, #ccc 0%, #bbb 50%, #aaa 100%);
box-shadow: none;
transform: none;
}
</style>

300
src/views/choujiang/lottery/Lottery3D.vue

@ -18,7 +18,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, defineExpose, watch } from "vue";
import { ref, onMounted, onBeforeUnmount, defineExpose, watch, computed } from "vue";
import * as THREE from "three";
import {
CSS3DRenderer,
@ -29,6 +29,17 @@ import TWEEN from "@tweenjs/tween.js";
import { NUMBER_MATRIX } from "../../../utils/config.js";
import CardItem from "./CardItem.vue";
import { createApp } from "vue";
import { getUserList } from "../../../api/API";
import { useLotteryStore } from "../../../store/lottery";
const lotteryStore = useLotteryStore();
const winners = computed({
get: () => lotteryStore.winners,
set: (val) => lotteryStore.setWinners(val),
});
const threeContainer = ref(null);
let renderer, scene, camera, animationId;
// let controls; // controls
@ -249,12 +260,21 @@ function selectCard(selectedCardIndex, currentLuckys, duration = 600) {
//
const pageIndex = index % cardsPerPage;
const pageLocate = pageLocates[cardPage][pageIndex];
//
let initialY;
if (isVisible) {
initialY = pageLocate.y;
} else {
//
initialY = pageLocate.y - 1000;
}
new TWEEN.Tween(object.position)
.to(
{
x: isVisible ? pageLocate.x : pageLocate.x,
y: isVisible ? pageLocate.y : pageLocate.y + 1000, //
x: pageLocate.x,
y: initialY,
z: 2200,
},
Math.random() * duration + duration
@ -319,25 +339,58 @@ function switchPage(direction) {
const object = threeDCards[cardIndex];
const cardPage = Math.floor(index / cardsPerPage);
const isVisible = cardPage === newPage;
const wasVisible = cardPage === currentPage.value;
//
const pageIndex = index % cardsPerPage;
const pageLocate = pageLocates[cardPage][pageIndex];
console.log(
"cardPage",
cardPage,
"pageIndex",
pageIndex,
"pageLocate",
pageLocate
);
//
let targetY;
if (isVisible) {
//
if (direction === 'next') {
//
targetY = pageLocate.y;
} else {
//
targetY = pageLocate.y;
}
} else {
//
if (direction === 'next') {
//
targetY = pageLocate.y + 1000;
} else {
//
targetY = pageLocate.y - 1000;
}
}
//
let startY;
if (wasVisible) {
//
startY = object.position.y;
} else {
//
if (direction === 'next') {
//
startY = pageLocate.y - 1000;
} else {
//
startY = pageLocate.y + 1000;
}
}
//
object.position.y = startY;
new TWEEN.Tween(object.position)
.to(
{
x: pageLocate.x,
y: isVisible ? pageLocate.y : pageLocate.y + 1000,
y: targetY,
z: 2200,
},
duration
@ -359,57 +412,6 @@ function switchPage(direction) {
.start();
}
// function switchPage(direction) {
// if (isPageTransitioning || totalPages.value <= 1) return;
// const newPage = direction === 'next' ? currentPage.value + 1 : currentPage.value - 1;
// if (newPage < 0 || newPage >= totalPages.value) return;
// isPageTransitioning = true;
// const duration = 800;
// // 使
// const pageLocates = window.pageLocates;
// if (!pageLocates) {
// console.error('');
// isPageTransitioning = false;
// return;
// }
// //
// globalCardIndexes.forEach((cardIndex, index) => {
// const object = threeDCards[cardIndex];
// const cardPage = Math.floor(index / cardsPerPage);
// const isVisible = cardPage === newPage;
// //
// const pageIndex = index % cardsPerPage;
// const pageLocate = pageLocates[cardPage][pageIndex];
// //
// new TWEEN.Tween(object.position)
// .to(
// {
// x: pageLocate.x,
// y: isVisible ? pageLocate.y : (direction === 'next' ? pageLocate.y - 1000 : pageLocate.y + 1000), //
// z: 2200,
// },
// duration
// )
// .easing(TWEEN.Easing.Cubic.InOut)
// .start();
// });
// new TWEEN.Tween({})
// .to({}, duration)
// .onUpdate(() => render())
// .onComplete(() => {
// currentPage.value = newPage;
// isPageTransitioning = false;
// console.log(` ${currentPage.value + 1} ${totalPages.value} `);
// })
// .start();
// }
//
function handleWheel(event) {
@ -459,6 +461,9 @@ function resetCard(selectedCardIndex, duration = 500) {
if (window.pageLocates) {
delete window.pageLocates;
}
//
globalCardIndexes = [];
selectedCardIndex.forEach((index) => {
const object = threeDCards[index];
@ -497,6 +502,11 @@ function resetCard(selectedCardIndex, duration = 500) {
.onComplete(() => {
selectedCardIndex.forEach((index) => {
const object = threeDCards[index];
//
if (object.element.dataset.originalContent) {
object.element.innerHTML = object.element.dataset.originalContent;
delete object.element.dataset.originalContent;
}
object.element.classList.remove("prize");
});
resolve();
@ -511,17 +521,22 @@ function changeCard(cardIndex, user) {
}
const card = threeDCards[cardIndex].element;
// card.innerHTML = `<div class="company">${
// user.company || ""
// }</div><div class="name">${user[1]}</div><div class="details">${
// user[0] || ""
// }<br/>${user[2] || "PSST"}</div>`;
card.style.setProperty("background-color", "#ffffff", "important");
// card.style.backgroundColor = '#ffffff';
// card.style.backgroundColor = '';
// 便
if (!card.dataset.originalContent) {
card.dataset.originalContent = card.innerHTML;
}
// -
// : { jwcode: "5412", username: "22" }
const jwcode = user.jwcode || user[0] || "";
const username = user.username || user[1] || "";
const company = user.company || user[2] || "PSST";
card.innerHTML = `<div style="font-size: 16px; font-weight: bold; color: #ffffff; text-align: center; display: flex; justify-content: center; align-items: center; width: 100%; height: 100%;">${jwcode}</div>`;
//
card.classList.add("prize");
}
function changeCard1() {
@ -542,15 +557,16 @@ function changeCard1() {
globalCardIndexes.forEach((cardIndex) => {
const card = threeDCards[cardIndex].element;
console.log("取消卡片", cardIndex, "的高光");
card.style.setProperty(
"background-color",
"rgba(254, 177, 48, 1)",
"important"
);
// prizeCSSbackgroundColor
card.classList.remove("prize");
// console.log('', cardIndex, '');
//
if (card.dataset.originalContent) {
card.innerHTML = card.dataset.originalContent;
delete card.dataset.originalContent;
}
// prizeCardItem
card.classList.remove('prize');
});
//
globalCardIndexes = [];
@ -641,7 +657,7 @@ function getTotalCards() {
return threeDCards.length;
}
onMounted(() => {
onMounted( async () => {
// 3D
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
@ -668,93 +684,23 @@ onMounted(() => {
HIGHLIGHT_CELL: highlightCells,
COMPANY: "演示公司",
};
const member = [
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
];
const userList = await getUserList();
// lotteryStore.setWinners(userList);
// console.log("userList", userList);
// 3D
const member = userList.data.map(item => [item.jwcode, item.username, "PSST"]);
// store
const userNames = userList.data.map(item => item.jwcode || item.username || "");
lotteryStore.setAllUsers(userNames);
const length = member.length;
const showTable = true;
const position = {
x: (100 * config.COLUMN_COUNT - 20) / 2,
y: (160 * config.ROW_COUNT - 20) / 2,
y: (120 * config.ROW_COUNT - 20) / 2,
};
createCards(member, length, showTable, position, config); // 3.
createSphereTargets();
@ -854,6 +800,18 @@ defineExpose({
animation: fadeInOut 2s ease-in-out infinite;
}
.price-font{
font-size: 16px;
font-weight: bold;
color: #ffffff;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
@keyframes fadeInOut {
0%,
100% {

54
src/views/choujiang/lottery/Mascot.vue

@ -0,0 +1,54 @@
<template>
<div class="mascot-container">
<img
src="../../../assets/qilin.webp"
alt="可爱的角色"
class="mascot-image"
/>
</div>
</template>
<script setup>
//
</script>
<style scoped>
.mascot-container {
position: fixed;
bottom: -10px;
right: -80px;
z-index: 1000;
pointer-events: none;
}
.mascot-image {
width: 400px;
height: 400px;
animation: bounce 2s ease-in-out infinite;
}
/* @keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
} */
/* 响应式设计 */
@media (max-width: 768px) {
.mascot-image {
width: 180px;
height: 180px;
}
.mascot-container {
bottom: 10px;
right: 10px;
}
}
</style>

32
src/views/choujiang/lottery/MusicPlayer.vue

@ -6,8 +6,8 @@
</template>
<script setup>
import { ref } from 'vue';
const musicSrc = '/src/assets/lottery/music.mp3';
import { ref, onMounted } from 'vue';
const musicSrc = '/src/assets/music.mp3';
const audioRef = ref(null);
const playing = ref(false);
@ -21,10 +21,36 @@ function toggleMusic() {
playing.value = false;
}
}
//
function isPlaying() {
return playing.value;
}
//
onMounted(() => {
//
setTimeout(() => {
if (audioRef.value) {
audioRef.value.play().then(() => {
playing.value = true;
}).catch((error) => {
console.log('自动播放失败,可能需要用户交互:', error);
});
}
}, 500);
});
//
defineExpose({
toggleMusic,
isPlaying
});
</script>
<style scoped>
.music-box {
position: absolute;
top: 24px;
right: 32px;
@ -42,7 +68,7 @@ function toggleMusic() {
}
.music-icon.playing {
animation: rotate 1.2s linear infinite;
box-shadow: 0 0 12px #0078ff;
/* box-shadow: 0 0 12px #0078ff; */
}
@keyframes rotate {
100% { transform: rotate(360deg); }

292
src/views/choujiang/lottery/PrizePanel.vue

@ -6,9 +6,17 @@
class="prize-panel-item"
v-for="(prize, idx) in prizes"
:key="prize.type || idx"
:class="{ 'revealed-highlight': idx === lastRevealedIdx }"
:class="{
'revealed-highlight': idx === lastRevealedIdx,
disabled: idx === nextRevealIdx && !canRevealPrize(idx),
}"
@click="handleReveal(idx)"
style="cursor: pointer"
:style="{
cursor:
idx === nextRevealIdx && !canRevealPrize(idx)
? 'not-allowed'
: 'pointer',
}"
>
<div v-if="isRevealed(idx)" class="prize-card">
<div class="prize-img-wrap">
@ -26,7 +34,7 @@
:style="{ width: getProgressPercent(prize) + '%' }"
></div>
<span class="progress-bar-text">
{{ prize.count - getLeftCount(prize) }}/{{ prize.count }}
{{ getLeftCount(prize) }}/{{ prize.count }}
</span>
</div>
</div>
@ -46,14 +54,14 @@
<div></div>
<div class="prize-panel-footer">
<div class="arrow-up" @click="openWinnerList"></div>
<button
<button
ref="winnerBtnRef"
class="winner-btn"
@click="toggleWinnerList"
>
获奖名单
</button>
<div
<!-- <div
v-if="showWinnerList"
class="winner-modal-mask"
@click="closeWinnerList"
@ -70,13 +78,12 @@
<div class="winner-modal-title">Homily ID</div>
<ul class="winner-list">
<li v-for="(user, idx) in fakeWinners" :key="idx">
<!-- <span>{{ user.id }}</span> - <span>{{ user.name }}</span> - -->
<span>{{ user.id }}</span>
<span>{{ user.id }}</span>
<span>{{ user.prize }}</span>
</li>
</ul>
</div>
</div>
</div> -->
<!-- </div> -->
</div>
</div>
</div>
@ -84,7 +91,10 @@
<div v-else>
<div class="prize-panel-root">
<div class="prize-panel-list" v-if="prizes && prizes.length && lastRevealedIdx >= 0">
<div
class="prize-panel-list"
v-if="prizes && prizes.length && lastRevealedIdx >= 0"
>
<div
class="prize-panel-item"
:key="prizes[lastRevealedIdx].type || lastRevealedIdx"
@ -93,40 +103,50 @@
>
<div class="prize-card">
<div class="prize-img-wrap">
<img class="prize-img" :src="prizes[lastRevealedIdx].img" :alt="prizes[lastRevealedIdx].title" />
<img
class="prize-img"
:src="prizes[lastRevealedIdx].img"
:alt="prizes[lastRevealedIdx].title"
/>
</div>
<div class="prize-info">
<div class="prize-row prize-row-top">
<span class="prize-level">{{ prizes[lastRevealedIdx].title }}</span>
<span class="prize-name">{{ prizes[lastRevealedIdx].text }}</span>
<span class="prize-level">{{
prizes[lastRevealedIdx].title
}}</span>
<span class="prize-name">{{
prizes[lastRevealedIdx].text
}}</span>
</div>
<div class="prize-row prize-row-bottom">
<div class="progress-bar-bg">
<div
class="progress-bar-fill"
:style="{ width: getProgressPercent(prizes[lastRevealedIdx]) + '%' }"
:style="{
width: getProgressPercent(prizes[lastRevealedIdx]) + '%',
}"
></div>
<span class="progress-bar-text">
{{ prizes[lastRevealedIdx].count - getLeftCount(prizes[lastRevealedIdx]) }}/{{ prizes[lastRevealedIdx].count }}
{{ getLeftCount(prizes[lastRevealedIdx]) }}/{{
prizes[lastRevealedIdx].count
}}
</span>
</div>
</div>
</div>
</div>
</div>
<div></div>
<div></div>
<div></div>
<div></div>
<div class="prize-panel-footer">
<div class="arrow-up" @click="openWinnerList"></div>
<button
ref="winnerBtnRef"
<div class="prize-panel-footer">
<div class="arrow-down" @click="toggleWinnerList"></div>
<!-- <div class="arrow-up " @click="openWinnerList"> </div>
<button
ref="winnerBtnRef"
class="winner-btn"
@click="toggleWinnerList"
>
获奖名单
</button>
</button> -->
<div
v-if="showWinnerList"
class="winner-modal-mask"
@ -141,11 +161,12 @@
}"
@click.stop
>
<div class="winner-modal-title">Homily ID</div>
<div class="winner-modal-title"><span>Homily ID</span><span>奖项</span></div>
<div style="background:linear-gradient(to left, rgb(232 76 10), rgb(195 6 6), rgb(240 90 9));height:1px;"></div>
<ul class="winner-list">
<li v-for="(user, idx) in fakeWinners" :key="idx">
<li v-for="(user, idx) in fakeWinners" :key="idx" style="display: flex; justify-content: space-between; align-items: center;" >
<!-- <span>{{ user.id }}</span> - <span>{{ user.name }}</span> - -->
<span>{{ user.id }}</span>
<span >{{ user.jwcode }}</span>
<span>{{ user.prize }}</span>
</li>
</ul>
@ -158,17 +179,42 @@
</template>
<script setup>
import { ref, computed, nextTick } from "vue";
import { ref, computed, nextTick, watch } from "vue";
import { useLotteryStore } from "../../../store/lottery";
const props = defineProps({
prizes: Array,
});
//
const revealedCount = ref(0);
//
const lotteryStore = useLotteryStore();
const lastRevealed = computed({
get: () => lotteryStore.lastRevealedIdx,
set: (val) => lotteryStore.setLastRevealedIdx(val),
});
const waitingForNextReveal = computed({
get: () => lotteryStore.waitingForNextReveal,
set: (val) => lotteryStore.setWaitingForNextReveal(val),
});
const winners = computed({
get: () => lotteryStore.winners,
set: (val) => lotteryStore.setWinners(val),
});
// watchwinners
watch(winners, (newVal) => {
console.log('中奖人数列表winners', newVal);
fakeWinners.value = newVal;
});
const lastRevealedIdx = ref(-1);
const showOne = ref(true);
lastRevealedIdx.value = lastRevealed.value;
const showOne = ref(true);
//
const isRevealed = (idx) =>
@ -177,11 +223,46 @@ const isRevealed = (idx) =>
const nextRevealIdx = computed(
() => (props.prizes?.length || 0) - revealedCount.value - 1
);
//
function canRevealPrize(idx) {
//
if (lastRevealedIdx.value === -1) {
return true;
}
//
const lastPrize = props.prizes[lastRevealedIdx.value];
if (lastPrize) {
const leftCount = getLeftCount(lastPrize);
//
if (leftCount > 0) {
waitingForNextReveal.value = false;
return false;
}
waitingForNextReveal.value = true;
}
return true;
}
//
function handleReveal(idx) {
if (idx === nextRevealIdx.value) {
//
if (idx === nextRevealIdx.value && canRevealPrize(idx)) {
revealedCount.value++;
lastRevealedIdx.value = idx; //
if(idx===0){
waitingForNextReveal.value = false;
}
console.log("lastRevealedIdx.value", lastRevealedIdx.value);
lastRevealed.value = idx;
console.log("lastRevealed.value", lastRevealed.value);
} else if (idx === nextRevealIdx.value && !canRevealPrize(idx)) {
//
console.log("上一个奖品还未抽完,不能揭秘下一个奖品");
// toast
// alert("");
}
}
//
@ -197,28 +278,31 @@ function getLeftCount(prize) {
//
const showWinnerList = ref(false);
const fakeWinners = ref([
{ id: "90044065", name: "张三", prize: "六等奖" },
{ id: "90044066", name: "李四", prize: "六等奖" },
{ id: "90044067", name: "王五", prize: "六等奖" },
{ id: "90044068", name: "赵六", prize: "六等奖" },
{ id: "90044069", name: "小明", prize: "六等奖" },
]);
const fakeWinners = ref([]);
fakeWinners.value = winners.value;
console.log('fakeWinners', fakeWinners.value);
function openWinnerList() {
showWinnerList.value = true;
// showWinnerList.value = true;
if (!showWinnerList.value) {
if (revealedCount.value === 0) {
alert('请先揭晓奖品,并抽奖!');}
}
if(revealedCount.value > 0)
{ showWinnerList.value = true;}
// showOnefalse
if (lastRevealedIdx.value >= 0) {
showOne.value = false;
}
//
nextTick(() => {
const btn = winnerBtnRef.value;
if (btn) {
const rect = btn.getBoundingClientRect();
modalLeft.value = rect.left - 23;
modalTop.value = rect.bottom + 18; // 4px
}
});
// nextTick(() => {
// const btn = winnerBtnRef.value;
// if (btn) {
// const rect = btn.getBoundingClientRect();
// modalLeft.value = rect.left - 23;
// modalTop.value = rect.bottom + 18; // 4px
// }
// });
}
function closeWinnerList() {
showWinnerList.value = false;
@ -228,33 +312,40 @@ function closeWinnerList() {
}
}
const winnerBtnRef = ref(null);
const winnerBtnRef = ref(null);
const modalLeft = ref(0);
const modalTop = ref(0);
function toggleWinnerList() {
showWinnerList.value = !showWinnerList.value;
console.log('toggleWinnerList - showWinnerList:', showWinnerList.value, 'showOne:', showOne.value, 'lastRevealedIdx:', lastRevealedIdx.value);
if (!showWinnerList.value) {
if (revealedCount.value === 0) {
alert('请先揭晓奖品,并抽奖!');}
}
if (showWinnerList.value) {
// showOnefalse
if (lastRevealedIdx.value >= 0) {
if (lastRevealedIdx.value > 0) {
showOne.value = false;
console.log('设置 showOne 为 false');
}
//
nextTick(() => {
const btn = winnerBtnRef.value;
if (btn) {
const rect = btn.getBoundingClientRect();
modalLeft.value = rect.left - 23;
modalTop.value = rect.bottom + 18; // 4px
}
});
// nextTick(() => {
// const btn = winnerBtnRef.value;
// if (btn) {
// const rect = btn.getBoundingClientRect();
// modalLeft.value = rect.left - 23;
// modalTop.value = rect.bottom + 18; // 4px
// }
// });
} else {
// showOnefalsetrue
if (!showOne.value) {
showOne.value = true;
console.log('设置 showOne 为 true');
}
}
@ -263,8 +354,8 @@ function toggleWinnerList() {
function getProgressPercent(prize) {
const total = prize.count || 1;
const left = getLeftCount(prize);
const got = total - left;
return Math.round((got / total) * 100);
//
return Math.round((left / total) * 100);
}
</script>
@ -276,6 +367,7 @@ function getProgressPercent(prize) {
background: none;
z-index: 10;
min-width: 320px;
max-width: 342px;
text-align: left;
display: flex;
flex-direction: column;
@ -287,7 +379,8 @@ function getProgressPercent(prize) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
min-width: 300px;
min-width: 320px;
}
.prize-card {
display: flex;
@ -326,6 +419,7 @@ function getProgressPercent(prize) {
}
.prize-row-top {
margin-bottom: 8px;
border: 1px solid #ea2b0a;
}
.prize-level {
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%);
@ -352,6 +446,17 @@ function getProgressPercent(prize) {
justify-content: center;
min-width: 80px;
} */
.custom-arrow-icon {
font-size: 24px; /* 图标大小 */
color: #d84315; /* 图标颜色,使用项目主题橙色 */
margin: 5px; /* 外边距 */
cursor: pointer; /* 鼠标悬停样式 */
transition: transform 0.3s ease; /* 过渡动画 */
}
.custom-arrow-icon:hover {
transform: scale(1.1); /* 悬停放大效果 */
}
.prize-count {
font-size: 20px;
font-weight: bold;
@ -367,7 +472,7 @@ function getProgressPercent(prize) {
.prize-panel-footer {
position: absolute;
left: 0;
bottom: 0;
bottom: -26px;
width: 100%;
display: flex;
flex-direction: column;
@ -376,15 +481,31 @@ function getProgressPercent(prize) {
/* 移除 move-up 相关 */
}
.arrow-up {
position: relative;
width: 36px;
height: 24px;
background: url("@/assets/arrow-up.svg") no-repeat center/contain;
margin-bottom: 4px;
cursor: pointer;
background-image: url('../../../assets/展开.png');
background-size: cover;
background-position: center;
}
.arrow-down {
position: fixed;
top: 120px;
left: 165px;
width: 36px;
height: 38px;
margin-bottom: 4px;
cursor: pointer;
background-image: url('../../../assets/展开.png');
background-size: cover;
background-position: center;
transform: rotate(180deg);
}
.winner-btn {
background: rgba(255, 210, 131, 0.8);
color: #fff;
color: #D5291F;
border: #fff;
border-radius: 8px;
padding: 15px 79px;
@ -395,8 +516,8 @@ function getProgressPercent(prize) {
}
.winner-modal-mask {
position: fixed;
top: 0;
left: 0;
top: 155px;
left:10px;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.01);
@ -409,16 +530,26 @@ function getProgressPercent(prize) {
background: rgba(255, 210, 131, 0.8);
border-radius: 12px;
padding-top: 12px;
padding-left: 25px;
padding-right: 25px;
padding-bottom: 10px;
min-width: 280px;
max-width: 90vw;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
position: relative;
margin-left: 10px;
}
.winner-modal-title {
font-size: 22px;
font-weight: bold;
margin-bottom: 18px;
color: #e64f39;
margin-bottom: 5px;
text-align: center;
display: flex;
justify-content: space-between; /* 左右对齐 */
align-items: center; /* 垂直居中对齐 */
/* 可添加padding或margin调整整体间距 */
padding: 5px 0;
}
.winner-modal-close {
position: absolute;
@ -501,10 +632,37 @@ function getProgressPercent(prize) {
border-radius: 8px 8px 8px 8px;
}
.prize-panel-item.revealed-highlight {
border: 3px solid #ff9800;
/* border: 3px solid #ff9800; */
box-shadow: 0 0 16px 4px #ff9800aa;
transform: scale(1.05);
z-index: 2;
transition: all 0.3s;
}
.prize-panel-item.disabled {
cursor: not-allowed !important;
position: relative;
}
.prize-panel-item.disabled::after {
content: "请先抽完上一个奖品";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
white-space: nowrap;
z-index: 10;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.prize-panel-item.disabled:hover::after {
opacity: 1;
}
</style>

4
src/views/choujiang/lottery/UserList.vue

@ -16,6 +16,10 @@
</template>
<script setup>
import { useLotteryStore } from "../../../store/lottery";
const lotteryStore = useLotteryStore();
const props = defineProps({
luckyUsers: {
type: Array,

217
src/views/choujiang/lottery/dataManager.js

@ -1,5 +1,5 @@
import { reactive } from 'vue';
import { getPrizeList, getUserList } from '../../../api/API';
export function useDataManager() {
const state = reactive({
basicData: {
@ -12,94 +12,56 @@ export function useDataManager() {
currentPrize: null,
currentLuckys: [],
isLotting: false,
// 新增:轮次管理
currentRound: 1,
config: {
prizes: [],
EACH_COUNT: [],
COMPANY: '',
HIGHLIGHT_CELL: [],
Resolution: 1
Resolution: 1,
ROW_COUNT: 7,
COLUMN_COUNT: 20
}
});
async function getBasicData() {
// 假数据,后续可替换为接口
const fakePrizes = [
{ type: 0, title: '特别奖', text: 'iPad', count: 10, img: '/src/assets/lottery/ipad.jpg' },
{ type: 1, title: '一等奖', text: 'Kindle', count: 20, img: '/src/assets/lottery/kindle.jpg' },
{ type: 2, title: '二等奖', text: 'MacBook Pro', count: 10, img: '/src/assets/lottery/mbp.jpg' }
];
const fakeEachCount = [15, 17, 36];
// 获取奖品列表
const prizeList = await getPrizeList();
const fakePrizes = prizeList.data.map((item, index) => ({
type: index, // 使用索引作为type
title: item.gradeName,
text: item.prizeName,
count: item.amount,
img: item.imageUrl
}));
const fakeEachCount = prizeList.data.map(item => item.perWin);
console.log("fakeEachCount", fakeEachCount);
const fakeCompany = '前端假公司';
const fakeLuckyData = {};
fakePrizes.forEach(prize => {
fakeLuckyData[prize.type] = [];
fakePrizes.forEach((prize, index) => {
fakeLuckyData[index] = [];
});
const fakeLeftUsers = [
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"], [0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"], [0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
];
// 获取真实用户数据
const userListResponse = await getUserList();
console.log("userList", userListResponse);
// 将后端返回的用户数据转换为兼容格式
const realUsers = userListResponse.data.map(item => ({
jwcode: item.jwcode,
username: item.username,
company: fakeCompany // 使用默认公司名称
}));
state.config.prizes = fakePrizes;
state.config.EACH_COUNT = fakeEachCount;
state.config.COMPANY = fakeCompany;
state.config.HIGHLIGHT_CELL = [];
state.basicData.prizes = fakePrizes;
state.basicData.leftUsers = fakeLeftUsers.slice();
state.basicData.users = realUsers; // 使用真实用户数据
state.basicData.leftUsers = realUsers.slice(); // 初始化剩余用户为所有用户
state.basicData.luckyUsers = fakeLuckyData;
determineCurrentPrize();
return Promise.resolve({
@ -108,74 +70,24 @@ export function useDataManager() {
EACH_COUNT: fakeEachCount,
COMPANY: fakeCompany
},
leftUsers: fakeLeftUsers.slice(),
leftUsers: realUsers.slice(),
luckyData: fakeLuckyData
});
}
async function getUsers() {
const fakeUsers = [
[0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"], [0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"], [0, "张三"],
[1, "李四"],
[2, "王五"],
[3, "赵六"],
[4, "孙七"],
[5, "周八"],
[6, "吴九"],
[7, "郑十"],
[8, "钱十一"],
[9, "孙十二"],
[10, "李十三"],
[11, "周十四"],
[12, "吴十五"],
[13, "郑十六"],
[14, "钱十七"],
[15, "孙十八"],
[16, "李十九"],
[17, "周二十"],
[18, "吴二一"],
[19, "郑二二"],
];
state.basicData.users = fakeUsers;
return Promise.resolve(fakeUsers);
const userList = await getUserList();
console.log("userList", userList);
// 将后端返回的用户数据转换为兼容格式
const realUsers = userList.data.map(item => ({
jwcode: item.jwcode,
username: item.username,
company: state.config.COMPANY || '前端假公司'
}));
state.basicData.users = realUsers;
return Promise.resolve(userList);
}
function determineCurrentPrize() {
@ -213,6 +125,35 @@ export function useDataManager() {
}
function updateCurrentPrize() { determineCurrentPrize(); }
// 新增:计算当前奖品的总轮次
function getTotalRounds(prizeIndex) {
const prize = state.basicData.prizes[prizeIndex];
const eachCount = state.config.EACH_COUNT[prizeIndex];
if (!prize || !eachCount) return 0;
return Math.ceil(prize.count / eachCount);
}
// 新增:计算当前轮次
function getCurrentRound(prizeIndex) {
const luckyUsers = state.basicData.luckyUsers[prizeIndex] || [];
const eachCount = state.config.EACH_COUNT[prizeIndex];
if (!eachCount) return 1;
return Math.floor(luckyUsers.length / eachCount) + 1;
}
// 新增:计算剩余数量
function getLeftCount(prizeIndex) {
const prize = state.basicData.prizes[prizeIndex];
const luckyUsers = state.basicData.luckyUsers[prizeIndex] || [];
if (!prize) return 0;
return prize.count - luckyUsers.length;
}
// 新增:更新当前轮次
function updateCurrentRound() {
state.currentRound = getCurrentRound(state.currentPrizeIndex);
}
return {
state,
getBasicData,
@ -224,6 +165,10 @@ export function useDataManager() {
getTotalCards,
setLotteryStatus,
resetAllData,
updateCurrentPrize
updateCurrentPrize,
getTotalRounds,
getCurrentRound,
getLeftCount,
updateCurrentRound
};
}

149
src/views/choujiang/lottery/lotteryEngine.js

@ -1,10 +1,24 @@
import { ref } from 'vue';
import { ref, computed, watch } from 'vue';
import { useLotteryStore } from '../../../store/lottery' // 路径根据实际情况调整
import { drawLottery } from '../../../api/API'; // 导入新的抽奖接口
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
const lotteryStore = useLotteryStore();
const winners = computed({
get: () => lotteryStore.winners,
set: (val) => lotteryStore.setWinners(val),
});
// 用watch监听winners的变化
// watch(winners, (newVal) => {
// console.log('winners', newVal);
// winners.value = newVal;
// });
export function useLotteryEngine(dataManager, renderer3D) {
const isLotting = ref(false);
const lotteryStore = useLotteryStore(); // 只获取一次
@ -17,35 +31,118 @@ export function useLotteryEngine(dataManager, renderer3D) {
changePrize();
// 重置卡片动画
await renderer3D.resetCard([]);
// 生成中奖卡片索引和中奖用户
const perCount = dataManager.state.config.EACH_COUNT[dataManager.state.currentPrizeIndex] || 1;
const totalCards = renderer3D.getTotalCards ? renderer3D.getTotalCards() : 50;
const leftUsers = dataManager.state.basicData.leftUsers;
let selectedCardIndex = [];
let currentLuckys = [];
let leftCount = leftUsers.length;
console.log('executeLottery - perCount:', perCount, 'leftCount:', leftCount, 'totalCards:', totalCards);
// 计算本次应该抽奖的人数
const currentPrizeIndex = dataManager.state.currentPrizeIndex;
const prize = dataManager.state.basicData.prizes[currentPrizeIndex];
const luckyUsers = dataManager.state.basicData.luckyUsers[currentPrizeIndex] || [];
const remainingPrizeCount = prize.count - luckyUsers.length; // 奖品剩余数量
const basePerCount = dataManager.state.config.EACH_COUNT[currentPrizeIndex] || 1;
const actualPerCount = Math.min(basePerCount, remainingPrizeCount); // 取最小值
console.log('executeLottery - currentPrizeIndex:', currentPrizeIndex, 'prize:', prize, 'basePerCount:', basePerCount, 'remainingPrizeCount:', remainingPrizeCount, 'actualPerCount:', actualPerCount);
// 随机抽取中奖用户和卡片索引
for (let i = 0; i < perCount && leftCount > 0; i++) {
const luckyId = getRandomInt(leftCount);
currentLuckys.push(leftUsers.splice(luckyId, 1)[0]);
leftCount--;
let cardIndex = getRandomInt(totalCards);
while (selectedCardIndex.includes(cardIndex)) {
cardIndex = getRandomInt(totalCards);
// 请求后端进行抽奖
try {
const lotteryData = {
gradeName: prize.title,
prizeName: prize.text,
perWin: basePerCount,
round: dataManager.state.currentRound
};
console.log('请求后端抽奖,参数:', lotteryData);
const response = await drawLottery(lotteryData);
console.log('response', response);
winners.value = response.data;
console.log('抽奖的winners', winners.value);
// winners.value = response.data;
console.log('后端抽奖返回结果:', response);
if (response && response.data && Array.isArray(response.data)) {
// 后端返回中奖用户数据
const currentLuckys = response.data.map(item => ({
jwcode: item.jwcode,
username: item.username
}));
console.log('后端返回的中奖用户:', currentLuckys);
// 生成随机卡片索引用于显示
const totalCards = dataManager.getTotalCards();
let selectedCardIndex = [];
for (let i = 0; i < currentLuckys.length; i++) {
let cardIndex = getRandomInt(totalCards);
while (selectedCardIndex.includes(cardIndex)) {
cardIndex = getRandomInt(totalCards);
}
selectedCardIndex.push(cardIndex);
}
console.log('executeLottery - selectedCardIndex:', selectedCardIndex, 'currentLuckys:', currentLuckys);
dataManager.state.currentLuckys = currentLuckys;
// 保存中奖用户到对应奖品
if (!dataManager.state.basicData.luckyUsers[currentPrizeIndex]) {
dataManager.state.basicData.luckyUsers[currentPrizeIndex] = [];
}
dataManager.state.basicData.luckyUsers[currentPrizeIndex].push(...currentLuckys);
// 更新轮次信息
dataManager.updateCurrentRound();
// 展示中奖动画
console.log('executeLottery - calling selectCard');
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys);
console.log('executeLottery - selectCard completed');
} else {
console.error('后端抽奖返回数据格式错误:', response);
throw new Error('抽奖失败:后端返回数据格式错误');
}
} catch (error) {
console.error('抽奖请求失败:', error);
// 如果后端请求失败,可以回退到前端随机抽奖逻辑
console.log('回退到前端随机抽奖逻辑');
const totalCards = dataManager.getTotalCards();
const leftUsers = dataManager.state.basicData.leftUsers;
let selectedCardIndex = [];
let currentLuckys = [];
let leftCount = leftUsers.length;
// 随机抽取中奖用户和卡片索引
for (let i = 0; i < actualPerCount && leftCount > 0; i++) {
const luckyId = getRandomInt(leftCount);
const selectedUser = leftUsers.splice(luckyId, 1)[0];
// 确保数据格式一致
currentLuckys.push({
jwcode: selectedUser.jwcode || selectedUser[0] || "",
username: selectedUser.username || selectedUser[1] || "",
company: selectedUser.company || selectedUser[2] || "PSST"
});
leftCount--;
let cardIndex = getRandomInt(totalCards);
while (selectedCardIndex.includes(cardIndex)) {
cardIndex = getRandomInt(totalCards);
}
selectedCardIndex.push(cardIndex);
}
console.log('executeLottery - selectedCardIndex:', selectedCardIndex, 'currentLuckys:', currentLuckys);
dataManager.state.currentLuckys = currentLuckys;
// 保存中奖用户到对应奖品
if (!dataManager.state.basicData.luckyUsers[currentPrizeIndex]) {
dataManager.state.basicData.luckyUsers[currentPrizeIndex] = [];
}
selectedCardIndex.push(cardIndex);
dataManager.state.basicData.luckyUsers[currentPrizeIndex].push(...currentLuckys);
// 更新轮次信息
dataManager.updateCurrentRound();
// 展示中奖动画
console.log('executeLottery - calling selectCard');
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys);
console.log('executeLottery - selectCard completed');
}
console.log('executeLottery - selectedCardIndex:', selectedCardIndex, 'currentLuckys:', currentLuckys);
dataManager.state.currentLuckys = currentLuckys;
// 展示中奖动画
console.log('executeLottery - calling selectCard');
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys);
console.log('executeLottery - selectCard completed');
dataManager.setLotteryStatus(false);
isLotting.value = false;
}
@ -66,6 +163,8 @@ export function useLotteryEngine(dataManager, renderer3D) {
lotteryStore.setLotteryState('idle'); // 直接用
await renderer3D.resetCard([]);
await dataManager.resetData();
// 重置轮次
dataManager.state.currentRound = 1;
renderer3D.switchScreen && renderer3D.switchScreen('enter');
}

2
src/views/homePage.vue

@ -2,11 +2,13 @@
<div>
<button @click="router.push('/choujiang')">抽奖</button>
<button @click="router.push('/zhongchou')">众筹</button>
<Mascot />
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
// import Mascot from "../choujiang/lottery/Mascot.vue";
const router = useRouter();

2
src/views/zhongchou/index.vue

@ -1,10 +1,12 @@
<template>
<div>
众筹
<Mascot />
</div>
</template>
<script setup>
import Mascot from "../choujiang/lottery/Mascot.vue";
</script>

98
修改完成总结.md

@ -0,0 +1,98 @@
# 抽奖逻辑修改完成总结
## 修改完成情况
✅ **已完成所有必要的修改**
## 修改文件清单
### 1. API接口层 (`src/api/API.js`)
- ✅ 新增 `drawLottery` 接口
- ✅ 支持传递奖项信息和轮次参数
### 2. 抽奖引擎 (`src/views/choujiang/lottery/lotteryEngine.js`)
- ✅ 修改 `executeLottery` 函数,每轮抽奖请求后端
- ✅ 添加错误处理机制,支持回退到前端随机抽奖
- ✅ 适配新的数据格式 `{ jwcode: "xxx", username: "xxx" }`
### 3. 数据管理器 (`src/views/choujiang/lottery/dataManager.js`)
- ✅ 使用真实用户数据替代假数据
- ✅ 确保数据格式与后端返回格式兼容
- ✅ 支持新旧两种数据格式
### 4. 3D显示组件 (`src/views/choujiang/lottery/Lottery3D.vue`)
- ✅ 修改 `changeCard` 函数,适配新的数据格式
- ✅ 支持显示 `jwcode``username` 字段
- ✅ 保持向后兼容性
### 5. 主抽奖页面 (`src/views/choujiang/index.vue`)
- ✅ 修改气泡提示逻辑,支持新的数据格式
- ✅ 确保中奖用户信息正确显示
## 核心功能
### 1. 后端抽奖接口
```javascript
// 请求参数
{
gradeName: "一等奖",
prizeName: "iPhone 15",
perWin: 5,
round: 1
}
// 返回数据
{
data: [
{ jwcode: "5412", username: "猪八戒22" },
{ jwcode: "45125", username: "宝玉" }
]
}
```
### 2. 错误处理机制
- 后端请求失败时自动回退到前端随机抽奖
- 确保抽奖功能不中断
- 提供详细的错误日志
### 3. 数据格式兼容性
- 支持新格式:`{ jwcode: "5412", username: "猪八戒22" }`
- 支持旧格式:`["5412", "猪八戒22", "PSST"]`
- 自动识别和适配不同格式
## 工作流程
1. **页面初始化** → 获取奖品列表和用户列表
2. **开始抽奖** → 用户点击抽奖按钮
3. **请求后端** → 发送抽奖请求,包含奖项信息和轮次
4. **获取结果** → 后端返回中奖用户列表
5. **显示动画** → 前端根据返回的中奖用户显示3D抽奖动画
6. **保存结果** → 将中奖用户保存到本地状态
## 测试建议
1. **正常流程测试**:验证完整的抽奖流程
2. **异常处理测试**:模拟后端接口异常
3. **数据格式测试**:验证新旧数据格式兼容性
4. **多轮抽奖测试**:验证轮次管理和奖品切换
5. **性能测试**:验证大量用户数据下的表现
## 部署注意事项
1. **后端接口**:确保实现 `/lottery/draw` 接口
2. **数据格式**:确保返回的数据格式符合要求
3. **错误处理**:建议后端提供详细的错误信息
4. **性能优化**:考虑大量并发抽奖请求的处理
## 后续优化建议
1. **缓存机制**:可以考虑缓存用户数据,减少重复请求
2. **实时更新**:可以考虑WebSocket实时更新中奖结果
3. **数据统计**:可以添加抽奖统计和分析功能
4. **界面优化**:可以根据实际需求优化3D动画效果
---
**修改完成时间**:2024年12月19日
**修改状态**:✅ 已完成
**测试状态**:🔄 待测试

92
抽奖逻辑修改说明.md

@ -0,0 +1,92 @@
# 抽奖逻辑修改说明
## 修改概述
本次修改将抽奖逻辑从前端随机抽奖改为每轮抽奖都请求后端获取中奖数据。
## 主要修改内容
### 1. API接口修改 (`src/api/API.js`)
新增了 `drawLottery` 接口,用于每轮抽奖时请求后端:
```javascript
export function drawLottery(data){
return request({
url: '/lottery/draw',
method: 'post',
data: {
gradeName: data.gradeName, // 奖项名称
prizeName: data.prizeName, // 奖品名称
perWin: data.perWin, // 每轮抽奖人数
round: data.round // 当前轮次
}
})
}
```
### 2. 抽奖引擎修改 (`src/views/choujiang/lottery/lotteryEngine.js`)
- 修改 `executeLottery` 函数,每轮抽奖都请求后端
- 后端返回数据格式:`{ data: [{ jwcode: "5412", username: "猪八戒22" }, ...] }`
- 添加了错误处理机制,如果后端请求失败会回退到前端随机抽奖
### 3. 数据管理器修改 (`src/views/choujiang/lottery/dataManager.js`)
- 使用真实的用户数据替代假数据
- 确保数据格式与后端返回格式兼容
- 用户数据格式:`{ jwcode: "5412", username: "猪八戒22", company: "公司名称" }`
### 4. 3D显示组件修改 (`src/views/choujiang/lottery/Lottery3D.vue`)
- 修改 `changeCard` 函数,适配新的数据格式
- 支持显示 `jwcode``username` 字段
## 后端接口要求
### 抽奖接口 `/lottery/draw`
**请求参数:**
```json
{
"gradeName": "一等奖",
"prizeName": "iPhone 15",
"perWin": 5,
"round": 1
}
```
**返回数据格式:**
```json
{
"data": [
{
"jwcode": "5412",
"username": "猪八戒22"
},
{
"jwcode": "45125",
"username": "宝玉"
}
]
}
```
## 工作流程
1. **初始化**:页面加载时获取奖品列表和用户列表
2. **开始抽奖**:用户点击抽奖按钮
3. **请求后端**:发送抽奖请求到后端,包含奖项信息和轮次
4. **获取结果**:后端返回中奖用户列表
5. **显示动画**:前端根据返回的中奖用户显示3D抽奖动画
6. **保存结果**:将中奖用户保存到本地状态
## 错误处理
如果后端抽奖接口请求失败,系统会自动回退到前端随机抽奖逻辑,确保抽奖功能不会中断。
## 兼容性
修改后的代码保持了与原有数据格式的兼容性,支持新旧两种数据格式:
- 新格式:`{ jwcode: "5412", username: "猪八戒22" }`
- 旧格式:`["5412", "猪八戒22", "PSST"]`

113
测试用例.md

@ -0,0 +1,113 @@
# 抽奖逻辑测试用例
## 测试环境准备
1. 确保后端服务正常运行
2. 确保以下接口可用:
- `/prize/list` - 获取奖品列表
- `/user/list` - 获取用户列表
- `/lottery/draw` - 抽奖接口(新增)
## 测试用例
### 测试用例1:正常抽奖流程
**测试步骤:**
1. 打开抽奖页面
2. 点击"进入抽奖"按钮
3. 点击"开始抽奖"按钮
4. 点击"结束抽奖"按钮
5. 观察抽奖结果
**预期结果:**
- 页面正常加载,显示奖品列表和用户卡片
- 抽奖动画正常播放
- 后端返回中奖用户数据
- 3D卡片正确显示中奖用户信息(jwcode和username)
### 测试用例2:后端接口异常处理
**测试步骤:**
1. 模拟后端抽奖接口返回错误
2. 执行抽奖流程
3. 观察系统行为
**预期结果:**
- 系统自动回退到前端随机抽奖逻辑
- 抽奖功能不中断
- 控制台显示错误日志
### 测试用例3:数据格式兼容性
**测试步骤:**
1. 使用新数据格式:`{ jwcode: "5412", username: "猪八戒22" }`
2. 使用旧数据格式:`["5412", "猪八戒22", "PSST"]`
3. 观察显示效果
**预期结果:**
- 两种数据格式都能正确显示
- 卡片内容包含jwcode和username信息
### 测试用例4:多轮抽奖
**测试步骤:**
1. 完成第一轮抽奖
2. 继续第二轮抽奖
3. 观察轮次信息是否正确传递
**预期结果:**
- 每轮抽奖都请求后端
- 轮次信息正确递增
- 中奖用户不重复
### 测试用例5:奖品切换
**测试步骤:**
1. 完成当前奖品的所有轮次抽奖
2. 观察是否自动切换到下一个奖品
3. 验证新奖品的抽奖逻辑
**预期结果:**
- 奖品自动切换
- 新奖品的抽奖参数正确传递
- 轮次重新开始计算
## 调试信息
在浏览器控制台中查看以下日志:
1. **用户数据加载:**
```
userList {data: Array(5)}
```
2. **抽奖请求:**
```
请求后端抽奖,参数: {gradeName: "一等奖", prizeName: "iPhone 15", perWin: 5, round: 1}
```
3. **后端返回结果:**
```
后端抽奖返回结果: {data: Array(5)}
后端返回的中奖用户: [{jwcode: "5412", username: "猪八戒22"}, ...]
```
4. **卡片选择:**
```
executeLottery - selectedCardIndex: [12, 34, 56, 78, 90]
executeLottery - currentLuckys: [{jwcode: "5412", username: "猪八戒22"}, ...]
```
## 常见问题排查
### 问题1:抽奖接口404错误
**解决方案:** 检查后端是否实现了 `/lottery/draw` 接口
### 问题2:数据格式不匹配
**解决方案:** 检查后端返回的数据格式是否符合 `{data: [{jwcode: "xxx", username: "xxx"}]}`
### 问题3:用户数据为空
**解决方案:** 检查 `/user/list` 接口是否正常返回数据
### 问题4:奖品数据为空
**解决方案:** 检查 `/prize/list` 接口是否正常返回数据
Loading…
Cancel
Save