1240 lines
37 KiB
Vue
1240 lines
37 KiB
Vue
<template>
|
||
<view class="application-detail-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">{{ $tr('申请详情') }}</view>
|
||
</tn-nav-bar>
|
||
|
||
<view class="page-main tn-padding" :style="{ paddingTop: vuex_custom_bar_height + 'px', paddingBottom: '48rpx' }">
|
||
<view class="hero-panel tn-margin-bottom-lg" :class="heroThemeClass">
|
||
<view class="hero-top-row">
|
||
<view class="hero-badge" :class="heroBadgeClass">{{ applicationTypeText }}</view>
|
||
<view class="hero-status" :class="['hero-status--' + statusType]">{{ statusText }}</view>
|
||
</view>
|
||
<view class="hero-title tn-margin-top">{{ displayTitle }}</view>
|
||
<view class="hero-meta-row tn-margin-top">
|
||
<view class="hero-meta-chip">
|
||
<text class="tn-icon-time tn-margin-right-xs"></text>
|
||
<text>{{ formatDateTime(detail.createTime) }}</text>
|
||
</view>
|
||
<view class="hero-meta-chip">
|
||
<text class="tn-icon-location tn-margin-right-xs"></text>
|
||
<text>{{ detail.intendedAreaName || '--' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-if="showPaymentStatusPanel"
|
||
class="payment-status-panel tn-margin-bottom-lg"
|
||
:class="`payment-status-panel--${paymentStatusType}`"
|
||
>
|
||
<view class="payment-status-panel__label">{{ $tr('支付状态') }}</view>
|
||
<view class="payment-status-panel__main">
|
||
<view class="payment-status-panel__value">{{ paymentStatusText }}</view>
|
||
<view v-if="canViewBill" class="payment-status-panel__hint">{{ $tr('请尽快完成定金支付') }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-if="statusReminderText"
|
||
class="status-reminder-panel tn-margin-bottom-lg"
|
||
:class="{
|
||
'status-reminder-panel--warning': shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail),
|
||
'status-reminder-panel--info': isWaitingRoomAssignment(detail)
|
||
}"
|
||
>
|
||
<view class="status-reminder-panel__icon">
|
||
<text :class="shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
|
||
</view>
|
||
<view class="status-reminder-panel__content">
|
||
<view class="status-reminder-panel__title">{{ $tr('当前进度提醒') }}</view>
|
||
<view class="status-reminder-panel__text">{{ statusReminderText }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="content-panel tn-margin-bottom-lg">
|
||
<view class="panel-title">{{ $tr('基础信息') }}</view>
|
||
<view class="meta-row tn-margin-top">
|
||
<text class="meta-label">{{ $tr('申请编号') }}</text>
|
||
<view class="meta-action-wrap">
|
||
<text class="meta-value">{{ detail.detailCode || '--' }}</text>
|
||
<view v-if="detail.detailCode" class="copy-btn" @click="copyCode">
|
||
<text class="tn-text-xs tn-text-bold">{{ $tr('复制') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ $tr('申请类型') }}</text>
|
||
<text class="meta-value">{{ applicationTypeText }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ $tr('当前状态') }}</text>
|
||
<text class="meta-value">{{ statusText }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ $tr('申请人') }}</text>
|
||
<text class="meta-value">{{ detail.userName || detail.phone || '--' }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ $tr('所属公寓') }}</text>
|
||
<text class="meta-value">{{ detail.intendedAreaName || '--' }}</text>
|
||
</view>
|
||
<view class="meta-row" v-if="detail.intendedRoomTypeName">
|
||
<text class="meta-label">{{ isViewingApplication ? $tr('意向房型') : $tr('申请房型') }}</text>
|
||
<text class="meta-value">{{ detail.intendedRoomTypeName }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ isViewingApplication ? $tr('预约时间') : $tr('预计租期') }}</text>
|
||
<text class="meta-value">{{ displayDateRange }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ $tr('提交时间') }}</text>
|
||
<text class="meta-value">{{ formatDateTime(detail.createTime) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="content-panel tn-margin-bottom-lg" v-if="viewingMeta.length">
|
||
<view class="panel-title">{{ $tr('带看信息') }}</view>
|
||
<view
|
||
v-for="item in viewingMeta"
|
||
:key="item.label"
|
||
class="meta-row tn-margin-top"
|
||
>
|
||
<text class="meta-label">{{ item.label }}</text>
|
||
<text class="meta-value">{{ item.value }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="content-panel tn-margin-bottom-lg">
|
||
<view class="panel-title">{{ isViewingApplication ? $tr('申请说明') : $tr('补充说明') }}</view>
|
||
<view class="remark-panel tn-margin-top">
|
||
<view
|
||
v-for="(line, index) in remarkLines"
|
||
:key="`${line}-${index}`"
|
||
class="remark-line"
|
||
>
|
||
{{ line }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="action-row">
|
||
<view class="ghost-btn" @click="goBackList">
|
||
<text class="tn-text-bold">{{ $tr('返回列表') }}</text>
|
||
</view>
|
||
<view
|
||
v-if="canCancelOrder"
|
||
class="danger-btn"
|
||
:class="{ 'danger-btn--loading': canceling }"
|
||
@click.stop="handleCancelOrder"
|
||
>
|
||
<text class="tn-text-bold">{{ canceling ? $tr('取消中...') : $tr('取消申请') }}</text>
|
||
</view>
|
||
<view
|
||
v-if="currentStepActionText"
|
||
class="action-btn"
|
||
@click.stop="handleCurrentStepAction"
|
||
>
|
||
<text class="tn-text-bold">{{ currentStepActionText }}</text>
|
||
</view>
|
||
<view
|
||
v-else-if="isReservationCheckinProcessing()"
|
||
class="action-btn action-btn--ghost"
|
||
@click.stop="refreshCurrentPage"
|
||
>
|
||
<text class="tn-text-bold">{{ $tr('刷新') }}</text>
|
||
</view>
|
||
<view
|
||
v-else-if="canViewBill"
|
||
class="action-btn"
|
||
@click.stop="goToBillPage"
|
||
>
|
||
<text class="tn-text-bold">{{ $tr('查看关联账单') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { accountQueryBills, contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
|
||
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
||
import {
|
||
RESERVATION_FLOW_STEP,
|
||
attachReservationFlow,
|
||
buildReservationFlowMap,
|
||
getReservationCurrentActionText,
|
||
getReservationCurrentStepText,
|
||
getReservationOrderNo,
|
||
parseReservationFlowResponse
|
||
} from '@/utils/reservation-flow'
|
||
|
||
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间[::]|意向房型[::]|意向区域[::]|viewing request|viewing time[::]|preferred room type[::]|preferred area[::]/i
|
||
|
||
function isViewingApplicationRecord(item = {}) {
|
||
if (!item || typeof item !== 'object') {
|
||
return false
|
||
}
|
||
|
||
const typeText = [
|
||
item.businessType,
|
||
item.applicationType,
|
||
item.applicationTypeDesc,
|
||
item.formType,
|
||
item.bizType,
|
||
item.type,
|
||
item.typeDesc
|
||
]
|
||
.filter(value => value !== null && typeof value !== 'undefined' && String(value).trim())
|
||
.join(' ')
|
||
|
||
if (/viewing|看房|带看/i.test(typeText)) {
|
||
return true
|
||
}
|
||
|
||
if ([item.appointmentViewingTime, item.viewingTime, item.bookViewingTime].some(value => String(value || '').trim())) {
|
||
return true
|
||
}
|
||
|
||
const remark = item && item.remark ? String(item.remark) : ''
|
||
return VIEWING_APPLICATION_PATTERN.test(remark)
|
||
}
|
||
|
||
function normalizeApplicationList(list = []) {
|
||
if (!Array.isArray(list)) {
|
||
return []
|
||
}
|
||
return list
|
||
.filter(item => item && typeof item === 'object')
|
||
.map((item) => {
|
||
return {
|
||
...item,
|
||
isViewingApplication: isViewingApplicationRecord(item)
|
||
}
|
||
})
|
||
}
|
||
|
||
function resolveApplicationPaymentStatusInfo(item = {}) {
|
||
if (!item || item.isViewingApplication) {
|
||
return {
|
||
key: 'exempt',
|
||
desc: ''
|
||
}
|
||
}
|
||
|
||
const desc = String(item.payStatusDesc || item.paymentStatusDesc || '').trim()
|
||
const status = Number(
|
||
item.payStatus !== undefined && item.payStatus !== null
|
||
? item.payStatus
|
||
: item.paymentStatus
|
||
)
|
||
|
||
if (status === 10 || /已支付|paid/i.test(desc)) {
|
||
return { key: 'paid', desc }
|
||
}
|
||
if (/退款|refunded?/i.test(desc)) {
|
||
return { key: 'refunded', desc }
|
||
}
|
||
if (/失败|failed?/i.test(desc)) {
|
||
return { key: 'failed', desc }
|
||
}
|
||
if (/处理中|processing/i.test(desc)) {
|
||
return { key: 'processing', desc }
|
||
}
|
||
if (status === 0 || /待支付|未支付|unpaid|pending/i.test(desc)) {
|
||
return { key: 'pending', desc }
|
||
}
|
||
|
||
return {
|
||
key: desc ? 'processing' : 'pending',
|
||
desc
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
detailCode: '',
|
||
detail: {},
|
||
canceling: false,
|
||
reservationContractDetail: null,
|
||
reservationInspectionDetail: null
|
||
}
|
||
},
|
||
watch: {
|
||
language() {
|
||
this.loadReservationFlow()
|
||
this.loadPendingDepositBillState()
|
||
}
|
||
},
|
||
computed: {
|
||
isViewingApplication() {
|
||
return !!(this.detail && this.detail.isViewingApplication)
|
||
},
|
||
displayTitle() {
|
||
if (this.isViewingApplication) {
|
||
return this.detail.intendedRoomTypeName || this.$tr('预约看房申请')
|
||
}
|
||
return this.detail.intendedRoomTypeName || this.$tr('入住申请')
|
||
},
|
||
applicationTypeText() {
|
||
return this.isViewingApplication ? this.$tr('预约看房') : this.$tr('入住申请')
|
||
},
|
||
statusText() {
|
||
if (this.language !== 'en' && this.detail && this.detail.statusDesc) {
|
||
return this.detail.statusDesc
|
||
}
|
||
const map = {
|
||
0: this.$tr('待审核'),
|
||
10: this.$tr('已通过'),
|
||
20: this.$tr('已拒绝'),
|
||
99: this.$tr('已结束')
|
||
}
|
||
return map[this.detail.status] || this.$tr('处理中')
|
||
},
|
||
statusType() {
|
||
if (this.detail.status === 10) {
|
||
return 'approved'
|
||
}
|
||
if (this.detail.status === 20) {
|
||
return 'rejected'
|
||
}
|
||
if (this.detail.status === 99) {
|
||
return 'finished'
|
||
}
|
||
return 'pending'
|
||
},
|
||
heroThemeClass() {
|
||
return this.isViewingApplication ? 'hero-viewing' : 'hero-stay'
|
||
},
|
||
heroBadgeClass() {
|
||
return this.isViewingApplication ? 'badge-viewing' : 'badge-stay'
|
||
},
|
||
showPaymentStatusPanel() {
|
||
return !this.isViewingApplication
|
||
},
|
||
paymentStatusText() {
|
||
if (this.isWaitingButlerRoomAssignment(this.detail)) {
|
||
return this.$tr('等待管家排房')
|
||
}
|
||
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
|
||
if (this.$getLanguage() !== 'en' && statusInfo.desc) {
|
||
return statusInfo.desc
|
||
}
|
||
const textMap = {
|
||
exempt: this.$tr('无需支付'),
|
||
paid: this.$tr('已支付'),
|
||
refunded: this.$tr('已退款'),
|
||
failed: this.$tr('支付失败'),
|
||
processing: this.$tr('处理中'),
|
||
pending: this.$tr('待支付')
|
||
}
|
||
return textMap[statusInfo.key] || statusInfo.desc || this.$tr('待支付')
|
||
},
|
||
paymentStatusType() {
|
||
if (this.isWaitingButlerRoomAssignment(this.detail)) {
|
||
return 'info'
|
||
}
|
||
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
|
||
const typeMap = {
|
||
exempt: 'neutral',
|
||
paid: 'success',
|
||
refunded: 'neutral',
|
||
failed: 'danger',
|
||
processing: 'info',
|
||
pending: 'warning'
|
||
}
|
||
return typeMap[statusInfo.key] || 'warning'
|
||
},
|
||
displayDateRange() {
|
||
if (!this.detail.start && !this.detail.end) {
|
||
return '--'
|
||
}
|
||
if (this.isViewingApplication) {
|
||
return `${this.formatDateTime(this.detail.start)}${this.$tr(' 至 ')}${this.formatDateTime(this.detail.end)}`
|
||
}
|
||
return `${this.formatDate(this.detail.start)}${this.$tr(' 至 ')}${this.formatDate(this.detail.end)}`
|
||
},
|
||
remarkLines() {
|
||
const text = this.detail && this.detail.remark ? String(this.detail.remark).trim() : ''
|
||
if (!text) {
|
||
return [this.$tr('暂无补充说明')]
|
||
}
|
||
return text.split(/\r?\n/).filter(Boolean)
|
||
},
|
||
viewingMeta() {
|
||
if (!this.isViewingApplication) {
|
||
return []
|
||
}
|
||
return this.parseViewingRemark()
|
||
},
|
||
currentStepActionText() {
|
||
if (this.isViewingApplication) {
|
||
return ''
|
||
}
|
||
if (this.isReservationInspectionPendingReview()) {
|
||
return ''
|
||
}
|
||
if (this.isReservationContractAwaitingReview()) {
|
||
return ''
|
||
}
|
||
if (this.isReservationCheckinProcessing()) {
|
||
return ''
|
||
}
|
||
return getReservationCurrentActionText(this.detail, this.language)
|
||
},
|
||
statusReminderText() {
|
||
if (this.isReservationInspectionPendingReview()) {
|
||
return this.getReservationInspectionStatusText()
|
||
}
|
||
if (this.isReservationContractAwaitingReview()) {
|
||
return this.getReservationContractStatusText()
|
||
}
|
||
if (this.isReservationCheckinProcessing()) {
|
||
return this.$tr('宿管正在办理入住,请耐心等待')
|
||
}
|
||
const reservationStepText = getReservationCurrentStepText(this.detail, this.language)
|
||
if (reservationStepText) {
|
||
return reservationStepText
|
||
}
|
||
if (this.isWaitingButlerRoomAssignment(this.detail)) {
|
||
return this.$tr('等待管家排房')
|
||
}
|
||
if (this.shouldPayDeposit(this.detail)) {
|
||
return this.$tr('请支付定金')
|
||
}
|
||
if (this.isWaitingRoomAssignment(this.detail)) {
|
||
return this.$tr('等待管家排房')
|
||
}
|
||
return ''
|
||
},
|
||
canViewBill() {
|
||
return this.shouldPayDeposit(this.detail)
|
||
&& this.hasPendingDepositBill(this.detail)
|
||
&& !this.isViewingApplication
|
||
&& this.detail
|
||
&& this.detail.detailCode
|
||
&& this.statusType === 'pending'
|
||
},
|
||
canCancelApplication() {
|
||
if (!(this.detail && this.detail.detailCode && Number(this.detail.status) === 0)) {
|
||
return false
|
||
}
|
||
if (this.isViewingApplication) {
|
||
return true
|
||
}
|
||
if (Number(this.detail.payStatus) === 10) {
|
||
return false
|
||
}
|
||
return this.hasPendingDepositBill(this.detail)
|
||
},
|
||
canCancelOrder() {
|
||
return this.canCancelApplication && !this.canceling
|
||
}
|
||
},
|
||
onLoad(options = {}) {
|
||
this.detailCode = options.code || ''
|
||
this.restoreDetail(options.payload || '')
|
||
},
|
||
onShow() {
|
||
this.loadReservationFlow()
|
||
this.loadPendingDepositBillState()
|
||
},
|
||
methods: {
|
||
getBillNo(item = {}) {
|
||
return item.no || item.billNo || ''
|
||
},
|
||
normalizeBillList(response) {
|
||
const payload = parseJsonStringMaybe(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 []
|
||
},
|
||
isPaidApplication(item = {}) {
|
||
return !item.isViewingApplication && Number(item.payStatus) === 10
|
||
},
|
||
shouldQueryPendingDepositBill(item = {}) {
|
||
return !item.isViewingApplication
|
||
&& Number(item.status) === 0
|
||
&& Number(item.payStatus) !== 10
|
||
&& !!String(item.detailCode || this.detailCode || '').trim()
|
||
},
|
||
hasPendingDepositBill(item = {}) {
|
||
return !!String(this.getBillNo(item && item.pendingDepositBill ? item.pendingDepositBill : {}) || '').trim()
|
||
},
|
||
isWaitingButlerRoomAssignment(item = {}) {
|
||
return this.shouldQueryPendingDepositBill(item)
|
||
&& !!(item && item.pendingDepositBillQueried)
|
||
&& !this.hasPendingDepositBill(item)
|
||
},
|
||
shouldPayDeposit(item = {}) {
|
||
return !item.isViewingApplication
|
||
&& Number(item.status) === 0
|
||
&& Number(item.payStatus) !== 10
|
||
},
|
||
isWaitingRoomAssignment(item = {}) {
|
||
return !item.isViewingApplication
|
||
&& Number(item.status) === 0
|
||
&& (Number(item.payStatus) === 10 || this.isWaitingButlerRoomAssignment(item))
|
||
},
|
||
getReservationCurrentStepValue() {
|
||
return Number(this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode.step : 0)
|
||
},
|
||
isReservationCheckinProcessing() {
|
||
return this.getReservationCurrentStepValue() === RESERVATION_FLOW_STEP.CHECKIN
|
||
},
|
||
getReservationInspectionNo() {
|
||
const currentNode = this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode : {}
|
||
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.INSPECTION) {
|
||
return ''
|
||
}
|
||
return String(currentNode.refNo || '').trim()
|
||
},
|
||
getReservationContractNo() {
|
||
const currentNode = this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode : {}
|
||
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.CONTRACT) {
|
||
return ''
|
||
}
|
||
return String(currentNode.refNo || '').trim()
|
||
},
|
||
normalizeInspectionDetail(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 {}
|
||
},
|
||
normalizeContractDetail(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 {}
|
||
},
|
||
isReservationInspectionPendingReview() {
|
||
if (this.getReservationCurrentStepValue() !== RESERVATION_FLOW_STEP.INSPECTION) {
|
||
return false
|
||
}
|
||
const detail = this.reservationInspectionDetail || {}
|
||
return Number(detail.status) === 2
|
||
},
|
||
getReservationInspectionStatusText() {
|
||
return this.$tr('inspectionPendingReviewHint')
|
||
},
|
||
isReservationContractAwaitingReview() {
|
||
if (this.getReservationCurrentStepValue() !== RESERVATION_FLOW_STEP.CONTRACT) {
|
||
return false
|
||
}
|
||
const detail = this.reservationContractDetail || {}
|
||
const queryStatus = Number(detail.queryStatus)
|
||
const approvalStatus = Number(detail.approvalStatus)
|
||
const hasSigned = !!String(detail.signTime || detail.signImageUrl || '').trim()
|
||
return queryStatus === 1 || (hasSigned && approvalStatus === 0)
|
||
},
|
||
getReservationContractStatusText() {
|
||
const detail = this.reservationContractDetail || {}
|
||
return detail.queryStatusDesc || detail.approvalStatusDesc || detail.statusDesc || ''
|
||
},
|
||
async ensureReservationInspectionDetail() {
|
||
const inspectionNo = this.getReservationInspectionNo()
|
||
if (!inspectionNo) {
|
||
this.reservationInspectionDetail = null
|
||
return null
|
||
}
|
||
try {
|
||
const detail = this.normalizeInspectionDetail(await roomInspectionDetail({
|
||
inspectionNo
|
||
}, this.language))
|
||
this.reservationInspectionDetail = detail && Object.keys(detail).length ? detail : null
|
||
return this.reservationInspectionDetail
|
||
} catch (error) {
|
||
console.error('load inspection detail failed', error)
|
||
this.reservationInspectionDetail = null
|
||
return null
|
||
}
|
||
},
|
||
async ensureReservationContractDetail() {
|
||
const contractNo = this.getReservationContractNo()
|
||
if (!contractNo) {
|
||
this.reservationContractDetail = null
|
||
return null
|
||
}
|
||
try {
|
||
const detail = this.normalizeContractDetail(await contractDetail({
|
||
contractNo
|
||
}, this.language))
|
||
this.reservationContractDetail = detail && Object.keys(detail).length ? detail : null
|
||
return this.reservationContractDetail
|
||
} catch (error) {
|
||
console.error('load contract detail failed', error)
|
||
this.reservationContractDetail = null
|
||
return null
|
||
}
|
||
},
|
||
restoreDetail(payload = '') {
|
||
if (payload) {
|
||
try {
|
||
const parsed = JSON.parse(decodeURIComponent(payload))
|
||
this.detail = parsed && typeof parsed === 'object' ? parsed : {}
|
||
return
|
||
} catch (error) {
|
||
console.error('parse application detail payload error', error)
|
||
}
|
||
}
|
||
|
||
const list = normalizeApplicationList(getMyApplicationList())
|
||
const matched = list.find(item => String(item.detailCode || '') === String(this.detailCode || ''))
|
||
this.detail = matched || {
|
||
detailCode: this.detailCode,
|
||
isViewingApplication: false
|
||
}
|
||
},
|
||
syncDetailToCache(detail = this.detail) {
|
||
const list = normalizeApplicationList(getMyApplicationList())
|
||
if (!list.length || !detail || !detail.detailCode) {
|
||
return
|
||
}
|
||
saveMyApplicationList(list.map((item) => {
|
||
if (String(item.detailCode || '') !== String(detail.detailCode || '')) {
|
||
return item
|
||
}
|
||
return {
|
||
...item,
|
||
...detail
|
||
}
|
||
}))
|
||
},
|
||
async loadPendingDepositBillState() {
|
||
const currentDetail = this.detail && typeof this.detail === 'object' ? this.detail : {}
|
||
const referenceTypeNo = String(currentDetail.detailCode || this.detailCode || '').trim()
|
||
if (!this.shouldQueryPendingDepositBill(currentDetail) || !referenceTypeNo) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await accountQueryBills({
|
||
referenceTypeNo,
|
||
status: 10,
|
||
paymentStatus: 0,
|
||
ProviderType: 'WechatMiniProgram'
|
||
}, this.language)
|
||
if (referenceTypeNo !== String(this.detail && this.detail.detailCode || this.detailCode || '').trim()) {
|
||
return
|
||
}
|
||
const billList = this.normalizeBillList(response)
|
||
const nextDetail = {
|
||
...this.detail,
|
||
pendingDepositBill: Array.isArray(billList) && billList.length ? billList[0] : null,
|
||
pendingDepositBillQueried: true
|
||
}
|
||
this.detail = nextDetail
|
||
this.syncDetailToCache(nextDetail)
|
||
} catch (error) {
|
||
console.error('load pending deposit bill failed', error)
|
||
}
|
||
},
|
||
async loadReservationFlow() {
|
||
if (!this.detail || this.detail.isViewingApplication) {
|
||
return
|
||
}
|
||
|
||
const orderNo = getReservationOrderNo(this.detail)
|
||
if (!orderNo) {
|
||
this.detail = attachReservationFlow(this.detail, {})
|
||
this.reservationInspectionDetail = null
|
||
this.reservationContractDetail = null
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await orderSearchedReservationFlow({
|
||
orderNos: [orderNo]
|
||
}, this.language)
|
||
const flowMap = buildReservationFlowMap(parseReservationFlowResponse(response))
|
||
this.detail = attachReservationFlow(this.detail, flowMap)
|
||
await Promise.all([
|
||
this.ensureReservationInspectionDetail(),
|
||
this.ensureReservationContractDetail()
|
||
])
|
||
|
||
const list = normalizeApplicationList(getMyApplicationList())
|
||
if (list.length) {
|
||
saveMyApplicationList(list.map((item) => {
|
||
if (String(item.detailCode || '') !== String(this.detail.detailCode || '')) {
|
||
return item
|
||
}
|
||
return this.detail
|
||
}))
|
||
}
|
||
} catch (error) {
|
||
console.error('load reservation flow failed', error)
|
||
this.detail = attachReservationFlow(this.detail, {})
|
||
this.reservationInspectionDetail = null
|
||
this.reservationContractDetail = null
|
||
}
|
||
},
|
||
parseViewingRemark() {
|
||
const lines = this.remarkLines
|
||
const targets = [
|
||
{ zh: '看房时间', en: 'Viewing Time' },
|
||
{ zh: '预计租住时长', en: 'Expected Stay Duration' },
|
||
{ zh: '租金预算', en: 'Budget' },
|
||
{ zh: '工作/通勤地点', en: 'Work / Commute Location' },
|
||
{ zh: '意向区域', en: 'Preferred Area' },
|
||
{ zh: '意向房型', en: 'Preferred Room Type' },
|
||
{ zh: '支付方式', en: 'Payment Method' }
|
||
]
|
||
return lines
|
||
.map((line) => {
|
||
const matched = targets.find(item => {
|
||
return line.startsWith(`${item.zh}:`)
|
||
|| line.startsWith(`${item.zh}:`)
|
||
|| line.startsWith(`${item.en}:`)
|
||
|| line.startsWith(`${item.en}:`)
|
||
})
|
||
if (!matched) {
|
||
return null
|
||
}
|
||
const localizedLabelMap = {
|
||
'看房时间': this.$tr('看房时间'),
|
||
'预计租住时长': this.$tr('预计租住时长'),
|
||
'租金预算': this.$tr('租金预算'),
|
||
'工作/通勤地点': this.$tr('工作/通勤地点'),
|
||
'意向区域': this.$tr('意向区域'),
|
||
'意向房型': this.$tr('意向房型'),
|
||
'支付方式': this.$tr('支付方式')
|
||
}
|
||
const rawValue = line
|
||
.replace(`${matched.zh}:`, '')
|
||
.replace(`${matched.zh}:`, '')
|
||
.replace(`${matched.en}:`, '')
|
||
.replace(`${matched.en}:`, '')
|
||
return {
|
||
label: localizedLabelMap[matched.zh] || matched.en,
|
||
value: rawValue || '--'
|
||
}
|
||
})
|
||
.filter(Boolean)
|
||
},
|
||
formatDateTime(value) {
|
||
if (!value) {
|
||
return '--'
|
||
}
|
||
const text = String(value).replace('T', ' ')
|
||
return text.length > 16 ? text.slice(0, 16) : text
|
||
},
|
||
formatDate(value) {
|
||
if (!value) {
|
||
return '--'
|
||
}
|
||
return String(value).slice(0, 10)
|
||
},
|
||
copyCode() {
|
||
if (!this.detail.detailCode) {
|
||
return
|
||
}
|
||
uni.setClipboardData({
|
||
data: String(this.detail.detailCode),
|
||
success: () => {
|
||
uni.showToast({
|
||
title: this.$tr('申请编号已复制'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
getCancelOrderContent() {
|
||
return this.isViewingApplication
|
||
? this.$tr('取消后将关闭当前预约看房申请,是否继续?')
|
||
: this.$tr('取消后将关闭当前申请单,是否继续?')
|
||
},
|
||
confirmCancelOrder() {
|
||
return new Promise((resolve) => {
|
||
uni.showModal({
|
||
title: this.$tr('取消申请'),
|
||
content: this.getCancelOrderContent(),
|
||
confirmColor: '#ef4444',
|
||
success: (res) => resolve(!!(res && res.confirm)),
|
||
fail: () => resolve(false)
|
||
})
|
||
})
|
||
},
|
||
syncCanceledDetailToCache() {
|
||
const list = normalizeApplicationList(getMyApplicationList())
|
||
if (!list.length) {
|
||
return
|
||
}
|
||
const nextList = list.map((item) => {
|
||
if (String(item.detailCode || '') !== String(this.detail.detailCode || '')) {
|
||
return item
|
||
}
|
||
return {
|
||
...item,
|
||
status: 99,
|
||
statusDesc: this.$tr('已结束')
|
||
}
|
||
})
|
||
saveMyApplicationList(nextList)
|
||
},
|
||
handleCancelOrder() {
|
||
if (!this.canCancelOrder || !this.detail.detailCode) {
|
||
return
|
||
}
|
||
this.confirmCancelOrder()
|
||
.then((confirmed) => {
|
||
if (!confirmed) {
|
||
return
|
||
}
|
||
this.canceling = true
|
||
return userDetailCancel({
|
||
detailCode: this.detail.detailCode
|
||
}, this.language)
|
||
.then(() => {
|
||
this.detail = {
|
||
...this.detail,
|
||
status: 99,
|
||
statusDesc: this.$tr('已结束')
|
||
}
|
||
this.syncCanceledDetailToCache()
|
||
uni.showToast({
|
||
title: this.$tr('申请已取消'),
|
||
icon: 'success'
|
||
})
|
||
})
|
||
})
|
||
.catch((error) => {
|
||
uni.showToast({
|
||
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
|
||
icon: 'none'
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.canceling = false
|
||
})
|
||
},
|
||
goBackList() {
|
||
uni.navigateBack()
|
||
},
|
||
goToBillPage() {
|
||
if (!this.detail.detailCode) {
|
||
uni.showToast({
|
||
title: this.$tr('申请单编号缺失'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
saveActiveApplicationCode(this.detail.detailCode)
|
||
uni.navigateTo({
|
||
url: `/pkgService/utility_recharge/utility_recharge?referenceTypeNo=${encodeURIComponent(this.detail.detailCode)}`
|
||
})
|
||
},
|
||
refreshCurrentPage() {
|
||
this.loadReservationFlow()
|
||
},
|
||
handleCurrentStepAction() {
|
||
const step = this.getReservationCurrentStepValue()
|
||
|
||
if (step === RESERVATION_FLOW_STEP.BILL) {
|
||
this.goToBillPage()
|
||
return
|
||
}
|
||
|
||
if (step === RESERVATION_FLOW_STEP.CONTRACT) {
|
||
if (this.isReservationContractAwaitingReview()) {
|
||
return
|
||
}
|
||
const contractNo = String(this.detail && this.detail.reservationCurrentNode && this.detail.reservationCurrentNode.refNo || '').trim()
|
||
if (!contractNo) {
|
||
uni.showToast({
|
||
title: this.$tr('合同编号缺失'),
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
uni.navigateTo({
|
||
url: `/pkgService/contract_sign/contract_sign?no=${encodeURIComponent(contractNo)}`
|
||
})
|
||
return
|
||
}
|
||
|
||
if (step === RESERVATION_FLOW_STEP.CHECKIN) {
|
||
uni.reLaunch({
|
||
url: '/pages/index/index?pageId=stay-service'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (step === RESERVATION_FLOW_STEP.INSPECTION) {
|
||
if (this.isReservationInspectionPendingReview()) {
|
||
return
|
||
}
|
||
const orderNo = getReservationOrderNo(this.detail)
|
||
uni.navigateTo({
|
||
url: orderNo
|
||
? `/pkgService/room_inspection/room_inspection?orderNo=${encodeURIComponent(orderNo)}`
|
||
: '/pkgService/room_inspection/room_inspection'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.application-detail-page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
||
}
|
||
|
||
.hero-panel,
|
||
.content-panel {
|
||
background: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
padding: 32rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||
border: 1px solid rgba($brand-primary, 0.05);
|
||
}
|
||
|
||
.hero-viewing {
|
||
background: linear-gradient(135deg, rgba($brand-primary, 0.12) 0%, rgba($brand-primary, 0.04) 100%);
|
||
}
|
||
|
||
.hero-stay {
|
||
background: linear-gradient(135deg, rgba(64, 203, 242, 0.12) 0%, rgba($brand-primary, 0.04) 100%);
|
||
}
|
||
|
||
.status-reminder-panel {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 20rpx;
|
||
padding: 28rpx 32rpx;
|
||
border-radius: 24rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||
}
|
||
|
||
.status-reminder-panel--warning {
|
||
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
|
||
border: 1px solid rgba(249, 115, 22, 0.16);
|
||
}
|
||
|
||
.status-reminder-panel--info {
|
||
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
|
||
border: 1px solid rgba(59, 130, 246, 0.14);
|
||
}
|
||
|
||
.payment-status-panel {
|
||
padding: 24rpx 28rpx;
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 24rpx;
|
||
border: 1rpx solid transparent;
|
||
}
|
||
|
||
.payment-status-panel__label {
|
||
flex-shrink: 0;
|
||
font-size: 24rpx;
|
||
color: #64748B;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.payment-status-panel__main {
|
||
min-width: 0;
|
||
text-align: right;
|
||
}
|
||
|
||
.payment-status-panel__value {
|
||
font-size: 34rpx;
|
||
line-height: 1.2;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.payment-status-panel__hint {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #9A3412;
|
||
}
|
||
|
||
.payment-status-panel--warning {
|
||
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
|
||
border-color: rgba(249, 115, 22, 0.16);
|
||
}
|
||
|
||
.payment-status-panel--warning .payment-status-panel__value {
|
||
color: #C2410C;
|
||
}
|
||
|
||
.payment-status-panel--success {
|
||
background: linear-gradient(180deg, rgba(236, 253, 245, 0.96) 0%, rgba(240, 253, 250, 0.98) 100%);
|
||
border-color: rgba(16, 185, 129, 0.16);
|
||
}
|
||
|
||
.payment-status-panel--success .payment-status-panel__value {
|
||
color: #047857;
|
||
}
|
||
|
||
.payment-status-panel--info {
|
||
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
|
||
border-color: rgba(59, 130, 246, 0.14);
|
||
}
|
||
|
||
.payment-status-panel--info .payment-status-panel__value {
|
||
color: #1D4ED8;
|
||
}
|
||
|
||
.payment-status-panel--danger {
|
||
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 245, 245, 0.98) 100%);
|
||
border-color: rgba(239, 68, 68, 0.16);
|
||
}
|
||
|
||
.payment-status-panel--danger .payment-status-panel__value {
|
||
color: #B91C1C;
|
||
}
|
||
|
||
.payment-status-panel--neutral {
|
||
background: linear-gradient(180deg, rgba(241, 245, 249, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
|
||
border-color: rgba(148, 163, 184, 0.16);
|
||
}
|
||
|
||
.payment-status-panel--neutral .payment-status-panel__value {
|
||
color: #475569;
|
||
}
|
||
|
||
.status-reminder-panel__icon {
|
||
width: 68rpx;
|
||
height: 68rpx;
|
||
border-radius: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
font-size: 34rpx;
|
||
}
|
||
|
||
.status-reminder-panel--warning .status-reminder-panel__icon {
|
||
color: #C2410C;
|
||
background: rgba(249, 115, 22, 0.12);
|
||
}
|
||
|
||
.status-reminder-panel--info .status-reminder-panel__icon {
|
||
color: #1D4ED8;
|
||
background: rgba(59, 130, 246, 0.12);
|
||
}
|
||
|
||
.status-reminder-panel__content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.status-reminder-panel__title {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
color: #0F172A;
|
||
}
|
||
|
||
.status-reminder-panel__text {
|
||
margin-top: 8rpx;
|
||
font-size: 30rpx;
|
||
line-height: 1.6;
|
||
font-weight: 600;
|
||
color: #334155;
|
||
}
|
||
|
||
.hero-top-row,
|
||
.hero-meta-row,
|
||
.meta-row,
|
||
.action-row {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.hero-top-row,
|
||
.meta-row {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.hero-meta-row,
|
||
.action-row {
|
||
gap: 16rpx;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.hero-badge,
|
||
.hero-status,
|
||
.hero-meta-chip,
|
||
.copy-btn,
|
||
.ghost-btn,
|
||
.action-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.hero-badge,
|
||
.hero-status {
|
||
padding: 10rpx 22rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.badge-viewing,
|
||
.badge-stay {
|
||
background: rgba($brand-primary, 0.14);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.hero-status--pending {
|
||
background: rgba(245, 158, 11, 0.12);
|
||
color: #B45309;
|
||
}
|
||
|
||
.hero-status--approved {
|
||
background: rgba($brand-primary, 0.12);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.hero-status--rejected {
|
||
background: rgba(239, 68, 68, 0.12);
|
||
color: #B91C1C;
|
||
}
|
||
|
||
.hero-status--finished {
|
||
background: rgba(100, 116, 139, 0.12);
|
||
color: #475569;
|
||
}
|
||
|
||
.hero-title,
|
||
.panel-title {
|
||
font-size: 34rpx;
|
||
font-weight: 700;
|
||
color: #1A2233;
|
||
}
|
||
|
||
.hero-meta-chip {
|
||
height: 56rpx;
|
||
padding: 0 20rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(255, 255, 255, 0.78);
|
||
color: #51607A;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.meta-row {
|
||
padding: 18rpx 0;
|
||
border-bottom: 1px dashed rgba($brand-primary, 0.14);
|
||
}
|
||
|
||
.meta-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.meta-label {
|
||
color: #6A7A99;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.meta-value {
|
||
max-width: 62%;
|
||
text-align: right;
|
||
color: #1A2233;
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.meta-action-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.copy-btn {
|
||
height: 48rpx;
|
||
padding: 0 18rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba($brand-primary, 0.1);
|
||
color: $brand-primary;
|
||
}
|
||
|
||
.remark-panel {
|
||
padding: 24rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba($brand-primary, 0.03);
|
||
border: 1rpx solid rgba($brand-primary, 0.12);
|
||
}
|
||
|
||
.remark-line {
|
||
color: #1A2233;
|
||
line-height: 1.8;
|
||
font-size: 27rpx;
|
||
}
|
||
|
||
.ghost-btn,
|
||
.danger-btn,
|
||
.action-btn {
|
||
flex: 1;
|
||
min-height: 92rpx;
|
||
border-radius: 999rpx;
|
||
}
|
||
|
||
.ghost-btn {
|
||
background: #FFFFFF;
|
||
border: 1rpx solid rgba($brand-primary, 0.2);
|
||
color: $brand-primary;
|
||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.06);
|
||
}
|
||
|
||
.danger-btn {
|
||
background: #FFFFFF;
|
||
border: 1rpx solid rgba(239, 68, 68, 0.22);
|
||
color: #DC2626;
|
||
box-shadow: 0 6rpx 16rpx rgba(239, 68, 68, 0.08);
|
||
}
|
||
|
||
.danger-btn--loading {
|
||
opacity: 0.72;
|
||
}
|
||
|
||
.action-btn {
|
||
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);
|
||
}
|
||
|
||
.action-btn--ghost {
|
||
background: rgba($brand-primary, 0.08);
|
||
color: $brand-primary;
|
||
border: 1rpx solid rgba($brand-primary, 0.18);
|
||
box-shadow: none;
|
||
}
|
||
</style>
|