Files
QSY-lvjuxcx/pages/index/component/MyApplicationTab.vue
liangjian 1111ac148b feat: 重构核心租住流程页面与交互体验
统一注册、看房、申请与入住等核心页面的视觉和流程衔接,补齐支付、签约与状态流转闭环,提升移动端操作连贯性。
2026-07-29 09:05:38 +08:00

3086 lines
101 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="my-application-page tn-flex tn-flex-direction-column" :style="{ height: 'calc(100vh - 100rpx - env(safe-area-inset-bottom))', boxSizing: 'border-box', paddingTop: (vuex_custom_bar_height) + 'px' }">
<view class="page-header tn-padding">
<view class="tn-text-bold tn-text-xl tn-color-black page-title">{{ $tr('我的申请') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ $tr('您的入住和预约看房申请记录') }}</view>
</view>
<scroll-view v-if="reservationOrders.length || list.length" scroll-y class="list-scroll-view tn-padding" @scrolltolower="loadMore">
<view v-if="reservationOrders.length" class="reservation-flow-panel tn-margin-bottom-lg" @click.stop>
<view class="reservation-flow-panel__head">
<view class="reservation-flow-panel__title-wrap">
<view class="reservation-flow-panel__title">{{ $tr('预定流程') }}</view>
<view class="reservation-flow-panel__desc">{{ $tr('当前预定单的流程进度') }}</view>
</view>
<view class="reservation-flow-panel__refresh" @click.stop="refreshStatus">
<text class="tn-text-bold">{{ $tr('刷新流程') }}</text>
</view>
</view>
<scroll-view scroll-x class="reservation-order-scroll" :show-scrollbar="false">
<view class="reservation-order-tabs">
<view
v-for="item in reservationOrders"
:key="item.orderNo"
class="reservation-order-chip"
:class="{ 'reservation-order-chip--active': activeReservationOrderNo === item.orderNo }"
@click.stop="switchReservationOrder(item)"
>
<view class="reservation-order-chip__title">{{ item.roomTypeText || item.roomTypeName || $tr('预定单') }}</view>
</view>
</view>
</scroll-view>
<view v-if="activeReservationOrder" class="reservation-grid-info tn-margin-top-lg">
<view class="grid-info-item">
<view class="grid-info-label">{{ $tr('所属公寓') }}</view>
<view class="grid-info-value">{{ activeReservationOrder.areaText || activeReservationOrder.areaName || '--' }}</view>
</view>
<view class="grid-info-item">
<view class="grid-info-label">{{ $tr('房型') }}</view>
<view class="grid-info-value">{{ activeReservationOrder.roomTypeText || activeReservationOrder.roomTypeName || '--' }}</view>
</view>
<view class="grid-info-item">
<view class="grid-info-label">{{ $tr('房间号') }}</view>
<view class="grid-info-value tn-font-mono">{{ activeReservationOrder.roomNoText || activeReservationOrder.roomNo || '--' }}</view>
</view>
<view class="grid-info-item">
<view class="grid-info-label">{{ $tr('订单状态') }}</view>
<view class="grid-info-value">{{ activeReservationOrder.orderStatusDesc || activeReservationOrder.statusText || '--' }}</view>
</view>
</view>
<view v-if="activeReservationFlowNodes.length" class="reservation-v-steps tn-margin-top-xl">
<view
v-for="(node, index) in activeReservationFlowNodes"
:key="`${activeReservationOrderNo}-${node.step}-${node.sort}`"
class="v-step-item"
:class="{
'v-step-item--current': !!node.isCurrent,
'v-step-item--last': index === activeReservationFlowNodes.length - 1
}"
>
<view class="v-step-indicator">
<view class="v-step-badge tn-font-mono">{{ node.sort || node.step }}</view>
<view v-if="index !== activeReservationFlowNodes.length - 1" class="v-step-line"></view>
</view>
<view class="v-step-content">
<view class="v-step-header">
<view class="v-step-title">{{ node.stepDesc || getReservationStepLabel(node.step) }}</view>
<view v-if="node.isCurrent" class="v-step-current-tag">{{ $tr('当前') }}</view>
</view>
<view
v-if="node.isCurrent && isReservationBillStep(activeReservationOrder) && getActiveReservationBillDetail()"
class="v-step-bill-card tn-margin-top-sm"
>
<view class="v-step-bill-row">
<text class="v-step-bill-label">{{ $tr('支付说明') }}</text>
<text class="v-step-bill-value">{{ getReservationBillDisplayTitle(getActiveReservationBillDetail()) }}</text>
</view>
<view class="v-step-bill-row">
<text class="v-step-bill-label">{{ $tr('订单金额') }}</text>
<text class="v-step-bill-value v-step-bill-value--price">¥{{ formatMoney(getReservationBillDisplayAmount(getActiveReservationBillDetail())) }}</text>
</view>
</view>
<view
v-if="node.isCurrent && getReservationCurrentStepHint(activeReservationOrder)"
class="v-step-hint-card tn-margin-top-sm"
>
<text class="v-step-hint-text">{{ getReservationCurrentStepHint(activeReservationOrder) }}</text>
</view>
<view
v-if="node.isCurrent && isReservationCheckinProcessing(activeReservationOrder)"
class="v-step-action tn-margin-top-sm"
>
<view class="step-refresh-btn" @click.stop="refreshStatus">
<text class="tn-text-bold">{{ $tr('刷新') }}</text>
</view>
</view>
<view v-if="node.isCurrent && activeReservationOrder && getCurrentStepActionText(activeReservationOrder)" class="v-step-action tn-margin-top-sm">
<view class="step-pay-btn" @click.stop="handleReservationOrderStepAction(activeReservationOrder)">
<text class="tn-text-bold">{{ getCurrentStepActionText(activeReservationOrder) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view
v-for="(item, index) in list"
:key="getApplicationKey(item)"
class="application-card tn-margin-bottom-lg"
@click="goToApplicationDetail(item)"
>
<view class="card-head tn-flex tn-flex-row-between tn-flex-col-center">
<view class="card-title-wrap tn-flex tn-flex-col-center">
<view class="title-icon tn-flex tn-flex-col-center tn-flex-row-center tn-margin-right-sm">
<text :class="item.isViewingApplication ? 'tn-icon-calendar' : 'tn-icon-safe'"></text>
</view>
<view class="card-title-block">
<view class="card-title">{{ getApplicationTitle(item) }}</view>
</view>
</view>
<view :class="['status-badge', 'status-' + resolveStatusType(item)]">
{{ getDisplayStatusText(item) }}
</view>
</view>
<view class="card-body tn-margin-top-lg">
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('所属公寓') }}</text>
<text class="info-value">{{ item.intendedAreaName || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('入住人') }}</text>
<text class="info-value">{{ item.userName || item.phone || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ item.isViewingApplication ? $tr('预约时间') : $tr('预计租期') }}</text>
<text class="info-value">{{ formatDateRange(item.start, item.end) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('提交时间') }}</text>
<text class="info-value">{{ formatDateTime(item.createTime) }}</text>
</view>
</view>
<view v-if="item.remark" class="remark-panel tn-margin-top-lg">
<view class="remark-line">
<text class="remark-label">{{ $tr('补充说明') }}</text>
<text class="remark-text">{{ item.remark }}</text>
</view>
</view>
<view v-if="getStatusReminderText(item)" class="reminder-panel tn-margin-top-lg" @click.stop>
<view class="reminder-head">
<view class="reminder-title">{{ $tr('当前进度提醒') }}</view>
<view class="reminder-refresh-btn" @click.stop="refreshStatus">
<text class="tn-text-bold">{{ $tr('刷新') }}</text>
</view>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('当前进度提醒') }}</text>
<text class="info-value">{{ getStatusReminderText(item) }}</text>
</view>
</view>
<view v-if="!isPaidApplication(item) && getApplicationPendingPaymentOrder(item)" class="pending-payment-panel tn-margin-top-lg" @click.stop>
<view class="pending-payment-title">{{ $tr('未完成支付订单') }}</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('订单金额') }}</text>
<text class="info-value info-value--highlight">¥{{ formatMoney(getApplicationPendingPaymentAmount(item)) }}</text>
</view>
<view v-if="getApplicationPendingPaymentExpireText(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('剩余支付时间') }}</text>
<text class="info-value" :class="{ 'info-value--expired': isApplicationPendingPaymentExpired(item) }">
{{ getApplicationPendingPaymentExpireText(item) }}
</text>
</view>
<view v-if="getApplicationPendingPaymentDescription(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('支付说明') }}</text>
<text class="info-value">{{ getApplicationPendingPaymentDescription(item) }}</text>
</view>
</view>
<view class="card-footer tn-margin-top-xl" @click.stop>
<view
v-if="canCancelOrder(item)"
class="cancel-btn"
:class="{ 'cancel-btn--loading': cancelingReferenceTypeNo === resolveReferenceTypeNo(item) }"
:data-reference-type-no="resolveReferenceTypeNo(item)"
@click.stop="cancelOrderAction"
>
<text class="tn-text-bold">{{ getCancelButtonText(item) }}</text>
</view>
<view
v-if="getCurrentStepActionText(item)"
class="pay-btn"
:data-reference-type-no="resolveReferenceTypeNo(item)"
:data-order-no="getReservationOrderNoFromItem(item)"
:data-ref-no="item.reservationCurrentNode && item.reservationCurrentNode.refNo ? item.reservationCurrentNode.refNo : ''"
:data-step="getReservationCurrentStepValue(item)"
@click.stop="handleCurrentStepAction"
>
<text class="tn-text-bold">{{ getCurrentStepActionText(item) }}</text>
</view>
<view
v-else-if="!isPaidApplication(item) && getApplicationPendingPaymentOrder(item)"
class="pay-btn"
:data-reference-type-no="resolveReferenceTypeNo(item)"
@click.stop="continuePendingPaymentAction"
>
<text class="tn-text-bold">{{ getPendingPaymentActionText(item) }}</text>
</view>
<view
v-else-if="canViewBill(item)"
class="pay-btn"
:data-reference-type-no="resolveReferenceTypeNo(item)"
@click.stop="payDepositAction"
>
<text class="tn-text-bold">{{ $tr('支付定金') }}</text>
</view>
</view>
</view>
<view v-if="hasMore || loadingMore" class="load-more-item tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center tn-padding-bottom">
<tn-loading v-if="loadingMore" mode="circle" color="#00BBE8"></tn-loading>
<text v-else-if="hasMore" class="tn-color-gray">{{ $tr('上拉加载更多') }}</text>
<text v-else class="tn-color-gray">{{ $tr('没有更多申请了') }}</text>
</view>
</scroll-view>
<view v-else-if="!loading" class="empty-panel tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center animation-fade-in tn-padding-left tn-padding-right">
<tn-empty icon="order" :text="$tr('暂无申请记录')"></tn-empty>
<view class="empty-desc tn-margin-top tn-text-center">{{ $tr('您还没有提交任何申请,去选房后可发起入住申请或预约看房。') }}</view>
<view class="empty-action tn-margin-top-xl" @click="goSelectRoom">
<text class="tn-text-bold">{{ $tr('去选房') }}</text>
</view>
</view>
<view v-if="loading && !list.length" class="empty-panel tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center tn-padding-left tn-padding-right">
<tn-loading mode="circle" color="#00BBE8" size="60"></tn-loading>
<view class="empty-desc tn-margin-top-lg">{{ $tr('正在同步您的入住申请记录,请稍候。') }}</view>
</view>
<!-- 支付确认弹窗 -->
<tn-popup v-model="showDepositNotice" mode="bottom" length="80%" :borderRadius="32" :closeBtn="true" @close="closeDepositNotice">
<view class="deposit-notice-popup">
<view class="popup-header">
<view class="popup-handle"></view>
<view class="popup-title">{{ getPaymentNoticeTitle() }}</view>
</view>
<view class="popup-body">
<view class="notice-content">
<view class="payment-sheet">
<view class="payment-hero">
<view class="payment-hero-label">{{ $tr('本次支付') }}</view>
<view class="payment-hero-amount">{{ getPaymentNoticeBillAmountDisplay() }}</view>
<view class="payment-hero-hint">{{ $tr('确认后将发起微信支付') }}</view>
</view>
<view v-if="paymentNoticeSourceLoading || paymentNoticeBillLoading" class="payment-sync-group">
<view v-if="paymentNoticeSourceLoading" class="payment-sync-chip">
{{ $tr('正在同步申请详情,请稍候...') }}
</view>
<view v-if="paymentNoticeBillLoading" class="payment-sync-chip">
{{ $tr('正在同步支付预览,请稍候...') }}
</view>
</view>
<view v-if="getPaymentNoticeDetailRows().length" class="payment-detail-panel">
<view class="payment-detail-header">
<view class="payment-detail-title">{{ $tr('入住申请信息') }}</view>
</view>
<view class="payment-detail-list">
<view
v-for="item in getPaymentNoticeDetailRows()"
:key="item.label"
class="payment-detail-row"
>
<text class="payment-detail-label">{{ item.label }}</text>
<text class="payment-detail-value">{{ item.value }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<view class="notice-action-btn" @click="confirmAndPayDeposit">
<text class="tn-text-bold">{{ getPaymentNoticeConfirmText() }}</text>
</view>
</view>
</view>
</tn-popup>
</view>
</template>
<script>
import { applicationFormDetail, applicationFormMePage, accountQueryBill, accountQueryBillPreview, accountQueryBills, contractDetail, orderSearchedMePage, orderSearchedReservationFlow, paymentBillPay, paymentPayingBills, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
import {
RESERVATION_FLOW_STEP,
attachReservationFlow,
buildReservationFlowMap,
getReservationCurrentActionText,
getReservationCurrentStepText,
getReservationStepMeta,
getReservationOrderNo,
parseReservationFlowResponse
} from '@/utils/reservation-flow'
import { getOrderStatusCode, normalizeBookingList, parseOrderListResponse } from '@/utils/booking-helper'
import { localizePayMethodText } from '@/utils/select-room-normalizers'
function formatPageResponse(response) {
const payload = response && response.data ? response.data : response
return payload && typeof payload === 'object' ? payload : {}
}
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 parseExpireTimeToTimestamp(value) {
if (value === null || typeof value === 'undefined' || value === '') {
return 0
}
if (typeof value === 'number' && Number.isFinite(value)) {
return value > 1e12 ? value : value * 1000
}
const text = String(value).trim()
if (!text) {
return 0
}
if (/^\d+$/.test(text)) {
const numericValue = Number(text)
if (!Number.isFinite(numericValue)) {
return 0
}
return numericValue > 1e12 ? numericValue : numericValue * 1000
}
const normalizedText = text.includes('T') ? text : text.replace(/-/g, '/')
const timestamp = Date.parse(normalizedText)
return Number.isFinite(timestamp) ? timestamp : 0
}
function parseJsonStringMaybe(value) {
if (typeof value !== 'string') {
return value
}
const text = value.trim()
if (!text) {
return value
}
if (!((text.startsWith('{') && text.endsWith('}')) || (text.startsWith('[') && text.endsWith(']')))) {
return value
}
try {
return JSON.parse(text)
} catch (error) {
return value
}
}
function padCountdownUnit(value) {
return String(Math.max(0, Number(value) || 0)).padStart(2, '0')
}
export default {
data() {
return {
currentIndex: 0,
status: 0,
pageIndex: 1,
pageSize: 10,
total: 0,
list: [],
loading: false,
loadingMore: false,
hasMore: true,
showDepositNotice: false,
pendingDepositReferenceTypeNo: '',
pendingDepositPaymentContext: null,
paymentNoticeSourceItem: null,
paymentNoticeSourceLoading: false,
paymentNoticeBillLoading: false,
paymentNoticeBillDetail: null,
reservationBillDetailMap: {},
reservationContractDetailMap: {},
reservationInspectionDetailMap: {},
reservationOrders: [],
activeReservationOrderNo: '',
countdownNow: Date.now(),
cancelingReferenceTypeNo: ''
}
},
watch: {
language() {
this.reloadList(true)
}
},
computed: {
language() {
return this.$getLanguage()
},
activeReservationOrder() {
const orderList = Array.isArray(this.reservationOrders) ? this.reservationOrders : []
if (!orderList.length) {
return null
}
return orderList.find(item => item.orderNo === this.activeReservationOrderNo) || orderList[0]
},
activeReservationFlowNodes() {
const order = this.activeReservationOrder
const nodes = order && order.reservationFlow && Array.isArray(order.reservationFlow.flowNodes)
? order.reservationFlow.flowNodes
: []
return [...nodes].sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
}
},
onShow() {
this.load()
},
mounted() {
this.startCountdownTimer()
},
beforeDestroy() {
this.clearCountdownTimer()
},
methods: {
load(force = false) {
return this.reloadList(force)
},
getReservationStepLabel(step) {
return getReservationStepMeta(step, this.language).label
},
switchReservationOrder(item = {}) {
const orderNo = String(item.orderNo || '').trim()
if (!orderNo) {
return
}
this.activeReservationOrderNo = orderNo
this.ensureReservationBillDetail(item)
this.ensureReservationInspectionDetail(item)
this.ensureReservationContractDetail(item)
},
getStatusText(status) {
const map = {
0: this.$tr('待审核'),
10: this.$tr('已通过'),
20: this.$tr('已拒绝'),
99: this.$tr('已结束')
}
return map[status] || this.$tr('处理中')
},
getDisplayStatusText(item = {}) {
if (this.$getLanguage() !== 'en' && item.statusDesc) {
return item.statusDesc
}
return this.getStatusText(item.status)
},
resolveStatusType(source = {}) {
const item = source && typeof source === 'object'
? source
: { status: source }
const status = Number(item.status)
if (status === 10) {
return 'approved'
}
if (status === 20) {
return 'rejected'
}
if (status === 99) {
return 'finished'
}
return 'pending'
},
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
},
getStatusReminderText(item = {}) {
if (this.isReservationContractAwaitingReview(item)) {
const contractDetail = this.getReservationContractDetail(item) || {}
return contractDetail.queryStatusDesc || contractDetail.approvalStatusDesc || contractDetail.statusDesc || getReservationCurrentStepText(item, this.language)
}
if (this.isReservationInspectionPendingReview(item)) {
return this.$tr('验房已完成,等待宿管审核')
}
const reservationStepText = getReservationCurrentStepText(item, this.language)
if (reservationStepText) {
return reservationStepText
}
if (this.shouldPayDeposit(item)) {
return this.$tr('请支付定金')
}
if (this.isWaitingRoomAssignment(item)) {
return this.$tr('等待预订中心分配房间')
}
return ''
},
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)
},
formatDateRange(start, end) {
if (!start && !end) {
return '--'
}
if (start && end) {
const startText = this.formatDateTime(start)
const endText = this.formatDateTime(end)
if (String(start).includes('T') || String(end).includes('T')) {
return `${startText}${this.$tr(' 至 ')}${endText}`
}
}
return `${this.formatDate(start)}${this.$tr(' 至 ')}${this.formatDate(end)}`
},
formatMoney(value) {
const amount = Number(value || 0)
return amount.toFixed(2)
},
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 []
},
normalizeBillDetail(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 {}
},
normalizeApplicationDetail(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 {}
},
async resolveApplicationDetailPaymentContext(referenceTypeNo = '') {
const normalizedReferenceTypeNo = String(referenceTypeNo || '').trim()
if (!normalizedReferenceTypeNo) {
return null
}
const detail = this.normalizeApplicationDetail(await applicationFormDetail(normalizedReferenceTypeNo, this.language))
if (!detail || !Object.keys(detail).length) {
return null
}
const normalizedDetail = {
...detail,
isViewingApplication: isViewingApplicationRecord(detail)
}
const detailBillNo = String(normalizedDetail.billNo || normalizedDetail.BillNo || '').trim()
const detailCode = String(normalizedDetail.detailCode || normalizedReferenceTypeNo).trim()
if (Array.isArray(this.list) && this.list.length) {
const nextList = this.list.map((item) => {
if (String(this.resolveReferenceTypeNo(item)) !== normalizedReferenceTypeNo) {
return item
}
return {
...item,
...normalizedDetail,
detailCode: detailCode || item.detailCode || normalizedReferenceTypeNo,
billNo: detailBillNo || item.billNo || ''
}
})
this.list = normalizeApplicationList(nextList)
saveMyApplicationList(this.list)
}
return {
referenceTypeNo: detailCode || normalizedReferenceTypeNo,
billNo: detailBillNo,
isViewingApplication: !!normalizedDetail.isViewingApplication,
detail: normalizedDetail
}
},
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 {}
},
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 {}
},
getBillPayRequests(bill = {}) {
return (Array.isArray(bill.payRequests) ? bill.payRequests : [])
.filter(item => item && typeof item === 'object')
.sort((prev, next) => new Date(next.createTime || 0).getTime() - new Date(prev.createTime || 0).getTime())
},
buildPendingPaymentOrderFromBillDetail(bill = {}) {
const payRequest = this.getBillPayRequests(bill).find((item) => {
return !item.paidTime
})
if (!payRequest) {
return null
}
return {
amount: Number(payRequest.amount || bill.remainingAmount || bill.totalCharge || 0),
expireTime: payRequest.expireTime || '',
payRequestNo: payRequest.no || '',
paymentNo: payRequest.no || '',
cashier: {
paymentInfo: payRequest.paymentInfo || '',
expireTime: payRequest.expireTime || ''
}
}
},
parsePaymentInfo(paymentInfo) {
if (!paymentInfo) {
return {}
}
if (typeof paymentInfo === 'object') {
return paymentInfo
}
const text = String(paymentInfo).trim()
if (!text) {
return {}
}
try {
return JSON.parse(text)
} catch (error) {
return {}
}
},
normalizeWechatPayOptions(paymentInfo) {
const source = this.parsePaymentInfo(paymentInfo)
return {
provider: 'wxpay',
timeStamp: String(source.timeStamp || source.timestamp || source.time_stamp || ''),
nonceStr: source.nonceStr || source.noncestr || source.nonce_str || '',
package: source.package || source.packageValue || source.package_value || '',
signType: source.signType || source.sign_type || 'MD5',
paySign: source.paySign || source.paysign || source.pay_sign || ''
}
},
requestWechatPayment(paymentInfo) {
const payOptions = this.normalizeWechatPayOptions(paymentInfo)
if (!payOptions.timeStamp || !payOptions.nonceStr || !payOptions.package || !payOptions.paySign) {
return Promise.reject(new Error(this.$tr('获取支付参数失败')))
}
return new Promise((resolve, reject) => {
uni.requestPayment({
...payOptions,
success: resolve,
fail: reject
})
})
},
getWechatLoginCode() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
if (!res || !res.code) {
reject(new Error(this.$tr('获取微信登录凭证失败')))
return
}
resolve(res.code)
},
fail: reject
})
})
},
normalizePayingBills(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 []
},
async getPendingPaymentOrder(billNo = '') {
const normalizedBillNo = String(billNo || '').trim()
if (!normalizedBillNo) {
return null
}
const response = await paymentPayingBills({
BillNo: normalizedBillNo
})
const orderList = this.normalizePayingBills(response)
return orderList.length ? orderList[0] : null
},
getApplicationPendingPaymentOrder(item = {}) {
return item && item.pendingPaymentOrder ? item.pendingPaymentOrder : null
},
getPendingPaymentOrderNo(order = {}) {
const cashier = order && order.cashier ? order.cashier : {}
return String(
order.payRequestNo
|| order.payNo
|| order.paymentNo
|| cashier.payRequestNo
|| cashier.payNo
|| cashier.paymentNo
|| ''
).trim()
},
getApplicationPendingPaymentOrderNo(item = {}) {
const order = this.getApplicationPendingPaymentOrder(item)
return order ? (this.getPendingPaymentOrderNo(order) || '--') : '--'
},
getApplicationPendingPaymentAmount(item = {}) {
const order = this.getApplicationPendingPaymentOrder(item)
const bill = item && item.pendingDepositBill ? item.pendingDepositBill : {}
if (order && (order.amount || order.amount === 0)) {
return order.amount
}
return bill.remainingAmount || bill.amount || 0
},
getApplicationPendingPaymentDescription(item = {}) {
const order = this.getApplicationPendingPaymentOrder(item)
return order ? (order.description || '') : ''
},
syncReservationPendingPaymentOrders(sourceList = []) {
const baseList = Array.isArray(sourceList) ? sourceList : []
const paymentContextMap = baseList.reduce((result, item) => {
const orderNo = String(getReservationOrderNo(item) || '').trim()
const pendingPaymentOrder = this.getApplicationPendingPaymentOrder(item)
const pendingDepositBill = item && item.pendingDepositBill ? item.pendingDepositBill : null
if (!orderNo || !pendingPaymentOrder) {
return result
}
result[orderNo] = {
pendingPaymentOrder,
pendingDepositBill
}
return result
}, {})
if (!Object.keys(paymentContextMap).length || !Array.isArray(this.reservationOrders) || !this.reservationOrders.length) {
return
}
this.reservationOrders = this.reservationOrders.map((item) => {
const orderNo = String(item && item.orderNo ? item.orderNo : '').trim()
const matchedContext = orderNo ? paymentContextMap[orderNo] : null
if (!matchedContext) {
return item
}
return {
...item,
pendingPaymentOrder: matchedContext.pendingPaymentOrder || null,
pendingDepositBill: matchedContext.pendingDepositBill || null
}
})
},
startCountdownTimer() {
if (this._countdownTimer) {
return
}
this._countdownTimer = setInterval(() => {
this.countdownNow = Date.now()
}, 1000)
},
clearCountdownTimer() {
if (this._countdownTimer) {
clearInterval(this._countdownTimer)
this._countdownTimer = null
}
},
getPendingPaymentExpireTimestamp(order = {}) {
return parseExpireTimeToTimestamp(order.expireTime || (order.cashier && order.cashier.expireTime) || '')
},
getPendingPaymentRemainingSeconds(order = {}) {
const expireTimestamp = this.getPendingPaymentExpireTimestamp(order)
if (!expireTimestamp) {
return 0
}
return Math.max(0, Math.ceil((expireTimestamp - this.countdownNow) / 1000))
},
formatPendingPaymentCountdown(seconds = 0) {
const totalSeconds = Math.max(0, Number(seconds) || 0)
const day = Math.floor(totalSeconds / 86400)
const hour = Math.floor((totalSeconds % 86400) / 3600)
const minute = Math.floor((totalSeconds % 3600) / 60)
const second = totalSeconds % 60
const timeText = `${padCountdownUnit(hour)}:${padCountdownUnit(minute)}:${padCountdownUnit(second)}`
return day > 0
? (this.language === 'en' ? `${day}d ${timeText}` : `${day}天 ${timeText}`)
: timeText
},
isApplicationPendingPaymentExpired(item = {}) {
const order = this.getApplicationPendingPaymentOrder(item)
if (!order) {
return false
}
const expireTimestamp = this.getPendingPaymentExpireTimestamp(order)
if (!expireTimestamp) {
return false
}
return expireTimestamp <= this.countdownNow
},
getApplicationPendingPaymentExpireText(item = {}) {
const order = this.getApplicationPendingPaymentOrder(item)
if (!order) {
return ''
}
const expireTimestamp = this.getPendingPaymentExpireTimestamp(order)
if (!expireTimestamp) {
return ''
}
if (expireTimestamp <= this.countdownNow) {
return this.$tr('已失效')
}
return this.formatPendingPaymentCountdown(this.getPendingPaymentRemainingSeconds(order))
},
getPendingPaymentActionText(item = {}) {
return this.isApplicationPendingPaymentExpired(item) ? this.$tr('支付定金') : this.$tr('继续支付')
},
getReservationOrderNoFromItem(item = {}) {
return getReservationOrderNo(item)
},
getReservationCurrentStepValue(item = {}) {
return Number(item && item.reservationCurrentNode ? item.reservationCurrentNode.step : 0)
},
getApplicationDetailCodeFromOrder(order = {}) {
const flowNodes = order && order.reservationFlow && Array.isArray(order.reservationFlow.flowNodes)
? order.reservationFlow.flowNodes
: []
const applicationNode = [...flowNodes]
.filter(item => item && typeof item === 'object' && Number(item.step || 0) === RESERVATION_FLOW_STEP.APPLICATION)
.sort((prev, next) => Number(prev.sort || 0) - Number(next.sort || 0))[0] || null
return String(
(applicationNode && (applicationNode.detailCode || applicationNode.code || applicationNode.refNo))
|| order.detailCode
|| ''
).trim()
},
isReservationBillStep(item = {}) {
return this.getReservationCurrentStepValue(item) === RESERVATION_FLOW_STEP.BILL
},
isReservationCheckinStep(item = {}) {
return this.getReservationCurrentStepValue(item) === RESERVATION_FLOW_STEP.CHECKIN
},
isReservationInspectionStep(item = {}) {
return this.getReservationCurrentStepValue(item) === RESERVATION_FLOW_STEP.INSPECTION
},
getReservationInspectionNo(order = {}) {
const currentNode = order && order.reservationCurrentNode ? order.reservationCurrentNode : {}
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.INSPECTION) {
return ''
}
return String(currentNode.refNo || '').trim()
},
getReservationContractNo(order = {}) {
const currentNode = order && order.reservationCurrentNode ? order.reservationCurrentNode : {}
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.CONTRACT) {
return ''
}
return String(currentNode.refNo || '').trim()
},
getReservationBillNo(order = {}) {
const currentNode = order && order.reservationCurrentNode ? order.reservationCurrentNode : {}
if (Number(currentNode.step || 0) !== RESERVATION_FLOW_STEP.BILL) {
return ''
}
return String(
currentNode.billNo
|| currentNode.BillNo
|| currentNode.accountBillNo
|| currentNode.paymentBillNo
|| currentNode.chargeBillNo
|| currentNode.refNo
|| order.billNo
|| ''
).trim()
},
getReservationBillDetailKey(order = {}) {
return this.getReservationBillNo(order)
},
getReservationContractDetail(order = {}) {
const contractNo = this.getReservationContractNo(order)
return contractNo ? (this.reservationContractDetailMap[contractNo] || null) : null
},
getReservationInspectionDetail(order = {}) {
const inspectionNo = this.getReservationInspectionNo(order)
return inspectionNo ? (this.reservationInspectionDetailMap[inspectionNo] || null) : null
},
isReservationContractAwaitingReview(order = {}) {
const detail = this.getReservationContractDetail(order)
if (!detail) {
return false
}
const queryStatus = Number(detail.queryStatus)
const approvalStatus = Number(detail.approvalStatus)
const hasSigned = !!String(detail.signTime || detail.signImageUrl || '').trim()
return queryStatus === 1 || (hasSigned && approvalStatus === 0)
},
isReservationInspectionPendingReview(order = {}) {
if (!this.isReservationInspectionStep(order)) {
return false
}
const detail = this.getReservationInspectionDetail(order)
if (!detail) {
return false
}
return Number(detail.status) === 2
},
getReservationCurrentStepHint(order = {}) {
if (this.isReservationInspectionPendingReview(order)) {
return this.$tr('验房已完成,等待宿管审核')
}
if (this.isReservationContractAwaitingReview(order)) {
const detail = this.getReservationContractDetail(order) || {}
return String(detail.queryStatusDesc || detail.approvalStatusDesc || '').trim() || this.$tr('合同已签,等待宿管审核')
}
if (this.isReservationCheckinProcessing(order)) {
return this.$tr('宿管正在办理入住,请耐心等待')
}
return ''
},
isReservationCheckinProcessing(order = {}) {
return this.isReservationCheckinStep(order)
},
hasReservationCurrentActionRef(item = {}) {
const currentNode = item && item.reservationCurrentNode ? item.reservationCurrentNode : {}
const step = Number(currentNode.step || 0)
if (!step) {
return false
}
if (step === RESERVATION_FLOW_STEP.BILL) {
return !!this.getReservationBillNo(item)
}
if (step === RESERVATION_FLOW_STEP.INSPECTION) {
if (this.isReservationInspectionPendingReview(item)) {
return false
}
return true
}
if (step === RESERVATION_FLOW_STEP.CONTRACT) {
return !!this.getReservationContractNo(item) && !this.isReservationContractAwaitingReview(item)
}
if (step === RESERVATION_FLOW_STEP.CHECKIN) {
return false
}
return true
},
getActiveReservationBillDetail() {
const order = this.activeReservationOrder
const detailKey = this.getReservationBillDetailKey(order)
return detailKey ? (this.reservationBillDetailMap[detailKey] || null) : null
},
getReservationBillDisplayTitle(bill = {}) {
return String(
bill.paymentFeeType
|| bill.remark
|| bill.createTypeDesc
|| bill.referenceTypeDesc
|| bill.billName
|| bill.name
|| this.$tr('支付定金')
).trim()
},
getReservationBillDisplayAmount(bill = {}) {
const amount = bill.remainingAmount
if (amount !== '' && amount !== null && amount !== undefined) {
return amount
}
return bill.totalCharge || bill.amount || 0
},
async ensureReservationBillDetail(order = {}) {
const billNo = this.getReservationBillNo(order)
if (!billNo || this.reservationBillDetailMap[billNo]) {
return this.reservationBillDetailMap[billNo] || null
}
try {
const detail = this.normalizeBillDetail(await accountQueryBill({
billNo
}, this.language))
if (detail && Object.keys(detail).length) {
this.$set(this.reservationBillDetailMap, billNo, detail)
return detail
}
} catch (error) {
console.log('[MyApplicationTab] load reservation bill detail failed', billNo, error)
}
return null
},
async ensureReservationInspectionDetail(order = {}) {
const inspectionNo = this.getReservationInspectionNo(order)
if (!inspectionNo || this.reservationInspectionDetailMap[inspectionNo]) {
return this.reservationInspectionDetailMap[inspectionNo] || null
}
try {
const detail = this.normalizeInspectionDetail(await roomInspectionDetail({
inspectionNo
}, this.language))
if (detail && Object.keys(detail).length) {
this.$set(this.reservationInspectionDetailMap, inspectionNo, detail)
return detail
}
} catch (error) {
console.log('[MyApplicationTab] load reservation inspection detail failed', inspectionNo, error)
}
return null
},
async ensureReservationContractDetail(order = {}) {
const contractNo = this.getReservationContractNo(order)
if (!contractNo || this.reservationContractDetailMap[contractNo]) {
return this.reservationContractDetailMap[contractNo] || null
}
try {
const detail = this.normalizeContractDetail(await contractDetail({
contractNo
}, this.language))
if (detail && Object.keys(detail).length) {
this.$set(this.reservationContractDetailMap, contractNo, detail)
return detail
}
} catch (error) {
console.log('[MyApplicationTab] load reservation contract detail failed', contractNo, error)
}
return null
},
async preloadReservationInspectionDetails(list = []) {
const inspectionNos = [...new Set(
(Array.isArray(list) ? list : [])
.map(item => this.getReservationInspectionNo(item))
.filter(Boolean)
)]
if (!inspectionNos.length) {
return
}
await Promise.all(inspectionNos.map((inspectionNo) => {
if (this.reservationInspectionDetailMap[inspectionNo]) {
return Promise.resolve()
}
return roomInspectionDetail({
inspectionNo
}, this.language)
.then((response) => {
const detail = this.normalizeInspectionDetail(response)
if (detail && Object.keys(detail).length) {
this.$set(this.reservationInspectionDetailMap, inspectionNo, detail)
}
})
.catch((error) => {
console.log('[MyApplicationTab] preload reservation inspection detail failed', inspectionNo, error)
})
}))
},
async preloadReservationContractDetails(list = []) {
const contractNos = [...new Set(
(Array.isArray(list) ? list : [])
.map(item => this.getReservationContractNo(item))
.filter(Boolean)
)]
if (!contractNos.length) {
return
}
await Promise.all(contractNos.map((contractNo) => {
if (this.reservationContractDetailMap[contractNo]) {
return Promise.resolve()
}
return contractDetail({
contractNo
}, this.language)
.then((response) => {
const detail = this.normalizeContractDetail(response)
if (detail && Object.keys(detail).length) {
this.$set(this.reservationContractDetailMap, contractNo, detail)
}
})
.catch((error) => {
console.log('[MyApplicationTab] preload reservation contract detail failed', contractNo, error)
})
}))
},
getCurrentStepActionText(item = {}) {
if (item.isViewingApplication) {
return ''
}
if (!this.hasReservationCurrentActionRef(item)) {
return ''
}
return getReservationCurrentActionText(item, this.language)
},
async loadReservationFlowsForList(list = []) {
const baseList = Array.isArray(list) ? list : []
const orderNos = [...new Set(
baseList
.filter(item => item && !item.isViewingApplication)
.map(item => getReservationOrderNo(item))
.filter(Boolean)
)]
const requestToken = Date.now()
this._reservationFlowRequestToken = requestToken
const applyFlowMap = (flowMap = {}) => {
if (this._reservationFlowRequestToken !== requestToken) {
return
}
this.list = (Array.isArray(this.list) ? this.list : []).map(item => attachReservationFlow(item, flowMap))
saveMyApplicationList(this.list)
}
if (!orderNos.length) {
applyFlowMap({})
return
}
try {
const response = await orderSearchedReservationFlow({
orderNos
}, this.language)
const flowMap = buildReservationFlowMap(parseReservationFlowResponse(response))
const nextList = (Array.isArray(this.list) ? this.list : []).map(item => attachReservationFlow(item, flowMap))
await Promise.all([
this.preloadReservationContractDetails(nextList),
this.preloadReservationInspectionDetails(nextList)
])
applyFlowMap(flowMap)
} catch (error) {
console.log('[MyApplicationTab] load reservation flow failed', error)
applyFlowMap({})
}
},
async fetchReservationOrders() {
try {
const response = await orderSearchedMePage({
PageIndex: 1,
PageSize: 100,
IsExport: false
}, this.language)
const orderList = normalizeBookingList(parseOrderListResponse(response), this.language)
const reservationOrderList = orderList.filter(item => getOrderStatusCode(item) === 0)
const orderNos = [...new Set(reservationOrderList.map(item => item.orderNo).filter(Boolean))]
if (!orderNos.length) {
this.reservationOrders = []
this.activeReservationOrderNo = ''
return
}
const flowResponse = await orderSearchedReservationFlow({
orderNos
}, this.language)
const flowMap = buildReservationFlowMap(parseReservationFlowResponse(flowResponse))
const reservationOrders = reservationOrderList
.map(item => attachReservationFlow(item, flowMap))
.filter(item => !!item.reservationCurrentNode)
await Promise.all([
this.preloadReservationContractDetails(reservationOrders),
this.preloadReservationInspectionDetails(reservationOrders)
])
this.reservationOrders = reservationOrders
this.syncReservationPendingPaymentOrders(this.list)
if (!reservationOrders.length) {
this.activeReservationOrderNo = ''
return
}
const existsActiveOrder = reservationOrders.some(item => item.orderNo === this.activeReservationOrderNo)
this.activeReservationOrderNo = existsActiveOrder
? this.activeReservationOrderNo
: reservationOrders[0].orderNo
this.ensureReservationBillDetail(this.activeReservationOrder)
this.ensureReservationInspectionDetail(this.activeReservationOrder)
this.ensureReservationContractDetail(this.activeReservationOrder)
} catch (error) {
console.log('[MyApplicationTab] fetch reservation orders failed', error)
this.reservationOrders = []
this.activeReservationOrderNo = ''
}
},
async loadPendingPaymentOrdersForList(list = []) {
const baseList = Array.isArray(list) ? list : []
const requestToken = Date.now()
this._pendingPaymentRequestToken = requestToken
await Promise.all(baseList.map(async (item, index) => {
if (!this.canViewBill(item)) {
return
}
const referenceTypeNo = this.resolveReferenceTypeNo(item)
if (!referenceTypeNo) {
return
}
try {
console.log('[MyApplicationTab] query bills for pending payment order', referenceTypeNo)
const billRes = await accountQueryBills({
referenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
})
const billList = this.normalizeBillList(billRes)
const bill = Array.isArray(billList) && billList.length ? billList[0] : null
if (!bill || this._pendingPaymentRequestToken !== requestToken) {
return
}
const billNo = this.getBillNo(bill)
if (!billNo) {
return
}
console.log('[MyApplicationTab] query payment/paying-bills', billNo)
const pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
if (this._pendingPaymentRequestToken !== requestToken) {
return
}
const currentItem = this.list[index]
if (!currentItem || this.getApplicationKey(currentItem) !== this.getApplicationKey(item)) {
return
}
const updatedItem = {
...currentItem,
pendingDepositBill: bill,
pendingPaymentOrder
}
this.$set(this.list, index, updatedItem)
} catch (error) {
console.log('[MyApplicationTab] load pending payment order failed', error)
}
}))
this.syncReservationPendingPaymentOrders(this.list)
},
async refreshStatus() {
if (this.loading || this.loadingMore) {
return
}
await this.reloadList(true)
if (this.total === 0 && !this.reservationOrders.length) {
this.$emit('refresh-shell-state')
uni.showToast({
title: this.$tr('未查到申请单,已同步预订单'),
icon: 'none'
})
return
}
if (this.list.length || this.reservationOrders.length) {
uni.showToast({
title: this.$tr('申请状态已刷新'),
icon: 'none'
})
}
},
async reloadList(force = false) {
const now = Date.now()
if (!force && this._lastReloadListAt && now - this._lastReloadListAt < 600) {
return Promise.resolve()
}
if (!force && this._reloadListPromise) {
return this._reloadListPromise
}
if (force) {
this.reservationBillDetailMap = {}
this.reservationContractDetailMap = {}
this.reservationInspectionDetailMap = {}
}
this._lastReloadListAt = now
this.pageIndex = 1
this.hasMore = true
this.currentIndex = 0
this._reloadListPromise = Promise.all([
this.fetchReservationOrders(),
this.fetchList(true)
]).finally(() => {
this._reloadListPromise = null
})
return this._reloadListPromise
},
onSwiperChange(e) {
this.currentIndex = e.detail.current
if (this.currentIndex >= this.list.length - 2 && this.hasMore) {
this.loadMore()
}
},
async loadMore() {
if (!this.hasMore || this.loading || this.loadingMore) {
return
}
this.pageIndex += 1
await this.fetchList(false)
},
async fetchList(isReload = false) {
if (isReload) {
this.loading = true
} else {
this.loadingMore = true
}
const params = {
PageIndex: this.pageIndex,
PageSize: this.pageSize
}
if (this.status !== '' && this.status !== null && typeof this.status !== 'undefined') {
params.Status = this.status
}
await applicationFormMePage(params, this.language)
.then(async (response) => {
const pageData = formatPageResponse(response)
console.log('pageData', pageData)
const nextList = normalizeApplicationList(pageData.data || [])
const total = Number(pageData.total || 0)
const mergedList = normalizeApplicationList(isReload ? nextList : this.list.concat(nextList))
.map(item => ({
...item,
pendingDepositBill: item && item.pendingDepositBill ? item.pendingDepositBill : null,
pendingPaymentOrder: item && item.pendingPaymentOrder ? item.pendingPaymentOrder : null
}))
this.total = total
this.list = mergedList
this.hasMore = this.list.length < total && nextList.length > 0
const activeCode = this.resolvePreferredReferenceTypeNo(this.list)
if (activeCode) {
saveActiveApplicationCode(activeCode)
}
await this.loadReservationFlowsForList(this.list)
await this.loadPendingPaymentOrdersForList(this.list)
saveMyApplicationList(this.list)
})
.catch((error) => {
if (!isReload) {
this.pageIndex = Math.max(1, this.pageIndex - 1)
}
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('申请记录加载失败'),
icon: 'none'
})
})
.finally(() => {
this.loading = false
this.loadingMore = false
})
},
changeStatus(value) {
if (this.status === value) {
return
}
this.status = value
this.reloadList()
},
getApplicationCode(item = {}) {
return item.detailCode || '--'
},
getApplicationKey(item = {}) {
return item.detailCode || item.formId || `${item.createTime || 'application'}-${item.phone || item.userName || 'unknown'}`
},
getApplicationTitle(item = {}) {
if (item.isViewingApplication) {
return this.$tr('预约看房申请')
}
return item.intendedRoomTypeName || this.$tr('入住申请')
},
resolveReferenceTypeNo(item = {}) {
return item.detailCode || ''
},
canViewBill(item = {}) {
return !item.isViewingApplication
&& (this.shouldPayDeposit(item) || this.isReservationBillStep(item))
&& this.resolveReferenceTypeNo(item)
},
canCancelApplication(item = {}) {
return !!(this.resolveReferenceTypeNo(item)
&& Number(item.status) === 0
&& (item.isViewingApplication || Number(item.payStatus) !== 10))
},
canCancelOrder(item = {}) {
return this.canCancelApplication(item)
},
getCancelButtonText(item = {}) {
return this.cancelingReferenceTypeNo === this.resolveReferenceTypeNo(item)
? this.$tr('取消中...')
: this.$tr('取消申请')
},
getCancelOrderContent(item = {}) {
return item.isViewingApplication
? this.$tr('取消后将关闭当前预约看房申请,是否继续?')
: this.$tr('取消后将关闭当前申请单,是否继续?')
},
confirmCancelOrder(item = {}) {
return new Promise((resolve) => {
uni.showModal({
title: this.$tr('取消申请'),
content: this.getCancelOrderContent(item),
confirmColor: '#ef4444',
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
})
})
},
syncCanceledItem(referenceTypeNo = '') {
const targetCode = String(referenceTypeNo || '')
if (!targetCode) {
return
}
const updateItem = (item = {}) => {
if (String(this.resolveReferenceTypeNo(item)) !== targetCode) {
return item
}
return {
...item,
status: 99,
statusDesc: this.$tr('已结束'),
pendingDepositBill: null,
pendingPaymentOrder: null
}
}
this.list = normalizeApplicationList(this.list.map(updateItem))
saveMyApplicationList(this.list)
const cachedList = normalizeApplicationList(getMyApplicationList())
if (cachedList.length) {
saveMyApplicationList(cachedList.map(updateItem))
}
},
cancelOrderAction(event) {
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
const referenceTypeNo = dataset.referenceTypeNo || dataset.referenceTypeNo === 0
? String(dataset.referenceTypeNo)
: ''
const currentItem = (Array.isArray(this.list) ? this.list : []).find((item) => {
return String(this.resolveReferenceTypeNo(item)) === referenceTypeNo
}) || {}
if (!referenceTypeNo || this.cancelingReferenceTypeNo) {
return
}
this.confirmCancelOrder(currentItem)
.then((confirmed) => {
if (!confirmed) {
return
}
this.cancelingReferenceTypeNo = referenceTypeNo
return userDetailCancel({
detailCode: referenceTypeNo
}, this.language)
.then(async () => {
this.syncCanceledItem(referenceTypeNo)
await this.reloadList(true)
this.$emit('refresh-shell-state')
uni.showToast({
title: this.$tr('申请已取消'),
icon: 'success'
})
})
})
.catch((error) => {
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
icon: 'none'
})
})
.finally(() => {
this.cancelingReferenceTypeNo = ''
})
},
continuePendingPaymentAction(event) {
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
const referenceTypeNo = dataset.referenceTypeNo || dataset.referenceTypeNo === 0
? String(dataset.referenceTypeNo)
: ''
if (!referenceTypeNo) {
uni.showToast({
title: this.$tr('申请单编号缺失'),
icon: 'none'
})
return
}
this.payDepositByReferenceTypeNo(referenceTypeNo)
},
resolvePreferredReferenceTypeNo(list = []) {
const sortedList = [...list]
.filter(item => !item.isViewingApplication && this.resolveReferenceTypeNo(item))
.sort((a, b) => new Date(b.createTime || 0).getTime() - new Date(a.createTime || 0).getTime())
if (!sortedList.length) {
return ''
}
const preferredItem = sortedList.find(item => Number(item.status) !== 99) || sortedList[0]
return this.resolveReferenceTypeNo(preferredItem)
},
async payDepositAction(event) {
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
const referenceTypeNo = dataset.referenceTypeNo || dataset.referenceTypeNo === 0
? String(dataset.referenceTypeNo)
: ''
if (!referenceTypeNo) {
uni.showToast({
title: this.$tr('申请单编号缺失'),
icon: 'none'
})
return
}
const currentItem = (Array.isArray(this.list) ? this.list : []).find((item) => {
return String(this.resolveReferenceTypeNo(item)) === referenceTypeNo
}) || {}
this.openDepositNoticeForPayment({
referenceTypeNo,
description: this.$tr('支付定金'),
mode: 'deposit',
sourceItem: currentItem
})
},
getPaymentContextMode(context = {}) {
const explicitMode = String(context.mode || '').trim()
if (explicitMode) {
return explicitMode
}
return context.billNo ? 'bill' : 'deposit'
},
isBillPaymentNotice() {
return this.getPaymentContextMode(this.pendingDepositPaymentContext || {}) === 'bill'
},
getPaymentNoticeTitle() {
return this.$tr('确认支付信息')
},
getPaymentNoticeConfirmText() {
return this.$tr('确认并支付')
},
getPaymentNoticeBillNo() {
const context = this.pendingDepositPaymentContext || {}
const detail = this.paymentNoticeBillDetail || {}
return String(this.getBillNo(detail) || context.billNo || '').trim()
},
getPaymentNoticeBillTitle() {
const detail = this.paymentNoticeBillDetail || {}
return String(
detail.description
|| detail.paymentFeeType
|| detail.remark
|| detail.createTypeDesc
|| detail.referenceTypeDesc
|| detail.billName
|| detail.name
|| (this.pendingDepositPaymentContext && this.pendingDepositPaymentContext.description)
|| this.$tr('账单支付')
).trim()
},
getPaymentNoticeBillAmount() {
const detail = this.paymentNoticeBillDetail || {}
const amount = detail.remainingAmount
if (amount !== '' && amount !== null && amount !== undefined) {
return amount
}
const source = this.paymentNoticeSourceItem || {}
const sourceAmount = source.remainingAmount
if (sourceAmount !== '' && sourceAmount !== null && sourceAmount !== undefined) {
return sourceAmount
}
const detailTotal = detail.totalCharge !== undefined && detail.totalCharge !== null && detail.totalCharge !== ''
? detail.totalCharge
: (detail.totalAmount !== undefined && detail.totalAmount !== null && detail.totalAmount !== '' ? detail.totalAmount : detail.amount)
if (detailTotal !== '' && detailTotal !== null && detailTotal !== undefined) {
return detailTotal
}
const sourceTotal = source.totalCharge !== undefined && source.totalCharge !== null && source.totalCharge !== ''
? source.totalCharge
: (source.totalAmount !== undefined && source.totalAmount !== null && source.totalAmount !== '' ? source.totalAmount : source.amount)
if (sourceTotal !== '' && sourceTotal !== null && sourceTotal !== undefined) {
return sourceTotal
}
return ''
},
getPaymentNoticeBillAmountDisplay() {
const amount = this.getPaymentNoticeBillAmount()
if (amount === '' || amount === null || amount === undefined) {
return '--'
}
return `¥${this.formatMoney(amount)}`
},
buildPaymentNoticePayMethod(source = {}) {
const pricePlanDetail = source && source.pricePlanDetail && typeof source.pricePlanDetail === 'object'
? source.pricePlanDetail
: {}
const payMethodText = String(
source.depositPrepayDesc
|| source.depositAndPrepayDesc
|| source.depositPrepayText
|| source.payMethod
|| source.payMethodDesc
|| source.paymentMethod
|| source.paymentMethodDesc
|| pricePlanDetail.payMethod
|| ''
).trim()
if (payMethodText) {
return localizePayMethodText(payMethodText, this.language)
}
const deposit = pricePlanDetail.deposit
const prepay = pricePlanDetail.prepay
if (deposit !== '' && deposit !== null && deposit !== undefined && prepay !== '' && prepay !== null && prepay !== undefined) {
return localizePayMethodText(`押${deposit}付${prepay}`, this.language)
}
return ''
},
getPaymentNoticeDetailRows() {
const source = this.paymentNoticeSourceItem || {}
const formatShortDate = (value) => {
if (!value) {
return ''
}
return String(value).replace('T', ' ').slice(0, 10)
}
const residentName = String(
source.userName
|| source.name
|| ''
).trim()
const areaName = String(
source.areaText
|| source.intendedAreaName
|| source.areaName
|| source.campusName
|| ''
).trim()
const roomTypeName = String(
source.roomTypeText
|| source.roomTypeName
|| source.intendedRoomTypeName
|| source.roomTypeCode
|| source.roomName
|| ''
).trim()
const roomNo = String(
source.roomNoText
|| source.roomNo
|| source.roomLabel
|| ''
).trim()
const moveInDate = formatShortDate(
source.startDateText
|| source.expectedCheckInTime
|| source.startDate
|| source.start
|| source.checkinDate
|| ''
)
const leaseStart = formatShortDate(
source.startDateText
|| source.expectedCheckInTime
|| source.startDate
|| source.start
|| source.checkinDate
|| ''
)
const leaseEnd = formatShortDate(
source.endDateText
|| source.expectedCheckOutTime
|| source.endDate
|| source.end
|| source.checkoutDate
|| ''
)
const leasePeriod = leaseStart && leaseEnd
? `${leaseStart}${this.$tr(' 至 ')}${leaseEnd}`
: ''
const floorLevel = String(
source.intendedFloorLevelDesc
|| source.floorLevelDesc
|| ''
).trim()
const payMethod = this.buildPaymentNoticePayMethod(source)
return [
residentName ? { label: this.$tr('入住人'), value: residentName } : null,
areaName ? { label: this.$tr('所属公寓'), value: areaName } : null,
roomTypeName ? { label: this.$tr('房型'), value: roomTypeName } : null,
roomNo ? { label: this.$tr('房间号'), value: roomNo } : null,
floorLevel ? { label: this.$tr('意向楼层'), value: floorLevel } : null,
moveInDate ? { label: this.$tr('预计入住'), value: moveInDate } : null,
leasePeriod ? { label: this.$tr('租期'), value: leasePeriod } : null,
payMethod ? { label: this.$tr('押付方式'), value: payMethod } : null,
].filter(Boolean)
},
normalizePaymentNoticePreview(response, orderNo = '') {
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
const normalizeList = (list = []) => list.filter(item => item && typeof item === 'object')
const normalizedOrderNo = String(orderNo || '').trim()
let candidates = []
if (Array.isArray(payload)) {
candidates = normalizeList(payload)
} else if (payload && Array.isArray(payload.data)) {
candidates = normalizeList(payload.data)
} else if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
candidates = [payload.data]
} else if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
candidates = [payload]
}
if (!candidates.length) {
return null
}
return candidates.find(item => String(item.orderNo || '').trim() === normalizedOrderNo) || candidates[0]
},
resolvePaymentNoticeOrderNo(context = {}, sourceItem = null) {
return String(
context.orderNo
|| (sourceItem && (sourceItem.orderNo || sourceItem.reservationOrderNo || getReservationOrderNo(sourceItem)))
|| (this.activeReservationOrder && this.activeReservationOrder.orderNo)
|| ''
).trim()
},
loadPaymentNoticeSourceDetail(referenceTypeNo = '', fallbackSource = null) {
const fallbackReferenceTypeNo = fallbackSource ? this.getApplicationDetailCodeFromOrder(fallbackSource) : ''
const normalizedReferenceTypeNo = String(referenceTypeNo || fallbackReferenceTypeNo || '').trim()
if (!normalizedReferenceTypeNo) {
this.paymentNoticeSourceLoading = false
this.paymentNoticeSourceItem = fallbackSource || this.paymentNoticeSourceItem || null
return Promise.resolve(this.paymentNoticeSourceItem)
}
this.paymentNoticeSourceLoading = true
return this.resolveApplicationDetailPaymentContext(normalizedReferenceTypeNo)
.then((context) => {
const detail = context && context.detail ? context.detail : null
this.paymentNoticeSourceItem = detail
? {
...(fallbackSource || {}),
...detail
}
: (fallbackSource || this.paymentNoticeSourceItem || null)
return this.paymentNoticeSourceItem
})
.catch(() => {
this.paymentNoticeSourceItem = fallbackSource || this.paymentNoticeSourceItem || null
return this.paymentNoticeSourceItem
})
.finally(() => {
this.paymentNoticeSourceLoading = false
})
},
loadPaymentNoticeBillDetail(orderNo = '') {
const normalizedOrderNo = String(orderNo || '').trim()
if (!normalizedOrderNo) {
this.paymentNoticeBillLoading = false
this.paymentNoticeBillDetail = null
return Promise.resolve(null)
}
const cacheKey = `preview:${normalizedOrderNo}`
const cached = this.reservationBillDetailMap[cacheKey] || null
if (cached) {
this.paymentNoticeBillLoading = false
this.paymentNoticeBillDetail = cached
return Promise.resolve(cached)
}
this.paymentNoticeBillLoading = true
this.paymentNoticeBillDetail = null
return accountQueryBillPreview({
OrderNo: normalizedOrderNo
}, this.language)
.then((response) => {
const detail = this.normalizePaymentNoticePreview(response, normalizedOrderNo)
const normalizedDetail = detail && Object.keys(detail).length ? detail : null
if (normalizedDetail) {
this.$set(this.reservationBillDetailMap, cacheKey, normalizedDetail)
}
this.paymentNoticeBillDetail = normalizedDetail
return normalizedDetail
})
.catch(() => {
this.paymentNoticeBillDetail = null
return null
})
.finally(() => {
this.paymentNoticeBillLoading = false
})
},
openDepositNoticeForPayment(context = {}) {
const paymentSourceItem = context.sourceItem || this.activeReservationOrder || null
const paymentReferenceTypeNo = String(
context.referenceTypeNo
|| (paymentSourceItem ? this.getApplicationDetailCodeFromOrder(paymentSourceItem) : '')
|| ''
).trim()
const paymentOrderNo = this.resolvePaymentNoticeOrderNo(context, paymentSourceItem)
this.pendingDepositPaymentContext = {
referenceTypeNo: paymentReferenceTypeNo,
orderNo: paymentOrderNo,
billNo: String(context.billNo || '').trim(),
paymentNo: String(context.paymentNo || '').trim(),
description: String(context.description || '').trim(),
mode: this.getPaymentContextMode(context)
}
this.paymentNoticeSourceItem = paymentSourceItem
this.pendingDepositReferenceTypeNo = this.pendingDepositPaymentContext.referenceTypeNo
this.loadPaymentNoticeBillDetail(paymentOrderNo)
.then((preview) => {
const mergedSource = preview
? {
...(paymentSourceItem || {}),
...preview
}
: paymentSourceItem
this.paymentNoticeSourceItem = mergedSource
const nextReferenceTypeNo = String(
(preview && (preview.applicationDetailCode || preview.detailCode))
|| this.pendingDepositPaymentContext.referenceTypeNo
|| ''
).trim()
this.pendingDepositPaymentContext.referenceTypeNo = nextReferenceTypeNo
this.pendingDepositReferenceTypeNo = nextReferenceTypeNo
return this.loadPaymentNoticeSourceDetail(nextReferenceTypeNo, mergedSource)
})
.catch(() => {
this.loadPaymentNoticeSourceDetail(this.pendingDepositPaymentContext.referenceTypeNo, this.paymentNoticeSourceItem)
})
this.showDepositNotice = true
},
closeDepositNotice() {
this.showDepositNotice = false
this.pendingDepositReferenceTypeNo = ''
this.pendingDepositPaymentContext = null
this.paymentNoticeSourceItem = null
this.paymentNoticeSourceLoading = false
this.paymentNoticeBillLoading = false
this.paymentNoticeBillDetail = null
},
resolveReservationBillPaymentContext(order = {}, fallbackContext = {}) {
const currentNode = order && order.reservationCurrentNode ? order.reservationCurrentNode : {}
const isBillStep = Number(currentNode.step || 0) === RESERVATION_FLOW_STEP.BILL
const candidateReferenceTypeNos = [
fallbackContext.referenceTypeNo,
this.getApplicationDetailCodeFromOrder(order),
currentNode.referenceTypeNo,
currentNode.detailCode,
currentNode.code,
order.referenceTypeNo,
order.detailCode,
order.code
]
const candidateBillNos = [
fallbackContext.billNo,
currentNode.billNo,
currentNode.BillNo,
currentNode.accountBillNo,
currentNode.paymentBillNo,
currentNode.chargeBillNo,
isBillStep ? currentNode.refNo : '',
order.billNo,
this.getBillNo(this.getActiveReservationBillDetail() || {})
]
const candidatePaymentNos = [
fallbackContext.paymentNo,
currentNode.paymentNo,
currentNode.payNo,
currentNode.payRequestNo,
currentNode.cashierNo
]
const pickFirst = (list = []) => {
for (let index = 0; index < list.length; index += 1) {
const value = String(list[index] || '').trim()
if (value) {
return value
}
}
return ''
}
return {
referenceTypeNo: pickFirst(candidateReferenceTypeNos),
billNo: pickFirst(candidateBillNos),
paymentNo: pickFirst(candidatePaymentNos),
description: String(currentNode.stepDesc || fallbackContext.description || '').trim() || this.$tr('账单支付'),
mode: isBillStep ? 'bill' : this.getPaymentContextMode(fallbackContext),
sourceItem: order
}
},
resolvePendingPaymentContextByPaymentNo(paymentNo = '') {
const normalizedPaymentNo = String(paymentNo || '').trim()
if (!normalizedPaymentNo) {
return null
}
const matchedItem = (Array.isArray(this.list) ? this.list : []).find((item) => {
const pendingOrder = this.getApplicationPendingPaymentOrder(item)
return pendingOrder && this.getPendingPaymentOrderNo(pendingOrder) === normalizedPaymentNo
})
if (!matchedItem) {
return null
}
return {
referenceTypeNo: String(this.resolveReferenceTypeNo(matchedItem) || '').trim(),
billNo: String(this.getBillNo(matchedItem.pendingDepositBill || {}) || '').trim(),
pendingPaymentOrder: this.getApplicationPendingPaymentOrder(matchedItem),
bill: matchedItem.pendingDepositBill || null
}
},
resolveReservationPaymentContextByPaymentNo(paymentNo = '') {
const normalizedPaymentNo = String(paymentNo || '').trim()
if (!normalizedPaymentNo) {
return null
}
const matchedOrder = (Array.isArray(this.reservationOrders) ? this.reservationOrders : []).find((item) => {
const pendingOrder = item && item.pendingPaymentOrder ? item.pendingPaymentOrder : null
return pendingOrder && this.getPendingPaymentOrderNo(pendingOrder) === normalizedPaymentNo
})
if (!matchedOrder) {
return null
}
return {
referenceTypeNo: String(this.resolveReferenceTypeNo(matchedOrder) || '').trim(),
billNo: String(this.getBillNo(matchedOrder.pendingDepositBill || {}) || '').trim(),
pendingPaymentOrder: matchedOrder.pendingPaymentOrder || null,
bill: matchedOrder.pendingDepositBill || null
}
},
async payDepositByReferenceTypeNo(referenceTypeNo = '') {
return this.payByPaymentContext({
referenceTypeNo
})
},
async payByPaymentContext(context = {}) {
const paymentMode = this.getPaymentContextMode(context)
let referenceTypeNo = String(context.referenceTypeNo || '').trim()
let billNo = String(context.billNo || '').trim()
const paymentNo = String(context.paymentNo || '').trim()
const description = String(context.description || '').trim() || (paymentMode === 'bill' ? this.$tr('账单支付') : this.$tr('支付定金'))
const emptyBillMessage = paymentMode === 'bill'
? this.$tr('暂无待支付账单')
: this.$tr('暂无待支付定金账单')
const missingContextMessage = paymentMode === 'bill'
? this.$tr('账单编号缺失')
: this.$tr('申请单编号缺失')
if (!referenceTypeNo && !billNo && !paymentNo) {
uni.showToast({
title: missingContextMessage,
icon: 'none'
})
return
}
if (referenceTypeNo) {
saveActiveApplicationCode(referenceTypeNo)
}
uni.showLoading({ title: this.$tr('正在获取账单...') })
try {
let bill = null
if (referenceTypeNo && !billNo) {
const detailPaymentContext = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
if (detailPaymentContext) {
if (detailPaymentContext.isViewingApplication) {
uni.showToast({
title: this.$tr('预约看房申请无需支付定金'),
icon: 'none'
})
return
}
referenceTypeNo = detailPaymentContext.referenceTypeNo || referenceTypeNo
billNo = detailPaymentContext.billNo || billNo
}
}
let pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
if (!pendingPaymentOrder && paymentNo) {
const matchedContext = this.resolvePendingPaymentContextByPaymentNo(paymentNo) || this.resolveReservationPaymentContextByPaymentNo(paymentNo)
if (matchedContext) {
referenceTypeNo = referenceTypeNo || matchedContext.referenceTypeNo
billNo = billNo || matchedContext.billNo
pendingPaymentOrder = matchedContext.pendingPaymentOrder || null
bill = matchedContext.bill || null
}
}
if (referenceTypeNo && !billNo) {
const detailPaymentContext = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
if (detailPaymentContext) {
if (detailPaymentContext.isViewingApplication) {
uni.showToast({
title: this.$tr('预约看房申请无需支付定金'),
icon: 'none'
})
return
}
referenceTypeNo = detailPaymentContext.referenceTypeNo || referenceTypeNo
billNo = detailPaymentContext.billNo || billNo
}
if (!pendingPaymentOrder && billNo) {
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
}
}
if (billNo && !bill) {
const billDetail = this.normalizeBillDetail(await accountQueryBill({
billNo
}, this.language))
if (billDetail && Object.keys(billDetail).length) {
bill = billDetail
billNo = this.getBillNo(bill) || billNo
this.$set(this.reservationBillDetailMap, billNo, billDetail)
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
if (!pendingPaymentOrder) {
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
}
}
}
const shouldQueryBillListByReference = !!referenceTypeNo && !(paymentMode === 'bill' && billNo)
if (shouldQueryBillListByReference) {
const billRes = await accountQueryBills({
referenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
})
const billList = this.normalizeBillList(billRes)
if (!Array.isArray(billList) || !billList.length) {
if (pendingPaymentOrder && billNo) {
bill = {
billNo,
no: billNo,
referenceTypeNo,
remainingAmount: Number(pendingPaymentOrder.amount || 0),
amount: Number(pendingPaymentOrder.amount || 0),
billName: description
}
} else {
uni.showToast({ title: emptyBillMessage, icon: 'none' })
return
}
} else {
bill = billNo
? (billList.find(item => String(this.getBillNo(item) || '').trim() === billNo) || null)
: null
if (!bill) {
bill = billList[0]
}
billNo = this.getBillNo(bill)
referenceTypeNo = referenceTypeNo || String(bill.referenceTypeNo || '').trim()
if (!pendingPaymentOrder && billNo) {
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
}
if (!pendingPaymentOrder) {
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(bill)
}
}
}
if (!billNo) {
uni.showToast({ title: emptyBillMessage, icon: 'none' })
return
}
uni.showLoading({ title: this.$tr('正在拉起支付...') })
const reusablePendingPaymentOrder = pendingPaymentOrder && !this.isApplicationPendingPaymentExpired({
pendingPaymentOrder
})
? pendingPaymentOrder
: null
if (reusablePendingPaymentOrder && reusablePendingPaymentOrder.cashier && reusablePendingPaymentOrder.cashier.paymentInfo) {
await this.requestWechatPayment(reusablePendingPaymentOrder.cashier.paymentInfo)
} else {
if (!referenceTypeNo || !bill) {
throw new Error(this.$tr('获取支付参数失败'))
}
const wechatCode = await this.getWechatLoginCode()
const payRes = await paymentBillPay({
referenceTypeNo,
billNo,
payMethod: 'Wechat',
amount: Number((bill && (bill.remainingAmount || bill.amount)) || 0),
description: (bill && (bill.billName || bill.name)) || description,
ExtraInfo: wechatCode
})
const payload = payRes && payRes.data ? payRes.data : payRes
const cashier = payload && payload.cashier ? payload.cashier : {}
await this.requestWechatPayment(cashier.paymentInfo)
}
uni.showToast({ title: this.$tr('支付成功'), icon: 'success' })
} catch (error) {
const errMsg = error && (error.errMsg || error.message || error.msg) ? (error.errMsg || error.message || error.msg) : this.$tr('支付失败')
if (/cancel/i.test(errMsg)) {
uni.showToast({ title: this.$tr('已取消支付'), icon: 'none' })
} else {
uni.showToast({ title: errMsg, icon: 'none' })
}
} finally {
uni.hideLoading()
setTimeout(() => {
this.refreshStatus()
}, 1500)
}
},
async confirmAndPayDeposit() {
const paymentContext = this.pendingDepositPaymentContext || {
referenceTypeNo: this.pendingDepositReferenceTypeNo
}
this.showDepositNotice = false
this.pendingDepositReferenceTypeNo = ''
this.pendingDepositPaymentContext = null
return this.payByPaymentContext(paymentContext)
},
handleCurrentStepAction(event) {
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
const step = Number(dataset.step || 0)
const referenceTypeNo = dataset.referenceTypeNo || dataset.referenceTypeNo === 0
? String(dataset.referenceTypeNo)
: ''
const contractNo = String(dataset.refNo || '').trim()
const currentItem = (Array.isArray(this.list) ? this.list : []).find(item => {
return String(this.resolveReferenceTypeNo(item)) === referenceTypeNo
})
if (step === RESERVATION_FLOW_STEP.BILL) {
if (!referenceTypeNo) {
uni.showToast({
title: this.$tr('申请单编号缺失'),
icon: 'none'
})
return
}
const paymentContext = this.resolveReservationBillPaymentContext(currentItem || {}, {
referenceTypeNo
})
this.openDepositNoticeForPayment(paymentContext)
return
}
if (step === RESERVATION_FLOW_STEP.CONTRACT) {
if (this.isReservationContractAwaitingReview(currentItem || {})) {
return
}
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(currentItem || {})) {
return
}
const orderNo = String(dataset.orderNo || '').trim()
uni.navigateTo({
url: orderNo
? `/pkgService/room_inspection/room_inspection?orderNo=${encodeURIComponent(orderNo)}`
: '/pkgService/room_inspection/room_inspection'
})
}
},
handleReservationOrderStepAction(order = {}) {
const step = this.getReservationCurrentStepValue(order)
if (step === RESERVATION_FLOW_STEP.BILL) {
const paymentContext = this.resolveReservationBillPaymentContext(order)
this.openDepositNoticeForPayment(paymentContext)
return
}
if (step === RESERVATION_FLOW_STEP.CONTRACT) {
if (this.isReservationContractAwaitingReview(order)) {
return
}
const contractNo = String(order && order.reservationCurrentNode && order.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(order)) {
return
}
const orderNo = getReservationOrderNo(order)
uni.navigateTo({
url: orderNo
? `/pkgService/room_inspection/room_inspection?orderNo=${encodeURIComponent(orderNo)}`
: '/pkgService/room_inspection/room_inspection'
})
}
},
goToApplicationDetail(item = {}) {
const code = item.detailCode || ''
if (!code) {
return
}
uni.navigateTo({
url: `/pkgService/application_detail/application_detail?code=${encodeURIComponent(code)}&payload=${encodeURIComponent(JSON.stringify(item))}`
})
},
goSelectRoom() {
uni.reLaunch({
url: '/pages/index/index?index=0&pageId=select-room'
})
}
}
}
</script>
<style scoped lang="scss">
.my-application-page {
display: flex;
flex-direction: column;
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
overflow: hidden;
}
.page-header {
flex-shrink: 0;
padding-bottom: 20rpx;
}
.page-title {
font-size: 44rpx;
font-weight: 800;
letter-spacing: 1rpx;
}
.list-scroll-view {
flex: 1;
height: 0;
}
.application-card,
.empty-panel {
background: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
border: 1rpx solid rgba($brand-primary, 0.05);
}
.reservation-flow-panel {
background: #ffffff;
border-radius: 32rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02), 0 0 2rpx rgba(0, 0, 0, 0.05);
border: none;
}
.reservation-flow-panel__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 24rpx;
}
.reservation-flow-panel__title-wrap {
flex: 1;
min-width: 0;
}
.reservation-flow-panel__title {
font-size: 32rpx;
font-weight: 800;
color: #0F172A;
letter-spacing: 0.5rpx;
}
.reservation-flow-panel__desc {
margin-top: 6rpx;
font-size: 22rpx;
color: #64748B;
}
.reservation-flow-panel__refresh {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 132rpx;
height: 56rpx;
padding: 0 20rpx;
border-radius: 16rpx;
background: rgba($brand-primary, 0.08);
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.16);
font-size: 22rpx;
}
.reservation-order-scroll {
width: 100%;
white-space: nowrap;
margin-bottom: 24rpx;
}
.reservation-order-tabs {
display: inline-flex;
gap: 16rpx;
}
.reservation-order-chip {
padding: 16rpx 24rpx;
border-radius: 16rpx;
background: #F8FAFC;
border: 1rpx solid #E2E8F0;
transition: all 0.2s ease;
min-width: 200rpx;
}
.reservation-order-chip--active {
background: rgba($brand-primary, 0.08);
border-color: rgba($brand-primary, 0.3);
}
.reservation-order-chip__title {
font-size: 24rpx;
font-weight: 600;
color: #334155;
}
.reservation-order-chip--active .reservation-order-chip__title {
color: $brand-primary;
}
.reservation-grid-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 24rpx;
background: #F8FAFC;
border-radius: 16rpx;
}
.grid-info-item {
display: flex;
flex-direction: column;
gap: 6rpx;
}
.grid-info-label {
font-size: 20rpx;
color: #64748B;
letter-spacing: 0.5rpx;
}
.grid-info-value {
font-size: 24rpx;
font-weight: 600;
color: #0F172A;
word-break: break-all;
}
.tn-font-mono {
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
}
.reservation-v-steps {
display: flex;
flex-direction: column;
padding-left: 8rpx;
}
.v-step-item {
display: flex;
position: relative;
min-height: 100rpx;
}
.v-step-indicator {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 24rpx;
width: 48rpx;
}
.v-step-badge {
width: 48rpx;
height: 48rpx;
border-radius: 16rpx;
background: #F1F5F9;
color: #64748B;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
font-weight: 700;
z-index: 2;
}
.v-step-item--current .v-step-badge {
background: $brand-primary;
color: #FFFFFF;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.2);
}
.v-step-line {
flex: 1;
width: 2rpx;
background: #E2E8F0;
margin-top: 8rpx;
margin-bottom: 8rpx;
}
.v-step-content {
flex: 1;
padding-bottom: 40rpx;
padding-top: 4rpx;
}
.v-step-item--last .v-step-content {
padding-bottom: 0;
}
.v-step-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.v-step-title {
font-size: 28rpx;
font-weight: 600;
color: #334155;
}
.v-step-item--current .v-step-title {
color: $brand-primary;
font-weight: 700;
}
.v-step-bill-card {
padding: 18rpx 20rpx;
background: rgba($brand-primary, 0.06);
border: 1rpx solid rgba($brand-primary, 0.14);
border-radius: 16rpx;
}
.v-step-bill-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.v-step-bill-row + .v-step-bill-row {
margin-top: 12rpx;
}
.v-step-bill-label {
flex-shrink: 0;
font-size: 22rpx;
color: #64748B;
}
.v-step-bill-value {
font-size: 24rpx;
color: #334155;
text-align: right;
word-break: break-all;
}
.v-step-bill-value--price {
color: $brand-primary;
font-weight: 700;
}
.v-step-hint-card {
padding: 16rpx 20rpx;
background: rgba($brand-primary, 0.06);
border-radius: 16rpx;
}
.v-step-hint-text {
font-size: 22rpx;
line-height: 1.6;
color: #64748B;
}
.v-step-current-tag {
padding: 4rpx 16rpx;
background: #FEF3C7;
color: #C2410C;
font-size: 20rpx;
font-weight: 700;
border-radius: 8rpx;
}
.step-pay-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 32rpx;
height: 64rpx;
background: $brand-primary;
color: #FFFFFF;
border-radius: 16rpx;
font-size: 24rpx;
font-weight: 600;
transition: opacity 0.2s;
}
.step-pay-btn:active {
opacity: 0.8;
}
.step-refresh-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 32rpx;
height: 64rpx;
background: rgba($brand-primary, 0.08);
color: $brand-primary;
border: 1rpx solid rgba($brand-primary, 0.18);
border-radius: 16rpx;
font-size: 24rpx;
font-weight: 600;
}
.step-refresh-btn:active {
opacity: 0.8;
}
.card-title-wrap {
display: flex;
align-items: center;
}
.title-icon {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
background: rgba($brand-primary, 0.08);
color: $brand-primary;
font-size: 28rpx;
}
.card-title {
font-size: 32rpx;
font-weight: 700;
color: #1A2233;
}
.card-code,
.info-label,
.remark-label {
color: #6A7A99;
}
.card-code,
.info-label,
.info-value,
.remark-text {
font-size: 24rpx;
}
.status-badge {
padding: 8rpx 20rpx;
border-radius: 999rpx;
font-size: 22rpx;
font-weight: 700;
}
.status-pending {
color: #b45309;
background: rgba(245, 158, 11, 0.12);
}
.status-approved {
color: $brand-primary;
background: rgba($brand-primary, 0.12);
}
.status-rejected {
color: #DC2626;
background: rgba(220, 38, 38, 0.12);
}
.status-finished {
color: #64748B;
background: #F1F5F9;
}
.card-body {
display: flex;
flex-direction: column;
gap: 0;
}
.info-row {
padding: 16rpx 0;
border-bottom: 1px dashed rgba($brand-primary, 0.15);
gap: 24rpx;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
flex: 0 0 170rpx;
color: #6A7A99;
}
.info-value {
flex: 1;
color: #1A2233;
text-align: right;
word-break: break-all;
font-weight: 500;
}
.info-value--highlight {
color: #c2410c;
font-weight: 700;
}
.info-value--expired {
color: #dc2626;
font-weight: 700;
}
.remark-panel,
.reminder-panel {
padding: 24rpx;
border-radius: 20rpx;
background: rgba($brand-primary, 0.02);
border: 1rpx solid rgba($brand-primary, 0.1);
}
.pending-payment-panel {
padding: 24rpx;
border-radius: 20rpx;
background: linear-gradient(180deg, rgba(255, 247, 237, 0.9) 0%, rgba(255, 251, 245, 0.95) 100%);
border: 1rpx solid rgba(249, 115, 22, 0.16);
}
.pending-payment-title,
.reminder-title {
margin-bottom: 12rpx;
font-size: 26rpx;
font-weight: 700;
color: #c2410c;
}
.reminder-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 12rpx;
}
.reminder-title {
margin-bottom: 0;
color: $brand-primary;
}
.reminder-refresh-btn {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 112rpx;
height: 52rpx;
padding: 0 18rpx;
border-radius: 14rpx;
background: rgba($brand-primary, 0.08);
border: 1rpx solid rgba($brand-primary, 0.16);
color: $brand-primary;
font-size: 22rpx;
}
.remark-line {
display: flex;
align-items: flex-start;
gap: 16rpx;
}
.remark-label {
flex: 0 0 96rpx;
font-size: 22rpx;
color: #6A7A99;
}
.remark-text {
flex: 1;
color: #1A2233;
line-height: 1.7;
}
.card-footer {
display: flex;
gap: 20rpx;
justify-content: flex-end;
}
.cancel-btn,
.pay-btn {
min-width: 220rpx;
height: 84rpx;
padding: 0 36rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
color: #ffffff;
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.25);
font-size: 26rpx;
white-space: nowrap;
flex-shrink: 0;
transition: all 0.3s;
}
.cancel-btn {
background: #ffffff;
color: #ef4444;
border: 1rpx solid rgba(239, 68, 68, 0.3);
box-shadow: 0 8rpx 20rpx rgba(239, 68, 68, 0.08);
}
.pay-btn:active,
.cancel-btn:active {
transform: scale(0.98);
}
.cancel-btn--loading,
.pay-btn--loading {
opacity: 0.72;
}
.empty-action {
width: 100%;
height: 96rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
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);
transition: all 0.3s;
}
.empty-action:active {
transform: scale(0.98);
}
.empty-panel .empty-action {
width: 80%;
}
.empty-desc {
color: #94A3B8;
font-size: 28rpx;
line-height: 1.6;
}
.animation-fade-in {
animation: fadeIn 0.4s ease forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 隐藏 scroll-view 滚动条 */
::-webkit-scrollbar {
display: none;
width: 0 !important;
height: 0 !important;
-webkit-appearance: none;
background: transparent;
}
.deposit-notice-popup {
background: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
height: 80vh;
}
.popup-header {
padding: 18rpx 88rpx 20rpx;
text-align: center;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(251, 253, 255, 0.96) 100%);
}
.popup-handle {
width: 88rpx;
height: 8rpx;
margin: 0 auto 18rpx;
border-radius: 999rpx;
background: rgba(148, 163, 184, 0.35);
}
.popup-title {
font-size: 34rpx;
font-weight: 600;
line-height: 1.2;
letter-spacing: 1rpx;
color: #1A2233;
}
.popup-body {
flex: 1;
padding: 28rpx 28rpx 24rpx;
overflow-y: auto;
background: linear-gradient(180deg, #F7FAFF 0%, #F2F5FC 100%);
}
.notice-content {
padding-bottom: 24rpx;
}
.payment-sheet {
position: relative;
padding: 28rpx;
border-radius: 32rpx;
background:
radial-gradient(circle at top right, rgba($brand-primary, 0.16) 0, rgba($brand-primary, 0) 38%),
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(246, 250, 255, 0.96) 100%);
border: 1rpx solid rgba($brand-primary, 0.1);
box-shadow: 0 24rpx 60rpx rgba($brand-primary, 0.1);
}
.payment-hero {
padding: 18rpx 6rpx 14rpx;
text-align: center;
}
.payment-hero-label {
font-size: 24rpx;
color: #7C8CA5;
letter-spacing: 2rpx;
}
.payment-hero-amount {
margin-top: 14rpx;
font-size: 68rpx;
line-height: 1;
font-weight: 700;
color: #183B73;
}
.payment-hero-hint {
margin-top: 16rpx;
font-size: 24rpx;
color: #6F809C;
}
.payment-sync-group {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
margin-top: 18rpx;
}
.payment-sync-chip {
padding: 10rpx 18rpx;
border-radius: 999rpx;
background: rgba($brand-primary, 0.08);
color: #4C6A95;
font-size: 22rpx;
}
.payment-detail-panel {
margin-top: 26rpx;
padding: 24rpx;
border-radius: 26rpx;
background: rgba(255, 255, 255, 0.82);
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.8);
}
.payment-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.payment-detail-title {
font-size: 28rpx;
line-height: 1.2;
color: #22324D;
font-weight: 700;
}
.payment-detail-list {
display: flex;
flex-direction: column;
}
.payment-detail-row {
padding: 22rpx 4rpx;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 28rpx;
}
.payment-detail-row + .payment-detail-row {
border-top: 1rpx solid rgba($brand-primary, 0.08);
}
.payment-detail-label {
flex: 0 0 148rpx;
font-size: 22rpx;
color: #7C8CA5;
line-height: 1.5;
}
.payment-detail-value {
flex: 1;
font-size: 26rpx;
line-height: 1.4;
color: #22324D;
word-break: break-all;
font-weight: 600;
text-align: right;
}
.popup-footer {
padding: 18rpx 28rpx calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.92) 26%, #FFFFFF 100%);
display: flex;
justify-content: center;
backdrop-filter: blur(8rpx);
}
.notice-action-btn {
width: 100%;
height: 88rpx;
padding: 0 40rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, $brand-primary 0%, #1F6FE5 100%);
color: #ffffff;
box-shadow: 0 14rpx 28rpx rgba($brand-primary, 0.22);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.notice-action-btn:active {
transform: scale(0.98);
}
</style>