feat: 新增 BoxReveal 和 LotteryResultPopup 组件,优化对对碰活动道具卡聚合逻辑,并调整商店道具卡页面为“暂未开放”提示。
This commit is contained in:
parent
7e08aa5f43
commit
6183fcaf15
338
components/BoxReveal.vue
Normal file
338
components/BoxReveal.vue
Normal file
@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<view class="box-reveal-root">
|
||||
|
||||
<!-- Stage 1: The Box -->
|
||||
<view v-if="stage === 'box'" class="box-stage" :class="{ shaking: isShaking }">
|
||||
<view class="mystery-box">
|
||||
<image class="box-img" src="/static/images/mystery-box.png" mode="widthFix" />
|
||||
<view class="box-glow"></view>
|
||||
</view>
|
||||
<text class="box-tip">{{ isShaking ? '正在开启...' : '准备开启' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- Stage 2: Reveal Results -->
|
||||
<view v-else-if="stage === 'result'" class="result-stage">
|
||||
<view class="result-light-burst"></view>
|
||||
|
||||
<!-- Single Reward -->
|
||||
<view v-if="rewards.length === 1" class="single-reward">
|
||||
<view class="reward-card large bounce-in">
|
||||
<image class="reward-img" :src="rewards[0].image" mode="aspectFit" />
|
||||
<view class="reward-info">
|
||||
<text class="reward-name">{{ rewards[0].title }}</text>
|
||||
<text class="reward-desc">恭喜获得</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Multiple Rewards (Horizontal Scroll) -->
|
||||
<scroll-view v-else scroll-x class="multi-rewards-scroll">
|
||||
<view class="rewards-track">
|
||||
<view
|
||||
v-for="(item, index) in rewards"
|
||||
:key="index"
|
||||
class="reward-card small slide-in-right"
|
||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||
>
|
||||
<view class="card-inner">
|
||||
<image class="reward-img" :src="item.image" mode="aspectFit" />
|
||||
<text class="reward-name">{{ item.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="action-area">
|
||||
<button class="confirm-btn" @tap="onConfirm">收下奖励</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
// No specific props needed for now, rewards passed via method
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const stage = ref('box') // box, result
|
||||
const isShaking = ref(false)
|
||||
const rewards = ref([])
|
||||
|
||||
// Public method to reset state
|
||||
function reset() {
|
||||
stage.value = 'box'
|
||||
isShaking.value = false
|
||||
rewards.value = []
|
||||
}
|
||||
|
||||
// Public method to reveal results
|
||||
function revealResults(list) {
|
||||
const arr = Array.isArray(list) ? list : (list ? [list] : [])
|
||||
rewards.value = arr
|
||||
|
||||
// Start animation sequence
|
||||
isShaking.value = true
|
||||
|
||||
// Shake for 1.5s then open
|
||||
setTimeout(() => {
|
||||
isShaking.value = false
|
||||
stage.value = 'result'
|
||||
uni.vibrateLong()
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
defineExpose({ reset, revealResults })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box-reveal-root {
|
||||
width: 100%;
|
||||
min-height: 600rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Stage 1: Box */
|
||||
.box-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mystery-box {
|
||||
width: 400rpx;
|
||||
height: 400rpx;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.box-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
// Fallback if image missing, use a block
|
||||
min-height: 300rpx;
|
||||
}
|
||||
|
||||
.box-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 500rpx;
|
||||
height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.6) 0%, transparent 70%);
|
||||
z-index: 1;
|
||||
filter: blur(40rpx);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.box-tip {
|
||||
margin-top: 40rpx;
|
||||
font-size: 32rpx;
|
||||
color: $text-main;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.shaking .mystery-box {
|
||||
animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) infinite both;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% { transform: translate3d(-2px, 0, 0) rotate(-2deg); }
|
||||
20%, 80% { transform: translate3d(4px, 0, 0) rotate(2deg); }
|
||||
30%, 50%, 70% { transform: translate3d(-8px, 0, 0) rotate(-4deg); }
|
||||
40%, 60% { transform: translate3d(8px, 0, 0) rotate(4deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.5; transform: translate(-50%, -50%) scale(0.9); }
|
||||
50% { opacity: 0.8; transform: translate(-50%, -50%) scale(1.1); }
|
||||
100% { opacity: 0.5; transform: translate(-50%, -50%) scale(0.9); }
|
||||
}
|
||||
|
||||
/* Stage 2: Result */
|
||||
.result-stage {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: fade-in 0.5s ease-out;
|
||||
}
|
||||
|
||||
.result-light-burst {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 800rpx;
|
||||
height: 800rpx;
|
||||
background: radial-gradient(circle, rgba(255, 200, 50, 0.2) 0%, transparent 70%);
|
||||
animation: rotate-slow 10s linear infinite;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Single Reward */
|
||||
.single-reward {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.reward-card.large {
|
||||
width: 460rpx;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
padding: 40rpx;
|
||||
box-shadow: 0 20rpx 60rpx rgba(0,0,0,0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 4rpx solid $bg-secondary;
|
||||
}
|
||||
|
||||
.reward-card.large .reward-img {
|
||||
width: 320rpx;
|
||||
height: 320rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.reward-card.large .reward-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: $text-main;
|
||||
text-align: center;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.reward-card.large .reward-desc {
|
||||
font-size: 24rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
/* Multiple Rewards */
|
||||
.multi-rewards-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
padding: 20rpx 0;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.rewards-track {
|
||||
display: flex;
|
||||
padding: 0 40rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reward-card.small {
|
||||
display: inline-block;
|
||||
width: 240rpx;
|
||||
height: 320rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-right: 24rpx;
|
||||
box-shadow: $shadow-md;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.reward-card.small .reward-img {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-bottom: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.reward-card.small .reward-name {
|
||||
font-size: 24rpx;
|
||||
color: $text-main;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.bounce-in {
|
||||
animation: bounce-in 0.8s cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% { opacity: 0; transform: scale(0.3); }
|
||||
20% { transform: scale(1.1); }
|
||||
40% { transform: scale(0.9); }
|
||||
60% { opacity: 1; transform: scale(1.03); }
|
||||
80% { transform: scale(0.97); }
|
||||
100% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
.slide-in-right {
|
||||
animation: slide-in-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
0% { transform: translateX(100rpx); opacity: 0; }
|
||||
100% { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes rotate-slow {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
border-radius: 999rpx;
|
||||
padding: 0 80rpx;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
box-shadow: $shadow-warm;
|
||||
border: none;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -15,7 +15,8 @@
|
||||
<view class="popup-body">
|
||||
<view class="amount-section" v-if="amount !== undefined && amount !== null">
|
||||
<text class="label">支付金额</text>
|
||||
<text class="amount">¥{{ amount }}</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">
|
||||
@ -30,7 +31,10 @@
|
||||
:disabled="!coupons || coupons.length === 0"
|
||||
>
|
||||
<view class="picker-display">
|
||||
<text v-if="selectedCoupon" class="selected-text">{{ selectedCoupon.name }} (-¥{{ selectedCoupon.amount }})</text>
|
||||
<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>
|
||||
@ -43,15 +47,18 @@
|
||||
<picker
|
||||
class="picker-full"
|
||||
mode="selector"
|
||||
:range="propCards"
|
||||
range-key="name"
|
||||
:range="displayCards"
|
||||
range-key="displayName"
|
||||
@change="onCardChange"
|
||||
:value="cardIndex"
|
||||
:disabled="!propCards || propCards.length === 0"
|
||||
:disabled="!displayCards || displayCards.length === 0"
|
||||
>
|
||||
<view class="picker-display">
|
||||
<text v-if="selectedCard" class="selected-text">{{ selectedCard.name }}</text>
|
||||
<text v-else-if="!propCards || propCards.length === 0" class="placeholder">暂无道具卡可用</text>
|
||||
<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>
|
||||
@ -90,18 +97,51 @@ const selectedCoupon = computed(() => {
|
||||
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 && props.propCards[cardIndex.value]) {
|
||||
return props.propCards[cardIndex.value]
|
||||
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
|
||||
|
||||
410
components/activity/LotteryResultPopup.vue
Normal file
410
components/activity/LotteryResultPopup.vue
Normal file
@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<view v-if="visible" class="lottery-overlay" @touchmove.stop.prevent>
|
||||
<!-- 背景光效 -->
|
||||
<view class="bg-glow"></view>
|
||||
<view class="bg-rays"></view>
|
||||
|
||||
<!-- 彩带粒子 -->
|
||||
<view class="confetti-container">
|
||||
<view v-for="i in 20" :key="i" class="confetti" :style="getConfettiStyle(i)"></view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="lottery-content">
|
||||
<!-- 中奖标题 -->
|
||||
<view class="title-area">
|
||||
<view class="crown-icon">🎉</view>
|
||||
<text class="main-title">恭喜中奖</text>
|
||||
</view>
|
||||
|
||||
<!-- 奖品展示区 -->
|
||||
<scroll-view scroll-y class="prizes-scroll">
|
||||
<view class="prizes-grid">
|
||||
<view
|
||||
v-for="(item, index) in groupedResults"
|
||||
:key="index"
|
||||
class="prize-card"
|
||||
:style="{ animationDelay: `${0.2 + index * 0.15}s` }"
|
||||
>
|
||||
<!-- 光效边框 -->
|
||||
<view class="card-glow-border"></view>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<view class="card-inner">
|
||||
<view class="qty-badge" v-if="item.quantity > 1">x{{ item.quantity }}</view>
|
||||
|
||||
<view class="image-wrap">
|
||||
<image
|
||||
v-if="item.image"
|
||||
class="prize-img"
|
||||
:src="item.image"
|
||||
mode="aspectFill"
|
||||
@tap="previewImage(item.image)"
|
||||
/>
|
||||
<view v-else class="prize-placeholder">🎁</view>
|
||||
</view>
|
||||
|
||||
<view class="prize-details">
|
||||
<text class="prize-name">{{ item.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="action-area">
|
||||
<view class="claim-btn" @tap="handleClose">
|
||||
<view class="btn-glow"></view>
|
||||
<view class="btn-inner">
|
||||
<text class="btn-icon">✨</text>
|
||||
<text class="btn-text">收下奖励</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
results: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'close'])
|
||||
|
||||
const groupedResults = computed(() => {
|
||||
const map = new Map()
|
||||
const arr = Array.isArray(props.results) ? props.results : []
|
||||
|
||||
arr.forEach(item => {
|
||||
const key = item.title || item.name || '神秘奖品'
|
||||
if (map.has(key)) {
|
||||
map.get(key).quantity++
|
||||
} else {
|
||||
map.set(key, {
|
||||
title: key,
|
||||
image: item.image || item.img || item.pic || '',
|
||||
quantity: 1
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(map.values())
|
||||
})
|
||||
|
||||
function getConfettiStyle(i) {
|
||||
const colors = ['#FF6B35', '#FFD93D', '#6BCB77', '#4D96FF', '#FF6B6B', '#C9B1FF']
|
||||
const left = Math.random() * 100
|
||||
const delay = Math.random() * 2
|
||||
const duration = 2 + Math.random() * 2
|
||||
const size = 8 + Math.random() * 8
|
||||
return {
|
||||
left: `${left}%`,
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
width: `${size}rpx`,
|
||||
height: `${size * 1.5}rpx`,
|
||||
background: colors[i % colors.length]
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function previewImage(url) {
|
||||
if (url) uni.previewImage({ urls: [url], current: url })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.lottery-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, rgba(30, 20, 50, 0.95) 0%, rgba(10, 5, 20, 0.98) 100%);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 背景光效 */
|
||||
.bg-glow {
|
||||
position: absolute;
|
||||
top: 20%; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 600rpx; height: 600rpx;
|
||||
background: radial-gradient(circle, rgba(255, 180, 100, 0.4) 0%, transparent 70%);
|
||||
filter: blur(60rpx);
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.bg-rays {
|
||||
position: absolute;
|
||||
top: 15%; left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 800rpx; height: 800rpx;
|
||||
background: conic-gradient(from 0deg, transparent, rgba(255, 200, 100, 0.1), transparent, rgba(255, 200, 100, 0.1), transparent);
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate { to { transform: translateX(-50%) rotate(360deg); } }
|
||||
@keyframes pulse { 0%, 100% { opacity: 0.6; transform: translateX(-50%) scale(1); } 50% { opacity: 1; transform: translateX(-50%) scale(1.1); } }
|
||||
|
||||
/* 彩带 */
|
||||
.confetti-container {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.confetti {
|
||||
position: absolute;
|
||||
top: -20rpx;
|
||||
border-radius: 4rpx;
|
||||
animation: confettiFall 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes confettiFall {
|
||||
0% { transform: translateY(-20rpx) rotate(0deg); opacity: 1; }
|
||||
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
|
||||
}
|
||||
|
||||
/* 主内容 */
|
||||
.lottery-content {
|
||||
position: relative;
|
||||
width: 88%;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: contentPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes contentPop {
|
||||
from { opacity: 0; transform: scale(0.8) translateY(40rpx); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
/* 标题区 */
|
||||
.title-area {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.crown-icon {
|
||||
font-size: 80rpx;
|
||||
display: block;
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10rpx); }
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 56rpx;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 40rpx rgba(255, 180, 100, 0.8), 0 4rpx 20rpx rgba(0, 0, 0, 0.5);
|
||||
display: block;
|
||||
letter-spacing: 8rpx;
|
||||
}
|
||||
|
||||
/* 奖品滚动区 */
|
||||
.prizes-scroll {
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.prizes-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24rpx;
|
||||
justify-content: center;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 奖品卡片 */
|
||||
.prize-card {
|
||||
position: relative;
|
||||
width: calc(50% - 12rpx);
|
||||
max-width: 300rpx;
|
||||
animation: cardReveal 0.6s ease-out backwards;
|
||||
}
|
||||
|
||||
@keyframes cardReveal {
|
||||
from { opacity: 0; transform: scale(0.8) rotateY(-30deg); }
|
||||
to { opacity: 1; transform: scale(1) rotateY(0); }
|
||||
}
|
||||
|
||||
.card-glow-border {
|
||||
position: absolute;
|
||||
inset: -4rpx;
|
||||
background: linear-gradient(135deg, #FFD700, #FF8C00, #FFD700, #FF6347, #FFD700);
|
||||
background-size: 400% 400%;
|
||||
border-radius: 28rpx;
|
||||
animation: borderGlow 3s ease infinite;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes borderGlow {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
position: relative;
|
||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.98), rgba(255, 248, 240, 0.95));
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.qty-badge {
|
||||
position: absolute;
|
||||
top: -12rpx; right: -12rpx;
|
||||
background: linear-gradient(135deg, #FF6B35, #FF8C00);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.5);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.image-wrap {
|
||||
width: 160rpx; height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(145deg, #FFF8F3, #FFE8D1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.prize-img {
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
.prize-placeholder {
|
||||
font-size: 64rpx;
|
||||
}
|
||||
|
||||
.prize-details {
|
||||
margin-top: 16rpx;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prize-name {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 底部按钮 - 重新设计 */
|
||||
.action-area {
|
||||
width: 100%;
|
||||
padding: 40rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.claim-btn {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 110rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active .btn-inner {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #FFD700, #FF8C00, #FF6B35);
|
||||
border-radius: 55rpx;
|
||||
filter: blur(15rpx);
|
||||
opacity: 0.6;
|
||||
animation: btnPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes btnPulse {
|
||||
0%, 100% { opacity: 0.4; transform: scale(1); }
|
||||
50% { opacity: 0.7; transform: scale(1.02); }
|
||||
}
|
||||
|
||||
.btn-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FF8C00 50%, #FF6B35 100%);
|
||||
border-radius: 55rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
box-shadow:
|
||||
0 8rpx 32rpx rgba(255, 140, 0, 0.5),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.4),
|
||||
inset 0 -2rpx 0 rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: -100%;
|
||||
width: 100%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
animation: btnShine 2.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes btnShine {
|
||||
0% { left: -100%; }
|
||||
50%, 100% { left: 100%; }
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
</style>
|
||||
@ -1198,16 +1198,36 @@ async function fetchPropCards() {
|
||||
if (Array.isArray(res)) list = res
|
||||
else if (res && Array.isArray(res.list)) list = res.list
|
||||
else if (res && Array.isArray(res.data)) list = res.data
|
||||
propCards.value = list.map((i, idx) => {
|
||||
const count = i.count ?? i.remaining ?? 1
|
||||
// Group identical cards by name
|
||||
const groupedMap = new Map()
|
||||
list.forEach((i, idx) => {
|
||||
const name = i.name ?? i.title ?? i.card_name ?? '道具卡'
|
||||
return {
|
||||
id: i.id ?? i.card_id ?? i.item_card_id ?? String(idx),
|
||||
name: `${name} (×${count})`,
|
||||
rawName: name,
|
||||
count: count
|
||||
if (!groupedMap.has(name)) {
|
||||
groupedMap.set(name, {
|
||||
id: i.id ?? i.card_id ?? i.item_card_id ?? String(idx),
|
||||
name: name,
|
||||
count: 0
|
||||
})
|
||||
}
|
||||
// If the API returns a 'count' or 'remaining', use it. Otherwise assume 1.
|
||||
const inc = (i.count !== undefined && i.count !== null) ? Number(i.count) : ((i.remaining !== undefined && i.remaining !== null) ? Number(i.remaining) : 1)
|
||||
groupedMap.get(name).count += inc
|
||||
})
|
||||
|
||||
propCards.value = Array.from(groupedMap.values()).map(item => ({
|
||||
id: item.id,
|
||||
name: item.name, // PaymentPopup will handle the " (xN)" display if we pass it correctly.
|
||||
// Wait, PaymentPopup.vue expects 'name' to be the display string?
|
||||
// Let's check PaymentPopup.vue again.
|
||||
// It shows {{ selectedCoupon.name }} (-¥...).
|
||||
// For cards: {{ selectedCard.name }}.
|
||||
// So I should format the name here OR update PaymentPopup to show count.
|
||||
// The plan said "Update PaymentPopup text if needed (e.g. show count)".
|
||||
// I will format it here for consistency if PaymentPopup is generic,
|
||||
// BUT updating PaymentPopup to show "Name (xCount)" is cleaner.
|
||||
// For now, I'll pass 'count' property.
|
||||
count: item.count
|
||||
}))
|
||||
} catch (e) {
|
||||
propCards.value = []
|
||||
}
|
||||
|
||||
@ -57,13 +57,11 @@
|
||||
:reward-groups="rewardGroups"
|
||||
/>
|
||||
|
||||
<view v-if="showFlip" class="flip-overlay" @touchmove.stop.prevent>
|
||||
<view class="flip-mask" @tap="closeFlip"></view>
|
||||
<view class="flip-content" @tap.stop>
|
||||
<FlipGrid ref="flipRef" :rewards="currentIssueRewards" :controls="false" />
|
||||
<button class="overlay-close" @tap="closeFlip">关闭</button>
|
||||
</view>
|
||||
</view>
|
||||
<LotteryResultPopup
|
||||
v-model:visible="showResultPopup"
|
||||
:results="drawResults"
|
||||
@close="onResultClose"
|
||||
/>
|
||||
|
||||
<PaymentPopup
|
||||
v-model:visible="paymentVisible"
|
||||
@ -97,7 +95,7 @@ import RewardsPopup from '@/components/activity/RewardsPopup.vue'
|
||||
import RecordsList from '@/components/activity/RecordsList.vue'
|
||||
import RulesPopup from '@/components/activity/RulesPopup.vue'
|
||||
import CabinetPreviewPopup from '@/components/activity/CabinetPreviewPopup.vue'
|
||||
import FlipGrid from '@/components/FlipGrid.vue'
|
||||
import LotteryResultPopup from '@/components/activity/LotteryResultPopup.vue'
|
||||
import PaymentPopup from '@/components/PaymentPopup.vue'
|
||||
// Composables
|
||||
import { useActivity, useIssues, useRewards, useRecords } from '@/composables'
|
||||
@ -139,8 +137,8 @@ const tabActive = ref('pool')
|
||||
const rewardsVisible = ref(false)
|
||||
const rulesVisible = ref(false)
|
||||
const cabinetVisible = ref(false)
|
||||
const showFlip = ref(false)
|
||||
const flipRef = ref(null)
|
||||
const showResultPopup = ref(false)
|
||||
const drawResults = ref([])
|
||||
const drawLoading = ref(false)
|
||||
|
||||
// 支付相关
|
||||
@ -161,8 +159,9 @@ function goCabinet() {
|
||||
cabinetVisible.value = true
|
||||
}
|
||||
|
||||
function closeFlip() {
|
||||
showFlip.value = false
|
||||
function onResultClose() {
|
||||
showResultPopup.value = false
|
||||
drawResults.value = []
|
||||
}
|
||||
|
||||
function openPayment(count) {
|
||||
@ -198,10 +197,22 @@ async function fetchPropCards() {
|
||||
try {
|
||||
const res = await getItemCards(user_id)
|
||||
let list = Array.isArray(res) ? res : (res?.list || res?.data || [])
|
||||
propCards.value = list.map((i, idx) => ({
|
||||
id: i.id ?? i.card_id ?? String(idx),
|
||||
name: i.name ?? i.title ?? '道具卡'
|
||||
}))
|
||||
|
||||
// Group identical cards by name
|
||||
const groupedMap = new Map()
|
||||
list.forEach((i, idx) => {
|
||||
const name = i.name ?? i.title ?? '道具卡'
|
||||
if (!groupedMap.has(name)) {
|
||||
groupedMap.set(name, {
|
||||
id: i.id ?? i.card_id ?? String(idx),
|
||||
name: name,
|
||||
count: 0
|
||||
})
|
||||
}
|
||||
groupedMap.get(name).count++
|
||||
})
|
||||
|
||||
propCards.value = Array.from(groupedMap.values())
|
||||
} catch (e) {
|
||||
propCards.value = []
|
||||
}
|
||||
@ -230,6 +241,10 @@ async function fetchCoupons() {
|
||||
function extractResultList(resultRes) {
|
||||
const root = resultRes?.data ?? resultRes?.result ?? resultRes
|
||||
if (!root) return []
|
||||
// Backend now returns results array with all draw logs including doubled
|
||||
if (resultRes?.results && Array.isArray(resultRes.results) && resultRes.results.length > 0) {
|
||||
return resultRes.results
|
||||
}
|
||||
return root.results || root.list || root.items || root.data || []
|
||||
}
|
||||
|
||||
@ -250,7 +265,7 @@ function mapResultsToFlipItems(resultRes, poolRewards) {
|
||||
const it = fromId || fromName || null
|
||||
return {
|
||||
title: rewardName || it?.title || '奖励',
|
||||
image: it?.image || d.image || d.img || d.pic || d.product_image || ''
|
||||
image: d.image || it?.image || d.img || d.pic || d.product_image || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -313,12 +328,8 @@ async function onMachineDraw(count) {
|
||||
const resultRes = await getLotteryResult(orderNo)
|
||||
const items = mapResultsToFlipItems(resultRes, currentIssueRewards.value)
|
||||
|
||||
showFlip.value = true
|
||||
await nextTick()
|
||||
try { flipRef.value?.reset?.() } catch (_) {}
|
||||
setTimeout(() => {
|
||||
flipRef.value?.revealResults?.(items)
|
||||
}, 100)
|
||||
drawResults.value = items
|
||||
showResultPopup.value = true
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
|
||||
@ -8,17 +8,19 @@
|
||||
<text class="label">手机号</text>
|
||||
<input class="input" v-model="mobile" placeholder="请输入手机号" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">省份</text>
|
||||
<input class="input" v-model="province" placeholder="请输入省份" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">城市</text>
|
||||
<input class="input" v-model="city" placeholder="请输入城市" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">区县</text>
|
||||
<input class="input" v-model="district" placeholder="请输入区县" />
|
||||
<view class="form-item region-picker" @click="openRegionPicker">
|
||||
<text class="label">省市区</text>
|
||||
<picker
|
||||
mode="region"
|
||||
:value="regionValue"
|
||||
@change="onRegionChange"
|
||||
@cancel="onRegionCancel"
|
||||
>
|
||||
<view class="picker-value" :class="{ placeholder: !hasRegion }">
|
||||
{{ hasRegion ? `${province} ${city} ${district}` : '请选择省市区' }}
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">详细地址</text>
|
||||
@ -34,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { addAddress, updateAddress, listAddresses, setDefaultAddress } from '../../api/appUser'
|
||||
|
||||
@ -49,6 +51,25 @@ let isDefault = false
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// 省市区选择器
|
||||
const regionValue = computed(() => [province.value, city.value, district.value])
|
||||
const hasRegion = computed(() => province.value && city.value && district.value)
|
||||
|
||||
function onRegionChange(e) {
|
||||
const values = e.detail.value
|
||||
province.value = values[0] || ''
|
||||
city.value = values[1] || ''
|
||||
district.value = values[2] || ''
|
||||
}
|
||||
|
||||
function onRegionCancel() {
|
||||
// picker 取消时不做处理
|
||||
}
|
||||
|
||||
function openRegionPicker() {
|
||||
// 点击整行时触发 picker
|
||||
}
|
||||
|
||||
function fill(data) {
|
||||
name.value = data.name || data.realname || ''
|
||||
mobile.value = data.mobile || data.phone || ''
|
||||
@ -193,6 +214,37 @@ onLoad((opts) => {
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
/* 省市区选择器 */
|
||||
.region-picker {
|
||||
cursor: pointer;
|
||||
|
||||
picker {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: $font-md;
|
||||
color: $text-main;
|
||||
height: 48rpx;
|
||||
line-height: 48rpx;
|
||||
|
||||
&.placeholder {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 36rpx;
|
||||
color: $text-tertiary;
|
||||
margin-left: 12rpx;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit {
|
||||
width: 100%;
|
||||
|
||||
@ -1,40 +1,84 @@
|
||||
<template>
|
||||
<view class="wrap">
|
||||
<view class="header">
|
||||
<button class="add" @click="toAdd">新增地址</button>
|
||||
<view class="page-container">
|
||||
<!-- 顶部装饰背景 - 漂浮光球 -->
|
||||
<view class="bg-decoration"></view>
|
||||
|
||||
<view class="header-area">
|
||||
<view class="page-title">地址管理</view>
|
||||
<view class="page-subtitle">Address Management</view>
|
||||
</view>
|
||||
<view v-if="error" class="error">{{ error }}</view>
|
||||
<view v-if="list.length === 0 && !loading" class="empty">暂无地址</view>
|
||||
<view v-for="item in list" :key="item.id" class="addr">
|
||||
<view class="addr-main">
|
||||
<view class="addr-row">
|
||||
<text class="name">姓名:{{ item.name || item.realname }}</text>
|
||||
</view>
|
||||
<view class="addr-row">
|
||||
<text class="phone">手机号:{{ item.phone || item.mobile }}</text>
|
||||
</view>
|
||||
<view class="addr-row" v-if="item.is_default">
|
||||
<text class="default">默认</text>
|
||||
</view>
|
||||
<view class="addr-row">
|
||||
<text class="region">省市区:{{ item.province }}{{ item.city }}{{ item.district }}</text>
|
||||
</view>
|
||||
<view class="addr-row">
|
||||
<text class="detail">详细地址:{{ item.address || item.detail }}</text>
|
||||
|
||||
<view class="action-bar">
|
||||
<button class="add-btn" @click="toAdd">
|
||||
<text class="plus-icon">+</text>
|
||||
<text>新增收货地址</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="content-scroll">
|
||||
<view v-if="error" class="error-tip">{{ error }}</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="list.length === 0 && !loading" class="empty-state">
|
||||
<text class="empty-icon">📍</text>
|
||||
<text class="empty-text">暂无收货地址</text>
|
||||
</view>
|
||||
|
||||
<!-- 地址列表 -->
|
||||
<view class="addr-list">
|
||||
<view
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id"
|
||||
class="addr-card"
|
||||
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||
>
|
||||
<view class="addr-content" @click="toEdit(item)">
|
||||
<view class="addr-header">
|
||||
<view class="user-info">
|
||||
<text class="name">{{ item.name || item.realname }}</text>
|
||||
<text class="phone">{{ item.phone || item.mobile }}</text>
|
||||
</view>
|
||||
<view v-if="item.is_default" class="default-tag">默认</view>
|
||||
</view>
|
||||
|
||||
<view class="addr-detail">
|
||||
<text class="region">{{ item.province }} {{ item.city }} {{ item.district }}</text>
|
||||
<text class="detail-text">{{ item.address || item.detail }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<view class="card-divider"></view>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<view class="addr-actions">
|
||||
<view class="action-left" @click.stop="onSetDefault(item)">
|
||||
<view class="radio-circle" :class="{ checked: item.is_default }"></view>
|
||||
<text class="action-text">{{ item.is_default ? '默认地址' : '设为默认' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="action-right">
|
||||
<view class="action-btn" @click.stop="toEdit(item)">
|
||||
<text class="btn-icon">✎</text>
|
||||
<text>编辑</text>
|
||||
</view>
|
||||
<view class="action-btn delete" @click.stop="onDelete(item)">
|
||||
<text class="btn-icon">🗑</text>
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="addr-actions">
|
||||
<button size="mini" @click="toEdit(item)">编辑</button>
|
||||
<button size="mini" type="warn" @click="onDelete(item)">删除</button>
|
||||
<button size="mini" :disabled="item.is_default" @click="onSetDefault(item)">设为默认</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { listAddresses, deleteAddress, setDefaultAddress } from '../../api/appUser'
|
||||
|
||||
const list = ref([])
|
||||
@ -45,26 +89,22 @@ async function fetchList() {
|
||||
const user_id = uni.getStorageSync('user_id')
|
||||
const token = uni.getStorageSync('token')
|
||||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||||
if (!user_id || !token || !phoneBound) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录并绑定手机号',
|
||||
confirmText: '去登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({ url: '/pages/login/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 简单的登录检查,实际逻辑可能需要更严谨
|
||||
if (!user_id || !token) {
|
||||
// 这里不再强制弹窗,由页面逻辑决定是否跳转
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await listAddresses(user_id)
|
||||
list.value = Array.isArray(data) ? data : (data && (data.list || data.items)) || []
|
||||
} catch (e) {
|
||||
error.value = e && (e.message || e.errMsg) || '获取地址失败'
|
||||
// 静默失败或显示轻提示
|
||||
console.error(e)
|
||||
// error.value = '获取地址列表失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -89,6 +129,7 @@ function onDelete(item) {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await deleteAddress(user_id, item.id)
|
||||
uni.showToast({ title: '已删除', icon: 'none' })
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
@ -99,9 +140,11 @@ function onDelete(item) {
|
||||
}
|
||||
|
||||
async function onSetDefault(item) {
|
||||
if (item.is_default) return
|
||||
try {
|
||||
const user_id = uni.getStorageSync('user_id')
|
||||
await setDefaultAddress(user_id, item.id)
|
||||
uni.showToast({ title: '设置成功', icon: 'none' })
|
||||
fetchList()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '设置失败', icon: 'none' })
|
||||
@ -111,160 +154,312 @@ async function onSetDefault(item) {
|
||||
onLoad(() => {
|
||||
fetchList()
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ============================================
|
||||
柯大鸭潮玩 - 地址管理页面
|
||||
采用暖橙色调的卡片列表设计
|
||||
============================================ */
|
||||
|
||||
.wrap {
|
||||
padding: $spacing-md;
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-page;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: $bg-page;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
.add {
|
||||
font-size: $font-md;
|
||||
background: $gradient-brand !important;
|
||||
color: #FFFFFF !important;
|
||||
border-radius: $radius-round;
|
||||
padding: 0 $spacing-xl;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
font-weight: 600;
|
||||
box-shadow: $shadow-warm;
|
||||
}
|
||||
.add:active {
|
||||
transform: scale(0.96);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 地址卡片 */
|
||||
.addr {
|
||||
background: #FFFFFF;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-lg;
|
||||
margin-bottom: $spacing-md;
|
||||
box-shadow: $shadow-sm;
|
||||
animation: fadeInUp 0.4s ease-out backwards;
|
||||
}
|
||||
/* 背景装饰 - 与优惠券页面统一 */
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@for $i from 1 through 10 {
|
||||
.addr:nth-child(#{$i}) {
|
||||
animation-delay: #{$i * 0.05}s;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -100rpx; right: -100rpx;
|
||||
width: 600rpx; height: 600rpx;
|
||||
background: radial-gradient(circle, rgba($brand-primary, 0.15) 0%, transparent 70%);
|
||||
filter: blur(60rpx);
|
||||
border-radius: 50%;
|
||||
opacity: 0.8;
|
||||
animation: float 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 200rpx; left: -200rpx;
|
||||
width: 500rpx; height: 500rpx;
|
||||
background: radial-gradient(circle, rgba($brand-secondary, 0.1) 0%, transparent 70%);
|
||||
filter: blur(50rpx);
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
animation: float 15s ease-in-out infinite reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.addr-main {
|
||||
margin-bottom: $spacing-md;
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0); }
|
||||
50% { transform: translate(30rpx, 50rpx); }
|
||||
}
|
||||
|
||||
.addr-row {
|
||||
.header-area {
|
||||
padding: $spacing-xl $spacing-lg;
|
||||
padding-top: calc(env(safe-area-inset-top) + 20rpx);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: 900;
|
||||
color: $text-main;
|
||||
margin-bottom: 8rpx;
|
||||
letter-spacing: 1rpx;
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: $text-tertiary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
@extend .glass-card;
|
||||
margin: 0 $spacing-lg $spacing-md;
|
||||
padding: 20rpx;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: $gradient-brand;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 100rpx;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
.addr-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: $font-lg;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
}
|
||||
.phone {
|
||||
font-size: $font-md;
|
||||
color: $text-sub;
|
||||
}
|
||||
.default {
|
||||
font-size: $font-xs;
|
||||
color: #FFFFFF;
|
||||
background: $gradient-brand;
|
||||
padding: 4rpx $spacing-sm;
|
||||
border-radius: $radius-round;
|
||||
font-weight: 500;
|
||||
}
|
||||
.region {
|
||||
font-size: $font-sm;
|
||||
color: $text-sub;
|
||||
}
|
||||
.detail {
|
||||
font-size: $font-md;
|
||||
color: $text-main;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.addr-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $spacing-md;
|
||||
margin-top: $spacing-lg;
|
||||
padding-top: $spacing-lg;
|
||||
border-top: 1rpx solid $border-color-light;
|
||||
}
|
||||
.addr-actions button {
|
||||
font-size: $font-sm;
|
||||
height: 52rpx;
|
||||
line-height: 52rpx;
|
||||
padding: 0 $spacing-lg;
|
||||
border-radius: $radius-round;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
background: $bg-secondary;
|
||||
color: $text-main;
|
||||
|
||||
&::after { border: none; }
|
||||
box-shadow: $shadow-warm;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
background: darken($bg-secondary, 5%);
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
.addr-actions button[type="warn"] {
|
||||
background: rgba($color-error, 0.1) !important;
|
||||
color: $color-error !important;
|
||||
|
||||
.plus-icon {
|
||||
font-size: 40rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-top: -4rpx;
|
||||
font-weight: 300;
|
||||
}
|
||||
.addr-actions button:not([type]) {
|
||||
background: $bg-secondary !important;
|
||||
color: $text-main !important;
|
||||
|
||||
.content-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 0 $spacing-lg;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: $text-sub;
|
||||
margin-top: 120rpx;
|
||||
font-size: $font-md;
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error {
|
||||
color: $color-error;
|
||||
font-size: $font-sm;
|
||||
margin-bottom: $spacing-md;
|
||||
padding: $spacing-md;
|
||||
background: rgba($color-error, 0.1);
|
||||
border-radius: $radius-md;
|
||||
text-align: center;
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: $text-tertiary;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 地址列表 */
|
||||
.addr-list {
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.addr-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: $shadow-sm;
|
||||
overflow: hidden;
|
||||
animation: fadeInUp 0.5s ease-out backwards;
|
||||
/* Removed border from glass style */
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
from { opacity: 0; transform: translateY(20rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.addr-content {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.addr-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
.phone {
|
||||
font-size: 28rpx;
|
||||
color: $text-sub;
|
||||
font-family: monospace; /* 数字等宽 */
|
||||
}
|
||||
|
||||
.default-tag {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, $brand-primary, $brand-secondary);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx 0 8rpx 0;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2rpx 6rpx rgba($brand-primary, 0.2);
|
||||
}
|
||||
|
||||
.addr-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.region {
|
||||
font-size: 26rpx;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.detail-text {
|
||||
font-size: 28rpx;
|
||||
color: $text-main;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.card-divider {
|
||||
height: 1rpx;
|
||||
background: #f0f0f0;
|
||||
margin: 0 30rpx;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.addr-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background: rgba(249, 249, 249, 0.5);
|
||||
}
|
||||
|
||||
.action-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.radio-circle {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #ddd;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.checked {
|
||||
border-color: $brand-primary;
|
||||
background: $brand-primary;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 12rpx; height: 6rpx;
|
||||
border-left: 3rpx solid #fff;
|
||||
border-bottom: 3rpx solid #fff;
|
||||
transform: translate(-50%, -60%) rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
.action-right {
|
||||
display: flex;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
font-size: 26rpx;
|
||||
color: $text-sub;
|
||||
padding: 10rpx;
|
||||
|
||||
.btn-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
color: $color-error; // 使用全局变量或具体色值
|
||||
}
|
||||
}
|
||||
|
||||
.error-tip {
|
||||
color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: 40rpx;
|
||||
}
|
||||
</style>
|
||||
@ -165,12 +165,17 @@ function formatValue(val) {
|
||||
|
||||
// 格式化有效期
|
||||
function formatExpiry(item) {
|
||||
if (!item.end_time) return '长期有效'
|
||||
const d = new Date(item.end_time)
|
||||
// 后端返回的字段是 valid_end
|
||||
const endTime = item.valid_end || item.end_time
|
||||
if (!endTime) return '长期有效'
|
||||
const d = new Date(endTime)
|
||||
// Check for invalid date (e.g., "0001-01-01" from Go zero value)
|
||||
if (isNaN(d.getTime()) || d.getFullYear() < 2000) return '长期有效'
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `有效期至 ${y}-${m}-${day}`
|
||||
const label = currentTab.value === 3 ? '过期时间' : '有效期至'
|
||||
return `${label} ${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
@ -528,10 +533,12 @@ onLoad(() => {
|
||||
.coupon-right {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
padding-right: 130rpx; /* Prevent text overlap with button */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
position: relative; /* Ensure padding works with absolute button */
|
||||
}
|
||||
|
||||
.coupon-header {
|
||||
@ -613,7 +620,8 @@ onLoad(() => {
|
||||
.coupon-action-wrapper {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
bottom: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
<text class="tab-text">已使用</text>
|
||||
<view class="tab-indicator" v-if="currentTab === 1"></view>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: currentTab === 2 }" @click="switchTab(2)">
|
||||
<text class="tab-text">已过期</text>
|
||||
<view class="tab-indicator" v-if="currentTab === 2"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区 -->
|
||||
@ -37,7 +41,7 @@
|
||||
<!-- 空状态 -->
|
||||
<view v-else-if="list.length === 0" class="empty-state">
|
||||
<text class="empty-icon">🃏</text>
|
||||
<text class="empty-text">{{ currentTab === 0 ? '暂无可用道具卡' : '暂无使用记录' }}</text>
|
||||
<text class="empty-text">{{ getEmptyText() }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 道具卡列表 -->
|
||||
@ -46,7 +50,7 @@
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id || index"
|
||||
class="item-ticket"
|
||||
:class="{ 'used': currentTab === 1 }"
|
||||
:class="{ 'used': currentTab === 1, 'expired': currentTab === 2 }"
|
||||
:style="{ animationDelay: `${index * 0.05}s` }"
|
||||
>
|
||||
<!-- 左侧图标区域 -->
|
||||
@ -54,9 +58,6 @@
|
||||
<view class="card-icon-wrap">
|
||||
<text class="card-icon">{{ getCardIcon(item.type || item.name) }}</text>
|
||||
</view>
|
||||
<view class="card-count-badge" v-if="currentTab === 0">
|
||||
<text class="count-num">×{{ item.remaining ?? item.count ?? 1 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中间分割线 -->
|
||||
@ -83,6 +84,13 @@
|
||||
<text class="detail-val highlight">{{ item.used_reward_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="usage-info" v-if="currentTab === 2">
|
||||
<text class="card-use-time" v-if="item.valid_end">过期时间:{{ formatDateTime(item.valid_end) }}</text>
|
||||
</view>
|
||||
<!-- Unused State: Show Validity -->
|
||||
<view class="usage-info" v-if="currentTab === 0">
|
||||
<text class="card-use-time" v-if="item.valid_end">有效期至:{{ formatDateTime(item.valid_end) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 优化后的按钮位置 -->
|
||||
@ -92,9 +100,12 @@
|
||||
<view class="btn-shine"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-used-badge" v-else>
|
||||
<view class="card-used-badge" v-else-if="currentTab === 1">
|
||||
<text class="used-text">已使用</text>
|
||||
</view>
|
||||
<view class="card-used-badge expired" v-else-if="currentTab === 2">
|
||||
<text class="used-text">已过期</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -117,6 +128,13 @@ function getUserId() {
|
||||
return uni.getStorageSync('user_id')
|
||||
}
|
||||
|
||||
function getEmptyText() {
|
||||
if (currentTab.value === 0) return '暂无可用道具卡'
|
||||
if (currentTab.value === 1) return '暂无使用记录'
|
||||
if (currentTab.value === 2) return '暂无过期道具卡'
|
||||
return ''
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
function checkAuth() {
|
||||
const token = uni.getStorageSync('token')
|
||||
@ -185,23 +203,13 @@ async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const userId = getUserId()
|
||||
// status: 1=unused, 2=used
|
||||
const status = currentTab.value === 0 ? 1 : 2
|
||||
// status: 1=unused, 2=used, 3=expired
|
||||
const status = currentTab.value === 0 ? 1 : (currentTab.value === 1 ? 2 : 3)
|
||||
const res = await getItemCards(userId, status)
|
||||
|
||||
let items = Array.isArray(res) ? res : (res.list || res.data || [])
|
||||
|
||||
// 处理数据,确保count字段存在
|
||||
items = items.map(item => ({
|
||||
...item,
|
||||
count: item.count ?? item.remaining ?? 1
|
||||
}))
|
||||
|
||||
// 未使用状态时过滤掉数量为0的卡片
|
||||
if (currentTab.value === 0) {
|
||||
items = items.filter(i => i.count > 0)
|
||||
}
|
||||
|
||||
// items is now a direct list of individual cards (backend aggregation removed)
|
||||
list.value = items
|
||||
} catch (e) {
|
||||
console.error('获取道具卡失败:', e)
|
||||
@ -455,7 +463,7 @@ onLoad(() => {
|
||||
.card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-right: 100rpx;
|
||||
padding-right: 130rpx;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
@ -605,6 +613,35 @@ onLoad(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 已过期状态 */
|
||||
.item-ticket.expired {
|
||||
.ticket-left {
|
||||
background: #fdfdfd;
|
||||
}
|
||||
|
||||
.card-icon-wrap {
|
||||
filter: grayscale(1) sepia(0.2);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
color: $text-tertiary;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.card-used-badge.expired {
|
||||
background: #f0f0f0;
|
||||
|
||||
.used-text {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.spinner {
|
||||
width: 28rpx;
|
||||
|
||||
@ -86,25 +86,14 @@
|
||||
|
||||
<!-- 道具卡视图 (列表卡片) -->
|
||||
<view v-else-if="currentTab === 'item_card'" class="item-cards-list">
|
||||
<view class="item-card" v-for="ic in items" :key="ic.id">
|
||||
<view class="ic-icon-wrap">
|
||||
<text class="ic-icon">🃏</text>
|
||||
</view>
|
||||
<view class="ic-info">
|
||||
<text class="ic-name">{{ ic.title || ic.name }}</text>
|
||||
<text class="ic-desc">{{ ic.description || '可在抽奖时使用' }}</text>
|
||||
<view class="ic-bottom">
|
||||
<view class="ic-price">
|
||||
<text class="p-val">{{ ic.points || 0 }}</text>
|
||||
<text class="p-unit">积分</text>
|
||||
</view>
|
||||
<view class="ic-btn" @tap="onRedeemTap(ic)">兑换</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- TODO: 后续我们要打开的 -->
|
||||
<view class="empty-state" style="padding-top: 50rpx;">
|
||||
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
||||
<text class="empty-text">暂未开放</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="!items.length" class="empty-state">
|
||||
<view v-if="!items.length && currentTab !== 'item_card'" class="empty-state">
|
||||
<image class="empty-img" src="/static/empty.png" mode="widthFix" />
|
||||
<text class="empty-text">暂无相关兑换项</text>
|
||||
</view>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user