+
-
-
-
-
-
-
-
-
-
-
-
- {{
- temporaryPrize.name }}
-
-
-
- {{ temporaryPrize.isUsedCount }}/{{
- temporaryPrize.count }}
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- {{ item.name }}
-
-
-
- {{ item.isUsedCount }}/{{
- item.count }}
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
+
diff --git a/src/views/Home/components/PrizeList/parts/OfficialPrizeList/index.scss b/src/views/Home/components/PrizeList/parts/OfficialPrizeList/index.scss
new file mode 100644
index 0000000..833331c
--- /dev/null
+++ b/src/views/Home/components/PrizeList/parts/OfficialPrizeList/index.scss
@@ -0,0 +1,140 @@
+.scroll-button::before,
+.scroll-button::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+ transform: translate(12px 12px);
+}
+
+.scroll-button::before {
+ transform: translate(0, -6px);
+ opacity: 0.6;
+}
+
+.scroll-button::after {
+ transform: translate(0, 6px);
+ opacity: 0.4;
+}
+
+/* 添加动画效果 */
+.scroll-button-down {
+ animation: bounce-down 2s infinite;
+}
+
+/* 添加动画效果 */
+.scroll-button-up {
+ animation: bounce-up 2s infinite;
+}
+
+.scroll-container {
+ &::before {
+ content: '';
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ border-radius: 12px;
+ }
+}
+
+.scroll-container-end {
+ height: 100%;
+
+ &::before {
+ content: '';
+ position: absolute;
+ height: 90%;
+ width: 100%;
+ border-radius: 12px;
+ }
+}
+
+.no-scroll {}
+
+@keyframes bounce-down {
+
+ 0%,
+ 20%,
+ 50%,
+ 80%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 40% {
+ transform: translateY(-5px);
+ }
+
+ 60% {
+ transform: translateY(-2px);
+ }
+}
+
+@keyframes bounce-up {
+
+ 0%,
+ 20%,
+ 50%,
+ 80%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 40% {
+ transform: translateY(5px);
+ }
+
+ 60% {
+ transform: translateY(2px);
+ }
+}
+
+.scroll-button:hover {
+ transform: translateY(-3px);
+}
+
+.current-prize {
+ position: relative;
+ display: block;
+ overflow: clip;
+ isolation: isolate;
+
+ border-radius: 20px;
+ padding: 3px;
+}
+
+.current-prize::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 400%;
+ height: 100%;
+ background: linear-gradient(115deg, #4fcf70, #fad648, #a767e5, #12bcfe, #44ce7b);
+ background-size: 25% 100%;
+ animation: an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop .75s linear infinite;
+ // animation-play-state: paused;
+ translate: -5% 0%;
+ transition: translate 0.25s ease-out;
+ animation-play-state: running;
+ transition-duration: 0.75s;
+ translate: 0% 0%;
+}
+
+.current-prize::after {
+ content: "";
+ position: absolute;
+ inset: 4px;
+ border-top-left-radius: 20px;
+ border-bottom-right-radius: 20px;
+ z-index: -1;
+}
+
+@keyframes an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop {
+ to {
+ transform: translateX(-25%);
+ }
+}
\ No newline at end of file
diff --git a/src/views/Home/components/PrizeList/parts/OfficialPrizeList/index.vue b/src/views/Home/components/PrizeList/parts/OfficialPrizeList/index.vue
new file mode 100644
index 0000000..5288d14
--- /dev/null
+++ b/src/views/Home/components/PrizeList/parts/OfficialPrizeList/index.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/views/Home/components/PrizeList/parts/OfficialPrizeList/useGsap.ts b/src/views/Home/components/PrizeList/parts/OfficialPrizeList/useGsap.ts
new file mode 100644
index 0000000..e9667fd
--- /dev/null
+++ b/src/views/Home/components/PrizeList/parts/OfficialPrizeList/useGsap.ts
@@ -0,0 +1,93 @@
+import type { Ref } from 'vue'
+import gsap from 'gsap'
+import { ScrollTrigger } from 'gsap/ScrollTrigger'
+import { onBeforeUnmount, onUnmounted, ref, watch } from 'vue'
+
+export function useGsap(scrollContainerRef: any, liRefs: any, isScroll: Ref
, prizeShow: any, temporaryPrizeShow: boolean) {
+ gsap.registerPlugin(ScrollTrigger)
+
+ const ctx = ref()
+ const showUpButton = ref(false)
+ const showDownButton = ref(true)
+ function initGsapAnimation() {
+ ctx.value = gsap.context(() => {
+ liRefs.value.forEach((box: any) => {
+ gsap.fromTo(box, { rotationX: -90, rotateZ: -20, opacity: 0 }, {
+ rotationX: 0,
+ rotateZ: 0,
+ opacity: 1,
+ scrollTrigger: {
+ trigger: box,
+ scroller: scrollContainerRef.value, // <- Specify the scroller!
+ start: 'bottom 100%',
+ end: 'top 70%',
+ scrub: true,
+ },
+ })
+ })
+ }, scrollContainerRef.value) // <- Scope!
+ }
+
+ function disposeGsapAnimation() {
+ if (!ctx.value) {
+ return
+ }
+ ctx.value.revert() // <- Easy Cleanup!
+ }
+ function scrollHandler() {
+ const scrollHeight = scrollContainerRef.value.scrollHeight
+ const scrollTop = scrollContainerRef.value.scrollTop
+ const containerHeight = scrollContainerRef.value.clientHeight
+ // 滚动滑到底部
+ if (scrollTop + containerHeight >= scrollHeight - 10) {
+ showDownButton.value = false
+ showUpButton.value = true
+ }
+ // 在中间
+ else if (scrollTop && scrollTop + containerHeight < scrollHeight) {
+ showDownButton.value = true
+ showUpButton.value = true
+ }
+ // 滚动滑到顶部
+ else {
+ showDownButton.value = true
+ showUpButton.value = false
+ }
+ }
+ function listenScrollContainer() {
+ scrollContainerRef.value.addEventListener('scroll', scrollHandler)
+ }
+ function removeScrollContainer() {
+ if (scrollContainerRef.value) {
+ scrollContainerRef.value.removeEventListener('scroll', scrollHandler)
+ }
+ }
+
+ function handleScroll(h: number) {
+ scrollContainerRef.value.scrollTop += h
+ }
+ watch([isScroll, prizeShow, temporaryPrizeShow], ([val1, val2, val3]) => {
+ if (val1 && val2 && !val3) {
+ setTimeout(() => {
+ initGsapAnimation()
+ listenScrollContainer()
+ }, 0)
+ }
+ })
+ onBeforeUnmount(() => {
+ if (!isScroll.value)
+ return
+ removeScrollContainer()
+ })
+ onUnmounted(() => {
+ if (!isScroll.value)
+ return
+ disposeGsapAnimation()
+ })
+
+ return {
+ showUpButton,
+ showDownButton,
+ handleScroll,
+ }
+}
diff --git a/src/views/Home/components/PrizeList/parts/OperationButton.vue b/src/views/Home/components/PrizeList/parts/OperationButton.vue
new file mode 100644
index 0000000..46cfe09
--- /dev/null
+++ b/src/views/Home/components/PrizeList/parts/OperationButton.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/views/Home/components/PrizeList/parts/TemporaryDialog.vue b/src/views/Home/components/PrizeList/parts/TemporaryDialog.vue
new file mode 100644
index 0000000..04139d1
--- /dev/null
+++ b/src/views/Home/components/PrizeList/parts/TemporaryDialog.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
diff --git a/src/views/Home/components/PrizeList/parts/TemporaryList.vue b/src/views/Home/components/PrizeList/parts/TemporaryList.vue
new file mode 100644
index 0000000..4c168cd
--- /dev/null
+++ b/src/views/Home/components/PrizeList/parts/TemporaryList.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ temporaryPrize.name }}
+
+
+
+ {{ temporaryPrize.isUsedCount }}/{{ temporaryPrize.count }}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/Home/components/PrizeList/usePrizeList.ts b/src/views/Home/components/PrizeList/usePrizeList.ts
new file mode 100644
index 0000000..ed039bf
--- /dev/null
+++ b/src/views/Home/components/PrizeList/usePrizeList.ts
@@ -0,0 +1,98 @@
+import type { IPrizeConfig } from '@/types/storeType'
+import { storeToRefs } from 'pinia'
+import { onMounted, ref } from 'vue'
+import i18n from '@/locales/i18n'
+
+import useStore from '@/store'
+
+export function usePrizeList(temporaryPrizeRef: any) {
+ const prizeConfig = useStore().prizeConfig
+ const globalConfig = useStore().globalConfig
+ const system = useStore().system
+ const {
+ getPrizeConfig: localPrizeList,
+ getCurrentPrize: currentPrize,
+ getTemporaryPrize: temporaryPrize,
+ } = storeToRefs(prizeConfig)
+ const {
+ getIsShowPrizeList: isShowPrizeList,
+ getImageList: localImageList,
+ }
+ = storeToRefs(globalConfig)
+ const { getIsMobile: isMobile } = storeToRefs(system)
+
+ const selectedPrize = ref()
+ const prizeShow = ref(structuredClone(isShowPrizeList.value))
+
+ function addTemporaryPrize() {
+ temporaryPrizeRef.value.showDialog()
+ }
+
+ function deleteTemporaryPrize() {
+ temporaryPrize.value.isShow = false
+ prizeConfig.setTemporaryPrize(temporaryPrize.value)
+ }
+ function submitTemporaryPrize() {
+ if (!temporaryPrize.value.name || !temporaryPrize.value.count) {
+ // eslint-disable-next-line no-alert
+ alert(i18n.global.t('error.completeInformation'))
+ return
+ }
+ temporaryPrize.value.isShow = true
+ temporaryPrize.value.id = new Date().getTime().toString()
+ prizeConfig.setCurrentPrize(temporaryPrize.value)
+ }
+ function selectPrize(item: IPrizeConfig) {
+ selectedPrize.value = item
+ selectedPrize.value.isUsedCount = 0
+ selectedPrize.value.isUsed = false
+
+ if (selectedPrize.value.separateCount.countList.length > 1) {
+ return
+ }
+ selectedPrize.value.separateCount = {
+ enable: true,
+ countList: [
+ {
+ id: '0',
+ count: item.count,
+ isUsedCount: 0,
+ },
+ ],
+ }
+ }
+ function submitData(value: any) {
+ selectedPrize.value!.separateCount.countList = value
+ selectedPrize.value = null
+ }
+ function changePersonCount() {
+ temporaryPrize.value.separateCount.countList = []
+ }
+ function setCurrentPrize() {
+ for (let i = 0; i < localPrizeList.value.length; i++) {
+ if (localPrizeList.value[i].isUsedCount < localPrizeList.value[i].count) {
+ prizeConfig.setCurrentPrize(localPrizeList.value[i])
+
+ return
+ }
+ }
+ }
+ onMounted(() => {
+ setCurrentPrize()
+ })
+
+ return {
+ temporaryPrize,
+ changePersonCount,
+ selectPrize,
+ currentPrize,
+ localImageList,
+ addTemporaryPrize,
+ submitTemporaryPrize,
+ submitData,
+ deleteTemporaryPrize,
+ prizeShow,
+ localPrizeList,
+ isMobile,
+ }
+}
diff --git a/src/views/Home/useViewModel.ts b/src/views/Home/useViewModel.ts
index fb27f80..7e568d2 100644
--- a/src/views/Home/useViewModel.ts
+++ b/src/views/Home/useViewModel.ts
@@ -8,6 +8,7 @@ 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 enterAudio from '@/assets/audio/enter.wav'
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
import i18n from '@/locales/i18n'
import useStore from '@/store'
@@ -16,6 +17,7 @@ import { rgba } from '@/utils/color'
import { LotteryStatus } from './type'
import { confettiFire, createSphereVertices, createTableVertices, getRandomElements, initTableData } from './utils'
+const maxAudioLimit = 10
export function useViewModel() {
const toast = useToast()
// store里面存储的值
@@ -26,7 +28,21 @@ export function useViewModel() {
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, getTitleFont: titleFont, getTitleFontSyncGlobal: titleFontSyncGlobal } = storeToRefs(globalConfig)
+ const {
+ getCardColor: cardColor,
+ getPatterColor: patternColor,
+ getPatternList: patternList,
+ getTextColor: textColor,
+ getLuckyColor: luckyColor,
+ getCardSize: cardSize,
+ getTextSize: textSize,
+ getRowCount: rowCount,
+ getIsShowAvatar: isShowAvatar,
+ getTitleFont: titleFont,
+ getTitleFontSyncGlobal: titleFontSyncGlobal,
+ getDefiniteTime: definiteTime,
+ getWinMusic: isPlayWinMusic,
+ } = storeToRefs(globalConfig)
// three初始值
const ballRotationY = ref(0)
const containerRef = ref()
@@ -53,7 +69,7 @@ export function useViewModel() {
const intervalTimer = ref(null)
const isInitialDone = ref(false)
const animationFrameId = ref(null)
-
+ const playingAudios = ref([])
function initThreeJs() {
const felidView = 40
const width = window.innerWidth
@@ -367,14 +383,6 @@ export function useViewModel() {
personPool.value.splice(index, 1)
}
})
- // 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}人`,
@@ -385,6 +393,13 @@ export function useViewModel() {
})
currentStatus.value = LotteryStatus.running
rollBall(10, 3000)
+ if (definiteTime.value) {
+ setTimeout(() => {
+ if (currentStatus.value === LotteryStatus.running) {
+ stopLottery()
+ }
+ }, definiteTime.value * 1000)
+ }
}
/**
* @description: 停止抽奖,抽出幸运人
@@ -429,11 +444,41 @@ export function useViewModel() {
.easing(TWEEN.Easing.Exponential.InOut)
.start()
.onComplete(() => {
+ if (isPlayWinMusic.value) {
+ playWinMusic()
+ }
confettiFire()
resetCamera()
})
})
}
+ // 播放音频,中将卡片越多audio对象越多,声音越大
+ function playWinMusic() {
+ if (playingAudios.value.length > maxAudioLimit) {
+ console.log('音频播放数量已达到上限,请勿重复播放')
+ return
+ }
+ const enterNewAudio = new Audio(enterAudio)
+ playingAudios.value.push(enterNewAudio)
+ enterNewAudio.play()
+ .then(() => {
+ // 当音频播放结束后,从数组中移除
+ enterNewAudio.onended = () => {
+ const index = playingAudios.value.indexOf(enterNewAudio)
+ if (index > -1) {
+ playingAudios.value.splice(index, 1)
+ }
+ }
+ })
+ .catch((error) => {
+ console.error('播放音频失败:', error)
+ // 如果播放失败,也从数组中移除
+ const index = playingAudios.value.indexOf(enterNewAudio)
+ if (index > -1) {
+ playingAudios.value.splice(index, 1)
+ }
+ })
+ }
/**
* @description: 继续,意味着这抽奖作数,计入数据库
*/
@@ -441,7 +486,6 @@ export function useViewModel() {
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++) {
@@ -627,11 +671,6 @@ export function useViewModel() {
intervalTimer.value = null
window.removeEventListener('keydown', listenKeyboard)
})
- // watch(() => allPersonList.value, (newVal) => {
- // if (newVal.length) {
- // init()
- // }
- // })
return {
setDefaultPersonList,