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
+2 -1
View File
@@ -19,7 +19,8 @@
"pages/profile/favorites/index",
"pages/profile/posts/index",
"pages/profile/about/index",
"pages/profile/exchange/index"
"pages/profile/exchange/index",
"pages/profile/exchange/orders/index"
],
"window": {
"backgroundTextStyle": "light",
+196 -1
View File
@@ -1 +1,196 @@
Page({});
import request from '../../../utils/request';
Page({
data: {
items: [],
currentSunlight: 0,
isLoading: true,
hasMore: true,
current: 1,
pageSize: 10,
activeType: 'PHYSICAL',
// Redeem popup
showRedeemPopup: false,
selectedItem: null,
redeemForm: {
recipientName: '',
phone: '',
address: ''
}
},
onLoad() {
this.fetchProfile();
this.fetchItems();
},
onShow() {
this.fetchProfile();
},
async fetchProfile() {
try {
const res = await request.get('/profile/detail');
if (res) {
this.setData({ currentSunlight: res.currentSunlight || 0 });
}
} catch (e) {
// Silent
}
},
async fetchItems(append = false) {
if (!append) {
this.setData({ isLoading: true, current: 1, items: [] });
}
try {
const res = await request.get('/exchange/list', {
current: this.data.current,
pageSize: this.data.pageSize,
type: this.data.activeType
});
const rawList = (res && res.list) ? res.list : [];
const total = (res && res.total) ? res.total : 0;
const now = Date.now();
const list = rawList.map(item => {
const hasStart = !!item.startTime;
const hasEnd = !!item.endTime;
const startTs = hasStart ? new Date(item.startTime).getTime() : 0;
const endTs = hasEnd ? new Date(item.endTime).getTime() : 0;
const notStarted = hasStart && now < startTs;
const hasEnded = hasEnd && now > endTs;
const isActive = !notStarted && !hasEnded;
let timeLabel = '';
if (hasStart && hasEnd) {
timeLabel = this.formatDate(item.startTime) + ' ~ ' + this.formatDate(item.endTime);
} else if (hasStart) {
timeLabel = this.formatDate(item.startTime) + ' 起';
} else if (hasEnd) {
timeLabel = '截止 ' + this.formatDate(item.endTime);
}
return {
...item,
hasTimeLimit: hasStart || hasEnd,
timeLabel,
notStarted,
hasEnded,
isActive
};
});
this.setData({
items: append ? [...this.data.items, ...list] : list,
hasMore: this.data.items.length + list.length < total
});
} catch (e) {
console.error('Fetch exchange items failed', e);
} finally {
this.setData({ isLoading: false });
wx.stopPullDownRefresh();
}
},
onReachBottom() {
if (!this.data.hasMore || this.data.isLoading) return;
this.setData({ current: this.data.current + 1 });
this.fetchItems(true);
},
onPullDownRefresh() {
this.fetchProfile();
this.fetchItems();
},
switchType(e) {
const key = e.currentTarget.dataset.key;
if (key === this.data.activeType) return;
this.setData({ activeType: key });
this.fetchItems();
},
// Redeem Flow
onItemTap(e) {
const item = e.currentTarget.dataset.item;
if (item.notStarted) {
wx.showToast({ title: '活动尚未开始', icon: 'none' });
return;
}
if (item.hasEnded) {
wx.showToast({ title: '活动已结束', icon: 'none' });
return;
}
if (item.stock === 0) {
wx.showToast({ title: '已兑完', icon: 'none' });
return;
}
this.setData({
selectedItem: item,
showRedeemPopup: true,
redeemForm: { recipientName: '', phone: '', address: '' }
});
},
onPopupClose() {
this.setData({ showRedeemPopup: false });
},
onFormInput(e) {
const field = e.currentTarget.dataset.field;
this.setData({ [`redeemForm.${field}`]: e.detail.value });
},
async confirmRedeem() {
const item = this.data.selectedItem;
if (!item) return;
// Check sunlight
if (this.data.currentSunlight < item.costSunlight) {
wx.showToast({ title: '阳光值不足', icon: 'none' });
return;
}
// Physical items require address
if (item.type === 'PHYSICAL') {
const { recipientName, phone, address } = this.data.redeemForm;
if (!recipientName || !phone || !address) {
wx.showToast({ title: '请填写完整收货信息', icon: 'none' });
return;
}
}
wx.showLoading({ title: '兑换中...', mask: true });
try {
await request.post('/exchange/redeem', {
itemId: item.id,
quantity: 1,
...this.data.redeemForm
});
wx.hideLoading();
wx.showToast({ title: '兑换成功!', icon: 'success' });
this.setData({ showRedeemPopup: false });
// Refresh data
this.fetchProfile();
this.fetchItems();
} catch (e) {
wx.hideLoading();
wx.showToast({ title: e.message || '兑换失败', icon: 'none' });
}
},
goToOrders() {
wx.navigateTo({ url: '/pages/profile/exchange/orders/index' });
},
formatDate(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const m = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
return m + '.' + day;
}
});
+6 -1
View File
@@ -1,6 +1,11 @@
{
"navigationBarTitleText": "兑换中心",
"disableScroll": true,
"usingComponents": {
"t-empty": "tdesign-miniprogram/empty/empty"
"t-empty": "tdesign-miniprogram/empty/empty",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-popup": "tdesign-miniprogram/popup/popup"
}
}
+139 -1
View File
@@ -1,3 +1,141 @@
<view class="exchange-page">
<t-empty icon="info-circle-filled" description="兑换中心功能正在开发中,敬请期待" />
<!-- Balance Header (sticky) -->
<view class="balance-header">
<view class="balance-card">
<view class="balance-left">
<text class="balance-label">我的阳光值</text>
<view class="balance-value-row">
<text class="balance-emoji">☀️</text>
<text class="balance-value">{{currentSunlight}}</text>
</view>
</view>
<view class="balance-right" bindtap="goToOrders">
<t-icon name="assignment" size="40rpx" color="#558B2F" />
<text class="orders-text">兑换记录</text>
</view>
</view>
</view>
<!-- Scrollable Content -->
<scroll-view scroll-y class="page-scroll" show-scrollbar="{{false}}" enhanced="{{true}}" lower-threshold="200" bindscrolltolower="onReachBottom">
<!-- Items Grid -->
<view class="items-grid" wx:if="{{items.length > 0}}">
<view class="item-card {{!item.isActive ? 'inactive' : ''}}" wx:for="{{items}}" wx:key="id" bindtap="onItemTap" data-item="{{item}}">
<view class="item-image-wrap">
<image wx:if="{{item.image}}" src="{{item.image.url}}" mode="aspectFill" class="item-img" lazy-load />
<view wx:else class="item-img-placeholder">
<t-icon name="gift" size="64rpx" color="#C5E1A5" />
</view>
<!-- Stock Badge -->
<view class="stock-badge" wx:if="{{item.isActive && item.stock >= 0 && item.stock <= 10 && item.stock > 0}}">
<text>仅剩 {{item.stock}}</text>
</view>
<!-- Status overlays -->
<view class="sold-out-mask" wx:if="{{item.stock === 0}}">
<text>已兑完</text>
</view>
<view class="sold-out-mask not-started" wx:elif="{{item.notStarted}}">
<text>未开始</text>
</view>
<view class="sold-out-mask ended" wx:elif="{{item.hasEnded}}">
<text>已结束</text>
</view>
</view>
<view class="item-info">
<text class="item-name">{{item.name}}</text>
<view class="item-price-row">
<text class="price-sun">☀️</text>
<text class="price-val">{{item.costSunlight}}</text>
</view>
<view class="item-time" wx:if="{{item.hasTimeLimit}}">
<t-icon name="time" size="24rpx" color="#9CA3AF" />
<text>{{item.timeLabel}}</text>
</view>
</view>
</view>
</view>
<!-- Loading / Empty States -->
<view class="state-footer" wx:if="{{isLoading}}">
<t-loading theme="circular" size="40rpx" text="加载中..." inherit-color />
</view>
<view class="empty-state" wx:elif="{{!isLoading && items.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 && items.length > 0}}">
<text class="no-more">— 已经到底啦 —</text>
</view>
<!-- Spacer -->
<view style="height: 60rpx;"></view>
</scroll-view>
<!-- Redeem Popup -->
<t-popup visible="{{showRedeemPopup}}" bind:visible-change="onPopupClose" placement="bottom">
<view class="redeem-popup" wx:if="{{selectedItem}}">
<view class="popup-header">
<text class="popup-title">确认兑换</text>
<view class="popup-close" bindtap="onPopupClose">
<t-icon name="close" size="40rpx" color="#999" />
</view>
</view>
<!-- Item Preview -->
<view class="redeem-item-preview">
<view class="preview-img-wrap">
<image wx:if="{{selectedItem.image}}" src="{{selectedItem.image.url}}" mode="aspectFill" class="preview-img" />
<view wx:else class="preview-img-placeholder">
<t-icon name="gift" size="48rpx" color="#C5E1A5" />
</view>
</view>
<view class="preview-info">
<text class="preview-name">{{selectedItem.name}}</text>
<text class="preview-desc">{{selectedItem.description}}</text>
<view class="preview-cost">
<text class="cost-sun">☀️</text>
<text class="cost-val">{{selectedItem.costSunlight}}</text>
<text class="cost-unit">阳光值</text>
</view>
</view>
</view>
<!-- Address Form (Physical only) -->
<view class="address-form" wx:if="{{selectedItem.type === 'PHYSICAL'}}">
<view class="form-title">收货信息</view>
<view class="form-item">
<text class="form-label">姓名</text>
<input placeholder="请输入收货人姓名" value="{{redeemForm.recipientName}}"
bindinput="onFormInput" data-field="recipientName" />
</view>
<view class="form-item">
<text class="form-label">电话</text>
<input type="number" placeholder="请输入联系电话" value="{{redeemForm.phone}}"
bindinput="onFormInput" data-field="phone" />
</view>
<view class="form-item">
<text class="form-label">地址</text>
<input placeholder="请输入详细收货地址" value="{{redeemForm.address}}"
bindinput="onFormInput" data-field="address" />
</view>
</view>
<!-- Balance Check -->
<view class="balance-check {{currentSunlight >= selectedItem.costSunlight ? '' : 'insufficient'}}">
<text>当前余额: ☀️ {{currentSunlight}}</text>
<text wx:if="{{currentSunlight < selectedItem.costSunlight}}" class="insufficient-tip">余额不足</text>
</view>
<!-- Confirm Button -->
<view class="redeem-btn {{currentSunlight >= selectedItem.costSunlight && selectedItem.stock !== 0 ? '' : 'disabled'}}"
bindtap="confirmRedeem">
<text>立即兑换</text>
</view>
</view>
</t-popup>
</view>
+439 -1
View File
@@ -1,9 +1,447 @@
page {
background: #F4F6F0;
}
.exchange-page {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-scroll {
flex: 1;
height: 0;
}
/* ========== Balance Header ========== */
.balance-header {
background: linear-gradient(180deg, #E8F5E9 0%, #F4F6F0 100%);
padding: 32rpx 32rpx 24rpx;
flex-shrink: 0;
}
.balance-card {
background: #fff;
border-radius: 24rpx;
padding: 32rpx 36rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
}
.balance-label {
font-size: 22rpx;
color: #9CA3AF;
display: block;
margin-bottom: 8rpx;
font-weight: 500;
}
.balance-value-row {
display: flex;
align-items: center;
gap: 8rpx;
}
.balance-emoji {
font-size: 36rpx;
}
.balance-value {
font-size: 48rpx;
font-weight: 800;
color: #558B2F;
line-height: 1;
}
.balance-right {
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
padding: 16rpx 24rpx;
border-radius: 20rpx;
background: #F1F8E9;
}
.orders-text {
font-size: 20rpx;
color: #558B2F;
font-weight: 600;
}
/* ========== Type Tabs ========== */
.type-tabs {
display: flex;
white-space: nowrap;
padding: 20rpx 32rpx;
gap: 16rpx;
flex-shrink: 0;
}
.type-tab {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10rpx 28rpx;
background: #fff;
border: 2rpx solid #E5E7EB;
border-radius: 40rpx;
font-size: 26rpx;
color: #6B7280;
font-weight: 600;
flex-shrink: 0;
margin-right: 0;
transition: all 0.2s;
}
.type-tab.active {
background: #333;
color: #fff;
border-color: #333;
}
/* ========== Items Grid ========== */
.items-grid {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 32rpx;
gap: 20rpx;
}
.item-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
transition: transform 0.15s;
}
.item-card:active {
transform: scale(0.97);
}
.item-image-wrap {
position: relative;
width: 100%;
aspect-ratio: 1;
background: #F8FAF5;
overflow: hidden;
}
.item-img {
width: 100%;
height: 100%;
display: block;
}
.item-img-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: linear-gradient(135deg, #F1F8E9, #E8F5E9);
}
.stock-badge {
position: absolute;
top: 12rpx;
right: 12rpx;
background: rgba(239, 83, 80, 0.85);
color: #fff;
font-size: 20rpx;
font-weight: 700;
padding: 4rpx 14rpx;
border-radius: 14rpx;
}
.sold-out-mask {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 30rpx;
font-weight: 800;
letter-spacing: 4rpx;
}
.sold-out-mask.not-started {
background: rgba(33, 150, 243, 0.5);
}
.sold-out-mask.ended {
background: rgba(0, 0, 0, 0.5);
}
.item-card.inactive {
opacity: 0.7;
}
.item-info {
padding: 16rpx 20rpx 20rpx;
}
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #1F2937;
display: block;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-price-row {
display: flex;
align-items: center;
gap: 6rpx;
}
.price-sun {
font-size: 24rpx;
}
.price-val {
font-size: 30rpx;
font-weight: 800;
color: #E65100;
}
.item-time {
display: flex;
align-items: center;
gap: 6rpx;
margin-top: 8rpx;
font-size: 22rpx;
color: #9CA3AF;
font-weight: 500;
}
/* ========== 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;
}
/* ========== Redeem Popup ========== */
.redeem-popup {
background: #fff;
border-radius: 32rpx 32rpx 0 0;
padding: 36rpx;
padding-bottom: calc(36rpx + env(safe-area-inset-bottom));
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 28rpx;
}
.popup-title {
font-size: 34rpx;
font-weight: 700;
color: #1F2937;
}
.popup-close {
padding: 8rpx;
}
/* Item Preview in Popup */
.redeem-item-preview {
display: flex;
gap: 24rpx;
padding: 24rpx;
background: #F8FAF5;
border-radius: 20rpx;
margin-bottom: 28rpx;
}
.preview-img-wrap {
width: 140rpx;
height: 140rpx;
border-radius: 16rpx;
overflow: hidden;
flex-shrink: 0;
background: #E8F5E9;
}
.preview-img {
width: 100%;
height: 100%;
}
.preview-img-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
gap: 8rpx;
}
.preview-name {
font-size: 30rpx;
font-weight: 700;
color: #1F2937;
}
.preview-desc {
font-size: 24rpx;
color: #9CA3AF;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.5;
}
.preview-cost {
display: flex;
align-items: center;
gap: 6rpx;
margin-top: 4rpx;
}
.cost-sun { font-size: 24rpx; }
.cost-val { font-size: 32rpx; font-weight: 800; color: #E65100; }
.cost-unit { font-size: 22rpx; color: #9CA3AF; margin-left: 2rpx; }
/* Address Form */
.address-form {
margin-bottom: 24rpx;
}
.form-title {
font-size: 28rpx;
font-weight: 700;
color: #374151;
margin-bottom: 16rpx;
}
.form-item {
display: flex;
align-items: center;
background: #F9FAFB;
border: 2rpx solid #F3F4F6;
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-bottom: 12rpx;
gap: 16rpx;
}
.form-label {
font-size: 26rpx;
color: #6B7280;
font-weight: 600;
flex-shrink: 0;
width: 60rpx;
}
.form-item input {
flex: 1;
font-size: 26rpx;
color: #1F2937;
}
/* Balance Check */
.balance-check {
padding: 20rpx 24rpx;
background: #F1F8E9;
border-radius: 16rpx;
margin-bottom: 20rpx;
font-size: 26rpx;
color: #558B2F;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.balance-check.insufficient {
background: #FFF3E0;
color: #E65100;
}
.insufficient-tip {
color: #EF5350;
font-weight: 700;
}
/* Redeem Button */
.redeem-btn {
width: 100%;
height: 92rpx;
background: linear-gradient(135deg, #558B2F, #689F38);
border-radius: 46rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 30rpx;
font-weight: 700;
transition: all 0.2s;
box-shadow: 0 8rpx 24rpx rgba(85, 139, 47, 0.25);
}
.redeem-btn:active {
transform: scale(0.97);
opacity: 0.9;
}
.redeem-btn.disabled {
background: #E5E7EB;
color: #9CA3AF;
pointer-events: none;
}
+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;
}
+1
View File
@@ -1,5 +1,6 @@
{
"navigationBarTitleText": "我的收藏",
"disableScroll": true,
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
-1
View File
@@ -46,7 +46,6 @@
<text class="menu-text">兑换中心</text>
</view>
<view class="menu-right-info">
<text class="menu-badge-text">开发中</text>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
</view>