- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面 - 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面 - 预约看样流程新增房型/楼栋/楼层选择器组件 - 新增工具模块: booking-entry、privacy、reservation-flow - api/index.js 补充楼层列表、看样提交等接口 - locales/messages.js 补充多语言文案 - manifest.json 开启微信小程序隐私检查 - 完善登录、注册、申请详情、账单等相关页面交互
1097 lines
33 KiB
Vue
1097 lines
33 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">{{ t('navTitle') }}</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="statusReminderText"
|
||
class="status-reminder-panel tn-margin-bottom-lg"
|
||
:class="{
|
||
'status-reminder-panel--warning': shouldPayDeposit(detail),
|
||
'status-reminder-panel--info': isWaitingRoomAssignment(detail)
|
||
}"
|
||
>
|
||
<view class="status-reminder-panel__icon">
|
||
<text :class="shouldPayDeposit(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
|
||
</view>
|
||
<view class="status-reminder-panel__content">
|
||
<view class="status-reminder-panel__title">{{ t('statusReminderTitle') }}</view>
|
||
<view class="status-reminder-panel__text">{{ statusReminderText }}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="content-panel tn-margin-bottom-lg">
|
||
<view class="panel-title">{{ t('basicInfoTitle') }}</view>
|
||
<view class="meta-row tn-margin-top">
|
||
<text class="meta-label">{{ t('applicationCodeLabel') }}</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">{{ t('copy') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ t('applicationTypeLabel') }}</text>
|
||
<text class="meta-value">{{ applicationTypeText }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ t('statusLabel') }}</text>
|
||
<text class="meta-value">{{ statusText }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ t('applicantLabel') }}</text>
|
||
<text class="meta-value">{{ detail.userName || detail.phone || '--' }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ t('apartmentLabel') }}</text>
|
||
<text class="meta-value">{{ detail.intendedAreaName || '--' }}</text>
|
||
</view>
|
||
<view class="meta-row" v-if="detail.intendedRoomTypeName">
|
||
<text class="meta-label">{{ isViewingApplication ? t('intendedRoomTypeLabel') : t('appliedRoomTypeLabel') }}</text>
|
||
<text class="meta-value">{{ detail.intendedRoomTypeName }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ isViewingApplication ? t('viewingTimeLabel') : t('leaseTermLabel') }}</text>
|
||
<text class="meta-value">{{ displayDateRange }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-label">{{ t('submittedAtLabel') }}</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">{{ t('viewingInfoTitle') }}</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 ? t('applicationRemarkTitle') : t('remarkTitle') }}</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">{{ t('backList') }}</text>
|
||
</view>
|
||
<view
|
||
v-if="canCancelOrder"
|
||
class="danger-btn"
|
||
:class="{ 'danger-btn--loading': canceling }"
|
||
@click.stop="handleCancelOrder"
|
||
>
|
||
<text class="tn-text-bold">{{ canceling ? t('cancelOrderLoading') : t('cancelOrder') }}</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">{{ t('refreshAction') }}</text>
|
||
</view>
|
||
<view
|
||
v-else-if="canViewBill"
|
||
class="action-btn"
|
||
@click.stop="goToBillPage"
|
||
>
|
||
<text class="tn-text-bold">{{ t('viewBill') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { 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 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 {
|
||
i18n: {
|
||
zh: {
|
||
navTitle: '申请详情',
|
||
basicInfoTitle: '基础信息',
|
||
applicationCodeLabel: '申请编号',
|
||
copy: '复制',
|
||
applicationTypeLabel: '申请类型',
|
||
statusLabel: '当前状态',
|
||
applicantLabel: '申请人',
|
||
apartmentLabel: '所属公寓',
|
||
intendedRoomTypeLabel: '意向房型',
|
||
appliedRoomTypeLabel: '申请房型',
|
||
viewingTimeLabel: '预约时间',
|
||
leaseTermLabel: '预计租期',
|
||
submittedAtLabel: '提交时间',
|
||
viewingInfoTitle: '带看信息',
|
||
applicationRemarkTitle: '申请说明',
|
||
remarkTitle: '补充说明',
|
||
backList: '返回列表',
|
||
viewBill: '查看关联账单',
|
||
viewingApplicationTitle: '预约看房申请',
|
||
stayApplicationTitle: '入住申请',
|
||
viewingApplicationType: '预约看房',
|
||
stayApplicationType: '入住申请',
|
||
statusPending: '待审核',
|
||
statusApproved: '已通过',
|
||
statusRejected: '已拒绝',
|
||
statusFinished: '已结束',
|
||
statusProcessing: '处理中',
|
||
statusPendingPayment: '请支付定金',
|
||
statusWaitingRoomAssign: '等待预订中心分配房间',
|
||
statusReminderTitle: '当前进度提醒',
|
||
emptyRemark: '暂无补充说明',
|
||
codeCopied: '申请编号已复制',
|
||
missingBillCode: '申请单编号缺失',
|
||
missingContractNo: '合同编号缺失',
|
||
checkinPendingHint: '宿管正在办理入住,请耐心等待',
|
||
refreshAction: '刷新',
|
||
cancelOrder: '取消订单',
|
||
cancelOrderLoading: '取消中...',
|
||
cancelOrderTitle: '取消订单',
|
||
cancelOrderContent: '取消后将关闭当前未支付的定金订单,是否继续?',
|
||
cancelOrderSuccess: '订单已取消',
|
||
cancelOrderFailed: '取消订单失败',
|
||
viewingTimeKey: '看房时间',
|
||
leaseDurationKey: '预计租住时长',
|
||
budgetKey: '租金预算',
|
||
commuteKey: '工作/通勤地点',
|
||
preferredAreaKey: '意向区域',
|
||
preferredRoomTypeKey: '意向房型',
|
||
paymentMethodKey: '支付方式',
|
||
dateRangeJoiner: ' 至 '
|
||
},
|
||
en: {
|
||
navTitle: 'Application Detail',
|
||
basicInfoTitle: 'Basic Information',
|
||
applicationCodeLabel: 'Application No.',
|
||
copy: 'Copy',
|
||
applicationTypeLabel: 'Application Type',
|
||
statusLabel: 'Status',
|
||
applicantLabel: 'Applicant',
|
||
apartmentLabel: 'Apartment',
|
||
intendedRoomTypeLabel: 'Preferred Room Type',
|
||
appliedRoomTypeLabel: 'Requested Room Type',
|
||
viewingTimeLabel: 'Viewing Time',
|
||
leaseTermLabel: 'Lease Term',
|
||
submittedAtLabel: 'Submitted At',
|
||
viewingInfoTitle: 'Viewing Details',
|
||
applicationRemarkTitle: 'Application Notes',
|
||
remarkTitle: 'Remarks',
|
||
backList: 'Back to List',
|
||
viewBill: 'View Related Bill',
|
||
viewingApplicationTitle: 'Viewing Request',
|
||
stayApplicationTitle: 'Stay Application',
|
||
viewingApplicationType: 'Viewing Request',
|
||
stayApplicationType: 'Stay Application',
|
||
statusPending: 'Pending',
|
||
statusApproved: 'Approved',
|
||
statusRejected: 'Rejected',
|
||
statusFinished: 'Closed',
|
||
statusProcessing: 'Processing',
|
||
statusPendingPayment: 'Please Pay Deposit',
|
||
statusWaitingRoomAssign: 'Waiting for Room Assignment',
|
||
statusReminderTitle: 'Current Progress',
|
||
emptyRemark: 'No additional remarks',
|
||
codeCopied: 'Application number copied',
|
||
missingBillCode: 'Missing application bill number',
|
||
missingContractNo: 'Missing contract number',
|
||
checkinPendingHint: 'Move-in is being processed, please wait',
|
||
refreshAction: 'Refresh',
|
||
cancelOrder: 'Cancel Order',
|
||
cancelOrderLoading: 'Canceling...',
|
||
cancelOrderTitle: 'Cancel Order',
|
||
cancelOrderContent: 'This will close the current unpaid deposit order. Continue?',
|
||
cancelOrderSuccess: 'Order canceled',
|
||
cancelOrderFailed: 'Failed to cancel order',
|
||
viewingTimeKey: 'Viewing Time',
|
||
leaseDurationKey: 'Expected Stay Duration',
|
||
budgetKey: 'Budget',
|
||
commuteKey: 'Work / Commute Location',
|
||
preferredAreaKey: 'Preferred Area',
|
||
preferredRoomTypeKey: 'Preferred Room Type',
|
||
paymentMethodKey: 'Payment Method',
|
||
dateRangeJoiner: ' to '
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
detailCode: '',
|
||
detail: {},
|
||
canceling: false,
|
||
reservationContractDetail: null,
|
||
reservationInspectionDetail: null
|
||
}
|
||
},
|
||
watch: {
|
||
language() {
|
||
this.loadReservationFlow()
|
||
}
|
||
},
|
||
computed: {
|
||
isViewingApplication() {
|
||
return !!(this.detail && this.detail.isViewingApplication)
|
||
},
|
||
displayTitle() {
|
||
if (this.isViewingApplication) {
|
||
return this.detail.intendedRoomTypeName || this.t('viewingApplicationTitle')
|
||
}
|
||
return this.detail.intendedRoomTypeName || this.t('stayApplicationTitle')
|
||
},
|
||
applicationTypeText() {
|
||
return this.isViewingApplication ? this.t('viewingApplicationType') : this.t('stayApplicationType')
|
||
},
|
||
statusText() {
|
||
if (this.language !== 'en' && this.detail && this.detail.statusDesc) {
|
||
return this.detail.statusDesc
|
||
}
|
||
const map = {
|
||
0: this.t('statusPending'),
|
||
10: this.t('statusApproved'),
|
||
20: this.t('statusRejected'),
|
||
99: this.t('statusFinished')
|
||
}
|
||
return map[this.detail.status] || this.t('statusProcessing')
|
||
},
|
||
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'
|
||
},
|
||
displayDateRange() {
|
||
if (!this.detail.start && !this.detail.end) {
|
||
return '--'
|
||
}
|
||
if (this.isViewingApplication) {
|
||
return `${this.formatDateTime(this.detail.start)}${this.t('dateRangeJoiner')}${this.formatDateTime(this.detail.end)}`
|
||
}
|
||
return `${this.formatDate(this.detail.start)}${this.t('dateRangeJoiner')}${this.formatDate(this.detail.end)}`
|
||
},
|
||
remarkLines() {
|
||
const text = this.detail && this.detail.remark ? String(this.detail.remark).trim() : ''
|
||
if (!text) {
|
||
return [this.t('emptyRemark')]
|
||
}
|
||
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.t('checkinPendingHint')
|
||
}
|
||
const reservationStepText = getReservationCurrentStepText(this.detail, this.language)
|
||
if (reservationStepText) {
|
||
return reservationStepText
|
||
}
|
||
if (this.shouldPayDeposit(this.detail)) {
|
||
return this.t('statusPendingPayment')
|
||
}
|
||
if (this.isWaitingRoomAssignment(this.detail)) {
|
||
return this.t('statusWaitingRoomAssign')
|
||
}
|
||
return ''
|
||
},
|
||
canViewBill() {
|
||
return this.shouldPayDeposit(this.detail)
|
||
&& !this.isViewingApplication
|
||
&& this.detail
|
||
&& this.detail.detailCode
|
||
&& this.statusType === 'pending'
|
||
},
|
||
canCancelOrder() {
|
||
return this.shouldPayDeposit(this.detail)
|
||
&& !this.isViewingApplication
|
||
&& this.detail
|
||
&& this.detail.detailCode
|
||
&& !this.canceling
|
||
}
|
||
},
|
||
onLoad(options = {}) {
|
||
this.detailCode = options.code || ''
|
||
this.restoreDetail(options.payload || '')
|
||
},
|
||
onShow() {
|
||
this.loadReservationFlow()
|
||
},
|
||
methods: {
|
||
t(key, params) {
|
||
return this.$tm('pkgService.application_detail.application_detail', key, params)
|
||
},
|
||
isPaidApplication(item = {}) {
|
||
return !item.isViewingApplication && Number(item.payStatus) === 10
|
||
},
|
||
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
|
||
},
|
||
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.t('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
|
||
}
|
||
},
|
||
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.t('viewingTimeKey'),
|
||
'预计租住时长': this.t('leaseDurationKey'),
|
||
'租金预算': this.t('budgetKey'),
|
||
'工作/通勤地点': this.t('commuteKey'),
|
||
'意向区域': this.t('preferredAreaKey'),
|
||
'意向房型': this.t('preferredRoomTypeKey'),
|
||
'支付方式': this.t('paymentMethodKey')
|
||
}
|
||
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.t('codeCopied'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
confirmCancelOrder() {
|
||
return new Promise((resolve) => {
|
||
uni.showModal({
|
||
title: this.t('cancelOrderTitle'),
|
||
content: this.t('cancelOrderContent'),
|
||
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.t('statusFinished')
|
||
}
|
||
})
|
||
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,
|
||
code: this.detail.detailCode,
|
||
referenceTypeNo: this.detail.detailCode
|
||
}, this.language)
|
||
.then(() => {
|
||
this.detail = {
|
||
...this.detail,
|
||
status: 99,
|
||
statusDesc: this.t('statusFinished')
|
||
}
|
||
this.syncCanceledDetailToCache()
|
||
uni.showToast({
|
||
title: this.t('cancelOrderSuccess'),
|
||
icon: 'success'
|
||
})
|
||
})
|
||
})
|
||
.catch((error) => {
|
||
uni.showToast({
|
||
title: (error && (error.message || error.msg)) || this.t('cancelOrderFailed'),
|
||
icon: 'none'
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.canceling = false
|
||
})
|
||
},
|
||
goBackList() {
|
||
uni.navigateBack()
|
||
},
|
||
goToBillPage() {
|
||
if (!this.detail.detailCode) {
|
||
uni.showToast({
|
||
title: this.t('missingBillCode'),
|
||
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.t('missingContractNo'),
|
||
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);
|
||
}
|
||
|
||
.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>
|