Files
QSY-lvjuxcx/pkgService/application_detail/application_detail.vue
liangjian 445bd850f4 feat: 新增工单中心、房间巡查、失物招领等业务模块
- 新增工单中心(ticket_center)与工单工作台(ticket_workbench)页面
- 新增房间巡查(room_inspection)与失物招领发布(lost_found_publish)页面
- 预约看样流程新增房型/楼栋/楼层选择器组件
- 新增工具模块: booking-entry、privacy、reservation-flow
- api/index.js 补充楼层列表、看样提交等接口
- locales/messages.js 补充多语言文案
- manifest.json 开启微信小程序隐私检查
- 完善登录、注册、申请详情、账单等相关页面交互
2026-07-24 09:35:46 +08:00

1097 lines
33 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">{{ 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>