Files
log-lottery/src/views/Home/useViewModel.ts
2025-09-30 22:47:36 +08:00

607 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Material, Object3D } from 'three'
import type { TargetType } from './type'
import type { IPersonConfig } from '@/types/storeType'
import * as TWEEN from '@tweenjs/tween.js'
import { storeToRefs } from 'pinia'
import { PerspectiveCamera, Scene } from 'three'
import { CSS3DObject, CSS3DRenderer } from 'three-css3d'
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
import { useToast } from 'vue-toast-notification'
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { selectCard } from '@/utils'
import { rgba } from '@/utils/color'
import { LotteryStatus } from './type'
import { confettiFire, createSphereVertices, createTableVertices, initTableData } from './util'
export function useViewModel() {
const toast = useToast()
// store里面存储的值
const { personConfig, globalConfig, prizeConfig } = useStore()
const {
getAllPersonList: allPersonList,
getNotPersonList: notPersonList,
getNotThisPrizePersonList: notThisPrizePersonList,
} = storeToRefs(personConfig)
const { getCurrentPrize: currentPrize } = storeToRefs(prizeConfig)
const { getCardColor: cardColor, getPatterColor: patternColor, getPatternList: patternList, getTextColor: textColor, getLuckyColor: luckyColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getIsShowAvatar: isShowAvatar } = storeToRefs(globalConfig)
// three初始值
const ballRotationY = ref(0)
const containerRef = ref<HTMLElement>()
const canOperate = ref(true)
const cameraZ = ref(3000)
const scene = ref()
const camera = ref()
const renderer = ref()
const controls = ref()
const objects = ref<any[]>([])
const targets: TargetType = {
grid: [],
helix: [],
table: [],
sphere: [],
}
// 页面数据初始值
const currentStatus = ref<LotteryStatus>(LotteryStatus.init) // 0为初始状态 1为抽奖准备状态2为抽奖中状态3为抽奖结束状态
const tableData = ref<any[]>([])
const luckyTargets = ref<any[]>([])
const luckyCardList = ref<number[]>([])
const luckyCount = ref(10)
const personPool = ref<IPersonConfig[]>([])
const intervalTimer = ref<any>(null)
function init() {
const felidView = 40
const width = window.innerWidth
const height = window.innerHeight
const aspect = width / height
const nearPlane = 1
const farPlane = 10000
const WebGLoutput = containerRef.value
scene.value = new Scene()
camera.value = new PerspectiveCamera(felidView, aspect, nearPlane, farPlane)
camera.value.position.z = cameraZ.value
renderer.value = new CSS3DRenderer()
renderer.value.setSize(width, height * 0.9)
renderer.value.domElement.style.position = 'absolute'
// 垂直居中
renderer.value.domElement.style.paddingTop = '50px'
renderer.value.domElement.style.top = '50%'
renderer.value.domElement.style.left = '50%'
renderer.value.domElement.style.transform = 'translate(-50%, -50%)'
WebGLoutput!.appendChild(renderer.value.domElement)
controls.value = new TrackballControls(camera.value, renderer.value.domElement)
controls.value.rotateSpeed = 1
controls.value.staticMoving = true
controls.value.minDistance = 500
controls.value.maxDistance = 6000
controls.value.addEventListener('change', render)
const tableLen = tableData.value.length
for (let i = 0; i < tableLen; i++) {
let element = document.createElement('div')
element.className = 'element-card'
const number = document.createElement('div')
number.className = 'card-id'
number.textContent = tableData.value[i].uid
if (isShowAvatar.value)
number.style.display = 'none'
element.appendChild(number)
const symbol = document.createElement('div')
symbol.className = 'card-name'
symbol.textContent = tableData.value[i].name
if (isShowAvatar.value)
symbol.className = 'card-name card-avatar-name'
element.appendChild(symbol)
const detail = document.createElement('div')
detail.className = 'card-detail'
detail.innerHTML = `${tableData.value[i].department}<br/>${tableData.value[i].identity}`
if (isShowAvatar.value)
detail.style.display = 'none'
element.appendChild(detail)
const avatar = document.createElement('img')
avatar.className = 'card-avatar'
avatar.src = tableData.value[i].avatar
avatar.alt = 'avatar'
avatar.style.width = '140px'
avatar.style.height = '140px'
if (!isShowAvatar.value)
avatar.style.display = 'none'
element.appendChild(avatar)
element = useElementStyle(element, tableData.value[i], i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value)
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.value.add(object)
objects.value.push(object)
}
// 创建横铺的界面
const tableVertices = createTableVertices({ tableData: tableData.value, rowCount: rowCount.value, cardSize: cardSize.value })
targets.table = tableVertices
// 创建球体
const sphereVertices = createSphereVertices({ objectsLength: objects.value.length })
targets.sphere = sphereVertices
window.addEventListener('resize', onWindowResize, false)
transform(targets.table, 1000)
render()
}
function render() {
if (renderer.value) {
renderer.value.render(scene.value, camera.value)
}
}
/**
* @description: 位置变换
* @param targets 目标位置
* @param duration 持续时间
*/
function transform(targets: any[], duration: number) {
TWEEN.removeAll()
if (intervalTimer.value) {
clearInterval(intervalTimer.value)
intervalTimer.value = null
randomBallData('sphere')
}
return new Promise((resolve) => {
const objLength = objects.value.length
for (let i = 0; i < objLength; ++i) {
const object = objects.value[i]
const target = targets[i]
// console.log('target', i, target, targets)
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()
.onComplete(() => {
if (luckyCardList.value.length) {
luckyCardList.value.forEach((cardIndex: any) => {
const item = objects.value[cardIndex]
useElementStyle(item.element, {} as any, i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value, 'sphere')
})
}
luckyTargets.value = []
luckyCardList.value = []
canOperate.value = true
})
}
// 这个补间用来在位置与旋转补间同步执行通过onUpdate在每次更新数据后渲染scene和camera
new TWEEN.Tween({})
.to({}, duration * 2)
.onUpdate(render)
.start()
.onComplete(() => {
canOperate.value = true
resolve('')
})
})
}
/**
* @description: 窗口大小改变时重新设置渲染器的大小
*/
function onWindowResize() {
camera.value.aspect = window.innerWidth / window.innerHeight
camera.value.updateProjectionMatrix()
renderer.value.setSize(window.innerWidth, window.innerHeight)
render()
}
/**
* [animation update all tween && controls]
*/
function animation() {
TWEEN.update()
if (controls.value) {
controls.value.update()
}
// 设置自动旋转
// 设置相机位置
requestAnimationFrame(animation)
}
/**
* @description: 旋转的动画
* @param rotateY 绕y轴旋转圈数
* @param duration 持续时间,单位秒
*/
function rollBall(rotateY: number, duration: number) {
TWEEN.removeAll()
return new Promise((resolve) => {
scene.value.rotation.y = 0
ballRotationY.value = Math.PI * rotateY * 1000
const rotateObj = new TWEEN.Tween(scene.value.rotation)
rotateObj
.to(
{
// x: Math.PI * rotateX * 1000,
x: 0,
y: ballRotationY.value,
// z: Math.PI * rotateZ * 1000
z: 0,
},
duration * 1000,
)
.onUpdate(render)
.start()
.onStop(() => {
resolve('')
})
.onComplete(() => {
resolve('')
})
})
}
/**
* @description: 视野转回正面
*/
function resetCamera() {
new TWEEN.Tween(camera.value.position)
.to(
{
x: 0,
y: 0,
z: 3000,
},
1000,
)
.onUpdate(render)
.start()
.onComplete(() => {
new TWEEN.Tween(camera.value.rotation)
.to(
{
x: 0,
y: 0,
z: 0,
},
1000,
)
.onUpdate(render)
.start()
.onComplete(() => {
canOperate.value = true
// camera.value.lookAt(scene.value.position)
camera.value.position.y = 0
camera.value.position.x = 0
camera.value.position.z = 3000
camera.value.rotation.x = 0
camera.value.rotation.y = 0
camera.value.rotation.z = -0
controls.value.reset()
})
})
}
/**
* @description: 开始抽奖,由横铺变换为球体(或其他图形)
* @returns 随机抽取球数据
*/
/// <IP_ADDRESS>description 进入抽奖准备状态
async function enterLottery() {
if (!canOperate.value) {
return
}
if (!intervalTimer.value) {
randomBallData()
}
if (patternList.value.length) {
for (let i = 0; i < patternList.value.length; i++) {
if (i < rowCount.value * 7) {
objects.value[patternList.value[i] - 1].element.style.backgroundColor = rgba(cardColor.value, Math.random() * 0.5 + 0.25)
}
}
}
canOperate.value = false
await transform(targets.sphere, 1000)
currentStatus.value = LotteryStatus.ready
rollBall(0.1, 2000)
}
/**
* @description 开始抽奖
*/
function startLottery() {
if (!canOperate.value) {
return
}
// 验证是否已抽完全部奖项
if (currentPrize.value.isUsed || !currentPrize.value) {
toast.open({
message: i18n.global.t('error.personIsAllDone'),
type: 'warning',
position: 'top-right',
duration: 10000,
})
return
}
personPool.value = currentPrize.value.isAll ? notThisPrizePersonList.value : notPersonList.value
// 验证抽奖人数是否还够
if (personPool.value.length < currentPrize.value.count - currentPrize.value.isUsedCount) {
toast.open({
message: i18n.global.t('error.personNotEnough'),
type: 'warning',
position: 'top-right',
duration: 10000,
})
return
}
luckyCount.value = 10
// 自定义抽奖个数
let leftover = currentPrize.value.count - currentPrize.value.isUsedCount
const customCount = currentPrize.value.separateCount
if (customCount && customCount.enable && customCount.countList.length > 0) {
for (let i = 0; i < customCount.countList.length; i++) {
if (customCount.countList[i].isUsedCount < customCount.countList[i].count) {
leftover = customCount.countList[i].count - customCount.countList[i].isUsedCount
break
}
}
}
luckyCount.value = leftover < luckyCount.value ? leftover : luckyCount.value
for (let i = 0; i < luckyCount.value; i++) {
if (personPool.value.length > 0) {
// 解决随机元素概率过于不均等问题
const randomIndex = Math.floor(Math.random() * (personPool.value.length - 1))
luckyTargets.value.push(personPool.value[randomIndex])
personPool.value.splice(randomIndex, 1)
}
}
toast.open({
// message: `现在抽取${currentPrize.value.name} ${leftover}人`,
message: i18n.global.t('error.startDraw', { count: currentPrize.value.name, leftover }),
type: 'default',
position: 'top-right',
duration: 8000,
})
currentStatus.value = LotteryStatus.running
rollBall(10, 3000)
}
/**
* @description: 停止抽奖,抽出幸运人
*/
async function stopLottery() {
if (!canOperate.value) {
return
}
// clearInterval(intervalTimer.value)
// intervalTimer.value = null
canOperate.value = false
rollBall(0, 1)
const windowSize = { width: window.innerWidth, height: window.innerHeight }
luckyTargets.value.forEach((person: IPersonConfig, index: number) => {
const cardIndex = selectCard(luckyCardList.value, tableData.value.length, person.id)
luckyCardList.value.push(cardIndex)
const totalLuckyCount = luckyTargets.value.length
const item = objects.value[cardIndex]
const { xTable, yTable } = useElementPosition(item, rowCount.value, totalLuckyCount, { width: cardSize.value.width * 2, height: cardSize.value.height * 2 }, windowSize, index)
new TWEEN.Tween(item.position)
.to({
x: xTable,
y: yTable,
z: 1000,
}, 1200)
.easing(TWEEN.Easing.Exponential.InOut)
.onStart(() => {
item.element = useElementStyle(item.element, person, cardIndex, patternList.value, patternColor.value, luckyColor.value, { width: cardSize.value.width * 2, height: cardSize.value.height * 2 }, textSize.value * 2, 'lucky')
})
.start()
.onComplete(() => {
canOperate.value = true
currentStatus.value = LotteryStatus.end
})
new TWEEN.Tween(item.rotation)
.to({
x: 0,
y: 0,
z: 0,
}, 900)
.easing(TWEEN.Easing.Exponential.InOut)
.start()
.onComplete(() => {
confettiFire()
resetCamera()
})
})
}
/**
* @description: 继续,意味着这抽奖作数,计入数据库
*/
async function continueLottery() {
if (!canOperate.value) {
return
}
const customCount = currentPrize.value.separateCount
if (customCount && customCount.enable && customCount.countList.length > 0) {
for (let i = 0; i < customCount.countList.length; i++) {
if (customCount.countList[i].isUsedCount < customCount.countList[i].count) {
customCount.countList[i].isUsedCount += luckyCount.value
break
}
}
}
currentPrize.value.isUsedCount += luckyCount.value
luckyCount.value = 0
if (currentPrize.value.isUsedCount >= currentPrize.value.count) {
currentPrize.value.isUsed = true
currentPrize.value.isUsedCount = currentPrize.value.count
}
personConfig.addAlreadyPersonList(luckyTargets.value, currentPrize.value)
prizeConfig.updatePrizeConfig(currentPrize.value)
await enterLottery()
}
/**
* @description: 放弃本次抽奖,回到初始状态
*/
function quitLottery() {
enterLottery()
currentStatus.value = LotteryStatus.init
}
/**
* @description: 随机替换卡片中的数据(不改变原有的值,只是显示)
* @param {string} mod 模式
*/
function randomBallData(mod: 'default' | 'lucky' | 'sphere' = 'default') {
// 两秒执行一次
intervalTimer.value = setInterval(() => {
// 产生随机数数组
const indexLength = 4
const cardRandomIndexArr: number[] = []
const personRandomIndexArr: number[] = []
for (let i = 0; i < indexLength; i++) {
// 解决随机元素概率过于不均等问题
const randomCardIndex = Math.floor(Math.random() * (tableData.value.length - 1))
const randomPersonIndex = Math.floor(Math.random() * (allPersonList.value.length - 1))
if (luckyCardList.value.includes(randomCardIndex)) {
continue
}
cardRandomIndexArr.push(randomCardIndex)
personRandomIndexArr.push(randomPersonIndex)
}
for (let i = 0; i < cardRandomIndexArr.length; i++) {
if (!objects.value[cardRandomIndexArr[i]]) {
continue
}
objects.value[cardRandomIndexArr[i]].element = useElementStyle(objects.value[cardRandomIndexArr[i]].element, allPersonList.value[personRandomIndexArr[i]], cardRandomIndexArr[i], patternList.value, patternColor.value, cardColor.value, { width: cardSize.value.width, height: cardSize.value.height }, textSize.value, mod, 'change')
}
}, 200)
}
/**
* @description: 键盘监听,快捷键操作
*/
function listenKeyboard(e: any) {
if ((e.keyCode !== 32 || e.keyCode !== 27) && !canOperate.value) {
return
}
if (e.keyCode === 27 && currentStatus.value === LotteryStatus.running) {
quitLottery()
}
if (e.keyCode !== 32) {
return
}
switch (currentStatus.value) {
case LotteryStatus.init:
enterLottery()
break
case LotteryStatus.ready:
startLottery()
break
case LotteryStatus.running:
stopLottery()
break
case LotteryStatus.end:
continueLottery()
break
default:
break
}
}
/**
* @description: 清理资源,避免内存溢出
*/
function cleanup() {
clearInterval(intervalTimer.value)
intervalTimer.value = null
if (scene.value) {
scene.value.traverse((object: Object3D) => {
if ((object as any).material) {
if (Array.isArray((object as any).material)) {
(object as any).material.forEach((material: Material) => {
material.dispose()
})
}
else {
(object as any).material.dispose()
}
}
if ((object as any).geometry) {
(object as any).geometry.dispose()
}
if ((object as any).texture) {
(object as any).texture.dispose()
}
})
scene.value.clear()
}
if (objects.value) {
objects.value.forEach((object) => {
if (object.element) {
object.element.remove()
}
})
objects.value = []
}
if (controls.value) {
controls.value.removeEventListener('change')
controls.value.dispose()
}
// 移除所有事件监听
window.removeEventListener('resize', onWindowResize)
scene.value = null
camera.value = null
renderer.value = null
controls.value = null
}
/**
* @description: 设置默认人员列表
*/
function setDefaultPersonList() {
personConfig.setDefaultPersonList()
// 刷新页面
window.location.reload()
}
onMounted(() => {
tableData.value = initTableData({ allPersonList: allPersonList.value, rowCount: rowCount.value })
init()
animation()
containerRef.value!.style.color = `${textColor}`
randomBallData()
window.addEventListener('keydown', listenKeyboard)
})
onUnmounted(() => {
nextTick(() => {
cleanup()
})
clearInterval(intervalTimer.value)
intervalTimer.value = null
window.removeEventListener('keydown', listenKeyboard)
})
return {
setDefaultPersonList,
startLottery,
continueLottery,
quitLottery,
containerRef,
stopLottery,
enterLottery,
tableData,
currentStatus,
}
}