bindbox-mini/components/PaymentPopup.vue

377 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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>
</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 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
}))
})
// Auto-select if only one card available? Or existing logic is fine.
// The watch logic handles index reset.
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
// Auto-select best coupon?
// Current logic just selects the first one if none selected and count > 0?
// "if (couponIndex.value < 0) couponIndex.value = 0"
// This logic is preserved below.
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/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>
/* ============================================
柯大鸭潮玩 - 支付弹窗组件
采用暖橙色调的底部弹窗设计
============================================ */
.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);
}
@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>