Files
QSY-lvjuxcx/pages/index/component/MyApplicationTab.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

2720 lines
90 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">{{ t('pageTitle') }}</view>
<view class="tn-color-gray tn-text-sm tn-margin-top-xs">{{ t('pageDesc') }}</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">{{ t('reservationFlowTitle') }}</view>
<view class="reservation-flow-panel__desc">{{ t('reservationFlowDesc') }}</view>
</view>
<view class="reservation-flow-panel__refresh" @click.stop="refreshStatus">
<text class="tn-text-bold">{{ t('refreshFlowAction') }}</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 || t('reservationOrderFallback') }}</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">{{ t('apartmentLabel') }}</view>
<view class="grid-info-value">{{ activeReservationOrder.areaText || activeReservationOrder.areaName || '--' }}</view>
</view>
<view class="grid-info-item">
<view class="grid-info-label">{{ t('reservationRoomLabel') }}</view>
<view class="grid-info-value">{{ activeReservationOrder.roomTypeText || activeReservationOrder.roomTypeName || '--' }}</view>
</view>
<view class="grid-info-item">
<view class="grid-info-label">{{ t('roomNoLabel') }}</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">{{ t('reservationStatusLabel') }}</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">{{ t('currentStepTag') }}</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">{{ t('pendingPaymentDescLabel') }}</text>
<text class="v-step-bill-value">{{ getReservationBillDisplayTitle(getActiveReservationBillDetail()) }}</text>
</view>
<view class="v-step-bill-row">
<text class="v-step-bill-label">{{ t('pendingPaymentAmountLabel') }}</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">{{ t('refreshAction') }}</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">{{ t('apartmentLabel') }}</text>
<text class="info-value">{{ item.intendedAreaName || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('occupantLabel') }}</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 ? t('viewingTimeLabel') : t('leaseTermLabel') }}</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">{{ t('submittedAtLabel') }}</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">{{ t('remarkLabel') }}</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">{{ t('statusReminderTitle') }}</view>
<view class="reminder-refresh-btn" @click.stop="refreshStatus">
<text class="tn-text-bold">{{ t('refreshAction') }}</text>
</view>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('statusReminderTitle') }}</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">{{ t('pendingPaymentTitle') }}</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ t('pendingPaymentAmountLabel') }}</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">{{ t('pendingPaymentExpireLabel') }}</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">{{ t('pendingPaymentDescLabel') }}</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">{{ t('payDeposit') }}</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">{{ t('pullUpMore') }}</text>
<text v-else class="tn-color-gray">{{ t('noMore') }}</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="t('emptyTitle')"></tn-empty>
<view class="empty-desc tn-margin-top tn-text-center">{{ t('emptyDesc') }}</view>
<view class="empty-action tn-margin-top-xl" @click="goSelectRoom">
<text class="tn-text-bold">{{ t('goSelectRoom') }}</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">{{ t('loadingDesc') }}</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-title">{{ t('depositNoticeTitle') }}</view>
</view>
<scroll-view scroll-y class="popup-body" @scrolltolower="onNoticeScrollToBottom">
<view class="notice-content">
<view class="notice-item">
<view class="notice-item-zh">1、缴纳线上定金可锁定房源并预约专属看房,平台收取定金后该房源临时下架,不再接待其他客户看房。</view>
<view class="notice-item-en">Payment of the online deposit reserves the housing unit and arranges exclusive on-site viewing for you. After receiving the deposit, we will temporarily remove the property from the rental list and stop arranging viewings for other clients.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">2、请于约定时间完成实地看房,并在看房结束后 3 个工作日内签署租赁合同;若逾期未看房或未完成签约,视作您主动放弃房源,定金不予退还。</view>
<view class="notice-item-en">You shall complete the on-site viewing at the agreed time and sign the rental contract within 3 working days after the viewing. Failure to complete the viewing or sign the contract within the time limit shall be deemed your voluntary waiver of the property, and the deposit will not be refunded.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">3、如因租客个人原因取消看房、看房后不愿承租房屋或拒绝签署租赁合同,定金不予退还。</view>
<view class="notice-item-en">The deposit is non-refundable if you cancel the viewing, refuse to rent the property or decline to sign the contract due to personal reasons.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">4、若因我方房源信息虚假、房屋无法正常交付等我方过错,造成无法看房或签约,定金将全额按原支付路径退还。</view>
<view class="notice-item-en">If the viewing or contract signing cannot be conducted due to our liabilities such as false property information or failure to deliver the housing normally, the full deposit will be refunded via your original payment channel.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">5、待双方正式签署租赁合同后,该定金可直接抵扣首期租金。</view>
<view class="notice-item-en">After both parties formally sign the rental contract, this deposit may be directly deducted from your initial rent.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">6、您支付定金,即代表已完整知晓房源全部信息,并自愿接受本须知全部条款约束。</view>
<view class="notice-item-en">By making the deposit payment, you confirm that you have fully reviewed all property information and voluntarily accept all terms and conditions set forth in this notice.</view>
</view>
</view>
</scroll-view>
<view class="popup-footer">
<view
class="action-btn"
:class="{ 'action-btn--disabled': !hasReadNotice }"
@click="confirmAndPayDeposit"
>
<text class="tn-text-bold">{{ t('agreeAndPay') }}</text>
<text v-if="!hasReadNotice" class="tn-margin-left-xs tn-text-sm">({{ t('scrollToRead') }})</text>
</view>
</view>
</view>
</tn-popup>
</view>
</template>
<script>
import { applicationFormMePage, accountQueryBill, 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'
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 {
i18n: {
zh: {
pageTitle: '我的申请',
pageDesc: '您的入住和预约看房申请记录',
toolbarSummary: '共 {total} 条申请',
refreshing: '刷新中...',
refresh: '刷新状态',
apartmentLabel: '所属公寓',
occupantLabel: '入住人',
viewingTimeLabel: '预约时间',
leaseTermLabel: '预计租期',
submittedAtLabel: '提交时间',
remarkLabel: '补充说明',
payDeposit: '支付定金',
continuePay: '继续支付',
pendingPaymentTitle: '未完成支付订单',
pendingPaymentOrderNoLabel: '支付单号',
pendingPaymentAmountLabel: '订单金额',
pendingPaymentDescLabel: '支付说明',
pendingPaymentExpireLabel: '剩余支付时间',
pendingPaymentExpired: '已失效',
noMore: '没有更多申请了',
pullUpMore: '上拉加载更多',
emptyTitle: '暂无申请记录',
emptyDesc: '您还没有提交任何申请,去选房后可发起入住申请或预约看房。',
goSelectRoom: '去选房',
loadingDesc: '正在同步您的入住申请记录,请稍候。',
statusPending: '待审核',
statusApproved: '已通过',
statusRejected: '已拒绝',
statusFinished: '已结束',
statusProcessing: '处理中',
statusPendingPayment: '请支付定金',
statusWaitingRoomAssign: '等待预订中心分配房间',
statusReminderTitle: '当前进度提醒',
refreshEmptySuccess: '未查到申请单,已同步预订单',
refreshSuccess: '申请状态已刷新',
loadError: '申请记录加载失败',
cancelOrder: '取消订单',
cancelOrderLoading: '取消中...',
cancelOrderTitle: '取消订单',
cancelOrderContent: '取消后将关闭当前未支付的定金订单,是否继续?',
cancelOrderSuccess: '订单已取消',
cancelOrderFailed: '取消订单失败',
viewingApplicationTitle: '预约看房申请',
stayApplicationTitle: '入住申请',
missingBillCode: '申请单编号缺失',
missingContractNo: '合同编号缺失',
missingApplicationCode: '申请编号缺失',
dateRangeJoiner: ' 至 ',
depositNoticeTitle: '租房定金须知',
agreeAndPay: '同意并支付',
scrollToRead: '请滑动阅读至底部',
fetchPaymentParamsFailed: '获取支付参数失败',
fetchWechatLoginCodeFailed: '获取微信登录凭证失败',
loadingBill: '正在获取账单...',
noPendingDepositBill: '暂无待支付定金账单',
loadingPayment: '正在拉起支付...',
depositPaymentDesc: '支付定金',
paymentSuccess: '支付成功',
paymentCanceled: '已取消支付',
paymentFailed: '支付失败',
reservationFlowTitle: '预定流程',
reservationFlowDesc: '当前预定单的流程进度',
reservationOrderFallback: '预定单',
reservationOrderNoLabel: '预定单号',
roomNoLabel: '房间号',
reservationRoomLabel: '房型',
reservationStatusLabel: '订单状态',
currentStepTag: '当前',
contractPendingReviewHint: '合同已签,等待宿管审核',
checkinPendingHint: '宿管正在办理入住,请耐心等待',
refreshAction: '刷新',
refreshFlowAction: '刷新流程'
},
en: {
pageTitle: 'My Applications',
pageDesc: 'Your stay and viewing application records',
toolbarSummary: '{total} applications',
refreshing: 'Refreshing...',
refresh: 'Refresh Status',
apartmentLabel: 'Apartment',
occupantLabel: 'Resident',
viewingTimeLabel: 'Viewing Time',
leaseTermLabel: 'Lease Term',
submittedAtLabel: 'Submitted At',
remarkLabel: 'Remarks',
payDeposit: 'Pay Deposit',
continuePay: 'Continue Pay',
pendingPaymentTitle: 'Unfinished Payment',
pendingPaymentOrderNoLabel: 'Payment Order No.',
pendingPaymentAmountLabel: 'Order Amount',
pendingPaymentDescLabel: 'Description',
pendingPaymentExpireLabel: 'Time Left',
pendingPaymentExpired: 'Expired',
noMore: 'No more applications',
pullUpMore: 'Pull up to load more',
emptyTitle: 'No applications yet',
emptyDesc: 'You have not submitted any application yet. Go select a room to start a stay or viewing application.',
goSelectRoom: 'Select Room',
loadingDesc: 'Syncing your application records. Please wait.',
statusPending: 'Pending',
statusApproved: 'Approved',
statusRejected: 'Rejected',
statusFinished: 'Closed',
statusProcessing: 'Processing',
statusPendingPayment: 'Please Pay Deposit',
statusWaitingRoomAssign: 'Waiting for Room Assignment',
statusReminderTitle: 'Current Progress',
refreshEmptySuccess: 'No application found. Reservation records synced.',
refreshSuccess: 'Application status refreshed',
loadError: 'Failed to load applications',
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',
viewingApplicationTitle: 'Viewing Request',
stayApplicationTitle: 'Stay Application',
missingBillCode: 'Missing application bill number',
missingContractNo: 'Missing contract number',
missingApplicationCode: 'Missing application number',
dateRangeJoiner: ' to ',
depositNoticeTitle: 'Rental Deposit Notice',
agreeAndPay: 'Agree and Pay',
scrollToRead: 'Scroll to bottom',
fetchPaymentParamsFailed: 'Failed to get payment parameters',
fetchWechatLoginCodeFailed: 'Failed to get WeChat login code',
loadingBill: 'Loading bill...',
noPendingDepositBill: 'No pending deposit bill available',
loadingPayment: 'Opening payment...',
depositPaymentDesc: 'Deposit Payment',
paymentSuccess: 'Payment succeeded',
paymentCanceled: 'Payment canceled',
paymentFailed: 'Payment failed',
reservationFlowTitle: 'Reservation Flow',
reservationFlowDesc: 'Progress of the current reservation order',
reservationOrderFallback: 'Reservation',
reservationOrderNoLabel: 'Order No.',
roomNoLabel: 'Room No.',
reservationRoomLabel: 'Room Type',
reservationStatusLabel: 'Order Status',
currentStepTag: 'Current',
contractPendingReviewHint: 'Contract signed, waiting for review',
checkinPendingHint: 'Move-in is being processed, please wait',
refreshAction: 'Refresh',
refreshFlowAction: 'Refresh Flow'
}
},
data() {
return {
currentIndex: 0,
status: 0,
pageIndex: 1,
pageSize: 10,
total: 0,
list: [],
loading: false,
loadingMore: false,
hasMore: true,
showDepositNotice: false,
hasReadNotice: false,
pendingDepositReferenceTypeNo: '',
pendingDepositPaymentContext: 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: {
t(key, params) {
return this.$tm('pages.index.component.MyApplicationTab', key, params)
},
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.t('statusPending'),
10: this.t('statusApproved'),
20: this.t('statusRejected'),
99: this.t('statusFinished')
}
return map[status] || this.t('statusProcessing')
},
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.t('inspectionPendingReviewHint')
}
const reservationStepText = getReservationCurrentStepText(item, this.language)
if (reservationStepText) {
return reservationStepText
}
if (this.shouldPayDeposit(item)) {
return this.t('statusPendingPayment')
}
if (this.isWaitingRoomAssignment(item)) {
return this.t('statusWaitingRoomAssign')
}
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.t('dateRangeJoiner')}${endText}`
}
}
return `${this.formatDate(start)}${this.t('dateRangeJoiner')}${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 {}
},
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.t('fetchPaymentParamsFailed')))
}
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.t('fetchWechatLoginCodeFailed')))
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.t('pendingPaymentExpired')
}
return this.formatPendingPaymentCountdown(this.getPendingPaymentRemainingSeconds(order))
},
getPendingPaymentActionText(item = {}) {
return this.isApplicationPendingPaymentExpired(item) ? this.t('payDeposit') : this.t('continuePay')
},
getReservationOrderNoFromItem(item = {}) {
return getReservationOrderNo(item)
},
getReservationCurrentStepValue(item = {}) {
return Number(item && item.reservationCurrentNode ? item.reservationCurrentNode.step : 0)
},
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.t('inspectionPendingReviewHint')
}
if (this.isReservationContractAwaitingReview(order)) {
const detail = this.getReservationContractDetail(order) || {}
return String(detail.queryStatusDesc || detail.approvalStatusDesc || '').trim() || this.t('contractPendingReviewHint')
}
if (this.isReservationCheckinProcessing(order)) {
return this.t('checkinPendingHint')
}
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.t('depositPaymentDesc')
).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.t('refreshEmptySuccess'),
icon: 'none'
})
return
}
if (this.list.length || this.reservationOrders.length) {
uni.showToast({
title: this.t('refreshSuccess'),
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.t('loadError'),
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.t('viewingApplicationTitle')
}
return item.intendedRoomTypeName || this.t('stayApplicationTitle')
},
resolveReferenceTypeNo(item = {}) {
return item.detailCode || ''
},
canViewBill(item = {}) {
return !item.isViewingApplication
&& (this.shouldPayDeposit(item) || this.isReservationBillStep(item))
&& this.resolveReferenceTypeNo(item)
},
canCancelOrder(item = {}) {
return this.shouldPayDeposit(item)
&& this.resolveReferenceTypeNo(item)
},
getCancelButtonText(item = {}) {
return this.cancelingReferenceTypeNo === this.resolveReferenceTypeNo(item)
? this.t('cancelOrderLoading')
: this.t('cancelOrder')
},
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)
})
})
},
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.t('statusFinished'),
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)
: ''
if (!referenceTypeNo || this.cancelingReferenceTypeNo) {
return
}
this.confirmCancelOrder()
.then((confirmed) => {
if (!confirmed) {
return
}
this.cancelingReferenceTypeNo = referenceTypeNo
return userDetailCancel({
detailCode: referenceTypeNo,
code: referenceTypeNo,
referenceTypeNo
}, this.language)
.then(async () => {
this.syncCanceledItem(referenceTypeNo)
await this.reloadList(true)
this.$emit('refresh-shell-state')
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.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.t('missingBillCode'),
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.t('missingBillCode'),
icon: 'none'
})
return
}
this.pendingDepositReferenceTypeNo = referenceTypeNo
this.hasReadNotice = false
this.showDepositNotice = true
},
onNoticeScrollToBottom() {
this.hasReadNotice = true
},
openDepositNoticeForPayment(context = {}) {
this.pendingDepositPaymentContext = {
referenceTypeNo: String(context.referenceTypeNo || '').trim(),
billNo: String(context.billNo || '').trim(),
paymentNo: String(context.paymentNo || '').trim(),
description: String(context.description || '').trim()
}
this.pendingDepositReferenceTypeNo = this.pendingDepositPaymentContext.referenceTypeNo
this.hasReadNotice = false
this.showDepositNotice = true
},
closeDepositNotice() {
this.showDepositNotice = false
this.pendingDepositReferenceTypeNo = ''
this.pendingDepositPaymentContext = 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,
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.t('depositPaymentDesc')
}
},
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 = {}) {
let referenceTypeNo = String(context.referenceTypeNo || '').trim()
let billNo = String(context.billNo || '').trim()
const paymentNo = String(context.paymentNo || '').trim()
const description = String(context.description || '').trim() || this.t('depositPaymentDesc')
if (!referenceTypeNo && !billNo && !paymentNo) {
uni.showToast({
title: this.t('missingBillCode'),
icon: 'none'
})
return
}
if (referenceTypeNo) {
saveActiveApplicationCode(referenceTypeNo)
}
uni.showLoading({ title: this.t('loadingBill') })
try {
let bill = null
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 (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)
}
}
}
if (referenceTypeNo) {
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: this.t('noPendingDepositBill'), 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: this.t('noPendingDepositBill'), icon: 'none' })
return
}
uni.showLoading({ title: this.t('loadingPayment') })
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.t('fetchPaymentParamsFailed'))
}
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.t('paymentSuccess'), icon: 'success' })
} catch (error) {
const errMsg = error && (error.errMsg || error.message || error.msg) ? (error.errMsg || error.message || error.msg) : this.t('paymentFailed')
if (/cancel/i.test(errMsg)) {
uni.showToast({ title: this.t('paymentCanceled'), icon: 'none' })
} else {
uni.showToast({ title: errMsg, icon: 'none' })
}
} finally {
uni.hideLoading()
setTimeout(() => {
this.refreshStatus()
}, 1500)
}
},
async confirmAndPayDeposit() {
if (!this.hasReadNotice) {
return
}
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.t('missingBillCode'),
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.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(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.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(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: 32rpx;
text-align: center;
border-bottom: 1px solid #F1F5F9;
}
.popup-title {
font-size: 36rpx;
font-weight: bold;
color: #1A2233;
}
.popup-body {
flex: 1;
padding: 32rpx;
overflow-y: auto;
}
.notice-content {
padding-bottom: 40rpx;
}
.notice-item {
margin-bottom: 32rpx;
}
.notice-item-zh {
font-size: 28rpx;
color: #334155;
line-height: 1.6;
margin-bottom: 8rpx;
font-weight: 500;
}
.notice-item-en {
font-size: 24rpx;
color: #64748B;
line-height: 1.5;
}
.popup-footer {
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
border-top: 1px solid #F1F5F9;
background: #FFFFFF;
display: flex;
justify-content: center;
}
.action-btn {
min-width: 420rpx;
max-width: 100%;
height: 92rpx;
padding: 0 40rpx;
border-radius: 999rpx;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: #ffffff;
box-shadow: 0 16rpx 34rpx rgba(34, 197, 94, 0.24);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.action-btn:active {
transform: scale(0.98);
}
.action-btn--disabled {
opacity: 0.6;
background: #94A3B8;
box-shadow: none;
}
</style>