feat:兑换中心

This commit is contained in:
Blizzard
2026-02-25 13:28:17 +08:00
parent 5789e8bf17
commit bcba77f912
11 changed files with 1169 additions and 10 deletions
+80
View File
@@ -0,0 +1,80 @@
import request from '../../../../utils/request';
const STATUS_MAP = {
1: { text: '待处理', theme: 'warning' },
2: { text: '处理中', theme: 'primary' },
3: { text: '已发货', theme: 'primary' },
4: { text: '已完成', theme: 'success' },
5: { text: '已取消', theme: 'default' }
};
Page({
data: {
orders: [],
isLoading: true,
hasMore: true,
current: 1,
pageSize: 10,
activeStatus: 0,
statusTabs: [
{ key: 0, label: '全部' },
{ key: 1, label: '待处理' },
{ key: 4, label: '已完成' },
{ key: 5, label: '已取消' }
]
},
onLoad() {
this.fetchOrders();
},
onShow() {
this.fetchOrders();
},
async fetchOrders(append = false) {
if (!append) {
this.setData({ isLoading: true, current: 1, orders: [] });
}
try {
const params = {
current: this.data.current,
pageSize: this.data.pageSize
};
if (this.data.activeStatus) {
params.status = this.data.activeStatus;
}
const res = await request.get('/exchange/orders', params);
let list = (res && res.list) ? res.list : [];
const total = (res && res.total) ? res.total : 0;
// Enrich with status display info
list = list.map(order => ({
...order,
statusInfo: STATUS_MAP[order.status] || { text: '未知', theme: 'default' }
}));
this.setData({
orders: append ? [...this.data.orders, ...list] : list,
hasMore: this.data.orders.length + list.length < total
});
} catch (e) {
console.error('Fetch orders failed', e);
} finally {
this.setData({ isLoading: false });
}
},
switchStatus(e) {
const key = e.currentTarget.dataset.key;
if (key === this.data.activeStatus) return;
this.setData({ activeStatus: key });
this.fetchOrders();
},
onReachBottom() {
if (!this.data.hasMore || this.data.isLoading) return;
this.setData({ current: this.data.current + 1 });
this.fetchOrders(true);
}
});
+10
View File
@@ -0,0 +1,10 @@
{
"navigationBarTitleText": "兑换记录",
"disableScroll": true,
"usingComponents": {
"t-empty": "tdesign-miniprogram/empty/empty",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-tag": "tdesign-miniprogram/tag/tag"
}
}
+68
View File
@@ -0,0 +1,68 @@
<view class="orders-page">
<!-- Status Tabs -->
<view class="status-tabs-wrap">
<scroll-view class="status-tabs" scroll-x enable-flex show-scrollbar="{{false}}">
<view wx:for="{{statusTabs}}" wx:key="key"
class="status-tab {{activeStatus === item.key ? 'active' : ''}}"
bindtap="switchStatus" data-key="{{item.key}}">
<text>{{item.label}}</text>
</view>
</scroll-view>
</view>
<scroll-view scroll-y class="page-scroll" show-scrollbar="{{false}}" enhanced="{{true}}" bindscrolltolower="onReachBottom">
<view class="orders-list" wx:if="{{orders.length > 0}}">
<view class="order-card" wx:for="{{orders}}" wx:key="id">
<view class="order-header">
<text class="order-time">{{item.createdAtStr}}</text>
<t-tag size="small" variant="light" theme="{{item.statusInfo.theme}}">
{{item.statusInfo.text}}
</t-tag>
</view>
<view class="order-body">
<view class="order-img-wrap">
<image wx:if="{{item.item && item.item.image}}" src="{{item.item.image.url}}" mode="aspectFill" class="order-img" />
<view wx:else class="order-img-placeholder">
<t-icon name="gift" size="48rpx" color="#C5E1A5" />
</view>
</view>
<view class="order-info">
<text class="order-item-name">{{item.itemName}}</text>
<text class="order-type-tag">{{item.itemType === 'PHYSICAL' ? '实物' : (item.itemType === 'VIRTUAL' ? '虚拟' : '优惠券')}}</text>
<view class="order-cost-row">
<text class="order-cost-sun">☀️</text>
<text class="order-cost-val">-{{item.costSunlight}}</text>
<text class="order-qty" wx:if="{{item.quantity > 1}}">x{{item.quantity}}</text>
</view>
</view>
</view>
<!-- Shipping Info (if physical and shipped) -->
<view class="order-shipping" wx:if="{{item.trackingNo}}">
<t-icon name="deliver" size="28rpx" color="#558B2F" />
<text class="tracking-text">快递单号: {{item.trackingNo}}</text>
</view>
</view>
</view>
<!-- Loading / Empty -->
<view class="state-footer" wx:if="{{isLoading}}">
<t-loading theme="circular" size="40rpx" text="加载中..." inherit-color />
</view>
<view class="empty-state" wx:elif="{{!isLoading && orders.length === 0}}">
<text class="empty-icon">📦</text>
<text class="empty-title">暂无兑换记录</text>
<text class="empty-desc">去兑换中心挑选心仪好礼吧</text>
</view>
<view class="state-footer" wx:elif="{{!hasMore && orders.length > 0}}">
<text class="no-more">— 已经到底啦 —</text>
</view>
<view style="height: 60rpx;"></view>
</scroll-view>
</view>
+224
View File
@@ -0,0 +1,224 @@
page {
background: #F4F6F0;
}
.orders-page {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-scroll {
flex: 1;
height: 0;
}
/* ========== Status Tabs ========== */
.status-tabs-wrap {
flex-shrink: 0;
background: #F4F6F0;
padding: 20rpx 0 12rpx;
}
.status-tabs {
display: flex;
white-space: nowrap;
padding-left: 32rpx;
height: 88rpx;
width: 100%;
box-sizing: border-box;
}
.status-tab {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 28rpx;
height: 72rpx;
background: #fff;
border-radius: 36rpx;
margin-right: 16rpx;
font-size: 26rpx;
color: #546E7A;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04);
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
flex-shrink: 0;
border: 2rpx solid transparent;
}
.status-tab:active {
transform: scale(0.95);
}
.status-tab.active {
background: #558B2F;
color: #fff;
font-weight: 700;
box-shadow: 0 8rpx 20rpx rgba(85, 139, 47, 0.3);
border-color: #558B2F;
}
/* ========== Orders List ========== */
.orders-list {
padding: 12rpx 32rpx;
}
.order-card {
background: #fff;
border-radius: 20rpx;
padding: 28rpx;
margin-bottom: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 16rpx;
border-bottom: 1rpx solid #F3F4F6;
}
.order-time {
font-size: 24rpx;
color: #9CA3AF;
font-weight: 500;
}
.order-body {
display: flex;
gap: 20rpx;
}
.order-img-wrap {
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
background: #F8FAF5;
}
.order-img {
width: 100%;
height: 100%;
}
.order-img-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #F1F8E9, #E8F5E9);
}
.order-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 8rpx;
}
.order-item-name {
font-size: 28rpx;
font-weight: 600;
color: #1F2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-type-tag {
display: inline-block;
width: fit-content;
font-size: 20rpx;
color: #6B7280;
background: #F3F4F6;
padding: 2rpx 14rpx;
border-radius: 10rpx;
font-weight: 500;
}
.order-cost-row {
display: flex;
align-items: center;
gap: 6rpx;
}
.order-cost-sun {
font-size: 22rpx;
}
.order-cost-val {
font-size: 28rpx;
font-weight: 800;
color: #E65100;
}
.order-qty {
font-size: 22rpx;
color: #9CA3AF;
margin-left: 4rpx;
}
/* Shipping */
.order-shipping {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1rpx solid #F3F4F6;
display: flex;
align-items: center;
gap: 8rpx;
}
.tracking-text {
font-size: 24rpx;
color: #558B2F;
font-weight: 600;
}
/* ========== Empty & Footer States ========== */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx 80rpx;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 24rpx;
}
.empty-title {
font-size: 30rpx;
font-weight: 700;
color: #374151;
margin-bottom: 12rpx;
}
.empty-desc {
font-size: 24rpx;
color: #9CA3AF;
font-weight: 500;
}
.state-footer {
padding: 40rpx;
display: flex;
justify-content: center;
align-items: center;
}
.no-more {
font-size: 24rpx;
color: #D1D5DB;
font-weight: 500;
}