918 lines
33 KiB
Vue
918 lines
33 KiB
Vue
<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">{{ $tr('预订确认') }}</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">{{ $tr('你正在预约') }}</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">{{ $tr('定金') }}</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">{{ $tr('填写预订时间') }}</view>
|
||
<view class="tn-color-gray tn-text-sm tn-margin-bottom-lg">{{ $tr('支付后会先生成预定单并进入审核,不会直接占用房间资源。') }}</view>
|
||
|
||
<view class="form-item tn-margin-bottom-lg">
|
||
<view class="form-label tn-text-bold">{{ $tr('入住日期 (Check-in)') }}</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 : $tr('请选择日期') }}
|
||
</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">{{ $tr('离开日期 (Check-out)') }}</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 : $tr('请选择日期') }}
|
||
</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">{{ $tr('入住晚数') }}</text>
|
||
<text class="tn-text-bold tn-color-black">{{ stayNights }} {{ $tr('晚') }}</text>
|
||
</view>
|
||
<view class="summary-row tn-flex tn-flex-row-between tn-margin-top-sm">
|
||
<text class="tn-color-gray">{{ $tr('当前状态') }}</text>
|
||
<text class="tn-text-bold tn-color-black">{{ $tr('待支付定金') }}</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">{{ $tr('定金') }}</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...') : $tr('提交并支付') }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<tn-popup v-model="showDepositNotice" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true" @close="closeDepositNotice">
|
||
<view class="deposit-notice-popup">
|
||
<view class="popup-header">
|
||
<view class="popup-title">{{ $tr('租房定金须知') }}</view>
|
||
<view v-if="pendingDepositPaymentContext && pendingDepositPaymentContext.paymentNo" class="popup-subtitle">
|
||
{{ $tr('支付号') }}:{{ pendingDepositPaymentContext.paymentNo }}
|
||
</view>
|
||
</view>
|
||
<scroll-view scroll-y class="popup-body" @scrolltolower="onNoticeScrollToBottom">
|
||
<view class="notice-content">
|
||
<view class="notice-item">
|
||
<view class="notice-item-zh">1、请在 5 分钟内支付定金或者款项,否则系统将自动取消这个订单,释放锁定的房源。</view>
|
||
<view class="notice-item-en">Please pay the deposit or full payment within 5 minutes. Otherwise, the system will automatically cancel this order and release the reserved housing unit.</view>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-item-zh">2、缴纳线上定金可锁定房源并预约专属看房,平台收取定金后该房源临时下架,不再接待其他客户看房。</view>
|
||
<view class="notice-item-en">Payment of the online deposit reserves the housing unit and arranges exclusive on-site viewing for you. After receiving the deposit, we will temporarily remove the property from the rental list and stop arranging viewings for other clients.</view>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-item-zh">3、请于约定时间完成实地看房,并在看房结束后 3 个工作日内签署租赁合同;若逾期未看房或未完成签约,视作您主动放弃房源,定金不予退还。</view>
|
||
<view class="notice-item-en">You shall complete the on-site viewing at the agreed time and sign the rental contract within 3 working days after the viewing. Failure to complete the viewing or sign the contract within the time limit shall be deemed your voluntary waiver of the property, and the deposit will not be refunded.</view>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-item-zh">4、如因租客个人原因取消看房、看房后不愿承租房屋或拒绝签署租赁合同,定金不予退还。</view>
|
||
<view class="notice-item-en">The deposit is non-refundable if you cancel the viewing, refuse to rent the property or decline to sign the contract due to personal reasons.</view>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-item-zh">5、若因我方房源信息虚假、房屋无法正常交付等我方过错,造成无法看房或签约,定金将全额按原支付路径退还。</view>
|
||
<view class="notice-item-en">If the viewing or contract signing cannot be conducted due to our liabilities such as false property information or failure to deliver the housing normally, the full deposit will be refunded via your original payment channel.</view>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-item-zh">6、待双方正式签署租赁合同后,该定金可直接抵扣首期租金。</view>
|
||
<view class="notice-item-en">After both parties formally sign the rental contract, this deposit may be directly deducted from your initial rent.</view>
|
||
</view>
|
||
<view class="notice-item">
|
||
<view class="notice-item-zh">7、您支付定金,即代表已完整知晓房源全部信息,并自愿接受本须知全部条款约束。</view>
|
||
<view class="notice-item-en">By making the deposit payment, you confirm that you have fully reviewed all property information and voluntarily accept all terms and conditions set forth in this notice.</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view class="popup-footer">
|
||
<view
|
||
class="notice-action-btn"
|
||
:class="{ 'notice-action-btn--disabled': !hasReadNotice }"
|
||
@click="confirmAndPayDeposit"
|
||
>
|
||
<text class="tn-text-bold">{{ $tr('同意并支付') }}</text>
|
||
<text v-if="!hasReadNotice" class="tn-margin-left-xs tn-text-sm">({{ $tr('请滑动阅读至底部') }})</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</tn-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { accountQueryBill, accountQueryBills, applicationFormDetail, paymentBillPay, paymentPayingBills, submitApplicationForm } from '../../api/index.js'
|
||
import { saveActiveApplicationCode, saveHomePageTabId } from '../../utils/app-state'
|
||
import { compactEnglishText } from '../../utils/select-room-normalizers'
|
||
|
||
function isPlainObject(value) {
|
||
return !!value && typeof value === 'object' && !Array.isArray(value)
|
||
}
|
||
|
||
function normalizeCodeValue(value) {
|
||
if (value === undefined || value === null) {
|
||
return ''
|
||
}
|
||
if (typeof value === 'string' || typeof value === 'number') {
|
||
return String(value).trim()
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function extractSubmitApplicationCode(response) {
|
||
const payload = response && response.data !== undefined ? response.data : response
|
||
const directValues = [
|
||
payload,
|
||
payload && payload.data,
|
||
payload && payload.result,
|
||
payload && payload.record,
|
||
payload && payload.application
|
||
]
|
||
|
||
for (let i = 0; i < directValues.length; i += 1) {
|
||
const directCode = normalizeCodeValue(directValues[i])
|
||
if (directCode) {
|
||
return directCode
|
||
}
|
||
}
|
||
|
||
const sources = directValues.filter(isPlainObject)
|
||
const codeFields = ['detailCode', 'code', 'applicationCode', 'formCode', 'referenceTypeNo', 'no']
|
||
for (let i = 0; i < sources.length; i += 1) {
|
||
const current = sources[i]
|
||
for (let j = 0; j < codeFields.length; j += 1) {
|
||
const nextCode = normalizeCodeValue(current[codeFields[j]])
|
||
if (nextCode) {
|
||
return nextCode
|
||
}
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function parseJsonStringMaybe(value) {
|
||
if (typeof value !== 'string') {
|
||
return value
|
||
}
|
||
const text = value.trim()
|
||
if (!text) {
|
||
return value
|
||
}
|
||
if (!((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')))) {
|
||
return value
|
||
}
|
||
try {
|
||
return JSON.parse(text)
|
||
} catch (error) {
|
||
return value
|
||
}
|
||
}
|
||
|
||
function parseExpireTimeToTimestamp(value) {
|
||
if (value === null || value === undefined || value === '') {
|
||
return 0
|
||
}
|
||
if (typeof value === 'number') {
|
||
return Number.isFinite(value) ? value : 0
|
||
}
|
||
const text = String(value).trim()
|
||
if (!text) {
|
||
return 0
|
||
}
|
||
if (/^\d{13,}$/.test(text)) {
|
||
return Number(text)
|
||
}
|
||
if (/^\d{10}$/.test(text)) {
|
||
return Number(text) * 1000
|
||
}
|
||
const timestamp = new Date(text).getTime()
|
||
return Number.isFinite(timestamp) ? timestamp : 0
|
||
}
|
||
|
||
function formatDateTime(date = '', endOfDay = false) {
|
||
if (!date) {
|
||
return null
|
||
}
|
||
return `${date}T${endOfDay ? '23:59:59' : '00:00:00'}`
|
||
}
|
||
|
||
function parseResponseObject(response) {
|
||
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
|
||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||
return payload
|
||
}
|
||
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
||
return payload.data
|
||
}
|
||
return {}
|
||
}
|
||
|
||
function parseResponseList(response) {
|
||
const payload = response && response.data ? response.data : response
|
||
if (Array.isArray(payload)) {
|
||
return payload.filter(item => item && typeof item === 'object')
|
||
}
|
||
if (payload && Array.isArray(payload.data)) {
|
||
return payload.data.filter(item => item && typeof item === 'object')
|
||
}
|
||
return []
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
isSubmitting: false,
|
||
applyData: null,
|
||
showDepositNotice: false,
|
||
hasReadNotice: false,
|
||
isClosingDepositNoticeForPayment: false,
|
||
depositNoticeReadyAt: 0,
|
||
pendingDepositPaymentContext: null,
|
||
form: {
|
||
startDate: '',
|
||
endDate: ''
|
||
}
|
||
}
|
||
},
|
||
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.$tr('房型:')}${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: {
|
||
onStartDateChange(e) {
|
||
this.form.startDate = e.detail.value;
|
||
},
|
||
onEndDateChange(e) {
|
||
this.form.endDate = e.detail.value;
|
||
},
|
||
goStayServiceFlow() {
|
||
saveHomePageTabId('stay-service')
|
||
uni.reLaunch({
|
||
url: '/pages/index/index?pageId=stay-service'
|
||
})
|
||
},
|
||
buildSubmitPayload() {
|
||
return {
|
||
intendedAreaCode: this.applyData && this.applyData.areaCode ? this.applyData.areaCode : null,
|
||
intendedRoomTypeCode: this.applyData && this.applyData.roomTypeCode ? this.applyData.roomTypeCode : null,
|
||
roomSlotHeight: 1,
|
||
hobbiesAndInterestsTagKeys: null,
|
||
lifestyleHabitsTagKeys: null,
|
||
remark: null,
|
||
start: formatDateTime(this.form.startDate, false),
|
||
end: formatDateTime(this.form.endDate, true)
|
||
}
|
||
},
|
||
normalizeApplicationDetail(response) {
|
||
return parseResponseObject(response)
|
||
},
|
||
normalizeBillDetail(response) {
|
||
return parseResponseObject(response)
|
||
},
|
||
normalizeBillList(response) {
|
||
return parseResponseList(response)
|
||
},
|
||
normalizePayingBills(response) {
|
||
return parseResponseList(response)
|
||
},
|
||
getBillNo(bill = {}) {
|
||
return String(bill.no || bill.billNo || '').trim()
|
||
},
|
||
getBillPayRequests(bill = {}) {
|
||
return (Array.isArray(bill.payRequests) ? bill.payRequests : [])
|
||
.filter(item => item && typeof item === 'object')
|
||
.sort((prev, next) => new Date(next.createTime || 0).getTime() - new Date(prev.createTime || 0).getTime())
|
||
},
|
||
buildPendingPaymentOrderFromBillDetail(bill = {}) {
|
||
const payRequest = this.getBillPayRequests(bill).find(item => !item.paidTime)
|
||
if (!payRequest) {
|
||
return null
|
||
}
|
||
return {
|
||
amount: Number(payRequest.amount || bill.remainingAmount || bill.totalCharge || 0),
|
||
expireTime: payRequest.expireTime || '',
|
||
payRequestNo: payRequest.no || '',
|
||
paymentNo: payRequest.no || '',
|
||
cashier: {
|
||
paymentInfo: payRequest.paymentInfo || '',
|
||
expireTime: payRequest.expireTime || ''
|
||
}
|
||
}
|
||
},
|
||
getPendingPaymentOrderNo(order = {}) {
|
||
const cashier = order && order.cashier ? order.cashier : {}
|
||
return String(
|
||
order.payRequestNo
|
||
|| order.payNo
|
||
|| order.paymentNo
|
||
|| cashier.payRequestNo
|
||
|| cashier.payNo
|
||
|| cashier.paymentNo
|
||
|| ''
|
||
).trim()
|
||
},
|
||
parsePaymentInfo(paymentInfo) {
|
||
if (!paymentInfo) {
|
||
return {}
|
||
}
|
||
if (typeof paymentInfo === 'object') {
|
||
return paymentInfo
|
||
}
|
||
const text = String(paymentInfo).trim()
|
||
if (!text) {
|
||
return {}
|
||
}
|
||
try {
|
||
return JSON.parse(text)
|
||
} catch (error) {
|
||
return {}
|
||
}
|
||
},
|
||
normalizeWechatPayOptions(paymentInfo) {
|
||
const source = this.parsePaymentInfo(paymentInfo)
|
||
return {
|
||
provider: 'wxpay',
|
||
timeStamp: String(source.timeStamp || source.timestamp || source.time_stamp || ''),
|
||
nonceStr: source.nonceStr || source.noncestr || source.nonce_str || '',
|
||
package: source.package || source.packageValue || source.package_value || '',
|
||
signType: source.signType || source.sign_type || 'MD5',
|
||
paySign: source.paySign || source.paysign || source.pay_sign || ''
|
||
}
|
||
},
|
||
requestWechatPayment(paymentInfo) {
|
||
const payOptions = this.normalizeWechatPayOptions(paymentInfo)
|
||
if (!payOptions.timeStamp || !payOptions.nonceStr || !payOptions.package || !payOptions.paySign) {
|
||
return Promise.reject(new Error(this.$tr('获取支付参数失败')))
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
uni.requestPayment({
|
||
...payOptions,
|
||
success: resolve,
|
||
fail: reject
|
||
})
|
||
})
|
||
},
|
||
getWechatLoginCode() {
|
||
return new Promise((resolve, reject) => {
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: (res) => {
|
||
if (!res || !res.code) {
|
||
reject(new Error(this.$tr('获取微信登录凭证失败')))
|
||
return
|
||
}
|
||
resolve(res.code)
|
||
},
|
||
fail: reject
|
||
})
|
||
})
|
||
},
|
||
async getPendingPaymentOrder(billNo = '') {
|
||
const normalizedBillNo = String(billNo || '').trim()
|
||
if (!normalizedBillNo) {
|
||
return null
|
||
}
|
||
const response = await paymentPayingBills({
|
||
BillNo: normalizedBillNo
|
||
}, this.language)
|
||
const orderList = this.normalizePayingBills(response)
|
||
return orderList.length ? orderList[0] : null
|
||
},
|
||
async resolveApplicationDetailPaymentContext(referenceTypeNo = '') {
|
||
const normalizedReferenceTypeNo = String(referenceTypeNo || '').trim()
|
||
if (!normalizedReferenceTypeNo) {
|
||
return null
|
||
}
|
||
const detail = this.normalizeApplicationDetail(await applicationFormDetail(normalizedReferenceTypeNo, this.language))
|
||
if (!detail || !Object.keys(detail).length) {
|
||
return null
|
||
}
|
||
return {
|
||
referenceTypeNo: String(detail.detailCode || normalizedReferenceTypeNo).trim(),
|
||
billNo: String(detail.billNo || detail.BillNo || '').trim(),
|
||
paymentNo: String(detail.paymentNo || detail.payNo || detail.payRequestNo || '').trim(),
|
||
detail
|
||
}
|
||
},
|
||
async resolvePaymentPreview(referenceTypeNo = '') {
|
||
const context = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
|
||
const billNo = context && context.billNo ? context.billNo : ''
|
||
const pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
|
||
return {
|
||
referenceTypeNo: context && context.referenceTypeNo ? context.referenceTypeNo : String(referenceTypeNo || '').trim(),
|
||
billNo,
|
||
paymentNo: this.getPendingPaymentOrderNo(pendingPaymentOrder) || (context && context.paymentNo ? context.paymentNo : ''),
|
||
pendingPaymentOrder
|
||
}
|
||
},
|
||
async payByPaymentContext(context = {}) {
|
||
let referenceTypeNo = String(context.referenceTypeNo || '').trim()
|
||
let billNo = String(context.billNo || '').trim()
|
||
const description = String(context.description || '').trim() || this.$tr('支付定金')
|
||
|
||
if (!referenceTypeNo && !billNo) {
|
||
throw new Error(this.$tr('申请单编号缺失'))
|
||
}
|
||
|
||
if (referenceTypeNo) {
|
||
saveActiveApplicationCode(referenceTypeNo)
|
||
}
|
||
|
||
uni.showLoading({ title: this.$tr('正在获取账单...') })
|
||
let bill = null
|
||
let pendingPaymentOrder = context.pendingPaymentOrder || null
|
||
|
||
if (referenceTypeNo && !billNo) {
|
||
const detailPaymentContext = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
|
||
if (detailPaymentContext) {
|
||
referenceTypeNo = detailPaymentContext.referenceTypeNo || referenceTypeNo
|
||
billNo = detailPaymentContext.billNo || billNo
|
||
}
|
||
}
|
||
|
||
if (!pendingPaymentOrder && billNo) {
|
||
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
|
||
}
|
||
|
||
if (billNo) {
|
||
const billDetail = this.normalizeBillDetail(await accountQueryBill({ billNo }, this.language))
|
||
if (billDetail && Object.keys(billDetail).length) {
|
||
bill = billDetail
|
||
billNo = this.getBillNo(bill) || billNo
|
||
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
|
||
if (!pendingPaymentOrder) {
|
||
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (referenceTypeNo) {
|
||
const billRes = await accountQueryBills({
|
||
referenceTypeNo,
|
||
status: 10,
|
||
paymentStatus: 0,
|
||
ProviderType: 'WechatMiniProgram'
|
||
}, this.language)
|
||
const billList = this.normalizeBillList(billRes)
|
||
if (Array.isArray(billList) && billList.length) {
|
||
bill = billNo
|
||
? (billList.find(item => String(this.getBillNo(item) || '').trim() === billNo) || null)
|
||
: null
|
||
if (!bill) {
|
||
bill = billList[0]
|
||
}
|
||
billNo = this.getBillNo(bill)
|
||
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
|
||
if (!pendingPaymentOrder && billNo) {
|
||
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
|
||
}
|
||
if (!pendingPaymentOrder) {
|
||
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!billNo) {
|
||
throw new Error(this.$tr('暂无待支付定金账单'))
|
||
}
|
||
|
||
uni.showLoading({ title: this.$tr('正在拉起支付...') })
|
||
const expireTimestamp = parseExpireTimeToTimestamp(
|
||
pendingPaymentOrder && (pendingPaymentOrder.expireTime || (pendingPaymentOrder.cashier && pendingPaymentOrder.cashier.expireTime))
|
||
)
|
||
const reusablePendingPaymentOrder = pendingPaymentOrder && (!expireTimestamp || expireTimestamp > Date.now())
|
||
? pendingPaymentOrder
|
||
: null
|
||
|
||
if (reusablePendingPaymentOrder && reusablePendingPaymentOrder.cashier && reusablePendingPaymentOrder.cashier.paymentInfo) {
|
||
await this.requestWechatPayment(reusablePendingPaymentOrder.cashier.paymentInfo)
|
||
return
|
||
}
|
||
|
||
if (!referenceTypeNo || !bill) {
|
||
throw new Error(this.$tr('获取支付参数失败'))
|
||
}
|
||
|
||
const wechatCode = await this.getWechatLoginCode()
|
||
const payRes = await paymentBillPay({
|
||
referenceTypeNo,
|
||
billNo,
|
||
payMethod: 'Wechat',
|
||
amount: Number((bill && (bill.remainingAmount || bill.amount)) || 0),
|
||
description: (bill && (bill.billName || bill.name)) || description,
|
||
ExtraInfo: wechatCode
|
||
}, this.language)
|
||
const payload = payRes && payRes.data ? payRes.data : payRes
|
||
const cashier = payload && payload.cashier ? payload.cashier : {}
|
||
await this.requestWechatPayment(cashier.paymentInfo)
|
||
},
|
||
openDepositNotice(paymentContext = {}) {
|
||
this.pendingDepositPaymentContext = {
|
||
referenceTypeNo: String(paymentContext.referenceTypeNo || '').trim(),
|
||
billNo: String(paymentContext.billNo || '').trim(),
|
||
paymentNo: String(paymentContext.paymentNo || '').trim(),
|
||
pendingPaymentOrder: paymentContext.pendingPaymentOrder || null
|
||
}
|
||
this.hasReadNotice = false
|
||
this.depositNoticeReadyAt = Date.now() + 600
|
||
setTimeout(() => {
|
||
this.showDepositNotice = true
|
||
}, 80)
|
||
},
|
||
closeDepositNotice() {
|
||
if (this.isClosingDepositNoticeForPayment) {
|
||
this.isClosingDepositNoticeForPayment = false
|
||
return
|
||
}
|
||
this.showDepositNotice = false
|
||
this.hasReadNotice = false
|
||
this.depositNoticeReadyAt = 0
|
||
this.pendingDepositPaymentContext = null
|
||
},
|
||
onNoticeScrollToBottom() {
|
||
this.hasReadNotice = true
|
||
},
|
||
async confirmAndPayDeposit() {
|
||
if (!this.hasReadNotice) {
|
||
return
|
||
}
|
||
if (Date.now() < this.depositNoticeReadyAt) {
|
||
return
|
||
}
|
||
const paymentContext = this.pendingDepositPaymentContext || {}
|
||
this.isClosingDepositNoticeForPayment = true
|
||
this.showDepositNotice = false
|
||
try {
|
||
await this.payByPaymentContext({
|
||
referenceTypeNo: paymentContext.referenceTypeNo,
|
||
billNo: paymentContext.billNo,
|
||
pendingPaymentOrder: paymentContext.pendingPaymentOrder,
|
||
description: this.$tr('支付定金')
|
||
})
|
||
uni.showToast({ title: this.$tr('支付成功'), icon: 'success' })
|
||
} catch (error) {
|
||
const errMsg = error && (error.errMsg || error.message || error.msg)
|
||
? (error.errMsg || error.message || error.msg)
|
||
: this.$tr('支付失败')
|
||
if (/cancel/i.test(errMsg)) {
|
||
uni.showToast({ title: this.$tr('已取消支付'), icon: 'none' })
|
||
} else {
|
||
uni.showToast({ title: errMsg, icon: 'none' })
|
||
}
|
||
} finally {
|
||
uni.hideLoading()
|
||
this.hasReadNotice = false
|
||
this.isClosingDepositNoticeForPayment = false
|
||
this.depositNoticeReadyAt = 0
|
||
this.pendingDepositPaymentContext = null
|
||
setTimeout(() => {
|
||
this.goStayServiceFlow()
|
||
}, 300)
|
||
}
|
||
},
|
||
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;
|
||
uni.showLoading({ title: this.language === 'zh' ? '提交中...' : 'Submitting...' });
|
||
|
||
const payload = this.buildSubmitPayload()
|
||
return submitApplicationForm(payload, this.language)
|
||
.then(async (response) => {
|
||
const applicationCode = extractSubmitApplicationCode(response)
|
||
if (!applicationCode) {
|
||
throw new Error(this.$tr('申请单编号缺失'))
|
||
}
|
||
saveActiveApplicationCode(applicationCode)
|
||
const paymentContext = await this.resolvePaymentPreview(applicationCode).catch(() => ({
|
||
referenceTypeNo: applicationCode,
|
||
billNo: '',
|
||
paymentNo: '',
|
||
pendingPaymentOrder: null
|
||
}))
|
||
uni.hideLoading()
|
||
this.openDepositNotice({
|
||
...paymentContext,
|
||
referenceTypeNo: paymentContext.referenceTypeNo || applicationCode
|
||
})
|
||
})
|
||
.catch((error) => {
|
||
console.error('submit booking application failed', error)
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: (error && (error.message || error.msg)) || this.$tr('申请提交失败'),
|
||
icon: 'none'
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.isSubmitting = false;
|
||
});
|
||
}
|
||
}
|
||
}
|
||
</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;
|
||
}
|
||
|
||
.deposit-notice-popup {
|
||
background: #FFFFFF;
|
||
border-radius: 32rpx 32rpx 0 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 80vh;
|
||
}
|
||
|
||
.popup-header {
|
||
padding: 32rpx;
|
||
text-align: center;
|
||
border-bottom: 1px solid #F1F5F9;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #1A2233;
|
||
}
|
||
|
||
.popup-subtitle {
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
.popup-body {
|
||
flex: 1;
|
||
padding: 32rpx;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.notice-content {
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.notice-item {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.notice-item-zh {
|
||
font-size: 28rpx;
|
||
color: #334155;
|
||
line-height: 1.6;
|
||
margin-bottom: 8rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.notice-item-en {
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.popup-footer {
|
||
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
||
border-top: 1px solid #F1F5F9;
|
||
background: #FFFFFF;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.notice-action-btn {
|
||
min-width: 420rpx;
|
||
max-width: 100%;
|
||
height: 92rpx;
|
||
padding: 0 40rpx;
|
||
border-radius: 999rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
background: linear-gradient(135deg, $brand-primary 0%, #0099CC 100%);
|
||
color: #ffffff;
|
||
box-shadow: 0 16rpx 34rpx rgba($brand-primary, 0.24);
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.notice-action-btn:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.notice-action-btn--disabled {
|
||
opacity: 0.6;
|
||
background: #94A3B8;
|
||
box-shadow: none;
|
||
}
|
||
</style>
|