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;