606 lines
14 KiB
Vue
606 lines
14 KiB
Vue
<template>
|
||
<view>
|
||
<!-- 祝福动画 -->
|
||
<view v-if="showBlessing" class="blessing-container">
|
||
<view class="blessing-animation">
|
||
<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>
|
||
|
||
<!-- 支付弹窗 -->
|
||
<view v-if="visible" class="payment-popup-mask" @tap="handleMaskClick">
|
||
<view class="payment-popup-content" @tap.stop>
|
||
<!-- 顶部提示 -->
|
||
<view class="risk-warning">
|
||
<text>盲盒具有随机性,请理性消费,购买即表示同意</text>
|
||
<text class="agreement-link" @tap="openAgreement">《购买协议》</text>
|
||
</view>
|
||
|
||
<view class="popup-header">
|
||
<text class="popup-title">确认支付</text>
|
||
<view class="close-icon" @tap="handleClose">×</view>
|
||
</view>
|
||
|
||
<view class="popup-body">
|
||
<view class="amount-section" v-if="amount !== undefined && amount !== null">
|
||
<text class="label">支付金额</text>
|
||
<text class="amount">¥{{ finalPayAmount }}</text>
|
||
<text v-if="finalPayAmount < amount" class="original-amount" style="text-decoration: line-through; color: #999; font-size: 24rpx; margin-left: 10rpx;">¥{{ amount }}</text>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">优惠券</text>
|
||
<picker
|
||
class="picker-full"
|
||
mode="selector"
|
||
:range="coupons"
|
||
range-key="name"
|
||
@change="onCouponChange"
|
||
:value="couponIndex"
|
||
:disabled="!coupons || coupons.length === 0"
|
||
>
|
||
<view class="picker-display">
|
||
<text v-if="selectedCoupon" class="selected-text">
|
||
{{ selectedCoupon.name }} (-¥{{ effectiveCouponDiscount.toFixed(2) }})
|
||
<text v-if="selectedCoupon.amount > maxDeductible" style="font-size: 20rpx; color: #FF9800;">(最高抵扣50%)</text>
|
||
</text>
|
||
<text v-else-if="!coupons || coupons.length === 0" class="placeholder">暂无优惠券可用</text>
|
||
<text v-else class="placeholder">请选择优惠券</text>
|
||
<text class="arrow"></text>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="form-item" v-if="showCards">
|
||
<text class="label">道具卡</text>
|
||
<picker
|
||
class="picker-full"
|
||
mode="selector"
|
||
:range="displayCards"
|
||
range-key="displayName"
|
||
@change="onCardChange"
|
||
:value="cardIndex"
|
||
:disabled="!displayCards || displayCards.length === 0"
|
||
>
|
||
<view class="picker-display">
|
||
<text v-if="selectedCard" class="selected-text">
|
||
{{ selectedCard.name }}
|
||
<text v-if="Number(selectedCard.count) > 1" style="color: #999; font-size: 24rpx; margin-left: 6rpx;">(拥有: {{ selectedCard.count }})</text>
|
||
</text>
|
||
<text v-else-if="!displayCards || displayCards.length === 0" class="placeholder">暂无道具卡可用</text>
|
||
<text v-else class="placeholder">请选择道具卡</text>
|
||
<text class="arrow"></text>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="popup-footer">
|
||
<button class="btn-cancel" @tap="handleClose">取消</button>
|
||
<button class="btn-confirm" @tap="handleConfirm">确认支付</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue'
|
||
|
||
const props = defineProps({
|
||
visible: { type: Boolean, default: false },
|
||
amount: { type: [Number, String], default: 0 },
|
||
coupons: { type: Array, default: () => [] },
|
||
propCards: { type: Array, default: () => [] },
|
||
showCards: { type: Boolean, default: true }
|
||
})
|
||
|
||
const emit = defineEmits(['update:visible', 'confirm', 'cancel'])
|
||
|
||
// 祝福动画相关
|
||
const showBlessing = ref(false)
|
||
const blessings = [
|
||
{
|
||
emoji: '🐏',
|
||
chars: ['三', '羊', '开', '泰'],
|
||
type: 'sheep'
|
||
},
|
||
{
|
||
emoji: '🐴',
|
||
chars: ['马', '到', '功', '成'],
|
||
type: 'horse'
|
||
}
|
||
]
|
||
const currentBlessing = ref(blessings[0])
|
||
|
||
// 监听弹窗打开,显示祝福动画
|
||
watch(() => props.visible, (newVal) => {
|
||
console.log('[PaymentPopup] visible changed:', newVal)
|
||
if (newVal) {
|
||
// 随机选择祝福语
|
||
const index = Math.floor(Math.random() * blessings.length)
|
||
currentBlessing.value = blessings[index]
|
||
|
||
// 延迟显示祝福动画
|
||
setTimeout(() => {
|
||
console.log('[PaymentPopup] 显示祝福动画')
|
||
showBlessing.value = true
|
||
|
||
// 3秒后隐藏祝福动画
|
||
setTimeout(() => {
|
||
showBlessing.value = false
|
||
console.log('[PaymentPopup] 隐藏祝福动画')
|
||
}, 3000)
|
||
}, 300) // 延迟300ms,让支付弹窗先滑入
|
||
} else {
|
||
showBlessing.value = false
|
||
}
|
||
})
|
||
|
||
const couponIndex = ref(-1)
|
||
const cardIndex = ref(-1)
|
||
|
||
const selectedCoupon = computed(() => {
|
||
if (couponIndex.value >= 0 && props.coupons[couponIndex.value]) {
|
||
return props.coupons[couponIndex.value]
|
||
}
|
||
return null
|
||
})
|
||
|
||
const maxDeductible = computed(() => {
|
||
const amt = Number(props.amount) || 0
|
||
return amt * 0.5
|
||
})
|
||
|
||
const effectiveCouponDiscount = computed(() => {
|
||
if (!selectedCoupon.value) return 0
|
||
const couponAmt = Number(selectedCoupon.value.amount) || 0
|
||
return Math.min(couponAmt, maxDeductible.value)
|
||
})
|
||
|
||
const displayCards = computed(() => {
|
||
if (!Array.isArray(props.propCards)) return []
|
||
return props.propCards.map(c => ({
|
||
...c,
|
||
displayName: (c.count && Number(c.count) > 1) ? `${c.name} (x${c.count})` : c.name
|
||
}))
|
||
})
|
||
|
||
const selectedCard = computed(() => {
|
||
if (cardIndex.value >= 0 && displayCards.value[cardIndex.value]) {
|
||
return displayCards.value[cardIndex.value]
|
||
}
|
||
return null
|
||
})
|
||
|
||
const finalPayAmount = computed(() => {
|
||
const amt = Number(props.amount) || 0
|
||
return Math.max(0, amt - effectiveCouponDiscount.value).toFixed(2)
|
||
})
|
||
|
||
watch(
|
||
[() => props.visible, () => (Array.isArray(props.coupons) ? props.coupons.length : 0)],
|
||
([vis, len]) => {
|
||
if (!vis) return
|
||
cardIndex.value = -1
|
||
if (len <= 0) {
|
||
couponIndex.value = -1
|
||
return
|
||
}
|
||
if (couponIndex.value < 0) {
|
||
couponIndex.value = 0
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
function onCouponChange(e) {
|
||
couponIndex.value = e.detail.value
|
||
}
|
||
|
||
function onCardChange(e) {
|
||
cardIndex.value = e.detail.value
|
||
}
|
||
|
||
function openAgreement() {
|
||
uni.navigateTo({
|
||
url: '/pages-user/agreement/purchase'
|
||
})
|
||
}
|
||
|
||
function handleMaskClick() {
|
||
handleClose()
|
||
}
|
||
|
||
function handleClose() {
|
||
emit('update:visible', false)
|
||
emit('cancel')
|
||
}
|
||
|
||
function handleConfirm() {
|
||
emit('confirm', {
|
||
coupon: selectedCoupon.value,
|
||
card: props.showCards ? selectedCard.value : null
|
||
})
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
/* ============================================
|
||
祝福动画样式
|
||
============================================ */
|
||
|
||
.blessing-container {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 10000;
|
||
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:has(.blessing-subtitle) .blessing-emoji {
|
||
animation: emojiBounce 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||
}
|
||
|
||
// 小马动画 - 从左边跑步进场
|
||
.blessing-animation: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: 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);
|
||
}
|
||
}
|
||
|
||
/* ============================================
|
||
柯大鸭潮玩 - 支付弹窗组件
|
||
采用暖橙色调的底部弹窗设计
|
||
============================================ */
|
||
|
||
.payment-popup-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.55);
|
||
z-index: 999;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.payment-popup-content {
|
||
width: 100%;
|
||
background: $bg-card;
|
||
border-radius: $radius-xl $radius-xl 0 0;
|
||
padding: $spacing-lg;
|
||
padding-bottom: calc($spacing-lg + constant(safe-area-inset-bottom));
|
||
padding-bottom: calc($spacing-lg + env(safe-area-inset-bottom));
|
||
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
position: relative;
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from { transform: translateY(100%); }
|
||
to { transform: translateY(0); }
|
||
}
|
||
|
||
.risk-warning {
|
||
background: linear-gradient(135deg, #FFF8F3, #FFF4E6);
|
||
color: #B45309;
|
||
font-size: $font-xs;
|
||
padding: $spacing-sm $spacing-md;
|
||
border-radius: $radius-md;
|
||
margin-bottom: $spacing-md;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-wrap: wrap;
|
||
line-height: 1.5;
|
||
text-align: center;
|
||
border: 1rpx solid rgba(255, 159, 67, 0.2);
|
||
}
|
||
|
||
.agreement-link {
|
||
color: $brand-primary;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.popup-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $spacing-lg;
|
||
position: relative;
|
||
}
|
||
.popup-title {
|
||
font-size: $font-lg;
|
||
font-weight: 700;
|
||
color: $text-main;
|
||
}
|
||
.close-icon {
|
||
position: absolute;
|
||
right: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 48rpx;
|
||
color: $text-placeholder;
|
||
line-height: 1;
|
||
padding: 10rpx;
|
||
transition: color 0.2s ease;
|
||
}
|
||
.close-icon:active {
|
||
color: $text-secondary;
|
||
}
|
||
|
||
.popup-body {
|
||
padding: $spacing-sm 0 $spacing-md;
|
||
}
|
||
|
||
.amount-section {
|
||
text-align: center;
|
||
margin-bottom: $spacing-xl;
|
||
padding: $spacing-md;
|
||
background: linear-gradient(145deg, #FFFFFF, #FFF8F3);
|
||
border-radius: $radius-lg;
|
||
border: 1rpx solid rgba(255, 159, 67, 0.1);
|
||
}
|
||
.amount-section .label {
|
||
font-size: $font-sm;
|
||
color: $text-secondary;
|
||
margin-right: $spacing-xs;
|
||
}
|
||
.amount-section .amount {
|
||
font-size: 56rpx;
|
||
font-weight: 800;
|
||
background: $gradient-brand;
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: $spacing-md;
|
||
}
|
||
.form-item .label {
|
||
display: block;
|
||
font-size: $font-sm;
|
||
color: $text-main;
|
||
font-weight: 600;
|
||
margin-bottom: $spacing-xs;
|
||
}
|
||
|
||
.picker-full {
|
||
width: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.picker-display {
|
||
border: 2rpx solid $border-color-light;
|
||
border-radius: $radius-md;
|
||
padding: $spacing-md;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: $font-sm;
|
||
background: $bg-page;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.picker-display:active {
|
||
border-color: $brand-primary;
|
||
background: #FFF8F3;
|
||
}
|
||
|
||
.selected-text {
|
||
color: $text-main;
|
||
font-weight: 500;
|
||
}
|
||
.placeholder {
|
||
color: $text-placeholder;
|
||
}
|
||
.arrow {
|
||
color: $text-placeholder;
|
||
width: 16rpx;
|
||
height: 16rpx;
|
||
border-right: 3rpx solid $text-placeholder;
|
||
border-bottom: 3rpx solid $text-placeholder;
|
||
transform: rotate(-45deg);
|
||
margin-right: $spacing-xs;
|
||
}
|
||
|
||
.popup-footer {
|
||
display: flex;
|
||
gap: $spacing-md;
|
||
margin-top: $spacing-sm;
|
||
}
|
||
.btn-cancel, .btn-confirm {
|
||
flex: 1;
|
||
border: none;
|
||
border-radius: $radius-lg;
|
||
font-size: $font-md;
|
||
padding: $spacing-md 0;
|
||
line-height: 1.5;
|
||
font-weight: 600;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.btn-cancel::after, .btn-confirm::after {
|
||
border: none;
|
||
}
|
||
.btn-cancel {
|
||
color: $text-secondary;
|
||
background: #F3F4F6;
|
||
}
|
||
.btn-cancel:active {
|
||
background: $border-color-light;
|
||
}
|
||
.btn-confirm {
|
||
color: #FFFFFF;
|
||
background: $gradient-brand;
|
||
box-shadow: $shadow-lg;
|
||
}
|
||
.btn-confirm:active {
|
||
transform: scale(0.97);
|
||
box-shadow: $shadow-md;
|
||
}
|
||
</style>
|