feat: 重构核心租住流程页面与交互体验

统一注册、看房、申请与入住等核心页面的视觉和流程衔接,补齐支付、签约与状态流转闭环,提升移动端操作连贯性。
This commit is contained in:
liangjian
2026-07-29 09:05:38 +08:00
parent da6bdc6a02
commit 1111ac148b
34 changed files with 8734 additions and 2801 deletions

View File

@@ -50,14 +50,6 @@
></stay-service-tab>
</scroll-view>
</view>
<view v-if="hasPage('my-application')" v-show="currentPageId === 'my-application'">
<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
<my-application-tab
ref="myApplicationTab"
@refresh-shell-state="handleMyApplicationRefreshState"
></my-application-tab>
</scroll-view>
</view>
<view v-if="hasPage('message-center')" v-show="currentPageId === 'message-center'">
<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
<message-center-tab ref="messageCenterTab"></message-center-tab>
@@ -108,27 +100,28 @@
import StayServiceTab from './component/StayServiceTab.vue'
import MessageCenterTab from './component/MessageCenterTab.vue'
import ProfileCenterTab from './component/ProfileCenterTab.vue'
import MyApplicationTab from './component/MyApplicationTab.vue'
import ContractAlert from '../../components/contract-alert/contract-alert.vue'
import { clearLegacyDemoCache, getHomePageTabId, getSessionSnapshot, saveHomePageTabId } from '../../utils/app-state'
import { applicationFormMePage, checkRecordByChecked, notificationPage, orderSearchedMePage, orderSearchedReservationFlow } from '@/api/index.js'
import { parseOrderListResponse, getOrderStatusCode } from '@/utils/booking-helper'
import {
clearLegacyDemoCache,
getCurrentUser,
getHomePageTabId,
getSessionSnapshot,
saveBookingList,
saveCurrentBooking,
saveCurrentUser,
saveEffectiveContracts,
saveHomePageTabId,
savePendingContractCount,
savePendingContracts,
setCheckedInStatus
} from '../../utils/app-state'
import { checkRecordByChecked, notificationPage, orderSearchedReservationFlow, userMe } from '@/api/index.js'
import { mergeBackendUserInfo } from '@/utils/flow'
import { bindPrivacyAuthorizationListener, getPrivacyAuthorizationStatus, openWechatPrivacyContract, unbindPrivacyAuthorizationListener } from '@/utils/privacy.js'
import { countPendingInspectionRecords, extractInspectionList, resolveNotificationUnreadTotal } from '../../utils/message-helper'
import { syncLoginStayState } from '../../utils/session-sync'
import { getReservationOrderNo, getReservationCurrentNode, parseReservationFlowResponse } from '@/utils/reservation-flow'
import { getReservationCurrentNode, hasEffectiveReservationFlow, parseReservationFlowResponse } from '@/utils/reservation-flow'
function formatPageResponse(response) {
const payload = response && response.data ? response.data : response
return payload && typeof payload === 'object' ? payload : {}
}
function normalizeApplicationList(list = []) {
if (!Array.isArray(list)) {
return []
}
return list.filter(item => item && typeof item === 'object')
}
const STAY_STATE_SYNC_TTL = 800
export default {
components: {
@@ -136,7 +129,6 @@
StayServiceTab,
MessageCenterTab,
ProfileCenterTab,
MyApplicationTab,
ContractAlert
},
data() {
@@ -150,15 +142,13 @@
sessionState: {
isLogin: false,
hasCheckedIn: false,
hasStayOrder: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
pendingContracts: []
},
tabDefinitions: [
{ id: 'select-room', labelKey: '选 房', icon: 'tn-icon-home-fill', image: '/static/tabbar/tn-tabbar0.png', component: 'SelectRoomTab', ref: 'selectRoomTab', refreshMethod: 'checkLoginStatus' },
{ id: 'select-room', labelKey: '选 房', icon: 'tn-icon-home-fill', image: '/static/tabbar/tn-tabbar0.png', component: 'SelectRoomTab', ref: 'selectRoomTab', refreshMethod: 'refreshSelectRoomState' },
{ id: 'stay-service', labelKey: '入 住', icon: 'tn-icon-safe', image: '/static/tabbar/tn-tabbar1.png', component: 'StayServiceTab', ref: 'stayServiceTab', refreshMethod: 'checkLocalStatus' },
{ id: 'my-application', labelKey: '申请单', icon: 'tn-icon-order', image: '/static/tabbar/tn-tabbar2.png', component: 'MyApplicationTab', ref: 'myApplicationTab', refreshMethod: 'load' },
{ id: 'message-center', labelKey: '消 息', icon: 'tn-icon-message', image: '/static/tabbar/tn-tabbar3.png', component: 'MessageCenterTab', ref: 'messageCenterTab', refreshMethod: 'load' },
{ id: 'profile-center', labelKey: '我 的', icon: 'tn-icon-my-fill', image: '/static/tabbar/tn-tabbar3.png', component: 'ProfileCenterTab', ref: 'profileCenterTab', refreshMethod: 'checkLoginStatus' }
],
@@ -181,16 +171,15 @@
return ['select-room', 'profile-center']
}
const tabs = []
if (this.sessionState.hasStayOrder) {
const shouldShowStayService = this.sessionState.hasCheckedIn
|| this.sessionState.hasPendingApplication
|| this.sessionState.hasActiveReservationFlow
|| this.sessionState.pendingContracts.length > 0
if (shouldShowStayService) {
tabs.push('stay-service')
} else if (this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow) {
tabs.push('my-application')
} else {
tabs.push('select-room')
}
if ((this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow) && !tabs.includes('my-application')) {
tabs.push('my-application')
}
tabs.push('message-center', 'profile-center')
return tabs
},
@@ -316,112 +305,190 @@
getStoreCurrentOrder() {
return (this.$store && this.$store.state && this.$store.state.vuex_current_order) || null
},
getStoreCurrentBooking() {
const user = this.getStoreUser()
return (user && user.currentBooking) || null
},
getStorePendingContracts() {
const list = this.$store && this.$store.state && this.$store.state.vuex_pending_contracts
return Array.isArray(list) ? list : []
},
getCachedStayStateResult() {
const storeUser = this.getStoreUser()
return {
hasCheckedIn: this.getStoreCheckinStatus(),
hasPendingApplication: !!storeUser.hasPendingApplication,
hasActiveReservationFlow: !!storeUser.hasActiveReservationFlow,
flowList: Array.isArray(storeUser.reservationFlowList) ? storeUser.reservationFlowList : []
}
},
syncStayState(force = false) {
if (!this.isCurrentLogin()) {
this.refreshSessionState()
return Promise.resolve()
return Promise.resolve({
hasCheckedIn: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
flowList: []
})
}
return syncLoginStayState({ force }).catch((error) => {
console.error('sync login stay state failed', error)
})
},
resolveApplicationState() {
return applicationFormMePage({
Status: 0,
PageIndex: 1,
PageSize: 1
}, this.$getLanguage())
const now = Date.now()
if (this._syncStayStatePromise) {
return this._syncStayStatePromise
}
if (this._lastStayStateSyncAt && now - this._lastStayStateSyncAt < STAY_STATE_SYNC_TTL) {
return Promise.resolve(this.getCachedStayStateResult())
}
const storeUser = this.getStoreUser()
const cachedUserCode = storeUser.userCode || storeUser.code || ''
// 先获取用户信息(含 userCode),再获取流程
this._syncStayStatePromise = userMe({}, this.$getLanguage())
.then((response) => {
const pageData = formatPageResponse(response)
const list = normalizeApplicationList(pageData.data || [])
const total = Number(pageData.total || 0)
const hasPendingApplication = list.some(item => Number(item && item.status) === 0)
return {
hasPendingApplication: total > 0 || hasPendingApplication
const responseData = response && response.data ? response.data : response
const info = responseData && responseData.data ? responseData.data : responseData
const mergedUser = mergeBackendUserInfo(storeUser, response)
const resolvedUserCode = mergedUser.userCode || mergedUser.code || info.userCode || info.code || cachedUserCode || ''
if (this.$store && this.$store.commit) {
this.$store.commit('$tStore', { name: 'vuex_user', value: mergedUser })
}
saveCurrentUser(mergedUser)
if (!resolvedUserCode) {
this.refreshSessionState()
return Promise.resolve({
hasCheckedIn: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
flowList: []
})
}
return this.fetchReservationFlowWithUserCode(resolvedUserCode)
})
.catch((error) => {
console.error('resolve application state failed', error)
return {
hasPendingApplication: false
console.error('fetch user info before flow failed', error)
if (!cachedUserCode) {
this.refreshSessionState()
return Promise.resolve({
hasCheckedIn: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
flowList: []
})
}
return this.fetchReservationFlowWithUserCode(cachedUserCode)
})
.then((result) => {
this._lastStayStateSyncAt = Date.now()
return result
})
.finally(() => {
this._syncStayStatePromise = null
})
return this._syncStayStatePromise
},
resolveApplicationFlowState(list = []) {
const orderList = normalizeApplicationList(list)
const reservationOrderList = orderList.filter(item => getOrderStatusCode(item) === 0)
const orderNos = [...new Set(reservationOrderList.map(item => getReservationOrderNo(item)).filter(Boolean))]
const result = {
hasPendingApplication: false,
hasActiveReservationFlow: false
}
if (!orderNos.length) {
return Promise.resolve(result)
}
fetchReservationFlowWithUserCode(userCode, options = {}) {
const { silent = true } = options
const storeUser = this.getStoreUser()
return orderSearchedReservationFlow({
orderNos
userCode
}, this.$getLanguage())
.then((response) => {
const flowList = parseReservationFlowResponse(response)
const hasActiveReservationFlow = flowList.some(item => !!getReservationCurrentNode(item))
const currentFlow = flowList.find(item => {
const node = getReservationCurrentNode(item)
return node && node.isCurrent
}) || flowList[0] || null
const currentNode = currentFlow ? getReservationCurrentNode(currentFlow) : null
const currentStep = currentNode ? currentNode.step : null
const currentRefNo = currentNode ? String(currentNode.refNo || '').trim() : ''
const hasCheckedIn = currentStep === 5
const hasActiveReservationFlow = flowList.some(item => hasEffectiveReservationFlow(item))
const hasPendingApplication = currentStep === 1 && !!currentRefNo
const currentBooking = hasActiveReservationFlow && currentFlow
? { orderNo: currentFlow.orderNo || '', refNo: currentRefNo, step: currentStep }
: null
if (this.$store && this.$store.commit) {
this.$store.commit('$tStore', { name: 'vuex_checkin_status', value: hasCheckedIn })
this.$store.commit('$tStore', { name: 'vuex_current_order', value: null })
this.$store.commit('$tStore', { name: 'vuex_pending_contracts', value: [] })
this.$store.commit('$tStore', { name: 'vuex_pending_contract_count', value: 0 })
this.$store.commit('$tStore', { name: 'vuex_effective_contracts', value: [] })
this.$store.commit('$tStore', { name: 'vuex_valid_orders', value: [] })
this.$store.commit('$tStore', {
name: 'vuex_user',
value: {
...storeUser,
hasCheckedIn,
currentOrder: null,
currentBooking,
currentFlow,
bookingStatus: hasCheckedIn ? 'checked_in' : 'none',
reservationFlowList: flowList,
currentReservationStep: currentStep,
currentReservationRefNo: currentRefNo,
hasActiveReservationFlow,
hasPendingApplication
}
})
}
setCheckedInStatus(hasCheckedIn)
saveCurrentBooking(currentBooking)
saveBookingList([])
savePendingContracts([])
savePendingContractCount(0)
saveEffectiveContracts([])
const currentUser = getCurrentUser() || {}
saveCurrentUser({
...currentUser,
hasCheckedIn,
currentOrder: null,
currentBooking,
currentFlow,
bookingStatus: hasCheckedIn ? 'checked_in' : 'none',
reservationFlowList: flowList,
currentReservationStep: currentStep,
currentReservationRefNo: currentRefNo,
hasActiveReservationFlow,
hasPendingApplication
})
this.refreshSessionState()
return {
hasPendingApplication: result.hasPendingApplication || hasActiveReservationFlow,
hasActiveReservationFlow
hasCheckedIn,
hasPendingApplication,
hasActiveReservationFlow,
flowList
}
})
.catch((error) => {
console.error('resolve application flow state failed', error)
return result
console.error('sync stay state by reservation flow failed', error)
this.refreshSessionState()
if (!silent) {
return Promise.reject(error)
}
return {
hasCheckedIn: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
flowList: []
}
})
},
refreshShellState(force = false, options = {}) {
const { skipMessageBadge = false } = options
const tasks = [
this.$store && this.$store.dispatch ? this.$store.dispatch('fetchGlobalEnums').catch(() => ({})) : Promise.resolve(),
this.syncStayState(force)
this.syncStayState(force).then((flowState = {}) => {
this.sessionState.hasPendingApplication = !!flowState.hasPendingApplication
this.sessionState.hasActiveReservationFlow = !!flowState.hasActiveReservationFlow
})
]
if (!skipMessageBadge) {
tasks.push(this.refreshMessageBadgeCount())
}
if (this.isCurrentLogin()) {
tasks.push(
this.resolveApplicationState()
.then((applicationState = {}) => {
return orderSearchedMePage({ PageIndex: 1, PageSize: 100, IsExport: false })
.then(res => {
const orderList = parseOrderListResponse(res)
return this.resolveApplicationFlowState(orderList)
.then((flowState = {}) => ({
hasPendingApplication: !!applicationState.hasPendingApplication,
hasActiveReservationFlow: !!flowState.hasActiveReservationFlow
}))
})
})
.then((flowState = {}) => {
this.sessionState.hasPendingApplication = !!flowState.hasPendingApplication
this.sessionState.hasActiveReservationFlow = !!flowState.hasActiveReservationFlow
})
.catch(() => {
this.sessionState.hasPendingApplication = false
this.sessionState.hasActiveReservationFlow = false
})
)
} else {
this.sessionState.hasPendingApplication = false
this.sessionState.hasActiveReservationFlow = false
}
return Promise.all(tasks).finally(() => {
this.refreshSessionState()
})
@@ -435,6 +502,10 @@
index: Number(options.index || 0)
})
this.$nextTick(() => {
this.refreshTabPageState('select-room')
})
setTimeout(() => {
this.isAppInitLoading = false
}, 1500)
@@ -464,17 +535,17 @@
const storePendingContracts = this.getStorePendingContracts()
const storeUser = this.getStoreUser()
const storeHasCheckedIn = this.getStoreCheckinStatus()
const storeCurrentBooking = this.getStoreCurrentBooking()
const pendingContracts = storePendingContracts.length
? storePendingContracts
: (storeUser.pendingContracts || (Array.isArray(session.pendingContracts) ? session.pendingContracts : []))
const hasStayOrder = !!(storeHasCheckedIn || storeCurrentBooking || session.currentBooking)
const hasCheckedIn = !!(storeHasCheckedIn || session.hasCheckedIn)
const hasActiveReservationFlow = !!(storeUser.hasActiveReservationFlow || session.resolvedUser.hasActiveReservationFlow || this.sessionState.hasActiveReservationFlow)
const hasPendingApplication = !!(storeUser.hasPendingApplication || session.resolvedUser.hasPendingApplication || this.sessionState.hasPendingApplication)
this.sessionState = {
isLogin: this.isCurrentLogin(),
hasCheckedIn: storeHasCheckedIn || session.hasCheckedIn,
hasStayOrder: hasStayOrder || pendingContracts.length > 0,
hasPendingApplication: !!this.sessionState.hasPendingApplication,
hasActiveReservationFlow: !!this.sessionState.hasActiveReservationFlow,
hasCheckedIn,
hasPendingApplication,
hasActiveReservationFlow,
pendingContracts
}
},
@@ -551,19 +622,13 @@
const explicitMatchedTab = list.find(item => item.id === pageId)
if (explicitMatchedTab) return explicitMatchedTab
}
const preferredPageId = getHomePageTabId()
const shouldPreferStayService = this.sessionState.isLogin && this.sessionState.hasStayOrder
if (shouldPreferStayService && (!preferredPageId || preferredPageId === 'select-room')) {
const shouldPreferFlowPage = this.sessionState.isLogin
&& (this.sessionState.hasCheckedIn || this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow || this.sessionState.pendingContracts.length > 0)
if (shouldPreferFlowPage) {
const stayServiceTab = list.find(item => item.id === 'stay-service')
if (stayServiceTab) return stayServiceTab
}
const shouldPreferApplicationFlow = this.sessionState.isLogin
&& !this.sessionState.hasStayOrder
&& (this.sessionState.hasPendingApplication || this.sessionState.hasActiveReservationFlow)
if (shouldPreferApplicationFlow && (!preferredPageId || preferredPageId === 'select-room')) {
const applicationTab = list.find(item => item.id === 'my-application')
if (applicationTab) return applicationTab
}
const preferredPageId = getHomePageTabId()
if (preferredPageId) {
const matchedTab = list.find(item => item.id === preferredPageId)
if (matchedTab) return matchedTab
@@ -595,28 +660,74 @@
}, 60)
}
},
handleStayServiceRefreshState() {
this.refreshSessionState()
refreshReservationFlowOnly() {
if (!this.isCurrentLogin()) {
this.refreshSessionState()
return Promise.resolve({
hasCheckedIn: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
flowList: []
})
}
const storeUser = this.getStoreUser()
let userCode = storeUser.userCode || storeUser.code || ''
if (!userCode) {
const session = getSessionSnapshot()
const fallbackUser = session.resolvedUser || session.currentUser || session.registeredUser || {}
userCode = fallbackUser.userCode || fallbackUser.code || ''
}
if (!userCode) {
this.refreshSessionState()
return Promise.resolve({
hasCheckedIn: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
flowList: []
})
}
return this.fetchReservationFlowWithUserCode(userCode, { silent: false })
},
async handleMyApplicationRefreshState() {
await this.refreshShellState(true)
this.syncCurrentTab()
this.$nextTick(() => {
this.refreshTabPageState(this.currentPageId)
this.getTabbarItemInfo()
})
handleStayServiceRefreshState(event = {}) {
const { pendingContracts } = event || {}
this.refreshReservationFlowOnly()
.then((flowState = {}) => {
if (pendingContracts && Array.isArray(pendingContracts)) {
if (this.$store && this.$store.commit) {
this.$store.commit('$tStore', { name: 'vuex_pending_contracts', value: pendingContracts })
}
savePendingContracts(pendingContracts)
}
this.sessionState.hasPendingApplication = !!flowState.hasPendingApplication
this.sessionState.hasActiveReservationFlow = !!flowState.hasActiveReservationFlow
this.refreshSessionState()
this.$nextTick(() => {
const stayServiceTab = this.getPageRef('stayServiceTab')
if (stayServiceTab && typeof stayServiceTab.onFlowRefreshed === 'function') {
stayServiceTab.onFlowRefreshed({ ...flowState, success: true })
}
})
})
.catch((error) => {
console.error('refresh reservation flow from stay service failed', error)
this.$nextTick(() => {
const stayServiceTab = this.getPageRef('stayServiceTab')
if (stayServiceTab && typeof stayServiceTab.onFlowRefreshed === 'function') {
stayServiceTab.onFlowRefreshed({ success: false })
}
})
})
},
async handleReservationFlowForceRefresh() {
await this.refreshShellState(true)
this.ensurePageLoaded('my-application')
this.ensurePageLoaded('stay-service')
this.syncCurrentTab()
this.$nextTick(() => {
const myApplicationTab = this.getPageRef('myApplicationTab')
if (myApplicationTab && typeof myApplicationTab.load === 'function') {
myApplicationTab.load(true)
} else {
this.refreshTabPageState('my-application')
}
if (this.currentPageId) {
this.refreshTabPageState('stay-service')
if (this.currentPageId && this.currentPageId !== 'stay-service') {
this.refreshTabPageState(this.currentPageId)
}
this.getTabbarItemInfo()