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 @@
+
+
+
+
+
+
+
+
+
+ {{ isShaking ? '正在开启...' : '准备开启' }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ rewards[0].title }}
+ 恭喜获得
+
+
+
+
+
+
+
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🎉
+ 恭喜中奖
+
+
+
+
+
+
+
+
+
+
+
+ x{{ item.quantity }}
+
+
+
+ 🎁
+
+
+
+ {{ item.title }}
+
+
+
+
+
+
+
+
+
+
+
+ ✨
+ 收下奖励
+
+
+
+
+
+
+
+
+
+
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 }}
- 积分
-
- 兑换
-
-
+
+
+
+ 暂未开放
-
+
暂无相关兑换项