Files
lgp-wx-plus/pages/order/list.vue
2026-08-20 19:15:41 +08:00

392 lines
12 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>