feat: 优化首页服务入口与租住业务流程

补齐首页待办跳转、押金支付引导和多类租住服务页面能力,统一租客端关键业务流程体验。
This commit is contained in:
liangjian
2026-08-25 13:53:12 +08:00
parent 1111ac148b
commit 9fee2c3a74
41 changed files with 6985 additions and 933 deletions

View File

@@ -23,16 +23,28 @@
</view>
</view>
<view
v-if="showPaymentStatusPanel"
class="payment-status-panel tn-margin-bottom-lg"
:class="`payment-status-panel--${paymentStatusType}`"
>
<view class="payment-status-panel__label">{{ $tr('支付状态') }}</view>
<view class="payment-status-panel__main">
<view class="payment-status-panel__value">{{ paymentStatusText }}</view>
<view v-if="canViewBill" class="payment-status-panel__hint">{{ $tr('请尽快完成定金支付') }}</view>
</view>
</view>
<view
v-if="statusReminderText"
class="status-reminder-panel tn-margin-bottom-lg"
:class="{
'status-reminder-panel--warning': shouldPayDeposit(detail),
'status-reminder-panel--warning': shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail),
'status-reminder-panel--info': isWaitingRoomAssignment(detail)
}"
>
<view class="status-reminder-panel__icon">
<text :class="shouldPayDeposit(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
<text :class="shouldPayDeposit(detail) && !isWaitingButlerRoomAssignment(detail) ? 'tn-icon-warning-fill' : 'tn-icon-time-fill'"></text>
</view>
<view class="status-reminder-panel__content">
<view class="status-reminder-panel__title">{{ $tr('当前进度提醒') }}</view>
@@ -145,7 +157,7 @@
</template>
<script>
import { contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
import { accountQueryBills, contractDetail, orderSearchedReservationFlow, roomInspectionDetail, userDetailCancel } from '@/api/index.js'
import { getMyApplicationList, saveActiveApplicationCode, saveMyApplicationList } from '@/utils/app-state'
import {
RESERVATION_FLOW_STEP,
@@ -202,6 +214,43 @@ function normalizeApplicationList(list = []) {
})
}
function resolveApplicationPaymentStatusInfo(item = {}) {
if (!item || item.isViewingApplication) {
return {
key: 'exempt',
desc: ''
}
}
const desc = String(item.payStatusDesc || item.paymentStatusDesc || '').trim()
const status = Number(
item.payStatus !== undefined && item.payStatus !== null
? item.payStatus
: item.paymentStatus
)
if (status === 10 || /已支付|paid/i.test(desc)) {
return { key: 'paid', desc }
}
if (/退款|refunded?/i.test(desc)) {
return { key: 'refunded', desc }
}
if (/失败|failed?/i.test(desc)) {
return { key: 'failed', desc }
}
if (/处理中|processing/i.test(desc)) {
return { key: 'processing', desc }
}
if (status === 0 || /待支付|未支付|unpaid|pending/i.test(desc)) {
return { key: 'pending', desc }
}
return {
key: desc ? 'processing' : 'pending',
desc
}
}
function parseJsonStringMaybe(value) {
if (typeof value !== 'string') {
return value
@@ -233,6 +282,7 @@ export default {
watch: {
language() {
this.loadReservationFlow()
this.loadPendingDepositBillState()
}
},
computed: {
@@ -278,6 +328,42 @@ export default {
heroBadgeClass() {
return this.isViewingApplication ? 'badge-viewing' : 'badge-stay'
},
showPaymentStatusPanel() {
return !this.isViewingApplication
},
paymentStatusText() {
if (this.isWaitingButlerRoomAssignment(this.detail)) {
return this.$tr('等待管家排房')
}
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
if (this.$getLanguage() !== 'en' && statusInfo.desc) {
return statusInfo.desc
}
const textMap = {
exempt: this.$tr('无需支付'),
paid: this.$tr('已支付'),
refunded: this.$tr('已退款'),
failed: this.$tr('支付失败'),
processing: this.$tr('处理中'),
pending: this.$tr('待支付')
}
return textMap[statusInfo.key] || statusInfo.desc || this.$tr('待支付')
},
paymentStatusType() {
if (this.isWaitingButlerRoomAssignment(this.detail)) {
return 'info'
}
const statusInfo = resolveApplicationPaymentStatusInfo(this.detail)
const typeMap = {
exempt: 'neutral',
paid: 'success',
refunded: 'neutral',
failed: 'danger',
processing: 'info',
pending: 'warning'
}
return typeMap[statusInfo.key] || 'warning'
},
displayDateRange() {
if (!this.detail.start && !this.detail.end) {
return '--'
@@ -329,26 +415,36 @@ export default {
if (reservationStepText) {
return reservationStepText
}
if (this.isWaitingButlerRoomAssignment(this.detail)) {
return this.$tr('等待管家排房')
}
if (this.shouldPayDeposit(this.detail)) {
return this.$tr('请支付定金')
}
if (this.isWaitingRoomAssignment(this.detail)) {
return this.$tr('等待预订中心分配房间')
return this.$tr('等待管家排房')
}
return ''
},
canViewBill() {
return this.shouldPayDeposit(this.detail)
&& this.hasPendingDepositBill(this.detail)
&& !this.isViewingApplication
&& this.detail
&& this.detail.detailCode
&& this.statusType === 'pending'
},
canCancelApplication() {
return !!(this.detail
&& this.detail.detailCode
&& Number(this.detail.status) === 0
&& (this.isViewingApplication || Number(this.detail.payStatus) !== 10))
if (!(this.detail && this.detail.detailCode && Number(this.detail.status) === 0)) {
return false
}
if (this.isViewingApplication) {
return true
}
if (Number(this.detail.payStatus) === 10) {
return false
}
return this.hasPendingDepositBill(this.detail)
},
canCancelOrder() {
return this.canCancelApplication && !this.canceling
@@ -360,11 +456,39 @@ export default {
},
onShow() {
this.loadReservationFlow()
this.loadPendingDepositBillState()
},
methods: {
getBillNo(item = {}) {
return item.no || item.billNo || ''
},
normalizeBillList(response) {
const payload = parseJsonStringMaybe(response && response.data ? response.data : response)
if (Array.isArray(payload)) {
return payload.filter(item => item && typeof item === 'object')
}
if (payload && Array.isArray(payload.data)) {
return payload.data.filter(item => item && typeof item === 'object')
}
return []
},
isPaidApplication(item = {}) {
return !item.isViewingApplication && Number(item.payStatus) === 10
},
shouldQueryPendingDepositBill(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
&& Number(item.payStatus) !== 10
&& !!String(item.detailCode || this.detailCode || '').trim()
},
hasPendingDepositBill(item = {}) {
return !!String(this.getBillNo(item && item.pendingDepositBill ? item.pendingDepositBill : {}) || '').trim()
},
isWaitingButlerRoomAssignment(item = {}) {
return this.shouldQueryPendingDepositBill(item)
&& !!(item && item.pendingDepositBillQueried)
&& !this.hasPendingDepositBill(item)
},
shouldPayDeposit(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
@@ -373,7 +497,7 @@ export default {
isWaitingRoomAssignment(item = {}) {
return !item.isViewingApplication
&& Number(item.status) === 0
&& Number(item.payStatus) === 10
&& (Number(item.payStatus) === 10 || this.isWaitingButlerRoomAssignment(item))
},
getReservationCurrentStepValue() {
return Number(this.detail && this.detail.reservationCurrentNode ? this.detail.reservationCurrentNode.step : 0)
@@ -493,6 +617,50 @@ export default {
isViewingApplication: false
}
},
syncDetailToCache(detail = this.detail) {
const list = normalizeApplicationList(getMyApplicationList())
if (!list.length || !detail || !detail.detailCode) {
return
}
saveMyApplicationList(list.map((item) => {
if (String(item.detailCode || '') !== String(detail.detailCode || '')) {
return item
}
return {
...item,
...detail
}
}))
},
async loadPendingDepositBillState() {
const currentDetail = this.detail && typeof this.detail === 'object' ? this.detail : {}
const referenceTypeNo = String(currentDetail.detailCode || this.detailCode || '').trim()
if (!this.shouldQueryPendingDepositBill(currentDetail) || !referenceTypeNo) {
return
}
try {
const response = await accountQueryBills({
referenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
}, this.language)
if (referenceTypeNo !== String(this.detail && this.detail.detailCode || this.detailCode || '').trim()) {
return
}
const billList = this.normalizeBillList(response)
const nextDetail = {
...this.detail,
pendingDepositBill: Array.isArray(billList) && billList.length ? billList[0] : null,
pendingDepositBillQueried: true
}
this.detail = nextDetail
this.syncDetailToCache(nextDetail)
} catch (error) {
console.error('load pending deposit bill failed', error)
}
},
async loadReservationFlow() {
if (!this.detail || this.detail.isViewingApplication) {
return
@@ -782,6 +950,85 @@ export default {
border: 1px solid rgba(59, 130, 246, 0.14);
}
.payment-status-panel {
padding: 24rpx 28rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
border: 1rpx solid transparent;
}
.payment-status-panel__label {
flex-shrink: 0;
font-size: 24rpx;
color: #64748B;
font-weight: 600;
}
.payment-status-panel__main {
min-width: 0;
text-align: right;
}
.payment-status-panel__value {
font-size: 34rpx;
line-height: 1.2;
font-weight: 700;
}
.payment-status-panel__hint {
margin-top: 8rpx;
font-size: 22rpx;
color: #9A3412;
}
.payment-status-panel--warning {
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
border-color: rgba(249, 115, 22, 0.16);
}
.payment-status-panel--warning .payment-status-panel__value {
color: #C2410C;
}
.payment-status-panel--success {
background: linear-gradient(180deg, rgba(236, 253, 245, 0.96) 0%, rgba(240, 253, 250, 0.98) 100%);
border-color: rgba(16, 185, 129, 0.16);
}
.payment-status-panel--success .payment-status-panel__value {
color: #047857;
}
.payment-status-panel--info {
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(59, 130, 246, 0.14);
}
.payment-status-panel--info .payment-status-panel__value {
color: #1D4ED8;
}
.payment-status-panel--danger {
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 245, 245, 0.98) 100%);
border-color: rgba(239, 68, 68, 0.16);
}
.payment-status-panel--danger .payment-status-panel__value {
color: #B91C1C;
}
.payment-status-panel--neutral {
background: linear-gradient(180deg, rgba(241, 245, 249, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(148, 163, 184, 0.16);
}
.payment-status-panel--neutral .payment-status-panel__value {
color: #475569;
}
.status-reminder-panel__icon {
width: 68rpx;
height: 68rpx;

View File

@@ -8,7 +8,7 @@
<view class="overview-panel">
<view class="overview-header">
<view>
<view class="overview-title">{{ $tr('合同影像') }}</view>
<view class="overview-title">{{ contractDisplayName || $tr('合同影像') }}</view>
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
</view>
<view class="overview-badge">
@@ -83,6 +83,7 @@
import { addFilePrefix } from '@/utils/common.js'
import { contractPreview } from '@/api/index.js'
import { normalizeRichTextImages } from '../../utils/rich-text'
import { fetchContractDisplayInfo } from '../../utils/contract-display'
function resolvePreviewPayload(response) {
const payload = response && response.data ? response.data : response
@@ -103,6 +104,7 @@ export default {
data() {
return {
contractNo: '',
contractDisplayName: '',
orderNo: '',
loading: false,
imageUrl: '',
@@ -114,9 +116,24 @@ export default {
this.contractNo = decodeURIComponent(options.no || '')
this.orderNo = decodeURIComponent(options.orderNo || '')
this.imageUrl = addFilePrefix(decodeURIComponent(options.contentImageUrl || ''))
this.fetchContractDisplayName()
this.fetchContractDetail()
},
methods: {
fetchContractDisplayName() {
if (!this.contractNo) {
return Promise.resolve()
}
return fetchContractDisplayInfo(this.contractNo, this.$getLanguage())
.then(({ contractDisplayName }) => {
this.contractDisplayName = contractDisplayName || ''
})
.catch((error) => {
console.error('fetch contract display name failed', error)
this.contractDisplayName = ''
})
},
fetchContractDetail() {
if (!this.contractNo || (this.imageUrl && this.contractContent)) {
return Promise.resolve()

View File

@@ -8,7 +8,7 @@
<view class="overview-panel">
<view class="overview-header">
<view>
<view class="overview-title">{{ $tr('签约前确认') }}</view>
<view class="overview-title">{{ contractDisplayName || $tr('签约前确认') }}</view>
<view class="overview-subtitle">{{ $tr('合同编号:') }}{{ contractNo || $tr('未获取') }}</view>
</view>
<view class="overview-badge">
@@ -106,6 +106,7 @@ import { contractPreview, contractSign, uploadCommonFile } from '@/api/index.js'
import { syncLoginStayState } from '../../utils/session-sync'
import ContractSignDialog from '../../components/ContractSignDialog.vue'
import { normalizeRichTextImages } from '../../utils/rich-text'
import { fetchContractDisplayInfo } from '../../utils/contract-display'
function normalizeContractContent(content = '') {
return normalizeRichTextImages(
@@ -144,6 +145,7 @@ export default {
data() {
return {
contractNo: '',
contractDisplayName: '',
orderNo: '',
contractContent: '',
loading: false,
@@ -158,9 +160,24 @@ export default {
this.contractNo = decodeURIComponent(options.no || '')
this.orderNo = decodeURIComponent(options.orderNo || '')
this.signerName = this.getSignerName()
this.fetchContractDisplayName()
this.fetchContractContent()
},
methods: {
fetchContractDisplayName() {
if (!this.contractNo) {
return Promise.resolve()
}
return fetchContractDisplayInfo(this.contractNo, this.$getLanguage())
.then(({ contractDisplayName }) => {
this.contractDisplayName = contractDisplayName || ''
})
.catch((error) => {
console.error('fetch contract display name failed', error)
this.contractDisplayName = ''
})
},
fetchContractContent() {
if (!this.contractNo) {
return Promise.resolve()

File diff suppressed because it is too large Load Diff

View File

@@ -52,6 +52,15 @@
</view>
</view>
<view
v-if="shouldShowApplicationPaymentStatus(item)"
class="payment-status-banner tn-margin-top"
:class="`payment-status-banner--${getApplicationPaymentStatusType(item)}`"
>
<text class="payment-status-banner__label">{{ $tr('支付状态') }}</text>
<text class="payment-status-banner__value">{{ getApplicationPaymentStatusText(item) }}</text>
</view>
<view class="card-body tn-margin-top">
<view class="info-row tn-flex tn-flex-row-between">
<text class="info-label">{{ $tr('所属公寓') }}</text>
@@ -154,6 +163,43 @@ function isViewingApplicationRecord(item = {}) {
return VIEWING_APPLICATION_PATTERN.test(remark)
}
function resolveApplicationPaymentStatusInfo(item = {}) {
if (!item || item.isViewingApplication) {
return {
key: 'exempt',
desc: ''
}
}
const desc = String(item.payStatusDesc || item.paymentStatusDesc || '').trim()
const status = Number(
item.payStatus !== undefined && item.payStatus !== null
? item.payStatus
: item.paymentStatus
)
if (status === 10 || /已支付|paid/i.test(desc)) {
return { key: 'paid', desc }
}
if (/退款|refunded?/i.test(desc)) {
return { key: 'refunded', desc }
}
if (/失败|failed?/i.test(desc)) {
return { key: 'failed', desc }
}
if (/处理中|processing/i.test(desc)) {
return { key: 'processing', desc }
}
if (status === 0 || /待支付|未支付|unpaid|pending/i.test(desc)) {
return { key: 'pending', desc }
}
return {
key: desc ? 'processing' : 'pending',
desc
}
}
function normalizeApplicationList(list = []) {
if (!Array.isArray(list)) {
return []
@@ -362,6 +408,36 @@ export default {
}
return item.intendedRoomTypeName || this.$tr('入住申请')
},
shouldShowApplicationPaymentStatus(item = {}) {
return !item.isViewingApplication
},
getApplicationPaymentStatusText(item = {}) {
const statusInfo = resolveApplicationPaymentStatusInfo(item)
if (this.$getLanguage() !== 'en' && statusInfo.desc) {
return statusInfo.desc
}
const textMap = {
exempt: this.$tr('无需支付'),
paid: this.$tr('已支付'),
refunded: this.$tr('已退款'),
failed: this.$tr('支付失败'),
processing: this.$tr('处理中'),
pending: this.$tr('待支付')
}
return textMap[statusInfo.key] || statusInfo.desc || this.$tr('待支付')
},
getApplicationPaymentStatusType(item = {}) {
const statusInfo = resolveApplicationPaymentStatusInfo(item)
const typeMap = {
exempt: 'neutral',
paid: 'success',
refunded: 'neutral',
failed: 'danger',
processing: 'info',
pending: 'warning'
}
return typeMap[statusInfo.key] || 'warning'
},
resolveReferenceTypeNo(item = {}) {
return item.detailCode || ''
},
@@ -555,6 +631,72 @@ export default {
font-weight: 700;
}
.payment-status-banner {
padding: 20rpx 24rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
border: 1rpx solid transparent;
}
.payment-status-banner__label {
font-size: 24rpx;
color: #64748B;
font-weight: 600;
}
.payment-status-banner__value {
font-size: 28rpx;
font-weight: 700;
}
.payment-status-banner--warning {
background: linear-gradient(180deg, rgba(255, 247, 237, 0.96) 0%, rgba(255, 251, 245, 0.98) 100%);
border-color: rgba(249, 115, 22, 0.16);
}
.payment-status-banner--warning .payment-status-banner__value {
color: #C2410C;
}
.payment-status-banner--success {
background: linear-gradient(180deg, rgba(236, 253, 245, 0.96) 0%, rgba(240, 253, 250, 0.98) 100%);
border-color: rgba(16, 185, 129, 0.16);
}
.payment-status-banner--success .payment-status-banner__value {
color: #047857;
}
.payment-status-banner--info {
background: linear-gradient(180deg, rgba(239, 246, 255, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(59, 130, 246, 0.14);
}
.payment-status-banner--info .payment-status-banner__value {
color: #1D4ED8;
}
.payment-status-banner--danger {
background: linear-gradient(180deg, rgba(254, 242, 242, 0.96) 0%, rgba(255, 245, 245, 0.98) 100%);
border-color: rgba(239, 68, 68, 0.16);
}
.payment-status-banner--danger .payment-status-banner__value {
color: #B91C1C;
}
.payment-status-banner--neutral {
background: linear-gradient(180deg, rgba(241, 245, 249, 0.96) 0%, rgba(248, 250, 252, 0.98) 100%);
border-color: rgba(148, 163, 184, 0.16);
}
.payment-status-banner--neutral .payment-status-banner__value {
color: #475569;
}
.status-pending {
background: rgba(245, 158, 11, 0.12);
color: #B45309;

View File

@@ -48,7 +48,7 @@
<view v-else-if="displayList.length" class="list-wrapper">
<view
v-for="(item, index) in displayList"
:key="item.no || item.contractNo || item.orderNo || index"
:key="getContractKey(item, index)"
class="contract-card"
:style="{ animationDelay: `${index * 0.08}s` }"
>
@@ -58,8 +58,8 @@
<text class="tn-icon-edit-write-fill"></text>
</view>
<view class="card-title-group">
<view class="card-title">{{ $tr('住宿合同') }}</view>
<view class="card-code">{{ item.no || item.contractNo || '--' }}</view>
<view class="card-title">{{ item.contractDisplayName || $tr('住宿合同') }}</view>
<view class="card-code">{{ $tr('合同编号:') }}{{ item.no || item.contractNo || '--' }}</view>
</view>
</view>
<view :class="['status-badge', `status-${resolveStatusType(item.approvalStatus)}`]">
@@ -88,10 +88,10 @@
<text class="info-label">{{ $tr('记录说明') }}</text>
<text class="info-value">{{ getStatusTip(item) }}</text>
</view>
<view v-if="item.signImageUrl" class="sign-row">
<text class="info-label">{{ $tr('我的签名') }}</text>
<view v-if="item.contentImageUrl" class="sign-row">
<text class="info-label">{{ $tr('合同图片') }}</text>
<view class="sign-preview">
<image :src="item.signImageUrl" mode="aspectFit"></image>
<image :src="item.contentImageUrl" mode="aspectFit" @click="previewContractImage(index)"></image>
</view>
</view>
</view>
@@ -100,7 +100,7 @@
<view
class="action-btn"
:class="{ 'action-btn--secondary': !shouldShowSignAction(item) }"
@click="handleContractAction(item)"
@click="handleContractAction(index)"
>
{{ resolveActionText(item) }}
</view>
@@ -121,8 +121,42 @@
<script>
import { addFilePrefix } from '@/utils/common.js'
import { getMyEffectiveContracts, getMyPendingSignContracts } from '@/api/index.js'
import { getSessionSnapshot } from '../../utils/app-state'
import { syncLoginStayState } from '../../utils/session-sync'
import { enrichContractDisplayNames } from '../../utils/contract-display'
function pickFirstValue(...values) {
for (let index = 0; index < values.length; index += 1) {
const value = values[index]
if (value === undefined || value === null) {
continue
}
const text = String(value).trim()
if (text) {
return text
}
}
return ''
}
function parseContractList(response) {
const payload = response && response.data ? response.data : response
if (Array.isArray(payload)) {
return payload
}
if (payload && Array.isArray(payload.data)) {
return payload.data
}
if (payload && payload.data && Array.isArray(payload.data.data)) {
return payload.data.data
}
return []
}
function normalizeContract(item = {}) {
return {
@@ -190,6 +224,67 @@ export default {
const list = this.$store && this.$store.state && this.$store.state.vuex_effective_contracts
return Array.isArray(list) ? list : []
},
resolveContractItem(source) {
if (typeof source === 'number') {
return this.displayList[source] || {}
}
if (source && typeof source === 'object') {
return source
}
return {}
},
resolveCurrentOrderNo(currentOrder = null) {
const storeCurrentOrder = this.$store && this.$store.state && this.$store.state.vuex_current_order
const session = getSessionSnapshot()
const sessionCurrentOrder = session && session.resolvedUser && session.resolvedUser.currentOrder
return pickFirstValue(
currentOrder && (currentOrder.orderNo || currentOrder.no || currentOrder.code || currentOrder.OrderNo),
storeCurrentOrder && (storeCurrentOrder.orderNo || storeCurrentOrder.no || storeCurrentOrder.code || storeCurrentOrder.OrderNo),
sessionCurrentOrder && (sessionCurrentOrder.orderNo || sessionCurrentOrder.no || sessionCurrentOrder.code || sessionCurrentOrder.OrderNo)
)
},
fetchLatestContractsByOrderNo(orderNo = '') {
const normalizedOrderNo = String(orderNo || '').trim()
if (!normalizedOrderNo) {
return Promise.resolve(null)
}
const query = {
OrderNo: normalizedOrderNo,
ProviderType: 'WechatMiniProgram'
}
return Promise.all([
getMyPendingSignContracts(query, this.language).catch(() => []),
getMyEffectiveContracts(query, this.language).catch(() => [])
]).then(([pendingContractsResponse, effectiveContractsResponse]) => {
return {
pendingContracts: parseContractList(pendingContractsResponse),
effectiveContracts: parseContractList(effectiveContractsResponse)
}
})
},
updateContractImageUrl(targetNo = '', imageUrl = '') {
const normalizedNo = String(targetNo || '').trim()
const normalizedImageUrl = addFilePrefix(String(imageUrl || '').trim())
if (!normalizedNo || !normalizedImageUrl) {
return
}
const patchList = (list = []) => list.map((contract) => {
const contractNo = String((contract && (contract.no || contract.contractNo)) || '').trim()
if (contractNo !== normalizedNo) {
return contract
}
return {
...contract,
contentImageUrl: normalizedImageUrl
}
})
this.pendingContracts = patchList(this.pendingContracts)
this.effectiveContracts = patchList(this.effectiveContracts)
},
isCurrentLogin() {
const storeUser = this.getStoreUser()
if (storeUser && Object.keys(storeUser).length) {
@@ -201,12 +296,23 @@ export default {
pendingContracts = [],
effectiveContracts = []
} = {}) {
this.pendingContracts = Array.isArray(pendingContracts)
const normalizedPendingContracts = Array.isArray(pendingContracts)
? pendingContracts.map(normalizeContract)
: []
this.effectiveContracts = Array.isArray(effectiveContracts)
const normalizedEffectiveContracts = Array.isArray(effectiveContracts)
? effectiveContracts.map(normalizeContract)
: []
this.pendingContracts = normalizedPendingContracts
this.effectiveContracts = normalizedEffectiveContracts
Promise.all([
enrichContractDisplayNames(normalizedPendingContracts, this.language),
enrichContractDisplayNames(normalizedEffectiveContracts, this.language)
]).then(([nextPendingContracts, nextEffectiveContracts]) => {
this.pendingContracts = nextPendingContracts
this.effectiveContracts = nextEffectiveContracts
})
},
refreshContracts() {
if (!this.isCurrentLogin()) {
@@ -218,20 +324,32 @@ export default {
this.loading = true
return syncLoginStayState({ force: true })
.then((result = {}) => {
this.applyContractState({
const fallbackState = {
pendingContracts: result.pendingContracts,
effectiveContracts: result.effectiveContracts
})
}
const currentOrderNo = this.resolveCurrentOrderNo(result.currentOrder)
return this.fetchLatestContractsByOrderNo(currentOrderNo)
.then((latestState) => {
this.applyContractState(latestState || fallbackState)
})
})
.catch((error) => {
console.error('refresh contracts failed', error)
const storePendingContracts = this.getStorePendingContracts()
const storeEffectiveContracts = this.getStoreEffectiveContracts()
const session = getSessionSnapshot()
this.applyContractState({
const fallbackState = {
pendingContracts: storePendingContracts.length ? storePendingContracts : session.pendingContracts,
effectiveContracts: storeEffectiveContracts.length ? storeEffectiveContracts : session.effectiveContracts
})
}
const currentOrderNo = this.resolveCurrentOrderNo()
return this.fetchLatestContractsByOrderNo(currentOrderNo)
.then((latestState) => {
this.applyContractState(latestState || fallbackState)
})
})
.finally(() => {
this.loading = false
@@ -265,6 +383,9 @@ export default {
formatDate(value) {
return value ? String(value).slice(0, 10) : '--'
},
getContractKey(item = {}, index = 0) {
return item.no || item.contractNo || item.orderNo || `contract-${index}`
},
formatDateRange(start, end) {
if (!start && !end) {
return '--'
@@ -290,15 +411,84 @@ export default {
if (this.shouldShowSignAction(item)) {
return this.$tr('去签约')
}
return this.$tr('查看详情')
return this.$tr('查看合同')
},
shouldShowSignAction(item = {}) {
return Number(item.approvalStatus) === 0 && !item.signImageUrl
},
handleContractAction(item = {}) {
previewContractImage(source = {}) {
const item = this.resolveContractItem(source)
const directImageUrl = addFilePrefix(String(item.contentImageUrl || '').trim())
if (directImageUrl) {
uni.previewImage({
urls: [directImageUrl],
current: directImageUrl
})
return Promise.resolve(directImageUrl)
}
const orderNo = String(item.orderNo || '').trim()
const contractNo = String(item.no || item.contractNo || '').trim()
if (!orderNo) {
uni.showToast({
title: this.$tr('暂无合同图片'),
icon: 'none'
})
return Promise.resolve('')
}
uni.showLoading({
title: this.$tr('加载中')
})
return this.fetchLatestContractsByOrderNo(orderNo)
.then((latestState) => {
const effectiveContracts = latestState && Array.isArray(latestState.effectiveContracts)
? latestState.effectiveContracts
: []
const matchedContract = effectiveContracts.find((contract) => {
const currentNo = String((contract && (contract.no || contract.contractNo)) || '').trim()
const currentOrderNo = String((contract && contract.orderNo) || '').trim()
if (contractNo && currentNo) {
return currentNo === contractNo
}
return currentOrderNo === orderNo
}) || effectiveContracts[0] || null
const imageUrl = addFilePrefix(String(matchedContract && matchedContract.contentImageUrl || '').trim())
if (!imageUrl) {
uni.showToast({
title: this.$tr('暂无合同图片'),
icon: 'none'
})
return ''
}
if (contractNo) {
this.updateContractImageUrl(contractNo, imageUrl)
}
uni.previewImage({
urls: [imageUrl],
current: imageUrl
})
return imageUrl
})
.catch((error) => {
console.error('preview contract image failed', error)
uni.showToast({
title: this.$tr('合同图片加载失败'),
icon: 'none'
})
return ''
})
.finally(() => {
uni.hideLoading()
})
},
handleContractAction(source = {}) {
const item = this.resolveContractItem(source)
const contractNo = encodeURIComponent(item.no || item.contractNo || '')
const orderNo = encodeURIComponent(item.orderNo || '')
const contentImageUrl = encodeURIComponent(item.contentImageUrl || '')
if (Number(item.approvalStatus) === 0 && !item.signImageUrl) {
uni.navigateTo({
@@ -307,9 +497,7 @@ export default {
return
}
uni.navigateTo({
url: `/pkgService/contract_detail/contract_detail?no=${contractNo}&orderNo=${orderNo}&contentImageUrl=${contentImageUrl}`
})
this.previewContractImage(item)
}
}
}

View File

@@ -71,7 +71,7 @@
<view class="item-order">{{ String(index + 1).padStart(2, '0') }}</view>
<view class="item-title-col">
<view class="item-title-row">
<text class="item-name">{{ item.deviceName || $tr('未命名设备') }}</text>
<text class="item-name">{{ getInspectionItemDisplayName(item) || $tr('未命名设备') }}</text>
<text v-if="item.isRequiredSnapshot" class="item-required">{{ $tr('必查') }}</text>
</view>
</view>
@@ -393,6 +393,7 @@ export default {
.map(item => ({
itemId: item.id,
deviceName: item.deviceName || '',
assetNameEn: item.assetNameEn || item.deviceNameEn || '',
brandSnapshot: item.brandSnapshot || '',
isRequiredSnapshot: !!item.isRequiredSnapshot,
expectedQuantityText: item.expectedQuantity === null || item.expectedQuantity === undefined ? '--' : String(item.expectedQuantity),
@@ -415,6 +416,12 @@ export default {
items
}
},
getInspectionItemDisplayName(item = {}) {
if (this.language === 'en') {
return item.assetNameEn || item.deviceName || ''
}
return item.deviceName || item.assetNameEn || ''
},
async loadPage(force = false) {
this.loading = true
try {
@@ -492,14 +499,23 @@ export default {
if (!result) {
return 'pending'
}
const label = this.getInspectionItemResultLabel(result, '')
if (/未检|待检|未查/i.test(label)) {
const matchedOption = (Array.isArray(this.inspectionResultOptions) ? this.inspectionResultOptions : []).find((option) => {
return String(option && option.key) === result
}) || null
const candidateText = [
this.getInspectionItemResultLabel(result, ''),
matchedOption && matchedOption.value,
matchedOption && matchedOption.label,
result
].filter(value => value !== undefined && value !== null).join(' ')
if (/未检|待检|未查|unchecked|notchecked|pending|not checked|not inspected/i.test(candidateText)) {
return 'pending'
}
if (/问题|异常|损坏|缺失|故障/i.test(label)) {
if (/问题|异常|损坏|缺失|故障|problem|issue|abnormal|damage|damaged|missing|fault|broken|unusable/i.test(candidateText)) {
return 'abnormal'
}
if (/正常|可用|完好/i.test(label)) {
if (/正常|可用|完好|normal|available|good|intact|ok/i.test(candidateText)) {
return 'normal'
}
return 'done'
@@ -944,6 +960,7 @@ export default {
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
padding: 22rpx 24rpx;
}
@@ -1004,8 +1021,9 @@ export default {
}
.item-header__right {
margin-left: auto;
flex-shrink: 0;
max-width: 56%;
max-width: none;
}
.item-body {
@@ -1099,6 +1117,7 @@ export default {
margin-top: 0;
justify-content: flex-end;
gap: 10rpx;
flex-wrap: nowrap;
}
.radio-btn {
@@ -1116,10 +1135,11 @@ export default {
.radio-btn--compact {
min-width: auto;
padding: 12rpx 18rpx;
padding: 12rpx 16rpx;
border-radius: 999rpx;
font-size: 22rpx;
line-height: 1.2;
white-space: nowrap;
}
.radio-btn--active {

View File

@@ -13,13 +13,13 @@
<text class="tn-icon-lock tn-color-gray" style="font-size: 80rpx;"></text>
</view>
<view class="hero-title tn-text-bold">{{ $tr('暂无门锁权限') }}</view>
<view class="hero-desc tn-margin-top-sm tn-text-center">{{ $tr('您当前暂未分配智能门锁,或门锁暂未联网。\n入住后即可使用一键开门与临时密码下发功能。') }}</view>
<view class="hero-desc tn-margin-top-sm tn-text-center">{{ $tr('您当前暂未分配智能门锁,或门锁暂未联网。\n入住后即可使用门锁密码、指纹录入与临时密码下发功能。') }}</view>
<view class="tn-flex tn-margin-top-xl tn-width-full tn-flex-row-between">
<view class="btn-light tn-flex-1 tn-margin-right-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
<text class="tn-icon-key tn-margin-right-xs"></text> {{ $tr('临时密码') }}
</view>
<view class="btn-primary tn-flex-1 tn-margin-left-sm tn-flex tn-flex-col-center tn-flex-row-center tn-padding-sm" style="border-radius: 12rpx; opacity: 0.6;">
<text class="tn-icon-unlock tn-margin-right-xs"></text> {{ $tr('一键开锁') }}
<text class="tn-icon-fingerprint tn-margin-right-xs"></text> {{ $tr('录入指纹') }}
</view>
</view>
</view>

View File

@@ -149,27 +149,31 @@
<scroll-view scroll-y class="popup-body" @scrolltolower="onNoticeScrollToBottom">
<view class="notice-content">
<view class="notice-item">
<view class="notice-item-zh">1、缴纳线上定金可锁定房源并预约专属看房,平台收取定金后该房源临时下架,不再接待其他客户看房。</view>
<view class="notice-item-zh">1、请在 5 分钟内支付定金或者款项,否则系统将自动取消这个订单,释放锁定的房源。</view>
<view class="notice-item-en">Please pay the deposit or full payment within 5 minutes. Otherwise, the system will automatically cancel this order and release the reserved housing unit.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">2、缴纳线上定金可锁定房源并预约专属看房,平台收取定金后该房源临时下架,不再接待其他客户看房。</view>
<view class="notice-item-en">Payment of the online deposit reserves the housing unit and arranges exclusive on-site viewing for you. After receiving the deposit, we will temporarily remove the property from the rental list and stop arranging viewings for other clients.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">2、请于约定时间完成实地看房,并在看房结束后 3 个工作日内签署租赁合同;若逾期未看房或未完成签约,视作您主动放弃房源,定金不予退还。</view>
<view class="notice-item-zh">3、请于约定时间完成实地看房,并在看房结束后 3 个工作日内签署租赁合同;若逾期未看房或未完成签约,视作您主动放弃房源,定金不予退还。</view>
<view class="notice-item-en">You shall complete the on-site viewing at the agreed time and sign the rental contract within 3 working days after the viewing. Failure to complete the viewing or sign the contract within the time limit shall be deemed your voluntary waiver of the property, and the deposit will not be refunded.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">3、如因租客个人原因取消看房、看房后不愿承租房屋或拒绝签署租赁合同,定金不予退还。</view>
<view class="notice-item-zh">4、如因租客个人原因取消看房、看房后不愿承租房屋或拒绝签署租赁合同,定金不予退还。</view>
<view class="notice-item-en">The deposit is non-refundable if you cancel the viewing, refuse to rent the property or decline to sign the contract due to personal reasons.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">4、若因我方房源信息虚假、房屋无法正常交付等我方过错,造成无法看房或签约,定金将全额按原支付路径退还。</view>
<view class="notice-item-zh">5、若因我方房源信息虚假、房屋无法正常交付等我方过错,造成无法看房或签约,定金将全额按原支付路径退还。</view>
<view class="notice-item-en">If the viewing or contract signing cannot be conducted due to our liabilities such as false property information or failure to deliver the housing normally, the full deposit will be refunded via your original payment channel.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">5、待双方正式签署租赁合同后,该定金可直接抵扣首期租金。</view>
<view class="notice-item-zh">6、待双方正式签署租赁合同后,该定金可直接抵扣首期租金。</view>
<view class="notice-item-en">After both parties formally sign the rental contract, this deposit may be directly deducted from your initial rent.</view>
</view>
<view class="notice-item">
<view class="notice-item-zh">6、您支付定金,即代表已完整知晓房源全部信息,并自愿接受本须知全部条款约束。</view>
<view class="notice-item-zh">7、您支付定金,即代表已完整知晓房源全部信息,并自愿接受本须知全部条款约束。</view>
<view class="notice-item-en">By making the deposit payment, you confirm that you have fully reviewed all property information and voluntarily accept all terms and conditions set forth in this notice.</view>
</view>
</view>
@@ -180,18 +184,91 @@
:class="{ 'notice-action-btn--disabled': !hasReadNotice }"
@click="confirmAndPayDeposit"
>
<text class="tn-text-bold">{{ $tr('同意并支付') }}</text>
<text class="tn-text-bold">{{ $tr('同意并继续') }}</text>
<text v-if="!hasReadNotice" class="tn-margin-left-xs tn-text-sm">({{ $tr('请滑动阅读至底部') }})</text>
</view>
</view>
</view>
</tn-popup>
<tn-popup v-model="showPaymentMethodPopup" mode="bottom" length="72%" :borderRadius="32" :closeBtn="true" @close="closePaymentMethodPopup">
<view class="payment-method-popup">
<view class="payment-method-popup__header">
<view class="payment-method-popup__eyebrow">{{ $tr('支付方式') }}</view>
<view class="payment-method-popup__title">{{ $tr('选择定金支付方式') }}</view>
<view class="payment-method-popup__desc">
{{ language === 'en'
? 'Choose the channel you want to use for the room-type application deposit.'
: '请选择房型申请定金的支付渠道,提交后可在“我的入住”继续查看进度。' }}
</view>
</view>
<view class="payment-method-popup__summary">
<view class="payment-method-popup__summary-label">{{ $tr('支付号') }}</view>
<view class="payment-method-popup__summary-value">
{{ (pendingPaymentMethodContext && pendingPaymentMethodContext.paymentNo) || $tr('提交后生成') }}
</view>
</view>
<view class="payment-method-card payment-method-card--wechat" @click="handlePaymentMethodSelect('wechat')">
<view class="payment-method-card__main">
<view class="payment-method-card__icon payment-method-card__icon--wechat">
<text class="tn-icon-wechat"></text>
</view>
<view class="payment-method-card__copy">
<view class="payment-method-card__title-row">
<view class="payment-method-card__title">{{ $tr('微信支付') }}</view>
<view class="payment-method-card__tag">{{ $tr('推荐') }}</view>
</view>
<view class="payment-method-card__desc">
{{ language === 'en'
? 'Continue with the standard online payment flow.'
: '继续拉起线上支付,适合直接完成定金支付。' }}
</view>
</view>
</view>
<view class="payment-method-card__arrow">
<text class="tn-icon-right"></text>
</view>
</view>
<view class="payment-method-card payment-method-card--offline" @click="handlePaymentMethodSelect('offline')">
<view class="payment-method-card__main">
<view class="payment-method-card__icon payment-method-card__icon--offline">
<text class="tn-icon-bank-card"></text>
</view>
<view class="payment-method-card__copy">
<view class="payment-method-card__title-row">
<view class="payment-method-card__title">{{ $tr('其他支付方式') }}</view>
</view>
<view class="payment-method-card__desc">
{{ language === 'en'
? 'View the configured phone, QR code, and offline payment instructions.'
: '查看后台配置的联系电话、二维码与线下支付说明。' }}
</view>
</view>
</view>
<view class="payment-method-card__arrow">
<text class="tn-icon-right"></text>
</view>
</view>
</view>
</tn-popup>
<deposit-other-payment-popup
v-model="showOfflinePaymentNotice"
:config="depositOtherPaymentConfig"
:loading="depositOtherPaymentConfigLoading"
@input="handleOfflinePaymentPopupInput"
/>
</view>
</template>
<script>
import { accountQueryBill, accountQueryBills, applicationFormDetail, paymentBillPay, paymentPayingBills, submitApplicationForm, sysdictPage } from '@/api/index.js'
import DepositOtherPaymentPopup from '@/components/DepositOtherPaymentPopup.vue'
import { saveActiveApplicationCode, saveHomePageTabId } from '@/utils/app-state'
import { createDefaultDepositOtherPaymentConfig, loadDepositOtherPaymentConfig } from '@/utils/deposit-other-payment'
import { compactEnglishText, localizePayMethodText } from '../../utils/select-room-normalizers'
import { getStoredUser } from '../../utils/flow'
@@ -725,6 +802,7 @@ const BUSINESS_TYPE_CONFIG = {
export default {
components: {
DepositOtherPaymentPopup,
TagSelector,
DateRangeSelector,
RoomTypeSelector,
@@ -747,9 +825,15 @@ export default {
scrollTop: 0,
lastAutoRemark: '',
showDepositNotice: false,
showPaymentMethodPopup: false,
showOfflinePaymentNotice: false,
hasReadNotice: false,
isClosingDepositNoticeForPayment: false,
pendingDepositPaymentContext: null
isSwitchingPaymentMethodPopup: false,
pendingDepositPaymentContext: null,
pendingPaymentMethodContext: null,
depositOtherPaymentConfig: createDefaultDepositOtherPaymentConfig(),
depositOtherPaymentConfigLoading: false
}
},
computed: {
@@ -882,6 +966,9 @@ export default {
language() {
this.syncLocalizedDraftContent()
this.loadDictOptions()
if (this.showOfflinePaymentNotice) {
this.loadDepositOtherPaymentConfig()
}
}
},
onPageScroll(event) {
@@ -1227,14 +1314,66 @@ export default {
},
async resolvePaymentPreview(referenceTypeNo = '') {
const context = await this.resolveApplicationDetailPaymentContext(referenceTypeNo).catch(() => null)
const billNo = context && context.billNo ? context.billNo : ''
const pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
return {
referenceTypeNo: context && context.referenceTypeNo ? context.referenceTypeNo : String(referenceTypeNo || '').trim(),
billNo,
paymentNo: this.getPendingPaymentOrderNo(pendingPaymentOrder) || (context && context.paymentNo ? context.paymentNo : ''),
pendingPaymentOrder
const normalizedReferenceTypeNo = context && context.referenceTypeNo
? context.referenceTypeNo
: String(referenceTypeNo || '').trim()
let billNo = context && context.billNo ? context.billNo : ''
let pendingPaymentOrder = billNo ? await this.getPendingPaymentOrder(billNo).catch(() => null) : null
let requiresDepositPayment = !!billNo
if (normalizedReferenceTypeNo) {
const billRes = await accountQueryBills({
referenceTypeNo: normalizedReferenceTypeNo,
status: 10,
paymentStatus: 0,
ProviderType: 'WechatMiniProgram'
}, this.language)
const billList = this.normalizeBillList(billRes)
const matchedBill = Array.isArray(billList) && billList.length
? ((billNo
? billList.find(item => String(this.getBillNo(item) || '').trim() === String(billNo || '').trim())
: null) || billList[0])
: null
if (matchedBill) {
billNo = this.getBillNo(matchedBill)
requiresDepositPayment = !!billNo
if (!pendingPaymentOrder && billNo) {
pendingPaymentOrder = await this.getPendingPaymentOrder(billNo).catch(() => null)
}
if (!pendingPaymentOrder) {
pendingPaymentOrder = this.buildPendingPaymentOrderFromBillDetail(matchedBill)
}
} else {
billNo = ''
pendingPaymentOrder = null
requiresDepositPayment = false
}
}
return {
referenceTypeNo: normalizedReferenceTypeNo,
billNo,
paymentNo: requiresDepositPayment
? (this.getPendingPaymentOrderNo(pendingPaymentOrder) || (context && context.paymentNo ? context.paymentNo : ''))
: '',
pendingPaymentOrder,
requiresDepositPayment
}
},
confirmSubmitRequest() {
return new Promise((resolve) => {
uni.showModal({
title: this.$tr('提交前确认'),
content: this.$tr('确认提交当前申请单吗?'),
// WeChat Mini Program limits modal button text length.
// Keep English actions short so the modal does not fail silently.
confirmText: this.language === 'en' ? 'OK' : this.$tr('确认提交'),
cancelText: this.language === 'en' ? 'Back' : this.$tr('再想想'),
success: (res) => resolve(!!(res && res.confirm)),
fail: () => resolve(false)
})
})
},
openDepositNotice(paymentContext = {}) {
this.pendingDepositPaymentContext = {
@@ -1243,6 +1382,7 @@ export default {
paymentNo: String(paymentContext.paymentNo || '').trim(),
pendingPaymentOrder: paymentContext.pendingPaymentOrder || null
}
this.pendingPaymentMethodContext = null
this.hasReadNotice = false
this.showDepositNotice = true
},
@@ -1254,11 +1394,92 @@ export default {
this.showDepositNotice = false
this.hasReadNotice = false
this.pendingDepositPaymentContext = null
this.pendingPaymentMethodContext = null
this.goStayServiceFlow()
},
onNoticeScrollToBottom() {
this.hasReadNotice = true
},
openPaymentMethodPopup(paymentContext = {}) {
this.pendingPaymentMethodContext = {
referenceTypeNo: String(paymentContext.referenceTypeNo || '').trim(),
billNo: String(paymentContext.billNo || '').trim(),
paymentNo: String(paymentContext.paymentNo || '').trim(),
pendingPaymentOrder: paymentContext.pendingPaymentOrder || null
}
this.showPaymentMethodPopup = true
},
closePaymentMethodPopup() {
if (this.isSwitchingPaymentMethodPopup) {
this.isSwitchingPaymentMethodPopup = false
return
}
this.showPaymentMethodPopup = false
this.pendingPaymentMethodContext = null
this.goStayServiceFlow()
},
loadDepositOtherPaymentConfig() {
this.depositOtherPaymentConfigLoading = true
return loadDepositOtherPaymentConfig(this.language)
.then((config) => {
this.depositOtherPaymentConfig = config || createDefaultDepositOtherPaymentConfig()
})
.catch((error) => {
console.error('load deposit other payment config failed', error)
this.depositOtherPaymentConfig = createDefaultDepositOtherPaymentConfig()
})
.finally(() => {
this.depositOtherPaymentConfigLoading = false
})
},
async handlePaymentMethodSelect(type = '') {
const paymentContext = this.pendingPaymentMethodContext || {}
this.isSwitchingPaymentMethodPopup = true
this.showPaymentMethodPopup = false
if (type === 'offline') {
setTimeout(() => {
this.showOfflinePaymentNotice = true
this.loadDepositOtherPaymentConfig()
}, 40)
return
}
try {
await this.payByPaymentContext({
referenceTypeNo: paymentContext.referenceTypeNo,
billNo: paymentContext.billNo,
pendingPaymentOrder: paymentContext.pendingPaymentOrder,
description: this.$tr('支付定金')
})
uni.showToast({ title: this.$tr('支付成功'), icon: 'success' })
} catch (error) {
const errMsg = error && (error.errMsg || error.message || error.msg)
? (error.errMsg || error.message || error.msg)
: this.$tr('支付失败')
if (/cancel/i.test(errMsg)) {
uni.showToast({ title: this.$tr('已取消支付'), icon: 'none' })
} else {
uni.showToast({ title: errMsg, icon: 'none' })
}
} finally {
uni.hideLoading()
this.pendingPaymentMethodContext = null
setTimeout(() => {
this.goStayServiceFlow()
}, 300)
}
},
handleOfflinePaymentPopupInput(value) {
this.showOfflinePaymentNotice = !!value
if (value) {
return
}
this.pendingPaymentMethodContext = null
setTimeout(() => {
this.goStayServiceFlow()
}, 300)
},
async payByPaymentContext(context = {}) {
let referenceTypeNo = String(context.referenceTypeNo || '').trim()
let billNo = String(context.billNo || '').trim()
@@ -1367,31 +1588,66 @@ export default {
const paymentContext = this.pendingDepositPaymentContext || {}
this.isClosingDepositNoticeForPayment = true
this.showDepositNotice = false
this.hasReadNotice = false
this.pendingDepositPaymentContext = null
setTimeout(() => {
this.openPaymentMethodPopup(paymentContext)
}, 40)
},
async executeSubmitRequest() {
this.isSubmitting = true
uni.showLoading({
title: this.$tr('提交中...')
})
try {
await this.payByPaymentContext({
referenceTypeNo: paymentContext.referenceTypeNo,
billNo: paymentContext.billNo,
pendingPaymentOrder: paymentContext.pendingPaymentOrder,
description: this.$tr('支付定金')
})
uni.showToast({ title: this.$tr('支付成功'), icon: 'success' })
} catch (error) {
const errMsg = error && (error.errMsg || error.message || error.msg)
? (error.errMsg || error.message || error.msg)
: this.$tr('支付失败')
if (/cancel/i.test(errMsg)) {
uni.showToast({ title: this.$tr('已取消支付'), icon: 'none' })
} else {
uni.showToast({ title: errMsg, icon: 'none' })
const payload = this.buildSubmitPayload()
const response = await submitApplicationForm(payload, this.language)
const applicationCode = extractSubmitApplicationCode(response)
if (this.isRoomTypeApply && !applicationCode) {
throw new Error(this.$tr('申请单编号缺失'))
}
if (applicationCode) {
saveActiveApplicationCode(applicationCode)
}
} finally {
uni.hideLoading()
this.hasReadNotice = false
this.isClosingDepositNoticeForPayment = false
this.pendingDepositPaymentContext = null
if (this.isRoomTypeApply) {
const paymentContext = await this.resolvePaymentPreview(applicationCode).catch(() => null)
if (paymentContext && paymentContext.requiresDepositPayment === false) {
uni.showToast({
title: this.$tr('申请单提交成功'),
icon: 'success'
})
setTimeout(() => {
this.goStayServiceFlow()
}, 800)
return
}
this.openDepositNotice({
...(paymentContext || {}),
referenceTypeNo: (paymentContext && paymentContext.referenceTypeNo) || applicationCode
})
return
}
uni.showToast({
title: this.$tr('申请单提交成功'),
icon: 'success'
})
setTimeout(() => {
this.goStayServiceFlow()
}, 300)
saveHomePageTabId('stay-service')
uni.reLaunch({
url: '/pages/index/index?pageId=stay-service'
})
}, 800)
} catch (error) {
console.error('submit application form failed', error)
uni.hideLoading()
throw error
} finally {
this.isSubmitting = false
}
},
submitRequest() {
@@ -1408,58 +1664,21 @@ export default {
return
}
this.isSubmitting = true
uni.showLoading({
title: this.$tr('提交中...')
})
const payload = this.buildSubmitPayload()
return submitApplicationForm(payload, this.language)
.then(async (response) => {
const applicationCode = extractSubmitApplicationCode(response)
if (this.isRoomTypeApply && !applicationCode) {
throw new Error(this.$tr('申请单编号缺失'))
}
if (applicationCode) {
saveActiveApplicationCode(applicationCode)
}
uni.hideLoading()
if (this.isRoomTypeApply) {
const paymentContext = await this.resolvePaymentPreview(applicationCode).catch(() => ({
referenceTypeNo: applicationCode,
billNo: '',
paymentNo: '',
pendingPaymentOrder: null
}))
this.openDepositNotice({
...paymentContext,
referenceTypeNo: paymentContext.referenceTypeNo || applicationCode
})
return
}
uni.showToast({
title: this.$tr('申请单提交成功'),
icon: 'success'
})
setTimeout(() => {
saveHomePageTabId('stay-service')
uni.reLaunch({
url: '/pages/index/index?pageId=stay-service'
})
}, 800)
return this.confirmSubmitRequest()
.then((confirmed) => {
if (!confirmed) {
return null
}
return this.executeSubmitRequest()
})
.catch((error) => {
console.error('submit application form failed', error)
uni.hideLoading()
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('申请单提交失败'),
icon: 'none'
})
})
.finally(() => {
this.isSubmitting = false
if (!error) {
return
}
uni.showToast({
title: (error && (error.message || error.msg)) || this.$tr('申请单提交失败'),
icon: 'none'
})
})
}
}
@@ -1805,6 +2024,172 @@ $shadow-active: 0 8rpx 24rpx rgba($brand-primary, 0.22);
margin-top: 16rpx;
}
.payment-method-popup {
padding: 30rpx 30rpx calc(28rpx + env(safe-area-inset-bottom));
background:
radial-gradient(circle at top right, rgba($brand-primary, 0.16) 0%, rgba($brand-primary, 0) 36%),
linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
border-radius: 32rpx 32rpx 0 0;
}
.payment-method-popup__header {
padding: 8rpx 4rpx 20rpx;
}
.payment-method-popup__eyebrow {
display: inline-flex;
align-items: center;
padding: 8rpx 18rpx;
border-radius: 999rpx;
background: rgba($brand-primary, 0.1);
color: $brand-primary;
font-size: 22rpx;
font-weight: 700;
}
.payment-method-popup__title {
margin-top: 18rpx;
font-size: 40rpx;
line-height: 1.25;
color: $text-primary;
font-weight: 700;
}
.payment-method-popup__desc {
margin-top: 12rpx;
font-size: 24rpx;
line-height: 1.7;
color: $text-secondary;
}
.payment-method-popup__summary {
margin-bottom: 22rpx;
padding: 22rpx 26rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.92);
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: 0 12rpx 28rpx rgba($brand-primary, 0.08);
}
.payment-method-popup__summary-label {
font-size: 22rpx;
color: $text-tertiary;
}
.payment-method-popup__summary-value {
margin-top: 8rpx;
font-size: 28rpx;
line-height: 1.5;
color: $text-primary;
font-weight: 700;
word-break: break-all;
}
.payment-method-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-top: 18rpx;
padding: 26rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.92);
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: 0 14rpx 30rpx rgba($brand-primary, 0.08);
}
.payment-method-card:active {
transform: scale(0.985);
}
.payment-method-card--wechat {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(239, 253, 244, 0.96) 100%);
border-color: rgba(34, 197, 94, 0.14);
}
.payment-method-card--offline {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(247, 250, 255, 0.96) 100%);
}
.payment-method-card__main {
display: flex;
align-items: center;
gap: 18rpx;
flex: 1;
min-width: 0;
}
.payment-method-card__icon {
width: 88rpx;
height: 88rpx;
border-radius: 26rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 42rpx;
flex-shrink: 0;
}
.payment-method-card__icon--wechat {
background: rgba(34, 197, 94, 0.12);
color: #16A34A;
}
.payment-method-card__icon--offline {
background: rgba($brand-primary, 0.1);
color: $brand-primary;
}
.payment-method-card__copy {
flex: 1;
min-width: 0;
}
.payment-method-card__title-row {
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
}
.payment-method-card__title {
font-size: 30rpx;
line-height: 1.35;
color: $text-primary;
font-weight: 700;
}
.payment-method-card__tag {
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: rgba(34, 197, 94, 0.12);
color: #15803D;
font-size: 20rpx;
font-weight: 700;
}
.payment-method-card__desc {
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.7;
color: $text-secondary;
}
.payment-method-card__arrow {
width: 56rpx;
height: 56rpx;
border-radius: 999rpx;
background: rgba(148, 163, 184, 0.12);
color: #64748B;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
flex-shrink: 0;
}
/* 导航栏背景色滚动过渡 */
.submit-request-page ::v-deep .tn-custom-nav-bar__bar {
transition: background-color 0.3s ease, box-shadow 0.3s ease;

View File

@@ -8,6 +8,7 @@
<current-application-bill-panel
ref="billPanel"
:reference-type-no="referenceTypeNo"
:auto-resolve-reference="!pendingBillNo || !!referenceTypeNo"
@reference-resolved="handleReferenceResolved"
@all-paid="handleAllPaid"
/>
@@ -16,6 +17,7 @@
</template>
<script>
import { accountQueryBill } from '@/api/index.js'
import { fetchEnums } from '@/utils/enum-store.js'
import { saveActiveApplicationCode, saveHomePageTabId } from '@/utils/app-state'
import CurrentApplicationBillPanel from '../components/CurrentApplicationBillPanel.vue'
@@ -26,17 +28,54 @@ export default {
},
data() {
return {
referenceTypeNo: ''
referenceTypeNo: '',
pendingBillNo: ''
}
},
async onLoad(options) {
await fetchEnums(false, this.$getLanguage())
this.referenceTypeNo = options && options.referenceTypeNo ? decodeURIComponent(options.referenceTypeNo) : ''
this.pendingBillNo = options && options.billNo ? decodeURIComponent(options.billNo) : ''
if (!this.referenceTypeNo && this.pendingBillNo) {
await this.resolveReferenceTypeNoByBillNo(this.pendingBillNo)
}
if (this.referenceTypeNo) {
saveActiveApplicationCode(this.referenceTypeNo)
}
},
methods: {
extractBillDetail(response) {
const payload = response && response.data ? response.data : response
if (payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
return payload.data
}
return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {}
},
async resolveReferenceTypeNoByBillNo(billNo = '') {
const currentBillNo = String(billNo || '').trim()
if (!currentBillNo) {
return ''
}
try {
const response = await accountQueryBill({ billNo: currentBillNo }, this.$getLanguage())
const detail = this.extractBillDetail(response)
const referenceTypeNo = String(
detail.referenceTypeNo
|| detail.ReferenceTypeNo
|| detail.detailCode
|| detail.applicationCode
|| ''
).trim()
if (referenceTypeNo) {
this.referenceTypeNo = referenceTypeNo
saveActiveApplicationCode(referenceTypeNo)
}
return referenceTypeNo
} catch (error) {
console.error('根据账单号解析申请单号失败', error)
return ''
}
},
handleReferenceResolved(referenceTypeNo) {
this.referenceTypeNo = referenceTypeNo || ''
if (this.referenceTypeNo) {