Files
QSY-lvjuxcx/pkgService/application_detail/application_detail.vue
liangjian 9fee2c3a74 feat: 优化首页服务入口与租住业务流程
补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
2026-08-25 13:53:12 +08:00

1240 lines
37 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>