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
-
165src/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 { createApp } from 'vue' |
||||
|
import { createPinia } from 'pinia' |
||||
|
|
||||
import './style.css' |
import './style.css' |
||||
import App from './App.vue' |
import App from './App.vue' |
||||
import router from './router' |
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> |
<template> |
||||
<div> |
|
||||
抽奖 |
|
||||
</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> |
</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"); |
||||
|
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,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