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

1975 lines
62 KiB
Vue
Raw Permalink 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="bill-panel">
<view class="hero-panel tn-margin-bottom-lg">
<view class="hero-title tn-margin-top">{{ $tr('待支付账单') }}</view>
<view class="hero-desc tn-margin-top-sm">{{ heroDesc }}</view>
<view v-if="currentReferenceTypeNo" class="source-tag tn-margin-top-sm">{{ $tr('申请单号:') }}{{ currentReferenceTypeNo }}</view>
<view class="hero-summary tn-margin-top-lg">
<view class="summary-card">
<view class="summary-label">{{ $tr('待支付数量') }}</view>
<view class="summary-value tn-margin-top-sm">{{ billList.length }}</view>
</view>
<view class="summary-card">
<view class="summary-label">{{ $tr('待支付总额') }}</view>
<view class="summary-value tn-margin-top-sm">¥{{ formatMoney(totalRemainingAmount) }}</view>
</view>
</view>
</view>
<view v-if="loading || resolvingReference" class="empty-panel tn-padding-lg">
<view class="empty-title">{{ $tr('账单加载中') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ $tr('正在同步您的待支付账单,请稍候。') }}</view>
</view>
<view v-else-if="billList.length">
<view
v-for="(item, index) in billList"
:key="getBillKey(item)"
class="bill-card tn-margin-bottom-lg"
>
<view class="card-head tn-flex tn-flex-row-between tn-flex-col-center">
<view class="card-title-wrap">
<view class="card-title">{{ getReferenceTypeText(item) }}</view>
<view class="card-code tn-margin-top-xs">{{ getBillNo(item) }}</view>
</view>
<view :class="['status-badge', 'status-' + resolvePaymentStatusType(getBillPaymentStatus(item))]">
{{ getBillPaymentStatusText(item) }}
</view>
</view>
<view class="card-body tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('关联业务编号') }}</text>
<text class="info-value">{{ getBillReferenceTypeNo(item) || '--' }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('应收合计') }}</text>
<text class="info-value">¥{{ formatMoney(getBillTotalCharge(item)) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('已付金额') }}</text>
<text class="info-value">¥{{ formatMoney(getBillPaidAmount(item)) }}</text>
</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(getBillRemainingAmount(item)) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('退款状态') }}</text>
<text class="info-value">{{ getRefundStatusText(getBillRefundStatus(item)) }}</text>
</view>
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('创建时间') }}</text>
<text class="info-value">{{ formatDateTime(getBillCreateTime(item)) }}</text>
</view>
</view>
<view v-if="getBillRemark(item) || getBillResult(item)" class="remark-panel tn-margin-top">
<view v-if="getBillRemark(item)" class="remark-line">
<text class="remark-label">{{ $tr('备注') }}</text>
<text class="remark-text">{{ getBillRemark(item) }}</text>
</view>
<view v-if="getBillResult(item)" class="remark-line tn-margin-top-sm">
<text class="remark-label">{{ $tr('处理结果') }}</text>
<text class="remark-text">{{ getBillResult(item) }}</text>
</view>
</view>
<view v-if="getPendingPaymentOrder(item)" class="pending-payment-panel tn-margin-top">
<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">{{ getPendingPaymentOrderNo(item) }}</text>
</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(getPendingPaymentAmount(item)) }}</text>
</view>
<view v-if="getPendingPaymentExpireText(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('剩余支付时间') }}</text>
<text class="info-value" :class="{ 'info-value--expired': isPendingPaymentExpired(item) }">
{{ getPendingPaymentExpireText(item) }}
</text>
</view>
<view v-if="getPendingPaymentDescription(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('支付说明') }}</text>
<text class="info-value">{{ getPendingPaymentDescription(item) }}</text>
</view>
<view v-if="getPendingPaymentChannel(item)" class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('支付渠道') }}</text>
<text class="info-value">{{ getPendingPaymentChannel(item) }}</text>
</view>
</view>
<view class="card-footer tn-margin-top-lg">
<view
v-if="canCancelOrder(item)"
class="cancel-btn"
:data-bill-index="index"
:class="{ 'cancel-btn--loading': cancelingBillNo === getBillNo(item) }"
@tap.stop="handleCancelOrder"
>
<text class="tn-text-bold">{{ getCancelButtonText(item) }}</text>
</view>
<view
class="pay-btn"
:data-bill-index="index"
:class="{ 'pay-btn--loading': payingBillNo === getBillNo(item) }"
@tap.stop="handleWechatPay"
>
<text class="tn-text-bold">{{ getPayButtonText(item) }}</text>
</view>
</view>
</view>
</view>
<view v-else class="empty-panel tn-padding-lg">
<view class="empty-title">{{ currentReferenceTypeNo ? $tr('暂无待支付账单') : $tr('暂无可查询申请单') }}</view>
<view class="empty-desc tn-margin-top-sm">{{ currentReferenceTypeNo ? $tr('当前申请单下没有查询到待支付账单,后续有新账单时会在这里展示。') : $tr('当前未找到可关联的申请单,请先前往“我的申请”选择具体申请后查看账单。') }}</view>
<view v-if="!currentReferenceTypeNo" class="empty-action tn-margin-top-lg" @tap="goToMyApplication">
<text class="tn-text-bold">{{ $tr('前往我的申请') }}</text>
</view>
</view>
<tn-popup v-model="showPaymentNotice" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true" @close="handlePaymentNoticeClose">
<view class="payment-notice-popup">
<view class="payment-notice-popup__header">
<view class="payment-notice-popup__title">{{ paymentNoticeContext.title }}</view>
</view>
<scroll-view scroll-y class="payment-notice-popup__body">
<view class="payment-notice-popup__summary">
<view class="payment-notice-popup__summary-row">
<text class="payment-notice-popup__summary-label">{{ paymentNoticeContext.billLabel }}</text>
<text class="payment-notice-popup__summary-value">{{ paymentNoticeContext.billNo }}</text>
</view>
<view class="payment-notice-popup__summary-row">
<text class="payment-notice-popup__summary-label">{{ paymentNoticeContext.itemLabel }}</text>
<text class="payment-notice-popup__summary-value">{{ paymentNoticeContext.itemTitle }}</text>
</view>
<view class="payment-notice-popup__summary-row">
<text class="payment-notice-popup__summary-label">{{ paymentNoticeContext.amountLabel }}</text>
<text class="payment-notice-popup__summary-value payment-notice-popup__summary-value--price">{{ paymentNoticeContext.amountText }}</text>
</view>
</view>
<view class="payment-notice-popup__content">
<view
v-for="(line, index) in paymentNoticeContext.lines"
:key="`payment-notice-line-${index}`"
class="payment-notice-popup__line"
>
{{ index + 1 }}. {{ line }}
</view>
</view>
</scroll-view>
<view class="payment-notice-popup__footer">
<view class="payment-notice-popup__action payment-notice-popup__action--ghost" @tap="closePaymentNotice(false)">
<text class="tn-text-bold">{{ paymentNoticeContext.cancelText }}</text>
</view>
<view class="payment-notice-popup__action payment-notice-popup__action--primary" @tap="closePaymentNotice(true)">
<text class="tn-text-bold">{{ paymentNoticeContext.confirmText }}</text>
</view>
</view>
</view>
</tn-popup>
<view v-if="showPaymentFeedbackPanel" class="payment-waiting-mask">
<view :class="['payment-waiting-panel', 'payment-waiting-panel--' + paymentStageVisualType]">
<view class="payment-waiting-glow payment-waiting-glow--one"></view>
<view class="payment-waiting-glow payment-waiting-glow--two"></view>
<view :class="['payment-waiting-badge', 'payment-waiting-badge--' + paymentStageVisualType]">
<text :class="[paymentBadgeIconClass, 'payment-waiting-badge__icon']"></text>
<text>{{ paymentStageLabel }}</text>
</view>
<view class="payment-waiting-icon-wrap">
<view v-if="isPaymentWaitingStage" class="payment-waiting-icon-ring"></view>
<view :class="['payment-waiting-icon-core', 'payment-waiting-icon-core--' + paymentStageVisualType]">
<text :class="[paymentFeedbackIconClass, 'payment-waiting-icon', { 'payment-waiting-icon--spin': isPaymentWaitingStage }]"></text>
</view>
</view>
<view class="payment-waiting-title">{{ paymentWaitingTitle }}</view>
<view class="payment-waiting-desc">{{ paymentWaitingDesc }}</view>
<view v-if="isPaymentWaitingStage" class="payment-progress-card">
<view class="payment-progress-head tn-flex tn-flex-row-between tn-flex-col-center">
<text class="payment-progress-label">{{ $tr('结果确认进度') }}</text>
<text class="payment-progress-value">{{ paymentProgressText }}</text>
</view>
<view class="payment-progress-track">
<view class="payment-progress-bar" :style="{ width: paymentProgressPercent + '%' }"></view>
</view>
<view class="payment-progress-note">{{ paymentStatusHintText }}</view>
</view>
<view class="payment-summary-grid">
<view class="payment-summary-item">
<view class="payment-summary-label">{{ $tr('账单号') }}</view>
<view class="payment-summary-value payment-summary-value--mono">{{ paymentWaitingBillNo || '--' }}</view>
</view>
<view class="payment-summary-item">
<view class="payment-summary-label">{{ $tr('本次支付') }}</view>
<view class="payment-summary-value payment-summary-value--price">¥{{ paymentWaitingAmountText }}</view>
</view>
</view>
<view class="payment-tips-card">
<view class="payment-tips-title">{{ paymentTipsTitle }}</view>
<view v-for="(tip, index) in paymentTips" :key="`${paymentStage}-${index}`" class="payment-tip-line">{{ tip }}</view>
</view>
<view v-if="showPaymentActionButton" class="payment-action-row">
<view class="payment-action-btn" @tap="handlePaymentResultAction">
<text class="tn-text-bold">{{ paymentActionText }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { accountQueryBills, applicationFormMePage, paymentBillPay, paymentPayingBills, paymentStatusByBillNo, userDetailCancel } from '@/api/index.js'
import {
ACCOUNT_BILL_PAYMENT_STATUS,
ACCOUNT_BILL_STATUS,
getAccountBillPaymentStatusLabel,
getAccountBillRefundStatusLabel,
getAccountReferenceTypeLabel
} from '@/utils/enum-store.js'
import { getActiveApplicationCode, getMyApplicationList, getSessionSnapshot, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
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 normalizeBillList(response) {
const payload = 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 []
}
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 padCountdownUnit(value) {
return String(Math.max(0, Number(value) || 0)).padStart(2, '0')
}
const PAYMENT_QUERY_STATUS = {
PENDING: 'pending',
SUCCESS: 'success',
FAILED: 'failed'
}
const PAYMENT_ORDER_STATUS = {
SUCCESS: 10,
FAILED: 99
}
const PAYMENT_QUERY_MAX_ATTEMPTS = 8
const PAYMENT_QUERY_INTERVAL = 1800
export default {
name: 'CurrentApplicationBillPanel',
props: {
referenceTypeNo: {
type: String,
default: ''
},
autoResolveReference: {
type: Boolean,
default: true
}
},
data() {
return {
loading: false,
billList: [],
pendingPaymentOrderMap: {},
payingBillNo: '',
cancelingBillNo: '',
currentReferenceTypeNo: '',
countdownNow: Date.now(),
resolvingReference: false,
paymentPollingBillNo: '',
paymentPollingAttempt: 0,
paymentStage: '',
paymentStatusHint: '',
paymentSession: null,
showPaymentNotice: false,
paymentNoticeResolver: null,
paymentNoticeContext: {
title: '',
billLabel: '',
billNo: '--',
itemLabel: '',
itemTitle: '',
amountLabel: '',
amountText: '',
lines: [],
confirmText: '',
cancelText: ''
}
}
},
computed: {
language() {
return this.$getLanguage()
},
totalRemainingAmount() {
return this.billList.reduce((sum, item) => {
return sum + Number(item && item.remainingAmount ? item.remainingAmount : 0)
}, 0)
},
heroDesc() {
if (this.currentReferenceTypeNo) {
return this.$tr('当前页面已按申请单编号查询其关联的待支付账单记录。')
}
return this.$tr('当前将优先读取最近申请单的编号来查询关联账单。')
},
showPaymentFeedbackPanel() {
return Boolean(this.paymentStage)
},
isPaymentWaitingStage() {
return this.paymentStage === 'launching' || this.paymentStage === 'polling'
},
isPaymentSuccessStage() {
return this.paymentStage === 'success'
},
isPaymentFailedStage() {
return this.paymentStage === 'failed'
},
paymentStageVisualType() {
if (this.isPaymentSuccessStage) {
return 'success'
}
if (this.isPaymentFailedStage) {
return 'failed'
}
return 'waiting'
},
paymentStageLabel() {
if (this.paymentStage === 'launching') {
return this.$tr('正在唤起微信收银台')
}
if (this.paymentStage === 'polling') {
return this.$tr('正在同步支付结果')
}
if (this.paymentStage === 'success') {
return this.$tr('支付结果已确认')
}
if (this.paymentStage === 'failed') {
return this.$tr('支付未完成')
}
return this.$tr('支付处理中')
},
paymentBadgeIconClass() {
if (this.isPaymentSuccessStage) {
return 'tn-icon-success-circle-fill'
}
if (this.isPaymentFailedStage) {
return 'tn-icon-warning-fill'
}
return 'tn-icon-secure'
},
paymentFeedbackIconClass() {
if (this.isPaymentSuccessStage) {
return 'tn-icon-success'
}
if (this.isPaymentFailedStage) {
return 'tn-icon-close'
}
return 'tn-icon-loading'
},
paymentWaitingTitle() {
if (this.paymentStage === 'launching') {
return this.$tr('请完成微信支付')
}
if (this.paymentStage === 'polling') {
return this.$tr('正在确认是否支付成功')
}
if (this.paymentStage === 'success') {
return this.$tr('支付成功')
}
if (this.paymentStage === 'failed') {
return this.$tr('支付结果确认失败')
}
return this.$tr('支付处理中')
},
paymentWaitingDesc() {
if (this.paymentStage === 'launching') {
return this.$tr('系统正在调起微信支付,请在微信收银台中完成付款后返回。')
}
if (this.paymentStage === 'polling') {
return this.$tr('支付完成后,系统会自动向后台查询支付状态,无需手动刷新。')
}
if (this.paymentStage === 'success') {
return this.$tr('本次支付已同步完成,账单状态已更新。')
}
if (this.paymentStage === 'failed') {
return this.$tr('系统暂时未能确认支付成功,您可以查看提示后稍后重试。')
}
return ''
},
paymentWaitingBillNo() {
return this.paymentPollingBillNo || this.payingBillNo || (this.paymentSession && this.paymentSession.billNo) || ''
},
paymentWaitingAmountText() {
const amount = this.paymentSession && Number(this.paymentSession.amount)
return Number.isFinite(amount) ? amount.toFixed(2) : '0.00'
},
paymentProgressPercent() {
if (this.paymentStage !== 'polling') {
return 16
}
const current = this.paymentPollingAttempt > 0 ? this.paymentPollingAttempt : 1
return Math.min(100, Math.max(18, Math.round((current / PAYMENT_QUERY_MAX_ATTEMPTS) * 100)))
},
paymentProgressText() {
if (this.paymentStage !== 'polling') {
return this.$tr('等待支付完成')
}
const current = this.paymentPollingAttempt > 0 ? this.paymentPollingAttempt : 1
return `${current}/${PAYMENT_QUERY_MAX_ATTEMPTS}`
},
paymentStatusHintText() {
if (this.paymentStatusHint) {
return this.paymentStatusHint
}
if (this.paymentStage === 'launching') {
return this.$tr('已为您准备好支付信息,等待微信支付完成。')
}
return this.$tr('正在与后台核对支付状态,请稍候。')
},
paymentTipsTitle() {
if (this.isPaymentFailedStage) {
return this.$tr('处理建议')
}
if (this.isPaymentSuccessStage) {
return this.$tr('完成说明')
}
return this.$tr('温馨提示')
},
paymentTips() {
if (this.isPaymentFailedStage) {
return [
this.$tr('若您已完成付款,请稍后刷新账单,避免重复支付。'),
this.$tr('若微信侧已扣款但页面未更新,请联系管家或客服协助核实。'),
this.$tr('若未扣款,可返回账单列表后重新发起支付。')
]
}
if (this.isPaymentSuccessStage) {
return [
this.$tr('若当前申请单还有其他待付账单,可继续在当前页完成支付。'),
this.$tr('若当前账单已全部支付完成,系统会自动返回首页。'),
this.$tr('支付记录同步可能略有延迟,刷新后即可看到最新状态。')
]
}
return [
this.$tr('支付成功后,系统可能需要几秒钟同步账单状态。'),
this.$tr('请不要重复点击支付,也不要提前关闭当前页面。'),
this.$tr('如果超时未完成,系统会自动再校验一次账单状态。')
]
},
showPaymentActionButton() {
return this.isPaymentFailedStage
},
paymentActionText() {
return this.isPaymentFailedStage ? this.$tr('我知道了') : ''
}
},
watch: {
referenceTypeNo: {
immediate: true,
handler(value) {
const nextReferenceTypeNo = value ? String(value).trim() : ''
if (nextReferenceTypeNo === this.currentReferenceTypeNo) {
return
}
this.currentReferenceTypeNo = nextReferenceTypeNo
if (this.currentReferenceTypeNo) {
saveActiveApplicationCode(this.currentReferenceTypeNo)
}
if (this._panelReady) {
this.refreshBillList()
}
}
}
},
mounted() {
this._panelReady = true
this.startCountdownTimer()
this.refreshBillList()
},
beforeDestroy() {
this.clearCountdownTimer()
},
methods: {
getBillNo(item = {}) {
return item.no || item.billNo || ''
},
getBillKey(item = {}) {
return this.getBillNo(item) || this.getBillReferenceTypeNo(item) || this.getBillCreateTime(item) || 'bill-item'
},
getBillReferenceTypeNo(item = {}) {
return item.referenceTypeNo || ''
},
getBillPaymentStatus(item = {}) {
return item.paymentStatus
},
getBillPaymentStatusText(item = {}) {
const translatedStatus = this.getPaymentStatusText(this.getBillPaymentStatus(item))
return translatedStatus || item.paymentStatusDesc || this.$tr('处理中')
},
getBillRefundStatus(item = {}) {
return item.refundStatus
},
getBillCreateTime(item = {}) {
return item.createTime || ''
},
getBillTotalCharge(item = {}) {
return item.totalCharge
},
getBillPaidAmount(item = {}) {
return item.paidAmount
},
getBillRemainingAmount(item = {}) {
return item.remainingAmount
},
getBillRemark(item = {}) {
return item.remark || ''
},
getBillResult(item = {}) {
return item.result || ''
},
getPendingPaymentOrder(item = {}) {
const billNo = this.getBillNo(item)
return billNo ? (this.pendingPaymentOrderMap[billNo] || null) : null
},
getPendingPaymentOrderNo(item = {}) {
const order = this.getPendingPaymentOrder(item)
return order ? (order.payRequestNo || order.payNo || order.paymentNo || '--') : '--'
},
getPendingPaymentAmount(item = {}) {
const order = this.getPendingPaymentOrder(item)
if (order && (order.amount || order.amount === 0)) {
return order.amount
}
return this.getBillRemainingAmount(item)
},
getPendingPaymentDescription(item = {}) {
const order = this.getPendingPaymentOrder(item)
return order ? (order.description || '') : ''
},
getPendingPaymentChannel(item = {}) {
const order = this.getPendingPaymentOrder(item)
return order && order.cashier ? (order.cashier.payChannel || '') : ''
},
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(item = {}) {
const order = this.getPendingPaymentOrder(item)
if (!order) {
return 0
}
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
},
isPendingPaymentExpired(item = {}) {
const order = this.getPendingPaymentOrder(item)
if (!order) {
return false
}
const expireTimestamp = this.getPendingPaymentExpireTimestamp(order)
if (!expireTimestamp) {
return false
}
return expireTimestamp <= this.countdownNow
},
getPendingPaymentExpireText(item = {}) {
const order = this.getPendingPaymentOrder(item)
if (!order) {
return ''
}
const expireTimestamp = this.getPendingPaymentExpireTimestamp(order)
if (!expireTimestamp) {
return ''
}
if (expireTimestamp <= this.countdownNow) {
return this.$tr('已失效')
}
return this.formatPendingPaymentCountdown(this.getPendingPaymentRemainingSeconds(item))
},
getPayButtonText(item = {}) {
const billNo = this.getBillNo(item)
if (this.paymentPollingBillNo && this.paymentPollingBillNo === billNo) {
if (!this.paymentPollingAttempt) {
return this.$tr('确认结果中...')
}
return this.$tr('查询结果中({current}/{total})', {
current: this.paymentPollingAttempt,
total: PAYMENT_QUERY_MAX_ATTEMPTS
})
}
if (this.payingBillNo === billNo) {
return this.paymentStage === 'launching' ? this.$tr('拉起支付中...') : this.$tr('确认结果中...')
}
if (this.getPendingPaymentOrder(item) && !this.isPendingPaymentExpired(item)) {
return this.$tr('继续支付')
}
return this.$tr('微信支付')
},
canCancelOrder(item = {}) {
if (!this.currentReferenceTypeNo) {
return false
}
const remainingAmount = Number(this.getBillRemainingAmount(item) || 0)
const paymentStatus = Number(this.getBillPaymentStatus(item))
return remainingAmount > 0 || paymentStatus !== ACCOUNT_BILL_PAYMENT_STATUS.PAID
},
getCancelButtonText(item = {}) {
return this.cancelingBillNo === this.getBillNo(item)
? this.$tr('取消中...')
: this.$tr('取消申请')
},
resolveReferenceTypeNo(item = {}) {
return item.code || item.detailCode || ''
},
resolvePreferredReferenceTypeNo(list = []) {
const sortedList = normalizeApplicationList(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)
},
ensureReferenceTypeNo() {
if (this.currentReferenceTypeNo) {
return Promise.resolve(this.currentReferenceTypeNo)
}
if (!this.autoResolveReference) {
return Promise.resolve('')
}
const session = getSessionSnapshot()
const sessionUser = session && session.currentUser ? session.currentUser : {}
const currentFlowRefNo = String(
sessionUser.currentReservationRefNo
|| (session && session.resolvedUser && session.resolvedUser.currentReservationRefNo)
|| ''
).trim()
if (currentFlowRefNo) {
this.currentReferenceTypeNo = currentFlowRefNo
saveActiveApplicationCode(currentFlowRefNo)
return Promise.resolve(this.currentReferenceTypeNo)
}
const cachedActiveCode = getActiveApplicationCode()
if (cachedActiveCode) {
this.currentReferenceTypeNo = cachedActiveCode
return Promise.resolve(this.currentReferenceTypeNo)
}
const cachedApplicationList = normalizeApplicationList(getMyApplicationList())
const cachedCode = this.resolvePreferredReferenceTypeNo(cachedApplicationList)
if (cachedCode) {
this.currentReferenceTypeNo = cachedCode
saveActiveApplicationCode(cachedCode)
return Promise.resolve(this.currentReferenceTypeNo)
}
this.resolvingReference = true
return applicationFormMePage({
PageIndex: 1,
PageSize: 20
})
.then((response) => {
const payload = response && response.data ? response.data : response
const list = normalizeApplicationList(payload && payload.data)
saveMyApplicationList(list)
const nextCode = this.resolvePreferredReferenceTypeNo(list)
this.currentReferenceTypeNo = nextCode
if (nextCode) {
saveActiveApplicationCode(nextCode)
}
return nextCode
})
.catch(() => '')
.finally(() => {
this.resolvingReference = false
})
},
formatMoney(value) {
const amount = Number(value || 0)
return amount.toFixed(2)
},
formatDateTime(value) {
if (!value) {
return '--'
}
const text = String(value).replace('T', ' ')
return text.length > 16 ? text.slice(0, 16) : text
},
getPaymentStatusText(status) {
return getAccountBillPaymentStatusLabel(status, this.$tr('处理中'))
},
resolvePaymentStatusType(status) {
if (Number(status) === ACCOUNT_BILL_PAYMENT_STATUS.PAID) {
return 'paid'
}
if (Number(status) === ACCOUNT_BILL_PAYMENT_STATUS.PARTIAL) {
return 'partial'
}
return 'pending'
},
getRefundStatusText(status) {
return getAccountBillRefundStatusLabel(status, '--')
},
getReferenceTypeText(item = {}) {
const translatedType = getAccountReferenceTypeLabel(item.referenceType, '')
return translatedType || item.referenceTypeDesc || this.$tr('账单记录')
},
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: (res) => resolve(res),
fail: (error) => reject(error)
})
})
},
normalizePendingPaymentOrders(response) {
const payload = 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 []
},
syncPendingPaymentOrders(list = []) {
const billNos = [...new Set(
(Array.isArray(list) ? list : [])
.map(item => this.getBillNo(item))
.filter(Boolean)
)]
if (!billNos.length) {
this.pendingPaymentOrderMap = {}
return Promise.resolve({})
}
const currentReferenceTypeNo = this.currentReferenceTypeNo
return Promise.all(
billNos.map((billNo) => {
return paymentPayingBills({ BillNo: billNo })
.then((response) => {
const orderList = this.normalizePendingPaymentOrders(response)
return {
billNo,
order: orderList.length ? orderList[0] : null
}
})
.catch(() => ({
billNo,
order: null
}))
})
).then((resultList) => {
if (currentReferenceTypeNo !== this.currentReferenceTypeNo) {
return this.pendingPaymentOrderMap
}
const nextMap = resultList.reduce((result, item) => {
if (item && item.billNo && item.order) {
result[item.billNo] = item.order
}
return result
}, {})
this.pendingPaymentOrderMap = nextMap
return nextMap
})
},
getWechatLoginCode() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
const code = res && res.code ? res.code : ''
if (!code) {
reject(new Error(this.$tr('获取微信登录凭证失败')))
return
}
resolve(code)
},
fail: (error) => {
reject(error || new Error(this.$tr('获取微信登录凭证失败')))
}
})
})
},
buildBillPayPayload(item = {}, wechatCode = '') {
const payload = {
referenceTypeNo: this.currentReferenceTypeNo,
billNo: this.getBillNo(item),
payMethod: 'Wechat',
amount: Number(this.getBillRemainingAmount(item) || 0),
description: this.getReferenceTypeText(item) || this.$tr('账单支付')
}
if (wechatCode) {
payload.ExtraInfo = wechatCode
}
return payload
},
resolveNoticeText(key, zhText, enText) {
return this.language === 'en' ? enText : zhText
},
resetPaymentNoticeContext() {
this.paymentNoticeContext = {
title: '',
billLabel: '',
billNo: '--',
itemLabel: '',
itemTitle: '',
amountLabel: '',
amountText: '',
lines: [],
confirmText: '',
cancelText: ''
}
},
closePaymentNotice(confirmed = false) {
const resolver = this.paymentNoticeResolver
this.paymentNoticeResolver = null
this.showPaymentNotice = false
this.resetPaymentNoticeContext()
if (typeof resolver === 'function') {
resolver(!!confirmed)
}
},
handlePaymentNoticeClose() {
if (this.paymentNoticeResolver) {
this.closePaymentNotice(false)
} else {
this.showPaymentNotice = false
this.resetPaymentNoticeContext()
}
},
confirmPaymentNotice(item = {}) {
const billNo = this.getBillNo(item) || '--'
const amountText = `¥${this.formatMoney(this.getBillRemainingAmount(item))}`
const modalTitle = this.resolveNoticeText('paymentNoticeTitle', '支付须知', 'Notice')
const itemLabel = this.resolveNoticeText('paymentNoticeItemLabel', '费用项目', 'Item')
const billLabel = this.resolveNoticeText('paymentNoticeBillLabel', '账单号', 'Bill')
const amountLabel = this.resolveNoticeText('paymentNoticeAmountLabel', '支付金额', 'Amount')
const line1 = this.resolveNoticeText('paymentNoticeLine1', '请确认本次支付账单和金额无误后再继续付款。', 'Please confirm the bill and amount before payment.')
const line2 = this.resolveNoticeText('paymentNoticeLine2', '支付完成后,账单状态可能需要几秒同步,请勿重复支付。', 'Bill status may sync a few seconds later. Do not pay twice.')
const line3 = this.resolveNoticeText('paymentNoticeLine3', '若支付中断或页面关闭,可稍后返回账单页查看最新结果。', 'If interrupted, return later to check the latest result.')
const itemTitle = this.getReferenceTypeText(item) || this.resolveNoticeText('billPaymentDesc', '账单支付', 'Bill Payment')
return new Promise((resolve) => {
this.paymentNoticeResolver = resolve
this.paymentNoticeContext = {
title: modalTitle,
billLabel,
billNo,
itemLabel,
itemTitle,
amountLabel,
amountText,
lines: [line1, line2, line3],
confirmText: this.resolveNoticeText('paymentNoticeConfirm', '继续支付', 'Continue'),
cancelText: this.resolveNoticeText('paymentNoticeCancel', '稍后再付', 'Later')
}
this.showPaymentNotice = true
})
},
confirmCancelOrder() {
return new Promise((resolve) => {
uni.showModal({
title: this.$tr('取消申请'),
content: this.$tr('取消后将关闭当前申请单,是否继续?'),
confirmColor: '#ef4444',
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
})
})
},
buildCancelOrderPayload(item = {}) {
return {
detailCode: this.currentReferenceTypeNo
}
},
createPaymentSession(item = {}) {
this.paymentSession = {
billNo: this.getBillNo(item),
amount: Number(this.getBillRemainingAmount(item) || 0),
title: this.getReferenceTypeText(item) || this.$tr('账单支付')
}
},
resetPaymentWaitingState() {
this.payingBillNo = ''
this.paymentPollingBillNo = ''
this.paymentPollingAttempt = 0
this.paymentStage = ''
this.paymentStatusHint = ''
this.paymentSession = null
},
openPaymentFailedState(message = '') {
this.paymentStage = 'failed'
this.paymentStatusHint = message || this.$tr('暂未查询到支付成功结果,请稍后刷新账单确认。')
this.payingBillNo = ''
this.paymentPollingBillNo = this.paymentWaitingBillNo
},
handlePaymentResultAction() {
if (this.isPaymentFailedStage) {
this.resetPaymentWaitingState()
}
},
wait(ms = 0) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
},
buildPaymentQueryContext(item = {}, cashier = {}) {
const paymentInfo = this.parsePaymentInfo(cashier.paymentInfo)
return {
referenceTypeNo: this.currentReferenceTypeNo,
billNo: this.getBillNo(item),
paymentNo: cashier.paymentNo || cashier.cashierNo || cashier.orderNo || '',
tradeNo: cashier.tradeNo || cashier.outTradeNo || paymentInfo.outTradeNo || ''
}
},
queryPaymentResult(queryContext = {}) {
return paymentStatusByBillNo({
BillNo: queryContext.billNo
})
.then((response) => {
const payload = response && response.data ? response.data : response
const payStatus = Number(payload && payload.payStatus)
const statusDesc = this.language !== 'en' && payload && payload.statusDesc ? payload.statusDesc : ''
if (payStatus === PAYMENT_ORDER_STATUS.SUCCESS) {
return {
status: PAYMENT_QUERY_STATUS.SUCCESS,
message: statusDesc || this.$tr('后台确认支付成功'),
data: payload
}
}
if (payStatus === PAYMENT_ORDER_STATUS.FAILED) {
return {
status: PAYMENT_QUERY_STATUS.FAILED,
message: statusDesc || this.$tr('后台返回支付失败'),
data: payload
}
}
return {
status: PAYMENT_QUERY_STATUS.PENDING,
message: statusDesc || this.$tr('后台仍在处理中'),
data: payload
}
})
},
pollPaymentUntilSettled(item = {}, cashier = {}) {
const queryContext = this.buildPaymentQueryContext(item, cashier)
let lastResult = null
this.paymentStage = 'polling'
this.paymentPollingBillNo = queryContext.billNo
this.paymentPollingAttempt = 0
this.paymentStatusHint = this.$tr('已支付成功,正在等待后台确认账单状态。')
const executePolling = async () => {
for (let attempt = 1; attempt <= PAYMENT_QUERY_MAX_ATTEMPTS; attempt += 1) {
if (attempt > 1) {
await this.wait(PAYMENT_QUERY_INTERVAL)
}
this.paymentPollingAttempt = attempt
lastResult = await this.queryPaymentResult(queryContext)
this.paymentStatusHint = lastResult && lastResult.message ? lastResult.message : this.$tr('正在与后台核对支付状态,请稍候。')
if (lastResult && lastResult.status === PAYMENT_QUERY_STATUS.SUCCESS) {
return lastResult
}
if (lastResult && lastResult.status === PAYMENT_QUERY_STATUS.FAILED) {
throw new Error(lastResult.message || this.$tr('支付失败'))
}
}
throw new Error((lastResult && lastResult.message) || this.$tr('支付结果确认超时,请稍后刷新账单确认'))
}
return executePolling()
},
emitPaymentSuccess() {
const hasPendingBills = this.billList.length > 0
this.paymentStage = 'success'
this.paymentStatusHint = hasPendingBills
? this.$tr('支付结果已确认,当前申请单仍有其他待支付账单。')
: this.$tr('支付结果已确认,当前申请单账单已全部完成。')
this.payingBillNo = ''
this.paymentPollingBillNo = this.paymentWaitingBillNo
return this.wait(1200)
.then(() => {
this.$emit('payment-success', {
referenceTypeNo: this.currentReferenceTypeNo,
hasPendingBills,
billList: this.billList
})
if (!hasPendingBills) {
this.$emit('all-paid', {
referenceTypeNo: this.currentReferenceTypeNo
})
return
}
this.resetPaymentWaitingState()
})
},
reloadListAfterPayment() {
return this.reloadList()
.catch(() => [])
.then(() => {
this.emitPaymentSuccess()
})
},
reconcilePaymentResult(billNo = '') {
return this.reloadList()
.catch(() => [])
.then(() => {
const billStillPending = this.billList.some(item => this.getBillNo(item) === billNo)
if (!billStillPending) {
this.emitPaymentSuccess()
return true
}
return false
})
},
handleWechatPay(event) {
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
const billIndex = Number(dataset.billIndex)
const item = Number.isInteger(billIndex) && billIndex >= 0 ? (this.billList[billIndex] || {}) : {}
const billNo = this.getBillNo(item)
const pendingPaymentOrder = this.getPendingPaymentOrder(item)
const reusablePendingPaymentOrder = pendingPaymentOrder && !this.isPendingPaymentExpired(item)
? pendingPaymentOrder
: null
if (!billNo || !this.currentReferenceTypeNo) {
uni.showToast({
title: this.$tr('账单信息不完整'),
icon: 'none'
})
return
}
if (this.payingBillNo) {
return
}
let cashier = {}
Promise.resolve()
.then(() => this.confirmPaymentNotice(item))
.then((confirmed) => {
if (!confirmed) {
throw new Error('PAYMENT_NOTICE_CANCELED')
}
this.payingBillNo = billNo
this.paymentStage = 'launching'
this.paymentStatusHint = this.$tr('正在唤起微信支付,请在微信内完成付款。')
this.createPaymentSession(item)
})
.then(() => {
if (reusablePendingPaymentOrder && reusablePendingPaymentOrder.cashier && reusablePendingPaymentOrder.cashier.paymentInfo) {
cashier = reusablePendingPaymentOrder.cashier
return cashier
}
return this.getWechatLoginCode()
.then((wechatCode) => paymentBillPay(this.buildBillPayPayload(item, wechatCode)))
.then((response) => {
const payload = response && response.data ? response.data : response
cashier = payload && payload.cashier ? payload.cashier : {}
return cashier
})
})
.then(() => {
return this.requestWechatPayment(cashier.paymentInfo)
})
.then(() => {
return this.pollPaymentUntilSettled(item, cashier)
})
.then(() => {
return this.reloadListAfterPayment()
})
.catch((error) => {
const errMsg = error && (error.errMsg || error.message || error.msg) ? (error.errMsg || error.message || error.msg) : ''
if (errMsg === 'PAYMENT_NOTICE_CANCELED') {
this.resetPaymentWaitingState()
return
}
if (/cancel/i.test(errMsg)) {
this.resetPaymentWaitingState()
uni.showToast({
title: this.$tr('已取消支付'),
icon: 'none'
})
return
}
return this.reconcilePaymentResult(billNo)
.then((hasConfirmedSuccess) => {
if (hasConfirmedSuccess) {
return
}
this.openPaymentFailedState(errMsg || this.$tr('支付结果确认失败,请稍后刷新账单确认'))
})
})
.finally(() => {
if (this.isPaymentWaitingStage) {
this.resetPaymentWaitingState()
}
})
},
handleCancelOrder(event) {
const dataset = event && event.currentTarget ? event.currentTarget.dataset || {} : {}
const billIndex = Number(dataset.billIndex)
const item = Number.isInteger(billIndex) && billIndex >= 0 ? (this.billList[billIndex] || {}) : {}
const billNo = this.getBillNo(item)
if (!billNo || !this.canCancelOrder(item) || this.cancelingBillNo || this.payingBillNo) {
return
}
this.confirmCancelOrder()
.then((confirmed) => {
if (!confirmed) {
return
}
this.cancelingBillNo = billNo
return userDetailCancel(this.buildCancelOrderPayload(item), this.language)
.then(() => {
uni.showToast({
title: this.$tr('申请已取消'),
icon: 'success'
})
return this.refreshBillList()
})
})
.catch((error) => {
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('取消申请失败'),
icon: 'none'
})
})
.finally(() => {
this.cancelingBillNo = ''
})
},
refreshBillList() {
return this.ensureReferenceTypeNo()
.then((referenceTypeNo) => {
if (!referenceTypeNo) {
this.billList = []
this.pendingPaymentOrderMap = {}
return []
}
this.$emit('reference-resolved', referenceTypeNo)
return this.reloadList()
})
},
reloadList() {
if (!this.currentReferenceTypeNo) {
this.billList = []
this.pendingPaymentOrderMap = {}
return Promise.resolve([])
}
const requestKey = this.currentReferenceTypeNo
if (this._reloadListPromise && this._reloadListKey === requestKey) {
return this._reloadListPromise
}
this.loading = true
this._reloadListKey = requestKey
this._reloadListPromise = accountQueryBills({
referenceTypeNo: this.currentReferenceTypeNo,
status: ACCOUNT_BILL_STATUS.ACTIVE,
paymentStatus: ACCOUNT_BILL_PAYMENT_STATUS.UNPAID
})
.then((response) => {
this.billList = normalizeBillList(response)
return this.syncPendingPaymentOrders(this.billList)
.then(() => this.billList)
})
.catch((error) => {
this.billList = []
this.pendingPaymentOrderMap = {}
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('账单加载失败'),
icon: 'none'
})
return []
})
.finally(() => {
this.loading = false
this._reloadListPromise = null
})
return this._reloadListPromise
},
goToMyApplication() {
uni.navigateTo({
url: '/pkgService/my_application/my_application'
})
}
}
}
</script>
<style scoped lang="scss">
.hero-panel,
.bill-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);
}
.hero-title,
.empty-title,
.card-title {
font-size: 32rpx;
font-weight: 700;
color: #1A2233;
}
.hero-desc,
.empty-desc,
.card-code,
.info-label,
.remark-label {
color: #6A7A99;
}
.hero-summary {
display: flex;
gap: 20rpx;
}
.source-tag {
display: inline-flex;
align-items: center;
padding: 8rpx 18rpx;
border-radius: 999rpx;
font-size: 22rpx;
color: $brand-primary;
background: rgba($brand-primary, 0.08);
}
.summary-card {
flex: 1;
padding: 24rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%);
border: 1rpx solid rgba($brand-primary, 0.1);
}
.summary-label {
font-size: 22rpx;
color: #6A7A99;
}
.summary-value {
font-size: 40rpx;
font-weight: 700;
color: #1A2233;
}
.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-partial {
color: $brand-primary;
background: rgba($brand-primary, 0.12);
}
.status-paid {
color: #166534;
background: rgba(34, 197, 94, 0.12);
}
.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 {
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 {
margin-bottom: 12rpx;
font-size: 26rpx;
font-weight: 700;
color: #c2410c;
}
.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 {
transform: scale(0.98);
}
.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);
}
.payment-notice-popup {
background: #ffffff;
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
height: 72vh;
}
.payment-notice-popup__header {
padding: 32rpx;
text-align: center;
border-bottom: 1px solid #eef2f7;
}
.payment-notice-popup__title {
font-size: 34rpx;
font-weight: 700;
color: #172033;
}
.payment-notice-popup__body {
flex: 1;
padding: 28rpx 32rpx 32rpx;
box-sizing: border-box;
}
.payment-notice-popup__summary {
padding: 28rpx;
border-radius: 24rpx;
background: linear-gradient(180deg, rgba(248, 251, 255, 0.98) 0%, rgba(238, 246, 255, 0.98) 100%);
border: 1rpx solid rgba($brand-primary, 0.08);
}
.payment-notice-popup__summary-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24rpx;
}
.payment-notice-popup__summary-row + .payment-notice-popup__summary-row {
margin-top: 18rpx;
}
.payment-notice-popup__summary-label {
flex: 0 0 140rpx;
font-size: 24rpx;
color: #6a7a99;
}
.payment-notice-popup__summary-value {
flex: 1;
text-align: right;
font-size: 24rpx;
font-weight: 600;
color: #172033;
word-break: break-all;
}
.payment-notice-popup__summary-value--price {
color: #d9480f;
}
.payment-notice-popup__content {
margin-top: 24rpx;
padding: 28rpx;
border-radius: 24rpx;
background: #f8fafc;
}
.payment-notice-popup__line {
font-size: 25rpx;
line-height: 1.75;
color: #475569;
}
.payment-notice-popup__line + .payment-notice-popup__line {
margin-top: 18rpx;
}
.payment-notice-popup__footer {
display: flex;
gap: 20rpx;
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
border-top: 1px solid #eef2f7;
background: #ffffff;
}
.payment-notice-popup__action {
flex: 1;
height: 92rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.payment-notice-popup__action:active {
transform: scale(0.98);
}
.payment-notice-popup__action--ghost {
color: #475569;
background: #f1f5f9;
}
.payment-notice-popup__action--primary {
color: #ffffff;
background: linear-gradient(135deg, rgba($brand-primary, 0.9) 0%, $brand-primary 100%);
box-shadow: 0 16rpx 34rpx rgba($brand-primary, 0.24);
}
.payment-waiting-mask {
position: fixed;
inset: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
padding: 32rpx;
background: rgba(8, 15, 35, 0.46);
backdrop-filter: blur(12rpx);
-webkit-backdrop-filter: blur(12rpx);
}
.payment-waiting-panel {
position: relative;
width: 100%;
max-width: 640rpx;
padding: 40rpx 32rpx 34rpx;
border-radius: 36rpx;
overflow: hidden;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(244, 249, 255, 0.98) 100%);
box-shadow: 0 30rpx 80rpx rgba(15, 23, 42, 0.18);
}
.payment-waiting-panel--success {
background: linear-gradient(180deg, rgba(248, 255, 251, 0.98) 0%, rgba(240, 250, 245, 0.98) 100%);
}
.payment-waiting-panel--failed {
background: linear-gradient(180deg, rgba(255, 250, 250, 0.98) 0%, rgba(255, 244, 244, 0.98) 100%);
}
.payment-waiting-glow {
position: absolute;
border-radius: 50%;
filter: blur(12rpx);
opacity: 0.8;
pointer-events: none;
}
.payment-waiting-glow--one {
top: -56rpx;
right: -30rpx;
width: 220rpx;
height: 220rpx;
background: radial-gradient(circle, rgba(64, 158, 255, 0.24) 0%, rgba(64, 158, 255, 0) 72%);
}
.payment-waiting-glow--two {
left: -60rpx;
bottom: -80rpx;
width: 260rpx;
height: 260rpx;
background: radial-gradient(circle, rgba($brand-primary, 0.18) 0%, rgba($brand-primary, 0) 74%);
}
.payment-waiting-badge {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
padding: 10rpx 20rpx;
border-radius: 999rpx;
color: $brand-primary;
background: rgba($brand-primary, 0.08);
font-size: 22rpx;
font-weight: 700;
}
.payment-waiting-badge--success {
color: #15803d;
background: rgba(34, 197, 94, 0.12);
}
.payment-waiting-badge--failed {
color: #c2410c;
background: rgba(249, 115, 22, 0.12);
}
.payment-waiting-badge__icon {
margin-right: 8rpx;
font-size: 24rpx;
}
.payment-waiting-icon-wrap {
position: relative;
z-index: 1;
width: 180rpx;
height: 180rpx;
margin: 28rpx auto 18rpx;
display: flex;
align-items: center;
justify-content: center;
}
.payment-waiting-icon-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2rpx solid rgba($brand-primary, 0.12);
border-top-color: rgba($brand-primary, 0.4);
border-bottom-color: rgba(76, 190, 255, 0.4);
animation: paymentRotate 1.8s linear infinite;
}
.payment-waiting-icon-core {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
background: linear-gradient(135deg, $brand-primary 0%, #68B6FF 100%);
box-shadow: 0 18rpx 40rpx rgba($brand-primary, 0.28);
}
.payment-waiting-icon-core--success {
background: linear-gradient(135deg, #22c55e 0%, #34d399 100%);
box-shadow: 0 18rpx 40rpx rgba(34, 197, 94, 0.24);
}
.payment-waiting-icon-core--failed {
background: linear-gradient(135deg, #f97316 0%, #fb7185 100%);
box-shadow: 0 18rpx 40rpx rgba(249, 115, 22, 0.24);
}
.payment-waiting-icon {
font-size: 52rpx;
}
.payment-waiting-icon--spin {
animation: paymentRotate 1.1s linear infinite;
}
.payment-waiting-title {
position: relative;
z-index: 1;
text-align: center;
font-size: 38rpx;
font-weight: 700;
color: #172033;
}
.payment-waiting-desc {
position: relative;
z-index: 1;
margin-top: 14rpx;
text-align: center;
font-size: 25rpx;
line-height: 1.7;
color: #66758f;
}
.payment-progress-card,
.payment-tips-card {
position: relative;
z-index: 1;
margin-top: 28rpx;
padding: 24rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.76);
border: 1rpx solid rgba($brand-primary, 0.08);
}
.payment-progress-head {
margin-bottom: 18rpx;
}
.payment-progress-label,
.payment-summary-label,
.payment-tips-title {
font-size: 22rpx;
font-weight: 700;
color: #5d6c87;
}
.payment-progress-value {
font-size: 22rpx;
font-weight: 700;
color: $brand-primary;
}
.payment-progress-track {
width: 100%;
height: 14rpx;
border-radius: 999rpx;
overflow: hidden;
background: rgba($brand-primary, 0.08);
}
.payment-progress-bar {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, $brand-primary 0%, #78BEFF 100%);
box-shadow: 0 6rpx 18rpx rgba($brand-primary, 0.22);
transition: width 0.35s ease;
}
.payment-progress-note {
margin-top: 16rpx;
font-size: 24rpx;
color: #23314d;
line-height: 1.6;
}
.payment-summary-grid {
position: relative;
z-index: 1;
display: flex;
gap: 18rpx;
margin-top: 20rpx;
}
.payment-summary-item {
flex: 1;
min-width: 0;
padding: 24rpx;
border-radius: 24rpx;
background: linear-gradient(180deg, rgba(248, 251, 255, 0.96) 0%, rgba(238, 246, 255, 0.96) 100%);
border: 1rpx solid rgba($brand-primary, 0.08);
}
.payment-summary-value {
margin-top: 12rpx;
font-size: 26rpx;
font-weight: 700;
color: #1A2233;
word-break: break-all;
}
.payment-summary-value--mono {
font-size: 24rpx;
}
.payment-summary-value--price {
color: #d9480f;
}
.payment-tip-line {
margin-top: 14rpx;
font-size: 23rpx;
line-height: 1.7;
color: #5f6e88;
}
.payment-action-row {
position: relative;
z-index: 1;
margin-top: 28rpx;
}
.payment-action-btn {
width: 100%;
height: 92rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
background: linear-gradient(135deg, #f97316 0%, #fb7185 100%);
box-shadow: 0 16rpx 34rpx rgba(249, 115, 22, 0.24);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.payment-action-btn:active {
transform: scale(0.98);
}
@keyframes paymentRotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>