chore: initial import
This commit is contained in:
354
pkgBooking/book/book.vue
Normal file
354
pkgBooking/book/book.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<view class="book-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="#F8F9FA">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="book-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '180rpx'}">
|
||||
|
||||
<!-- 选中的房源卡片 (Room Preview) -->
|
||||
<view v-if="applyData" class="room-preview-card tn-margin-bottom-xl tn-margin-top-sm tn-flex tn-flex-row-between tn-flex-col-center tn-padding">
|
||||
<view class="preview-img-box">
|
||||
<image :src="applyData.image" mode="aspectFit" class="preview-img"></image>
|
||||
</view>
|
||||
<view class="preview-info tn-flex-1 tn-margin-left tn-flex tn-flex-direction-column tn-flex-row-between">
|
||||
<view>
|
||||
<view class="tn-text-sm tn-color-gray">{{ t('bookingFor') }}</view>
|
||||
<view class="tn-text-bold tn-text-lg tn-color-black tn-margin-top-xs clamp-text-2">
|
||||
{{ displayStyleName }}
|
||||
</view>
|
||||
<view v-if="applyData.roomTypeName || applyData.roomTypeNameEn" class="tn-color-gray tn-text-xs tn-margin-top-xs">
|
||||
{{ displayRoomTypeLine }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-bottom tn-margin-top-sm">
|
||||
<view class="room-tag tn-text-xs tn-text-bold">{{ displayLocationText }}</view>
|
||||
<view class="tn-color-black tn-text tn-text-bold">
|
||||
<text class="tn-text-sm tn-color-gray tn-margin-right-xs">{{ t('deposit') }}</text>$500.00
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 填写日期表单 -->
|
||||
<view class="form-section tn-margin-bottom-xl tn-padding-lg form-card">
|
||||
<view class="section-title tn-text-bold tn-text-xl tn-margin-bottom-xs">{{ t('checkInDate') }}</view>
|
||||
<view class="tn-color-gray tn-text-sm tn-margin-bottom-lg">{{ t('checkInDesc') }}</view>
|
||||
|
||||
<view class="form-item tn-margin-bottom-lg">
|
||||
<view class="form-label tn-text-bold">{{ t('startDate') }}</view>
|
||||
<picker mode="date" @change="onStartDateChange">
|
||||
<view class="form-input tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<text :class="form.startDate ? 'tn-color-black tn-text-bold' : 'tn-color-gray'">
|
||||
{{ form.startDate ? form.startDate : t('selectDate') }}
|
||||
</text>
|
||||
<text class="tn-icon-calendar tn-color-black tn-text-xl"></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item tn-margin-bottom-sm">
|
||||
<view class="form-label tn-text-bold">{{ t('endDate') }}</view>
|
||||
<picker mode="date" @change="onEndDateChange" :start="form.startDate">
|
||||
<view class="form-input tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<text :class="form.endDate ? 'tn-color-black tn-text-bold' : 'tn-color-gray'">
|
||||
{{ form.endDate ? form.endDate : t('selectDate') }}
|
||||
</text>
|
||||
<text class="tn-icon-calendar tn-color-black tn-text-xl"></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view v-if="stayNights > 0" class="tn-margin-top-lg booking-summary">
|
||||
<view class="summary-row tn-flex tn-flex-row-between">
|
||||
<text class="tn-color-gray">{{ t('stayNights') }}</text>
|
||||
<text class="tn-text-bold tn-color-black">{{ stayNights }} {{ t('nightUnit') }}</text>
|
||||
</view>
|
||||
<view class="summary-row tn-flex tn-flex-row-between tn-margin-top-sm">
|
||||
<text class="tn-color-gray">{{ t('bookingStatus') }}</text>
|
||||
<text class="tn-text-bold tn-color-black">{{ t('pendingPayment') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部固定按钮区 -->
|
||||
<view class="fixed-bottom-btn-area tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="tn-flex tn-flex-direction-column">
|
||||
<view class="tn-text-sm tn-color-gray">{{ t('deposit') }}</view>
|
||||
<view class="tn-text-bold tn-text-xl" style="color: $brand-primary;">$500.00</view>
|
||||
</view>
|
||||
<view class="custom-btn btn-dark" style="width: 60%; height: 100rpx;" @click="submitBooking" :class="{ 'btn-disabled': isSubmitting }">
|
||||
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '处理中...' : 'Processing...') : t('submitOrder') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compactEnglishText } from '../../utils/select-room-normalizers'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isSubmitting: false,
|
||||
applyData: null,
|
||||
form: {
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
},
|
||||
i18n: {
|
||||
zh: {
|
||||
title: '预订确认',
|
||||
bookingFor: '你正在预约',
|
||||
deposit: '定金',
|
||||
checkInDate: '填写预订时间',
|
||||
checkInDesc: '支付后会先生成预定单并进入审核,不会直接占用房间资源。',
|
||||
startDate: '入住日期 (Check-in)',
|
||||
endDate: '离开日期 (Check-out)',
|
||||
selectDate: '请选择日期',
|
||||
submitOrder: '进入支付页',
|
||||
stayNights: '入住晚数',
|
||||
nightUnit: '晚',
|
||||
bookingStatus: '当前状态',
|
||||
pendingPayment: '待支付定金',
|
||||
roomTypeLabel: '房型:'
|
||||
},
|
||||
en: {
|
||||
title: 'Booking Confirm',
|
||||
bookingFor: 'Booking For',
|
||||
deposit: 'Deposit',
|
||||
checkInDate: 'Booking Period',
|
||||
checkInDesc: 'Payment will create a booking order first, without occupying room inventory.',
|
||||
startDate: 'Check-in Date',
|
||||
endDate: 'Check-out Date',
|
||||
selectDate: 'Select Date',
|
||||
submitOrder: 'Go To Payment',
|
||||
stayNights: 'Stay Nights',
|
||||
nightUnit: 'nights',
|
||||
bookingStatus: 'Status',
|
||||
pendingPayment: 'Deposit Pending',
|
||||
roomTypeLabel: 'Room Type: '
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
},
|
||||
displayStyleName() {
|
||||
if (!this.applyData) {
|
||||
return ''
|
||||
}
|
||||
return this.language === 'en'
|
||||
? (this.applyData.styleNameEnDisplay || compactEnglishText(this.applyData.styleNameEn || this.applyData.styleNameZh || '', 24))
|
||||
: (this.applyData.styleNameZh || this.applyData.styleNameEn || '')
|
||||
},
|
||||
displayRoomTypeLine() {
|
||||
if (!this.applyData) {
|
||||
return ''
|
||||
}
|
||||
const roomTypeText = this.language === 'en'
|
||||
? (this.applyData.roomTypeNameEnDisplay || compactEnglishText(this.applyData.roomTypeNameEn || this.applyData.roomTypeName || '', 22))
|
||||
: (this.applyData.roomTypeName || this.applyData.roomTypeNameEn || '')
|
||||
const areaText = this.language === 'en'
|
||||
? (this.applyData.roomAreaEnDisplay || compactEnglishText(this.applyData.roomAreaEn || this.applyData.roomArea || '', 28))
|
||||
: (this.applyData.roomArea || this.applyData.roomAreaEn || '')
|
||||
return [roomTypeText ? `${this.t('roomTypeLabel')}${roomTypeText}` : '', areaText].filter(Boolean).join(' | ')
|
||||
},
|
||||
displayLocationText() {
|
||||
if (!this.applyData) {
|
||||
return ''
|
||||
}
|
||||
const buildingText = this.language === 'en'
|
||||
? (this.applyData.buildingEnDisplay || compactEnglishText(this.applyData.buildingEn || this.applyData.building || '', 18))
|
||||
: (this.applyData.building || this.applyData.buildingEn || '')
|
||||
const floorText = this.language === 'en'
|
||||
? (this.applyData.floorEnDisplay || compactEnglishText(this.applyData.floorEn || this.applyData.floor || '', 12))
|
||||
: (this.applyData.floor || this.applyData.floorEn || '')
|
||||
return [buildingText, floorText].filter(Boolean).join(' - ')
|
||||
},
|
||||
stayNights() {
|
||||
if (!this.form.startDate || !this.form.endDate) {
|
||||
return 0
|
||||
}
|
||||
const start = new Date(this.form.startDate).getTime()
|
||||
const end = new Date(this.form.endDate).getTime()
|
||||
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
|
||||
return 0
|
||||
}
|
||||
return Math.ceil((end - start) / (24 * 60 * 60 * 1000))
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.applyData) {
|
||||
Promise.resolve(options.applyData)
|
||||
.then(decodeURIComponent)
|
||||
.then(JSON.parse)
|
||||
.then((data) => {
|
||||
this.applyData = data && typeof data === 'object' ? data : {}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to parse apply data', e);
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgBooking.book.book', key)
|
||||
},
|
||||
onStartDateChange(e) {
|
||||
this.form.startDate = e.detail.value;
|
||||
},
|
||||
onEndDateChange(e) {
|
||||
this.form.endDate = e.detail.value;
|
||||
},
|
||||
submitBooking() {
|
||||
if (this.isSubmitting) return;
|
||||
if (!this.form.startDate || !this.form.endDate) {
|
||||
uni.showToast({ title: this.language === 'zh' ? '请选择入住与离开日期' : 'Please select check-in and check-out dates', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
if (new Date(this.form.endDate).getTime() <= new Date(this.form.startDate).getTime()) {
|
||||
uni.showToast({ title: this.language === 'zh' ? '离开日期需晚于入住日期' : 'Check-out must be later than check-in', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
const orderData = {
|
||||
...this.applyData,
|
||||
startDate: this.form.startDate,
|
||||
endDate: this.form.endDate,
|
||||
stayNights: this.stayNights,
|
||||
sourceType: 'booking-intent'
|
||||
};
|
||||
|
||||
let roomDataStr = encodeURIComponent(JSON.stringify(orderData));
|
||||
|
||||
uni.showLoading({ title: this.language === 'zh' ? '正在生成订单...' : 'Creating Order...' });
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
this.isSubmitting = false;
|
||||
uni.redirectTo({
|
||||
url: `/pkgBooking/pay/pay?roomData=${roomDataStr}`
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.book-page {
|
||||
background-color: #F8F9FA;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 选中的房源卡片预览 */
|
||||
.room-preview-card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.03);
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.room-preview-card:hover {
|
||||
border-color: rgba($brand-primary-rgb, 0.3);
|
||||
box-shadow: 0 16rpx 50rpx rgba($brand-primary-rgb, 0.08);
|
||||
}
|
||||
.preview-img-box {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.preview-info {
|
||||
height: 150rpx;
|
||||
}
|
||||
.room-tag {
|
||||
background-color: #FAFFEB;
|
||||
color: #557A2B;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.clamp-text-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.form-card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.03);
|
||||
}
|
||||
.form-label {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.form-input {
|
||||
background-color: #F8F9FA;
|
||||
height: 100rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 0 36rpx;
|
||||
font-size: 30rpx;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.form-input:active {
|
||||
border-color: rgba($brand-primary-rgb, 0.5);
|
||||
background-color: #FAFFEB;
|
||||
}
|
||||
|
||||
.booking-summary {
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #F8F9FA;
|
||||
}
|
||||
|
||||
/* 底部固定按钮区 */
|
||||
.fixed-bottom-btn-area {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 -10rpx 40rpx rgba(0,0,0,0.04);
|
||||
padding: 30rpx 40rpx calc(30rpx + env(safe-area-inset-bottom));
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 自定义按钮样式 */
|
||||
.custom-btn {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100rpx;
|
||||
padding: 12rpx 36rpx;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 10rpx 30rpx rgba(34, 34, 34, 0.2);
|
||||
}
|
||||
.custom-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.btn-dark {
|
||||
background-color: #222222;
|
||||
color: $brand-primary;
|
||||
}
|
||||
.btn-disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
705
pkgBooking/book_viewing/book_viewing.vue
Normal file
705
pkgBooking/book_viewing/book_viewing.vue
Normal file
@@ -0,0 +1,705 @@
|
||||
<template>
|
||||
<view class="book-viewing-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="true" :backTitle="t('backTitle')" backgroundColor="#F0F6FF" :bottomShadow="false" :fixed="true">
|
||||
<view class="nav-title">{{ t('navTitle') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="content-wrapper" :style="{ paddingTop: vuex_custom_bar_height + 'px' }">
|
||||
|
||||
<!-- 房型/区域信息 -->
|
||||
<view class="info-card tn-flex tn-flex-row-between tn-flex-col-center" v-if="displayInfoTitle">
|
||||
<view class="info-left">
|
||||
<view class="info-title">{{ displayInfoTitle }}</view>
|
||||
<view class="info-desc">{{ displayInfoDesc }}</view>
|
||||
</view>
|
||||
<view class="info-icon">
|
||||
<text class="tn-icon-company"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 看房时间卡片 -->
|
||||
<view class="form-card">
|
||||
<view class="card-header tn-flex tn-flex-row-between tn-flex-col-bottom">
|
||||
<view class="header-left tn-flex tn-flex-col-bottom">
|
||||
<text class="card-title">{{ t('selectViewingTime') }}</text>
|
||||
<text class="card-subtitle">{{ t('selectViewingTimeDesc') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="date-scroll" scroll-x :show-scrollbar="false">
|
||||
<view class="date-flex">
|
||||
<view
|
||||
v-for="(item, index) in dateList"
|
||||
:key="index"
|
||||
class="date-item tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center"
|
||||
:class="{ 'date-item--active': currentDateIndex === index }"
|
||||
@click="selectDate(index)"
|
||||
>
|
||||
<text class="date-week">{{ item.week }}</text>
|
||||
<text class="date-day">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="time-grid">
|
||||
<view
|
||||
v-for="(item, index) in timeSlots"
|
||||
:key="index"
|
||||
class="time-slot"
|
||||
:class="{ 'time-slot--active': currentTimeIndex === index }"
|
||||
@click="selectTimeSlot(index)"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 租住需求卡片 -->
|
||||
<view class="form-card">
|
||||
<view class="card-header">
|
||||
<view class="header-left tn-flex tn-flex-col-bottom">
|
||||
<text class="card-title">{{ t('rentalNeeds') }}</text>
|
||||
<text class="card-subtitle">{{ t('rentalNeedsDesc') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field-item">
|
||||
<view class="field-label">{{ t('durationLabel') }}</view>
|
||||
<view class="tag-grid">
|
||||
<view
|
||||
v-for="(item, index) in durationOptions"
|
||||
:key="index"
|
||||
class="tag-item"
|
||||
:class="{ 'tag-item--active': formData.duration === item }"
|
||||
@click="selectDuration(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field-item">
|
||||
<view class="field-label">{{ t('budgetLabel') }}</view>
|
||||
<view class="input-box tn-flex tn-flex-col-center">
|
||||
<text class="tn-icon-money input-icon"></text>
|
||||
<input
|
||||
class="custom-input"
|
||||
v-model="formData.budget"
|
||||
type="digit"
|
||||
:placeholder="t('budgetPlaceholder')"
|
||||
placeholder-style="color: #A1B0C4;"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="field-item">
|
||||
<view class="field-label">{{ t('workplaceLabel') }}</view>
|
||||
<view class="input-box tn-flex tn-flex-col-center">
|
||||
<text class="tn-icon-location input-icon"></text>
|
||||
<input
|
||||
class="custom-input"
|
||||
v-model="formData.workplace"
|
||||
type="text"
|
||||
:placeholder="t('workplacePlaceholder')"
|
||||
placeholder-style="color: #A1B0C4;"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<view class="footer-bar tn-safe-area-inset-bottom">
|
||||
<view class="submit-btn" @click="handleSubmit">
|
||||
<text>{{ t('submitBooking') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { submitApplicationForm } from '@/api/index.js'
|
||||
import { compactEnglishText, localizePayMethodText } from '../../utils/select-room-normalizers'
|
||||
|
||||
function buildViewingDateTime(date = '', time = '', isEnd = false) {
|
||||
if (!date || !time) {
|
||||
return null
|
||||
}
|
||||
const range = String(time).split('-')
|
||||
const selected = isEnd ? range[1] : range[0]
|
||||
if (!selected) {
|
||||
return null
|
||||
}
|
||||
return `${date}T${selected}:00`
|
||||
}
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
},
|
||||
displayInfoTitle() {
|
||||
if (!this.sourceData) {
|
||||
return ''
|
||||
}
|
||||
if (this.language === 'en') {
|
||||
return this.sourceData.roomTypeNameEnDisplay
|
||||
|| this.sourceData.roomNameEnDisplay
|
||||
|| compactEnglishText(this.sourceData.roomTypeNameEn || this.sourceData.roomNameEn || this.sourceData.bannerTitleEn || this.sourceData.bannerTitle || this.sourceData.areaLabelEn || this.sourceData.areaLabel || this.sourceData.areaNameEn || this.sourceData.areaName || '', 24)
|
||||
}
|
||||
return this.sourceData.roomTypeName || this.sourceData.roomName || this.sourceData.bannerTitle || this.sourceData.areaLabel || this.sourceData.areaName || ''
|
||||
},
|
||||
displayInfoDesc() {
|
||||
if (!this.sourceData) {
|
||||
return this.t('defaultInfoDesc')
|
||||
}
|
||||
const bannerCopy = this.language === 'en'
|
||||
? compactEnglishText(this.sourceData.bannerSubtitleEn || this.sourceData.bannerDescEn || this.sourceData.bannerSubtitle || this.sourceData.bannerDesc || '', 48)
|
||||
: (this.sourceData.bannerSubtitle || this.sourceData.bannerDesc || this.sourceData.bannerSubtitleEn || this.sourceData.bannerDescEn || '')
|
||||
const areaLabel = this.language === 'en'
|
||||
? (this.sourceData.areaLabelEnDisplay || this.sourceData.areaNameEnDisplay || compactEnglishText(this.sourceData.areaLabelEn || this.sourceData.areaLabel || this.sourceData.areaNameEn || this.sourceData.areaName || '', 26))
|
||||
: (this.sourceData.areaLabel || this.sourceData.areaName || this.sourceData.areaLabelEn || this.sourceData.areaNameEn || '')
|
||||
const meta = [bannerCopy, areaLabel, localizePayMethodText(this.sourceData.payMethod, this.language)].filter(Boolean).join(' · ')
|
||||
return meta || this.t('defaultInfoDesc')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dateList: [],
|
||||
timeSlots: [
|
||||
'10:00-11:00', '11:00-12:00', '12:00-13:00',
|
||||
'13:00-14:00', '14:00-15:00', '15:00-16:00',
|
||||
'16:00-17:00', '17:00-18:00', '18:00-19:00',
|
||||
'19:00-20:00'
|
||||
],
|
||||
durationOptions: [],
|
||||
currentDateIndex: 0,
|
||||
currentTimeIndex: -1,
|
||||
formData: {
|
||||
budget: '',
|
||||
duration: '',
|
||||
workplace: ''
|
||||
},
|
||||
sourceData: null,
|
||||
isSubmitting: false,
|
||||
i18n: {
|
||||
zh: {
|
||||
backTitle: '返回',
|
||||
navTitle: '预约现场带看',
|
||||
selectViewingTime: '选择看房时间',
|
||||
selectViewingTimeDesc: '请选择您方便的时间段',
|
||||
rentalNeeds: '您的租住需求',
|
||||
rentalNeedsDesc: '帮助管家更好地为您推荐房源',
|
||||
durationLabel: '预计租住时长',
|
||||
budgetLabel: '租金预算 (元/月)',
|
||||
budgetPlaceholder: '请输入您的心理价位',
|
||||
workplaceLabel: '工作/通勤地点',
|
||||
workplacePlaceholder: '请输入主要通勤地点',
|
||||
submitBooking: '提交预约',
|
||||
defaultInfoDesc: '专属管家为您提供一对一带看服务',
|
||||
duration1: '1-3个月',
|
||||
duration2: '4-6个月',
|
||||
duration3: '7-9个月',
|
||||
duration4: '10-12个月',
|
||||
duration5: '1年以上',
|
||||
week0: '周日',
|
||||
week1: '周一',
|
||||
week2: '周二',
|
||||
week3: '周三',
|
||||
week4: '周四',
|
||||
week5: '周五',
|
||||
week6: '周六',
|
||||
today: '今天',
|
||||
tomorrow: '明天',
|
||||
remarkTitle: '预约看房申请',
|
||||
remarkTime: '看房时间:{date} {time}',
|
||||
remarkDuration: '预计租住时长:{duration}',
|
||||
remarkBudget: '租金预算:{budget} 元/月',
|
||||
remarkWorkplace: '工作/通勤地点:{workplace}',
|
||||
remarkArea: '意向区域:{area}',
|
||||
remarkRoomType: '意向房型:{roomType}',
|
||||
remarkSource: '当前来源:{room}',
|
||||
remarkPayMethod: '支付方式:{payMethod}',
|
||||
remarkTail: '备注:请宿管确认带看安排;若带看完成并锁定房号,可转为预定单通知租客支付。',
|
||||
selectTimeRequired: '请选择看房时间段',
|
||||
selectDurationRequired: '请选择预计租住时长',
|
||||
submitting: '提交中...',
|
||||
submitSuccessTitle: '预约成功',
|
||||
submitSuccessContent: '您的预约看房申请已提交。\n系统会自动转发给对应区域有房间权限的宿管,宿管确认带看后可直接完结订单,或选定房型房号后转为预定单发送给租客支付。',
|
||||
submitFailed: '预约提交失败'
|
||||
},
|
||||
en: {
|
||||
backTitle: 'Back',
|
||||
navTitle: 'On-site Viewing',
|
||||
selectViewingTime: 'Select Viewing Time',
|
||||
selectViewingTimeDesc: 'Choose a time slot that works for you',
|
||||
rentalNeeds: 'Rental Needs',
|
||||
rentalNeedsDesc: 'Help the manager recommend a suitable room',
|
||||
durationLabel: 'Expected Stay Duration',
|
||||
budgetLabel: 'Budget (CNY/month)',
|
||||
budgetPlaceholder: 'Enter your expected budget',
|
||||
workplaceLabel: 'Work / Commute Location',
|
||||
workplacePlaceholder: 'Enter your main commute location',
|
||||
submitBooking: 'Submit Request',
|
||||
defaultInfoDesc: 'A dedicated manager will arrange a one-on-one viewing for you',
|
||||
duration1: '1-3 months',
|
||||
duration2: '4-6 months',
|
||||
duration3: '7-9 months',
|
||||
duration4: '10-12 months',
|
||||
duration5: 'Over 1 year',
|
||||
week0: 'Sun',
|
||||
week1: 'Mon',
|
||||
week2: 'Tue',
|
||||
week3: 'Wed',
|
||||
week4: 'Thu',
|
||||
week5: 'Fri',
|
||||
week6: 'Sat',
|
||||
today: 'Today',
|
||||
tomorrow: 'Tomorrow',
|
||||
remarkTitle: 'Viewing Request',
|
||||
remarkTime: 'Viewing Time: {date} {time}',
|
||||
remarkDuration: 'Expected Stay: {duration}',
|
||||
remarkBudget: 'Budget: {budget} CNY/month',
|
||||
remarkWorkplace: 'Work / Commute Location: {workplace}',
|
||||
remarkArea: 'Preferred Area: {area}',
|
||||
remarkRoomType: 'Preferred Room Type: {roomType}',
|
||||
remarkSource: 'Current Source: {room}',
|
||||
remarkPayMethod: 'Payment Method: {payMethod}',
|
||||
remarkTail: 'Note: Please confirm the viewing schedule with the manager. After viewing and locking a room number, the order can be converted into a booking for payment.',
|
||||
selectTimeRequired: 'Please select a viewing time slot',
|
||||
selectDurationRequired: 'Please select the expected stay duration',
|
||||
submitting: 'Submitting...',
|
||||
submitSuccessTitle: 'Booking Submitted',
|
||||
submitSuccessContent: 'Your viewing request has been submitted.\nThe system will forward it to the authorized manager of the selected area. After the manager confirms the viewing, the order can be completed directly or converted into a booking for payment once a room is selected.',
|
||||
submitFailed: 'Failed to submit the booking'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.syncDurationOptions()
|
||||
this.generateDates()
|
||||
if (options.payload) {
|
||||
try {
|
||||
this.sourceData = JSON.parse(decodeURIComponent(options.payload))
|
||||
} catch (e) {
|
||||
console.error('Failed to parse payload', e)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgBooking.book_viewing.book_viewing', key, params)
|
||||
},
|
||||
syncDurationOptions() {
|
||||
this.durationOptions = [
|
||||
this.t('duration1'),
|
||||
this.t('duration2'),
|
||||
this.t('duration3'),
|
||||
this.t('duration4'),
|
||||
this.t('duration5')
|
||||
]
|
||||
},
|
||||
generateDates() {
|
||||
const dates = []
|
||||
const today = new Date()
|
||||
const weeks = [
|
||||
this.t('week0'),
|
||||
this.t('week1'),
|
||||
this.t('week2'),
|
||||
this.t('week3'),
|
||||
this.t('week4'),
|
||||
this.t('week5'),
|
||||
this.t('week6')
|
||||
]
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(today)
|
||||
d.setDate(today.getDate() + i)
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
|
||||
let weekStr = weeks[d.getDay()]
|
||||
if (i === 0) weekStr = this.t('today');
|
||||
else if (i === 1) weekStr = this.t('tomorrow');
|
||||
|
||||
dates.push({
|
||||
date: `${d.getFullYear()}-${month}-${day}`,
|
||||
label: `${month}/${day}`,
|
||||
week: weekStr
|
||||
})
|
||||
}
|
||||
this.dateList = dates
|
||||
},
|
||||
selectDate(index) {
|
||||
this.currentDateIndex = index
|
||||
this.currentTimeIndex = -1
|
||||
},
|
||||
selectTimeSlot(index) {
|
||||
this.currentTimeIndex = index
|
||||
},
|
||||
selectDuration(item) {
|
||||
this.formData.duration = item
|
||||
},
|
||||
buildRemark(selectedDate, selectedTime) {
|
||||
const lines = [
|
||||
this.t('remarkTitle'),
|
||||
this.t('remarkTime', { date: selectedDate, time: selectedTime }),
|
||||
this.t('remarkDuration', { duration: this.formData.duration })
|
||||
]
|
||||
|
||||
if (this.formData.budget) {
|
||||
lines.push(this.t('remarkBudget', { budget: this.formData.budget }))
|
||||
}
|
||||
if (this.formData.workplace) {
|
||||
lines.push(this.t('remarkWorkplace', { workplace: this.formData.workplace }))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.areaName || this.sourceData.areaNameEn)) {
|
||||
lines.push(this.t('remarkArea', {
|
||||
area: this.language === 'en'
|
||||
? (this.sourceData.areaNameEnDisplay || this.sourceData.areaLabelEnDisplay || this.sourceData.areaNameEn || this.sourceData.areaLabelEn || this.sourceData.areaName || this.sourceData.areaLabel || '')
|
||||
: (this.sourceData.areaName || this.sourceData.areaLabel || this.sourceData.areaNameEn || this.sourceData.areaLabelEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.roomTypeName || this.sourceData.roomTypeNameEn)) {
|
||||
lines.push(this.t('remarkRoomType', {
|
||||
roomType: this.language === 'en'
|
||||
? (this.sourceData.roomTypeNameEnDisplay || this.sourceData.roomTypeNameEn || this.sourceData.roomTypeName || '')
|
||||
: (this.sourceData.roomTypeName || this.sourceData.roomTypeNameEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && (this.sourceData.roomName || this.sourceData.roomNameEn)) {
|
||||
lines.push(this.t('remarkSource', {
|
||||
room: this.language === 'en'
|
||||
? (this.sourceData.roomNameEnDisplay || this.sourceData.roomNameEn || this.sourceData.roomName || '')
|
||||
: (this.sourceData.roomName || this.sourceData.roomNameEn || '')
|
||||
}))
|
||||
}
|
||||
if (this.sourceData && this.sourceData.payMethod) {
|
||||
lines.push(this.t('remarkPayMethod', { payMethod: localizePayMethodText(this.sourceData.payMethod, this.language) }))
|
||||
}
|
||||
|
||||
lines.push(this.t('remarkTail'))
|
||||
return lines.join('\n')
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.isSubmitting) {
|
||||
return
|
||||
}
|
||||
if (this.currentTimeIndex === -1) {
|
||||
uni.showToast({ title: this.t('selectTimeRequired'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.formData.duration) {
|
||||
uni.showToast({ title: this.t('selectDurationRequired'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const selectedDate = this.dateList[this.currentDateIndex].date
|
||||
const selectedTime = this.timeSlots[this.currentTimeIndex]
|
||||
const payload = {
|
||||
intendedAreaCode: this.sourceData && this.sourceData.areaCode ? this.sourceData.areaCode : null,
|
||||
intendedRoomTypeCode: this.sourceData && this.sourceData.roomTypeCode ? this.sourceData.roomTypeCode : null,
|
||||
roomSlotHeight: 1,
|
||||
hobbiesAndInterestsTagKeys: null,
|
||||
lifestyleHabitsTagKeys: null,
|
||||
remark: this.buildRemark(selectedDate, selectedTime),
|
||||
start: buildViewingDateTime(selectedDate, selectedTime, false),
|
||||
end: buildViewingDateTime(selectedDate, selectedTime, true)
|
||||
};
|
||||
|
||||
this.isSubmitting = true
|
||||
uni.showLoading({ title: this.t('submitting'), mask: true })
|
||||
return submitApplicationForm(payload)
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
uni.showModal({
|
||||
title: this.t('submitSuccessTitle'),
|
||||
content: this.t('submitSuccessContent'),
|
||||
showCancel: false,
|
||||
confirmColor: '#01BEFF',
|
||||
success: () => {
|
||||
uni.redirectTo({
|
||||
url: '/pkgService/my_application/my_application'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('submit viewing application failed', error)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: (error && (error.message || error.msg)) || this.t('submitFailed'),
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.isSubmitting = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.book-viewing-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #1D2129;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
padding-bottom: calc(140rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 顶部信息卡片 */
|
||||
.info-card {
|
||||
margin: 30rpx;
|
||||
padding: 40rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #01BEFF 0%, #0099FF 100%);
|
||||
box-shadow: 0 16rpx 32rpx rgba(1, 190, 255, 0.2);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.info-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info-desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
letter-spacing: 1rpx;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 80rpx;
|
||||
opacity: 0.2;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
/* 通用表单卡片 */
|
||||
.form-card {
|
||||
margin: 0 30rpx 30rpx;
|
||||
padding: 40rpx 30rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #1D2129;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
/* 日期选择 */
|
||||
.date-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.date-flex {
|
||||
display: inline-flex;
|
||||
gap: 20rpx;
|
||||
padding-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.date-item {
|
||||
width: 110rpx;
|
||||
height: 130rpx;
|
||||
background: #F4F8FF;
|
||||
border-radius: 16rpx;
|
||||
flex-shrink: 0;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.date-item--active {
|
||||
background: #EAF0FF;
|
||||
border-color: #01BEFF;
|
||||
}
|
||||
|
||||
.date-week {
|
||||
font-size: 24rpx;
|
||||
color: #86909C;
|
||||
margin-bottom: 8rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.date-item--active .date-week {
|
||||
color: #01BEFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.date-day {
|
||||
font-size: 28rpx;
|
||||
color: #1D2129;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.date-item--active .date-day {
|
||||
color: #01BEFF;
|
||||
}
|
||||
|
||||
/* 时间段选择 */
|
||||
.time-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.time-slot {
|
||||
width: calc(33.33% - 14rpx);
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
text-align: center;
|
||||
background: #F4F8FF;
|
||||
border-radius: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: #4E5969;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.time-slot--active {
|
||||
background: #01BEFF;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 8rpx 16rpx rgba(1, 190, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 需求字段 */
|
||||
.field-item {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.field-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 28rpx;
|
||||
color: #1D2129;
|
||||
font-weight: bold;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 标签选择 */
|
||||
.tag-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 0 32rpx;
|
||||
height: 68rpx;
|
||||
line-height: 68rpx;
|
||||
background: #F4F8FF;
|
||||
border-radius: 34rpx;
|
||||
font-size: 26rpx;
|
||||
color: #4E5969;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tag-item--active {
|
||||
background: #EAF0FF;
|
||||
border-color: #01BEFF;
|
||||
color: #01BEFF;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.input-box {
|
||||
background: #F4F8FF;
|
||||
border-radius: 16rpx;
|
||||
height: 88rpx;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
font-size: 36rpx;
|
||||
color: #A1B0C4;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #1D2129;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.footer-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
padding: 24rpx 40rpx;
|
||||
box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: linear-gradient(135deg, #01BEFF 0%, #0099FF 100%);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
letter-spacing: 2rpx;
|
||||
box-shadow: 0 12rpx 24rpx rgba(1, 190, 255, 0.25);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
324
pkgBooking/booking_detail/booking_detail.vue
Normal file
324
pkgBooking/booking_detail/booking_detail.vue
Normal 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>
|
||||
267
pkgBooking/booking_list/booking_list.vue
Normal file
267
pkgBooking/booking_list/booking_list.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<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>
|
||||
500
pkgBooking/pay/pay.vue
Normal file
500
pkgBooking/pay/pay.vue
Normal file
@@ -0,0 +1,500 @@
|
||||
<template>
|
||||
<view class="pay-page tn-safe-area-inset-bottom">
|
||||
<tn-nav-bar :isBack="true" :bottomShadow="false" backgroundColor="#F8F9FA">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">{{ t('title') }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="pay-content tn-padding" :style="{paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '200rpx'}">
|
||||
|
||||
<!-- 支付金额区 -->
|
||||
<view class="amount-area tn-flex tn-flex-direction-column tn-flex-col-center tn-margin-top-xl tn-margin-bottom-xl">
|
||||
<view class="tn-color-gray tn-text-md">{{ t('paymentAmount') }}</view>
|
||||
<view class="tn-text-bold tn-color-black tn-margin-top-sm amount-text">
|
||||
<text class="tn-text-xl tn-margin-right-xs">$</text>500.00
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 账单详情卡片 -->
|
||||
<view class="bill-card tn-padding-lg tn-margin-bottom-xl">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom">
|
||||
<view class="tn-flex tn-flex-col-center">
|
||||
<view class="icon-bg-box tn-flex tn-flex-col-center tn-flex-row-center tn-margin-right-sm">
|
||||
<text class="tn-icon-building bill-head-icon"></text>
|
||||
</view>
|
||||
<view class="tn-color-gray tn-text-md">{{ t('billTo') }}</view>
|
||||
</view>
|
||||
<view class="tn-text-bold tn-text-md tn-color-black">{{ t('company') }}</view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom">
|
||||
<view class="tn-color-gray tn-text-md">{{ t('roomType') }}</view>
|
||||
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">
|
||||
{{ displayStyleName }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom" v-if="roomData && (roomData.roomTypeName || roomData.roomTypeNameEn)">
|
||||
<view class="tn-color-gray tn-text-md">{{ t('roomTypeNameLabel') }}</view>
|
||||
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">
|
||||
{{ displayRoomTypeName }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom">
|
||||
<view class="tn-color-gray tn-text-md">{{ t('location') }}</view>
|
||||
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">{{ displayLocationText }}</view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom" v-if="roomData && roomData.startDate">
|
||||
<view class="tn-color-gray tn-text-md">{{ t('bookingPeriod') }}</view>
|
||||
<view class="tn-text-bold tn-text-md tn-color-black clamp-text-1" style="max-width: 400rpx; text-align: right;">
|
||||
{{ roomData.startDate }} {{ t('toLabel') }} {{ roomData.endDate }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-bottom" v-if="roomData && roomData.stayNights">
|
||||
<view class="tn-color-gray tn-text-md">{{ t('stayNights') }}</view>
|
||||
<view class="tn-text-bold tn-text-md tn-color-black">{{ roomData.stayNights }} {{ t('nightUnit') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="bill-divider tn-margin-top-md tn-margin-bottom"></view>
|
||||
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin-top-md">
|
||||
<view class="tn-color-black tn-text-bold tn-text-lg">{{ t('total') }}</view>
|
||||
<view class="tn-text-bold" style="color: $brand-primary; font-size: 40rpx;">$500.00</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式选择 -->
|
||||
<view class="payment-methods">
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black tn-margin-bottom-lg">{{ t('paymentMethod') }}</view>
|
||||
|
||||
<view class="method-item tn-flex tn-flex-row-between tn-flex-col-center tn-padding tn-margin-bottom"
|
||||
:class="payMethod === 'Wechat' ? 'method-active' : ''" @click="payMethod = 'Wechat'">
|
||||
<view class="tn-flex tn-flex-col-center">
|
||||
<view class="method-icon-box tn-flex tn-flex-row-center tn-flex-col-center tn-margin-right-md" style="background-color: rgba(9, 184, 62, 0.1);">
|
||||
<text class="tn-icon-wechat-fill method-brand-icon method-brand-icon--wechat"></text>
|
||||
</view>
|
||||
<text class="tn-text-bold tn-text-md tn-color-black">Wechat Pay</text>
|
||||
</view>
|
||||
<view class="radio-box tn-flex tn-flex-col-center tn-flex-row-center" :class="payMethod === 'Wechat' ? 'radio-active' : ''">
|
||||
<text v-if="payMethod === 'Wechat'" class="tn-icon-success-circle-fill method-check-icon"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="method-item tn-flex tn-flex-row-between tn-flex-col-center tn-padding tn-margin-bottom"
|
||||
:class="payMethod === 'alipay' ? 'method-active' : ''" @click="payMethod = 'alipay'">
|
||||
<view class="tn-flex tn-flex-col-center">
|
||||
<view class="method-icon-box tn-flex tn-flex-row-center tn-flex-col-center tn-margin-right-md" style="background-color: rgba(22, 119, 255, 0.1);">
|
||||
<text class="tn-icon-alipay method-brand-icon method-brand-icon--alipay"></text>
|
||||
</view>
|
||||
<text class="tn-text-bold tn-text-md tn-color-black">AliPay</text>
|
||||
</view>
|
||||
<view class="radio-box tn-flex tn-flex-col-center tn-flex-row-center" :class="payMethod === 'alipay' ? 'radio-active' : ''">
|
||||
<text v-if="payMethod === 'alipay'" class="tn-icon-success-circle-fill method-check-icon"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="method-item tn-flex tn-flex-row-between tn-flex-col-center tn-padding tn-margin-bottom"
|
||||
:class="payMethod === 'card' ? 'method-active' : ''" @click="payMethod = 'card'">
|
||||
<view class="tn-flex tn-flex-col-center">
|
||||
<view class="method-icon-box tn-flex tn-flex-row-center tn-flex-col-center tn-margin-right-md" style="background-color: rgba(34, 34, 34, 0.1);">
|
||||
<text class="tn-icon-bankcard-fill method-brand-icon method-brand-icon--card"></text>
|
||||
</view>
|
||||
<text class="tn-text-bold tn-text-md tn-color-black">Credit Card</text>
|
||||
</view>
|
||||
<view class="radio-box tn-flex tn-flex-col-center tn-flex-row-center" :class="payMethod === 'card' ? 'radio-active' : ''">
|
||||
<text v-if="payMethod === 'card'" class="tn-icon-success-circle-fill method-check-icon"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip-panel tn-padding-lg">
|
||||
<view class="tn-text-bold tn-text-lg tn-color-black">{{ t('fallbackTitle') }}</view>
|
||||
<view class="tn-color-gray tn-text-sm tn-margin-top-sm">{{ t('fallbackDesc') }}</view>
|
||||
<view class="tip-action tn-margin-top-lg" @click="fallbackToApplication">
|
||||
<text class="tn-text-bold">{{ t('fallbackAction') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部固定支付按钮 -->
|
||||
<view class="fixed-bottom-btn-area tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="tn-flex tn-flex-direction-column">
|
||||
<view class="tn-text-sm tn-color-gray">{{ t('total') }}</view>
|
||||
<view class="tn-text-bold tn-text-xl tn-color-black">$500.00</view>
|
||||
</view>
|
||||
<view class="custom-btn btn-primary" style="width: 60%; height: 100rpx;" @click="handlePay" :class="{ 'btn-disabled': isSubmitting }">
|
||||
<text class="tn-text-bold tn-text-lg">{{ isSubmitting ? (language === 'zh' ? '支付中...' : 'Processing...') : t('payNow') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compactEnglishText } from '../../utils/select-room-normalizers'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isSubmitting: false,
|
||||
payMethod: 'Wechat',
|
||||
roomData: null,
|
||||
i18n: {
|
||||
zh: {
|
||||
title: '支付定金',
|
||||
paymentAmount: '应付定金',
|
||||
billTo: '收款方',
|
||||
company: '绿居物业管理有限公司',
|
||||
roomType: '房源风格',
|
||||
roomTypeNameLabel: '房型',
|
||||
location: '楼栋位置',
|
||||
bookingPeriod: '预订时间',
|
||||
toLabel: '至',
|
||||
stayNights: '入住晚数',
|
||||
nightUnit: '晚',
|
||||
total: '合计',
|
||||
paymentMethod: '选择支付方式',
|
||||
payNow: '确认支付并提交预订',
|
||||
fallbackTitle: '支付接口暂未接入',
|
||||
fallbackDesc: '当前版本还未接通真实定金支付。你可以先改走已接好的入住申请提交流程,后续由工作人员跟进。',
|
||||
fallbackAction: '改为提交入住申请',
|
||||
fallbackNavigateTitle: '入住申请',
|
||||
fallbackNavigateDesc: '支付接口暂未接入,先提交入住申请,由工作人员继续跟进。',
|
||||
paying: '支付中...',
|
||||
pendingTitle: '支付接口待接入',
|
||||
paymentNoticeTitle: '用户须知',
|
||||
paymentNoticeMethodLabel: '支付方式',
|
||||
paymentNoticeAmountLabel: '支付金额',
|
||||
paymentNoticeItemLabel: '支付项目',
|
||||
paymentNoticeItemValue: '房源定金',
|
||||
paymentNoticeLine1: '请确认支付方式和金额无误后再继续操作。',
|
||||
paymentNoticeLine2: '当前页面为预支付引导页,真实支付接口暂未接入。',
|
||||
paymentNoticeLine3: '确认后将进入后续提交流程,由工作人员继续跟进。',
|
||||
paymentNoticeConfirm: '继续',
|
||||
paymentNoticeCancel: '取消',
|
||||
laterAction: '我知道了'
|
||||
},
|
||||
en: {
|
||||
title: 'Deposit Payment',
|
||||
paymentAmount: 'Deposit Amount',
|
||||
billTo: 'Bill To',
|
||||
company: 'Lüju Property Management',
|
||||
roomType: 'Style',
|
||||
roomTypeNameLabel: 'Room Type',
|
||||
location: 'Location',
|
||||
bookingPeriod: 'Booking Period',
|
||||
toLabel: 'to',
|
||||
stayNights: 'Stay Nights',
|
||||
nightUnit: 'nights',
|
||||
total: 'Total',
|
||||
paymentMethod: 'Payment Method',
|
||||
payNow: 'Pay and Submit',
|
||||
fallbackTitle: 'Payment API Pending',
|
||||
fallbackDesc: 'The real deposit payment flow is not ready yet. You can submit an application first and let staff follow up.',
|
||||
fallbackAction: 'Submit Application Instead',
|
||||
fallbackNavigateTitle: 'Stay Application',
|
||||
fallbackNavigateDesc: 'The payment API is not ready yet. Submit a stay application first and let staff follow up.',
|
||||
paying: 'Processing...',
|
||||
pendingTitle: 'Payment API Pending',
|
||||
paymentNoticeTitle: 'Payment Notice',
|
||||
paymentNoticeMethodLabel: 'Payment Method',
|
||||
paymentNoticeAmountLabel: 'Amount',
|
||||
paymentNoticeItemLabel: 'Charge Item',
|
||||
paymentNoticeItemValue: 'Deposit',
|
||||
paymentNoticeLine1: 'Please confirm the payment method and amount before continuing.',
|
||||
paymentNoticeLine2: 'This page is a pre-payment guide and the real payment API is not connected yet.',
|
||||
paymentNoticeLine3: 'After confirmation, the process continues to the follow-up submission flow for staff handling.',
|
||||
paymentNoticeConfirm: 'Continue',
|
||||
paymentNoticeCancel: 'Cancel',
|
||||
laterAction: 'Later'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
},
|
||||
displayStyleName() {
|
||||
if (!this.roomData) {
|
||||
return 'Premium Suite'
|
||||
}
|
||||
return this.language === 'en'
|
||||
? (this.roomData.styleNameEnDisplay || compactEnglishText(this.roomData.styleNameEn || this.roomData.styleNameZh || '', 24) || 'Premium Suite')
|
||||
: (this.roomData.styleNameZh || this.roomData.styleNameEn || 'Premium Suite')
|
||||
},
|
||||
displayRoomTypeName() {
|
||||
if (!this.roomData) {
|
||||
return ''
|
||||
}
|
||||
return this.language === 'en'
|
||||
? (this.roomData.roomTypeNameEnDisplay || compactEnglishText(this.roomData.roomTypeNameEn || this.roomData.roomTypeName || '', 22))
|
||||
: (this.roomData.roomTypeName || this.roomData.roomTypeNameEn || '')
|
||||
},
|
||||
displayLocationText() {
|
||||
if (!this.roomData) {
|
||||
return '-'
|
||||
}
|
||||
const buildingText = this.language === 'en'
|
||||
? (this.roomData.buildingEnDisplay || compactEnglishText(this.roomData.buildingEn || this.roomData.building || '', 18))
|
||||
: (this.roomData.building || this.roomData.buildingEn || '')
|
||||
const floorText = this.language === 'en'
|
||||
? (this.roomData.floorEnDisplay || compactEnglishText(this.roomData.floorEn || this.roomData.floor || '', 12))
|
||||
: (this.roomData.floor || this.roomData.floorEn || '')
|
||||
return [buildingText, floorText].filter(Boolean).join(' - ') || '-'
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.roomData) {
|
||||
Promise.resolve(options.roomData)
|
||||
.then(decodeURIComponent)
|
||||
.then(JSON.parse)
|
||||
.then((data) => {
|
||||
this.roomData = data && typeof data === 'object' ? data : {}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Failed to parse room data in pay page', e);
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgBooking.pay.pay', key)
|
||||
},
|
||||
confirmPaymentNotice() {
|
||||
const content = [
|
||||
`${this.t('paymentNoticeItemLabel')}: ${this.t('paymentNoticeItemValue')}`,
|
||||
`${this.t('paymentNoticeMethodLabel')}: ${this.payMethod}`,
|
||||
`${this.t('paymentNoticeAmountLabel')}: $500.00`,
|
||||
'',
|
||||
`1. ${this.t('paymentNoticeLine1')}`,
|
||||
`2. ${this.t('paymentNoticeLine2')}`,
|
||||
`3. ${this.t('paymentNoticeLine3')}`
|
||||
].join('\n')
|
||||
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: this.t('paymentNoticeTitle'),
|
||||
content,
|
||||
confirmText: this.t('paymentNoticeConfirm'),
|
||||
cancelText: this.t('paymentNoticeCancel'),
|
||||
success: (res) => resolve(!!(res && res.confirm)),
|
||||
fail: () => resolve(false)
|
||||
})
|
||||
})
|
||||
},
|
||||
buildApplicationPayload() {
|
||||
return {
|
||||
sourceType: 'booking-payment-fallback',
|
||||
areaCode: this.roomData && this.roomData.areaCode ? this.roomData.areaCode : '',
|
||||
areaName: this.roomData && this.roomData.building ? this.roomData.building : '',
|
||||
areaNameEn: this.roomData && (this.roomData.areaNameEn || this.roomData.buildingEn) ? (this.roomData.areaNameEn || this.roomData.buildingEn) : '',
|
||||
areaNameEnDisplay: this.roomData && (this.roomData.areaNameEnDisplay || this.roomData.buildingEnDisplay || this.roomData.areaNameEn || this.roomData.buildingEn)
|
||||
? (this.roomData.areaNameEnDisplay || this.roomData.buildingEnDisplay || this.roomData.areaNameEn || this.roomData.buildingEn)
|
||||
: '',
|
||||
roomTypeCode: this.roomData && this.roomData.roomTypeCode ? this.roomData.roomTypeCode : '',
|
||||
roomTypeName: this.roomData && this.roomData.roomTypeName ? this.roomData.roomTypeName : '',
|
||||
roomTypeNameEn: this.roomData && (this.roomData.roomTypeNameEn || this.roomData.styleNameEn) ? (this.roomData.roomTypeNameEn || this.roomData.styleNameEn) : '',
|
||||
roomTypeNameEnDisplay: this.roomData && (this.roomData.roomTypeNameEnDisplay || this.roomData.styleNameEnDisplay || this.roomData.roomTypeNameEn || this.roomData.styleNameEn)
|
||||
? (this.roomData.roomTypeNameEnDisplay || this.roomData.styleNameEnDisplay || this.roomData.roomTypeNameEn || this.roomData.styleNameEn)
|
||||
: '',
|
||||
roomName: this.roomData && this.roomData.styleNameZh ? this.roomData.styleNameZh : '',
|
||||
roomNameEn: this.roomData && this.roomData.styleNameEn ? this.roomData.styleNameEn : '',
|
||||
roomNameEnDisplay: this.roomData && (this.roomData.styleNameEnDisplay || this.roomData.styleNameEn)
|
||||
? (this.roomData.styleNameEnDisplay || this.roomData.styleNameEn)
|
||||
: '',
|
||||
payMethod: this.payMethod,
|
||||
price: '$500.00',
|
||||
start: this.roomData && this.roomData.startDate ? this.roomData.startDate : '',
|
||||
end: this.roomData && this.roomData.endDate ? this.roomData.endDate : ''
|
||||
}
|
||||
},
|
||||
fallbackToApplication() {
|
||||
const payload = this.buildApplicationPayload()
|
||||
const titleZh = '入住申请'
|
||||
const titleEn = 'Stay Application'
|
||||
const descZh = '支付接口暂未接入,先提交入住申请,由工作人员继续跟进。'
|
||||
const descEn = 'The payment API is not ready yet. Submit a stay application first and let staff follow up.'
|
||||
uni.navigateTo({
|
||||
url: `/pkgService/submit_request/submit_request?title=${encodeURIComponent(this.t('fallbackNavigateTitle'))}&titleZh=${encodeURIComponent(titleZh)}&titleEn=${encodeURIComponent(titleEn)}&desc=${encodeURIComponent(this.t('fallbackNavigateDesc'))}&descZh=${encodeURIComponent(descZh)}&descEn=${encodeURIComponent(descEn)}&payload=${encodeURIComponent(JSON.stringify(payload))}`
|
||||
})
|
||||
},
|
||||
handlePay() {
|
||||
if (this.isSubmitting) return
|
||||
this.confirmPaymentNotice().then((confirmed) => {
|
||||
if (!confirmed) return
|
||||
this.isSubmitting = true
|
||||
uni.showLoading({ title: this.t('paying') })
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
this.isSubmitting = false
|
||||
uni.showModal({
|
||||
title: this.t('pendingTitle'),
|
||||
content: this.t('fallbackDesc'),
|
||||
confirmText: this.t('fallbackAction'),
|
||||
cancelText: this.t('laterAction'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.fallbackToApplication()
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 1500)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pay-page {
|
||||
background-color: #F8F9FA;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.amount-area {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.amount-text {
|
||||
font-size: 88rpx;
|
||||
text-shadow: 0 10rpx 30rpx rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.bill-card {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.03);
|
||||
}
|
||||
.icon-bg-box {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #F8F9FA;
|
||||
}
|
||||
|
||||
.bill-head-icon {
|
||||
color: #222222;
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.bill-divider {
|
||||
height: 2rpx;
|
||||
background-color: transparent;
|
||||
border-bottom: 2rpx dashed #E6E6E6;
|
||||
margin-left: -10rpx;
|
||||
margin-right: -10rpx;
|
||||
}
|
||||
|
||||
.payment-methods {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
.method-item {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
border: 4rpx solid transparent;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.02);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.method-active {
|
||||
border-color: $brand-primary;
|
||||
background-color: #FAFFEB;
|
||||
box-shadow: 0 12rpx 30rpx rgba($brand-primary-rgb, 0.1);
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
.method-icon-box {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.method-brand-icon {
|
||||
font-size: 38rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.method-brand-icon--wechat {
|
||||
color: #09b83e;
|
||||
}
|
||||
|
||||
.method-brand-icon--alipay {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.method-brand-icon--card {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.radio-box {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 22rpx;
|
||||
border: 4rpx solid #E6E6E6;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.method-check-icon {
|
||||
color: #222222;
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.radio-active {
|
||||
border-color: $brand-primary;
|
||||
background-color: $brand-primary;
|
||||
}
|
||||
|
||||
/* 底部固定按钮区 */
|
||||
.fixed-bottom-btn-area {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 -10rpx 40rpx rgba(0,0,0,0.04);
|
||||
padding: 30rpx 40rpx calc(30rpx + env(safe-area-inset-bottom));
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* 自定义按钮样式 */
|
||||
.custom-btn {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 100rpx;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 10rpx 30rpx rgba($brand-primary-rgb, 0.3);
|
||||
}
|
||||
.custom-btn:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 5rpx 15rpx rgba($brand-primary-rgb, 0.2);
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: $brand-primary;
|
||||
color: #000000;
|
||||
}
|
||||
.btn-disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
.clamp-text-1 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tip-panel {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.tip-action {
|
||||
height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000000;
|
||||
background-color: rgba($brand-primary-rgb, 0.18);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user