48 Commits
bf600b46c7
...
4ffa3a5472
-
7.env.development
-
4.env.production
-
2.vercel/project.json
-
3.vscode/extensions.json
-
44CORS_README.md
-
4README.md
-
2index.html
-
1829package-lock.json
-
16package.json
-
BINpublic/img/edifier.jpg
-
BINpublic/img/huawei.png
-
BINpublic/img/ipad.jpg
-
BINpublic/img/kindle.jpg
-
BINpublic/img/mbp.jpg
-
BINpublic/img/secrit.jpg
-
BINpublic/img/spark.jpg
-
73src/api/API.js
-
14src/api/zhongchouApi.js
-
588src/assets/PrizePanel1.vue
-
BINsrc/assets/beijingtu.jpg
-
BINsrc/assets/bg@2x.png
-
11src/assets/css/animate.min.css
-
BINsrc/assets/daijiemi.png
-
BINsrc/assets/image.png
-
BINsrc/assets/img/bg.png
-
BINsrc/assets/img/展开.png
-
BINsrc/assets/img/待揭秘.png
-
BINsrc/assets/img/抽奖按钮.png
-
BINsrc/assets/img/麒麟.png
-
BINsrc/assets/loginback.png
-
BINsrc/assets/lottery
-
BINsrc/assets/music.mp3
-
BINsrc/assets/qilin.webp
-
BINsrc/assets/展开.png
-
BINsrc/assets/登录背景.png
-
BINsrc/data/music.mp3
-
21src/main.js
-
25src/router/index.js
-
81src/store/lottery.js
-
25src/stores/auth.js
-
56src/style.css
-
320src/utils/CSS3DRenderer.js
-
517src/utils/TrackballControls.js
-
55src/utils/config.js
-
260src/utils/prizeList.js
-
113src/utils/request.js
-
957src/utils/three.min.js
-
1src/utils/tween.min.js
-
101src/views/choujiang/Login.vue
-
2153src/views/choujiang/hxl-cj/cj.vue
-
347src/views/choujiang/index.vue
-
185src/views/choujiang/lottery/CardItem.vue
-
78src/views/choujiang/lottery/ControlBar.vue
-
886src/views/choujiang/lottery/Lottery3D.vue
-
54src/views/choujiang/lottery/Mascot.vue
-
76src/views/choujiang/lottery/MusicPlayer.vue
-
716src/views/choujiang/lottery/PrizePanel.vue
-
37src/views/choujiang/lottery/Qipao.vue
-
62src/views/choujiang/lottery/UserList.vue
-
217src/views/choujiang/lottery/dataManager.js
-
198src/views/choujiang/lottery/lotteryEngine.js
-
2src/views/homePage.vue
-
22src/views/zhongchou/index.vue
-
42vite.config.js
-
22vite.config.js.timestamp-1753077096347-78067037e45f7.mjs
-
98修改完成总结.md
-
92抽奖逻辑修改说明.md
-
113测试用例.md
@ -1 +1 @@ |
|||
{"projectName":"trae_zf0uu3bt"} |
|||
{"projectName":"trae_zf0uu3bt"} |
@ -1,3 +0,0 @@ |
|||
{ |
|||
"recommendations": ["Vue.volar"] |
|||
} |
@ -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代理 |
|||
- 确保目标服务器允许跨域请求 |
1829
package-lock.json
File diff suppressed because it is too large
View File
After Width: 198 | Height: 198 | Size: 23 KiB |
After Width: 200 | Height: 200 | Size: 49 KiB |
After Width: 220 | Height: 220 | Size: 44 KiB |
After Width: 289 | Height: 289 | Size: 42 KiB |
After Width: 220 | Height: 220 | Size: 6.9 KiB |
After Width: 224 | Height: 224 | Size: 12 KiB |
After Width: 260 | Height: 260 | Size: 37 KiB |
@ -1,19 +1,56 @@ |
|||
// import request from "../utils/request";
|
|||
// const APIurl = import.meta.env.VITE_APP_API_BASE_URL;
|
|||
// // 开发环境使用代理,生产环境使用环境变量
|
|||
// // const APIurl = import.meta.env.DEV ? '/Api' : import.meta.env.VITE_APP_API_BASE_URL;
|
|||
import axios from "axios"; |
|||
import request from "../utils/request"; |
|||
|
|||
// export function addRecordAPI(data) {
|
|||
// return request({
|
|||
// url: `Api/api/funding/addRecord`,
|
|||
// method: 'post',
|
|||
// data: data
|
|||
// })
|
|||
// }
|
|||
// // 新增:获取活动信息接口
|
|||
// export function getActivity1API() {
|
|||
// return request({
|
|||
// url: `Api/api/funding/getActivity1`,
|
|||
// method: 'post'
|
|||
// })
|
|||
// }
|
|||
const APIurl = import.meta.env.VITE_APP_API_BASE_URL; |
|||
|
|||
// 查询所有奖品和对应等级,按sort排序
|
|||
export const getPrizeListApi = function (params) { |
|||
return request({ |
|||
url: `${APIurl}/api/prize/list`, |
|||
method: "POST", |
|||
data: new URLSearchParams(params), |
|||
}); |
|||
}; |
|||
|
|||
// 查询用户
|
|||
export const getUserListApi = function (params) { |
|||
return request({ |
|||
url: `${APIurl}/api/user/list`, |
|||
method: "POST", |
|||
data: new URLSearchParams(params), |
|||
}); |
|||
}; |
|||
|
|||
// 查询中奖名单
|
|||
export const getGetPrizeUserListApi = function (params) { |
|||
return request({ |
|||
url: `${APIurl}/admin/win/list`, |
|||
method: "POST", |
|||
data: params, |
|||
}); |
|||
}; |
|||
|
|||
// 开始抽奖
|
|||
export const startLotteryApi = function (params) { |
|||
return request({ |
|||
url: `${APIurl}/api/lottery/start`, |
|||
method: "POST", |
|||
data: params, |
|||
}); |
|||
}; |
|||
|
|||
// 新增:每轮抽奖接口
|
|||
export function drawLottery(data) { |
|||
return request({ |
|||
url: `${APIurl}/api/lottery/start`, |
|||
method: "post", |
|||
data: { |
|||
gradeName: data.gradeName, |
|||
prizeName: data.prizeName, |
|||
perWin: data.perWin, |
|||
remainNum: data.remainNum, |
|||
gradeId: data.gradeId, |
|||
prizeId: data.prizeId, |
|||
}, |
|||
}); |
|||
} |
@ -0,0 +1,588 @@ |
|||
<template> |
|||
<div class="prize-panel-root"> |
|||
<div class="prize-panel-list" v-if="prizes && prizes.length" :style="containerStyle"> |
|||
<div |
|||
class="prize-panel-item" |
|||
v-for="(prize, idx) in prizes" |
|||
:key="prize.type || idx" |
|||
:class="{ |
|||
'revealed-highlight': idx === lastRevealedIdx, |
|||
'winner-mode-highlight': showWinnerList && idx === lastRevealedIdx |
|||
}" |
|||
@click="showWinnerList ? null : handleReveal(idx)" |
|||
:style="{ cursor: showWinnerList ? 'default' : 'pointer' }" |
|||
:ref="el => setPrizeRef(el, idx)" |
|||
v-show="!shouldHideOtherPrizes || idx === lastRevealedIdx" |
|||
> |
|||
<div v-if="isRevealed(idx)" class="prize-card"> |
|||
<div class="prize-img-wrap"> |
|||
<img class="prize-img" :src="prize.img" :alt="prize.title" /> |
|||
</div> |
|||
<div class="prize-info"> |
|||
<div class="prize-row prize-row-top"> |
|||
<span class="prize-level">{{ prize.title }}</span> |
|||
<span class="prize-name">{{ prize.text }}</span> |
|||
</div> |
|||
<div class="prize-row prize-row-bottom"> |
|||
<div class="progress-bar-bg"> |
|||
<div |
|||
class="progress-bar-fill" |
|||
:style="{ width:getProgressPercent(prize) + '%' }" |
|||
></div> |
|||
<span class="progress-bar-text"> |
|||
{{ prize.count-getLeftCount(prize) }}/{{ prize.count }} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div v-else class="prize-card prize-card-mask"> |
|||
<img src="../../../assets/daijiemi.png" alt="待揭秘" class="prize-mask-img" /> |
|||
</div> |
|||
</div> |
|||
<!-- 隐藏占位div --> |
|||
<div v-show="!shouldHideOtherPrizes"></div> |
|||
<div v-show="!shouldHideOtherPrizes"></div> |
|||
<div v-show="!shouldHideOtherPrizes"></div> |
|||
<div v-show="!shouldHideOtherPrizes"></div> |
|||
<!-- 动态定位的获奖名单按钮 --> |
|||
<div |
|||
class="prize-panel-footer" |
|||
:class="{ 'winner-mode': shouldHideOtherPrizes }" |
|||
:style="winnerBtnStyle" |
|||
> |
|||
<div class="arrow-up" @click="openWinnerList"></div> |
|||
<button ref="winnerBtnRef" class="winner-btn" @click="toggleWinnerList"> |
|||
{{ showWinnerList ? '关闭名单' : '获奖名单' }} |
|||
</button> |
|||
<div |
|||
v-if="showWinnerList" |
|||
class="winner-modal-mask" |
|||
@click="closeWinnerList" |
|||
> |
|||
<div |
|||
class="winner-modal" |
|||
:style="{ position: 'absolute', left: modalLeft + 'px', top: modalTop + 'px' }" |
|||
@click.stop |
|||
> |
|||
<div class="winner-modal-header"> |
|||
<div class="winner-modal-title">Homily ID</div> |
|||
<div class="winner-modal-close" @click="closeWinnerList">×</div> |
|||
</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.prize }}</span> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, computed, nextTick, watch } from "vue"; |
|||
const props = defineProps({ |
|||
prizes: Array, |
|||
}); |
|||
// 新增:控制已揭秘奖品数量 |
|||
const revealedCount = ref(0); |
|||
// 新增:记录最新揭秘的奖品索引 |
|||
const lastRevealedIdx = ref(-1); |
|||
// 新增:奖品引用数组 |
|||
const prizeRefs = ref([]); |
|||
// 新增:获奖名单按钮样式 |
|||
const winnerBtnStyle = ref({ |
|||
position: 'absolute', |
|||
left: '0', |
|||
bottom: '0', |
|||
width: '100%' |
|||
}); |
|||
|
|||
// 新增:容器样式计算属性 |
|||
const containerStyle = computed(() => { |
|||
if (shouldHideOtherPrizes.value) { |
|||
return { |
|||
justifyContent: 'flex-start', |
|||
alignItems: 'flex-start', |
|||
paddingTop: '20px' |
|||
}; |
|||
} |
|||
return {}; |
|||
}); |
|||
|
|||
// 设置奖品引用 |
|||
function setPrizeRef(el, idx) { |
|||
if (el) { |
|||
prizeRefs.value[idx] = el; |
|||
} |
|||
} |
|||
|
|||
// 计算哪些奖品已揭秘 |
|||
const isRevealed = idx => idx >= (props.prizes?.length || 0) - revealedCount.value; |
|||
// 允许点击的卡片index |
|||
const nextRevealIdx = computed(() => (props.prizes?.length || 0) - revealedCount.value - 1); |
|||
// 卡片点击事件 |
|||
function handleReveal(idx) { |
|||
if (idx === nextRevealIdx.value) { |
|||
revealedCount.value++; |
|||
lastRevealedIdx.value = idx; // 记录最新揭秘的索引 |
|||
} |
|||
} |
|||
// 计算未抽取数量 |
|||
function getLeftCount(prize) { |
|||
// 这里假设奖品有 type 字段,且 dataManager.state.basicData.luckyUsers 可用 |
|||
// 由于本组件无 luckyUsers 数据,建议父组件传入或全局可访问 |
|||
// 这里用 window.dataManager 兼容演示 |
|||
let luckyUsers = |
|||
(window.dataManager && window.dataManager.state.basicData.luckyUsers) || {}; |
|||
let got = luckyUsers[prize.type]?.length || 0; |
|||
return prize.count - got; |
|||
} |
|||
|
|||
// 新增部分 |
|||
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 shouldHideOtherPrizes = computed(() => { |
|||
return showWinnerList.value && lastRevealedIdx.value >= 0; |
|||
}); |
|||
|
|||
// 新增:定位获奖名单按钮到高亮奖品下方 |
|||
function positionWinnerBtn() { |
|||
if (lastRevealedIdx.value >= 0 && prizeRefs.value[lastRevealedIdx.value]) { |
|||
const highlightedPrize = prizeRefs.value[lastRevealedIdx.value]; |
|||
|
|||
// 当显示获奖名单且有高亮奖品时,进行特殊定位 |
|||
if (shouldHideOtherPrizes.value) { |
|||
const rect = highlightedPrize.getBoundingClientRect(); |
|||
const containerRect = highlightedPrize.parentElement.getBoundingClientRect(); |
|||
|
|||
// 计算相对于容器的位置,考虑奖品高度和间距 |
|||
const relativeTop = rect.bottom - containerRect.top + 18; // 18px间距 |
|||
|
|||
winnerBtnStyle.value = { |
|||
position: 'absolute', |
|||
left: '0', |
|||
top: relativeTop + 'px', |
|||
width: '100%', |
|||
zIndex: '20' |
|||
}; |
|||
} else { |
|||
// 正常模式下,按钮保持在底部,不进行特殊定位 |
|||
winnerBtnStyle.value = { |
|||
position: 'absolute', |
|||
left: '0', |
|||
bottom: '0', |
|||
width: '100%' |
|||
}; |
|||
} |
|||
} else { |
|||
// 如果没有高亮奖品,回到默认位置 |
|||
winnerBtnStyle.value = { |
|||
position: 'absolute', |
|||
left: '0', |
|||
bottom: '0', |
|||
width: '100%' |
|||
}; |
|||
} |
|||
} |
|||
|
|||
function openWinnerList() { |
|||
showWinnerList.value = true; |
|||
// 只有在有高亮奖品时才重新定位按钮 |
|||
if (lastRevealedIdx.value >= 0) { |
|||
nextTick(() => { |
|||
positionWinnerBtn(); |
|||
}); |
|||
} else { |
|||
// 如果没有高亮奖品,按钮保持在底部 |
|||
winnerBtnStyle.value = { |
|||
position: 'absolute', |
|||
left: '0', |
|||
bottom: '0', |
|||
width: '100%' |
|||
}; |
|||
} |
|||
} |
|||
|
|||
function closeWinnerList() { |
|||
showWinnerList.value = false; |
|||
// 关闭获奖名单时,按钮回到底部 |
|||
nextTick(() => { |
|||
winnerBtnStyle.value = { |
|||
position: 'absolute', |
|||
left: '0', |
|||
bottom: '0', |
|||
width: '100%' |
|||
}; |
|||
}); |
|||
} |
|||
|
|||
const winnerBtnRef = ref(null); |
|||
const modalLeft = ref(0); |
|||
const modalTop = ref(0); |
|||
|
|||
function toggleWinnerList() { |
|||
showWinnerList.value = !showWinnerList.value; |
|||
if (showWinnerList.value) { |
|||
nextTick(() => { |
|||
const btn = winnerBtnRef.value; |
|||
if (btn) { |
|||
const rect = btn.getBoundingClientRect(); |
|||
modalLeft.value = rect.left-22; |
|||
modalTop.value = rect.bottom + 20; // 4px间距 |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// 监听高亮奖品变化,自动重新定位按钮 |
|||
watch(lastRevealedIdx, () => { |
|||
nextTick(() => { |
|||
// 只有在获奖名单模式下才重新定位按钮 |
|||
if (lastRevealedIdx.value >= 0 && showWinnerList.value) { |
|||
positionWinnerBtn(); |
|||
} else if (lastRevealedIdx.value >= 0) { |
|||
// 正常模式下,按钮保持在底部 |
|||
winnerBtnStyle.value = { |
|||
position: 'absolute', |
|||
left: '0', |
|||
bottom: '0', |
|||
width: '100%' |
|||
}; |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
function getProgressPercent(prize) { |
|||
const total = prize.count || 1; |
|||
const left = getLeftCount(prize); |
|||
const got = total - left; |
|||
return Math.round((got / total) * 100); |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.prize-panel-list { |
|||
position: absolute; |
|||
top: 20px; |
|||
left: 20px; |
|||
background: none; |
|||
z-index: 10; |
|||
min-width: 320px; |
|||
text-align: left; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 18px; |
|||
/* 新增:支持flexbox排序 */ |
|||
align-items: flex-start; |
|||
/* 新增:支持滚动和定位 */ |
|||
max-height: calc(100vh - 40px); |
|||
/* overflow-y: auto; */ |
|||
/* 新增:当显示获奖名单时的特殊样式 */ |
|||
transition: all 0.3s ease; |
|||
} |
|||
.prize-panel-item { |
|||
background: #ffd283; |
|||
border-radius: 6px 6px 6px 6px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
|||
display: flex; |
|||
align-items: center; |
|||
min-width: 300px; |
|||
transition: opacity 0.3s ease, transform 0.3s ease; |
|||
} |
|||
.prize-card { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
padding: 10px 18px; |
|||
|
|||
} |
|||
.prize-img-wrap { |
|||
width: 64px; |
|||
height: 64px; |
|||
border-radius: 50%; |
|||
background: #fff; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
overflow: hidden; |
|||
margin-right: 18px; |
|||
border: 2px solid #fff3e0; |
|||
} |
|||
.prize-img { |
|||
width: 60px; |
|||
height: 60px; |
|||
object-fit: contain; |
|||
} |
|||
.prize-info { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
} |
|||
.prize-row { |
|||
display: flex; |
|||
align-items: center; |
|||
background: #ffffff; |
|||
border-radius: 93px 93px 93px 93px; |
|||
} |
|||
.prize-row-top { |
|||
margin-bottom: 8px; |
|||
} |
|||
.prize-level { |
|||
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%); |
|||
color: #fff; |
|||
border-radius: 15.71px 15.71px 15.71px 15.71px; |
|||
padding: 2px 18px; |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
margin-right: 12px; |
|||
} |
|||
.prize-name { |
|||
font-size: 18px; |
|||
color: #d84315; |
|||
font-weight: 500; |
|||
} |
|||
/* .prize-row-bottom { |
|||
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%); |
|||
background: #8a3500; |
|||
border-radius: 16px; |
|||
color: #fff; |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
padding: 2px 0 2px 0; |
|||
justify-content: center; |
|||
min-width: 80px; |
|||
} */ |
|||
.prize-count { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
} |
|||
.prize-divider { |
|||
margin: 0 4px; |
|||
font-size: 20px; |
|||
} |
|||
.prize-total { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
} |
|||
/* 新增:获奖名单按钮容器样式调整 */ |
|||
.prize-panel-footer { |
|||
position: absolute; |
|||
left: 0; |
|||
width: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
z-index: 20; |
|||
transition: all 0.3s ease; |
|||
/* 移除 bottom 定位,改为动态定位 */ |
|||
} |
|||
|
|||
/* 新增:获奖名单模式下的按钮样式 */ |
|||
.prize-panel-footer.winner-mode { |
|||
position: relative; |
|||
margin-top: 18px; |
|||
} |
|||
.arrow-up { |
|||
width: 36px; |
|||
height: 24px; |
|||
background: url("@/assets/arrow-up.svg") no-repeat center/contain; |
|||
margin-bottom: 4px; |
|||
cursor: pointer; |
|||
} |
|||
.winner-btn { |
|||
background: rgba(255, 210, 131, 0.8); |
|||
color: #fff; |
|||
border: #fff; |
|||
border-radius: 8px; |
|||
padding: 15px 79px; |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
cursor: pointer; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.winner-btn:hover { |
|||
background: rgba(255, 210, 131, 1); |
|||
transform: translateY(-2px); |
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
|||
} |
|||
.winner-modal-mask { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background: rgba(0,0,0,0.01); |
|||
z-index: 1000; |
|||
display: flex; |
|||
align-items: flex-start; |
|||
justify-content: center; |
|||
} |
|||
.winner-modal { |
|||
background:rgba(255, 210, 131, 0.8); |
|||
border-radius: 12px; |
|||
/* margin-top: 2vh; */ |
|||
padding-top: 12px; |
|||
min-width: 280px; |
|||
max-width: 90vw; |
|||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12); |
|||
position: relative; |
|||
} |
|||
.winner-modal-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 18px; |
|||
padding: 0 12px; |
|||
} |
|||
.winner-modal-title { |
|||
font-size: 22px; |
|||
font-weight: bold; |
|||
text-align: center; |
|||
flex: 1; |
|||
} |
|||
.winner-modal-close { |
|||
font-size: 24px; |
|||
color: #d84315; |
|||
cursor: pointer; |
|||
width: 30px; |
|||
height: 30px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 50%; |
|||
background: rgba(255, 255, 255, 0.8); |
|||
transition: all 0.2s ease; |
|||
} |
|||
|
|||
.winner-modal-close:hover { |
|||
background: rgba(255, 255, 255, 1); |
|||
transform: scale(1.1); |
|||
} |
|||
.winner-list { |
|||
max-height: 260px; |
|||
/* background: rgba(255, 210, 131, 0.8);/ */ |
|||
overflow-y: auto; |
|||
padding: 0; |
|||
margin: 0; |
|||
list-style: none; |
|||
} |
|||
.winner-list li { |
|||
padding: 8px 0; |
|||
/* border-bottom: 1px solid #f2f2f2; */ |
|||
font-size: 17px; |
|||
color: #d84315; |
|||
display: flex; |
|||
gap: 12px; |
|||
align-items: center; |
|||
justify-content: center; |
|||
text-align: center; |
|||
} |
|||
.progress-bar-bg { |
|||
position: relative; |
|||
width: 220px; |
|||
height: 28px; |
|||
background: #E9620E; |
|||
border-radius: 16px; |
|||
overflow: hidden; |
|||
display: flex; |
|||
align-items: center; |
|||
margin: 0 auto; |
|||
border: #E13726; |
|||
} |
|||
.progress-bar-fill { |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
height: 100%; |
|||
/* background: linear-gradient(90deg, #ff9800 0%, #8a3500 100%); */ |
|||
background: #8a3500; |
|||
border-radius: 16px; |
|||
transition: width 0.4s; |
|||
z-index: 1; |
|||
} |
|||
.progress-bar-text { |
|||
position: relative; |
|||
width: 100%; |
|||
text-align: center; |
|||
color: #ffffff; |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
z-index: 2; |
|||
letter-spacing: 1px; |
|||
} |
|||
.prize-card-mask { |
|||
position: relative; |
|||
width: 342px; |
|||
height: 88px; |
|||
display: flex; |
|||
/* align-items: center; |
|||
justify-content: center; */ |
|||
padding: 0; |
|||
overflow: hidden; |
|||
} |
|||
.prize-mask-img { |
|||
object-fit: cover; |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 98%; |
|||
object-fit: cover; |
|||
left: 0; |
|||
top: 0; |
|||
border-radius: 8px 8px 8px 8px; |
|||
|
|||
} |
|||
.prize-panel-item.revealed-highlight { |
|||
border: 3px solid #ff9800; |
|||
box-shadow: 0 0 16px 4px #ff9800aa; |
|||
transform: scale(1.05); |
|||
z-index: 2; |
|||
transition: all 0.3s; |
|||
/* 确保高亮奖品在顶部 */ |
|||
position: relative; |
|||
} |
|||
|
|||
/* 新增:只有在获奖名单模式下才上移 */ |
|||
.prize-panel-item.revealed-highlight.winner-mode-highlight { |
|||
order: -1; |
|||
margin-bottom: 18px; |
|||
/* 确保在左侧顶部显示 */ |
|||
align-self: flex-start; |
|||
width: 100%; |
|||
} |
|||
|
|||
/* 新增:获奖名单模式下的高亮样式 */ |
|||
.prize-panel-item.winner-mode-highlight { |
|||
transform: scale(1.05); |
|||
box-shadow: 0 0 24px 8px #ff9800dd; |
|||
border: 4px solid #ff9800; |
|||
animation: winnerPulse 2s ease-in-out infinite; |
|||
/* 确保在左侧顶部显示 */ |
|||
margin-top: 0; |
|||
margin-bottom: 18px; |
|||
} |
|||
|
|||
@keyframes winnerPulse { |
|||
0%, 100% { |
|||
box-shadow: 0 0 24px 8px #ff9800dd; |
|||
} |
|||
50% { |
|||
box-shadow: 0 0 32px 12px #ff9800ff; |
|||
} |
|||
} |
|||
</style> |
After Width: 3844 | Height: 2156 | Size: 1.9 MiB |
After Width: 3840 | Height: 2160 | Size: 5.6 MiB |
11
src/assets/css/animate.min.css
File diff suppressed because it is too large
View File
After Width: 766 | Height: 220 | Size: 144 KiB |
After Width: 664 | Height: 199 | Size: 83 KiB |
After Width: 1920 | Height: 1080 | Size: 1.8 MiB |
After Width: 40 | Height: 34 | Size: 2.9 KiB |
After Width: 383 | Height: 110 | Size: 47 KiB |
After Width: 259 | Height: 95 | Size: 8.1 KiB |
After Width: 252 | Height: 335 | Size: 104 KiB |
After Width: 1920 | Height: 1080 | Size: 1.7 MiB |
After Width: 40 | Height: 34 | Size: 2.9 KiB |
After Width: 5760 | Height: 3240 | Size: 867 KiB |
@ -1,6 +1,17 @@ |
|||
import { createApp } from 'vue' |
|||
// import './style.css'
|
|||
import App from './App.vue' |
|||
import router from './router' |
|||
import { createApp } from "vue"; |
|||
import { createPinia } from "pinia"; |
|||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; |
|||
import "./style.css"; |
|||
import App from "./App.vue"; |
|||
import router from "./router"; |
|||
import ElementPlus from "element-plus"; |
|||
|
|||
createApp(App).use(router).mount('#app') |
|||
const pinia = createPinia(); |
|||
pinia.use(piniaPluginPersistedstate); |
|||
|
|||
const app = createApp(App); |
|||
|
|||
app.use(router); |
|||
app.use(ElementPlus); |
|||
app.use(pinia); |
|||
app.mount("#app"); |
@ -0,0 +1,81 @@ |
|||
// activityLink/src/store/lottery.js
|
|||
import { defineStore } from 'pinia' |
|||
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) |
|||
|
|||
const winnerList = ref([]) |
|||
|
|||
|
|||
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 "" |
|||
} |
|||
|
|||
function setWinnerList(list) { |
|||
winnerList.value = list |
|||
} |
|||
|
|||
return { |
|||
lotteryState, |
|||
setLotteryState, |
|||
lastRevealedIdx, |
|||
setLastRevealedIdx, |
|||
waitingForNextReveal, |
|||
setWaitingForNextReveal, |
|||
winners, |
|||
setWinners, |
|||
clearWinners, |
|||
allUsers, |
|||
isUsersLoaded, |
|||
setAllUsers, |
|||
getRandomUserName, |
|||
winnerList, |
|||
setWinnerList |
|||
} |
|||
}) |
@ -0,0 +1,25 @@ |
|||
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: { |
|||
storage: sessionStorage, // 使用会话存储替代默认的localStorage
|
|||
paths: ['isLoggedIn'] // 可选:指定需要持久化的状态字段
|
|||
} |
|||
}); |
|||
//开启持久化)
|
@ -0,0 +1,320 @@ |
|||
/** |
|||
* Based on http://www.emagix.net/academic/mscs-project/item/camera-sync-with-css3-and-webgl-threejs
|
|||
* @author mrdoob / http://mrdoob.com/
|
|||
* @author yomotsu / https://yomotsu.net/
|
|||
*/ |
|||
|
|||
import * as THREE from "three"; |
|||
|
|||
export class CSS3DObject extends THREE.Object3D { |
|||
constructor(element) { |
|||
super(); |
|||
|
|||
this.element = element; |
|||
this.element.style.position = "absolute"; |
|||
|
|||
this.addEventListener("removed", function () { |
|||
if (this.element.parentNode !== null) { |
|||
this.element.parentNode.removeChild(this.element); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export class CSS3DSprite extends CSS3DObject { |
|||
constructor(element) { |
|||
super(element); |
|||
} |
|||
} |
|||
|
|||
//
|
|||
|
|||
export function CSS3DRenderer() { |
|||
console.log("THREE.CSS3DRenderer", THREE.REVISION); |
|||
|
|||
var _width, _height; |
|||
var _widthHalf, _heightHalf; |
|||
|
|||
var matrix = new THREE.Matrix4(); |
|||
|
|||
var cache = { |
|||
camera: { fov: 0, style: "" }, |
|||
objects: new WeakMap(), |
|||
}; |
|||
|
|||
var domElement = document.createElement("div"); |
|||
domElement.style.overflow = "hidden"; |
|||
|
|||
this.domElement = domElement; |
|||
|
|||
var cameraElement = document.createElement("div"); |
|||
|
|||
cameraElement.style.WebkitTransformStyle = "preserve-3d"; |
|||
cameraElement.style.transformStyle = "preserve-3d"; |
|||
|
|||
domElement.appendChild(cameraElement); |
|||
|
|||
var isIE = /Trident/i.test(navigator.userAgent); |
|||
|
|||
this.getSize = function () { |
|||
return { |
|||
width: _width, |
|||
height: _height, |
|||
}; |
|||
}; |
|||
|
|||
this.setSize = function (width, height) { |
|||
_width = width; |
|||
_height = height; |
|||
_widthHalf = _width / 2; |
|||
_heightHalf = _height / 2; |
|||
|
|||
domElement.style.width = width + "px"; |
|||
domElement.style.height = height + "px"; |
|||
|
|||
cameraElement.style.width = width + "px"; |
|||
cameraElement.style.height = height + "px"; |
|||
}; |
|||
|
|||
function epsilon(value) { |
|||
return Math.abs(value) < 1e-10 ? 0 : value; |
|||
} |
|||
|
|||
function getCameraCSSMatrix(matrix) { |
|||
var elements = matrix.elements; |
|||
|
|||
return ( |
|||
"matrix3d(" + |
|||
epsilon(elements[0]) + |
|||
"," + |
|||
epsilon(-elements[1]) + |
|||
"," + |
|||
epsilon(elements[2]) + |
|||
"," + |
|||
epsilon(elements[3]) + |
|||
"," + |
|||
epsilon(elements[4]) + |
|||
"," + |
|||
epsilon(-elements[5]) + |
|||
"," + |
|||
epsilon(elements[6]) + |
|||
"," + |
|||
epsilon(elements[7]) + |
|||
"," + |
|||
epsilon(elements[8]) + |
|||
"," + |
|||
epsilon(-elements[9]) + |
|||
"," + |
|||
epsilon(elements[10]) + |
|||
"," + |
|||
epsilon(elements[11]) + |
|||
"," + |
|||
epsilon(elements[12]) + |
|||
"," + |
|||
epsilon(-elements[13]) + |
|||
"," + |
|||
epsilon(elements[14]) + |
|||
"," + |
|||
epsilon(elements[15]) + |
|||
")" |
|||
); |
|||
} |
|||
|
|||
function getObjectCSSMatrix(matrix, cameraCSSMatrix) { |
|||
var elements = matrix.elements; |
|||
var matrix3d = |
|||
"matrix3d(" + |
|||
epsilon(elements[0]) + |
|||
"," + |
|||
epsilon(elements[1]) + |
|||
"," + |
|||
epsilon(elements[2]) + |
|||
"," + |
|||
epsilon(elements[3]) + |
|||
"," + |
|||
epsilon(-elements[4]) + |
|||
"," + |
|||
epsilon(-elements[5]) + |
|||
"," + |
|||
epsilon(-elements[6]) + |
|||
"," + |
|||
epsilon(-elements[7]) + |
|||
"," + |
|||
epsilon(elements[8]) + |
|||
"," + |
|||
epsilon(elements[9]) + |
|||
"," + |
|||
epsilon(elements[10]) + |
|||
"," + |
|||
epsilon(elements[11]) + |
|||
"," + |
|||
epsilon(elements[12]) + |
|||
"," + |
|||
epsilon(elements[13]) + |
|||
"," + |
|||
epsilon(elements[14]) + |
|||
"," + |
|||
epsilon(elements[15]) + |
|||
")"; |
|||
|
|||
if (isIE) { |
|||
return ( |
|||
"translate(-50%,-50%)" + |
|||
"translate(" + |
|||
_widthHalf + |
|||
"px," + |
|||
_heightHalf + |
|||
"px)" + |
|||
cameraCSSMatrix + |
|||
matrix3d |
|||
); |
|||
} |
|||
|
|||
return "translate(-50%,-50%)" + matrix3d; |
|||
} |
|||
|
|||
function renderObject(object, camera, cameraCSSMatrix) { |
|||
if (object instanceof CSS3DObject) { |
|||
var style; |
|||
|
|||
if (object instanceof CSS3DSprite) { |
|||
// http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/
|
|||
|
|||
matrix.copy(camera.matrixWorldInverse); |
|||
matrix.transpose(); |
|||
matrix.copyPosition(object.matrixWorld); |
|||
matrix.scale(object.scale); |
|||
|
|||
matrix.elements[3] = 0; |
|||
matrix.elements[7] = 0; |
|||
matrix.elements[11] = 0; |
|||
matrix.elements[15] = 1; |
|||
|
|||
style = getObjectCSSMatrix(matrix, cameraCSSMatrix); |
|||
} else { |
|||
style = getObjectCSSMatrix(object.matrixWorld, cameraCSSMatrix); |
|||
} |
|||
|
|||
var element = object.element; |
|||
var cachedObject = cache.objects.get(object); |
|||
|
|||
if (cachedObject === undefined || cachedObject.style !== style) { |
|||
element.style.WebkitTransform = style; |
|||
element.style.transform = style; |
|||
|
|||
var objectData = { style: style }; |
|||
|
|||
if (isIE) { |
|||
objectData.distanceToCameraSquared = getDistanceToSquared( |
|||
camera, |
|||
object |
|||
); |
|||
} |
|||
|
|||
cache.objects.set(object, objectData); |
|||
} |
|||
|
|||
if (element.parentNode !== cameraElement) { |
|||
cameraElement.appendChild(element); |
|||
} |
|||
} |
|||
|
|||
for (var i = 0, l = object.children.length; i < l; i++) { |
|||
renderObject(object.children[i], camera, cameraCSSMatrix); |
|||
} |
|||
} |
|||
|
|||
var getDistanceToSquared = (function () { |
|||
var a = new THREE.Vector3(); |
|||
var b = new THREE.Vector3(); |
|||
|
|||
return function (object1, object2) { |
|||
a.setFromMatrixPosition(object1.matrixWorld); |
|||
b.setFromMatrixPosition(object2.matrixWorld); |
|||
|
|||
return a.distanceToSquared(b); |
|||
}; |
|||
})(); |
|||
|
|||
function filterAndFlatten(scene) { |
|||
var result = []; |
|||
|
|||
scene.traverse(function (object) { |
|||
if (object instanceof THREE.CSS3DObject) result.push(object); |
|||
}); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
function zOrder(scene) { |
|||
var sorted = filterAndFlatten(scene).sort(function (a, b) { |
|||
var distanceA = cache.objects.get(a).distanceToCameraSquared; |
|||
var distanceB = cache.objects.get(b).distanceToCameraSquared; |
|||
|
|||
return distanceA - distanceB; |
|||
}); |
|||
|
|||
var zMax = sorted.length; |
|||
|
|||
for (var i = 0, l = sorted.length; i < l; i++) { |
|||
sorted[i].element.style.zIndex = zMax - i; |
|||
} |
|||
} |
|||
|
|||
this.render = function (scene, camera) { |
|||
var fov = camera.projectionMatrix.elements[5] * _heightHalf; |
|||
|
|||
if (cache.camera.fov !== fov) { |
|||
if (camera.isPerspectiveCamera) { |
|||
domElement.style.WebkitPerspective = fov + "px"; |
|||
domElement.style.perspective = fov + "px"; |
|||
} |
|||
|
|||
cache.camera.fov = fov; |
|||
} |
|||
|
|||
scene.updateMatrixWorld(); |
|||
|
|||
if (camera.parent === null) camera.updateMatrixWorld(); |
|||
|
|||
if (camera.isOrthographicCamera) { |
|||
var tx = -(camera.right + camera.left) / 2; |
|||
var ty = (camera.top + camera.bottom) / 2; |
|||
} |
|||
|
|||
var cameraCSSMatrix = camera.isOrthographicCamera |
|||
? "scale(" + |
|||
fov + |
|||
")" + |
|||
"translate(" + |
|||
epsilon(tx) + |
|||
"px," + |
|||
epsilon(ty) + |
|||
"px)" + |
|||
getCameraCSSMatrix(camera.matrixWorldInverse) |
|||
: "translateZ(" + |
|||
fov + |
|||
"px)" + |
|||
getCameraCSSMatrix(camera.matrixWorldInverse); |
|||
|
|||
var style = |
|||
cameraCSSMatrix + "translate(" + _widthHalf + "px," + _heightHalf + "px)"; |
|||
|
|||
if (cache.camera.style !== style && !isIE) { |
|||
cameraElement.style.WebkitTransform = style; |
|||
cameraElement.style.transform = style; |
|||
|
|||
cache.camera.style = style; |
|||
} |
|||
|
|||
renderObject(scene, camera, cameraCSSMatrix); |
|||
|
|||
if (isIE) { |
|||
// IE10 and 11 does not support 'preserve-3d'.
|
|||
// Thus, z-order in 3D will not work.
|
|||
// We have to calc z-order manually and set CSS z-index for IE.
|
|||
// FYI: z-index can't handle object intersection
|
|||
zOrder(scene); |
|||
} |
|||
}; |
|||
} |
@ -0,0 +1,517 @@ |
|||
import * as THREE from 'three'; |
|||
|
|||
// 创建 TrackballControls 类并导出
|
|||
export class TrackballControls extends THREE.EventDispatcher { |
|||
constructor(object, domElement) { |
|||
super(); // 调用父类构造函数
|
|||
|
|||
var _this = this; |
|||
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; |
|||
|
|||
this.object = object; |
|||
this.domElement = (domElement !== undefined) ? domElement : document; |
|||
|
|||
// API
|
|||
this.enabled = true; |
|||
this.screen = { left: 0, top: 0, width: 0, height: 0 }; |
|||
this.rotateSpeed = 1.0; |
|||
this.zoomSpeed = 1.2; |
|||
this.panSpeed = 0.3; |
|||
this.noRotate = false; |
|||
this.noZoom = false; |
|||
this.noPan = false; |
|||
this.staticMoving = false; |
|||
this.dynamicDampingFactor = 0.2; |
|||
this.minDistance = 0; |
|||
this.maxDistance = Infinity; |
|||
this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/]; |
|||
|
|||
// internals
|
|||
this.target = new THREE.Vector3(); |
|||
|
|||
var EPS = 0.000001; |
|||
|
|||
var lastPosition = new THREE.Vector3(); |
|||
|
|||
var _state = STATE.NONE, |
|||
_prevState = STATE.NONE, |
|||
_eye = new THREE.Vector3(), |
|||
_movePrev = new THREE.Vector2(), |
|||
_moveCurr = new THREE.Vector2(), |
|||
_lastAxis = new THREE.Vector3(), |
|||
_lastAngle = 0, |
|||
_zoomStart = new THREE.Vector2(), |
|||
_zoomEnd = new THREE.Vector2(), |
|||
_touchZoomDistanceStart = 0, |
|||
_touchZoomDistanceEnd = 0, |
|||
_panStart = new THREE.Vector2(), |
|||
_panEnd = new THREE.Vector2(); |
|||
|
|||
// for reset
|
|||
|
|||
this.target0 = this.target.clone(); |
|||
this.position0 = this.object.position.clone(); |
|||
this.up0 = this.object.up.clone(); |
|||
|
|||
// events
|
|||
|
|||
var changeEvent = { type: "change" }; |
|||
var startEvent = { type: "start" }; |
|||
var endEvent = { type: "end" }; |
|||
|
|||
// methods
|
|||
|
|||
this.handleResize = function () { |
|||
if (this.domElement === document) { |
|||
this.screen.left = 0; |
|||
this.screen.top = 0; |
|||
this.screen.width = window.innerWidth; |
|||
this.screen.height = window.innerHeight; |
|||
} else { |
|||
var box = this.domElement.getBoundingClientRect(); |
|||
// adjustments come from similar code in the jquery offset() function
|
|||
var d = this.domElement.ownerDocument.documentElement; |
|||
this.screen.left = box.left + window.pageXOffset - d.clientLeft; |
|||
this.screen.top = box.top + window.pageYOffset - d.clientTop; |
|||
this.screen.width = box.width; |
|||
this.screen.height = box.height; |
|||
} |
|||
}; |
|||
|
|||
var getMouseOnScreen = (function () { |
|||
var vector = new THREE.Vector2(); |
|||
|
|||
return function getMouseOnScreen(pageX, pageY) { |
|||
vector.set( |
|||
(pageX - _this.screen.left) / _this.screen.width, |
|||
(pageY - _this.screen.top) / _this.screen.height |
|||
); |
|||
|
|||
return vector; |
|||
}; |
|||
})(); |
|||
|
|||
var getMouseOnCircle = (function () { |
|||
var vector = new THREE.Vector2(); |
|||
|
|||
return function getMouseOnCircle(pageX, pageY) { |
|||
vector.set( |
|||
(pageX - _this.screen.width * 0.5 - _this.screen.left) / |
|||
(_this.screen.width * 0.5), |
|||
(_this.screen.height + 2 * (_this.screen.top - pageY)) / |
|||
_this.screen.width // screen.width intentional
|
|||
); |
|||
|
|||
return vector; |
|||
}; |
|||
})(); |
|||
|
|||
this.rotateCamera = (function () { |
|||
var axis = new THREE.Vector3(), |
|||
quaternion = new THREE.Quaternion(), |
|||
eyeDirection = new THREE.Vector3(), |
|||
objectUpDirection = new THREE.Vector3(), |
|||
objectSidewaysDirection = new THREE.Vector3(), |
|||
moveDirection = new THREE.Vector3(), |
|||
angle; |
|||
|
|||
return function rotateCamera() { |
|||
moveDirection.set( |
|||
_moveCurr.x - _movePrev.x, |
|||
_moveCurr.y - _movePrev.y, |
|||
0 |
|||
); |
|||
angle = moveDirection.length(); |
|||
|
|||
if (angle) { |
|||
_eye.copy(_this.object.position).sub(_this.target); |
|||
|
|||
eyeDirection.copy(_eye).normalize(); |
|||
objectUpDirection.copy(_this.object.up).normalize(); |
|||
objectSidewaysDirection |
|||
.crossVectors(objectUpDirection, eyeDirection) |
|||
.normalize(); |
|||
|
|||
objectUpDirection.setLength(_moveCurr.y - _movePrev.y); |
|||
objectSidewaysDirection.setLength(_moveCurr.x - _movePrev.x); |
|||
|
|||
moveDirection.copy(objectUpDirection.add(objectSidewaysDirection)); |
|||
|
|||
axis.crossVectors(moveDirection, _eye).normalize(); |
|||
|
|||
angle *= _this.rotateSpeed; |
|||
quaternion.setFromAxisAngle(axis, angle); |
|||
|
|||
_eye.applyQuaternion(quaternion); |
|||
_this.object.up.applyQuaternion(quaternion); |
|||
|
|||
_lastAxis.copy(axis); |
|||
_lastAngle = angle; |
|||
} else if (!_this.staticMoving && _lastAngle) { |
|||
_lastAngle *= Math.sqrt(1.0 - _this.dynamicDampingFactor); |
|||
_eye.copy(_this.object.position).sub(_this.target); |
|||
quaternion.setFromAxisAngle(_lastAxis, _lastAngle); |
|||
_eye.applyQuaternion(quaternion); |
|||
_this.object.up.applyQuaternion(quaternion); |
|||
} |
|||
|
|||
_movePrev.copy(_moveCurr); |
|||
}; |
|||
})(); |
|||
|
|||
this.zoomCamera = function () { |
|||
var factor; |
|||
|
|||
if (_state === STATE.TOUCH_ZOOM_PAN) { |
|||
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; |
|||
_touchZoomDistanceStart = _touchZoomDistanceEnd; |
|||
_eye.multiplyScalar(factor); |
|||
} else { |
|||
factor = 1.0 + (_zoomEnd.y - _zoomStart.y) * _this.zoomSpeed; |
|||
|
|||
if (factor !== 1.0 && factor > 0.0) { |
|||
_eye.multiplyScalar(factor); |
|||
} |
|||
|
|||
if (_this.staticMoving) { |
|||
_zoomStart.copy(_zoomEnd); |
|||
} else { |
|||
_zoomStart.y += |
|||
(_zoomEnd.y - _zoomStart.y) * this.dynamicDampingFactor; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
this.panCamera = (function () { |
|||
var mouseChange = new THREE.Vector2(), |
|||
objectUp = new THREE.Vector3(), |
|||
pan = new THREE.Vector3(); |
|||
|
|||
return function panCamera() { |
|||
mouseChange.copy(_panEnd).sub(_panStart); |
|||
|
|||
if (mouseChange.lengthSq()) { |
|||
mouseChange.multiplyScalar(_eye.length() * _this.panSpeed); |
|||
|
|||
pan.copy(_eye).cross(_this.object.up).setLength(mouseChange.x); |
|||
pan.add(objectUp.copy(_this.object.up).setLength(mouseChange.y)); |
|||
|
|||
_this.object.position.add(pan); |
|||
_this.target.add(pan); |
|||
|
|||
if (_this.staticMoving) { |
|||
_panStart.copy(_panEnd); |
|||
} else { |
|||
_panStart.add( |
|||
mouseChange |
|||
.subVectors(_panEnd, _panStart) |
|||
.multiplyScalar(_this.dynamicDampingFactor) |
|||
); |
|||
} |
|||
} |
|||
}; |
|||
})(); |
|||
|
|||
this.checkDistances = function () { |
|||
if (!_this.noZoom || !_this.noPan) { |
|||
if (_eye.lengthSq() > _this.maxDistance * _this.maxDistance) { |
|||
_this.object.position.addVectors( |
|||
_this.target, |
|||
_eye.setLength(_this.maxDistance) |
|||
); |
|||
_zoomStart.copy(_zoomEnd); |
|||
} |
|||
|
|||
if (_eye.lengthSq() < _this.minDistance * _this.minDistance) { |
|||
_this.object.position.addVectors( |
|||
_this.target, |
|||
_eye.setLength(_this.minDistance) |
|||
); |
|||
_zoomStart.copy(_zoomEnd); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
this.update = function () { |
|||
_eye.subVectors(_this.object.position, _this.target); |
|||
|
|||
if (!_this.noRotate) { |
|||
_this.rotateCamera(); |
|||
} |
|||
|
|||
if (!_this.noZoom) { |
|||
_this.zoomCamera(); |
|||
} |
|||
|
|||
if (!_this.noPan) { |
|||
_this.panCamera(); |
|||
} |
|||
|
|||
_this.object.position.addVectors(_this.target, _eye); |
|||
|
|||
_this.checkDistances(); |
|||
|
|||
_this.object.lookAt(_this.target); |
|||
|
|||
if (lastPosition.distanceToSquared(_this.object.position) > EPS) { |
|||
_this.dispatchEvent(changeEvent); |
|||
|
|||
lastPosition.copy(_this.object.position); |
|||
} |
|||
}; |
|||
|
|||
this.reset = function () { |
|||
_state = STATE.NONE; |
|||
_prevState = STATE.NONE; |
|||
|
|||
_this.target.copy(_this.target0); |
|||
_this.object.position.copy(_this.position0); |
|||
_this.object.up.copy(_this.up0); |
|||
|
|||
_eye.subVectors(_this.object.position, _this.target); |
|||
|
|||
_this.object.lookAt(_this.target); |
|||
|
|||
_this.dispatchEvent(changeEvent); |
|||
|
|||
lastPosition.copy(_this.object.position); |
|||
}; |
|||
|
|||
// listeners
|
|||
|
|||
function keydown(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
window.removeEventListener("keydown", keydown); |
|||
|
|||
_prevState = _state; |
|||
|
|||
if (_state !== STATE.NONE) { |
|||
return; |
|||
} else if ( |
|||
event.keyCode === _this.keys[STATE.ROTATE] && |
|||
!_this.noRotate |
|||
) { |
|||
_state = STATE.ROTATE; |
|||
} else if (event.keyCode === _this.keys[STATE.ZOOM] && !_this.noZoom) { |
|||
_state = STATE.ZOOM; |
|||
} else if (event.keyCode === _this.keys[STATE.PAN] && !_this.noPan) { |
|||
_state = STATE.PAN; |
|||
} |
|||
} |
|||
|
|||
function keyup(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
_state = _prevState; |
|||
|
|||
window.addEventListener("keydown", keydown, false); |
|||
} |
|||
|
|||
function mousedown(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
|
|||
if (_state === STATE.NONE) { |
|||
_state = event.button; |
|||
} |
|||
|
|||
// 阻止浏览器的默认行为
|
|||
return; |
|||
|
|||
if (_state === STATE.ROTATE && !_this.noRotate) { |
|||
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); |
|||
_movePrev.copy(_moveCurr); |
|||
} else if (_state === STATE.ZOOM && !_this.noZoom) { |
|||
_zoomStart.copy(getMouseOnScreen(event.pageX, event.pageY)); |
|||
_zoomEnd.copy(_zoomStart); |
|||
} else if (_state === STATE.PAN && !_this.noPan) { |
|||
_panStart.copy(getMouseOnScreen(event.pageX, event.pageY)); |
|||
_panEnd.copy(_panStart); |
|||
} |
|||
|
|||
document.addEventListener("mousemove", mousemove, false); |
|||
document.addEventListener("mouseup", mouseup, false); |
|||
|
|||
_this.dispatchEvent(startEvent); |
|||
} |
|||
|
|||
function mousemove(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
|
|||
if (_state === STATE.ROTATE && !_this.noRotate) { |
|||
_movePrev.copy(_moveCurr); |
|||
_moveCurr.copy(getMouseOnCircle(event.pageX, event.pageY)); |
|||
} else if (_state === STATE.ZOOM && !_this.noZoom) { |
|||
_zoomEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); |
|||
} else if (_state === STATE.PAN && !_this.noPan) { |
|||
_panEnd.copy(getMouseOnScreen(event.pageX, event.pageY)); |
|||
} |
|||
} |
|||
|
|||
function mouseup(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
|
|||
_state = STATE.NONE; |
|||
|
|||
document.removeEventListener("mousemove", mousemove); |
|||
document.removeEventListener("mouseup", mouseup); |
|||
_this.dispatchEvent(endEvent); |
|||
} |
|||
|
|||
function mousewheel(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
if (_this.noZoom === true) return; |
|||
|
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
|
|||
// return;
|
|||
|
|||
switch (event.deltaMode) { |
|||
case 2: |
|||
// Zoom in pages
|
|||
_zoomStart.y -= event.deltaY * 0.025; |
|||
break; |
|||
|
|||
case 1: |
|||
// Zoom in lines
|
|||
_zoomStart.y -= event.deltaY * 0.01; |
|||
break; |
|||
|
|||
default: |
|||
// undefined, 0, assume pixels
|
|||
_zoomStart.y -= event.deltaY * 0.00025; |
|||
break; |
|||
} |
|||
|
|||
_this.dispatchEvent(startEvent); |
|||
_this.dispatchEvent(endEvent); |
|||
} |
|||
|
|||
function touchstart(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
event.preventDefault(); |
|||
|
|||
switch (event.touches.length) { |
|||
case 1: |
|||
_state = STATE.TOUCH_ROTATE; |
|||
_moveCurr.copy( |
|||
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY) |
|||
); |
|||
_movePrev.copy(_moveCurr); |
|||
break; |
|||
|
|||
default: // 2 or more
|
|||
_state = STATE.TOUCH_ZOOM_PAN; |
|||
var dx = event.touches[0].pageX - event.touches[1].pageX; |
|||
var dy = event.touches[0].pageY - event.touches[1].pageY; |
|||
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( |
|||
dx * dx + dy * dy |
|||
); |
|||
|
|||
var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; |
|||
var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; |
|||
_panStart.copy(getMouseOnScreen(x, y)); |
|||
_panEnd.copy(_panStart); |
|||
break; |
|||
} |
|||
|
|||
_this.dispatchEvent(startEvent); |
|||
} |
|||
|
|||
function touchmove(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
|
|||
switch (event.touches.length) { |
|||
case 1: |
|||
_movePrev.copy(_moveCurr); |
|||
_moveCurr.copy( |
|||
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY) |
|||
); |
|||
break; |
|||
|
|||
default: // 2 or more
|
|||
var dx = event.touches[0].pageX - event.touches[1].pageX; |
|||
var dy = event.touches[0].pageY - event.touches[1].pageY; |
|||
_touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy); |
|||
|
|||
var x = (event.touches[0].pageX + event.touches[1].pageX) / 2; |
|||
var y = (event.touches[0].pageY + event.touches[1].pageY) / 2; |
|||
_panEnd.copy(getMouseOnScreen(x, y)); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
function touchend(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
switch (event.touches.length) { |
|||
case 0: |
|||
_state = STATE.NONE; |
|||
break; |
|||
|
|||
case 1: |
|||
_state = STATE.TOUCH_ROTATE; |
|||
_moveCurr.copy( |
|||
getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY) |
|||
); |
|||
_movePrev.copy(_moveCurr); |
|||
break; |
|||
} |
|||
|
|||
_this.dispatchEvent(endEvent); |
|||
} |
|||
|
|||
function contextmenu(event) { |
|||
if (_this.enabled === false) return; |
|||
|
|||
event.preventDefault(); |
|||
} |
|||
|
|||
this.dispose = function () { |
|||
this.domElement.removeEventListener("contextmenu", contextmenu, false); |
|||
this.domElement.removeEventListener("mousedown", mousedown, false); |
|||
// this.domElement.removeEventListener("wheel", mousewheel, false);
|
|||
|
|||
this.domElement.removeEventListener("touchstart", touchstart, false); |
|||
this.domElement.removeEventListener("touchend", touchend, false); |
|||
this.domElement.removeEventListener("touchmove", touchmove, false); |
|||
|
|||
document.removeEventListener("mousemove", mousemove, false); |
|||
document.removeEventListener("mouseup", mouseup, false); |
|||
|
|||
window.removeEventListener("keydown", keydown, false); |
|||
window.removeEventListener("keyup", keyup, false); |
|||
}; |
|||
|
|||
this.domElement.addEventListener("contextmenu", contextmenu, false); |
|||
this.domElement.addEventListener("mousedown", mousedown, false); |
|||
// this.domElement.addEventListener("wheel", mousewheel, false);
|
|||
|
|||
this.domElement.addEventListener("touchstart", touchstart, false); |
|||
this.domElement.addEventListener("touchend", touchend, false); |
|||
this.domElement.addEventListener("touchmove", touchmove, false); |
|||
|
|||
window.addEventListener("keydown", keydown, false); |
|||
window.addEventListener("keyup", keyup, false); |
|||
|
|||
this.handleResize(); |
|||
|
|||
// force an update at start
|
|||
this.update(); |
|||
} |
|||
} |
@ -0,0 +1,55 @@ |
|||
const NUMBER_MATRIX = [ |
|||
[ |
|||
//H
|
|||
[0, 0], |
|||
[3, 0], |
|||
[0, 1], |
|||
[3, 1], |
|||
[0, 2], |
|||
[1, 2], |
|||
[2, 2], |
|||
[3, 2], |
|||
[0, 3], |
|||
[3, 3], |
|||
[0, 4], |
|||
[3, 4], |
|||
], |
|||
[ |
|||
//L
|
|||
[0, 0], |
|||
[0, 1], |
|||
[0, 2], |
|||
[0, 3], |
|||
[0, 4], |
|||
[1, 4], |
|||
[2, 4], |
|||
[3, 4], |
|||
], |
|||
[ |
|||
//2
|
|||
[0, 0], |
|||
[1, 0], |
|||
[2, 0], |
|||
[3, 0], |
|||
[3, 1], |
|||
[2, 2], |
|||
[1, 3], |
|||
[0, 4], |
|||
[1, 4], |
|||
[2, 4], |
|||
[3, 4], |
|||
], |
|||
[ |
|||
//7
|
|||
[0, 0], |
|||
[1, 0], |
|||
[2, 0], |
|||
[3, 0], |
|||
[3, 1], |
|||
[2, 2], |
|||
[1, 3], |
|||
[1, 4], |
|||
], |
|||
]; |
|||
|
|||
export { NUMBER_MATRIX }; |
@ -0,0 +1,260 @@ |
|||
import * as TWEEN from "./tween.min.js"; |
|||
|
|||
const MAX_TOP = 300; |
|||
const MAX_WIDTH = document.body.clientWidth; |
|||
|
|||
let defaultType = 0; |
|||
let prizes; |
|||
let lastDanMuList = []; |
|||
let prizeElement = {}; |
|||
let lasetPrizeIndex = 0; |
|||
|
|||
const DEFAULT_MESS = [ |
|||
"我是该抽中一等奖还是一等奖呢,纠结ing...", |
|||
"听说要提前一个月吃素才能中大奖喔!", |
|||
"好想要一等奖啊!!!", |
|||
"一等奖有没有人想要呢?", |
|||
"五等奖也不错,只要自己能中奖就行", |
|||
"祝大家新年快乐!", |
|||
"中不中奖不重要,大家吃好喝好。", |
|||
"新年,祝福大家事事顺遂。", |
|||
"作为专业陪跑的我,我就看看你们有谁跟我一样", |
|||
"新的一年祝福大家越来越好!", |
|||
"来年再战!!!", |
|||
]; |
|||
|
|||
class DanMu { |
|||
constructor(option) { |
|||
if (typeof option !== "object") { |
|||
option = { |
|||
text: option, |
|||
}; |
|||
} |
|||
|
|||
this.position = {}; |
|||
this.text = option.text; |
|||
this.onComplete = option.onComplete; |
|||
|
|||
this.init(); |
|||
} |
|||
|
|||
init() { |
|||
this.element = document.createElement("div"); |
|||
this.element.className = "dan-mu"; |
|||
document.body.appendChild(this.element); |
|||
|
|||
this.start(); |
|||
} |
|||
|
|||
setText(text) { |
|||
this.text = text || this.text; |
|||
this.element.textContent = this.text; |
|||
this.width = this.element.clientWidth + 100; |
|||
} |
|||
|
|||
start(text) { |
|||
let speed = ~~(Math.random() * 10000) + 6000; |
|||
this.position = { |
|||
x: MAX_WIDTH, |
|||
}; |
|||
let delay = speed / 10; |
|||
|
|||
this.setText(text); |
|||
this.element.style.transform = "translateX(" + this.position.x + "px)"; |
|||
this.element.style.top = ~~(Math.random() * MAX_TOP) + 10 + "px"; |
|||
this.element.classList.add("active"); |
|||
this.tween = new TWEEN.Tween(this.position) |
|||
.to( |
|||
{ |
|||
x: -this.width, |
|||
}, |
|||
speed |
|||
) |
|||
.onUpdate(() => { |
|||
this.render(); |
|||
}) |
|||
.onComplete(() => { |
|||
this.onComplete && this.onComplete(); |
|||
}) |
|||
.start(); |
|||
} |
|||
|
|||
render() { |
|||
this.element.style.transform = "translateX(" + this.position.x + "px)"; |
|||
} |
|||
} |
|||
|
|||
class Qipao { |
|||
constructor(option) { |
|||
if (typeof option !== "object") { |
|||
option = { |
|||
text: option, |
|||
}; |
|||
} |
|||
|
|||
this.text = option.text; |
|||
this.onComplete = option.onComplete; |
|||
this.$par = document.querySelector(".qipao-container"); |
|||
if (!this.$par) { |
|||
this.$par = document.createElement("div"); |
|||
this.$par.className = "qipao-container"; |
|||
document.body.appendChild(this.$par); |
|||
} |
|||
|
|||
this.init(); |
|||
} |
|||
|
|||
init() { |
|||
this.element = document.createElement("div"); |
|||
this.element.className = "qipao animated"; |
|||
this.$par.appendChild(this.element); |
|||
|
|||
this.start(); |
|||
} |
|||
|
|||
setText(text) { |
|||
this.text = text || this.text; |
|||
this.element.textContent = this.text; |
|||
} |
|||
|
|||
start(text) { |
|||
this.setText(text); |
|||
this.element.classList.remove("bounceOutRight"); |
|||
this.element.classList.add("bounceInRight"); |
|||
|
|||
setTimeout(() => { |
|||
this.element.classList.remove("bounceInRight"); |
|||
this.element.classList.add("bounceOutRight"); |
|||
this.onComplete && this.onComplete(); |
|||
}, 4000); |
|||
} |
|||
} |
|||
function setPrizes(pri) { |
|||
prizes = pri; |
|||
defaultType = prizes[0]["type"]; |
|||
lasetPrizeIndex = pri.length - 1; |
|||
} |
|||
|
|||
function showPrizeList(currentPrizeIndex) { |
|||
let currentPrize = prizes[currentPrizeIndex]; |
|||
if (currentPrize.type === defaultType) { |
|||
currentPrize.count = "不限制"; |
|||
} |
|||
} |
|||
|
|||
function resetPrize(currentPrizeIndex) { |
|||
prizeElement = {}; |
|||
lasetPrizeIndex = currentPrizeIndex; |
|||
showPrizeList(currentPrizeIndex); |
|||
} |
|||
|
|||
let setPrizeData = (function () { |
|||
return function (currentPrizeIndex, count, isInit) { |
|||
let currentPrize = prizes[currentPrizeIndex], |
|||
type = currentPrize.type, |
|||
elements = prizeElement[type], |
|||
totalCount = currentPrize.count; |
|||
|
|||
if (!elements) { |
|||
elements = { |
|||
box: document.querySelector(`#prize-item-${type}`), |
|||
bar: document.querySelector(`#prize-bar-${type}`), |
|||
text: document.querySelector(`#prize-count-${type}`), |
|||
}; |
|||
prizeElement[type] = elements; |
|||
} |
|||
|
|||
if (!prizeElement.prizeType) { |
|||
prizeElement.prizeType = document.querySelector("#prizeType"); |
|||
prizeElement.prizeLeft = document.querySelector("#prizeLeft"); |
|||
prizeElement.prizeText = document.querySelector("#prizeText"); |
|||
} |
|||
|
|||
if (isInit) { |
|||
for (let i = prizes.length - 1; i > currentPrizeIndex; i--) { |
|||
let type = prizes[i]["type"]; |
|||
document.querySelector(`#prize-item-${type}`).className = |
|||
"prize-item done"; |
|||
document.querySelector(`#prize-bar-${type}`).style.width = "0"; |
|||
document.querySelector(`#prize-count-${type}`).textContent = |
|||
"0" + "/" + prizes[i]["count"]; |
|||
} |
|||
} |
|||
|
|||
if (lasetPrizeIndex !== currentPrizeIndex) { |
|||
let lastPrize = prizes[lasetPrizeIndex], |
|||
lastBox = document.querySelector(`#prize-item-${lastPrize.type}`); |
|||
lastBox.classList.remove("shine"); |
|||
lastBox.classList.add("done"); |
|||
elements.box && elements.box.classList.add("shine"); |
|||
prizeElement.prizeType.textContent = currentPrize.text; |
|||
prizeElement.prizeText.textContent = currentPrize.title; |
|||
|
|||
lasetPrizeIndex = currentPrizeIndex; |
|||
} |
|||
|
|||
if (currentPrizeIndex === 0) { |
|||
prizeElement.prizeType.textContent = "特别奖"; |
|||
prizeElement.prizeText.textContent = " "; |
|||
prizeElement.prizeLeft.textContent = "不限制"; |
|||
return; |
|||
} |
|||
|
|||
count = totalCount - count; |
|||
count = count < 0 ? 0 : count; |
|||
let percent = (count / totalCount).toFixed(2); |
|||
elements.bar && (elements.bar.style.width = percent * 100 + "%"); |
|||
elements.text && (elements.text.textContent = count + "/" + totalCount); |
|||
prizeElement.prizeLeft.textContent = count; |
|||
}; |
|||
})(); |
|||
|
|||
function startMaoPao() { |
|||
let len = DEFAULT_MESS.length, |
|||
count = 5, |
|||
index = ~~(Math.random() * len), |
|||
danmuList = [], |
|||
total = 0; |
|||
|
|||
function restart() { |
|||
total = 0; |
|||
danmuList.forEach((item) => { |
|||
let text = |
|||
lastDanMuList.length > 0 |
|||
? lastDanMuList.shift() |
|||
: DEFAULT_MESS[index++]; |
|||
item.start(text); |
|||
index = index > len ? 0 : index; |
|||
}); |
|||
} |
|||
|
|||
for (let i = 0; i < count; i++) { |
|||
setTimeout(() => { |
|||
danmuList.push( |
|||
new DanMu({ |
|||
text: DEFAULT_MESS[index++], |
|||
onComplete: function () { |
|||
setTimeout(() => { |
|||
this.start(DEFAULT_MESS[index++]); |
|||
index = index > len ? 0 : index; |
|||
}, 1000); |
|||
}, |
|||
}) |
|||
); |
|||
index = index > len ? 0 : index; |
|||
}, 1500 * i); |
|||
} |
|||
} |
|||
|
|||
function addDanMu(text) { |
|||
lastDanMuList.push(text); |
|||
} |
|||
|
|||
export { |
|||
startMaoPao, |
|||
showPrizeList, |
|||
setPrizeData, |
|||
addDanMu, |
|||
setPrizes, |
|||
resetPrize, |
|||
}; |
957
src/utils/three.min.js
File diff suppressed because it is too large
View File
@ -0,0 +1 @@ |
|||
"use strict";var TWEEN=TWEEN||(function(){var b=[];return{REVISION:"7",getAll:function(){return b},removeAll:function(){b=[]},add:function(a){b.push(a)},remove:function(a){a=b.indexOf(a);-1!==a&&b.splice(a,1)},update:function(f){if(0===b.length){return !1}for(var a=0,e=b.length,f=void 0!==f?f:Date.now();a<e;){b[a].update(f)?a++:(b.splice(a,1),e--)}return !0}}})();TWEEN.Tween=function(A){var y={},z={},x=1000,w=0,v=null,u=TWEEN.Easing.Linear.None,g=TWEEN.Interpolation.Linear,t=[],q=null,o=!1,j=null,B=null,i=null;this.to=function(b,d){null!==d&&(x=d);z=b;return this};this.start=function(b){TWEEN.add(this);o=!1;v=void 0!==b?b:Date.now();v+=w;for(var a in z){if(null!==A[a]){if(z[a] instanceof Array){if(0===z[a].length){continue}z[a]=[A[a]].concat(z[a])}y[a]=A[a]}}return this};this.stop=function(){TWEEN.remove(this);B&&B.call(this);return this};this.delay=function(b){w=b;return this};this.easing=function(b){u=b;return this};this.interpolation=function(b){g=b;return this};this.chain=function(){t=arguments;return this};this.onStart=function(b){q=b;return this};this.onStop=function(b){B=b;return this};this.onUpdate=function(b){j=b;return this};this.onComplete=function(b){i=b;return this};this.update=function(h){if(h<v){return !0}!1===o&&(null!==q&&q.call(A),(o=!0));var d=(h-v)/x,d=1<d?1:d,b=u(d),a;for(a in y){var c=y[a],f=z[a];A[a]=f instanceof Array?g(f,b):c+(f-c)*b}null!==j&&j.call(A,b);if(1==d){null!==i&&i.call(A);d=0;for(b=t.length;d<b;d++){t[d].start(h)}return !1}return !0}};TWEEN.Easing={Linear:{None:function(b){return b}},Quadratic:{In:function(b){return b*b},Out:function(b){return b*(2-b)},InOut:function(b){return 1>(b*=2)?0.5*b*b:-0.5*(--b*(b-2)-1)}},Cubic:{In:function(b){return b*b*b},Out:function(b){return --b*b*b+1},InOut:function(b){return 1>(b*=2)?0.5*b*b*b:0.5*((b-=2)*b*b+2)}},Quartic:{In:function(b){return b*b*b*b},Out:function(b){return 1- --b*b*b*b},InOut:function(b){return 1>(b*=2)?0.5*b*b*b*b:-0.5*((b-=2)*b*b*b-2)}},Quintic:{In:function(b){return b*b*b*b*b},Out:function(b){return --b*b*b*b*b+1},InOut:function(b){return 1>(b*=2)?0.5*b*b*b*b*b:0.5*((b-=2)*b*b*b*b+2)}},Sinusoidal:{In:function(b){return 1-Math.cos((b*Math.PI)/2)},Out:function(b){return Math.sin((b*Math.PI)/2)},InOut:function(b){return 0.5*(1-Math.cos(Math.PI*b))}},Exponential:{In:function(b){return 0===b?0:Math.pow(1024,b-1)},Out:function(b){return 1===b?1:1-Math.pow(2,-10*b)},InOut:function(b){return 0===b?0:1===b?1:1>(b*=2)?0.5*Math.pow(1024,b-1):0.5*(-Math.pow(2,-10*(b-1))+2)}},Circular:{In:function(b){return 1-Math.sqrt(1-b*b)},Out:function(b){return Math.sqrt(1- --b*b)},InOut:function(b){return 1>(b*=2)?-0.5*(Math.sqrt(1-b*b)-1):0.5*(Math.sqrt(1-(b-=2)*b)+1)}},Elastic:{In:function(e){var f,d=0.1;if(0===e){return 0}if(1===e){return 1}!d||1>d?((d=1),(f=0.1)):(f=(0.4*Math.asin(1/d))/(2*Math.PI));return -(d*Math.pow(2,10*(e-=1))*Math.sin(((e-f)*2*Math.PI)/0.4))},Out:function(e){var f,d=0.1;if(0===e){return 0}if(1===e){return 1}!d||1>d?((d=1),(f=0.1)):(f=(0.4*Math.asin(1/d))/(2*Math.PI));return(d*Math.pow(2,-10*e)*Math.sin(((e-f)*2*Math.PI)/0.4)+1)},InOut:function(e){var f,d=0.1;if(0===e){return 0}if(1===e){return 1}!d||1>d?((d=1),(f=0.1)):(f=(0.4*Math.asin(1/d))/(2*Math.PI));return 1>(e*=2)?-0.5*d*Math.pow(2,10*(e-=1))*Math.sin(((e-f)*2*Math.PI)/0.4):0.5*d*Math.pow(2,-10*(e-=1))*Math.sin(((e-f)*2*Math.PI)/0.4)+1}},Back:{In:function(b){return b*b*(2.70158*b-1.70158)},Out:function(b){return --b*b*(2.70158*b+1.70158)+1},InOut:function(b){return 1>(b*=2)?0.5*b*b*(3.5949095*b-2.5949095):0.5*((b-=2)*b*(3.5949095*b+2.5949095)+2)}},Bounce:{In:function(b){return 1-TWEEN.Easing.Bounce.Out(1-b)},Out:function(b){return b<1/2.75?7.5625*b*b:b<2/2.75?7.5625*(b-=1.5/2.75)*b+0.75:b<2.5/2.75?7.5625*(b-=2.25/2.75)*b+0.9375:7.5625*(b-=2.625/2.75)*b+0.984375},InOut:function(b){return 0.5>b?0.5*TWEEN.Easing.Bounce.In(2*b):0.5*TWEEN.Easing.Bounce.Out(2*b-1)+0.5}}};TWEEN.Interpolation={Linear:function(h,l){var g=h.length-1,k=g*l,j=Math.floor(k),i=TWEEN.Interpolation.Utils.Linear;return 0>l?i(h[0],h[1],k):1<l?i(h[g],h[g-1],g-k):i(h[j],h[j+1>g?g:j+1],k-j)},Bezier:function(i,n){var g=0,m=i.length-1,l=Math.pow,k=TWEEN.Interpolation.Utils.Bernstein,j;for(j=0;j<=m;j++){g+=l(1-n,m-j)*l(n,j)*i[j]*k(m,j)}return g},CatmullRom:function(h,l){var g=h.length-1,k=g*l,j=Math.floor(k),i=TWEEN.Interpolation.Utils.CatmullRom;return h[0]===h[g]?(0>l&&(j=Math.floor((k=g*(1+l)))),i(h[(j-1+g)%g],h[j],h[(j+1)%g],h[(j+2)%g],k-j)):0>l?h[0]-(i(h[0],h[0],h[1],h[1],-k)-h[0]):1<l?h[g]-(i(h[g],h[g],h[g-1],h[g-1],k-g)-h[g]):i(h[j?j-1:0],h[j],h[g<j+1?g:j+1],h[g<j+2?g:j+2],k-j)},Utils:{Linear:function(e,f,d){return(f-e)*d+e},Bernstein:function(e,f){var d=TWEEN.Interpolation.Utils.Factorial;return d(e)/d(f)/d(e-f)},Factorial:(function(){var b=[1];return function(f){var a=1,e;if(b[f]){return b[f]}for(e=f;1<e;e--){a*=e}return(b[f]=a)}})(),CatmullRom:function(h,l,g,k,j){var h=0.5*(g-h),k=0.5*(k-l),i=j*j;return((2*l-2*g+h+k)*j*i+(-3*l+3*g-2*h-k)*i+h*j+l)}}}; |
@ -0,0 +1,101 @@ |
|||
<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) { |
|||
// 添加登录状态存储 |
|||
authStore.login(); // 使用Pinia登录方法 |
|||
router.push('/choujiang'); |
|||
} else { |
|||
alert('密码错误,请重试'); |
|||
} |
|||
}; |
|||
// 登录逻辑处理 |
|||
console.log('登录信息:', { |
|||
password: password.value, |
|||
}); |
|||
// 这里可以添加实际的登录API调用 |
|||
</script> |
|||
<style scoped> |
|||
.login-container { |
|||
background-image: url('../../assets/登录背景.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: 55%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
width: 450px; /* 增加卡片宽度 */ |
|||
height: 285px; |
|||
padding: 2.5rem; /* 增加内边距 */ |
|||
background: rgba(255, 255, 255, 0.3); /* 调整背景颜色为半透明白色 */ |
|||
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: 1.2rem; |
|||
border: 1px solid #ddd; |
|||
border-radius: 4px; |
|||
font-size: 1.3rem; /* 增大字体大小 */ |
|||
margin-bottom: 2.5rem; /* 增加底部间距 */ |
|||
box-sizing: border-box |
|||
} |
|||
|
|||
.login-button { |
|||
width: 100%; |
|||
padding: 1.2rem; /* 增加按钮内边距 */ |
|||
background-color: #e92821a3; |
|||
color: white; |
|||
border: none; |
|||
border-radius: 4px; |
|||
font-size: 1.3rem; /* 增大字体大小 */ |
|||
cursor: pointer; |
|||
transition: background-color 0.3s; |
|||
box-sizing: border-box |
|||
} |
|||
|
|||
.login-button:hover { |
|||
transform: scale(1.03); |
|||
} |
|||
</style> |
2153
src/views/choujiang/hxl-cj/cj.vue
File diff suppressed because it is too large
View File
@ -1,13 +1,356 @@ |
|||
<template> |
|||
<div> |
|||
抽奖 |
|||
<div class="choujiang-main"> |
|||
<Lottery3D ref="lottery3DRef" /> |
|||
<PrizePanel :prizes="dataManager.state.basicData.prizes" /> |
|||
<ControlBar |
|||
:lottery-state="lotteryState" |
|||
:is-disabled="isDisabled" |
|||
@lottery-click="handleLotteryClick" |
|||
@reset="handleReset" |
|||
@export="handleExport" |
|||
/> |
|||
<MusicPlayer ref="musicPlayerRef" /> |
|||
<Mascot /> |
|||
|
|||
<!-- 透明弹窗 --> |
|||
<div v-if="showPrizeExhaustedModal" class="prize-exhausted-modal"> |
|||
<div class="modal-content"> |
|||
<p class="modal-text">该礼品已抽取完毕,请揭秘下一个礼品</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-if="showPrizeExhaustedModal1" class="prize-exhausted-modal"> |
|||
<div class="modal-content"> |
|||
<p class="modal-text">请先揭秘一个礼品</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- <UserList |
|||
:lucky-users=" |
|||
dataManager.state.basicData.luckyUsers[ |
|||
dataManager.state.currentPrize?.type |
|||
] || [] |
|||
" |
|||
:left-users="dataManager.state.basicData.leftUsers" |
|||
/> --> |
|||
<!-- <Qipao :text="qipaoText" :show="showQipao" /> --> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import Lottery3D from "./lottery/Lottery3D.vue"; |
|||
import PrizePanel from "./lottery/PrizePanel.vue"; |
|||
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"; |
|||
|
|||
import { useLotteryStore } from "../../store/lottery"; // 路径根据实际情况调整 |
|||
import { drawLottery } from "../../api/API"; |
|||
|
|||
const qipaoText = ref(""); |
|||
const showQipao = ref(false); |
|||
const showPrizeExhaustedModal = ref(false); |
|||
const showPrizeExhaustedModal1 = ref(false); |
|||
|
|||
// const lotteryState = ref('idle'); // idle, ready, rotating, result |
|||
|
|||
// 新增 |
|||
const lotteryStore = useLotteryStore(); |
|||
const lotteryState = computed({ |
|||
get: () => lotteryStore.lotteryState, |
|||
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 winnerList = computed({ |
|||
get: () => lotteryStore.winnerList, |
|||
set: (val) => lotteryStore.setWinnerList(val), |
|||
}); |
|||
|
|||
const isDisabled = ref(false); |
|||
|
|||
watch(isDisabled, (newVal, oldVal) => { |
|||
console.log("isDisabled 变化:", oldVal, "->", newVal); |
|||
}); |
|||
|
|||
// 数据与抽奖主流程 |
|||
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), |
|||
switchScreen: (...args) => lottery3DRef.value?.switchScreen?.(...args), |
|||
rotateBallStart: (...args) => lottery3DRef.value?.rotateBallStart?.(...args), |
|||
rotateBallStop: (...args) => lottery3DRef.value?.rotateBallStop?.(...args), |
|||
selectCard: (...args) => lottery3DRef.value?.selectCard?.(...args), |
|||
}); |
|||
|
|||
onMounted(async () => { |
|||
isDisabled.value = true; |
|||
setTimeout(() => { |
|||
isDisabled.value = false; |
|||
}, 4000); |
|||
|
|||
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; |
|||
// 适配新的数据格式,支持 jwcode 和 username |
|||
const names = luckys |
|||
.map((item) => item.username || item[1] || item.jwcode || "") |
|||
.join("、"); |
|||
qipaoText.value = `恭喜${names}获得${prize?.title || ""}!`; |
|||
showQipao.value = true; |
|||
setTimeout(() => { |
|||
showQipao.value = false; |
|||
}, 3000); |
|||
} |
|||
|
|||
async function handleLotteryClick() { |
|||
if (isDisabled.value) return; // 2秒内不能重复点击 |
|||
isDisabled.value = true; |
|||
// setTimeout(() => { |
|||
// isDisabled.value = false; |
|||
// }, 2000); |
|||
|
|||
switch (lotteryState.value) { |
|||
case "idle": |
|||
setTimeout(() => { |
|||
isDisabled.value = false; |
|||
}, 2000); |
|||
// 先切换到球体布局 |
|||
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); |
|||
|
|||
break; |
|||
case "ready": |
|||
if (waitingForNextReveal.value) { |
|||
console.log("waitingForNextReveal.value", waitingForNextReveal.value); |
|||
// 显示弹窗提示 |
|||
showPrizeExhaustedModal.value = true; |
|||
setTimeout(() => { |
|||
showPrizeExhaustedModal.value = false; |
|||
}, 1000); |
|||
isDisabled.value = false; |
|||
|
|||
break; |
|||
} |
|||
|
|||
if (lastRevealed.value === -1) { |
|||
console.log("lastRevealed.value", lastRevealed.value); |
|||
showPrizeExhaustedModal1.value = true; |
|||
setTimeout(() => { |
|||
showPrizeExhaustedModal1.value = false; |
|||
}, 1000); |
|||
isDisabled.value = false; |
|||
|
|||
break; |
|||
} |
|||
|
|||
console.log("lotteryState 变更前:", lotteryState.value, "-> rotating"); |
|||
lotteryState.value = "rotating"; |
|||
console.log("lotteryState 变更后:", lotteryState.value); |
|||
|
|||
const prize = dataManager.state.basicData.prizes[lastRevealed.value]; |
|||
console.log("准备调用 drawLottery,prize:", prize); |
|||
console.log("lastRevealed.value:", lastRevealed.value); |
|||
// 先让球转起来,不等它结束 |
|||
const rotatePromise = lottery3DRef.value?.rotateBallStart?.(); |
|||
|
|||
// 同时请求接口 |
|||
try { |
|||
winnerList.value = await drawLottery({ |
|||
perWin: prize.perWin, |
|||
remainNum: prize.remainNum, |
|||
gradeId: prize.gradeId, |
|||
prizeId: prize.prizeId, |
|||
}); |
|||
setTimeout(() => { |
|||
isDisabled.value = false; |
|||
}, 2000); |
|||
|
|||
console.log("drawLottery 调用成功,结果:", winnerList.value); |
|||
} catch (error) { |
|||
console.error("drawLottery 调用失败:", error); |
|||
} |
|||
|
|||
// 如果你还需要等球转完再做别的,可以 await rotatePromise |
|||
|
|||
break; |
|||
case "rotating": |
|||
setTimeout(() => { |
|||
isDisabled.value = false; |
|||
}, 2000); |
|||
// 停止转动并开奖 |
|||
// const prize = dataManager.state.basicData.prizes[lastRevealed.value]; |
|||
// console.log("准备调用 drawLottery,prize:", prize); |
|||
// console.log("lastRevealed.value:", lastRevealed.value); |
|||
// try { |
|||
// winnerList.value = await drawLottery({ |
|||
// perWin: prize.perWin, |
|||
// remainNum: prize.remainNum, |
|||
// gradeId: prize.gradeId, |
|||
// prizeId: prize.prizeId, |
|||
// }); |
|||
// console.log("drawLottery 调用成功,结果:", winnerList.value); |
|||
// } catch (error) { |
|||
// console.error("drawLottery 调用失败:", error); |
|||
// } |
|||
|
|||
await lottery3DRef.value?.rotateBallStop?.(); |
|||
await lotteryEngine.executeLottery(); |
|||
await nextTick(); |
|||
showLotteryQipao(); |
|||
console.log("lotteryState 变更前:", lotteryState.value, "-> idle"); |
|||
lotteryState.value = "result"; |
|||
|
|||
console.log("lotteryState 变更后:", lotteryState.value); |
|||
|
|||
break; |
|||
case "result": |
|||
setTimeout(() => { |
|||
isDisabled.value = false; |
|||
}, 2800); |
|||
// result 状态下点击不做任何事,或者你可以加提示 |
|||
|
|||
await lottery3DRef.value?.switchScreen?.("lottery"); |
|||
|
|||
await new Promise((resolve) => setTimeout(resolve, 2500)); |
|||
|
|||
// 去除高光 |
|||
lottery3DRef.value?.changeCard1?.(); |
|||
|
|||
//延迟2秒 |
|||
|
|||
lotteryState.value = "ready"; |
|||
|
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
function handleReset() { |
|||
lotteryEngine.resetLottery(); |
|||
} |
|||
function handleExport() { |
|||
dataManager.exportData(); |
|||
} |
|||
|
|||
function handlePrevPrize() { |
|||
if (dataManager.state.currentPrizeIndex > 0) { |
|||
dataManager.state.currentPrizeIndex--; |
|||
dataManager.state.currentPrize = |
|||
dataManager.state.basicData.prizes[dataManager.state.currentPrizeIndex]; |
|||
} |
|||
} |
|||
function handleNextPrize() { |
|||
if ( |
|||
dataManager.state.currentPrizeIndex < |
|||
dataManager.state.basicData.prizes.length - 1 |
|||
) { |
|||
dataManager.state.currentPrizeIndex++; |
|||
dataManager.state.currentPrize = |
|||
dataManager.state.basicData.prizes[dataManager.state.currentPrizeIndex]; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.choujiang-main { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
overflow: hidden; |
|||
/* 添加背景图片 */ |
|||
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> |
@ -0,0 +1,185 @@ |
|||
<template> |
|||
<div |
|||
:id="`card-${id}`" |
|||
:class="[ |
|||
'element', |
|||
{ lightitem: isBold, highlight: highlight, prize: prize }, |
|||
]" |
|||
:style="cardStyle" |
|||
> |
|||
<!-- <div class="company">{{ company }}</div> --> |
|||
<!-- <div class="name">{{ user[1] }}</div> --> |
|||
<div class="details">{{ displayText }}</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { computed, ref, onMounted, onBeforeUnmount, watch } from "vue"; |
|||
|
|||
import { useLotteryStore } from "../../../store/lottery"; // 路径根据实际情况调整 |
|||
|
|||
const lotteryStore = useLotteryStore(); // 只获取一次 |
|||
const lotteryState = computed({ |
|||
get: () => lotteryStore.lotteryState, |
|||
set: (val) => lotteryStore.setLotteryState(val), |
|||
}); |
|||
|
|||
const props = defineProps({ |
|||
id: [String, Number], |
|||
user: { type: Array, required: true }, |
|||
isBold: Boolean, |
|||
showTable: Boolean, |
|||
company: String, |
|||
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() * 10000 + 10000; // 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 { |
|||
...baseStyle, |
|||
background: 'linear-gradient(180deg, rgba(243,153,38,0.7) 0%, rgba(207,56,35,1) 100%)', |
|||
}; |
|||
} |
|||
} |
|||
|
|||
// 如果是result状态且有prize类,不设置背景色,让CSS类控制 |
|||
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 { |
|||
/* 高亮样式 */ |
|||
} |
|||
.highlight { |
|||
/* 响应式高亮样式 */ |
|||
} |
|||
.prize { |
|||
/* 中奖样式 - 使用更高优先级 */ |
|||
background: linear-gradient(180deg, #F39B26 0%, #E13A26 100%) !important; |
|||
} |
|||
.company { |
|||
/* ... */ |
|||
} |
|||
.name { |
|||
/* ... */ |
|||
} |
|||
.details { |
|||
font-size: 26px; |
|||
color: white; |
|||
text-align: center; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
height: 100%; |
|||
transition: opacity 0.3s ease; |
|||
} |
|||
</style> |
@ -0,0 +1,78 @@ |
|||
<template> |
|||
<div class="control-bar"> |
|||
<button |
|||
:disabled="isDisabled" |
|||
@click="$emit('lottery-click')" |
|||
class="lottery-button" |
|||
> |
|||
{{ lotteryState === 'idle' ? '进入抽奖' : lotteryState === 'ready' ? '开始抽奖' : lotteryState === 'rotating' ? '结束抽奖' : '开始抽奖' }} |
|||
</button> |
|||
<!-- <button @click="$emit('reset')">重置</button> --> |
|||
<!-- <button @click="$emit('export')">导出结果</button> --> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
const props = defineProps({ |
|||
lotteryState: String, |
|||
isDisabled: Boolean |
|||
}); |
|||
defineEmits(['lottery-click', 'reset', 'export']); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.control-bar { |
|||
position: absolute; |
|||
bottom: 40px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
display: flex; |
|||
gap: 24px; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.lottery-button { |
|||
padding: 12px 32px; |
|||
font-size: 20px; |
|||
font-weight: 700; |
|||
border-radius: 50px; |
|||
border: none; |
|||
background: linear-gradient(90deg, #ff8c42 0%, #ff6b35 50%, #ff5722 100%); |
|||
color: #fff; |
|||
cursor: pointer; |
|||
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; |
|||
} |
|||
|
|||
.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> |
@ -0,0 +1,886 @@ |
|||
<template> |
|||
<div class="lottery-3d-container"> |
|||
<div ref="threeContainer" class="three-container"></div> |
|||
|
|||
<!-- 分页指示器 --> |
|||
<div v-if="totalPages > 1" class="page-indicator"> |
|||
第 {{ currentPage + 1 }} 页 / 共 {{ totalPages }} 页 |
|||
</div> |
|||
|
|||
<!-- 滚动提示 --> |
|||
<!-- <div v-if="totalPages > 1 && currentPage === 0" class="scroll-hint"> |
|||
向下滚动查看更多 |
|||
</div> |
|||
<div v-if="totalPages > 1 && currentPage === totalPages - 1" class="scroll-hint"> |
|||
向上滚动查看上一页 |
|||
</div> --> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { |
|||
ref, |
|||
onMounted, |
|||
onBeforeUnmount, |
|||
defineExpose, |
|||
watch, |
|||
computed, |
|||
} from "vue"; |
|||
import * as THREE from "three"; |
|||
import { |
|||
CSS3DRenderer, |
|||
CSS3DObject, |
|||
} from "three/examples/jsm/renderers/CSS3DRenderer.js"; |
|||
// import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'; // 移除拖拽控件 |
|||
import TWEEN from "@tweenjs/tween.js"; |
|||
import { NUMBER_MATRIX } from "../../../utils/config.js"; |
|||
import CardItem from "./CardItem.vue"; |
|||
import { createApp } from "vue"; |
|||
import { getUserListApi } 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 |
|||
|
|||
// 全局变量存储当前选中的卡片索引数组 |
|||
let globalCardIndexes = []; |
|||
|
|||
// 分页相关变量 |
|||
const currentPage = ref(0); |
|||
const totalPages = ref(0); |
|||
const cardsPerPage = 10; // 每页显示的卡片数量 |
|||
let isPageTransitioning = false; // 防止页面切换时的重复操作 |
|||
|
|||
// 3D卡片与目标 |
|||
const threeDCards = []; |
|||
const targets = { |
|||
table: [], |
|||
sphere: [], |
|||
}; |
|||
function swapCardContents() { |
|||
// 确保有足够卡片且不在抽奖状态 |
|||
if (threeDCards.length < 2 || globalCardIndexes.length > 0) return; |
|||
|
|||
// 随机选择两张不同的卡片 |
|||
let indexA = Math.floor(Math.random() * threeDCards.length); |
|||
let indexB = Math.floor(Math.random() * threeDCards.length); |
|||
while (indexA === indexB) { |
|||
indexB = Math.floor(Math.random() * threeDCards.length); |
|||
} |
|||
|
|||
const cardA = threeDCards[indexA].element; |
|||
const cardB = threeDCards[indexB].element; |
|||
|
|||
// 保存原始内容(如果尚未保存) |
|||
if (!cardA.dataset.originalContent) { |
|||
cardA.dataset.originalContent = cardA.innerHTML; |
|||
} |
|||
if (!cardB.dataset.originalContent) { |
|||
cardB.dataset.originalContent = cardB.innerHTML; |
|||
} |
|||
|
|||
// 交换内容并添加动画效果 |
|||
[cardA.innerHTML, cardB.innerHTML] = [cardB.innerHTML, cardA.innerHTML]; |
|||
cardA.classList.add('swap-animation'); |
|||
cardB.classList.add('swap-animation'); |
|||
|
|||
// 动画结束后移除动画类 |
|||
setTimeout(() => { |
|||
cardA.classList.remove('swap-animation'); |
|||
cardB.classList.remove('swap-animation'); |
|||
}, 500); |
|||
} |
|||
function createElement(css = "", text = "") { |
|||
const dom = document.createElement("div"); |
|||
dom.className = css; |
|||
dom.innerHTML = text; |
|||
return dom; |
|||
} |
|||
|
|||
function createCard(user, isBold, id, showTable, company) { |
|||
// 使用 CardItem 组件动态渲染为 DOM 节点 |
|||
const container = document.createElement("div"); |
|||
const app = createApp(CardItem, { |
|||
id, |
|||
user, |
|||
isBold, |
|||
showTable, |
|||
company, |
|||
// highlight, prize 可后续补充 |
|||
}); |
|||
app.mount(container); |
|||
return container.firstElementChild; |
|||
} |
|||
|
|||
function createCards(member, length, showTable, position, config) { |
|||
let index = 0; |
|||
for (let i = 0; i < config.ROW_COUNT; i++) { |
|||
for (let j = 0; j < config.COLUMN_COUNT; j++) { |
|||
// 4. 判断是否高亮 |
|||
const isBold = (config.HIGHLIGHT_CELL || []).includes(j + "-" + i); |
|||
const element = createCard( |
|||
member[index % length], |
|||
isBold, |
|||
index, |
|||
showTable, |
|||
config.COMPANY |
|||
); |
|||
const object = new CSS3DObject(element); |
|||
object.position.x = Math.random() * 4000 - 2000; |
|||
object.position.y = Math.random() * 4000 - 2000; |
|||
object.position.z = Math.random() * 4000 - 2000; |
|||
scene.add(object); |
|||
threeDCards.push(object); |
|||
const targetObject = new THREE.Object3D(); |
|||
targetObject.position.x = j * 140 - position.x; |
|||
targetObject.position.y = -(i * 180) + position.y; |
|||
targets.table.push(targetObject); |
|||
index++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
function createSphereTargets() { |
|||
const vector = new THREE.Vector3(); |
|||
for (let i = 0, l = threeDCards.length; i < l; i++) { |
|||
const phi = Math.acos(-1 + (2 * i) / l); |
|||
const theta = Math.sqrt(l * Math.PI) * phi; |
|||
const object = new THREE.Object3D(); |
|||
object.position.setFromSphericalCoords(600, phi, theta); |
|||
object.position.y -= 200; // 向下偏移200px |
|||
|
|||
// 修正朝向计算:让卡牌朝向球体中心点 |
|||
vector.set(0, -200, 0); // 球体中心点,Y轴偏移与上面保持一致 |
|||
object.lookAt(vector); |
|||
targets.sphere.push(object); |
|||
} |
|||
} |
|||
|
|||
// 动画与切换相关方法 |
|||
function switchScreen(type) { |
|||
if (highlightTimeout) { |
|||
clearTimeout(highlightTimeout); |
|||
highlightTimeout = null; |
|||
} |
|||
// 示例:enter/table/sphere 切换 |
|||
if (type === "enter") { |
|||
transform(targets.table, 2000, () => { |
|||
addHighlight(); |
|||
highlightTimeout = null; |
|||
}); // 动画结束后加高亮 |
|||
} else { |
|||
transform(targets.sphere, 2000, () => removeHighlight()); // 动画结束后移除高亮 |
|||
} |
|||
} |
|||
|
|||
function transform(targetsArr, duration, onComplete) { |
|||
for (let i = 0; i < threeDCards.length; i++) { |
|||
const object = threeDCards[i]; |
|||
const target = targetsArr[i]; |
|||
new TWEEN.Tween(object.position) |
|||
.to( |
|||
{ |
|||
x: target.position.x, |
|||
y: target.position.y, |
|||
z: target.position.z, |
|||
}, |
|||
Math.random() * duration + duration |
|||
) |
|||
.easing(TWEEN.Easing.Exponential.InOut) |
|||
.start(); |
|||
new TWEEN.Tween(object.rotation) |
|||
.to( |
|||
{ |
|||
x: target.rotation.x, |
|||
y: target.rotation.y, |
|||
z: target.rotation.z, |
|||
}, |
|||
Math.random() * duration + duration |
|||
) |
|||
.easing(TWEEN.Easing.Exponential.InOut) |
|||
.start(); |
|||
} |
|||
new TWEEN.Tween({}) |
|||
.to({}, duration * 2) |
|||
.onUpdate(() => render()) |
|||
.onComplete(() => { |
|||
if (onComplete) onComplete(); |
|||
}) |
|||
.start(); |
|||
} |
|||
|
|||
function selectCard(selectedCardIndex, currentLuckys, duration = 600) { |
|||
if (highlightTimeout) { |
|||
clearTimeout(highlightTimeout); |
|||
highlightTimeout = null; |
|||
} |
|||
removeHighlight(); // 开始抽奖前移除高亮 |
|||
console.log("selectCard called:", { |
|||
selectedCardIndex, |
|||
currentLuckys, |
|||
duration, |
|||
}); |
|||
return new Promise((resolve) => { |
|||
const width = 140; |
|||
|
|||
// 计算总页数 |
|||
totalPages.value = Math.ceil(currentLuckys.length / cardsPerPage); |
|||
currentPage.value = 0; |
|||
|
|||
// 为每页计算位置信息 |
|||
const pageLocates = []; |
|||
|
|||
for (let page = 0; page < totalPages.value; page++) { |
|||
const startIndex = page * cardsPerPage; |
|||
const endIndex = Math.min( |
|||
(page + 1) * cardsPerPage, |
|||
currentLuckys.length |
|||
); |
|||
const pageCount = endIndex - startIndex; |
|||
|
|||
const pageLocate = []; |
|||
|
|||
// 根据当前页的人数决定排列方式 |
|||
if (pageCount > 5) { |
|||
// 大于5个分两排显示(与抽10人时的排列相同) |
|||
const yPosition = [-87, 87]; |
|||
const mid = Math.ceil(pageCount / 2); |
|||
let tag = -(mid - 1) / 2; |
|||
|
|||
for (let i = 0; i < mid; i++) { |
|||
pageLocate.push({ |
|||
x: tag * width, |
|||
y: yPosition[0], |
|||
}); |
|||
tag++; |
|||
} |
|||
|
|||
tag = -(pageCount - mid - 1) / 2; |
|||
for (let i = mid; i < pageCount; i++) { |
|||
pageLocate.push({ |
|||
x: tag * width, |
|||
y: yPosition[1], |
|||
}); |
|||
tag++; |
|||
} |
|||
} else { |
|||
// 小于等于5个一排显示(与抽不足10人时的排列相同) |
|||
let tag = -(pageCount - 1) / 2; |
|||
for (let i = 0; i < pageCount; i++) { |
|||
pageLocate.push({ |
|||
x: tag * width, |
|||
y: 0, |
|||
}); |
|||
tag++; |
|||
} |
|||
} |
|||
|
|||
pageLocates.push(pageLocate); |
|||
} |
|||
|
|||
console.log("pageLocates calculated:", pageLocates); |
|||
|
|||
// 初始化所有卡片位置(隐藏超出第一页的卡片) |
|||
selectedCardIndex.forEach((cardIndex, index) => { |
|||
changeCard(cardIndex, currentLuckys[index]); |
|||
const object = threeDCards[cardIndex]; |
|||
|
|||
// 计算卡片应该在第几页 |
|||
const cardPage = Math.floor(index / cardsPerPage); |
|||
const isVisible = cardPage === 0; |
|||
|
|||
// 计算在当前页中的索引 |
|||
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: pageLocate.x, |
|||
y: initialY, |
|||
z: 2200, |
|||
}, |
|||
Math.random() * duration + duration |
|||
) |
|||
.easing(TWEEN.Easing.Exponential.InOut) |
|||
.start(); |
|||
|
|||
new TWEEN.Tween(object.rotation) |
|||
.to( |
|||
{ |
|||
x: 0, |
|||
y: 0, |
|||
z: 0, |
|||
}, |
|||
Math.random() * duration + duration |
|||
) |
|||
.easing(TWEEN.Easing.Exponential.InOut) |
|||
.start(); |
|||
|
|||
object.element.classList.add("prize"); |
|||
}); |
|||
|
|||
// 保存页面位置信息到全局变量,供switchPage使用 |
|||
window.pageLocates = pageLocates; |
|||
|
|||
new TWEEN.Tween({}) |
|||
.to({}, duration * 2) |
|||
.onUpdate(() => render()) |
|||
.start() |
|||
.onComplete(() => { |
|||
console.log("selectCard animation completed"); |
|||
// 如果有多页,添加鼠标滚轮事件监听 |
|||
if (totalPages.value > 1) { |
|||
addWheelListener(); |
|||
} |
|||
resolve(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
// 分页切换函数 |
|||
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; |
|||
} |
|||
console.log("globalCardIndexes", globalCardIndexes); |
|||
// 动画切换卡片位置 |
|||
globalCardIndexes.forEach((cardIndex, index) => { |
|||
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]; |
|||
|
|||
// 根据切换方向决定动画效果 |
|||
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: targetY, |
|||
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) { |
|||
if (isPageTransitioning || totalPages.value <= 1) return; |
|||
|
|||
event.preventDefault(); |
|||
|
|||
if (event.deltaY > 0) { |
|||
// 向下滚动,显示下一页 |
|||
switchPage("next"); |
|||
} else if (event.deltaY < 0) { |
|||
// 向上滚动,显示上一页 |
|||
switchPage("prev"); |
|||
} |
|||
} |
|||
|
|||
// 添加鼠标滚轮事件监听器 |
|||
function addWheelListener() { |
|||
if (threeContainer.value) { |
|||
threeContainer.value.addEventListener("wheel", handleWheel, { |
|||
passive: false, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
// 移除鼠标滚轮事件监听器 |
|||
function removeWheelListener() { |
|||
if (threeContainer.value) { |
|||
threeContainer.value.removeEventListener("wheel", handleWheel); |
|||
} |
|||
} |
|||
|
|||
function resetCard(selectedCardIndex, duration = 500) { |
|||
if (!selectedCardIndex || selectedCardIndex.length === 0) { |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
// 移除鼠标滚轮事件监听器 |
|||
removeWheelListener(); |
|||
|
|||
// 重置分页状态 |
|||
currentPage.value = 0; |
|||
totalPages.value = 0; |
|||
isPageTransitioning = false; |
|||
|
|||
// 清理保存的页面位置信息 |
|||
if (window.pageLocates) { |
|||
delete window.pageLocates; |
|||
} |
|||
|
|||
// 清空全局卡片索引数组 |
|||
globalCardIndexes = []; |
|||
|
|||
selectedCardIndex.forEach((index) => { |
|||
const object = threeDCards[index]; |
|||
const target = targets.sphere[index]; |
|||
|
|||
new TWEEN.Tween(object.position) |
|||
.to( |
|||
{ |
|||
x: target.position.x, |
|||
y: target.position.y, |
|||
z: target.position.z, |
|||
}, |
|||
Math.random() * duration + duration |
|||
) |
|||
.easing(TWEEN.Easing.Exponential.InOut) |
|||
.start(); |
|||
|
|||
new TWEEN.Tween(object.rotation) |
|||
.to( |
|||
{ |
|||
x: target.rotation.x, |
|||
y: target.rotation.y, |
|||
z: target.rotation.z, |
|||
}, |
|||
Math.random() * duration + duration |
|||
) |
|||
.easing(TWEEN.Easing.Exponential.InOut) |
|||
.start(); |
|||
}); |
|||
|
|||
return new Promise((resolve) => { |
|||
new TWEEN.Tween({}) |
|||
.to({}, duration * 2) |
|||
.onUpdate(() => render()) |
|||
.start() |
|||
.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(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
function changeCard(cardIndex, user) { |
|||
// 保存到全局变量数组 |
|||
if (!globalCardIndexes.includes(cardIndex)) { |
|||
globalCardIndexes.push(cardIndex); |
|||
} |
|||
|
|||
const card = threeDCards[cardIndex].element; |
|||
|
|||
// 保存原始内容,以便后续恢复 |
|||
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: 30px; 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() { |
|||
console.log("执行取消高光,当前卡片索引:", globalCardIndexes); |
|||
|
|||
// 移除鼠标滚轮事件监听器 |
|||
removeWheelListener(); |
|||
|
|||
// 重置分页状态 |
|||
currentPage.value = 0; |
|||
totalPages.value = 0; |
|||
isPageTransitioning = false; |
|||
|
|||
// 清理保存的页面位置信息 |
|||
if (window.pageLocates) { |
|||
delete window.pageLocates; |
|||
} |
|||
|
|||
globalCardIndexes.forEach((cardIndex) => { |
|||
const card = threeDCards[cardIndex].element; |
|||
// console.log('取消卡片', cardIndex, '的高光'); |
|||
|
|||
// 恢复原始内容 |
|||
if (card.dataset.originalContent) { |
|||
card.innerHTML = card.dataset.originalContent; |
|||
delete card.dataset.originalContent; |
|||
} |
|||
|
|||
// 移除prize类,让CardItem组件的样式重新生效 |
|||
card.classList.remove("prize"); |
|||
}); |
|||
// 清空数组 |
|||
globalCardIndexes = []; |
|||
} |
|||
|
|||
function shine(cardIndex, color) { |
|||
const card = threeDCards[cardIndex].element; |
|||
card.style.backgroundColor = |
|||
color || `rgba(0,127,127,${Math.random() * 0.7 + 0.25})`; |
|||
} |
|||
|
|||
// 响应式高亮索引 |
|||
const highlightedIndexes = ref([]); |
|||
|
|||
// 替换 addHighlight 和 removeHighlight 为响应式写法 |
|||
function addHighlight(indexes = null) { |
|||
if (indexes) { |
|||
highlightedIndexes.value = [...indexes]; |
|||
} else { |
|||
// 默认高亮所有 .lightitem |
|||
highlightedIndexes.value = threeDCards |
|||
// .map((obj, idx) => |
|||
// obj.element.classList.contains("lightitem") ? idx : null |
|||
// ) |
|||
.filter((idx) => idx !== null); |
|||
} |
|||
} |
|||
|
|||
function removeHighlight(indexes = null) { |
|||
if (indexes) { |
|||
highlightedIndexes.value = highlightedIndexes.value.filter( |
|||
(i) => !indexes.includes(i) |
|||
); |
|||
} else { |
|||
highlightedIndexes.value = []; |
|||
} |
|||
} |
|||
|
|||
// 监听高亮索引变化并同步到DOM |
|||
watch(highlightedIndexes, (newVal) => { |
|||
threeDCards.forEach((cardObj, idx) => { |
|||
if (newVal.includes(idx)) { |
|||
cardObj.element.classList.add("highlight"); |
|||
} else { |
|||
cardObj.element.classList.remove("highlight"); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
let rotateObj = null; |
|||
let highlightTimeout = null; |
|||
|
|||
function rotateBallStart() { |
|||
return new Promise((resolve) => { |
|||
if (!scene) return resolve(); |
|||
scene.rotation.y = 0; |
|||
rotateObj = new TWEEN.Tween(scene.rotation) |
|||
.to({ y: Math.PI * 6 * 1000 }, 3000 * 1000) |
|||
.onUpdate(() => render()) |
|||
.onComplete(() => resolve()) |
|||
.start(); |
|||
}); |
|||
} |
|||
|
|||
function rotateBallStop() { |
|||
return new Promise((resolve) => { |
|||
if (!scene || !rotateObj) return resolve(); |
|||
rotateObj.stop(); |
|||
// 完全还原原生补偿动画逻辑 |
|||
const currentY = scene.rotation.y; |
|||
const targetY = Math.ceil(currentY / (2 * Math.PI)) * 2 * Math.PI; |
|||
const deltaY = Math.abs(targetY - currentY); |
|||
const duration = 500 + 1000 * (deltaY / Math.PI); |
|||
new TWEEN.Tween(scene.rotation) |
|||
.to({ y: targetY }, duration) |
|||
.easing(TWEEN.Easing.Cubic.Out) |
|||
.onUpdate(() => render()) |
|||
.onComplete(() => { |
|||
scene.rotation.y = 0; |
|||
render(); |
|||
resolve(); |
|||
}) |
|||
.start(); |
|||
}); |
|||
} |
|||
|
|||
function getTotalCards() { |
|||
return threeDCards.length; |
|||
} |
|||
|
|||
onMounted(async () => { |
|||
// 初始化 3D 场景 |
|||
scene = new THREE.Scene(); |
|||
camera = new THREE.PerspectiveCamera( |
|||
40, |
|||
window.innerWidth / window.innerHeight, |
|||
1, |
|||
10000 |
|||
); |
|||
camera.position.z = 3000; |
|||
// camera.position.y = 250; // 整体上移10px |
|||
// 或 |
|||
// scene.position.y = 10; // 整体上移10px |
|||
renderer = new CSS3DRenderer(); |
|||
renderer.setSize(window.innerWidth, window.innerHeight); |
|||
if (threeContainer.value) { |
|||
threeContainer.value.appendChild(renderer.domElement); |
|||
} |
|||
|
|||
// 2. 生成高亮坐标(以数字8为例) |
|||
let text = "0123"; |
|||
let step = 5; |
|||
let xoffset = 1; |
|||
let yoffset = 1; |
|||
let highlight = []; |
|||
text.split("").forEach((n) => { |
|||
highlight = highlight.concat( |
|||
NUMBER_MATRIX[n].map((item) => { |
|||
return `${item[0] + xoffset}-${item[1] + yoffset}`; |
|||
}) |
|||
); |
|||
xoffset += step; |
|||
}); |
|||
// const highlightCells = NUMBER_MATRIX["0"].map(([x, y]) => `${x}-${y}`); |
|||
const config = { |
|||
ROW_COUNT: 7, // 数字矩阵是5行 |
|||
COLUMN_COUNT: 21, // 数字矩阵是4列 |
|||
HIGHLIGHT_CELL: highlight, |
|||
COMPANY: "演示公司", |
|||
}; |
|||
|
|||
const userList = await getUserListApi(); |
|||
console.log("3D调用一次接口", userList); |
|||
// 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: (120 * config.ROW_COUNT - 20) / 2, |
|||
}; |
|||
createCards(member, length, showTable, position, config); // 3. 传递高亮配置 |
|||
createSphereTargets(); |
|||
|
|||
// 先渲染散落状态 |
|||
render(); |
|||
animate(); |
|||
|
|||
// 延迟后自动聚集到界面中间 |
|||
setTimeout(() => { |
|||
switchScreen("enter"); |
|||
}, 500); |
|||
|
|||
window.addEventListener("resize", onWindowResize); |
|||
// swapInterval.value = setInterval(swapCardContents, swapIntervalTime.value); |
|||
}); |
|||
|
|||
onBeforeUnmount(() => { |
|||
window.removeEventListener("resize", onWindowResize); |
|||
removeWheelListener(); // 移除鼠标滚轮事件监听器 |
|||
if (animationId) cancelAnimationFrame(animationId); |
|||
if (highlightTimeout) { |
|||
clearTimeout(highlightTimeout); |
|||
highlightTimeout = null; |
|||
} |
|||
// if (swapInterval.value) { |
|||
// clearInterval(swapInterval.value); |
|||
// swapInterval.value = null; |
|||
// } |
|||
}); |
|||
|
|||
function render() { |
|||
renderer.render(scene, camera); |
|||
} |
|||
|
|||
function animate() { |
|||
animationId = requestAnimationFrame(animate); |
|||
TWEEN.update(); |
|||
// controls.update(); // 移除controls |
|||
render(); |
|||
} |
|||
|
|||
function onWindowResize() { |
|||
camera.aspect = window.innerWidth / window.innerHeight; |
|||
camera.updateProjectionMatrix(); |
|||
renderer.setSize(window.innerWidth, window.innerHeight); |
|||
render(); |
|||
} |
|||
|
|||
defineExpose({ |
|||
resetCard, |
|||
addHighlight, |
|||
switchScreen, |
|||
rotateBallStart, |
|||
rotateBallStop, |
|||
selectCard, |
|||
getTotalCards, |
|||
changeCard1, |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.lottery-3d-container { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
.three-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
/* 分页指示器样式 */ |
|||
.page-indicator { |
|||
position: absolute; |
|||
bottom: 20px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
z-index: 1000; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
color: white; |
|||
padding: 8px 16px; |
|||
border-radius: 20px; |
|||
font-size: 14px; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
/* 滚动提示样式 */ |
|||
.scroll-hint { |
|||
position: absolute; |
|||
bottom: 60px; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
z-index: 1000; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
color: white; |
|||
padding: 8px 16px; |
|||
border-radius: 20px; |
|||
font-size: 12px; |
|||
pointer-events: none; |
|||
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% { |
|||
opacity: 0.6; |
|||
} |
|||
50% { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
</style> |
@ -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> |
@ -0,0 +1,76 @@ |
|||
<template> |
|||
<div class="music-box" @click="toggleMusic"> |
|||
<audio ref="audioRef" :src="musicSrc" loop></audio> |
|||
<div class="music-icon" :class="{ playing }"></div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, onMounted } from 'vue'; |
|||
const musicSrc = '/src/assets/music.mp3'; |
|||
const audioRef = ref(null); |
|||
const playing = ref(false); |
|||
|
|||
function toggleMusic() { |
|||
if (!audioRef.value) return; |
|||
if (audioRef.value.paused) { |
|||
audioRef.value.play(); |
|||
playing.value = true; |
|||
} else { |
|||
audioRef.value.pause(); |
|||
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; |
|||
width: 48px; |
|||
height: 48px; |
|||
z-index: 20; |
|||
cursor: pointer; |
|||
} |
|||
.music-icon { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: url('/src/assets/lottery/edifier.jpg') no-repeat center/cover; |
|||
border-radius: 50%; |
|||
transition: box-shadow 0.2s; |
|||
} |
|||
.music-icon.playing { |
|||
animation: rotate 1.2s linear infinite; |
|||
/* box-shadow: 0 0 12px #0078ff; */ |
|||
} |
|||
@keyframes rotate { |
|||
100% { transform: rotate(360deg); } |
|||
} |
|||
</style> |
@ -0,0 +1,716 @@ |
|||
<template> |
|||
<div v-if="showOne"> |
|||
<div class="prize-panel-root"> |
|||
<div class="prize-panel-list" v-if="prizes && prizes.length"> |
|||
<div |
|||
class="prize-panel-item" |
|||
v-for="(prize, idx) in prizes" |
|||
:key="prize.type || idx" |
|||
:class="{ |
|||
'revealed-highlight': idx === lastRevealedIdx, |
|||
disabled: idx === nextRevealIdx && !canRevealPrize(idx), |
|||
}" |
|||
@click="handleReveal(idx)" |
|||
:style="{ |
|||
cursor: |
|||
idx === nextRevealIdx && !canRevealPrize(idx) |
|||
? 'not-allowed' |
|||
: 'pointer', |
|||
}" |
|||
> |
|||
<div v-if="isRevealed(idx)" class="prize-card"> |
|||
<div class="prize-img-wrap"> |
|||
<img class="prize-img" :src="prize.img" :alt="prize.title" /> |
|||
</div> |
|||
<div class="prize-info"> |
|||
<div class="prize-row prize-row-top"> |
|||
<span class="prize-level">{{ prize.title }}</span> |
|||
<span class="prize-name">{{ prize.text }}</span> |
|||
</div> |
|||
<div class="prize-row prize-row-bottom"> |
|||
<div class="progress-bar-bg"> |
|||
<div |
|||
class="progress-bar-fill" |
|||
:style="{ width: getProgressPercent(prize) + '%' }" |
|||
></div> |
|||
<span class="progress-bar-text"> |
|||
{{ getLeftCount(prize) }}/{{ prize.count }} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div v-else class="prize-card prize-card-mask"> |
|||
<img |
|||
src="../../../assets/daijiemi.png" |
|||
alt="待揭秘" |
|||
class="prize-mask-img" |
|||
/> |
|||
</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" |
|||
class="winner-btn" |
|||
@click="toggleWinnerList" |
|||
> |
|||
获奖名单 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-else> |
|||
<div class="prize-panel-root"> |
|||
<div |
|||
class="prize-panel-list1" |
|||
v-if="prizes && prizes.length && lastRevealedIdx >= 0" |
|||
> |
|||
<div |
|||
class="prize-panel-item" |
|||
:key="prizes[lastRevealedIdx].type || lastRevealedIdx" |
|||
:class="{ 'revealed-highlight': true }" |
|||
style="cursor: pointer" |
|||
> |
|||
<div class="prize-card"> |
|||
<div class="prize-img-wrap"> |
|||
<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> |
|||
</div> |
|||
<div class="prize-row prize-row-bottom"> |
|||
<div class="progress-bar-bg"> |
|||
<div |
|||
class="progress-bar-fill" |
|||
:style="{ |
|||
width: getProgressPercent(prizes[lastRevealedIdx]) + '%', |
|||
}" |
|||
></div> |
|||
<span class="progress-bar-text"> |
|||
{{ getLeftCount(prizes[lastRevealedIdx]) }}/{{ |
|||
prizes[lastRevealedIdx].count |
|||
}} |
|||
</span> |
|||
</div> |
|||
<div class="prize-panel-footer"> |
|||
<div class="arrow-down" @click="toggleWinnerList"></div> |
|||
<div |
|||
v-if="showWinnerList" |
|||
class="winner-modal-mask" |
|||
@click="closeWinnerList" |
|||
> |
|||
<div |
|||
class="winner-modal" |
|||
:style="{ |
|||
position: 'absolute', |
|||
left: modalLeft + 'px', |
|||
top: modalTop + 'px', |
|||
}" |
|||
@click.stop |
|||
> |
|||
<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" |
|||
style=" |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
" |
|||
> |
|||
<!-- <span>{{ user.id }}</span> - <span>{{ user.name }}</span> - --> |
|||
<span>{{ user.jwcode }}</span> |
|||
<span>{{ user.prizeName }}</span> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, computed, nextTick, watch, onMounted } from "vue"; |
|||
import { useLotteryStore } from "../../../store/lottery"; |
|||
import { useDataManager } from "../lottery/dataManager"; |
|||
|
|||
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), |
|||
}); |
|||
|
|||
// 用watch监听winners的变化 |
|||
// watch(winners, (newVal) => { |
|||
// console.log("中奖人数列表winners", newVal); |
|||
// fakeWinners.value = newVal; |
|||
// }); |
|||
|
|||
const lastRevealedIdx = ref(-1); |
|||
|
|||
lastRevealedIdx.value = lastRevealed.value; |
|||
|
|||
const showOne = ref(true); |
|||
|
|||
// 计算哪些奖品已揭秘 |
|||
const isRevealed = (idx) => |
|||
idx >= (props.prizes?.length || 0) - revealedCount.value; |
|||
// 允许点击的卡片index |
|||
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) { |
|||
alert("请按顺序揭秘奖品!"); |
|||
return; |
|||
} |
|||
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("上一个奖品还未抽完,请等待抽奖完成后再揭秘下一个奖品"); |
|||
} |
|||
} |
|||
// 计算未抽取数量 |
|||
function getLeftCount(prize) { |
|||
// 这里假设奖品有 type 字段,且 dataManager.state.basicData.luckyUsers 可用 |
|||
// 由于本组件无 luckyUsers 数据,建议父组件传入或全局可访问 |
|||
// 这里用 window.dataManager 兼容演示 |
|||
let luckyUsers = |
|||
(window.dataManager && window.dataManager.state.basicData.luckyUsers) || {}; |
|||
let got = luckyUsers[prize.type]?.length || 0; |
|||
return prize.remainNum; |
|||
} |
|||
|
|||
// 新增部分 |
|||
const showWinnerList = ref(false); |
|||
const fakeWinners = ref([]); |
|||
// fakeWinners.value = winners.value; |
|||
console.log("fakeWinners", fakeWinners.value); |
|||
|
|||
import { getGetPrizeUserListApi } from "../../../api/API"; |
|||
const updateWinners = async () => { |
|||
try { |
|||
const response = await getGetPrizeUserListApi({ |
|||
|
|||
}); |
|||
console.log("updatePrizeList response", response); |
|||
fakeWinners.value = response.data.list; |
|||
console.log("updateWinners fakeWinners", fakeWinners.value); |
|||
} catch (error) { |
|||
console.error("updatePrizeList error", error); |
|||
} |
|||
}; |
|||
async function openWinnerList() { |
|||
// showWinnerList.value = true; |
|||
if (!showWinnerList.value) { |
|||
if (revealedCount.value === 0) { |
|||
alert("请先揭晓奖品,并抽奖!"); |
|||
} |
|||
// await updatePrizeList(); |
|||
} |
|||
if (revealedCount.value > 0) { |
|||
showWinnerList.value = true; |
|||
} |
|||
// 当存在最新揭秘的奖品时,点击获奖名单将showOne设置为false |
|||
if (lastRevealedIdx.value >= 0) { |
|||
showOne.value = false; |
|||
} |
|||
} |
|||
function closeWinnerList() { |
|||
showWinnerList.value = false; |
|||
// 当关闭获奖名单且showOne为false时,将其切换回true |
|||
if (!showOne.value) { |
|||
showOne.value = true; |
|||
} |
|||
} |
|||
|
|||
const winnerBtnRef = ref(null); |
|||
const modalLeft = ref(0); |
|||
const modalTop = ref(0); |
|||
|
|||
async function toggleWinnerList() { |
|||
await updateWinners(); |
|||
|
|||
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) { |
|||
// 当存在最新揭秘的奖品时,点击获奖名单将showOne设置为false |
|||
|
|||
if (lastRevealedIdx.value > 0) { |
|||
showOne.value = false; |
|||
console.log("设置 showOne 为 false"); |
|||
} |
|||
} else { |
|||
// 当关闭获奖名单且showOne为false时,将其切换回true |
|||
if (!showOne.value) { |
|||
showOne.value = true; |
|||
|
|||
console.log("设置 showOne 为 true"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
function getProgressPercent(prize) { |
|||
const total = prize.count || 1; |
|||
const left = getLeftCount(prize); |
|||
// 返回剩余数量的百分比,这样开始时是满的,抽完后是空的 |
|||
return Math.round((left / total) * 100); |
|||
} |
|||
|
|||
// onMounted(() => { |
|||
// updateWinners(); |
|||
// }); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.prize-panel-list { |
|||
position: relative; |
|||
background: none; |
|||
z-index: 10; |
|||
min-width: 300px; |
|||
max-width: 342px; |
|||
text-align: left; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 18px; |
|||
height: 700px; |
|||
overflow-x: hidden !important; |
|||
overflow-y: auto; |
|||
padding-right: 10px; |
|||
scrollbar-width: thin; |
|||
scrollbar-color: #ffd283 rgba(255, 210, 131, 0.3); /* Firefox */ |
|||
} |
|||
.prize-panel-list1 { |
|||
position: relative; |
|||
background: none; |
|||
z-index: 10; |
|||
min-width: 320px; |
|||
max-width: 342px; |
|||
text-align: left; |
|||
display: flex; |
|||
} |
|||
|
|||
.prize-panel-list::-webkit-scrollbar { |
|||
width: 6px; |
|||
} |
|||
|
|||
.prize-panel-list::-webkit-scrollbar-track { |
|||
background: rgba(255, 210, 131, 0.3); |
|||
border-radius: 3px; |
|||
} |
|||
|
|||
.prize-panel-list::-webkit-scrollbar-thumb { |
|||
background-color: #ffd283; |
|||
border-radius: 3px; |
|||
} |
|||
.prize-panel-item { |
|||
background: #ffd283; |
|||
border-radius: 6px 6px 6px 6px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
|||
display: flex; |
|||
align-items: center; |
|||
min-width: 320px; |
|||
} |
|||
.prize-card { |
|||
display: flex; |
|||
align-items: center; |
|||
width: 100%; |
|||
padding: 10px 18px; |
|||
} |
|||
.prize-img-wrap { |
|||
width: 64px; |
|||
height: 64px; |
|||
border-radius: 50%; |
|||
background: #fff; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
overflow: hidden; |
|||
margin-right: 18px; |
|||
border: 2px solid #fff3e0; |
|||
} |
|||
.prize-img { |
|||
width: 60px; |
|||
height: 60px; |
|||
object-fit: contain; |
|||
} |
|||
.prize-info { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
} |
|||
.prize-row { |
|||
display: flex; |
|||
align-items: center; |
|||
background: #ffffff; |
|||
border-radius: 93px 93px 93px 93px; |
|||
} |
|||
.prize-row-top { |
|||
margin-bottom: 8px; |
|||
border: 1px solid #ea2b0a; |
|||
} |
|||
.prize-level { |
|||
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%); |
|||
color: #fff; |
|||
border-radius: 15.71px 15.71px 15.71px 15.71px; |
|||
padding: 2px 18px; |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
margin-right: 12px; |
|||
} |
|||
.prize-name { |
|||
font-size: 18px; |
|||
color: #d84315; |
|||
font-weight: 500; |
|||
} |
|||
/* .prize-row-bottom { |
|||
background: linear-gradient(90deg, #ff9800 0%, #ff5722 100%); |
|||
background: #8a3500; |
|||
border-radius: 16px; |
|||
color: #fff; |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
padding: 2px 0 2px 0; |
|||
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; |
|||
} |
|||
.prize-divider { |
|||
margin: 0 4px; |
|||
font-size: 20px; |
|||
} |
|||
.prize-total { |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
} |
|||
.prize-panel-root { |
|||
position: absolute; |
|||
top: 20px; |
|||
left: 20px; |
|||
background: none; |
|||
z-index: 10; |
|||
min-width: 320px; |
|||
max-width: 342px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.prize-panel-footer { |
|||
position: relative; |
|||
width: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
z-index: 20; |
|||
padding: 10px 0; |
|||
border-radius: 0 0 6px 6px; |
|||
margin-top: 10px; |
|||
} |
|||
.arrow-up { |
|||
position: relative; |
|||
width: 50px; |
|||
height: 30px; |
|||
margin-bottom: 4px; |
|||
cursor: pointer; |
|||
background-image: url("../../../assets/展开.png"); |
|||
background-size: cover; |
|||
background-position: center; |
|||
transform: rotate(180deg); |
|||
} |
|||
.arrow-down { |
|||
position: fixed; |
|||
top: 100px; |
|||
left: 150px; |
|||
width: 47px; |
|||
height: 28px; |
|||
margin-bottom: 4px; |
|||
cursor: pointer; |
|||
background-image: url("../../../assets/展开.png"); |
|||
background-size: cover; |
|||
background-position: center; |
|||
} |
|||
.winner-btn { |
|||
background: rgba(255, 210, 131, 0.8); |
|||
color: #d5291f; |
|||
border: #fff; |
|||
border-radius: 8px; |
|||
padding: 15px 79px; |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
cursor: pointer; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
|||
} |
|||
.winner-modal-mask { |
|||
position: fixed; |
|||
top: 142px; |
|||
left: -4px; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background: rgba(0, 0, 0, 0.01); |
|||
z-index: 1000; |
|||
display: flex; |
|||
align-items: flex-start; |
|||
justify-content: center; |
|||
} |
|||
.winner-modal { |
|||
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; |
|||
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; |
|||
right: 18px; |
|||
top: 12px; |
|||
font-size: 26px; |
|||
color: #888; |
|||
cursor: pointer; |
|||
} |
|||
.winner-list { |
|||
max-height: 260px; |
|||
/* background: rgba(255, 210, 131, 0.8);/ */ |
|||
overflow-y: auto; |
|||
padding: 0; |
|||
margin: 0; |
|||
list-style: none; |
|||
/* 隐藏滚动条但保留滚动功能 */ |
|||
scrollbar-width: none; /* Firefox */ |
|||
-ms-overflow-style: none; /* IE and Edge */ |
|||
} |
|||
|
|||
.winner-list::-webkit-scrollbar { |
|||
display: none; /* Chrome, Safari and Opera */ |
|||
} |
|||
.winner-list li { |
|||
padding: 8px 0; |
|||
/* border-bottom: 1px solid #f2f2f2; */ |
|||
font-size: 17px; |
|||
color: #d84315; |
|||
display: flex; |
|||
gap: 12px; |
|||
align-items: center; |
|||
justify-content: center; |
|||
text-align: center; |
|||
} |
|||
.progress-bar-bg { |
|||
position: relative; |
|||
width: 220px; |
|||
height: 28px; |
|||
background: #e9620e; |
|||
border-radius: 16px; |
|||
overflow: hidden; |
|||
display: flex; |
|||
align-items: center; |
|||
margin: 0 auto; |
|||
border: #e13726; |
|||
} |
|||
.progress-bar-fill { |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
height: 100%; |
|||
/* background: linear-gradient(90deg, #ff9800 0%, #8a3500 100%); */ |
|||
background: #8a3500; |
|||
border-radius: 16px; |
|||
transition: width 0.4s; |
|||
z-index: 1; |
|||
} |
|||
.progress-bar-text { |
|||
position: relative; |
|||
width: 100%; |
|||
text-align: center; |
|||
color: #ffffff; |
|||
font-size: 18px; |
|||
font-weight: bold; |
|||
z-index: 2; |
|||
letter-spacing: 1px; |
|||
} |
|||
.prize-card-mask { |
|||
position: relative; |
|||
width: 342px; |
|||
height: 88px; |
|||
display: flex; |
|||
/* align-items: center; |
|||
justify-content: center; */ |
|||
padding: 0; |
|||
overflow: hidden; |
|||
} |
|||
.prize-mask-img { |
|||
object-fit: cover; |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 98%; |
|||
object-fit: cover; |
|||
left: 0; |
|||
top: 0; |
|||
border-radius: 8px 8px 8px 8px; |
|||
} |
|||
.prize-panel-item.revealed-highlight { |
|||
/* 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> |
@ -0,0 +1,37 @@ |
|||
<template> |
|||
<div v-if="show" class="qipao" :style="styleObj"> |
|||
{{ text }} |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, watch, computed } from 'vue'; |
|||
const props = defineProps({ |
|||
text: String, |
|||
show: Boolean |
|||
}); |
|||
const styleObj = computed(() => ({ |
|||
position: 'absolute', |
|||
background: 'rgba(0,0,0,0.8)', |
|||
color: '#fff', |
|||
padding: '8px 12px', |
|||
borderRadius: '4px', |
|||
fontSize: '14px', |
|||
left: Math.random() * (window.innerWidth - 200) + 'px', |
|||
top: Math.random() * (window.innerHeight - 100) + 'px', |
|||
zIndex: 1000, |
|||
animation: 'fadeInOut 3s ease-in-out forwards' |
|||
})); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.qipao { |
|||
white-space: nowrap; |
|||
} |
|||
@keyframes fadeInOut { |
|||
0% { opacity: 0; } |
|||
10% { opacity: 1; } |
|||
90% { opacity: 1; } |
|||
100% { opacity: 0; } |
|||
} |
|||
</style> |
@ -0,0 +1,62 @@ |
|||
<template> |
|||
<div class="user-list-panel"> |
|||
<div class="lucky-list"> |
|||
<div class="list-title">中奖名单</div> |
|||
<ul> |
|||
<li v-for="(user, idx) in luckyUsers" :key="idx">{{ user[1] }}</li> |
|||
</ul> |
|||
</div> |
|||
<!-- <div class="left-list"> |
|||
<div class="list-title">未中奖用户</div> |
|||
<ul> |
|||
<li v-for="(user, idx) in leftUsers" :key="idx">{{ user[1] }}</li> |
|||
</ul> |
|||
</div> --> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { useLotteryStore } from "../../../store/lottery"; |
|||
const lotteryStore = useLotteryStore(); |
|||
|
|||
|
|||
const props = defineProps({ |
|||
luckyUsers: { |
|||
type: Array, |
|||
default: () => [] |
|||
}, |
|||
leftUsers: { |
|||
type: Array, |
|||
default: () => [] |
|||
} |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.user-list-panel { |
|||
position: absolute; |
|||
right: 24px; |
|||
top: 80px; |
|||
width: 220px; |
|||
background: rgba(255,255,255,0.95); |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 8px rgba(0,0,0,0.08); |
|||
z-index: 15; |
|||
padding: 16px 12px; |
|||
font-size: 15px; |
|||
} |
|||
.list-title { |
|||
font-weight: bold; |
|||
margin-bottom: 6px; |
|||
font-size: 16px; |
|||
} |
|||
ul { |
|||
margin: 0 0 12px 0; |
|||
padding: 0; |
|||
list-style: none; |
|||
} |
|||
li { |
|||
padding: 2px 0; |
|||
border-bottom: 1px dashed #eee; |
|||
} |
|||
</style> |
@ -0,0 +1,217 @@ |
|||
import { reactive } from 'vue'; |
|||
import { getPrizeListApi, getUserListApi } from '../../../api/API'; |
|||
export function useDataManager() { |
|||
const state = reactive({ |
|||
basicData: { |
|||
prizes: [], |
|||
users: [], |
|||
luckyUsers: {}, |
|||
leftUsers: [] |
|||
}, |
|||
currentPrizeIndex: 0, |
|||
currentPrize: null, |
|||
currentLuckys: [], |
|||
isLotting: false, |
|||
// 新增:轮次管理
|
|||
currentRound: 1, |
|||
config: { |
|||
prizes: [], |
|||
EACH_COUNT: [], |
|||
COMPANY: '', |
|||
HIGHLIGHT_CELL: [], |
|||
Resolution: 1, |
|||
ROW_COUNT: 7, |
|||
COLUMN_COUNT: 20 |
|||
} |
|||
}); |
|||
|
|||
async function getBasicData() { |
|||
// 获取奖品列表
|
|||
const prizeList = await getPrizeListApi(); |
|||
const fakePrizes = prizeList.data.map((item, index) => ({ |
|||
type: index, // 使用索引作为type
|
|||
title: item.gradeName, |
|||
text: item.prizeName, |
|||
count: item.amount, |
|||
img: item.imageUrl, |
|||
gradeId: item.gradeId, |
|||
prizeId: item.prizeId, |
|||
remainNum: item.remainNum, |
|||
perWin: item.perWin |
|||
})); |
|||
|
|||
const fakeEachCount = prizeList.data.map(item => item.perWin); |
|||
console.log("fakeEachCount", fakeEachCount); |
|||
const fakeCompany = '前端假公司'; |
|||
const fakeLuckyData = {}; |
|||
fakePrizes.forEach((prize, index) => { |
|||
fakeLuckyData[index] = []; |
|||
}); |
|||
|
|||
// 获取真实用户数据
|
|||
const userListResponse = await getUserListApi(); |
|||
console.log("dataManager调用一次接口", userListResponse); |
|||
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.users = realUsers; // 使用真实用户数据
|
|||
state.basicData.leftUsers = realUsers.slice(); // 初始化剩余用户为所有用户
|
|||
state.basicData.luckyUsers = fakeLuckyData; |
|||
determineCurrentPrize(); |
|||
return Promise.resolve({ |
|||
cfgData: { |
|||
prizes: fakePrizes, |
|||
EACH_COUNT: fakeEachCount, |
|||
COMPANY: fakeCompany |
|||
}, |
|||
leftUsers: realUsers.slice(), |
|||
luckyData: fakeLuckyData |
|||
}); |
|||
} |
|||
|
|||
async function getUsers() { |
|||
const userList = await getUserListApi(); |
|||
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() { |
|||
let prizeIndex = state.basicData.prizes.length - 1; |
|||
for (; prizeIndex > -1; prizeIndex--) { |
|||
if ( |
|||
state.basicData.luckyUsers[prizeIndex] && |
|||
state.basicData.luckyUsers[prizeIndex].length >= |
|||
state.basicData.prizes[prizeIndex].count |
|||
) { |
|||
continue; |
|||
} |
|||
state.currentPrizeIndex = prizeIndex; |
|||
state.currentPrize = state.basicData.prizes[state.currentPrizeIndex]; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
async function saveData(type, data) { |
|||
return Promise.resolve(); |
|||
} |
|||
async function setErrorData(data) { return Promise.resolve(); } |
|||
async function exportData() { alert('导出功能为假数据模式,无实际导出。'); return Promise.resolve(); } |
|||
async function resetData() { return Promise.resolve(); } |
|||
|
|||
function getTotalCards() { |
|||
return state.config.ROW_COUNT * state.config.COLUMN_COUNT || 50; |
|||
} |
|||
function setLotteryStatus(status = false) { state.isLotting = status; } |
|||
function resetAllData() { |
|||
state.basicData.luckyUsers = {}; |
|||
state.basicData.leftUsers = state.basicData.users.slice(); |
|||
state.currentLuckys = []; |
|||
determineCurrentPrize(); |
|||
} |
|||
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); |
|||
} |
|||
|
|||
// 新增:更新奖品列表数据
|
|||
async function updatePrizeList() { |
|||
try { |
|||
console.log('开始更新奖品列表数据...'); |
|||
const prizeList = await getPrizeListApi(); |
|||
const fakePrizes = prizeList.data.map((item, index) => ({ |
|||
type: index, // 使用索引作为type
|
|||
title: item.gradeName, |
|||
text: item.prizeName, |
|||
count: item.amount, |
|||
img: item.imageUrl, |
|||
gradeId: item.gradeId, |
|||
prizeId: item.prizeId, |
|||
remainNum: item.remainNum, |
|||
perWin: item.perWin |
|||
})); |
|||
|
|||
const fakeEachCount = prizeList.data.map(item => item.perWin); |
|||
console.log("更新后的奖品列表:", fakePrizes); |
|||
console.log("更新后的每轮数量:", fakeEachCount); |
|||
|
|||
// 更新配置数据
|
|||
state.config.prizes = fakePrizes; |
|||
state.config.EACH_COUNT = fakeEachCount; |
|||
state.basicData.prizes = fakePrizes; |
|||
|
|||
// 重新确定当前奖品
|
|||
determineCurrentPrize(); |
|||
|
|||
console.log('奖品列表数据更新完成'); |
|||
return Promise.resolve(fakePrizes); |
|||
} catch (error) { |
|||
console.error('更新奖品列表失败:', error); |
|||
return Promise.reject(error); |
|||
} |
|||
} |
|||
|
|||
return { |
|||
state, |
|||
getBasicData, |
|||
getUsers, |
|||
saveData, |
|||
setErrorData, |
|||
exportData, |
|||
resetData, |
|||
getTotalCards, |
|||
setLotteryStatus, |
|||
resetAllData, |
|||
updateCurrentPrize, |
|||
getTotalRounds, |
|||
getCurrentRound, |
|||
getLeftCount, |
|||
updateCurrentRound, |
|||
updatePrizeList |
|||
}; |
|||
} |
@ -0,0 +1,198 @@ |
|||
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),
|
|||
// });
|
|||
|
|||
const lastRevealed = computed({ |
|||
get: () => lotteryStore.lastRevealedIdx, |
|||
set: (val) => lotteryStore.setLastRevealedIdx(val), |
|||
}); |
|||
|
|||
const winnerList = computed({ |
|||
get: () => lotteryStore.winnerList, |
|||
set: (val) => lotteryStore.setWinnerList(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(); // 只获取一次
|
|||
|
|||
async function executeLottery() { |
|||
if (isLotting.value) return; |
|||
isLotting.value = true; |
|||
dataManager.setLotteryStatus(true); |
|||
await dataManager.saveData(); |
|||
changePrize(); |
|||
// 重置卡片动画
|
|||
await renderer3D.resetCard([]); |
|||
|
|||
// 计算本次应该抽奖的人数
|
|||
const currentPrizeIndex = dataManager.state.currentPrizeIndex; |
|||
const prize = dataManager.state.basicData.prizes[lastRevealed.value]; |
|||
const luckyUsers = dataManager.state.basicData.luckyUsers[lastRevealed.value] || []; |
|||
const remainingPrizeCount = prize.count - luckyUsers.length; // 奖品剩余数量
|
|||
const basePerCount = dataManager.state.config.EACH_COUNT[lastRevealed.value] || 1; |
|||
const actualPerCount = Math.min(basePerCount, remainingPrizeCount); // 取最小值
|
|||
|
|||
console.log('executeLottery - currentPrizeIndex:', lastRevealed.value, 'prize:', prize, 'basePerCount:', basePerCount, 'remainingPrizeCount:', remainingPrizeCount, 'actualPerCount:', actualPerCount); |
|||
|
|||
// 请求后端进行抽奖
|
|||
try { |
|||
const lotteryData = { |
|||
gradeId: prize.gradeId, |
|||
prizeId: prize.prizeId, |
|||
perWin: basePerCount, |
|||
remainNum: prize.remainNum |
|||
}; |
|||
|
|||
// console.log('请求后端抽奖,参数:', lotteryData);
|
|||
const response = winnerList.value |
|||
console.log('后端抽奖返回结果:', response); |
|||
// winners.value = response.data.data;
|
|||
// console.log('抽奖的winners', winners.value);
|
|||
// winners.value = response.data;
|
|||
|
|||
// console.log('后端抽奖返回结果:', response);
|
|||
|
|||
if (response && response.data.data && Array.isArray(response.data.data)) { |
|||
// 后端返回中奖用户数据
|
|||
const currentLuckys = response.data.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] = []; |
|||
} |
|||
dataManager.state.basicData.luckyUsers[currentPrizeIndex].push(...currentLuckys); |
|||
// 更新轮次信息
|
|||
dataManager.updateCurrentRound(); |
|||
// 展示中奖动画
|
|||
console.log('executeLottery - calling selectCard'); |
|||
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys); |
|||
console.log('executeLottery - selectCard completed'); |
|||
} |
|||
|
|||
// 抽奖完成后更新奖品列表数据
|
|||
try { |
|||
await dataManager.updatePrizeList(); |
|||
console.log('抽奖完成后奖品列表已更新'); |
|||
} catch (error) { |
|||
console.error('更新奖品列表失败:', error); |
|||
} |
|||
|
|||
dataManager.setLotteryStatus(false); |
|||
isLotting.value = false; |
|||
} |
|||
|
|||
function performLottery() { |
|||
// 已合并到 executeLottery
|
|||
} |
|||
|
|||
function changePrize() { |
|||
// 可根据中奖情况切换奖品
|
|||
dataManager.updateCurrentPrize(); |
|||
} |
|||
|
|||
async function resetLottery() { |
|||
if (isLotting.value) return; |
|||
dataManager.resetAllData(); |
|||
renderer3D.addHighlight && renderer3D.addHighlight(); |
|||
lotteryStore.setLotteryState('idle'); // 直接用
|
|||
await renderer3D.resetCard([]); |
|||
await dataManager.resetData(); |
|||
// 重置轮次
|
|||
dataManager.state.currentRound = 1; |
|||
renderer3D.switchScreen && renderer3D.switchScreen('enter'); |
|||
} |
|||
|
|||
return { |
|||
isLotting, |
|||
executeLottery, |
|||
performLottery, |
|||
changePrize, |
|||
resetLottery |
|||
}; |
|||
} |
@ -0,0 +1,22 @@ |
|||
// vite.config.js |
|||
import { defineConfig } from "file:///E:/hxl/gitFile/activityLink/node_modules/vite/dist/node/index.js"; |
|||
import vue from "file:///E:/hxl/gitFile/activityLink/node_modules/@vitejs/plugin-vue/dist/index.mjs"; |
|||
var vite_config_default = defineConfig({ |
|||
plugins: [ |
|||
vue() |
|||
], |
|||
server: { |
|||
proxy: { |
|||
"/Api": { |
|||
target: "https://dbqb.nfdxy.net/devLotApi", |
|||
// target: 'http://localhost:8080', |
|||
changeOrigin: true, |
|||
rewrite: (path) => path.replace(/^\/Api/, "") |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
export { |
|||
vite_config_default as default |
|||
}; |
|||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJFOlxcXFxoeGxcXFxcZ2l0RmlsZVxcXFxhY3Rpdml0eUxpbmtcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkU6XFxcXGh4bFxcXFxnaXRGaWxlXFxcXGFjdGl2aXR5TGlua1xcXFx2aXRlLmNvbmZpZy5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vRTovaHhsL2dpdEZpbGUvYWN0aXZpdHlMaW5rL3ZpdGUuY29uZmlnLmpzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcbmltcG9ydCB2dWUgZnJvbSAnQHZpdGVqcy9wbHVnaW4tdnVlJ1xuXG4vLyBodHRwczovL3ZpdGUuZGV2L2NvbmZpZy9cbi8vIGV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4vLyAgIHBsdWdpbnM6IFt2dWUoKV0sXG4vLyB9KVxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgdnVlKCksXG4gIF0sXG4gIHNlcnZlcjoge1xuICAgIHByb3h5OiB7XG4gICAgICAnL0FwaSc6IHtcbiAgICAgICAgdGFyZ2V0OiAnaHR0cHM6Ly9kYnFiLm5mZHh5Lm5ldC9kZXZMb3RBcGknLFxuICAgICAgICAvLyB0YXJnZXQ6ICdodHRwOi8vbG9jYWxob3N0OjgwODAnLFxuICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXG4gICAgICAgIHJld3JpdGU6IChwYXRoKSA9PiBwYXRoLnJlcGxhY2UoL15cXC9BcGkvLCAnJylcbiAgICAgIH1cbiAgICB9XG4gIH1cbn0pXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQTJRLFNBQVMsb0JBQW9CO0FBQ3hTLE9BQU8sU0FBUztBQU9oQixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTO0FBQUEsSUFDUCxJQUFJO0FBQUEsRUFDTjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ0wsUUFBUTtBQUFBLFFBQ04sUUFBUTtBQUFBO0FBQUEsUUFFUixjQUFjO0FBQUEsUUFDZCxTQUFTLENBQUMsU0FBUyxLQUFLLFFBQVEsVUFBVSxFQUFFO0FBQUEsTUFDOUM7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== |
@ -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日 |
|||
**修改状态**:✅ 已完成 |
|||
**测试状态**:🔄 待测试 |
@ -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"]` |
@ -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` 接口是否正常返回数据 |