Files
QSY-lvjuxcx/pages/index/component/MyApplicationTab.vue
liangjian 6e721ccc7e refactor: 国际化文本迁移至全局 locales/messages.js
将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。

- 新增 utils/i18n.js 国际化工具函数
- 更新 store/$t.mixin.js 翻译混入逻辑
- 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
2026-07-24 13:43:41 +08:00

2567 lines
84 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-title">{{ $tr('租房定金须知') }}</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">{{ $tr('同意并支付') }}</text>
<text v-if="!hasReadNotice" class="tn-margin-left-xs tn-text-sm">({{ $tr('请滑动阅读至底部') }})</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 {
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: {
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 {}
},
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)
},
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)
},
canCancelOrder(item = {}) {
return this.shouldPayDeposit(item)
&& this.resolveReferenceTypeNo(item)
},
getCancelButtonText(item = {}) {
return this.cancelingReferenceTypeNo === this.resolveReferenceTypeNo(item)
? this.$tr('取消中...')
: this.$tr('取消订单')
},
confirmCancelOrder() {
return new Promise((resolve) => {
uni.showModal({
title: this.$tr('取消订单'),
content: this.$tr('取消后将关闭当前未支付的定金订单,是否继续?'),
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)
: ''
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.$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
}
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.$tr('支付定金')
}
},
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.$tr('支付定金')
if (!referenceTypeNo && !billNo && !paymentNo) {
uni.showToast({
title: this.$tr('申请单编号缺失'),
icon: 'none'
})
return
}
if (referenceTypeNo) {
saveActiveApplicationCode(referenceTypeNo)
}
uni.showLoading({ title: this.$tr('正在获取账单...') })
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.$tr('暂无待支付定金账单'), 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.$tr('暂无待支付定金账单'), 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() {
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.$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: 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>