From 6183fcaf1502aa2e2b3896dea276ecc6e46f930e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=96=B9=E6=88=90?= Date: Fri, 26 Dec 2025 12:46:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20BoxReveal=20?= =?UTF-8?q?=E5=92=8C=20LotteryResultPopup=20=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AF=B9=E5=AF=B9=E7=A2=B0=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E9=81=93=E5=85=B7=E5=8D=A1=E8=81=9A=E5=90=88=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E8=B0=83=E6=95=B4=E5=95=86=E5=BA=97=E9=81=93?= =?UTF-8?q?=E5=85=B7=E5=8D=A1=E9=A1=B5=E9=9D=A2=E4=B8=BA=E2=80=9C=E6=9A=82?= =?UTF-8?q?=E6=9C=AA=E5=BC=80=E6=94=BE=E2=80=9D=E6=8F=90=E7=A4=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/BoxReveal.vue | 338 +++++++++++++ components/PaymentPopup.vue | 58 ++- components/activity/LotteryResultPopup.vue | 410 ++++++++++++++++ pages/activity/duiduipeng/index.vue | 34 +- pages/activity/wuxianshang/index.vue | 57 ++- pages/address/edit.vue | 76 ++- pages/address/index.vue | 525 ++++++++++++++------- pages/coupons/index.vue | 16 +- pages/item-cards/index.vue | 77 ++- pages/shop/index.vue | 21 +- 10 files changed, 1356 insertions(+), 256 deletions(-) create mode 100644 components/BoxReveal.vue create mode 100644 components/activity/LotteryResultPopup.vue diff --git a/components/BoxReveal.vue b/components/BoxReveal.vue new file mode 100644 index 0000000..efd2fb6 --- /dev/null +++ b/components/BoxReveal.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/components/PaymentPopup.vue b/components/PaymentPopup.vue index 179bdd2..957a867 100644 --- a/components/PaymentPopup.vue +++ b/components/PaymentPopup.vue @@ -15,7 +15,8 @@ 支付金额 - ¥{{ amount }} + ¥{{ finalPayAmount }} + ¥{{ amount }} @@ -30,7 +31,10 @@ :disabled="!coupons || coupons.length === 0" > - {{ selectedCoupon.name }} (-¥{{ selectedCoupon.amount }}) + + {{ selectedCoupon.name }} (-¥{{ effectiveCouponDiscount.toFixed(2) }}) + (最高抵扣50%) + 暂无优惠券可用 请选择优惠券 @@ -43,15 +47,18 @@ - {{ selectedCard.name }} - 暂无道具卡可用 + + {{ selectedCard.name }} + (拥有: {{ selectedCard.count }}) + + 暂无道具卡可用 请选择道具卡 @@ -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 diff --git a/components/activity/LotteryResultPopup.vue b/components/activity/LotteryResultPopup.vue new file mode 100644 index 0000000..bf70eb5 --- /dev/null +++ b/components/activity/LotteryResultPopup.vue @@ -0,0 +1,410 @@ + + + + + diff --git a/pages/activity/duiduipeng/index.vue b/pages/activity/duiduipeng/index.vue index 042a1d2..47a393e 100644 --- a/pages/activity/duiduipeng/index.vue +++ b/pages/activity/duiduipeng/index.vue @@ -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 = [] } diff --git a/pages/activity/wuxianshang/index.vue b/pages/activity/wuxianshang/index.vue index 944ba8d..93e2f08 100644 --- a/pages/activity/wuxianshang/index.vue +++ b/pages/activity/wuxianshang/index.vue @@ -57,13 +57,11 @@ :reward-groups="rewardGroups" /> - - - - - - - + ({ - 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 { diff --git a/pages/address/edit.vue b/pages/address/edit.vue index 040d262..1361c67 100644 --- a/pages/address/edit.vue +++ b/pages/address/edit.vue @@ -8,17 +8,19 @@ 手机号 - - 省份 - - - - 城市 - - - - 区县 - + + 省市区 + + + {{ hasRegion ? `${province} ${city} ${district}` : '请选择省市区' }} + + + 详细地址 @@ -34,7 +36,7 @@ \ No newline at end of file diff --git a/pages/coupons/index.vue b/pages/coupons/index.vue index 39df375..859e05e 100644 --- a/pages/coupons/index.vue +++ b/pages/coupons/index.vue @@ -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; } diff --git a/pages/item-cards/index.vue b/pages/item-cards/index.vue index c7b4a2f..dc1cb0f 100644 --- a/pages/item-cards/index.vue +++ b/pages/item-cards/index.vue @@ -18,6 +18,10 @@ 已使用 + + 已过期 + + @@ -37,7 +41,7 @@ 🃏 - {{ currentTab === 0 ? '暂无可用道具卡' : '暂无使用记录' }} + {{ getEmptyText() }} @@ -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 @@ {{ getCardIcon(item.type || item.name) }} - - ×{{ item.remaining ?? item.count ?? 1 }} - @@ -83,6 +84,13 @@ {{ item.used_reward_name }} + + 过期时间:{{ formatDateTime(item.valid_end) }} + + + + 有效期至:{{ formatDateTime(item.valid_end) }} + @@ -92,9 +100,12 @@ - + 已使用 + + 已过期 + @@ -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; diff --git a/pages/shop/index.vue b/pages/shop/index.vue index 689177e..b4c30ea 100644 --- a/pages/shop/index.vue +++ b/pages/shop/index.vue @@ -86,25 +86,14 @@ - - - 🃏 - - - {{ ic.title || ic.name }} - {{ ic.description || '可在抽奖时使用' }} - - - {{ ic.points || 0 }} - 积分 - - 兑换 - - + + + + 暂未开放 - + 暂无相关兑换项