8 changed files with 1563 additions and 496 deletions
-
614package-lock.json
-
2package.json
-
588src/assets/PrizePanel1.vue
-
6src/views/choujiang/index.vue
-
87src/views/choujiang/lottery/CardItem.vue
-
428src/views/choujiang/lottery/Lottery3D.vue
-
252src/views/choujiang/lottery/PrizePanel.vue
-
78src/views/choujiang/lottery/dataManager.js
614
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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue