Browse Source
Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into hongxilin/feature-20250710175148-抽奖
songtongtong/feature-20250717104937-众筹
Merge branch 'milestone-20250722-抽奖' of http://39.101.133.168:8807/hongxilin/activityLink into hongxilin/feature-20250710175148-抽奖
songtongtong/feature-20250717104937-众筹
20 changed files with 2962 additions and 317 deletions
-
3.vscode/extensions.json
-
551package-lock.json
-
1package.json
-
588src/assets/PrizePanel1.vue
-
BINsrc/assets/beijingtu.jpg
-
BINsrc/assets/daijiemi.png
-
BINsrc/assets/image.png
-
BINsrc/assets/lottery
-
BINsrc/assets/登录.png
-
16src/store/lottery.js
-
167src/views/choujiang/index.vue
-
102src/views/choujiang/lottery/CardItem.vue
-
43src/views/choujiang/lottery/ControlBar.vue
-
843src/views/choujiang/lottery/Lottery3D.vue
-
50src/views/choujiang/lottery/MusicPlayer.vue
-
510src/views/choujiang/lottery/PrizePanel.vue
-
37src/views/choujiang/lottery/Qipao.vue
-
58src/views/choujiang/lottery/UserList.vue
-
229src/views/choujiang/lottery/dataManager.js
-
79src/views/choujiang/lottery/lotteryEngine.js
@ -1,3 +0,0 @@ |
|||
{ |
|||
"recommendations": ["Vue.volar"] |
|||
} |
551
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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: 766 | Height: 220 | Size: 144 KiB |
After Width: 664 | Height: 199 | Size: 83 KiB |
After Width: 1920 | Height: 1080 | Size: 1.2 MiB |
@ -0,0 +1,16 @@ |
|||
// 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
|
|||
|
|||
function setLotteryState(state) { |
|||
lotteryState.value = state |
|||
} |
|||
|
|||
return { |
|||
lotteryState, |
|||
setLotteryState |
|||
} |
|||
}) |
@ -1,13 +1,176 @@ |
|||
<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 /> |
|||
<!-- <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 { ref, onMounted, nextTick, computed, watch } from "vue"; |
|||
import { useDataManager } from "./lottery/dataManager.js"; |
|||
import { useLotteryEngine } from "./lottery/lotteryEngine.js"; |
|||
|
|||
import { useLotteryStore } from "../../store/lottery"; // 路径根据实际情况调整 |
|||
|
|||
const qipaoText = ref(""); |
|||
const showQipao = 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 isDisabled = ref(false); |
|||
|
|||
watch(isDisabled, (newVal, oldVal) => { |
|||
console.log("isDisabled 变化:", oldVal, "->", newVal); |
|||
}); |
|||
|
|||
// 数据与抽奖主流程 |
|||
const dataManager = useDataManager(); |
|||
let lottery3DRef = 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 () => { |
|||
await dataManager.getBasicData(); |
|||
await dataManager.getUsers(); |
|||
}); |
|||
|
|||
function showLotteryQipao() { |
|||
const luckys = dataManager.state.currentLuckys; |
|||
const prize = dataManager.state.currentPrize; |
|||
if (!luckys || luckys.length === 0) return; |
|||
const names = luckys.map((item) => item[1]).join("、"); |
|||
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": |
|||
// 先切换到球体布局 |
|||
console.log("lotteryState 变更前:", lotteryState.value, "-> ready"); |
|||
lotteryState.value = "ready"; |
|||
console.log("lotteryState 变更后:", lotteryState.value); |
|||
await lottery3DRef.value?.switchScreen?.("lottery"); |
|||
break; |
|||
case "ready": |
|||
// 立即切换为“停止抽奖” |
|||
console.log("lotteryState 变更前:", lotteryState.value, "-> rotating"); |
|||
lotteryState.value = "rotating"; |
|||
console.log("lotteryState 变更后:", lotteryState.value); |
|||
// 开始转动 |
|||
// isRunning.value = false; // 无论如何都恢复 |
|||
|
|||
await lottery3DRef.value?.rotateBallStart?.(); |
|||
break; |
|||
case "rotating": |
|||
// 停止转动并开奖 |
|||
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": |
|||
// result 状态下点击不做任何事,或者你可以加提示 |
|||
await lottery3DRef.value?.switchScreen?.("lottery"); |
|||
|
|||
// 去除高光 |
|||
lottery3DRef.value?.changeCard1?.(); |
|||
|
|||
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: relative; |
|||
overflow: hidden; |
|||
/* 添加背景图片 */ |
|||
background: url('../../assets/登录.png') ; |
|||
background-size: 1920px 980px; |
|||
|
|||
} |
|||
</style> |
@ -0,0 +1,102 @@ |
|||
<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">{{ (user[0] || "") + "\n" + (user[2] || "") }}</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { computed } 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 cardStyle = computed(() => { |
|||
if (props.isBold && props.showTable) { |
|||
if (lotteryState.value === "idle") { |
|||
return { |
|||
// backgroundColor: "rgba(226, 60, 38, 1)", |
|||
background: 'linear-gradient(135deg, rgba(243,153,38,0.7) 0%, rgba(207,56,35,1) 100%)', |
|||
width: "130px", |
|||
height: "170px", |
|||
}; |
|||
} |
|||
} |
|||
// return { |
|||
// // background: 'linear-gradient(135deg,rgba(255, 170, 22, 100) 0%, rgba(255, 170, 22, 100) 100%)', |
|||
// backgroundColor:'rgba(254, 177, 48, 100)', |
|||
// width: '130px', |
|||
// height: '170px', |
|||
// border: '1px solid rgb(255,255,255)', |
|||
// }; |
|||
if (lotteryState.value === "result") { |
|||
return { |
|||
background: 'linear-gradient(135deg,rgba(255, 170, 22, 100) 0%, rgba(255, 170, 22, 100) 100%)', |
|||
// backgroundColor: "rgba(254, 177, 48, 100)", |
|||
width: "130px", |
|||
height: "170px", |
|||
border: "1px solid rgb(255,255,255)", |
|||
}; |
|||
} else { |
|||
return { |
|||
backgroundColor: "rgba(254, 177, 48, 100)", |
|||
width: "130px", |
|||
height: "170px", |
|||
border: "1px solid rgb(255,255,255)", |
|||
}; |
|||
} |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.element { |
|||
transition: background 2s; |
|||
/* 你的基础样式 */ |
|||
} |
|||
.lightitem { |
|||
/* 高亮样式 */ |
|||
} |
|||
.highlight { |
|||
/* 响应式高亮样式 */ |
|||
} |
|||
.prize { |
|||
/* 中奖样式 */ |
|||
background-color: #fc0202; |
|||
} |
|||
.company { |
|||
/* ... */ |
|||
} |
|||
.name { |
|||
/* ... */ |
|||
} |
|||
.details { |
|||
font-size: 30px; |
|||
text-align: center; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
height: 100%; |
|||
} |
|||
</style> |
@ -0,0 +1,43 @@ |
|||
<template> |
|||
<div class="control-bar"> |
|||
<button :disabled="isDisabled" |
|||
@click="$emit('lottery-click')"> |
|||
{{ 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; |
|||
} |
|||
button { |
|||
padding: 10px 24px; |
|||
font-size: 18px; |
|||
border-radius: 6px; |
|||
border: none; |
|||
background: #0078ff; |
|||
color: #fff; |
|||
cursor: pointer; |
|||
transition: background 0.2s; |
|||
} |
|||
button:hover { |
|||
background: #005bb5; |
|||
} |
|||
</style> |
@ -0,0 +1,843 @@ |
|||
<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 } 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"; |
|||
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 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 -= 400; // 向下偏移10px// object.position.y -= 20; // yOffset 是你想要的偏移量 |
|||
vector.copy(object.position).multiplyScalar(2); |
|||
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]; |
|||
|
|||
new TWEEN.Tween(object.position) |
|||
.to( |
|||
{ |
|||
x: isVisible ? pageLocate.x : pageLocate.x, |
|||
y: isVisible ? pageLocate.y : pageLocate.y + 1000, // 隐藏的卡片移到下方 |
|||
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; |
|||
} |
|||
|
|||
// 动画切换卡片位置 |
|||
globalCardIndexes.forEach((cardIndex, index) => { |
|||
const object = threeDCards[cardIndex]; |
|||
const cardPage = Math.floor(index / cardsPerPage); |
|||
const isVisible = cardPage === newPage; |
|||
|
|||
// 计算在当前页中的索引 |
|||
const pageIndex = index % cardsPerPage; |
|||
const pageLocate = pageLocates[cardPage][pageIndex]; |
|||
|
|||
new TWEEN.Tween(object.position) |
|||
.to( |
|||
{ |
|||
x: pageLocate.x, |
|||
y: isVisible ? pageLocate.y : pageLocate.y + 1000, |
|||
z: 2200, |
|||
}, |
|||
duration |
|||
) |
|||
.easing(TWEEN.Easing.Cubic.InOut) |
|||
.start(); |
|||
}); |
|||
|
|||
new TWEEN.Tween({}) |
|||
.to({}, duration) |
|||
.onUpdate(() => render()) |
|||
.onComplete(() => { |
|||
currentPage.value = newPage; |
|||
isPageTransitioning = false; |
|||
console.log(`切换到第 ${currentPage.value + 1} 页,共 ${totalPages.value} 页`); |
|||
}) |
|||
.start(); |
|||
} |
|||
|
|||
|
|||
// function switchPage(direction) { |
|||
// if (isPageTransitioning || totalPages.value <= 1) return; |
|||
|
|||
// const newPage = direction === 'next' ? currentPage.value + 1 : currentPage.value - 1; |
|||
// if (newPage < 0 || newPage >= totalPages.value) return; |
|||
|
|||
// isPageTransitioning = true; |
|||
// const duration = 800; |
|||
|
|||
// // 使用保存的页面位置信息 |
|||
// const pageLocates = window.pageLocates; |
|||
// if (!pageLocates) { |
|||
// console.error('页面位置信息未找到'); |
|||
// isPageTransitioning = false; |
|||
// return; |
|||
// } |
|||
|
|||
// // 动画切换卡片位置 |
|||
// globalCardIndexes.forEach((cardIndex, index) => { |
|||
// const object = threeDCards[cardIndex]; |
|||
// const cardPage = Math.floor(index / cardsPerPage); |
|||
// const isVisible = cardPage === newPage; |
|||
|
|||
// // 计算在当前页中的索引 |
|||
// const pageIndex = index % cardsPerPage; |
|||
// const pageLocate = pageLocates[cardPage][pageIndex]; |
|||
|
|||
// // 修改目标位置以实现从上方或下方飞出的效果 |
|||
// new TWEEN.Tween(object.position) |
|||
// .to( |
|||
// { |
|||
// x: pageLocate.x, |
|||
// y: isVisible ? pageLocate.y : (direction === 'next' ? pageLocate.y - 1000 : pageLocate.y + 1000), // 向下翻页时从上方下落,向上翻页时从下方飞出 |
|||
// z: 2200, |
|||
// }, |
|||
// duration |
|||
// ) |
|||
// .easing(TWEEN.Easing.Cubic.InOut) |
|||
// .start(); |
|||
// }); |
|||
|
|||
// new TWEEN.Tween({}) |
|||
// .to({}, duration) |
|||
// .onUpdate(() => render()) |
|||
// .onComplete(() => { |
|||
// currentPage.value = newPage; |
|||
// isPageTransitioning = false; |
|||
// console.log(`切换到第 ${currentPage.value + 1} 页,共 ${totalPages.value} 页`); |
|||
// }) |
|||
// .start(); |
|||
// } |
|||
|
|||
// 鼠标滚轮事件处理 |
|||
function handleWheel(event) { |
|||
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; |
|||
} |
|||
|
|||
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]; |
|||
object.element.classList.remove("prize"); |
|||
}); |
|||
resolve(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
function changeCard(cardIndex, user) { |
|||
// 保存到全局变量数组 |
|||
if (!globalCardIndexes.includes(cardIndex)) { |
|||
globalCardIndexes.push(cardIndex); |
|||
} |
|||
|
|||
const card = threeDCards[cardIndex].element; |
|||
// card.innerHTML = `<div class="company">${ |
|||
// user.company || "" |
|||
// }</div><div class="name">${user[1]}</div><div class="details">${ |
|||
// user[0] || "" |
|||
// }<br/>${user[2] || "PSST"}</div>`; |
|||
|
|||
card.style.setProperty('background-color', '#ffffff', 'important'); |
|||
|
|||
// card.style.backgroundColor = '#ffffff'; |
|||
|
|||
// card.style.backgroundColor = ''; |
|||
|
|||
|
|||
} |
|||
|
|||
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, '的高光'); |
|||
card.style.setProperty('background-color', 'rgba(254, 177, 48, 1)', 'important'); |
|||
|
|||
// 移除prize类,因为CSS规则会覆盖backgroundColor |
|||
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(() => { |
|||
// 初始化 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为例) |
|||
const highlightCells = NUMBER_MATRIX["0"].map(([x, y]) => `${x}-${y}`); |
|||
const config = { |
|||
ROW_COUNT: 7, // 数字矩阵是5行 |
|||
COLUMN_COUNT: 20, // 数字矩阵是4列 |
|||
HIGHLIGHT_CELL: highlightCells, |
|||
COMPANY: "演示公司", |
|||
}; |
|||
const member = [ |
|||
[0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], |
|||
[0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], |
|||
[0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], |
|||
[0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], |
|||
]; |
|||
const length = member.length; |
|||
const showTable = true; |
|||
const position = { |
|||
x: (100 * config.COLUMN_COUNT - 20) / 2, |
|||
y: (160 * config.ROW_COUNT - 20) / 2, |
|||
}; |
|||
createCards(member, length, showTable, position, config); // 3. 传递高亮配置 |
|||
createSphereTargets(); |
|||
|
|||
// 先渲染散落状态 |
|||
render(); |
|||
animate(); |
|||
|
|||
// 延迟后自动聚集到界面中间 |
|||
setTimeout(() => { |
|||
switchScreen("enter"); |
|||
}, 500); |
|||
|
|||
window.addEventListener("resize", onWindowResize); |
|||
}); |
|||
|
|||
onBeforeUnmount(() => { |
|||
window.removeEventListener("resize", onWindowResize); |
|||
removeWheelListener(); // 移除鼠标滚轮事件监听器 |
|||
if (animationId) cancelAnimationFrame(animationId); |
|||
if (highlightTimeout) { |
|||
clearTimeout(highlightTimeout); |
|||
highlightTimeout = 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; |
|||
} |
|||
|
|||
@keyframes fadeInOut { |
|||
0%, 100% { opacity: 0.6; } |
|||
50% { opacity: 1; } |
|||
} |
|||
</style> |
@ -0,0 +1,50 @@ |
|||
<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 } from 'vue'; |
|||
const musicSrc = '/src/assets/lottery/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; |
|||
} |
|||
} |
|||
</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,510 @@ |
|||
<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 }" |
|||
@click="handleReveal(idx)" |
|||
style="cursor: 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"> |
|||
{{ 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> |
|||
<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 |
|||
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">Homily ID</div> |
|||
<ul class="winner-list"> |
|||
<li v-for="(user, idx) in fakeWinners" :key="idx"> |
|||
<!-- <span>{{ user.id }}</span> - <span>{{ user.name }}</span> - --> |
|||
<span>{{ user.id }}</span> |
|||
<span>{{ user.prize }}</span> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div v-else> |
|||
<div class="prize-panel-root"> |
|||
<div class="prize-panel-list" 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"> |
|||
{{ prizes[lastRevealedIdx].count - getLeftCount(prizes[lastRevealedIdx]) }}/{{ prizes[lastRevealedIdx].count }} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div></div> |
|||
<div class="prize-panel-footer"> |
|||
<div class="arrow-up" @click="openWinnerList"></div> |
|||
<button |
|||
ref="winnerBtnRef" |
|||
class="winner-btn" |
|||
@click="toggleWinnerList" |
|||
> |
|||
获奖名单 |
|||
</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-title">Homily ID</div> |
|||
<ul class="winner-list"> |
|||
<li v-for="(user, idx) in fakeWinners" :key="idx"> |
|||
<!-- <span>{{ user.id }}</span> - <span>{{ user.name }}</span> - --> |
|||
<span>{{ user.id }}</span> |
|||
<span>{{ user.prize }}</span> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, computed, nextTick } from "vue"; |
|||
const props = defineProps({ |
|||
prizes: Array, |
|||
}); |
|||
// 新增:控制已揭秘奖品数量 |
|||
const revealedCount = ref(0); |
|||
// 新增:记录最新揭秘的奖品索引 |
|||
const lastRevealedIdx = ref(-1); |
|||
|
|||
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 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: "六等奖" }, |
|||
]); |
|||
function openWinnerList() { |
|||
showWinnerList.value = true; |
|||
// 当存在最新揭秘的奖品时,点击获奖名单将showOne设置为false |
|||
if (lastRevealedIdx.value >= 0) { |
|||
showOne.value = false; |
|||
} |
|||
// 设置弹窗位置 |
|||
nextTick(() => { |
|||
const btn = winnerBtnRef.value; |
|||
if (btn) { |
|||
const rect = btn.getBoundingClientRect(); |
|||
modalLeft.value = rect.left - 23; |
|||
modalTop.value = rect.bottom + 18; // 4px间距 |
|||
} |
|||
}); |
|||
} |
|||
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); |
|||
|
|||
function toggleWinnerList() { |
|||
showWinnerList.value = !showWinnerList.value; |
|||
console.log('toggleWinnerList - showWinnerList:', showWinnerList.value, 'showOne:', showOne.value, 'lastRevealedIdx:', lastRevealedIdx.value); |
|||
|
|||
if (showWinnerList.value) { |
|||
// 当存在最新揭秘的奖品时,点击获奖名单将showOne设置为false |
|||
if (lastRevealedIdx.value >= 0) { |
|||
showOne.value = false; |
|||
console.log('设置 showOne 为 false'); |
|||
} |
|||
// 设置弹窗位置 |
|||
nextTick(() => { |
|||
const btn = winnerBtnRef.value; |
|||
if (btn) { |
|||
const rect = btn.getBoundingClientRect(); |
|||
modalLeft.value = rect.left - 23; |
|||
modalTop.value = rect.bottom + 18; // 4px间距 |
|||
} |
|||
}); |
|||
} 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); |
|||
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; |
|||
} |
|||
.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; |
|||
} |
|||
.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; |
|||
bottom: 0; |
|||
width: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
z-index: 20; |
|||
/* 移除 move-up 相关 */ |
|||
} |
|||
.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); |
|||
} |
|||
.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; |
|||
padding-top: 12px; |
|||
min-width: 280px; |
|||
max-width: 90vw; |
|||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12); |
|||
position: relative; |
|||
} |
|||
.winner-modal-title { |
|||
font-size: 22px; |
|||
font-weight: bold; |
|||
margin-bottom: 18px; |
|||
text-align: center; |
|||
} |
|||
.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; |
|||
} |
|||
.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; |
|||
} |
|||
</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,58 @@ |
|||
<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> |
|||
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,229 @@ |
|||
import { reactive } from 'vue'; |
|||
|
|||
export function useDataManager() { |
|||
const state = reactive({ |
|||
basicData: { |
|||
prizes: [], |
|||
users: [], |
|||
luckyUsers: {}, |
|||
leftUsers: [] |
|||
}, |
|||
currentPrizeIndex: 0, |
|||
currentPrize: null, |
|||
currentLuckys: [], |
|||
isLotting: false, |
|||
config: { |
|||
prizes: [], |
|||
EACH_COUNT: [], |
|||
COMPANY: '', |
|||
HIGHLIGHT_CELL: [], |
|||
Resolution: 1 |
|||
} |
|||
}); |
|||
|
|||
async function getBasicData() { |
|||
// 假数据,后续可替换为接口
|
|||
const fakePrizes = [ |
|||
{ type: 0, title: '特别奖', text: 'iPad', count: 10, img: '/src/assets/lottery/ipad.jpg' }, |
|||
{ type: 1, title: '一等奖', text: 'Kindle', count: 20, img: '/src/assets/lottery/kindle.jpg' }, |
|||
{ type: 2, title: '二等奖', text: 'MacBook Pro', count: 10, img: '/src/assets/lottery/mbp.jpg' } |
|||
]; |
|||
const fakeEachCount = [15, 17, 36]; |
|||
const fakeCompany = '前端假公司'; |
|||
const fakeLuckyData = {}; |
|||
fakePrizes.forEach(prize => { |
|||
fakeLuckyData[prize.type] = []; |
|||
}); |
|||
const fakeLeftUsers = [ |
|||
[0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], [0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], [0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], |
|||
]; |
|||
state.config.prizes = fakePrizes; |
|||
state.config.EACH_COUNT = fakeEachCount; |
|||
state.config.COMPANY = fakeCompany; |
|||
state.config.HIGHLIGHT_CELL = []; |
|||
state.basicData.prizes = fakePrizes; |
|||
state.basicData.leftUsers = fakeLeftUsers.slice(); |
|||
state.basicData.luckyUsers = fakeLuckyData; |
|||
determineCurrentPrize(); |
|||
return Promise.resolve({ |
|||
cfgData: { |
|||
prizes: fakePrizes, |
|||
EACH_COUNT: fakeEachCount, |
|||
COMPANY: fakeCompany |
|||
}, |
|||
leftUsers: fakeLeftUsers.slice(), |
|||
luckyData: fakeLuckyData |
|||
}); |
|||
} |
|||
|
|||
async function getUsers() { |
|||
const fakeUsers = [ |
|||
[0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], [0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], [0, "张三"], |
|||
[1, "李四"], |
|||
[2, "王五"], |
|||
[3, "赵六"], |
|||
[4, "孙七"], |
|||
[5, "周八"], |
|||
[6, "吴九"], |
|||
[7, "郑十"], |
|||
[8, "钱十一"], |
|||
[9, "孙十二"], |
|||
[10, "李十三"], |
|||
[11, "周十四"], |
|||
[12, "吴十五"], |
|||
[13, "郑十六"], |
|||
[14, "钱十七"], |
|||
[15, "孙十八"], |
|||
[16, "李十九"], |
|||
[17, "周二十"], |
|||
[18, "吴二一"], |
|||
[19, "郑二二"], |
|||
]; |
|||
state.basicData.users = fakeUsers; |
|||
return Promise.resolve(fakeUsers); |
|||
} |
|||
|
|||
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(); } |
|||
|
|||
return { |
|||
state, |
|||
getBasicData, |
|||
getUsers, |
|||
saveData, |
|||
setErrorData, |
|||
exportData, |
|||
resetData, |
|||
getTotalCards, |
|||
setLotteryStatus, |
|||
resetAllData, |
|||
updateCurrentPrize |
|||
}; |
|||
} |
@ -0,0 +1,79 @@ |
|||
import { ref } from 'vue'; |
|||
import { useLotteryStore } from '../../../store/lottery' // 路径根据实际情况调整
|
|||
|
|||
function getRandomInt(max) { |
|||
return Math.floor(Math.random() * max); |
|||
} |
|||
|
|||
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 perCount = dataManager.state.config.EACH_COUNT[dataManager.state.currentPrizeIndex] || 1; |
|||
const totalCards = renderer3D.getTotalCards ? renderer3D.getTotalCards() : 50; |
|||
const leftUsers = dataManager.state.basicData.leftUsers; |
|||
let selectedCardIndex = []; |
|||
let currentLuckys = []; |
|||
let leftCount = leftUsers.length; |
|||
|
|||
console.log('executeLottery - perCount:', perCount, 'leftCount:', leftCount, 'totalCards:', totalCards); |
|||
|
|||
// 随机抽取中奖用户和卡片索引
|
|||
for (let i = 0; i < perCount && leftCount > 0; i++) { |
|||
const luckyId = getRandomInt(leftCount); |
|||
currentLuckys.push(leftUsers.splice(luckyId, 1)[0]); |
|||
leftCount--; |
|||
let cardIndex = getRandomInt(totalCards); |
|||
while (selectedCardIndex.includes(cardIndex)) { |
|||
cardIndex = getRandomInt(totalCards); |
|||
} |
|||
selectedCardIndex.push(cardIndex); |
|||
} |
|||
|
|||
console.log('executeLottery - selectedCardIndex:', selectedCardIndex, 'currentLuckys:', currentLuckys); |
|||
|
|||
dataManager.state.currentLuckys = currentLuckys; |
|||
// 展示中奖动画
|
|||
console.log('executeLottery - calling selectCard'); |
|||
await renderer3D.selectCard?.(selectedCardIndex, currentLuckys); |
|||
console.log('executeLottery - selectCard completed'); |
|||
dataManager.setLotteryStatus(false); |
|||
isLotting.value = false; |
|||
} |
|||
|
|||
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(); |
|||
renderer3D.switchScreen && renderer3D.switchScreen('enter'); |
|||
} |
|||
|
|||
return { |
|||
isLotting, |
|||
executeLottery, |
|||
performLottery, |
|||
changePrize, |
|||
resetLottery |
|||
}; |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue