392 lines
12 KiB
Vue
392 lines
12 KiB
Vue
<template>
|
||
<view class="order-page" :style="themePageStyle">
|
||
<ThemeNavbar title="我的订单" :show-back="false" />
|
||
<!-- 顶部状态 Tab:点击与左右滑共用 currentIndex -->
|
||
<view class="status-tabs" :style="{ background: themeSurface, borderColor: themeBorder }">
|
||
<view
|
||
v-for="(tab, index) in tabs"
|
||
:key="tab.key"
|
||
class="status-tab"
|
||
:class="{active: currentIndex === index}"
|
||
:style="{ color: currentIndex === index ? themePrimary : themeTextSoft }"
|
||
@tap="onTabTap(index)"
|
||
>
|
||
<text>{{ tab.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<swiper
|
||
class="order-swiper"
|
||
:style="{ height: swiperHeight + 'px' }"
|
||
:current="currentIndex"
|
||
@change="onSwiperChange"
|
||
>
|
||
<swiper-item v-for="(tab, index) in tabs" :key="tab.key">
|
||
<scroll-view
|
||
class="order-list"
|
||
scroll-y
|
||
@scrolltolower="loadMore(index)"
|
||
>
|
||
<view
|
||
v-if="tabCaches[index] && tabCaches[index].loading && tabCaches[index].list.length === 0"
|
||
class="empty-tip"
|
||
:style="{ color: themeTextSoft }"
|
||
>加载中...</view>
|
||
<view
|
||
v-else-if="tabCaches[index] && tabCaches[index].list.length === 0"
|
||
class="empty-tip"
|
||
:style="{ color: themeTextSoft }"
|
||
>
|
||
<text class="empty-icon">📭</text>
|
||
<text>暂无订单</text>
|
||
</view>
|
||
|
||
<view
|
||
v-for="order in (tabCaches[index] && tabCaches[index].list) || []"
|
||
:key="order.id"
|
||
class="order-card"
|
||
:style="{ background: themeSurface, borderColor: themeBorder }"
|
||
@tap="goDetail(order.id)"
|
||
>
|
||
<view class="order-card-header">
|
||
<text class="order-no" :style="{ color: themeTextSoft }">订单号:{{ order.order_no }}</text>
|
||
<text class="order-status" :style="{ color: statusColor(order.status) }">
|
||
{{ statusText(order.status) }}
|
||
</text>
|
||
</view>
|
||
|
||
<view class="order-card-body">
|
||
<view class="order-item-thumb" v-if="order.items && order.items.length">
|
||
<image
|
||
v-for="(item, idx) in order.items.slice(0, 4)"
|
||
:key="idx"
|
||
:src="item.cover"
|
||
mode="aspectFill"
|
||
class="thumb"
|
||
/>
|
||
<view v-if="order.items.length > 4" class="thumb-more">
|
||
+{{ order.items.length - 4 }}
|
||
</view>
|
||
</view>
|
||
<view v-else class="order-item-empty">共 {{ order.item_count || 0 }} 件商品</view>
|
||
</view>
|
||
|
||
<view class="order-card-footer" :style="{ borderColor: themeBorder }">
|
||
<view class="order-amount">
|
||
<text class="amount-label" :style="{ color: themeTextSoft }">合计</text>
|
||
<text class="amount-value" :style="{ color: themePrice }">¥{{ order.total_amount_text || '0' }}</text>
|
||
</view>
|
||
<view class="order-time" :style="{ color: themeTextSoft }">{{ order.created_at }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-if="tabCaches[index] && tabCaches[index].loading && tabCaches[index].list.length > 0"
|
||
class="load-more-tip"
|
||
>加载中...</view>
|
||
<view
|
||
v-if="tabCaches[index] && tabCaches[index].noMore && tabCaches[index].list.length > 0"
|
||
class="load-more-tip"
|
||
>没有更多了</view>
|
||
</scroll-view>
|
||
</swiper-item>
|
||
</swiper>
|
||
<ThemeTabBar id="themeTabBar" ref="themeTabBar" />
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import {getOrderListApi} from '@/api/page/order.js'
|
||
import themeMixin from '@/mixins/theme'
|
||
import ThemeNavbar from '@/components/theme/ThemeNavbar.vue'
|
||
import ThemeTabBar from '@/components/theme/ThemeTabBar.vue'
|
||
|
||
/**
|
||
* 订单状态枚举(与后端 OrderModel 一致)
|
||
* 0=待付款 1=已付款 2=已发货 3=已完成 4=已取消
|
||
*/
|
||
const STATUS_MAP = {
|
||
0: '待付款',
|
||
1: '已付款',
|
||
2: '已发货',
|
||
3: '已完成',
|
||
4: '已取消'
|
||
}
|
||
|
||
function emptyCache() {
|
||
return {list: [], page: 1, noMore: false, loaded: false, loading: false}
|
||
}
|
||
|
||
export default {
|
||
components: { ThemeNavbar, ThemeTabBar },
|
||
mixins: [themeMixin],
|
||
data() {
|
||
return {
|
||
tabs: [
|
||
{label: '全部', value: undefined, key: 'all'},
|
||
{label: '待付款', value: 0, key: '0'},
|
||
{label: '待发货', value: 1, key: '1'},
|
||
{label: '待收货', value: 2, key: '2'},
|
||
{label: '已完成', value: 3, key: '3'}
|
||
],
|
||
currentIndex: 0,
|
||
tabCaches: [],
|
||
pageSize: 20,
|
||
swiperHeight: 400
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
this.tabCaches = this.tabs.map(() => emptyCache())
|
||
if (options.status !== undefined && options.status !== '') {
|
||
const idx = this.tabs.findIndex((tab) => Number(tab.value) === Number(options.status))
|
||
if (idx >= 0) this.currentIndex = idx
|
||
}
|
||
this.measureSwiper()
|
||
this.ensureTab(this.currentIndex, true)
|
||
},
|
||
onReady() {
|
||
this.measureSwiper()
|
||
},
|
||
onPullDownRefresh() {
|
||
this.ensureTab(this.currentIndex, true).finally(() => {
|
||
uni.stopPullDownRefresh()
|
||
})
|
||
},
|
||
methods: {
|
||
/**
|
||
* swiper 必须有明确像素高度,否则微信里内容高度为 0
|
||
*/
|
||
measureSwiper() {
|
||
try {
|
||
const sys = uni.getSystemInfoSync() || {}
|
||
const status = sys.statusBarHeight || 20
|
||
const menu = uni.getMenuButtonBoundingClientRect && uni.getMenuButtonBoundingClientRect()
|
||
const inner = menu && menu.height ? (menu.top - status) * 2 + menu.height : 44
|
||
const navH = this.isPcWeixin ? 0 : (status + inner)
|
||
const tabH = 44
|
||
const safeBottom = (sys.safeAreaInsets && sys.safeAreaInsets.bottom) || 0
|
||
const tabbarH = 60 + safeBottom
|
||
this.swiperHeight = Math.max(240, (sys.windowHeight || 667) - navH - tabH - tabbarH)
|
||
} catch (e) {
|
||
this.swiperHeight = 400
|
||
}
|
||
},
|
||
statusText(status) {
|
||
return STATUS_MAP[status] || '-'
|
||
},
|
||
statusColor(status) {
|
||
if (status === 3) return this.themeAccent
|
||
if (status === 4) return this.themeTextSoft
|
||
return this.themePrimary
|
||
},
|
||
onTabTap(index) {
|
||
if (this.currentIndex === index) return
|
||
this.currentIndex = index
|
||
this.ensureTab(index)
|
||
},
|
||
onSwiperChange(e) {
|
||
const index = (e.detail && e.detail.current) || 0
|
||
this.currentIndex = index
|
||
this.ensureTab(index)
|
||
},
|
||
/**
|
||
* 每个 Tab 自己缓存列表,滑到没加载过的才请求,避免来回清空
|
||
*/
|
||
ensureTab(index, force = false) {
|
||
const cache = this.tabCaches[index]
|
||
if (!cache) return Promise.resolve()
|
||
if (!force && (cache.loaded || cache.loading)) return Promise.resolve()
|
||
return this.fetchList(index, true)
|
||
},
|
||
async fetchList(index, reset = false) {
|
||
const cache = this.tabCaches[index]
|
||
if (!cache || cache.loading) return
|
||
cache.loading = true
|
||
this.$forceUpdate()
|
||
if (reset) {
|
||
cache.page = 1
|
||
cache.noMore = false
|
||
}
|
||
try {
|
||
const tab = this.tabs[index]
|
||
const res = await getOrderListApi({
|
||
status: tab.value,
|
||
page: cache.page,
|
||
pageSize: this.pageSize
|
||
})
|
||
const items = Array.isArray(res) ? res : (res.items || res.list || [])
|
||
cache.list = reset ? items : cache.list.concat(items)
|
||
cache.loaded = true
|
||
if (items.length < this.pageSize) {
|
||
cache.noMore = true
|
||
}
|
||
} catch (e) {
|
||
// 拦截器已统一 toast
|
||
} finally {
|
||
cache.loading = false
|
||
this.$forceUpdate()
|
||
}
|
||
},
|
||
loadMore(index) {
|
||
const cache = this.tabCaches[index]
|
||
if (!cache || cache.noMore || cache.loading) return
|
||
cache.page += 1
|
||
this.fetchList(index, false)
|
||
},
|
||
goDetail(id) {
|
||
uni.navigateTo({url: `/pages/order/detail?id=${id}`})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.order-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.status-tabs {
|
||
display: flex;
|
||
border-bottom: 1rpx solid;
|
||
flex-shrink: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.status-tab {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 24rpx 0;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.status-tab.active {
|
||
font-weight: 600;
|
||
position: relative;
|
||
}
|
||
|
||
.order-swiper {
|
||
width: 100%;
|
||
}
|
||
|
||
.order-list {
|
||
height: 100%;
|
||
padding: 24rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.order-card {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 28rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.order-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.order-no {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.order-status {
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.order-card-body {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.order-item-thumb {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
.thumb {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 8rpx;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.thumb-more {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #f5f5f5;
|
||
border-radius: 8rpx;
|
||
color: #999;
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.order-item-empty {
|
||
color: #999;
|
||
font-size: 26rpx;
|
||
padding: 40rpx 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.order-card-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-top: 1rpx solid #f5f5f5;
|
||
padding-top: 20rpx;
|
||
}
|
||
|
||
.order-amount {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.amount-label {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.amount-value {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1a1a1a;
|
||
}
|
||
|
||
.order-time {
|
||
font-size: 22rpx;
|
||
color: #bbb;
|
||
}
|
||
|
||
.empty-tip {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 120rpx 0;
|
||
color: #999;
|
||
font-size: 26rpx;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 80rpx;
|
||
}
|
||
|
||
.load-more-tip {
|
||
text-align: center;
|
||
padding: 24rpx;
|
||
color: #bbb;
|
||
font-size: 22rpx;
|
||
}
|
||
</style>
|