chore: initial import

This commit is contained in:
liangjian
2026-06-29 09:42:53 +08:00
commit 9fea127de1
241 changed files with 82767 additions and 0 deletions

View File

@@ -0,0 +1,324 @@
<template>
<view class="booking-detail-page tn-safe-area-inset-bottom">
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="#FFFFFF">
<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="booking-title">{{ t('loadingTitle') }}</view>
<view class="demo-desc tn-margin-top-sm">{{ t('loadingDesc') }}</view>
</view>
<view v-else-if="booking" class="booking-card tn-padding-lg">
<view class="tn-flex tn-flex-row-between tn-flex-col-center">
<view>
<view class="booking-title">{{ t('orderNoLabel') }}</view>
<view class="booking-order tn-margin-top-xs">{{ booking.orderNo }}</view>
</view>
<view class="status-badge" :class="'status-' + booking.tagType">{{ booking.statusText }}</view>
</view>
<view class="info-box tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('roomTypeLabel') }}</text>
<text class="info-value">{{ booking.roomTypeText }}</text>
</view>
<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">{{ booking.buildingFloorText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('roomNoLabel') }}</text>
<text class="info-value">{{ booking.roomNoText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between tn-margin-bottom-sm">
<text class="info-label">{{ t('areaLabel') }}</text>
<text class="info-value">{{ booking.areaText }}</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">{{ booking.startDateText }}{{ t('dateRangeJoiner') }}{{ booking.endDateText }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('submittedAtLabel') }}</text>
<text class="info-value">{{ booking.createdAtText }}</text>
</view>
</view>
<view v-if="booking.rejectReasonText" class="reason-box tn-margin-top">
<view class="reason-title">{{ t('rejectReasonTitle') }}</view>
<view class="reason-content tn-margin-top-xs">{{ booking.rejectReasonText }}</view>
</view>
<view class="flow-box tn-margin-top">
<view class="flow-title">{{ t('flowTitle') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('pending_review') ? 'active' : ''">{{ t('flowStep1') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('approved_wait_assign') ? 'active' : ''">{{ t('flowStep2') }}</view>
<view class="flow-step tn-margin-top-sm" :class="isStepActive('assigned_wait_room') ? 'active' : ''">{{ t('flowStep3') }}</view>
</view>
</view>
<view v-else class="empty-card tn-padding-lg">
<view class="booking-title">{{ t('emptyTitle') }}</view>
<view class="demo-desc tn-margin-top-sm">{{ t('emptyDesc') }}</view>
</view>
</view>
</view>
</template>
<script>
import { orderSearchedDetail, orderSearchedMePage } from '@/api/index.js'
import { getSessionSnapshot } from '../../utils/app-state'
import {
normalizeBookingDetail,
normalizeBookingList,
parseOrderDetailResponse,
parseOrderListResponse
} from '../../utils/booking-helper'
export default {
i18n: {
zh: {
navTitle: '预订详情',
loadingTitle: '订单加载中',
loadingDesc: '正在同步该预订的真实详情,请稍候。',
orderNoLabel: '预定单号',
roomTypeLabel: '风格房型',
buildingFloorLabel: '楼栋楼层',
roomNoLabel: '房间号',
areaLabel: '所属区域',
stayDateLabel: '入住时间',
submittedAtLabel: '提交时间',
rejectReasonTitle: '驳回原因',
flowTitle: '当前流程',
flowStep1: '1. 已支付,等待角色审批',
flowStep2: '2. 审批通过,等待分配管家',
flowStep3: '3. 已分配管家,等待分配房号',
emptyTitle: '暂无预订记录',
emptyDesc: '当前暂无可查看的预订详情,请先前往预订记录确认订单。',
cachedDetailShown: '网络异常,已展示最近同步详情',
loadFailed: '预订详情加载失败',
dateRangeJoiner: ' 至 '
},
en: {
navTitle: 'Booking Detail',
loadingTitle: 'Loading booking',
loadingDesc: 'Syncing the latest booking details. Please wait.',
orderNoLabel: 'Booking No.',
roomTypeLabel: 'Room Type',
buildingFloorLabel: 'Building / Floor',
roomNoLabel: 'Room No.',
areaLabel: 'Area',
stayDateLabel: 'Stay Dates',
submittedAtLabel: 'Submitted At',
rejectReasonTitle: 'Rejection Reason',
flowTitle: 'Current Flow',
flowStep1: '1. Paid and waiting for approval',
flowStep2: '2. Approved and waiting for butler assignment',
flowStep3: '3. Butler assigned and waiting for room allocation',
emptyTitle: 'No booking record',
emptyDesc: 'No booking detail is available right now. Please check your booking records first.',
cachedDetailShown: 'Network issue detected. Showing the latest synced detail.',
loadFailed: 'Failed to load booking detail',
dateRangeJoiner: ' to '
}
},
data() {
return {
orderNo: '',
booking: null,
loading: false
}
},
computed: {
language() {
return this.$getLanguage()
}
},
watch: {
language() {
this.refreshBooking()
}
},
onLoad(options) {
this.orderNo = options.orderNo || ''
},
async onShow() {
await this.refreshBooking()
},
methods: {
t(key, params) {
return this.$tm('pkgBooking.booking_detail.booking_detail', 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
},
resolveBookingFromList(orderList = []) {
const list = Array.isArray(orderList) ? orderList : []
if (this.orderNo) {
return list.find(item => String(item.orderNo) === String(this.orderNo)) || null
}
return list[0] || null
},
refreshBooking() {
if (!this.isCurrentLogin()) {
this.booking = null
this.loading = false
return Promise.resolve()
}
this.loading = true
const requestPromise = this.orderNo
? orderSearchedDetail(this.orderNo)
: orderSearchedMePage({
PageIndex: 1,
PageSize: 100,
IsExport: false
})
return requestPromise
.then((response) => {
if (this.orderNo) {
this.booking = normalizeBookingDetail(parseOrderDetailResponse(response), this.language)
return
}
const orderList = normalizeBookingList(parseOrderListResponse(response), this.language)
this.booking = this.resolveBookingFromList(orderList)
})
.catch((error) => {
console.error('fetch booking detail failed', error)
const storeBookingList = this.getStoreBookingList()
const session = getSessionSnapshot()
const cachedList = storeBookingList.length ? storeBookingList : session.bookingList
this.booking = this.resolveBookingFromList(normalizeBookingList(cachedList, this.language))
uni.showToast({
title: this.booking
? this.t('cachedDetailShown')
: ((error && (error.message || error.msg)) || this.t('loadFailed')),
icon: 'none'
})
})
.finally(() => {
this.loading = false
})
},
isStepActive(targetStatus) {
if (!this.booking) return false
const order = ['pending_review', 'approved_wait_assign', 'assigned_wait_room']
return order.indexOf(this.booking.statusKey) >= order.indexOf(targetStatus)
}
}
}
</script>
<style scoped>
.booking-detail-page {
background: #f8f9fa;
min-height: 100vh;
}
.booking-card,
.empty-card {
background: #ffffff;
border-radius: 28rpx;
box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.04);
}
.booking-title,
.flow-title {
font-size: 30rpx;
font-weight: bold;
color: #222222;
}
.booking-order {
font-size: 26rpx;
color: #666666;
}
.status-badge {
padding: 10rpx 20rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: bold;
}
.status-pending,
.status-pending_review {
background: rgba(255, 165, 0, 0.12);
color: #d18a00;
}
.status-approved,
.status-approved_wait_assign,
.status-assigned_wait_room {
background: rgba($brand-primary-rgb, 0.18);
color: #4f6e17;
}
.status-rejected {
background: rgba(255, 90, 95, 0.12);
color: #d34d52;
}
.info-box,
.flow-box,
.reason-box {
padding: 24rpx;
background: #f8f9fa;
border-radius: 20rpx;
}
.info-label,
.demo-desc,
.reason-content {
color: #777777;
}
.info-value {
max-width: 60%;
text-align: right;
color: #222222;
}
.flow-step {
padding: 18rpx 22rpx;
background: #ffffff;
border-radius: 18rpx;
color: #777777;
}
.flow-step.active {
background: rgba($brand-primary-rgb, 0.16);
color: #4f6e17;
font-weight: bold;
}
.demo-btn {
padding: 16rpx 28rpx;
border-radius: 999rpx;
font-size: 26rpx;
}
.demo-btn-pass {
background: rgba($brand-primary-rgb, 0.18);
color: #4f6e17;
}
.demo-btn-reject {
background: rgba(255, 90, 95, 0.12);
color: #d34d52;
}
</style>