Files
QSY-lvjuxcx/pkgBooking/booking_list/booking_list.vue
2026-06-29 09:42:53 +08:00

268 lines
7.8 KiB
Vue

<template>
<view class="booking-list-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="transparent">
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</view>
</tn-nav-bar>
<view class="page-content tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
<view v-if="loading" class="empty-card tn-padding-lg">
<view class="empty-title">{{ t('loadingTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
</view>
<view v-else-if="bookingList.length">
<view
v-for="item in bookingList"
:key="item.orderNo"
class="booking-card tn-margin-bottom-lg"
@click="goDetail(item)"
>
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view>
<view class="booking-title">{{ item.roomTypeText || t('bookingFallbackTitle') }}</view>
<view class="booking-subtitle tn-margin-top-xs">{{ item.orderNo }}</view>
</view>
<view class="status-badge" :class="'status-' + item.tagType">
{{ item.statusText }}
</view>
</view>
<view class="info-grid tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('buildingFloorLabel') }}</text>
<text class="info-value">{{ item.buildingFloorText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('stayDateLabel') }}</text>
<text class="info-value">{{ item.startDateText }}{{ t('dateRangeJoiner') }}{{ item.endDateText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('submittedAtLabel') }}</text>
<text class="info-value">{{ item.createdAtText }}</text>
</view>
</view>
</view>
</view>
<view v-else class="empty-card tn-padding-lg">
<view class="empty-title">{{ t('emptyTitle') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
<view class="custom-btn btn-primary tn-margin-top-lg" @click="goSelectRoom">
<text class="tn-text-bold">{{ t('goSelectRoom') }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { orderSearchedMePage } from '@/api/index.js'
import { getSessionSnapshot } from '../../utils/app-state'
import { normalizeBookingList, parseOrderListResponse } from '../../utils/booking-helper'
export default {
i18n: {
zh: {
navTitle: '预订记录',
loadingTitle: '订单加载中',
loadingDesc: '正在同步您的真实预订记录,请稍候。',
bookingFallbackTitle: '房源预订',
buildingFloorLabel: '楼栋楼层',
stayDateLabel: '入住时间',
submittedAtLabel: '提交时间',
emptyTitle: '暂无预订记录',
emptyDesc: '当前暂无预订记录,完成预订后会在这里展示。',
goSelectRoom: '去选房',
cachedListShown: '网络异常,已展示最近同步记录',
loadFailed: '预订记录加载失败',
dateRangeJoiner: ' 至 '
},
en: {
navTitle: 'Booking Records',
loadingTitle: 'Loading bookings',
loadingDesc: 'Syncing your latest booking records. Please wait.',
bookingFallbackTitle: 'Room Booking',
buildingFloorLabel: 'Building / Floor',
stayDateLabel: 'Stay Dates',
submittedAtLabel: 'Submitted At',
emptyTitle: 'No booking records',
emptyDesc: 'No booking record is available yet. Completed bookings will appear here.',
goSelectRoom: 'Select Room',
cachedListShown: 'Network issue detected. Showing the latest synced records.',
loadFailed: 'Failed to load booking records',
dateRangeJoiner: ' to '
}
},
data() {
return {
bookingList: [],
loading: false
}
},
computed: {
language() {
return this.$getLanguage()
}
},
watch: {
language() {
this.fetchBookingList()
}
},
async onShow() {
await this.fetchBookingList()
},
methods: {
t(key, params) {
return this.$tm('pkgBooking.booking_list.booking_list', key, params)
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
getStoreBookingList() {
const list = this.$store && this.$store.state && this.$store.state.vuex_valid_orders
return Array.isArray(list) ? list : []
},
isCurrentLogin() {
const storeUser = this.getStoreUser()
if (storeUser && Object.keys(storeUser).length) {
return true
}
return !!getSessionSnapshot().isLogin
},
applyBookingList(list = []) {
this.bookingList = Array.isArray(list) ? list : []
},
fetchBookingList() {
if (!this.isCurrentLogin()) {
this.applyBookingList()
this.loading = false
return Promise.resolve()
}
this.loading = true
return orderSearchedMePage({
PageIndex: 1,
PageSize: 100,
IsExport: false
})
.then((response) => {
this.applyBookingList(normalizeBookingList(parseOrderListResponse(response), this.language))
})
.catch((error) => {
console.error('fetch booking list failed', error)
const storeBookingList = this.getStoreBookingList()
const session = getSessionSnapshot()
const cachedList = storeBookingList.length ? storeBookingList : session.bookingList
this.applyBookingList(normalizeBookingList(cachedList, this.language))
uni.showToast({
title: this.bookingList.length
? this.t('cachedListShown')
: ((error && (error.message || error.msg)) || this.t('loadFailed')),
icon: 'none'
})
})
.finally(() => {
this.loading = false
})
},
goDetail(item) {
uni.navigateTo({
url: `/pkgBooking/booking_detail/booking_detail?orderNo=${item.orderNo}`
})
},
goSelectRoom() {
uni.reLaunch({
url: '/pages/index/index?index=0&pageId=select-room'
})
}
}
}
</script>
<style scoped lang="scss">
.booking-list-page {
min-height: 100vh;
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
}
.booking-card,
.empty-card {
background: #ffffff;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.booking-title,
.empty-title {
font-size: 32rpx;
font-weight: bold;
color: #1A2233;
}
.booking-subtitle,
.empty-desc,
.info-label {
color: #6A7A99;
}
.info-value {
max-width: 58%;
text-align: right;
color: #1A2233;
font-weight: 500;
}
.info-row {
padding: 16rpx 0;
border-bottom: 1px dashed rgba($brand-primary, 0.15);
}
.info-row:last-child {
border-bottom: none;
}
.status-badge {
padding: 8rpx 20rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: bold;
}
.status-pending {
background: rgba(245, 158, 11, 0.12);
color: #B45309;
}
.status-approved {
background: rgba($brand-primary, 0.12);
color: $brand-primary;
}
.status-rejected {
background: rgba(239, 68, 68, 0.12);
color: #B91C1C;
}
.custom-btn {
width: 100%;
height: 96rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 999rpx;
transition: all 0.3s;
}
.custom-btn:active {
transform: scale(0.98);
}
.btn-primary {
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #ffffff;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.25);
}
</style>