feat:新增动画,修复一番赏的逻辑错误,无限赏和一番赏目前按照权重升序排列

This commit is contained in:
tsui110 2025-12-29 20:06:37 +08:00
parent 28e0721e3f
commit a634c6caac
16 changed files with 416 additions and 860 deletions

View File

@ -1,251 +0,0 @@
<template>
<view v-if="visible" class="blessing-container">
<view class="blessing-animation" :class="currentBlessing.type">
<view class="blessing-emoji">{{ currentBlessing.emoji }}</view>
<view v-if="currentBlessing.type === 'sheep'" class="blessing-subtitle">小羊祝你</view>
<view class="blessing-text">
<text v-for="(char, index) in currentBlessing.chars"
:key="index"
class="char"
:class="{ 'from-left': index % 2 === 0, 'from-right': index % 2 === 1 }"
:style="{ animationDelay: index * 0.15 + 's' }">
{{ char }}
</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { registerBlessing, unregisterBlessing } from '@/utils/blessing.js'
const visible = ref(false)
const blessings = [
{
emoji: '🐏',
chars: ['三', '羊', '开', '泰'],
type: 'sheep'
},
{
emoji: '🐴',
chars: ['马', '到', '功', '成'],
type: 'horse'
}
]
const currentBlessing = ref(blessings[0])
const handleShow = ({ type }) => {
console.log('[BlessingAnimation] handleShow 被调用, type:', type)
//
let index = 0
if (type === 'random') {
index = Math.floor(Math.random() * blessings.length)
} else if (type === 'sheep') {
index = 0
} else if (type === 'horse') {
index = 1
} else {
index = Math.floor(Math.random() * blessings.length)
}
currentBlessing.value = blessings[index]
visible.value = true
console.log('[BlessingAnimation] 显示祝福:', currentBlessing.value)
// 3
setTimeout(() => {
visible.value = false
console.log('[BlessingAnimation] 隐藏祝福')
}, 3000)
}
onMounted(() => {
console.log('[BlessingAnimation] 组件已挂载,准备注册监听器')
registerBlessing(handleShow)
console.log('[BlessingAnimation] 监听器注册完成')
})
onUnmounted(() => {
console.log('[BlessingAnimation] 组件即将卸载')
unregisterBlessing()
})
</script>
<style lang="scss" scoped>
.blessing-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000; // (999)
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx;
width: 100%;
max-width: 600rpx;
}
.blessing-animation {
text-align: center;
animation: blessingFadeIn 0.5s ease-out;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(255, 248, 243, 0.98));
padding: 40rpx 30rpx;
border-radius: 24rpx;
box-shadow: 0 12rpx 48rpx rgba(255, 107, 0, 0.3);
backdrop-filter: blur(20rpx);
border: 2rpx solid rgba(255, 159, 67, 0.3);
}
@keyframes blessingFadeIn {
from {
opacity: 0;
transform: translateY(-30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.blessing-emoji {
font-size: 100rpx;
line-height: 1;
margin-bottom: 16rpx;
display: block;
}
// -
.blessing-animation.sheep .blessing-emoji {
animation: emojiBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
// -
.blessing-animation.horse .blessing-emoji {
animation: emojiRun 0.8s ease-out;
}
@keyframes emojiBounce {
0% {
transform: scale(0) rotate(-180deg);
opacity: 0;
}
50% {
transform: scale(1.2) rotate(10deg);
}
100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
@keyframes emojiRun {
0% {
transform: translateX(-300rpx) scale(0.8);
opacity: 0;
}
60% {
transform: translateX(30rpx) scale(1.1);
}
80% {
transform: translateX(-15rpx) scale(0.95);
}
100% {
transform: translateX(0) scale(1);
opacity: 1;
}
}
.blessing-subtitle {
font-size: 28rpx;
color: #FF9500;
font-weight: 700;
margin-top: 12rpx;
margin-bottom: 8rpx;
opacity: 0;
animation: subtitleFadeIn 0.5s ease-out 0.3s forwards;
}
@keyframes subtitleFadeIn {
from {
opacity: 0;
transform: translateY(10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.blessing-text {
display: flex;
justify-content: center;
gap: 12rpx;
margin-top: 16rpx;
.char {
font-size: 48rpx;
font-weight: 900;
color: #FF6B00;
text-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.3);
opacity: 0;
animation: charAppear 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
//
.char.from-left {
animation-name: charAppearFromLeft;
}
//
.char.from-right {
animation-name: charAppearFromRight;
}
}
@keyframes charAppear {
0% {
opacity: 0;
transform: translateY(30rpx) scale(0.5);
}
60% {
transform: translateY(-8rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes charAppearFromLeft {
0% {
opacity: 0;
transform: translateX(-80rpx) scale(0.5);
}
60% {
transform: translateX(10rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
}
}
@keyframes charAppearFromRight {
0% {
opacity: 0;
transform: translateX(80rpx) scale(0.5);
}
60% {
transform: translateX(-10rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
}
}
</style>

View File

@ -1,233 +0,0 @@
<template>
<view v-if="visible" class="blessing-float" :class="{ 'hide': shouldHide }" @tap="handleClose">
<view class="blessing-content">
<view class="blessing-emoji">{{ currentBlessing.emoji }}</view>
<view v-if="currentBlessing.type === 'sheep'" class="blessing-subtitle">小羊祝你</view>
<view class="blessing-text">
<text v-for="(char, index) in currentBlessing.chars"
:key="index"
class="char"
:class="{ 'from-left': index % 2 === 0, 'from-right': index % 2 === 1 }"
:style="{ animationDelay: index * 0.15 + 's' }">
{{ char }}
</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { registerBlessing, unregisterBlessing } from '@/utils/blessing.js'
const visible = ref(false)
const shouldHide = ref(false)
const blessings = [
{
emoji: '🐏',
chars: ['三', '羊', '开', '泰'],
type: 'sheep'
},
{
emoji: '🐴',
chars: ['马', '到', '功', '成'],
type: 'horse'
}
]
const currentBlessing = ref(blessings[0])
const handleShow = ({ type }) => {
console.log('[BlessingFloat] handleShow 被调用, type:', type)
//
let index = 0
if (type === 'random') {
index = Math.floor(Math.random() * blessings.length)
} else if (type === 'sheep') {
index = 0
} else if (type === 'horse') {
index = 1
} else {
index = Math.floor(Math.random() * blessings.length)
}
currentBlessing.value = blessings[index]
visible.value = true
shouldHide.value = false
console.log('[BlessingFloat] 显示祝福:', currentBlessing.value)
// 3
setTimeout(() => {
shouldHide.value = true
setTimeout(() => {
visible.value = false
console.log('[BlessingFloat] 隐藏祝福')
}, 300)
}, 3000)
}
const handleClose = () => {
shouldHide.value = true
setTimeout(() => {
visible.value = false
console.log('[BlessingFloat] 用户点击关闭')
}, 300)
}
onMounted(() => {
console.log('[BlessingFloat] 组件已挂载,准备注册监听器')
registerBlessing(handleShow)
console.log('[BlessingFloat] 监听器注册完成')
})
onUnmounted(() => {
console.log('[BlessingFloat] 组件即将卸载')
unregisterBlessing()
})
</script>
<style lang="scss" scoped>
.blessing-float {
position: fixed;
top: 200rpx;
right: 30rpx;
z-index: 99999;
animation: floatIn 0.5s ease-out;
transition: all 0.3s ease;
&.hide {
opacity: 0;
transform: translateY(-50rpx) scale(0.8);
}
}
@keyframes floatIn {
from {
opacity: 0;
transform: translateY(-50rpx) scale(0.8);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.blessing-content {
background: linear-gradient(135deg, #FF6B00 0%, #FF9500 100%);
padding: 24rpx 20rpx;
border-radius: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 107, 0, 0.4);
text-align: center;
min-width: 200rpx;
backdrop-filter: blur(10rpx);
}
.blessing-emoji {
font-size: 60rpx;
line-height: 1;
margin-bottom: 8rpx;
display: block;
animation: emojiBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
@keyframes emojiBounce {
0% {
transform: scale(0) rotate(-180deg);
opacity: 0;
}
50% {
transform: scale(1.2) rotate(10deg);
}
100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
.blessing-subtitle {
font-size: 20rpx;
color: #FFFFFF;
font-weight: 700;
margin-bottom: 6rpx;
opacity: 0;
animation: subtitleFadeIn 0.5s ease-out 0.3s forwards;
}
@keyframes subtitleFadeIn {
from {
opacity: 0;
transform: translateY(10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.blessing-text {
display: flex;
justify-content: center;
gap: 8rpx;
margin-top: 8rpx;
.char {
font-size: 32rpx;
font-weight: 900;
color: #FFFFFF;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
opacity: 0;
animation: charAppear 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.char.from-left {
animation-name: charAppearFromLeft;
}
.char.from-right {
animation-name: charAppearFromRight;
}
}
@keyframes charAppear {
0% {
opacity: 0;
transform: translateY(20rpx) scale(0.5);
}
60% {
transform: translateY(-4rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes charAppearFromLeft {
0% {
opacity: 0;
transform: translateX(-40rpx) scale(0.5);
}
60% {
transform: translateX(6rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
}
}
@keyframes charAppearFromRight {
0% {
opacity: 0;
transform: translateX(40rpx) scale(0.5);
}
60% {
transform: translateX(-6rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
}
}
</style>

View File

@ -1,267 +0,0 @@
<template>
<view v-if="visible" class="blessing-popup-mask" @tap="handleClose">
<view class="blessing-popup-content" @tap.stop>
<view class="blessing-emoji">{{ currentBlessing.emoji }}</view>
<view v-if="currentBlessing.type === 'sheep'" class="blessing-subtitle">小羊祝你</view>
<view class="blessing-text">
<text v-for="(char, index) in currentBlessing.chars"
:key="index"
class="char"
:class="{ 'from-left': index % 2 === 0, 'from-right': index % 2 === 1 }"
:style="{ animationDelay: index * 0.15 + 's' }">
{{ char }}
</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { registerBlessing, unregisterBlessing } from '@/utils/blessing.js'
const visible = ref(false)
const blessings = [
{
emoji: '🐏',
chars: ['三', '羊', '开', '泰'],
type: 'sheep'
},
{
emoji: '🐴',
chars: ['马', '到', '功', '成'],
type: 'horse'
}
]
const currentBlessing = ref(blessings[0])
const handleShow = ({ type }) => {
console.log('[BlessingPopup] handleShow 被调用, type:', type)
//
let index = 0
if (type === 'random') {
index = Math.floor(Math.random() * blessings.length)
} else if (type === 'sheep') {
index = 0
} else if (type === 'horse') {
index = 1
} else {
index = Math.floor(Math.random() * blessings.length)
}
currentBlessing.value = blessings[index]
visible.value = true
console.log('[BlessingPopup] 显示祝福:', currentBlessing.value)
// 3
setTimeout(() => {
visible.value = false
console.log('[BlessingPopup] 隐藏祝福')
}, 3000)
}
const handleClose = () => {
visible.value = false
console.log('[BlessingPopup] 用户点击关闭')
}
onMounted(() => {
console.log('[BlessingPopup] 组件已挂载,准备注册监听器')
registerBlessing(handleShow)
console.log('[BlessingPopup] 监听器注册完成')
})
onUnmounted(() => {
console.log('[BlessingPopup] 组件即将卸载')
unregisterBlessing()
})
</script>
<style lang="scss" scoped>
.blessing-popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 10000; // (999)
display: flex;
align-items: center;
justify-content: center;
animation: maskFadeIn 0.3s ease-out;
}
@keyframes maskFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.blessing-popup-content {
width: 600rpx;
max-width: 90%;
background: rgba(255, 255, 255, 0.98);
border-radius: 32rpx;
padding: 60rpx 40rpx;
text-align: center;
animation: popupScaleIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20rpx);
}
@keyframes popupScaleIn {
0% {
opacity: 0;
transform: scale(0.8);
}
60% {
transform: scale(1.05);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.blessing-emoji {
font-size: 140rpx;
line-height: 1;
margin-bottom: 24rpx;
display: block;
}
// -
.blessing-popup-content:has(.blessing-subtitle) .blessing-emoji {
animation: emojiBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
// -
.blessing-popup-content:not(:has(.blessing-subtitle)) .blessing-emoji {
animation: emojiRun 0.8s ease-out;
}
@keyframes emojiBounce {
0% {
transform: scale(0) rotate(-180deg);
opacity: 0;
}
50% {
transform: scale(1.2) rotate(10deg);
}
100% {
transform: scale(1) rotate(0deg);
opacity: 1;
}
}
@keyframes emojiRun {
0% {
transform: translateX(-300rpx) scale(0.8);
opacity: 0;
}
60% {
transform: translateX(30rpx) scale(1.1);
}
80% {
transform: translateX(-15rpx) scale(0.95);
}
100% {
transform: translateX(0) scale(1);
opacity: 1;
}
}
.blessing-subtitle {
font-size: 32rpx;
color: #FF9500;
font-weight: 700;
margin-bottom: 16rpx;
opacity: 0;
animation: subtitleFadeIn 0.5s ease-out 0.3s forwards;
}
@keyframes subtitleFadeIn {
from {
opacity: 0;
transform: translateY(10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.blessing-text {
display: flex;
justify-content: center;
gap: 16rpx;
margin-top: 24rpx;
.char {
font-size: 56rpx;
font-weight: 900;
color: #FF6B00;
text-shadow: 0 4rpx 12rpx rgba(255, 107, 0, 0.3);
opacity: 0;
animation: charAppear 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
//
.char.from-left {
animation-name: charAppearFromLeft;
}
//
.char.from-right {
animation-name: charAppearFromRight;
}
}
@keyframes charAppear {
0% {
opacity: 0;
transform: translateY(30rpx) scale(0.5);
}
60% {
transform: translateY(-8rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes charAppearFromLeft {
0% {
opacity: 0;
transform: translateX(-80rpx) scale(0.5);
}
60% {
transform: translateX(10rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
}
}
@keyframes charAppearFromRight {
0% {
opacity: 0;
transform: translateX(80rpx) scale(0.5);
}
60% {
transform: translateX(-10rpx) scale(1.1);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
}
}
</style>

View File

@ -2,7 +2,7 @@
<view>
<!-- 祝福动画 -->
<view v-if="showBlessing" class="blessing-container">
<view class="blessing-animation">
<view class="blessing-animation" :class="currentBlessing.type">
<view class="blessing-emoji">{{ currentBlessing.emoji }}</view>
<view v-if="currentBlessing.type === 'sheep'" class="blessing-subtitle">小羊祝你</view>
<view class="blessing-text">
@ -117,8 +117,33 @@ const blessings = [
},
{
emoji: '🐴',
chars: ['马', '到', '功', '成'],
chars: ['一', '马', '当', '先'],
type: 'horse'
},
{
emoji: '🍊',
chars: ['心', '想', '事', '橙'],
type: 'orange'
},
{
emoji: '🐵',
chars: ['财', '源', '广', '进'],
type: 'monkey'
},
{
emoji: '🐮',
chars: ['牛', '气', '冲', '天'],
type: 'ox'
},
{
emoji: '🐶',
chars: ['旺', '旺', '旺', '旺'],
type: 'dog'
},
{
emoji: '🐔',
chars: ['吉', '祥', '如', '意'],
type: 'chicken'
}
]
const currentBlessing = ref(blessings[0])
@ -242,17 +267,16 @@ function handleConfirm() {
.blessing-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
pointer-events: none;
display: flex;
justify-content: center;
align-items: center;
padding: 20rpx;
width: 100%;
max-width: 600rpx;
}
.blessing-animation {
@ -285,13 +309,38 @@ function handleConfirm() {
}
// -
.blessing-animation:has(.blessing-subtitle) .blessing-emoji {
animation: emojiBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
.blessing-animation.sheep .blessing-emoji {
animation: emojiBounce 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
// -
.blessing-animation:not(:has(.blessing-subtitle)) .blessing-emoji {
animation: emojiRun 0.8s ease-out;
.blessing-animation.horse .blessing-emoji {
animation: emojiRun 1.5s ease-out;
}
// -
.blessing-animation.orange .blessing-emoji {
animation: emojiRotate 1.5s ease-out;
}
// -
.blessing-animation.monkey .blessing-emoji {
animation: emojiSwing 1.5s ease-out;
}
// -
.blessing-animation.ox .blessing-emoji {
animation: emojiCharge 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
// -
.blessing-animation.dog .blessing-emoji {
animation: emojiWag 1.5s ease-in-out;
}
// -
.blessing-animation.chicken .blessing-emoji {
animation: emojiPeck 1.5s ease-in-out;
}
@keyframes emojiBounce {
@ -325,6 +374,120 @@ function handleConfirm() {
}
}
@keyframes emojiRotate {
0% {
transform: scale(0) rotate(0deg);
opacity: 0;
}
40% {
transform: scale(1.2) rotate(180deg);
opacity: 1;
}
60% {
transform: scale(0.95) rotate(360deg);
}
80% {
transform: scale(1.05) rotate(360deg);
}
100% {
transform: scale(1) rotate(360deg);
opacity: 1;
}
}
@keyframes emojiSwing {
0% {
transform: scale(0) translateY(-50rpx) rotate(-30deg);
opacity: 0;
}
40% {
transform: scale(1.15) translateY(10rpx) rotate(20deg);
opacity: 1;
}
60% {
transform: scale(0.9) translateY(-5rpx) rotate(-10deg);
}
80% {
transform: scale(1.05) translateY(3rpx) rotate(5deg);
}
100% {
transform: scale(1) translateY(0) rotate(0deg);
opacity: 1;
}
}
@keyframes emojiCharge {
0% {
transform: scale(0) translateX(-100rpx) rotate(45deg);
opacity: 0;
}
50% {
transform: scale(1.3) translateX(20rpx) rotate(-20deg);
opacity: 1;
}
70% {
transform: scale(0.85) translateX(-10rpx) rotate(10deg);
}
85% {
transform: scale(1.08) translateX(5rpx) rotate(-5deg);
}
100% {
transform: scale(1) translateX(0) rotate(0deg);
opacity: 1;
}
}
@keyframes emojiWag {
0% {
transform: scale(0) translateY(-30rpx) rotate(-15deg);
opacity: 0;
}
30% {
transform: scale(1.2) translateY(0) rotate(15deg);
opacity: 1;
}
50% {
transform: scale(0.9) translateY(-15rpx) rotate(-15deg);
}
70% {
transform: scale(1.1) translateY(0) rotate(15deg);
}
85% {
transform: scale(0.95) translateY(-5rpx) rotate(-5deg);
}
100% {
transform: scale(1) translateY(0) rotate(0deg);
opacity: 1;
}
}
@keyframes emojiPeck {
0% {
transform: scale(0) translateY(-40rpx) rotate(0deg);
opacity: 0;
}
25% {
transform: scale(1.15) translateY(10rpx) rotate(10deg);
opacity: 1;
}
40% {
transform: scale(0.85) translateY(-5rpx) rotate(-10deg);
}
55% {
transform: scale(1.1) translateY(8rpx) rotate(8deg);
}
70% {
transform: scale(0.9) translateY(-3rpx) rotate(-8deg);
}
85% {
transform: scale(1.05) translateY(2rpx) rotate(3deg);
}
100% {
transform: scale(1) translateY(0) rotate(0deg);
opacity: 1;
}
}
.blessing-subtitle {
font-size: 28rpx;
color: #FF9500;

View File

@ -39,22 +39,14 @@
</view>
</view>
</view>
<!-- 支付弹窗 -->
<PaymentPopup
v-model:visible="paymentVisible"
:amount="totalAmount"
:coupons="coupons"
:showCards="false"
@confirm="onPaymentConfirm"
/>
<!-- 支付弹窗已移至父组件避免在 scroll-view 内导致定位问题 -->
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { getIssueChoices, getUserCoupons, joinLottery, createWechatOrder, getLotteryResult } from '@/api/appUser'
import PaymentPopup from '@/components/PaymentPopup.vue'
import { requestLotterySubscription } from '@/utils/subscribe'
const props = defineProps({
@ -66,20 +58,35 @@ const props = defineProps({
hideActionBar: { type: Boolean, default: false } //
})
const emit = defineEmits(['payment-success', 'selection-change'])
const emit = defineEmits(['payment-success', 'selection-change', 'payment-visible-change', 'payment-amount-change', 'payment-coupons-change'])
const choices = ref([])
const loading = ref(false)
const selectedItems = ref([])
const paymentVisible = ref(false)
//
watch(paymentVisible, (newVal) => {
emit('payment-visible-change', newVal)
})
//
const coupons = ref([])
const coupons = ref([])
const totalAmount = computed(() => {
return (selectedItems.value.length * props.pricePerDraw).toFixed(2)
})
//
watch(totalAmount, (newVal) => {
emit('payment-amount-change', newVal)
})
//
watch(coupons, (newVal) => {
emit('payment-coupons-change', newVal)
})
const disabled = computed(() => !!props.disabled)
const disabledMessage = computed(() => props.disabledText || '暂不可下单')
@ -168,39 +175,43 @@ function handleSelect(item) {
emit('selection-change', [...selectedItems.value])
}
function handleBuy() {
async function handleBuy() {
if (disabled.value) {
uni.showToast({ title: disabledMessage.value, icon: 'none' })
return
}
if (selectedItems.value.length === 0) return
//
emit('payment-amount-change', totalAmount.value)
await fetchCoupons()
paymentVisible.value = true
fetchCoupons()
}
function handleRandomOne() {
async function handleRandomOne() {
if (disabled.value) {
uni.showToast({ title: disabledMessage.value, icon: 'none' })
return
}
const available = choices.value.filter(item =>
const available = choices.value.filter(item =>
!item.is_sold && item.status !== 'sold' && !isSelected(item)
)
if (available.length === 0) {
uni.showToast({ title: '没有可选位置了', icon: 'none' })
return
}
const randomIndex = Math.floor(Math.random() * available.length)
const randomItem = available[randomIndex]
//
selectedItems.value.push(randomItem)
//
//
emit('payment-amount-change', totalAmount.value)
await fetchCoupons()
paymentVisible.value = true
fetchCoupons()
}
@ -222,9 +233,12 @@ async function fetchCoupons() {
amount: Number(yuan).toFixed(2)
}
})
//
emit('payment-coupons-change', coupons.value)
} catch (e) {
console.error('fetchCoupons error', e)
coupons.value = []
emit('payment-coupons-change', [])
}
}
@ -329,6 +343,10 @@ async function onPaymentConfirm(paymentData) {
defineExpose({
handleRandomOne,
handleBuy,
onPaymentConfirm,
setPaymentVisible: (visible) => {
paymentVisible.value = visible
},
selectedItems: () => selectedItems.value
})
</script>

View File

@ -111,10 +111,14 @@ defineProps({
}
.bg-image {
width: 100%;
height: 100%;
width: 115%;
height: 115%;
max-width: 115%;
max-height: 115%;
position: absolute;
top: -7.5%;
left: -7.5%;
filter: blur(40rpx) brightness(0.85) saturate(1.1);
transform: scale(1.15);
}
.bg-mask {

View File

@ -43,10 +43,10 @@
:tabs="[{key: 'pool', label: '本机奖池'}, {key: 'records', label: '购买记录'}]"
>
<!-- 奖池预览 -->
<RewardsPreview
<RewardsPreview
v-if="tabActive === 'pool'"
:rewards="currentIssueRewards"
:grouped="true"
:rewards="previewRewards"
:grouped="detail?.play_type !== 'match'"
@view-all="openRewardsPopup"
/>
@ -196,6 +196,7 @@ import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
import LotteryResultPopup from '@/components/activity/LotteryResultPopup.vue'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards, getUserCoupons, getItemCards, createWechatOrder, getMatchingCardTypes, createMatchingPreorder, checkMatchingGame, getIssueDrawLogs, getMatchingGameCards } from '../../../api/appUser'
import { levelToAlpha } from '@/utils/activity'
import { vibrateShort } from '@/utils/vibrate.js'
const detail = ref({})
const statusText = ref('')
@ -314,27 +315,64 @@ const currentIssueRewards = computed(() => {
return (iid && Array.isArray(m[iid])) ? m[iid] : []
})
// rewards
const previewRewards = computed(() => {
const isMatchType = detail.value?.play_type === 'match'
if (isMatchType) {
// min_score
return [...currentIssueRewards.value].sort((a, b) => (a.min_score - b.min_score))
} else {
//
return currentIssueRewards.value
}
})
const rewardGroups = computed(() => {
const isMatchType = detail.value?.play_type === 'match'
// min_score
if (isMatchType) {
// min_score
const sortedRewards = [...currentIssueRewards.value].sort((a, b) => (a.min_score - b.min_score))
//
return sortedRewards.map(item => ({
level: `${item.min_score}对子`,
rewards: [item],
totalPercent: item.percent.toFixed(1)
}))
}
//
const groups = {}
currentIssueRewards.value.forEach(item => {
let level = item.level || '赏'
// min_score > 0
if (item.min_score > 0 && level !== 'BOSS') {
level = `${item.min_score}对子`
}
if (!groups[level]) groups[level] = []
groups[level].push(item)
})
return Object.keys(groups).sort((a, b) => {
// Last BOSS
if (a === 'Last' || a === 'BOSS') return -1
if (b === 'Last' || b === 'BOSS') return 1
// weight
// weight
const minWeightA = Math.min(...groups[a].map(item => item.weight || 0))
const minWeightB = Math.min(...groups[b].map(item => item.weight || 0))
return minWeightA - minWeightB
}).map(key => {
const rewards = groups[key]
// weight
// weight
rewards.sort((a, b) => (a.weight - b.weight))
const total = rewards.reduce((sum, item) => sum + (Number(item.percent) || 0), 0)
return {
level: key,
@ -511,7 +549,7 @@ function normalizeIssues(list) {
status_text: i.status_text ?? (i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : i.status === 2 ? '已结束' : '')
}))
}
function normalizeRewards(list) {
function normalizeRewards(list, playType = 'normal') {
const arr = unwrap(list)
const items = arr.map((i, idx) => ({
...i, // Spread original properties first
@ -528,8 +566,16 @@ function normalizeRewards(list) {
...it,
percent: total > 0 ? Math.round((it.weight / total) * 1000) / 10 : 0
}))
// weight
enriched.sort((a, b) => (a.weight - b.weight))
// play_type
if (playType === 'match') {
// min_score min_score=0
enriched.sort((a, b) => (a.min_score - b.min_score))
} else {
// weight
enriched.sort((a, b) => (a.weight - b.weight))
}
return enriched
}
async function fetchRewardsForIssues(activityId) {
@ -537,10 +583,13 @@ async function fetchRewardsForIssues(activityId) {
const promises = list.map(it => getActivityIssueRewards(activityId, it.id))
const results = await Promise.allSettled(promises)
// play_type
const playType = detail.value?.play_type || 'normal'
results.forEach((res, i) => {
const issueId = list[i] && list[i].id
if (!issueId) return
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
const value = res.status === 'fulfilled' ? normalizeRewards(res.value, playType) : []
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: value }
})
}
@ -847,7 +896,7 @@ function drawOne() {
function manualDraw() {
if (gameLoading.value) return
if (!canManualDraw.value) return
uni.vibrateShort({ type: 'light' })
vibrateShort()
drawOne()
chance.value = Math.max(0, Number(chance.value || 0) - 1)
pickedHandIndex.value = -1
@ -879,7 +928,7 @@ async function autoDrawIfStuck() {
async function onCellTap(cell) {
if (gameLoading.value) return
if (!cell || cell.empty) return
uni.vibrateShort({ type: 'light' })
vibrateShort()
const hi = Number(cell.handIndex)
if (!Number.isFinite(hi) || hi < 0) return
@ -1019,7 +1068,7 @@ function onResultClose() {
async function advanceOne() {
if (gameLoading.value) return
uni.vibrateShort({ type: 'light' })
vibrateShort()
const entry = gameEntry.value || null
const gameId = entry && entry.game_id ? String(entry.game_id) : ''
if (!gameId) return
@ -1077,7 +1126,7 @@ async function autoRun() {
}
async function onParticipate() {
uni.vibrateShort({ type: 'medium' })
vibrateShort()
const aid = activityId.value || ''
const iid = currentIssueId.value || ''
if (!aid || !iid) { uni.showToast({ title: '期数未选择', icon: 'none' }); return }
@ -1123,7 +1172,7 @@ async function applyResumeEntry(entry) {
}
async function onResumeGame() {
uni.vibrateShort({ type: 'medium' })
vibrateShort()
const aid = activityId.value || ''
const latest = syncResumeGame(aid)
if (!latest || !latest.entry || !latest.entry.game_id) return

View File

@ -51,16 +51,19 @@
<!-- 选号组件 - 隐藏内置操作栏 -->
<view class="selector-body" v-if="activityId && currentIssueId">
<YifanSelector
<YifanSelector
ref="yifanSelectorRef"
:activity-id="activityId"
:issue-id="currentIssueId"
:activity-id="activityId"
:issue-id="currentIssueId"
:price-per-draw="Number(detail.price_draw || 0) / 100"
:disabled="!isOrderAllowed"
:disabled-text="orderBlockedReason"
:hide-action-bar="true"
@payment-success="onPaymentSuccess"
@selection-change="onSelectionChange"
@payment-visible-change="onPaymentVisibleChange"
@payment-amount-change="onPaymentAmountChange"
@payment-coupons-change="onPaymentCouponsChange"
/>
</view>
</view>
@ -68,7 +71,7 @@
<template #footer>
<!-- 固定底部操作栏 -->
<view class="float-bar">
<view class="float-bar" v-show="!isPaymentVisible">
<view class="float-bar-inner">
<view class="selection-info" v-if="selectedCount > 0">
已选 <text class="highlight">{{ selectedCount }}</text> 个位置
@ -112,6 +115,16 @@
v-model:visible="cabinetVisible"
:activity-id="activityId"
/>
<!-- 支付弹窗 YifanSelector 提升到这里确保祝福动画位置正确 -->
<PaymentPopup
v-model:visible="paymentVisible"
:amount="paymentAmount"
:coupons="paymentCoupons"
:showCards="false"
@confirm="onPaymentConfirm"
@cancel="onPaymentCancel"
/>
</template>
</ActivityPageLayout>
</template>
@ -130,6 +143,7 @@ import RulesPopup from '@/components/activity/RulesPopup.vue'
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
import FlipGrid from '@/components/FlipGrid.vue'
import YifanSelector from '@/components/YifanSelector.vue'
import PaymentPopup from '@/components/PaymentPopup.vue'
// Composables
import { useActivity, useIssues, useRewards, useRecords } from '../../composables'
// Utils
@ -174,12 +188,53 @@ const showFlip = ref(false)
const flipRef = ref(null)
const yifanSelectorRef = ref(null)
const selectedCount = ref(0) //
const isPaymentVisible = ref(false) //
const paymentVisible = ref(false) //
const paymentAmount = ref('0') //
const paymentCoupons = ref([]) //
//
function onSelectionChange(items) {
selectedCount.value = Array.isArray(items) ? items.length : 0
}
// YifanSelector
function onPaymentVisibleChange(visible) {
isPaymentVisible.value = visible
paymentVisible.value = visible
}
//
function onPaymentAmountChange(amount) {
paymentAmount.value = amount
}
//
function onPaymentCouponsChange(coupons) {
paymentCoupons.value = coupons
}
// YifanSelector
async function onPaymentConfirm(paymentData) {
if (yifanSelectorRef.value && yifanSelectorRef.value.onPaymentConfirm) {
await yifanSelectorRef.value.onPaymentConfirm(paymentData)
}
}
//
function onPaymentCancel() {
// PaymentPopup v-model paymentVisible
// watch YifanSelector
}
// YifanSelector
watch(paymentVisible, (newVal) => {
// YifanSelector
if (!newVal && yifanSelectorRef.value && yifanSelectorRef.value.setPaymentVisible) {
yifanSelectorRef.value.setPaymentVisible(false)
}
})
//
function handleRandomDraw() {
if (yifanSelectorRef.value && yifanSelectorRef.value.handleRandomOne) {

View File

@ -124,6 +124,7 @@
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getUserCoupons } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const list = ref([])
const loading = ref(false)
@ -214,7 +215,7 @@ function getCouponClass() {
// Tab
function switchTab(tab) {
if (currentTab.value === tab) return
uni.vibrateShort({ type: 'light' })
vibrateShort()
currentTab.value = tab
list.value = []
page.value = 1
@ -271,7 +272,7 @@ async function fetchData(append = false) {
// 使
function onUseCoupon(item) {
uni.vibrateShort({ type: 'medium' })
vibrateShort()
//
uni.switchTab({
url: '/pages/index/index'

View File

@ -125,6 +125,7 @@
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getItemCards } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const list = ref([])
const loading = ref(false)
@ -193,7 +194,7 @@ function getCardIcon(type) {
// Tab
function switchTab(tab) {
if (currentTab.value === tab) return
uni.vibrateShort({ type: 'light' })
vibrateShort()
currentTab.value = tab
list.value = []
page.value = 1
@ -251,7 +252,7 @@ async function fetchData(append = false) {
// 使
function onUseCard(item) {
uni.vibrateShort({ type: 'medium' })
vibrateShort()
//
uni.switchTab({
url: '/pages/index/index'

View File

@ -129,6 +129,7 @@
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getOrders, cancelOrder as cancelOrderApi, createWechatOrder } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const currentTab = ref('pending')
const orders = ref([])
@ -289,7 +290,7 @@ function getStatusClass(item) {
function switchTab(tab) {
if (currentTab.value === tab) return
uni.vibrateShort({ type: 'light' })
vibrateShort()
currentTab.value = tab
fetchOrders(false)
}

View File

@ -128,6 +128,7 @@
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getTasks, getTaskProgress, claimTaskReward } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const tasks = ref([])
const loading = ref(false)
@ -315,8 +316,8 @@ function getTierProgressText(task, tier) {
async function claimReward(task, tier) {
const key = `${task.id}_${tier.id}`
if (claiming[key]) return
uni.vibrateShort({ type: 'medium' })
vibrateShort()
claiming[key] = true
try {
const userId = getUserId()

View File

@ -164,6 +164,7 @@
import { ref, computed } from 'vue'
import { onShow, onReachBottom, onShareAppMessage, onPullDownRefresh } from '@dcloudio/uni-app'
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare } from '@/api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const currentTab = ref(0)
const aggregatedList = ref([])
@ -514,7 +515,7 @@ async function loadInventory(uid) {
}
function toggleSelect(item) {
uni.vibrateShort({ type: 'light' })
vibrateShort()
item.selected = !item.selected
if (item.selected) {
//
@ -529,7 +530,7 @@ function toggleSelect(item) {
}
function toggleSelectAll() {
uni.vibrateShort({ type: 'light' })
vibrateShort()
const newState = !isAllSelected.value
aggregatedList.value.forEach(item => {
item.selected = newState
@ -554,7 +555,7 @@ function changeCount(item, delta) {
}
async function onRedeem() {
uni.vibrateShort({ type: 'medium' })
vibrateShort()
const user_id = uni.getStorageSync('user_id')
if (!user_id) return
@ -602,7 +603,7 @@ async function onRedeem() {
}
async function onShip() {
uni.vibrateShort({ type: 'medium' })
vibrateShort()
const user_id = uni.getStorageSync('user_id')
if (!user_id) return
@ -685,7 +686,7 @@ onShareAppMessage((res) => {
})
async function onInvite(item) {
uni.vibrateShort({ type: 'medium' })
vibrateShort()
const user_id = uni.getStorageSync('user_id')
if (!user_id) {
uni.navigateTo({ url: '/pages/login/index' })

View File

@ -149,6 +149,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { request } from '../../utils/request'
import { wechatLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const loading = ref(false)
const agreementChecked = ref(false)
@ -235,7 +236,7 @@ function toPurchaseAgreement() {
async function handleSendCode() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort()
vibrateShort()
return
}
@ -276,7 +277,7 @@ async function handleSendCode() {
async function handleSmsLogin() {
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort()
vibrateShort()
return
}
@ -315,7 +316,7 @@ async function handleSmsLogin() {
function onGetPhoneNumber(e) {
if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort()
vibrateShort()
return
}

View File

@ -1,40 +0,0 @@
/**
* 祝福动画工具
* 用于在支付弹窗等场景显示祝福动画
*/
// 使用简单的全局变量来存储回调函数
let blessingCallback = null
/**
* 显示祝福动画
* @param {Object} options - 配置选项
* @param {string} options.type - 祝福类型 'sheep' | 'horse' | 'random'
*/
export function showBlessing(options = {}) {
const type = options.type || 'random'
console.log('[showBlessing] 触发祝福动画, type:', type)
if (blessingCallback) {
blessingCallback({ type })
} else {
console.warn('[showBlessing] 没有注册的监听器')
}
}
/**
* 注册祝福动画监听
* @param {Function} callback - 回调函数
*/
export function registerBlessing(callback) {
blessingCallback = callback
console.log('[registerBlessing] 祝福动画监听器已注册')
}
/**
* 移除祝福动画监听
*/
export function unregisterBlessing() {
blessingCallback = null
console.log('[unregisterBlessing] 祝福动画监听器已移除')
}

52
utils/vibrate.js Normal file
View File

@ -0,0 +1,52 @@
/**
* 震动工具函数
* 统一处理不同平台的震动API兼容性
*/
/**
* 短震动
* 微信小程序不支持 type 参数会忽略该参数
* @param {Object} options - 配置项
* @param {string} options.type - 震动类型 'light' | 'medium' | 'heavy'仅在部分平台有效
*/
export function vibrateShort(options = {}) {
// #ifdef MP-WEIXIN
// 微信小程序不支持 type 参数,直接调用
uni.vibrateShort({
fail: (err) => {
console.warn('[vibrateShort] 震动失败:', err)
}
})
// #endif
// #ifdef H5 || APP-PLUS
// H5和App可能支持 type 参数
uni.vibrateShort({
...options,
fail: (err) => {
console.warn('[vibrateShort] 震动失败:', err)
}
})
// #endif
// #ifdef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO
// 其他小程序平台,尝试传递参数
uni.vibrateShort({
...options,
fail: (err) => {
console.warn('[vibrateShort] 震动失败:', err)
}
})
// #endif
}
/**
* 长震动
*/
export function vibrateLong() {
uni.vibrateLong({
fail: (err) => {
console.warn('[vibrateLong] 震动失败:', err)
}
})
}