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> |
<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> |
</div> |
||||
</template> |
</template> |
||||
|
|
||||
<script setup> |
<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> |
</script> |
||||
|
|
||||
<style scoped> |
<style scoped> |
||||
|
.choujiang-main { |
||||
|
width: 100vw; |
||||
|
height: 100vh; |
||||
|
position: relative; |
||||
|
overflow: hidden; |
||||
|
/* 添加背景图片 */ |
||||
|
background: url('../../assets/登录.png') ; |
||||
|
background-size: 1920px 980px; |
||||
|
|
||||
|
} |
||||
</style> |
</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