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>
<!-- 祝福动画 --> <!-- 祝福动画 -->
<view v-if="showBlessing" class="blessing-container"> <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 class="blessing-emoji">{{ currentBlessing.emoji }}</view>
<view v-if="currentBlessing.type === 'sheep'" class="blessing-subtitle">小羊祝你</view> <view v-if="currentBlessing.type === 'sheep'" class="blessing-subtitle">小羊祝你</view>
<view class="blessing-text"> <view class="blessing-text">
@ -117,8 +117,33 @@ const blessings = [
}, },
{ {
emoji: '🐴', emoji: '🐴',
chars: ['马', '到', '功', '成'], chars: ['一', '马', '当', '先'],
type: 'horse' 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]) const currentBlessing = ref(blessings[0])
@ -242,17 +267,16 @@ function handleConfirm() {
.blessing-container { .blessing-container {
position: fixed; position: fixed;
top: 50%; top: 0;
left: 50%; left: 0;
transform: translate(-50%, -50%); right: 0;
bottom: 0;
z-index: 10000; z-index: 10000;
pointer-events: none; pointer-events: none;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20rpx; padding: 20rpx;
width: 100%;
max-width: 600rpx;
} }
.blessing-animation { .blessing-animation {
@ -285,13 +309,38 @@ function handleConfirm() {
} }
// - // -
.blessing-animation:has(.blessing-subtitle) .blessing-emoji { .blessing-animation.sheep .blessing-emoji {
animation: emojiBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55); animation: emojiBounce 1.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
} }
// - // -
.blessing-animation:not(:has(.blessing-subtitle)) .blessing-emoji { .blessing-animation.horse .blessing-emoji {
animation: emojiRun 0.8s ease-out; 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 { @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 { .blessing-subtitle {
font-size: 28rpx; font-size: 28rpx;
color: #FF9500; color: #FF9500;

View File

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

View File

@ -111,10 +111,14 @@ defineProps({
} }
.bg-image { .bg-image {
width: 100%; width: 115%;
height: 100%; 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); filter: blur(40rpx) brightness(0.85) saturate(1.1);
transform: scale(1.15);
} }
.bg-mask { .bg-mask {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -164,6 +164,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { onShow, onReachBottom, onShareAppMessage, onPullDownRefresh } from '@dcloudio/uni-app' import { onShow, onReachBottom, onShareAppMessage, onPullDownRefresh } from '@dcloudio/uni-app'
import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare } from '@/api/appUser' import { getInventory, getProductDetail, redeemInventory, requestShipping, cancelShipping, listAddresses, getShipments, createAddressShare } from '@/api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const currentTab = ref(0) const currentTab = ref(0)
const aggregatedList = ref([]) const aggregatedList = ref([])
@ -514,7 +515,7 @@ async function loadInventory(uid) {
} }
function toggleSelect(item) { function toggleSelect(item) {
uni.vibrateShort({ type: 'light' }) vibrateShort()
item.selected = !item.selected item.selected = !item.selected
if (item.selected) { if (item.selected) {
// //
@ -529,7 +530,7 @@ function toggleSelect(item) {
} }
function toggleSelectAll() { function toggleSelectAll() {
uni.vibrateShort({ type: 'light' }) vibrateShort()
const newState = !isAllSelected.value const newState = !isAllSelected.value
aggregatedList.value.forEach(item => { aggregatedList.value.forEach(item => {
item.selected = newState item.selected = newState
@ -554,7 +555,7 @@ function changeCount(item, delta) {
} }
async function onRedeem() { async function onRedeem() {
uni.vibrateShort({ type: 'medium' }) vibrateShort()
const user_id = uni.getStorageSync('user_id') const user_id = uni.getStorageSync('user_id')
if (!user_id) return if (!user_id) return
@ -602,7 +603,7 @@ async function onRedeem() {
} }
async function onShip() { async function onShip() {
uni.vibrateShort({ type: 'medium' }) vibrateShort()
const user_id = uni.getStorageSync('user_id') const user_id = uni.getStorageSync('user_id')
if (!user_id) return if (!user_id) return
@ -685,7 +686,7 @@ onShareAppMessage((res) => {
}) })
async function onInvite(item) { async function onInvite(item) {
uni.vibrateShort({ type: 'medium' }) vibrateShort()
const user_id = uni.getStorageSync('user_id') const user_id = uni.getStorageSync('user_id')
if (!user_id) { if (!user_id) {
uni.navigateTo({ url: '/pages/login/index' }) 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 { onLoad } from '@dcloudio/uni-app'
import { request } from '../../utils/request' import { request } from '../../utils/request'
import { wechatLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser' import { wechatLogin, bindPhone, getUserStats, getPointsBalance, sendSmsCode, smsLogin } from '../../api/appUser'
import { vibrateShort } from '@/utils/vibrate.js'
const loading = ref(false) const loading = ref(false)
const agreementChecked = ref(false) const agreementChecked = ref(false)
@ -235,7 +236,7 @@ function toPurchaseAgreement() {
async function handleSendCode() { async function handleSendCode() {
if (!agreementChecked.value) { if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' }) uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort() vibrateShort()
return return
} }
@ -276,7 +277,7 @@ async function handleSendCode() {
async function handleSmsLogin() { async function handleSmsLogin() {
if (!agreementChecked.value) { if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' }) uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort() vibrateShort()
return return
} }
@ -315,7 +316,7 @@ async function handleSmsLogin() {
function onGetPhoneNumber(e) { function onGetPhoneNumber(e) {
if (!agreementChecked.value) { if (!agreementChecked.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' }) uni.showToast({ title: '请先同意用户协议', icon: 'none' })
uni.vibrateShort() vibrateShort()
return 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)
}
})
}