<template>
|
<view class="orders-page">
|
<app-nav title="我的订单" />
|
|
<view class="status-tabs">
|
<view
|
v-for="tab in statusTabs"
|
:key="tab.key"
|
class="status-tab"
|
:class="{ 'status-tab--active': activeStatus === tab.key }"
|
@tap="switchStatus(tab.key)"
|
>
|
<text class="status-tab__text">{{ tab.text }}</text>
|
<view class="status-tab__bar" />
|
</view>
|
</view>
|
|
<scroll-view class="orders-scroll" scroll-y="true" @scrolltolower="loadMore">
|
<view class="orders-content">
|
<view
|
v-for="order in orders"
|
:key="order.id"
|
class="order-card"
|
@tap="handleViewOrder(order)"
|
>
|
<view class="order-card__top">
|
<view class="order-card__title-wrap">
|
<text class="order-card__scenic">{{ order.scenic }}</text>
|
<text class="order-card__service">{{ order.service }}</text>
|
<text class="order-card__duration">{{ order.duration }}</text>
|
</view>
|
<text class="order-card__status" :class="`order-card__status--${order.status}`">
|
{{ getStatusText(order.status) }}
|
</text>
|
</view>
|
|
<view class="order-card__time">
|
<text class="order-card__time-label">下单时间</text>
|
<text class="order-card__time-value">{{ order.createdAt }}</text>
|
</view>
|
|
<view class="order-card__price-row">
|
<view class="order-card__unit">
|
<text class="order-card__unit-price">¥{{ order.unitPrice }}/{{ order.priceUnit }}</text>
|
<text class="order-card__quantity">x{{ order.quantity }}</text>
|
</view>
|
<text class="order-card__total">¥{{ order.total }}</text>
|
</view>
|
|
<view class="order-card__actions">
|
<view
|
v-if="order.status === 'pendingReview'"
|
class="order-card__button order-card__button--secondary"
|
@tap.stop="handleReview(order)"
|
>
|
去评价
|
</view>
|
<view
|
v-if="order.status === 'pendingPay'"
|
class="order-card__button order-card__button--secondary"
|
@tap.stop="handleCancel(order)"
|
>
|
取消订单
|
</view>
|
<view
|
class="order-card__button"
|
@tap.stop="handleViewOrder(order)"
|
>
|
查看订单
|
</view>
|
</view>
|
</view>
|
|
<view v-if="!loading && orders.length === 0" class="empty-state">
|
<text class="empty-state__title">暂无订单</text>
|
<text class="empty-state__desc">完成讲解预约后,订单会显示在这里</text>
|
</view>
|
<view v-if="loading" class="list-tip">订单加载中...</view>
|
<view v-else-if="orders.length > 0 && !hasMore" class="list-tip">没有更多订单了</view>
|
</view>
|
</scroll-view>
|
|
<cancel-order-sheet
|
:visible="cancelSheetVisible"
|
@close="closeCancelSheet"
|
@confirm="confirmCancelOrder"
|
/>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, ref } from 'vue'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import AppNav from '@/components/app-nav/app-nav.vue'
|
import CancelOrderSheet from '@/components/cancel-order-sheet/cancel-order-sheet.vue'
|
import { cancelTourOrder, getMiniOrders } from '@/services/order'
|
import { isLoggedIn, useLoginGuard } from '@/utils/auth-guard'
|
|
useLoginGuard('/pages/orders/index')
|
|
const statusTabs = [
|
{ key: 'all', text: '全部订单' },
|
{ key: 'pendingPay', text: '待付款' },
|
{ key: 'pendingGuide', text: '待讲解' },
|
{ key: 'serving', text: '讲解中' },
|
{ key: 'completed', text: '已完成' }
|
]
|
|
const statusTextMap = {
|
pendingPay: '待付款',
|
pendingAccept: '待接单',
|
pendingGuide: '待讲解',
|
serving: '讲解中',
|
pendingReview: '待评价',
|
completed: '已完成',
|
canceled: '已取消',
|
refunded: '已退款'
|
}
|
|
const activeStatus = ref('all')
|
const orders = ref([])
|
const page = ref(1)
|
const pageSize = 10
|
const total = ref(0)
|
const loading = ref(false)
|
const cancelSheetVisible = ref(false)
|
const selectedCancelOrder = ref(null)
|
const cancelingOrder = ref(false)
|
let requestSequence = 0
|
|
const hasMore = computed(() => orders.value.length < total.value)
|
|
onLoad((options) => {
|
const status = options && options.status
|
if (statusTabs.some((item) => item.key === status)) {
|
activeStatus.value = status
|
}
|
})
|
|
onShow(() => {
|
if (isLoggedIn()) loadOrders(true)
|
})
|
|
function switchStatus(status) {
|
if (activeStatus.value === status) return
|
activeStatus.value = status
|
loadOrders(true)
|
}
|
|
async function loadOrders(reset = false) {
|
if (loading.value && !reset) return
|
const sequence = ++requestSequence
|
const targetPage = reset ? 1 : page.value
|
loading.value = true
|
try {
|
const result = await getMiniOrders({
|
status: activeStatus.value,
|
page: targetPage,
|
size: pageSize
|
})
|
if (sequence !== requestSequence) return
|
const records = (result.records || []).map(mapOrder)
|
orders.value = reset ? records : orders.value.concat(records)
|
total.value = Number(result.total || 0)
|
page.value = targetPage + 1
|
} catch (error) {
|
if (reset && sequence === requestSequence) {
|
orders.value = []
|
total.value = 0
|
}
|
} finally {
|
if (sequence === requestSequence) loading.value = false
|
}
|
}
|
|
function loadMore() {
|
if (!loading.value && hasMore.value) loadOrders(false)
|
}
|
|
function mapOrder(order) {
|
return {
|
id: order.id,
|
orderNo: order.orderNo,
|
scenic: order.scenicName || '景区讲解',
|
service: order.serviceName || '讲解服务',
|
duration: `${Number(order.durationMinutes || 0)}分钟`,
|
createdAt: formatDateTime(order.placedAt),
|
unitPrice: formatMoney(order.unitPrice),
|
priceUnit: order.priceUnit || '人',
|
quantity: Number(order.peopleCount || 1),
|
total: formatMoney(order.totalAmount),
|
status: order.displayStatus
|
}
|
}
|
|
function formatDateTime(value) {
|
return value ? value.replace('T', ' ').slice(0, 19) : '-'
|
}
|
|
function formatMoney(value) {
|
return Number(value || 0).toFixed(2)
|
}
|
|
function getStatusText(status) {
|
return statusTextMap[status] || '全部订单'
|
}
|
|
function handleViewOrder(order) {
|
if (order.status === 'pendingPay') {
|
uni.navigateTo({
|
url: `/pages/order-pending-pay/index?id=${order.id}`
|
})
|
return
|
}
|
|
if (order.status === 'pendingAccept') {
|
uni.navigateTo({
|
url: `/pages/order-waiting/index?orderId=${order.id}`
|
})
|
return
|
}
|
|
if (order.status === 'pendingGuide') {
|
uni.navigateTo({
|
url: `/pages/order-pending-guide/index?id=${order.id}`
|
})
|
return
|
}
|
|
if (order.status === 'serving') {
|
uni.navigateTo({
|
url: `/pages/order-serving/index?id=${order.id}`
|
})
|
return
|
}
|
|
if (order.status === 'pendingReview') {
|
uni.navigateTo({
|
url: `/pages/order-pending-review/index?id=${order.id}`
|
})
|
return
|
}
|
|
if (order.status === 'completed') {
|
uni.navigateTo({
|
url: `/pages/order-completed/index?id=${order.id}`
|
})
|
return
|
}
|
|
if (order.status === 'canceled') {
|
uni.navigateTo({
|
url: `/pages/order-canceled/index?id=${order.id}`
|
})
|
return
|
}
|
|
if (order.status === 'refunded') {
|
uni.showToast({ title: '该订单已退款', icon: 'none' })
|
return
|
}
|
|
uni.showToast({
|
title: `订单${order.id}开发中`,
|
icon: 'none'
|
})
|
}
|
|
function handleReview(order) {
|
uni.navigateTo({
|
url: `/pages/order-pending-review/index?id=${order.id}`
|
})
|
}
|
|
function handleCancel(order) {
|
selectedCancelOrder.value = order
|
cancelSheetVisible.value = true
|
}
|
|
function closeCancelSheet() {
|
cancelSheetVisible.value = false
|
}
|
|
async function confirmCancelOrder() {
|
const orderId = selectedCancelOrder.value && selectedCancelOrder.value.id
|
cancelSheetVisible.value = false
|
if (!orderId || cancelingOrder.value) return
|
|
cancelingOrder.value = true
|
try {
|
await cancelTourOrder(orderId)
|
} catch (error) {
|
uni.showToast({ title: error.message || '取消订单失败', icon: 'none' })
|
return
|
} finally {
|
cancelingOrder.value = false
|
selectedCancelOrder.value = null
|
}
|
|
uni.navigateTo({
|
url: `/pages/order-canceled/index?id=${orderId}`
|
})
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
.orders-page {
|
height: 100vh;
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
background: #f4f6fa;
|
}
|
|
.status-tabs {
|
position: relative;
|
z-index: 10;
|
display: flex;
|
align-items: center;
|
height: 88rpx;
|
padding: 0 22rpx;
|
background: rgba(255, 255, 255, 0.98);
|
box-shadow: 0 4rpx 14rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.status-tab {
|
position: relative;
|
flex: 1;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
height: 88rpx;
|
min-width: 0;
|
color: #6b7280;
|
}
|
|
.status-tab__text {
|
font-size: 25rpx;
|
font-weight: 800;
|
line-height: 1;
|
white-space: nowrap;
|
}
|
|
.status-tab__bar {
|
position: absolute;
|
left: 50%;
|
bottom: 0;
|
width: 54rpx;
|
height: 8rpx;
|
border-radius: 8rpx 8rpx 0 0;
|
background: transparent;
|
transform: translateX(-50%);
|
}
|
|
.status-tab--active {
|
color: #111827;
|
}
|
|
.status-tab--active .status-tab__bar {
|
background: #278bff;
|
box-shadow: 0 4rpx 10rpx rgba(39, 139, 255, 0.18);
|
}
|
|
.orders-scroll {
|
flex: 1;
|
min-height: 0;
|
}
|
|
.orders-content {
|
padding: 24rpx 28rpx 56rpx;
|
}
|
|
.order-card {
|
margin-bottom: 24rpx;
|
padding: 26rpx;
|
border: 2rpx solid #edf1f6;
|
border-radius: 24rpx;
|
background: #ffffff;
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.order-card__top {
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 18rpx;
|
}
|
|
.order-card__title-wrap {
|
display: flex;
|
align-items: center;
|
flex: 1;
|
min-width: 0;
|
}
|
|
.order-card__scenic,
|
.order-card__service {
|
color: #111827;
|
font-size: 27rpx;
|
font-weight: 900;
|
line-height: 1;
|
white-space: nowrap;
|
}
|
|
.order-card__service {
|
margin-left: 10rpx;
|
}
|
|
.order-card__duration {
|
margin-left: 12rpx;
|
color: #8a93a2;
|
font-size: 24rpx;
|
font-weight: 750;
|
line-height: 1;
|
white-space: nowrap;
|
}
|
|
.order-card__status {
|
flex: 0 0 auto;
|
color: #111827;
|
font-size: 25rpx;
|
font-weight: 850;
|
line-height: 1;
|
}
|
|
.order-card__status--pendingPay {
|
color: #ff9f1a;
|
}
|
|
.order-card__status--pendingAccept,
|
.order-card__status--pendingGuide,
|
.order-card__status--serving {
|
color: #278bff;
|
}
|
|
.order-card__status--pendingReview {
|
color: #ff9f1a;
|
}
|
|
.order-card__status--completed {
|
color: #6b7280;
|
}
|
|
.order-card__status--canceled {
|
color: #8a93a2;
|
}
|
|
.order-card__meta {
|
display: flex;
|
align-items: center;
|
gap: 18rpx;
|
margin-top: 20rpx;
|
color: #8a93a2;
|
font-size: 23rpx;
|
font-weight: 650;
|
line-height: 1;
|
}
|
|
.order-card__time {
|
display: flex;
|
align-items: center;
|
gap: 16rpx;
|
margin-top: 26rpx;
|
color: #8a93a2;
|
font-size: 23rpx;
|
font-weight: 650;
|
line-height: 1;
|
}
|
|
.order-card__time-label {
|
color: #6b7280;
|
}
|
|
.order-card__time-value {
|
color: #8a93a2;
|
}
|
|
.order-card__price-row {
|
display: flex;
|
align-items: flex-end;
|
justify-content: space-between;
|
margin-top: 44rpx;
|
}
|
|
.order-card__unit {
|
display: flex;
|
align-items: baseline;
|
gap: 28rpx;
|
}
|
|
.order-card__unit-price {
|
color: #8a93a2;
|
font-size: 31rpx;
|
font-weight: 850;
|
line-height: 1;
|
}
|
|
.list-tip {
|
padding: 28rpx 0 12rpx;
|
color: #8a93a2;
|
font-size: 23rpx;
|
text-align: center;
|
}
|
|
.order-card__quantity {
|
color: #8a93a2;
|
font-size: 22rpx;
|
font-weight: 800;
|
line-height: 1;
|
}
|
|
.order-card__total {
|
color: #111827;
|
font-size: 33rpx;
|
font-weight: 900;
|
line-height: 1;
|
}
|
|
.order-card__actions {
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
gap: 18rpx;
|
margin-top: 32rpx;
|
}
|
|
.order-card__button {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
min-width: 146rpx;
|
height: 58rpx;
|
padding: 0 22rpx;
|
border-radius: 14rpx;
|
background: #278bff;
|
color: #ffffff;
|
font-size: 24rpx;
|
font-weight: 900;
|
box-shadow: 0 6rpx 14rpx rgba(39, 139, 255, 0.16);
|
}
|
|
.order-card__button--secondary {
|
border: 2rpx solid #d6e8ff;
|
background: #eff6ff;
|
color: #278bff;
|
box-shadow: none;
|
}
|
|
.empty-state {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
min-height: 420rpx;
|
border-radius: 28rpx;
|
background: #ffffff;
|
box-shadow: 0 6rpx 18rpx rgba(17, 24, 39, 0.04);
|
}
|
|
.empty-state__title {
|
color: #111827;
|
font-size: 30rpx;
|
font-weight: 900;
|
line-height: 1;
|
}
|
|
.empty-state__desc {
|
margin-top: 18rpx;
|
color: #8a93a2;
|
font-size: 24rpx;
|
font-weight: 650;
|
line-height: 1;
|
}
|
</style>
|