2171 lines
74 KiB
Vue
2171 lines
74 KiB
Vue
<template>
|
|
<view class="bill-panel">
|
|
<view class="hero-panel tn-margin-bottom-lg">
|
|
<view class="hero-title tn-margin-top">{{ t('heroTitle') }}</view>
|
|
<view class="hero-desc tn-margin-top-sm">{{ heroDesc }}</view>
|
|
<view v-if="currentReferenceTypeNo" class="source-tag tn-margin-top-sm">{{ t('applicationNoPrefix') }}{{ currentReferenceTypeNo }}</view>
|
|
<view class="hero-summary tn-margin-top-lg">
|
|
<view class="summary-card">
|
|
<view class="summary-label">{{ t('pendingCountLabel') }}</view>
|
|
<view class="summary-value tn-margin-top-sm">{{ billList.length }}</view>
|
|
</view>
|
|
<view class="summary-card">
|
|
<view class="summary-label">{{ t('pendingAmountLabel') }}</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">{{ t('loadingTitle') }}</view>
|
|
<view class="empty-desc tn-margin-top-sm">{{ t('loadingDesc') }}</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">{{ t('relatedBusinessNoLabel') }}</text>
|
|
<text class="info-value">{{ getBillReferenceTypeNo(item) || '--' }}</text>
|
|
</view>
|
|
<view class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('totalChargeLabel') }}</text>
|
|
<text class="info-value">¥{{ formatMoney(getBillTotalCharge(item)) }}</text>
|
|
</view>
|
|
<view class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('paidAmountLabel') }}</text>
|
|
<text class="info-value">¥{{ formatMoney(getBillPaidAmount(item)) }}</text>
|
|
</view>
|
|
<view class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('remainingAmountLabel') }}</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">{{ t('refundStatusLabel') }}</text>
|
|
<text class="info-value">{{ getRefundStatusText(getBillRefundStatus(item)) }}</text>
|
|
</view>
|
|
<view class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('createTimeLabel') }}</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">{{ t('remarkLabel') }}</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">{{ t('resultLabel') }}</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">{{ t('pendingPaymentTitle') }}</view>
|
|
<view class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('pendingPaymentOrderNoLabel') }}</text>
|
|
<text class="info-value">{{ getPendingPaymentOrderNo(item) }}</text>
|
|
</view>
|
|
<view class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('pendingPaymentAmountLabel') }}</text>
|
|
<text class="info-value info-value--highlight">¥{{ formatMoney(getPendingPaymentAmount(item)) }}</text>
|
|
</view>
|
|
<view v-if="getPendingPaymentExpireText(item)" class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('pendingPaymentExpireLabel') }}</text>
|
|
<text class="info-value" :class="{ 'info-value--expired': isPendingPaymentExpired(item) }">
|
|
{{ getPendingPaymentExpireText(item) }}
|
|
</text>
|
|
</view>
|
|
<view v-if="getPendingPaymentDescription(item)" class="info-row tn-flex tn-flex-row-between">
|
|
<text class="info-label">{{ t('pendingPaymentDescLabel') }}</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">{{ t('pendingPaymentChannelLabel') }}</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 ? t('emptyPendingTitle') : t('emptyApplicationTitle') }}</view>
|
|
<view class="empty-desc tn-margin-top-sm">{{ currentReferenceTypeNo ? t('emptyPendingDesc') : t('emptyApplicationDesc') }}</view>
|
|
<view v-if="!currentReferenceTypeNo" class="empty-action tn-margin-top-lg" @tap="goToMyApplication">
|
|
<text class="tn-text-bold">{{ t('goToMyApplication') }}</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">{{ t('paymentProgressLabel') }}</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">{{ t('billNoLabel') }}</view>
|
|
<view class="payment-summary-value payment-summary-value--mono">{{ paymentWaitingBillNo || '--' }}</view>
|
|
</view>
|
|
<view class="payment-summary-item">
|
|
<view class="payment-summary-label">{{ t('currentPaymentLabel') }}</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, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
|
|
|
|
const VIEWING_APPLICATION_PATTERN = /预约看房申请|看房时间:|意向房型:|意向区域:|viewing request|viewing time|preferred room type|preferred area/i
|
|
|
|
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) => {
|
|
const remark = item && item.remark ? String(item.remark) : ''
|
|
const isViewingApplication = VIEWING_APPLICATION_PATTERN.test(remark)
|
|
return {
|
|
...item,
|
|
isViewingApplication
|
|
}
|
|
})
|
|
}
|
|
|
|
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: ''
|
|
},
|
|
i18n: {
|
|
zh: {
|
|
heroTitle: '待支付账单',
|
|
applicationNoPrefix: '申请单号:',
|
|
pendingCountLabel: '待支付数量',
|
|
pendingAmountLabel: '待支付总额',
|
|
loadingTitle: '账单加载中',
|
|
loadingDesc: '正在同步您的待支付账单,请稍候。',
|
|
relatedBusinessNoLabel: '关联业务编号',
|
|
totalChargeLabel: '应收合计',
|
|
paidAmountLabel: '已付金额',
|
|
remainingAmountLabel: '待付金额',
|
|
refundStatusLabel: '退款状态',
|
|
createTimeLabel: '创建时间',
|
|
remarkLabel: '备注',
|
|
resultLabel: '处理结果',
|
|
pendingPaymentTitle: '未完成支付订单',
|
|
pendingPaymentOrderNoLabel: '支付单号',
|
|
pendingPaymentAmountLabel: '订单金额',
|
|
pendingPaymentDescLabel: '支付说明',
|
|
pendingPaymentChannelLabel: '支付渠道',
|
|
pendingPaymentExpireLabel: '剩余支付时间',
|
|
pendingPaymentExpired: '已失效',
|
|
emptyPendingTitle: '暂无待支付账单',
|
|
emptyApplicationTitle: '暂无可查询申请单',
|
|
emptyPendingDesc: '当前申请单下没有查询到待支付账单,后续有新账单时会在这里展示。',
|
|
emptyApplicationDesc: '当前未找到可关联的申请单,请先前往“我的申请”选择具体申请后查看账单。',
|
|
goToMyApplication: '前往我的申请',
|
|
paymentProgressLabel: '结果确认进度',
|
|
billNoLabel: '账单号',
|
|
currentPaymentLabel: '本次支付',
|
|
heroDescResolved: '当前页面已按申请单编号查询其关联的待支付账单记录。',
|
|
heroDescAutoResolve: '当前将优先读取最近申请单的编号来查询关联账单。',
|
|
paymentStageLaunching: '正在唤起微信收银台',
|
|
paymentStagePolling: '正在同步支付结果',
|
|
paymentStageSuccess: '支付结果已确认',
|
|
paymentStageFailed: '支付未完成',
|
|
paymentStageDefault: '支付处理中',
|
|
paymentTitleLaunching: '请完成微信支付',
|
|
paymentTitlePolling: '正在确认是否支付成功',
|
|
paymentTitleSuccess: '支付成功',
|
|
paymentTitleFailed: '支付结果确认失败',
|
|
paymentTitleDefault: '支付处理中',
|
|
paymentDescLaunching: '系统正在调起微信支付,请在微信收银台中完成付款后返回。',
|
|
paymentDescPolling: '支付完成后,系统会自动向后台查询支付状态,无需手动刷新。',
|
|
paymentDescSuccess: '本次支付已同步完成,账单状态已更新。',
|
|
paymentDescFailed: '系统暂时未能确认支付成功,您可以查看提示后稍后重试。',
|
|
paymentProgressWaiting: '等待支付完成',
|
|
paymentHintLaunching: '已为您准备好支付信息,等待微信支付完成。',
|
|
paymentHintPolling: '正在与后台核对支付状态,请稍候。',
|
|
paymentTipsTitleFailed: '处理建议',
|
|
paymentTipsTitleSuccess: '完成说明',
|
|
paymentTipsTitleDefault: '温馨提示',
|
|
paymentTipsFailed1: '若您已完成付款,请稍后刷新账单,避免重复支付。',
|
|
paymentTipsFailed2: '若微信侧已扣款但页面未更新,请联系管家或客服协助核实。',
|
|
paymentTipsFailed3: '若未扣款,可返回账单列表后重新发起支付。',
|
|
paymentTipsSuccess1: '若当前申请单还有其他待付账单,可继续在当前页完成支付。',
|
|
paymentTipsSuccess2: '若当前账单已全部支付完成,系统会自动返回首页。',
|
|
paymentTipsSuccess3: '支付记录同步可能略有延迟,刷新后即可看到最新状态。',
|
|
paymentTipsDefault1: '支付成功后,系统可能需要几秒钟同步账单状态。',
|
|
paymentTipsDefault2: '请不要重复点击支付,也不要提前关闭当前页面。',
|
|
paymentTipsDefault3: '如果超时未完成,系统会自动再校验一次账单状态。',
|
|
paymentActionAcknowledge: '我知道了',
|
|
buttonConfirming: '确认结果中...',
|
|
buttonPolling: '查询结果中({current}/{total})',
|
|
buttonLaunching: '拉起支付中...',
|
|
wechatPayButton: '微信支付',
|
|
continuePayButton: '继续支付',
|
|
paymentStatusPending: '处理中',
|
|
billRecordFallback: '账单记录',
|
|
paymentParamIncomplete: '微信支付参数不完整',
|
|
wechatCodeFailed: '获取微信登录凭证失败',
|
|
billPaymentDesc: '账单支付',
|
|
paymentFailedHint: '暂未查询到支付成功结果,请稍后刷新账单确认。',
|
|
paymentResultSuccess: '后台确认支付成功',
|
|
paymentResultFailed: '后台返回支付失败',
|
|
paymentResultPending: '后台仍在处理中',
|
|
paymentHintPaidWaiting: '已支付成功,正在等待后台确认账单状态。',
|
|
paymentFailedError: '支付失败',
|
|
paymentTimeoutError: '支付结果确认超时,请稍后刷新账单确认',
|
|
paymentSuccessPendingBills: '支付结果已确认,当前申请单仍有其他待支付账单。',
|
|
paymentSuccessAllDone: '支付结果已确认,当前申请单账单已全部完成。',
|
|
billInfoIncomplete: '账单信息不完整',
|
|
paymentNoticeTitle: '用户须知',
|
|
paymentNoticeBillLabel: '账单号',
|
|
paymentNoticeAmountLabel: '支付金额',
|
|
paymentNoticeItemLabel: '费用项目',
|
|
paymentNoticeLine1: '请确认本次支付账单和金额无误后再继续付款。',
|
|
paymentNoticeLine2: '支付完成后,账单状态可能需要几秒同步,请勿重复支付。',
|
|
paymentNoticeLine3: '若支付中断或页面关闭,可稍后返回账单页查看最新结果。',
|
|
paymentNoticeConfirm: '继续支付',
|
|
paymentNoticeCancel: '稍后再付',
|
|
paymentHintLaunchingToast: '正在唤起微信支付,请在微信内完成付款。',
|
|
paymentCanceled: '已取消支付',
|
|
paymentResultFailedRetry: '支付结果确认失败,请稍后刷新账单确认',
|
|
billLoadFailed: '账单加载失败',
|
|
cancelOrderButton: '取消订单',
|
|
cancelOrderLoading: '取消中...',
|
|
cancelOrderTitle: '取消订单',
|
|
cancelOrderContent: '取消后将关闭当前未支付订单,是否继续?',
|
|
cancelOrderSuccess: '订单已取消',
|
|
cancelOrderFailed: '取消订单失败'
|
|
},
|
|
en: {
|
|
heroTitle: 'Pending Bills',
|
|
applicationNoPrefix: 'Application No: ',
|
|
pendingCountLabel: 'Pending Count',
|
|
pendingAmountLabel: 'Pending Amount',
|
|
loadingTitle: 'Loading Bills',
|
|
loadingDesc: 'Syncing your pending bills, please wait.',
|
|
relatedBusinessNoLabel: 'Related Business No.',
|
|
totalChargeLabel: 'Total Charge',
|
|
paidAmountLabel: 'Paid Amount',
|
|
remainingAmountLabel: 'Remaining Amount',
|
|
refundStatusLabel: 'Refund Status',
|
|
createTimeLabel: 'Created At',
|
|
remarkLabel: 'Remark',
|
|
resultLabel: 'Result',
|
|
pendingPaymentTitle: 'Unfinished Payment',
|
|
pendingPaymentOrderNoLabel: 'Payment Order No.',
|
|
pendingPaymentAmountLabel: 'Order Amount',
|
|
pendingPaymentDescLabel: 'Description',
|
|
pendingPaymentChannelLabel: 'Channel',
|
|
pendingPaymentExpireLabel: 'Time Left',
|
|
pendingPaymentExpired: 'Expired',
|
|
emptyPendingTitle: 'No Pending Bills',
|
|
emptyApplicationTitle: 'No Application Available',
|
|
emptyPendingDesc: 'No pending bills are found under the current application. New bills will appear here later.',
|
|
emptyApplicationDesc: 'No related application is found. Please go to "My Applications" and select an application first.',
|
|
goToMyApplication: 'Go to My Applications',
|
|
paymentProgressLabel: 'Confirmation Progress',
|
|
billNoLabel: 'Bill No.',
|
|
currentPaymentLabel: 'This Payment',
|
|
heroDescResolved: 'This page is querying pending bills by the current application number.',
|
|
heroDescAutoResolve: 'The latest application number will be used first to query related bills.',
|
|
paymentStageLaunching: 'Launching Wechat Pay',
|
|
paymentStagePolling: 'Syncing Payment Result',
|
|
paymentStageSuccess: 'Payment Confirmed',
|
|
paymentStageFailed: 'Payment Incomplete',
|
|
paymentStageDefault: 'Processing Payment',
|
|
paymentTitleLaunching: 'Complete Wechat Pay',
|
|
paymentTitlePolling: 'Confirming Payment Result',
|
|
paymentTitleSuccess: 'Payment Successful',
|
|
paymentTitleFailed: 'Payment Confirmation Failed',
|
|
paymentTitleDefault: 'Processing Payment',
|
|
paymentDescLaunching: 'Wechat Pay is being launched. Complete the payment in Wechat and return here.',
|
|
paymentDescPolling: 'After payment, the system checks the payment status automatically. No manual refresh is needed.',
|
|
paymentDescSuccess: 'This payment has been synchronized and the bill status is updated.',
|
|
paymentDescFailed: 'The system cannot confirm the payment for now. Please review the tips and try again later.',
|
|
paymentProgressWaiting: 'Waiting for Payment',
|
|
paymentHintLaunching: 'Payment information is ready. Waiting for Wechat payment to complete.',
|
|
paymentHintPolling: 'Checking payment status with the backend, please wait.',
|
|
paymentTipsTitleFailed: 'Suggestions',
|
|
paymentTipsTitleSuccess: 'Completion Notes',
|
|
paymentTipsTitleDefault: 'Tips',
|
|
paymentTipsFailed1: 'If you have completed the payment, refresh the bill later to avoid duplicate payment.',
|
|
paymentTipsFailed2: 'If Wechat has deducted the amount but the page is not updated, contact your manager or support for verification.',
|
|
paymentTipsFailed3: 'If no deduction occurred, return to the bill list and try again.',
|
|
paymentTipsSuccess1: 'If there are other pending bills under this application, you can continue paying on this page.',
|
|
paymentTipsSuccess2: 'If all bills are paid, the system returns to the home page automatically.',
|
|
paymentTipsSuccess3: 'Payment record synchronization may be slightly delayed. Refresh to see the latest status.',
|
|
paymentTipsDefault1: 'After successful payment, the system may need a few seconds to sync the bill status.',
|
|
paymentTipsDefault2: 'Do not tap the pay button repeatedly or close this page too early.',
|
|
paymentTipsDefault3: 'If the process times out, the system checks the bill status again automatically.',
|
|
paymentActionAcknowledge: 'Got It',
|
|
buttonConfirming: 'Confirming...',
|
|
buttonPolling: 'Checking ({current}/{total})',
|
|
buttonLaunching: 'Launching...',
|
|
wechatPayButton: 'Wechat Pay',
|
|
continuePayButton: 'Continue Pay',
|
|
paymentStatusPending: 'Processing',
|
|
billRecordFallback: 'Bill Record',
|
|
paymentParamIncomplete: 'Incomplete Wechat payment parameters',
|
|
wechatCodeFailed: 'Failed to get Wechat login code',
|
|
billPaymentDesc: 'Bill Payment',
|
|
paymentFailedHint: 'Payment success is not confirmed yet. Please refresh the bill later.',
|
|
paymentResultSuccess: 'Backend confirmed payment success',
|
|
paymentResultFailed: 'Backend returned payment failure',
|
|
paymentResultPending: 'Backend is still processing',
|
|
paymentHintPaidWaiting: 'Payment succeeded. Waiting for backend confirmation of the bill status.',
|
|
paymentFailedError: 'Payment failed',
|
|
paymentTimeoutError: 'Payment confirmation timed out. Please refresh the bill later.',
|
|
paymentSuccessPendingBills: 'Payment is confirmed. There are still other unpaid bills under this application.',
|
|
paymentSuccessAllDone: 'Payment is confirmed. All bills under this application are completed.',
|
|
billInfoIncomplete: 'Incomplete bill information',
|
|
paymentNoticeTitle: 'Payment Notice',
|
|
paymentNoticeBillLabel: 'Bill No.',
|
|
paymentNoticeAmountLabel: 'Amount',
|
|
paymentNoticeItemLabel: 'Charge Item',
|
|
paymentNoticeLine1: 'Please confirm the bill and payment amount before continuing.',
|
|
paymentNoticeLine2: 'After payment, the bill status may take a few seconds to sync. Please do not pay repeatedly.',
|
|
paymentNoticeLine3: 'If the payment is interrupted or the page is closed, return to the bill page later to check the latest result.',
|
|
paymentNoticeConfirm: 'Continue',
|
|
paymentNoticeCancel: 'Later',
|
|
paymentHintLaunchingToast: 'Launching Wechat Pay. Please complete the payment in Wechat.',
|
|
paymentCanceled: 'Payment canceled',
|
|
paymentResultFailedRetry: 'Payment confirmation failed. Please refresh the bill later.',
|
|
billLoadFailed: 'Failed to load bills',
|
|
cancelOrderButton: 'Cancel Order',
|
|
cancelOrderLoading: 'Canceling...',
|
|
cancelOrderTitle: 'Cancel Order',
|
|
cancelOrderContent: 'This will close the current unpaid order. Continue?',
|
|
cancelOrderSuccess: 'Order canceled',
|
|
cancelOrderFailed: 'Failed to cancel order'
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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.t('heroDescResolved')
|
|
}
|
|
return this.t('heroDescAutoResolve')
|
|
},
|
|
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.t('paymentStageLaunching')
|
|
}
|
|
if (this.paymentStage === 'polling') {
|
|
return this.t('paymentStagePolling')
|
|
}
|
|
if (this.paymentStage === 'success') {
|
|
return this.t('paymentStageSuccess')
|
|
}
|
|
if (this.paymentStage === 'failed') {
|
|
return this.t('paymentStageFailed')
|
|
}
|
|
return this.t('paymentStageDefault')
|
|
},
|
|
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.t('paymentTitleLaunching')
|
|
}
|
|
if (this.paymentStage === 'polling') {
|
|
return this.t('paymentTitlePolling')
|
|
}
|
|
if (this.paymentStage === 'success') {
|
|
return this.t('paymentTitleSuccess')
|
|
}
|
|
if (this.paymentStage === 'failed') {
|
|
return this.t('paymentTitleFailed')
|
|
}
|
|
return this.t('paymentTitleDefault')
|
|
},
|
|
paymentWaitingDesc() {
|
|
if (this.paymentStage === 'launching') {
|
|
return this.t('paymentDescLaunching')
|
|
}
|
|
if (this.paymentStage === 'polling') {
|
|
return this.t('paymentDescPolling')
|
|
}
|
|
if (this.paymentStage === 'success') {
|
|
return this.t('paymentDescSuccess')
|
|
}
|
|
if (this.paymentStage === 'failed') {
|
|
return this.t('paymentDescFailed')
|
|
}
|
|
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.t('paymentProgressWaiting')
|
|
}
|
|
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.t('paymentHintLaunching')
|
|
}
|
|
return this.t('paymentHintPolling')
|
|
},
|
|
paymentTipsTitle() {
|
|
if (this.isPaymentFailedStage) {
|
|
return this.t('paymentTipsTitleFailed')
|
|
}
|
|
if (this.isPaymentSuccessStage) {
|
|
return this.t('paymentTipsTitleSuccess')
|
|
}
|
|
return this.t('paymentTipsTitleDefault')
|
|
},
|
|
paymentTips() {
|
|
if (this.isPaymentFailedStage) {
|
|
return [
|
|
this.t('paymentTipsFailed1'),
|
|
this.t('paymentTipsFailed2'),
|
|
this.t('paymentTipsFailed3')
|
|
]
|
|
}
|
|
if (this.isPaymentSuccessStage) {
|
|
return [
|
|
this.t('paymentTipsSuccess1'),
|
|
this.t('paymentTipsSuccess2'),
|
|
this.t('paymentTipsSuccess3')
|
|
]
|
|
}
|
|
return [
|
|
this.t('paymentTipsDefault1'),
|
|
this.t('paymentTipsDefault2'),
|
|
this.t('paymentTipsDefault3')
|
|
]
|
|
},
|
|
showPaymentActionButton() {
|
|
return this.isPaymentFailedStage
|
|
},
|
|
paymentActionText() {
|
|
return this.isPaymentFailedStage ? this.t('paymentActionAcknowledge') : ''
|
|
}
|
|
},
|
|
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: {
|
|
t(key, params) {
|
|
const translatedText = this.$tm('pkgService.components.CurrentApplicationBillPanel', key, params)
|
|
if (translatedText && translatedText !== key) {
|
|
return translatedText
|
|
}
|
|
|
|
const languagePack = this.i18n && this.i18n[this.language] ? this.i18n[this.language] : this.i18n.zh
|
|
const fallbackText = languagePack && languagePack[key] ? languagePack[key] : key
|
|
if (!params || typeof fallbackText !== 'string') {
|
|
return fallbackText
|
|
}
|
|
|
|
return fallbackText.replace(/\{(\w+)\}/g, (match, token) => {
|
|
return Object.prototype.hasOwnProperty.call(params, token) ? params[token] : match
|
|
})
|
|
},
|
|
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.t('paymentStatusPending')
|
|
},
|
|
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.t('pendingPaymentExpired')
|
|
}
|
|
return this.formatPendingPaymentCountdown(this.getPendingPaymentRemainingSeconds(item))
|
|
},
|
|
getPayButtonText(item = {}) {
|
|
const billNo = this.getBillNo(item)
|
|
if (this.paymentPollingBillNo && this.paymentPollingBillNo === billNo) {
|
|
if (!this.paymentPollingAttempt) {
|
|
return this.t('buttonConfirming')
|
|
}
|
|
return this.t('buttonPolling', {
|
|
current: this.paymentPollingAttempt,
|
|
total: PAYMENT_QUERY_MAX_ATTEMPTS
|
|
})
|
|
}
|
|
if (this.payingBillNo === billNo) {
|
|
return this.paymentStage === 'launching' ? this.t('buttonLaunching') : this.t('buttonConfirming')
|
|
}
|
|
if (this.getPendingPaymentOrder(item) && !this.isPendingPaymentExpired(item)) {
|
|
return this.t('continuePayButton')
|
|
}
|
|
return this.t('wechatPayButton')
|
|
},
|
|
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.t('cancelOrderLoading')
|
|
: this.t('cancelOrderButton')
|
|
},
|
|
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 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.t('paymentStatusPending'))
|
|
},
|
|
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.t('billRecordFallback')
|
|
},
|
|
parsePaymentInfo(paymentInfo) {
|
|
if (!paymentInfo) {
|
|
return {}
|
|
}
|
|
if (typeof paymentInfo === 'object') {
|
|
return paymentInfo
|
|
}
|
|
const text = String(paymentInfo).trim()
|
|
if (!text) {
|
|
return {}
|
|
}
|
|
try {
|
|
return JSON.parse(text)
|
|
} catch (error) {
|
|
return {}
|
|
}
|
|
},
|
|
normalizeWechatPayOptions(paymentInfo) {
|
|
const source = this.parsePaymentInfo(paymentInfo)
|
|
return {
|
|
provider: 'wxpay',
|
|
timeStamp: String(source.timeStamp || source.timestamp || source.time_stamp || ''),
|
|
nonceStr: source.nonceStr || source.noncestr || source.nonce_str || '',
|
|
package: source.package || source.packageValue || source.package_value || '',
|
|
signType: source.signType || source.sign_type || 'MD5',
|
|
paySign: source.paySign || source.paysign || source.pay_sign || ''
|
|
}
|
|
},
|
|
requestWechatPayment(paymentInfo) {
|
|
const payOptions = this.normalizeWechatPayOptions(paymentInfo)
|
|
if (!payOptions.timeStamp || !payOptions.nonceStr || !payOptions.package || !payOptions.paySign) {
|
|
return Promise.reject(new Error(this.t('paymentParamIncomplete')))
|
|
}
|
|
|
|
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.t('wechatCodeFailed')))
|
|
return
|
|
}
|
|
resolve(code)
|
|
},
|
|
fail: (error) => {
|
|
reject(error || new Error(this.t('wechatCodeFailed')))
|
|
}
|
|
})
|
|
})
|
|
},
|
|
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.t('billPaymentDesc')
|
|
}
|
|
if (wechatCode) {
|
|
payload.ExtraInfo = wechatCode
|
|
}
|
|
return payload
|
|
},
|
|
resolveNoticeText(key, zhText, enText) {
|
|
const translatedText = this.t(key)
|
|
if (translatedText && translatedText !== key) {
|
|
return translatedText
|
|
}
|
|
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.t('cancelOrderTitle'),
|
|
content: this.t('cancelOrderContent'),
|
|
confirmColor: '#ef4444',
|
|
success: (res) => resolve(!!(res && res.confirm)),
|
|
fail: () => resolve(false)
|
|
})
|
|
})
|
|
},
|
|
buildCancelOrderPayload(item = {}) {
|
|
const pendingOrder = this.getPendingPaymentOrder(item) || {}
|
|
const billNo = this.getBillNo(item)
|
|
const paymentNo = pendingOrder.paymentNo || pendingOrder.payNo || pendingOrder.payRequestNo || ''
|
|
return {
|
|
detailCode: this.currentReferenceTypeNo,
|
|
code: this.currentReferenceTypeNo,
|
|
referenceTypeNo: this.currentReferenceTypeNo,
|
|
billNo,
|
|
BillNo: billNo,
|
|
paymentNo,
|
|
payNo: paymentNo,
|
|
payRequestNo: pendingOrder.payRequestNo || ''
|
|
}
|
|
},
|
|
createPaymentSession(item = {}) {
|
|
this.paymentSession = {
|
|
billNo: this.getBillNo(item),
|
|
amount: Number(this.getBillRemainingAmount(item) || 0),
|
|
title: this.getReferenceTypeText(item) || this.t('billPaymentDesc')
|
|
}
|
|
},
|
|
resetPaymentWaitingState() {
|
|
this.payingBillNo = ''
|
|
this.paymentPollingBillNo = ''
|
|
this.paymentPollingAttempt = 0
|
|
this.paymentStage = ''
|
|
this.paymentStatusHint = ''
|
|
this.paymentSession = null
|
|
},
|
|
openPaymentFailedState(message = '') {
|
|
this.paymentStage = 'failed'
|
|
this.paymentStatusHint = message || this.t('paymentFailedHint')
|
|
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.t('paymentResultSuccess'),
|
|
data: payload
|
|
}
|
|
}
|
|
if (payStatus === PAYMENT_ORDER_STATUS.FAILED) {
|
|
return {
|
|
status: PAYMENT_QUERY_STATUS.FAILED,
|
|
message: statusDesc || this.t('paymentResultFailed'),
|
|
data: payload
|
|
}
|
|
}
|
|
return {
|
|
status: PAYMENT_QUERY_STATUS.PENDING,
|
|
message: statusDesc || this.t('paymentResultPending'),
|
|
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.t('paymentHintPaidWaiting')
|
|
|
|
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.t('paymentHintPolling')
|
|
if (lastResult && lastResult.status === PAYMENT_QUERY_STATUS.SUCCESS) {
|
|
return lastResult
|
|
}
|
|
if (lastResult && lastResult.status === PAYMENT_QUERY_STATUS.FAILED) {
|
|
throw new Error(lastResult.message || this.t('paymentFailedError'))
|
|
}
|
|
}
|
|
throw new Error((lastResult && lastResult.message) || this.t('paymentTimeoutError'))
|
|
}
|
|
|
|
return executePolling()
|
|
},
|
|
emitPaymentSuccess() {
|
|
const hasPendingBills = this.billList.length > 0
|
|
this.paymentStage = 'success'
|
|
this.paymentStatusHint = hasPendingBills
|
|
? this.t('paymentSuccessPendingBills')
|
|
: this.t('paymentSuccessAllDone')
|
|
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.t('billInfoIncomplete'),
|
|
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.t('paymentHintLaunchingToast')
|
|
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.t('paymentCanceled'),
|
|
icon: 'none'
|
|
})
|
|
return
|
|
}
|
|
return this.reconcilePaymentResult(billNo)
|
|
.then((hasConfirmedSuccess) => {
|
|
if (hasConfirmedSuccess) {
|
|
return
|
|
}
|
|
this.openPaymentFailedState(errMsg || this.t('paymentResultFailedRetry'))
|
|
})
|
|
})
|
|
.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.t('cancelOrderSuccess'),
|
|
icon: 'success'
|
|
})
|
|
return this.refreshBillList()
|
|
})
|
|
})
|
|
.catch((error) => {
|
|
uni.showToast({
|
|
title: (error && (error.message || error.msg)) || this.t('cancelOrderFailed'),
|
|
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.t('billLoadFailed'),
|
|
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>
|