feat: 添加积分兑换商品功能及优化订单显示
- 在request.js中添加积分兑换商品API - 在shop页面实现积分兑换功能及UI优化 - 在orders页面优化订单显示逻辑,支持优惠券和道具卡标签 - 在mine页面调整订单导航逻辑,支持跳转至cabinet指定tab - 优化道具卡和优惠券的显示及状态处理
This commit is contained in:
parent
be57eda392
commit
a350bcc4ed
@ -1133,15 +1133,22 @@ async function fetchPropCards() {
|
||||
const user_id = uni.getStorageSync('user_id')
|
||||
if (!user_id) return
|
||||
try {
|
||||
const res = await getItemCards(user_id, 0)
|
||||
// Status 1 = Unused
|
||||
const res = await getItemCards(user_id, 1)
|
||||
let list = []
|
||||
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) => ({
|
||||
id: i.id ?? i.card_id ?? i.item_card_id ?? String(idx),
|
||||
name: i.name ?? i.title ?? i.card_name ?? '道具卡'
|
||||
}))
|
||||
propCards.value = list.map((i, idx) => {
|
||||
const count = i.count ?? i.remaining ?? 1
|
||||
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
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
propCards.value = []
|
||||
}
|
||||
|
||||
@ -159,6 +159,15 @@ const isAllSelected = computed(() => {
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
// Check for external tab switch request
|
||||
try {
|
||||
const targetTab = uni.getStorageSync('cabinet_target_tab')
|
||||
if (targetTab !== '' && targetTab !== null && targetTab !== undefined) {
|
||||
currentTab.value = Number(targetTab)
|
||||
uni.removeStorageSync('cabinet_target_tab')
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const token = uni.getStorageSync('token')
|
||||
const phoneBound = !!uni.getStorageSync('phone_bound')
|
||||
console.log('cabinet onShow token:', token, 'isLogin:', !!token, 'phoneBound:', phoneBound)
|
||||
|
||||
@ -93,30 +93,34 @@
|
||||
<text class="section-more" @click="toOrders('all')">全部订单 ›</text>
|
||||
</view>
|
||||
<view class="grid-row">
|
||||
<view class="grid-item" @click="toOrders('pending')">
|
||||
<view class="icon-wrapper">
|
||||
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA4YTIgMiAwIDAgMC0xLTEuNzNsLTctNGEyIDIgMCAwIDAtMiAwbC03IDRBMiAyIDAgMCAwIDMgOHY4YTIgMiAwIDAgMCAxIDEuNzNsNyA0YTIgMiAwIDAgMCAyIDBsNy00QTIgMiAwIDAgMCAyMSAxNlY4eiIvPjxwYXRoIGQ9Ik0zLjI3IDYuOTZMMTIgMTIuMDFsMTAgLTUuMDUiLz48cGF0aCBkPSJNMTIgMjIuMDhWMTIiLz48L3N2Zz4=" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="grid-label">盒柜</text>
|
||||
</view>
|
||||
<!-- 1. 待付款 -->
|
||||
<view class="grid-item" @click="toOrders('pending')">
|
||||
<view class="icon-wrapper">
|
||||
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjEiIHk9IjQiIHdpZHRoPSIyMiIgaGVpZ2h0PSIxNiIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PGxpbmUgeDE9IjEiIHkxPSIxMCIgeDI9IjIzIiB5Mj0iMTAiPjwvbGluZT48L3N2Zz4=" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="grid-label">待付款</text>
|
||||
</view>
|
||||
<view class="grid-item" @click="toOrders('pending')">
|
||||
<!-- 2. 待发货 (Jump to Cabinet Tab 1) -->
|
||||
<view class="grid-item" @click="toCabinetTab(1)">
|
||||
<view class="icon-wrapper">
|
||||
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMiAyMmg1LjlhMiAyIDAgMCAwIDEuOS0xLjQybDMuMTMtMTEuMDlhMiAyIDAgMCAwLTEuOS0yLjUxaC00LjE2IiAvPjxwYXRoIGQ9Ik0xOC40MiA5aC02LjMyIiAvPjxwYXRoIGQ9Ik0xNSA2VjNhMSAxIDAgMCAwLTEtMUg2YTEgMSAwIDAgMC0xIDF2MTQiIC8+PHBhdGggZD0iTTkgMTNoNyIgLz48L3N2Zz4=" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="grid-label">待发货</text>
|
||||
</view>
|
||||
<view class="grid-item" @click="toOrders('completed')">
|
||||
<!-- 3. 已发货 (Jump to Cabinet Tab 1) -->
|
||||
<view class="grid-item" @click="toCabinetTab(1)">
|
||||
<view class="icon-wrapper">
|
||||
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjEiIHk9IjMiIHdpZHRoPSIxNSIgaGVpZ2h0PSIxMyIgcng9IjIiIHJ5PSIyIj48L3JlY3Q+PHBvbHlsaW5lIHBvaW50cz0iMTYgOCAyMCA4IDIwIDIxIDIgMjEgMiAxNiA2IDE2IiAvPjwvc3ZnPg==" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="grid-label">已发货</text>
|
||||
</view>
|
||||
<!-- 4. 全部订单 (Was Box Cabinet) -->
|
||||
<view class="grid-item" @click="toOrders('completed')">
|
||||
<view class="icon-wrapper">
|
||||
<image class="grid-icon-img" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNGRjZCMDAiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0yMSA4YTIgMiAwIDAgMC0xLTEuNzNsLTctNGEyIDIgMCAwIDAtMiAwbC03IDRBMiAyIDAgMCAwIDMgOHY4YTIgMiAwIDAgMCAxIDEuNzNsNyA0YTIgMiAwIDAgMCAyIDBsNy00QTIgMiAwIDAgMCAyMSAxNlY4eiIvPjxwYXRoIGQ9Ik0zLjI3IDYuOTZMMTIgMTIuMDFsMTAgLTUuMDUiLz48cGF0aCBkPSJNMTIgMjIuMDhWMTIiLz48L3N2Zz4=" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="grid-label">全部订单</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -254,7 +258,12 @@
|
||||
</view>
|
||||
|
||||
<view class="coupon-footer-row">
|
||||
<text class="coupon-expire-v2">{{ formatCouponExpiry(item) }}</text>
|
||||
<view class="coupon-footer-left">
|
||||
<text class="coupon-expire-v2">{{ formatCouponExpiry(item) }}</text>
|
||||
<!-- 使用时间 (已使用状态) -->
|
||||
<text class="coupon-used-time" v-if="couponsTab === 2 && item.used_at">使用时间:{{ formatDateTime(item.used_at) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="coupon-action-v2" v-if="couponsTab === 1">
|
||||
<text class="use-btn-v2">去使用</text>
|
||||
</view>
|
||||
@ -262,9 +271,6 @@
|
||||
<text class="status-tag" :class="couponsTab === 2 ? 'used' : 'expired'">{{ couponsTab === 2 ? '已使用' : '已过期' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 使用时间 (已使用状态) -->
|
||||
<text class="coupon-used-time" v-if="couponsTab === 2 && item.used_at">使用时间:{{ formatDateTime(item.used_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -598,6 +604,10 @@ export default {
|
||||
toOrders(status) {
|
||||
uni.navigateTo({ url: `/pages/orders/index?status=${status}` })
|
||||
},
|
||||
toCabinetTab(tabIndex) {
|
||||
uni.setStorageSync('cabinet_target_tab', tabIndex)
|
||||
uni.switchTab({ url: '/pages/cabinet/index' })
|
||||
},
|
||||
toAddresses() {
|
||||
uni.navigateTo({ url: '/pages/address/index' })
|
||||
},
|
||||
@ -790,23 +800,27 @@ export default {
|
||||
this.itemCardsLoading = true
|
||||
this.itemCardsList = []
|
||||
try {
|
||||
// Mocking used status via local filter if API doesn't support
|
||||
// Or assume API supports status param.
|
||||
const res = await getItemCards(this.userId)
|
||||
// Assuming res is list of cards with counts.
|
||||
// For "used", we might need a different API or field.
|
||||
// For now, just show all for unused, and empty for used as mock.
|
||||
let list = Array.isArray(res) ? res : (res.list || [])
|
||||
// Pass status: 0(tab)=>1(unused), 1(tab)=>2(used)
|
||||
const status = this.itemCardsTab === 0 ? 1 : 2
|
||||
const res = await getItemCards(this.userId, status)
|
||||
|
||||
// Robustly get the list (support res.list or res.data)
|
||||
let list = Array.isArray(res) ? res : (res.list || res.data || [])
|
||||
|
||||
this.itemCardsList = list.map(item => {
|
||||
return {
|
||||
...item,
|
||||
// Ensure count exists, default to 1 if undefined
|
||||
count: item.count ?? item.remaining ?? 1
|
||||
}
|
||||
})
|
||||
|
||||
// For unused tab, filter out items with 0 count if any
|
||||
if (this.itemCardsTab === 0) {
|
||||
// Show cards with count > 0
|
||||
this.itemCardsList = list.filter(i => (i.count || i.remaining) > 0)
|
||||
} else {
|
||||
// Mock used history or filter
|
||||
this.itemCardsList = []
|
||||
this.itemCardsList = this.itemCardsList.filter(i => i.count > 0)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error('loadItemCards error:', e)
|
||||
} finally {
|
||||
this.itemCardsLoading = false
|
||||
}
|
||||
@ -882,8 +896,24 @@ export default {
|
||||
},
|
||||
formatDateTime(ts) {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
return `${this.formatDate(ts)} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
|
||||
// Convert string timestamp (e.g. "2023-01-01T...") to timestamp if needed
|
||||
let d
|
||||
if (typeof ts === 'string') {
|
||||
// iOS compatibility for ISO strings
|
||||
const safeTs = ts.replace(/-/g, '/')
|
||||
d = new Date(safeTs)
|
||||
if (isNaN(d.getTime())) {
|
||||
// Fallback for non-ISO strings or just numbers as strings
|
||||
const n = Number(ts)
|
||||
if (!isNaN(n)) d = new Date(n * 1000)
|
||||
}
|
||||
} else {
|
||||
d = new Date(ts * 1000)
|
||||
}
|
||||
|
||||
if (!d || isNaN(d.getTime())) return ''
|
||||
|
||||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1166,11 +1196,67 @@ export default {
|
||||
line-height: 1;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.status-text, .empty-state {
|
||||
padding: 60rpx; text-align: center; color: $text-sub; font-size: 26rpx;
|
||||
}
|
||||
.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: $text-tertiary;
|
||||
padding: 30rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.text {
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 60rpx;
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗 Tab 栏 */
|
||||
.popup-tabs {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0 20rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid $border-color-light;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.popup-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: $text-sub;
|
||||
padding: 24rpx 0;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.popup-tab.active {
|
||||
color: $text-main;
|
||||
font-weight: 700;
|
||||
}
|
||||
.popup-tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 6rpx;
|
||||
background: $brand-primary;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
/* 列表滚动区 */
|
||||
.points-list, .coupon-scroll, .item-cards-scroll, .task-list-scroll, .invite-list-scroll {
|
||||
flex: 1; min-height: 400rpx; background: $bg-grey; padding: 20rpx;
|
||||
@ -1197,20 +1283,103 @@ export default {
|
||||
.coupon-left-v2 {
|
||||
width: 180rpx; background: linear-gradient(135deg, #FFF5E6, #fff);
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 20rpx; border-right: 2rpx dashed #E0E0E0;
|
||||
padding: 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
.coupon-remaining { color: $brand-primary; font-weight: 900; }
|
||||
.coupon-symbol { font-size: 24rpx; }
|
||||
.coupon-amount-num { font-size: 56rpx; line-height: 1; }
|
||||
.coupon-label { font-size: 20rpx; color: $brand-primary; margin-top: 8rpx; border: 1px solid $brand-primary; padding: 2rpx 8rpx; border-radius: 6rpx; }
|
||||
.coupon-right-v2 { flex: 1; padding: 24rpx; display: flex; flex-direction: column; justify-content: space-between; }
|
||||
.coupon-name-v2 { font-size: $font-md; font-weight: 700; color: $text-main; margin-bottom: 8rpx; }
|
||||
|
||||
/* 优惠券分割线 */
|
||||
.coupon-divider-v2 {
|
||||
width: 30rpx;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.divider-notch {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background: $bg-grey; /* Match scroll container bg */
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
.divider-notch.top { top: -12rpx; }
|
||||
.divider-notch.bottom { bottom: -12rpx; }
|
||||
.divider-dash {
|
||||
width: 0;
|
||||
height: 80%;
|
||||
border-left: 2rpx dashed #eee;
|
||||
}
|
||||
|
||||
.coupon-right-v2 {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden; /* Ensure content stays inside */
|
||||
}
|
||||
.coupon-name-v2 {
|
||||
font-size: $font-md;
|
||||
font-weight: 700;
|
||||
color: $text-main;
|
||||
margin-bottom: 8rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.coupon-original { font-size: 20rpx; color: $text-tertiary; text-decoration: line-through; margin-left: 8rpx; display: inline-block; }
|
||||
.coupon-rules { font-size: $font-xs; color: $text-sub; margin-bottom: 16rpx; }
|
||||
.coupon-footer-row { display: flex; justify-content: space-between; align-items: center; margin-top: auto; }
|
||||
|
||||
/* 优惠券进度条 */
|
||||
.coupon-progress-wrap {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.coupon-progress-bar {
|
||||
height: 6rpx;
|
||||
background: $bg-secondary;
|
||||
border-radius: 100rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.coupon-progress-fill {
|
||||
height: 100%;
|
||||
background: $brand-primary;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.coupon-progress-text {
|
||||
font-size: 18rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.coupon-footer-row { display: flex; justify-content: space-between; align-items: flex-end; margin-top: auto; }
|
||||
.coupon-footer-left { display: flex; flex-direction: column; }
|
||||
.coupon-expire-v2 { font-size: 20rpx; color: $text-tertiary; }
|
||||
.use-btn-v2 { background: $brand-primary; color: #fff; font-size: 22rpx; padding: 8rpx 24rpx; border-radius: 100rpx; }
|
||||
.status-tag { font-size: 22rpx; color: $text-tertiary; background: #F5F5F5; padding: 4rpx 12rpx; border-radius: 6rpx; }
|
||||
.coupon-used-time { font-size: 18rpx; color: $text-tertiary; margin-top: 4rpx; text-align: left; }
|
||||
|
||||
/* 过期/已使用状态 */
|
||||
.coupon-used .coupon-left-v2, .coupon-expired .coupon-left-v2 {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.coupon-used .coupon-remaining, .coupon-expired .coupon-remaining,
|
||||
.coupon-used .coupon-label, .coupon-expired .coupon-label {
|
||||
color: $text-tertiary;
|
||||
border-color: $text-tertiary;
|
||||
}
|
||||
.coupon-used .coupon-name-v2, .coupon-expired .coupon-name-v2 {
|
||||
color: $text-sub;
|
||||
}
|
||||
|
||||
/* 道具卡 */
|
||||
.item-cards-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20rpx; }
|
||||
@ -1227,6 +1396,41 @@ export default {
|
||||
.card-count-badge { position: absolute; top: 20rpx; right: 20rpx; background: rgba(0,0,0,0.05); padding: 4rpx 12rpx; border-radius: 100rpx; }
|
||||
.count-num { font-size: 22rpx; font-weight: 700; color: $text-main; }
|
||||
|
||||
/* 道具卡已使用状态 */
|
||||
.item-card.used {
|
||||
background: #fafafa;
|
||||
}
|
||||
.item-card.used .card-name {
|
||||
color: $text-sub;
|
||||
}
|
||||
.item-card.used .card-desc {
|
||||
color: $text-tertiary;
|
||||
}
|
||||
.item-card.used .card-icon-wrap {
|
||||
background: #f0f0f0;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.card-used-badge {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
background: #eee;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 100rpx;
|
||||
}
|
||||
.used-text {
|
||||
font-size: 22rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
.card-use-time {
|
||||
font-size: 18rpx;
|
||||
color: $text-tertiary;
|
||||
margin-top: 12rpx;
|
||||
display: block;
|
||||
border-top: 1rpx dashed #eee;
|
||||
padding-top: 8rpx;
|
||||
}
|
||||
|
||||
/* 任务中心弹窗 */
|
||||
.task-center-popup { background: #F8F9FA; }
|
||||
.overall-progress {
|
||||
|
||||
@ -63,8 +63,8 @@
|
||||
</view>
|
||||
<view class="item-meta">
|
||||
<view class="price-wrap">
|
||||
<text class="currency">¥</text>
|
||||
<text class="price">{{ formatPrice(item.price) }}</text>
|
||||
<text class="currency" v-if="item.price > 0">¥</text>
|
||||
<text class="price">{{ item.price > 0 ? formatPrice(item.price) : '奖品' }}</text>
|
||||
</view>
|
||||
<text class="item-quantity">x{{ item.quantity }}</text>
|
||||
</view>
|
||||
@ -87,8 +87,8 @@
|
||||
</view>
|
||||
<view class="item-meta">
|
||||
<view class="price-wrap">
|
||||
<text class="currency">¥</text>
|
||||
<text class="price">{{ formatPrice(order.actual_amount) }}</text>
|
||||
<text class="currency" v-if="order.actual_amount > 0">¥</text>
|
||||
<text class="price">{{ order.actual_amount > 0 ? formatPrice(order.actual_amount) : '奖品' }}</text>
|
||||
</view>
|
||||
<text class="item-quantity">x1</text>
|
||||
</view>
|
||||
@ -130,19 +130,39 @@
|
||||
<text class="label">商品总额</text>
|
||||
<text class="value">¥{{ formatPrice(order.total_amount) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="order.discount_amount">
|
||||
<text class="label">优惠金额</text>
|
||||
|
||||
<!-- 优惠券 -->
|
||||
<view class="info-row" v-if="order.coupon_info">
|
||||
<text class="label">优惠券</text>
|
||||
<view class="value-wrap">
|
||||
<text class="tag-small coupon">{{ order.coupon_info.name }}</text>
|
||||
<text class="value discount">-¥{{ formatPrice(order.coupon_info.value) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 道具卡 -->
|
||||
<view class="info-row" v-if="order.item_card_info">
|
||||
<text class="label">道具卡</text>
|
||||
<view class="value-wrap">
|
||||
<text class="tag-small card">{{ order.item_card_info.name }}</text>
|
||||
<text class="value" v-if="order.item_card_info.effect_type === 1">双倍奖励</text>
|
||||
<text class="value" v-else>已使用</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-row" v-if="order.discount_amount && !order.coupon_info">
|
||||
<text class="label">其他优惠</text>
|
||||
<text class="value discount">-¥{{ formatPrice(order.discount_amount) }}</text>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="total-row">
|
||||
<text class="total-label">实付款</text>
|
||||
<view class="total-price-wrap">
|
||||
<text class="currency">¥</text>
|
||||
<text class="total-price">{{ formatPrice(order.actual_amount) }}</text>
|
||||
</view>
|
||||
<text class="total-label">{{ order.actual_amount > 0 ? '实付款' : '状态' }}</text>
|
||||
<view class="total-price-wrap">
|
||||
<text class="currency" v-if="order.actual_amount > 0">¥</text>
|
||||
<text class="total-price">{{ order.actual_amount > 0 ? formatPrice(order.actual_amount) : '无需支付' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 抽奖凭证(有凭证数据时显示) -->
|
||||
<view class="section-card proof-section" v-if="order.draw_receipts && order.draw_receipts.length > 0">
|
||||
@ -656,6 +676,24 @@ function showProofHelp() {
|
||||
background: $bg-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-small {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
|
||||
&.coupon {
|
||||
color: #FF6B6B;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
border: 1rpx solid rgba(255, 107, 107, 0.2);
|
||||
}
|
||||
|
||||
&.card {
|
||||
color: #6C5CE7;
|
||||
background: rgba(108, 92, 231, 0.1);
|
||||
border: 1rpx solid rgba(108, 92, 231, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
|
||||
@ -82,6 +82,8 @@
|
||||
<view class="product-meta">
|
||||
<text class="meta-item" v-if="item.activity_name">{{ item.activity_name }}</text>
|
||||
<text class="meta-item" v-if="item.issue_number">第{{ item.issue_number }}期</text>
|
||||
<text class="meta-item coupon-tag" v-if="item.coupon_info">券: {{ item.coupon_info.name }}</text>
|
||||
<text class="meta-item card-tag" v-if="item.item_card_info">卡: {{ item.item_card_info.name }}</text>
|
||||
</view>
|
||||
<text class="order-time">{{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
@ -94,9 +96,9 @@
|
||||
<text class="no-value">{{ item.order_no }}</text>
|
||||
</view>
|
||||
<view class="order-amount">
|
||||
<text class="amount-label">实付</text>
|
||||
<text class="amount-value">{{ formatAmount(item.actual_amount || item.total_amount) }}</text>
|
||||
</view>
|
||||
<text class="amount-label" v-if="shouldShowAmountLabel(item)">实付</text>
|
||||
<text class="amount-value">{{ getAmountText(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
@ -167,6 +169,22 @@ function formatAmount(a) {
|
||||
return `¥${yuan.toFixed(2)}`
|
||||
}
|
||||
|
||||
function shouldShowAmountLabel(item) {
|
||||
const amount = item.actual_amount || item.total_amount
|
||||
return amount > 0
|
||||
}
|
||||
|
||||
function getAmountText(item) {
|
||||
const amount = item.actual_amount || item.total_amount
|
||||
if (amount > 0) return formatAmount(amount)
|
||||
|
||||
// 金额为0的情况
|
||||
if (item.source_type === 3 || item.source_type === 2) {
|
||||
return '奖品'
|
||||
}
|
||||
return '免费'
|
||||
}
|
||||
|
||||
function getOrderTitle(item) {
|
||||
// 1. 优先使用 items 中的商品名称(通常是实物购买或中奖)
|
||||
if (item.items && item.items.length > 0 && item.items[0].title) {
|
||||
@ -724,6 +742,14 @@ onReachBottom(() => {
|
||||
background: $bg-secondary;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: $radius-sm;
|
||||
.coupon-tag {
|
||||
color: #FF6B6B;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
}
|
||||
.card-tag {
|
||||
color: #6C5CE7;
|
||||
background: rgba(108, 92, 231, 0.1);
|
||||
}
|
||||
}
|
||||
.order-time {
|
||||
font-size: $font-xs;
|
||||
|
||||
@ -7,14 +7,25 @@
|
||||
<view class="info-card">
|
||||
<view class="title">{{ detail.title || detail.name || '-' }}</view>
|
||||
<view class="price-row">
|
||||
<text class="price">¥{{ formatPrice(detail.price_sale || detail.price) }}</text>
|
||||
<text class="points" v-if="detail.points_required">{{ detail.points_required }}积分</text>
|
||||
<view class="points-wrap" v-if="detail.points_required">
|
||||
<text class="points-val">{{ detail.points_required }}</text>
|
||||
<text class="points-unit">积分</text>
|
||||
</view>
|
||||
<text class="price" v-else>¥{{ formatPrice(detail.price_sale || detail.price) }}</text>
|
||||
</view>
|
||||
<view class="stock" v-if="detail.stock !== null && detail.stock !== undefined">库存:{{ detail.stock }}</view>
|
||||
<view class="desc" v-if="detail.description">{{ detail.description }}</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">商品不存在</view>
|
||||
|
||||
<!-- Action Bar Moved Outside info-card -->
|
||||
<view class="action-bar-placeholder" v-if="detail.id"></view>
|
||||
<view class="action-bar" v-if="detail.id">
|
||||
<view class="action-btn redeem" v-if="detail.points_required" @tap="onRedeem">立即兑换</view>
|
||||
<view class="action-btn buy" v-else @tap="onBuy">立即购买</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -22,6 +33,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getProductDetail } from '../../api/appUser'
|
||||
import { redeemProductByPoints } from '../../utils/request.js'
|
||||
|
||||
const detail = ref({})
|
||||
const loading = ref(false)
|
||||
@ -43,6 +55,52 @@ async function fetchDetail(id) {
|
||||
}
|
||||
}
|
||||
|
||||
function onBuy() {
|
||||
uni.showToast({ title: '暂未开放购买', icon: 'none' })
|
||||
}
|
||||
|
||||
async function onRedeem() {
|
||||
const p = detail.value
|
||||
if (!p || !p.id) return
|
||||
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
confirmText: '去登录',
|
||||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认兑换',
|
||||
content: `是否消耗 ${p.points_required} 积分兑换 ${p.title}?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '兑换中...' })
|
||||
try {
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
if (!userId) throw new Error('用户ID不存在')
|
||||
|
||||
await redeemProductByPoints(userId, p.id, 1)
|
||||
uni.showToast({ title: '兑换成功', icon: 'success' })
|
||||
|
||||
// Refresh detail
|
||||
setTimeout(() => {
|
||||
fetchDetail(p.id)
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((opts) => {
|
||||
const id = opts && opts.id
|
||||
if (id) fetchDetail(id)
|
||||
@ -81,15 +139,14 @@ onLoad((opts) => {
|
||||
}
|
||||
|
||||
.info-card {
|
||||
margin: $spacing-lg;
|
||||
margin-top: -60rpx;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20rpx);
|
||||
border-radius: $radius-xl;
|
||||
background: #fff;
|
||||
border-radius: $radius-xl $radius-xl 0 0;
|
||||
padding: $spacing-xl;
|
||||
box-shadow: $shadow-lg;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: -40rpx; /* Slight overlap */
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -120,12 +177,19 @@ onLoad((opts) => {
|
||||
}
|
||||
}
|
||||
|
||||
.points {
|
||||
font-size: $font-sm;
|
||||
color: $brand-primary;
|
||||
padding: 6rpx $spacing-md;
|
||||
background: rgba($brand-primary, 0.1);
|
||||
border-radius: 100rpx;
|
||||
.points-wrap {
|
||||
display: flex; align-items: baseline;
|
||||
}
|
||||
.points-val {
|
||||
font-size: 48rpx;
|
||||
font-weight: 900;
|
||||
color: #FF9800;
|
||||
font-family: 'DIN Alternate', sans-serif;
|
||||
}
|
||||
.points-unit {
|
||||
font-size: 24rpx;
|
||||
color: #FF9800;
|
||||
margin-left: 6rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -156,6 +220,33 @@ onLoad((opts) => {
|
||||
}
|
||||
}
|
||||
|
||||
.action-bar-placeholder { height: 120rpx; }
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
padding: 20rpx 40rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(255,255,255,0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
z-index: 10;
|
||||
}
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.action-btn.redeem { background: linear-gradient(135deg, #FFB74D, #FF9800); box-shadow: 0 8rpx 20rpx rgba(255, 152, 0, 0.3); }
|
||||
.action-btn.buy { background: linear-gradient(135deg, #FF6B6B, #FF3B30); box-shadow: 0 8rpx 20rpx rgba(255, 59, 48, 0.3); }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(40rpx); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
|
||||
@ -37,14 +37,15 @@
|
||||
<view class="product-info">
|
||||
<text class="product-title">{{ p.title }}</text>
|
||||
<view class="product-bottom">
|
||||
<view class="price-row">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-val">{{ p.price }}</text>
|
||||
</view>
|
||||
<view class="points-badge" v-if="p.points">
|
||||
<view class="price-row" v-if="p.points">
|
||||
<text class="points-val">{{ p.points }}</text>
|
||||
<text class="points-unit">积分</text>
|
||||
</view>
|
||||
<view class="price-row" v-else>
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-val">{{ p.price }}</text>
|
||||
</view>
|
||||
<view class="redeem-btn" v-if="p.points" @tap.stop="onRedeemTap(p)">兑换</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -108,6 +109,46 @@ function onProductTap(p) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onRedeemTap(p) {
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
confirmText: '去登录',
|
||||
success: (res) => { if (res.confirm) uni.navigateTo({ url: '/pages/login/index' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uni.showModal({
|
||||
title: '确认兑换',
|
||||
content: `是否消耗 ${p.points} 积分兑换 ${p.title}?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showLoading({ title: '兑换中...' })
|
||||
try {
|
||||
// Get user_id from storage
|
||||
const userId = uni.getStorageSync('user_id')
|
||||
if (!userId) throw new Error('用户ID不存在')
|
||||
|
||||
await redeemProductByPoints(userId, p.id, 1)
|
||||
uni.showToast({ title: '兑换成功', icon: 'success' })
|
||||
|
||||
// Refresh products to update stock/points if needed
|
||||
setTimeout(() => {
|
||||
loadProducts()
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '兑换失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Filter logic
|
||||
const allProducts = ref([]) // Store all fetched products for client-side filtering
|
||||
|
||||
@ -473,33 +514,20 @@ onShareTimeline(() => {
|
||||
gap: 8rpx;
|
||||
}
|
||||
.price-row {
|
||||
color: $accent-red;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
display: flex; align-items: baseline;
|
||||
}
|
||||
.price-symbol {
|
||||
.points-val { font-size: 36rpx; font-weight: 700; color: #FF9800; }
|
||||
.points-unit { font-size: 22rpx; color: #FF9800; margin-left: 4rpx; }
|
||||
.price-symbol { font-size: 24rpx; color: #FF3B30; }
|
||||
.price-val { font-size: 36rpx; font-weight: 700; color: #FF3B30; }
|
||||
|
||||
.redeem-btn {
|
||||
background: #FF9800;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.price-val {
|
||||
font-size: 34rpx;
|
||||
}
|
||||
.points-badge {
|
||||
background: rgba($brand-primary, 0.1);
|
||||
color: $brand-primary;
|
||||
border: 1px solid rgba($brand-primary, 0.2);
|
||||
border-radius: 8rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.points-val {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.points-unit {
|
||||
font-size: 20rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 30rpx;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Loading & Empty */
|
||||
|
||||
@ -66,6 +66,14 @@ export function authRequest(options) {
|
||||
return request({ ...options, header })
|
||||
}
|
||||
|
||||
export function redeemProductByPoints(user_id, product_id, count) {
|
||||
return authRequest({
|
||||
url: '/api/app/users/points/redeem-product',
|
||||
method: 'POST',
|
||||
data: { user_id, product_id, count }
|
||||
})
|
||||
}
|
||||
|
||||
function getLanguage() {
|
||||
try { return (uni.getSystemInfoSync().language || 'zh-CN') } catch (_) { return 'zh-CN' }
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user