21 changed files with 1872 additions and 7 deletions
-
3.vscode/extensions.json
-
214package-lock.json
-
1package.json
-
BINsrc/assets/beijingtu.jpg
-
BINsrc/assets/daijiemi.png
-
BINsrc/assets/image.png
-
BINsrc/assets/lottery
-
BINsrc/assets/登录.png
-
5src/main.js
-
16src/store/lottery.js
-
73src/utils/config.js
-
163src/views/choujiang/index.vue
-
93src/views/choujiang/lottery/CardItem.vue
-
43src/views/choujiang/lottery/ControlBar.vue
-
503src/views/choujiang/lottery/Lottery3D.vue
-
50src/views/choujiang/lottery/MusicPlayer.vue
-
386src/views/choujiang/lottery/PrizePanel.vue
-
37src/views/choujiang/lottery/Qipao.vue
-
58src/views/choujiang/lottery/UserList.vue
-
153src/views/choujiang/lottery/dataManager.js
-
79src/views/choujiang/lottery/lotteryEngine.js
@ -1,3 +0,0 @@ |
|||
{ |
|||
"recommendations": ["Vue.volar"] |
|||
} |
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 |
@ -1,6 +1,9 @@ |
|||
import { createApp } from 'vue' |
|||
import { createPinia } from 'pinia' |
|||
|
|||
import './style.css' |
|||
import App from './App.vue' |
|||
import router from './router' |
|||
// import { createPinia } from 'pinia'
|
|||
|
|||
createApp(App).use(router).mount('#app') |
|||
createApp(App).use(router).use(createPinia()).mount('#app') |
@ -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 |
|||
} |
|||
}) |
@ -0,0 +1,73 @@ |
|||
// 【2,3】:第3列,第4行
|
|||
export const NUMBER_MATRIX = { |
|||
"0": [ |
|||
[1, 1], [4, 1], [6, 1], [10, 1], [11, 1], [12, 1], [13, 1],[15,1],[16,1],[17,1],[18,1], |
|||
[1,2], [4,2], [6,2], [13,2], [18,2], |
|||
[1,3],[2,3],[3,3],[4,3] ,[6,3], [12,3], [17,3], |
|||
[1,4], [4,4], [6,4], [11,4], [16,4], |
|||
[1,5],[4,5], [6,5],[7,5],[8,5], [10,5],[11,5],[12,5],[13,5], [16,5] |
|||
], |
|||
"1": [ |
|||
[1, 0], [2, 0], |
|||
[0, 1], [1, 1], [2, 1], |
|||
[1, 2], |
|||
[1, 3], |
|||
[1, 4] |
|||
], |
|||
"2": [ |
|||
[0, 0], [1, 0], [2, 0], [3, 0], |
|||
[3, 1], |
|||
[0, 2], [1, 2], [2, 2], [3, 2], |
|||
[0, 3], |
|||
[0, 4], [1, 4], [2, 4], [3, 4] |
|||
], |
|||
"3": [ |
|||
[0, 0], [1, 0], [2, 0], [3, 0], |
|||
[3, 1], |
|||
[0, 2], [1, 2], [2, 2], [3, 2], |
|||
[3, 3], |
|||
[0, 4], [1, 4], [2, 4], [3, 4] |
|||
], |
|||
"4": [ |
|||
[0, 0], [3, 0], |
|||
[0, 1], [3, 1], |
|||
[0, 2], [1, 2], [2, 2], [3, 2], |
|||
[3, 3], |
|||
[3, 4] |
|||
], |
|||
"5": [ |
|||
[0, 0], [1, 0], [2, 0], [3, 0], |
|||
[0, 1], |
|||
[0, 2], [1, 2], [2, 2], [3, 2], |
|||
[3, 3], |
|||
[0, 4], [1, 4], [2, 4], [3, 4] |
|||
], |
|||
"6": [ |
|||
[0, 0], [1, 0], [2, 0], [3, 0], |
|||
[0, 1], |
|||
[0, 2], [1, 2], [2, 2], [3, 2], |
|||
[0, 3], [3, 3], |
|||
[0, 4], [1, 4], [2, 4], [3, 4] |
|||
], |
|||
"7": [ |
|||
[0, 0], [1, 0], [2, 0], [3, 0], |
|||
[3, 1], |
|||
[2, 2], |
|||
[1, 3], |
|||
[0, 4] |
|||
], |
|||
"8": [ |
|||
[0, 0], [1, 0], [2, 0], [3, 0], |
|||
[0, 1], [3, 1], |
|||
[0, 2], [1, 2], [2, 2], [3, 2], |
|||
[0, 3], [3, 3], |
|||
[0, 4], [1, 4], [2, 4], [3, 4] |
|||
], |
|||
"9": [ |
|||
[0, 0], [1, 0], [2, 0], [3, 0], |
|||
[0, 1], [3, 1], |
|||
[0, 2], [1, 2], [2, 2], [3, 2], |
|||
[3, 3], |
|||
[0, 4], [1, 4], [2, 4], [3, 4] |
|||
] |
|||
}; |
@ -1,13 +1,172 @@ |
|||
<template> |
|||
<div> |
|||
抽奖 |
|||
<div class="choujiang-main"> |
|||
<Lottery3D ref="lottery3DRef" /> |
|||
<PrizePanel |
|||
:prizes="dataManager.state.basicData.prizes" |
|||
/> |
|||
<ControlBar |
|||
:lottery-state="lotteryState" |
|||
:is-disabled="isDisabled" |
|||
@lottery-click="handleLotteryClick" |
|||
@reset="handleReset" |
|||
@export="handleExport" |
|||
/> |
|||
<MusicPlayer /> |
|||
<!-- <UserList |
|||
:lucky-users=" |
|||
dataManager.state.basicData.luckyUsers[ |
|||
dataManager.state.currentPrize?.type |
|||
] || [] |
|||
" |
|||
:left-users="dataManager.state.basicData.leftUsers" |
|||
/> --> |
|||
<Qipao :text="qipaoText" :show="showQipao" /> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import Lottery3D from "./lottery/Lottery3D.vue"; |
|||
import PrizePanel from "./lottery/PrizePanel.vue"; |
|||
import ControlBar from "./lottery/ControlBar.vue"; |
|||
import MusicPlayer from "./lottery/MusicPlayer.vue"; |
|||
import Qipao from "./lottery/Qipao.vue"; |
|||
import UserList from "./lottery/UserList.vue"; |
|||
import { ref, onMounted, nextTick, computed, watch } from "vue"; |
|||
import { useDataManager } from "./lottery/dataManager.js"; |
|||
import { useLotteryEngine } from "./lottery/lotteryEngine.js"; |
|||
|
|||
import { useLotteryStore } from "../../store/lottery"; // 路径根据实际情况调整 |
|||
|
|||
const qipaoText = ref(""); |
|||
const showQipao = ref(false); |
|||
|
|||
// const lotteryState = ref('idle'); // idle, ready, rotating, result |
|||
|
|||
// 新增 |
|||
const lotteryStore = useLotteryStore(); |
|||
const lotteryState = computed({ |
|||
get: () => lotteryStore.lotteryState, |
|||
set: (val) => lotteryStore.setLotteryState(val), |
|||
}); |
|||
|
|||
const isDisabled = ref(false); |
|||
|
|||
watch(isDisabled, (newVal, oldVal) => { |
|||
console.log("isDisabled 变化:", oldVal, "->", newVal); |
|||
}); |
|||
|
|||
// 数据与抽奖主流程 |
|||
const dataManager = useDataManager(); |
|||
let lottery3DRef = ref(null); |
|||
const lotteryEngine = useLotteryEngine(dataManager, { |
|||
resetCard: (...args) => lottery3DRef.value?.resetCard?.(...args), |
|||
addHighlight: (...args) => lottery3DRef.value?.addHighlight?.(...args), |
|||
switchScreen: (...args) => lottery3DRef.value?.switchScreen?.(...args), |
|||
rotateBallStart: (...args) => lottery3DRef.value?.rotateBallStart?.(...args), |
|||
rotateBallStop: (...args) => lottery3DRef.value?.rotateBallStop?.(...args), |
|||
selectCard: (...args) => lottery3DRef.value?.selectCard?.(...args), |
|||
}); |
|||
|
|||
onMounted(async () => { |
|||
await dataManager.getBasicData(); |
|||
await dataManager.getUsers(); |
|||
}); |
|||
|
|||
function showLotteryQipao() { |
|||
const luckys = dataManager.state.currentLuckys; |
|||
const prize = dataManager.state.currentPrize; |
|||
if (!luckys || luckys.length === 0) return; |
|||
const names = luckys.map((item) => item[1]).join("、"); |
|||
qipaoText.value = `恭喜${names}获得${prize?.title || ""}!`; |
|||
showQipao.value = true; |
|||
setTimeout(() => { |
|||
showQipao.value = false; |
|||
}, 3000); |
|||
} |
|||
|
|||
async function handleLotteryClick() { |
|||
if (isDisabled.value) return; // 2秒内不能重复点击 |
|||
isDisabled.value = true; |
|||
setTimeout(() => { |
|||
isDisabled.value = false; |
|||
}, 2000); |
|||
|
|||
switch (lotteryState.value) { |
|||
case "idle": |
|||
// 先切换到球体布局 |
|||
console.log("lotteryState 变更前:", lotteryState.value, "-> ready"); |
|||
lotteryState.value = "ready"; |
|||
console.log("lotteryState 变更后:", lotteryState.value); |
|||
await lottery3DRef.value?.switchScreen?.("lottery"); |
|||
break; |
|||
case "ready": |
|||
// 立即切换为“停止抽奖” |
|||
console.log("lotteryState 变更前:", lotteryState.value, "-> rotating"); |
|||
lotteryState.value = "rotating"; |
|||
console.log("lotteryState 变更后:", lotteryState.value); |
|||
// 开始转动 |
|||
// isRunning.value = false; // 无论如何都恢复 |
|||
|
|||
await lottery3DRef.value?.rotateBallStart?.(); |
|||
break; |
|||
case "rotating": |
|||
// 停止转动并开奖 |
|||
await lottery3DRef.value?.rotateBallStop?.(); |
|||
await lotteryEngine.executeLottery(); |
|||
await nextTick(); |
|||
showLotteryQipao(); |
|||
console.log("lotteryState 变更前:", lotteryState.value, "-> idle"); |
|||
lotteryState.value = "result"; |
|||
|
|||
console.log("lotteryState 变更后:", lotteryState.value); |
|||
|
|||
break; |
|||
case "result": |
|||
// result 状态下点击不做任何事,或者你可以加提示 |
|||
await lottery3DRef.value?.switchScreen?.("lottery"); |
|||
lotteryState.value = "ready"; |
|||
|
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
function handleReset() { |
|||
lotteryEngine.resetLottery(); |
|||
} |
|||
function handleExport() { |
|||
dataManager.exportData(); |
|||
} |
|||
|
|||
function handlePrevPrize() { |
|||
if (dataManager.state.currentPrizeIndex > 0) { |
|||
dataManager.state.currentPrizeIndex--; |
|||
dataManager.state.currentPrize = |
|||
dataManager.state.basicData.prizes[dataManager.state.currentPrizeIndex]; |
|||
} |
|||
} |
|||
function handleNextPrize() { |
|||
if ( |
|||
dataManager.state.currentPrizeIndex < |
|||
dataManager.state.basicData.prizes.length - 1 |
|||
) { |
|||
dataManager.state.currentPrizeIndex++; |
|||
dataManager.state.currentPrize = |
|||
dataManager.state.basicData.prizes[dataManager.state.currentPrizeIndex]; |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.choujiang-main { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
position: relative; |
|||
overflow: hidden; |
|||
/* 添加背景图片 */ |
|||
background: url('../../assets/登录.png') ; |
|||
background-size: 1920px 980px; |
|||
|
|||
} |
|||
</style> |
@ -0,0 +1,93 @@ |
|||
<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', |
|||
// width: '68px', |
|||
// height: '88px', |
|||
|
|||
}; |
|||
} |
|||
// return { |
|||
// backgroundColor: 'rgb(226, 64, 35)', |
|||
// // backgroundColor: 'rgb(226, 164, 35)', |
|||
// width: '125px', |
|||
// height: '170px', |
|||
// }; |
|||
} |
|||
return { |
|||
// background: 'linear-gradient(135deg,rgba(255, 170, 22, 100) 0%, rgba(255, 170, 22, 100) 100%)', |
|||
// backgroundColor:'rgba(255, 170, 22, 100)', |
|||
backgroundColor:'rgba(254, 177, 48, 100)', |
|||
width: '130px', |
|||
height: '170px', |
|||
// width: '68px', |
|||
// height: '88px', |
|||
border: '1px solid rgb(255,255,255)', |
|||
}; |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.element { |
|||
transition: background-Color 2s; |
|||
/* 你的基础样式 */ |
|||
} |
|||
.lightitem { |
|||
/* 高亮样式 */ |
|||
} |
|||
.highlight { |
|||
/* 响应式高亮样式 */ |
|||
} |
|||
.prize { |
|||
/* 中奖样式 */ |
|||
} |
|||
.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,503 @@ |
|||
<template> |
|||
<div class="lottery-3d-container"> |
|||
<div ref="threeContainer" class="three-container"></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 |
|||
|
|||
// 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; |
|||
let tag = -(currentLuckys.length - 1) / 2; |
|||
const locates = []; |
|||
|
|||
// 计算位置信息, 大于5个分两排显示 |
|||
if (currentLuckys.length > 5) { |
|||
const yPosition = [-87, 87]; |
|||
const l = selectedCardIndex.length; |
|||
const mid = Math.ceil(l / 2); |
|||
tag = -(mid - 1) / 2; |
|||
|
|||
for (let i = 0; i < mid; i++) { |
|||
locates.push({ |
|||
x: tag * width, |
|||
y: yPosition[0], |
|||
}); |
|||
tag++; |
|||
} |
|||
|
|||
tag = -(l - mid - 1) / 2; |
|||
for (let i = mid; i < l; i++) { |
|||
locates.push({ |
|||
x: tag * width, |
|||
y: yPosition[1], |
|||
}); |
|||
tag++; |
|||
} |
|||
} else { |
|||
for (let i = selectedCardIndex.length; i > 0; i--) { |
|||
locates.push({ |
|||
x: tag * width, |
|||
y: 0, |
|||
}); |
|||
tag++; |
|||
} |
|||
} |
|||
|
|||
console.log("locates calculated:", locates); |
|||
|
|||
selectedCardIndex.forEach((cardIndex, index) => { |
|||
// console.log("animating card:", cardIndex, "to position:", locates[index]); |
|||
changeCard(cardIndex, currentLuckys[index]); |
|||
const object = threeDCards[cardIndex]; |
|||
|
|||
new TWEEN.Tween(object.position) |
|||
.to( |
|||
{ |
|||
x: locates[index].x, |
|||
y: locates[index].y, |
|||
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"); |
|||
}); |
|||
|
|||
new TWEEN.Tween({}) |
|||
.to({}, duration * 2) |
|||
.onUpdate(() => render()) |
|||
.start() |
|||
.onComplete(() => { |
|||
console.log("selectCard animation completed"); |
|||
resolve(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
function resetCard(selectedCardIndex, duration = 500) { |
|||
if (!selectedCardIndex || selectedCardIndex.length === 0) { |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
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) { |
|||
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>`; |
|||
} |
|||
|
|||
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, "郑二二"], |
|||
]; |
|||
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); |
|||
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, |
|||
}); |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.lottery-3d-container { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
overflow: hidden; |
|||
position: relative; |
|||
} |
|||
.three-container { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
</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,386 @@ |
|||
<template> |
|||
<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> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import { ref, computed, nextTick } from "vue"; |
|||
const props = defineProps({ |
|||
prizes: Array, |
|||
}); |
|||
// 新增:控制已揭秘奖品数量 |
|||
const revealedCount = ref(0); |
|||
// 新增:记录最新揭秘的奖品索引 |
|||
const lastRevealedIdx = ref(-1); |
|||
// 计算哪些奖品已揭秘 |
|||
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; |
|||
} |
|||
function closeWinnerList() { |
|||
showWinnerList.value = false; |
|||
} |
|||
|
|||
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; |
|||
modalTop.value = rect.bottom + 4; // 4px间距 |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
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,153 @@ |
|||
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 = [10, 10, 10]; |
|||
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, "郑二二"], |
|||
]; |
|||
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, "郑二二"], |
|||
]; |
|||
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