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

1204 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="index" style="background-color: #F6F6F6;">
<!-- 启动全屏加载遮罩 -->
<view v-if="isAppInitLoading" class="init-loading-mask tn-flex tn-flex-direction-column tn-flex-col-center tn-flex-row-center">
<view class="loading-logo">
<text class="tn-icon-moon tn-color-white" style="font-size: 80rpx; transform: rotate(-45deg); display: block;"></text>
</view>
<view class="loading-desc tn-margin-top-lg" style="color: rgba(255,255,255,0.8); letter-spacing: 4rpx; font-size: 32rpx; font-weight: bold;">{{ $tr('绿居公寓') }}</view>
<view class="loading-spinner tn-margin-top-xl">
<tn-loading mode="circle" color="#00BBE8" size="60"></tn-loading>
</view>
</view>
<!-- #ifdef MP-WEIXIN -->
<view v-if="showPrivacyPopup" class="privacy-mask">
<view class="privacy-dialog">
<view class="privacy-title">{{ $tr('隐私授权提示') }}</view>
<view class="privacy-content">
{{ $tr('进入小程序前,请先阅读并同意') }}
<text class="privacy-link" @click="openPrivacyContract">{{ $tr('《用户隐私保护指引》') }}</text>
{{ $tr(',同意后才能继续使用相关功能。') }}
</view>
<view class="privacy-action-row">
<view class="privacy-action-btn" @click="handlePrivacyDisagree">{{ $tr('暂不授权') }}</view>
<button
id="index-privacy-agree-btn"
class="privacy-action-btn is-primary"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgreePrivacyAuthorization"
>
{{ $tr('同意并继续') }}
</button>
</view>
</view>
</view>
<!-- #endif -->
<!-- 二级页面 -->
<view v-if="hasPage('select-room')" v-show="currentPageId === 'select-room'">
<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
<select-room-tab ref="selectRoomTab"></select-room-tab>
</scroll-view>
</view>
<view v-if="hasPage('stay-service')" v-show="currentPageId === 'stay-service'">
<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
<stay-service-tab
ref="stayServiceTab"
@refresh-shell-state="handleStayServiceRefreshState"
></stay-service-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>
</scroll-view>
</view>
<view v-if="hasPage('profile-center')" v-show="currentPageId === 'profile-center'">
<scroll-view class="custom-tabbar-page" scroll-y enable-back-to-top @scrolltolower="tabbarPageScrollLower">
<profile-center-tab ref="profileCenterTab"></profile-center-tab>
</scroll-view>
</view>
<contract-alert
v-if="sessionState.isLogin && sessionState.pendingContracts.length"
:pending-contracts="sessionState.pendingContracts"
/>
<!-- <view class="bg-tabbar-shadow"></view> -->
<!-- 底部导航栏 -->
<view class="tabbar">
<view class="tabbar__bg" :style="[wrapStyle]"></view>
<view class="tabbar__list">
<block v-for="(item, index) in currentTabbarList" :key="item.id">
<view :id="`tabbar_item_${index}`" class="tabbar__item"
:class="[{'tabbar__item--active': index === currentTabbarIndex}]" @click="changeTabbar(index, item.id)">
<!-- 可以用字体图标的方式 -->
<view class="tabbar__item__icon-wrap">
<view class="tabbar__item__icon" :class="[item.icon]"></view>
<view
v-if="item.id === 'message-center' && messageBadgeCount > 0"
class="tabbar__item__badge"
>
{{ getMessageBadgeText() }}
</view>
</view>
<!-- <image class="tabbar__item__image" :src="item.image"></image> -->
<!-- 底部文字,需要自行显示出来-->
<view class="tabbar__item__text">{{ item.name }}</view>
</view>
</block>
</view>
<!-- <view class="tabbar__select-active-bg" :class="[showActiceBg ? 'tabbar__select-active-bg--show' : 'tabbar__select-active-bg--hide']" :style="[activeBgStyle]"></view> -->
<view class="tabbar__select-active-bg" :animation="activeBgAnimation"></view>
</view>
</view>
</template>
<script>
import SelectRoomTab from './component/SelectRoomTab.vue'
import StayServiceTab from './component/StayServiceTab.vue'
import MessageCenterTab from './component/MessageCenterTab.vue'
import ProfileCenterTab from './component/ProfileCenterTab.vue'
import ContractAlert from '../../components/contract-alert/contract-alert.vue'
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 { getReservationCurrentNode, hasEffectiveReservationFlow, parseReservationFlowResponse } from '@/utils/reservation-flow'
const STAY_STATE_SYNC_TTL = 800
export default {
components: {
SelectRoomTab,
StayServiceTab,
MessageCenterTab,
ProfileCenterTab,
ContractAlert
},
data() {
return {
wrapMaskPositionLeft: -15,
activeBgPositionLeft: 0,
currentPageId: 'select-room',
tabbarRectInfo: [],
activeBgAnimation: {},
messageBadgeCount: 0,
sessionState: {
isLogin: false,
hasCheckedIn: 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: 'refreshSelectRoomState' },
{ id: 'stay-service', labelKey: '入 住', icon: 'tn-icon-safe', image: '/static/tabbar/tn-tabbar1.png', component: 'StayServiceTab', ref: 'stayServiceTab', refreshMethod: 'checkLocalStatus' },
{ 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' }
],
loadedPages: ['select-room'],
isAppInitLoading: true,
isTabSwitching: false,
showPrivacyPopup: false,
resolvePrivacyAuthorization: null,
isCheckingPrivacyAuthorization: false
}
},
computed: {
wrapStyle() {
return {
'-webkit-mask-position': `${this.wrapMaskPositionLeft}px -1px, 100%`
}
},
visibleTabIds() {
if (!this.sessionState.isLogin) {
return ['select-room', 'profile-center']
}
const tabs = []
const shouldShowStayService = this.sessionState.hasCheckedIn
|| this.sessionState.hasPendingApplication
|| this.sessionState.hasActiveReservationFlow
|| this.sessionState.pendingContracts.length > 0
if (shouldShowStayService) {
tabs.push('stay-service')
} else {
tabs.push('select-room')
}
tabs.push('message-center', 'profile-center')
return tabs
},
currentTabbarList() {
return this.tabDefinitions
.filter(item => this.visibleTabIds.includes(item.id))
.map(item => ({
...item,
name: this.$tr(item.labelKey)
}))
},
currentTabbarIndex() {
const index = this.currentTabbarList.findIndex(item => item.id === this.currentPageId)
return index === -1 ? 0 : index
},
},
onShow() {
this.handlePageShow()
this.checkEntryPrivacyAuthorization()
},
onLoad(options) {
clearLegacyDemoCache()
uni.$on('messageBadgeCountChange', this.handleMessageBadgeCountChange)
uni.$on('reservation-flow-force-refresh', this.handleReservationFlowForceRefresh)
this.initPrivacyAuthorization()
this.checkEntryPrivacyAuthorization()
this.handlePageLoad(options)
},
onUnload() {
uni.$off('messageBadgeCountChange', this.handleMessageBadgeCountChange)
uni.$off('reservation-flow-force-refresh', this.handleReservationFlowForceRefresh)
this.releasePrivacyAuthorization()
},
onReady() {
this.$nextTick(() => {
this.getTabbarItemInfo()
})
},
methods: {
initPrivacyAuthorization() {
// #ifdef MP-WEIXIN
if (typeof wx === 'undefined' || !wx.onNeedPrivacyAuthorization) {
return
}
this.handlePrivacyAuthorizationRequest = (resolve) => {
this.resolvePrivacyAuthorization = resolve
this.showPrivacyPopup = true
}
bindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
// #endif
},
releasePrivacyAuthorization() {
// #ifdef MP-WEIXIN
if (!this.handlePrivacyAuthorizationRequest) {
return
}
unbindPrivacyAuthorizationListener(this.handlePrivacyAuthorizationRequest)
this.handlePrivacyAuthorizationRequest = null
this.resolvePrivacyAuthorization = null
this.showPrivacyPopup = false
// #endif
},
checkEntryPrivacyAuthorization() {
// #ifdef MP-WEIXIN
if (this.isCheckingPrivacyAuthorization) {
return
}
this.isCheckingPrivacyAuthorization = true
getPrivacyAuthorizationStatus()
.then((res) => {
if (!this.resolvePrivacyAuthorization) {
this.showPrivacyPopup = !!(res && res.needAuthorization)
}
})
.finally(() => {
this.isCheckingPrivacyAuthorization = false
})
// #endif
},
openPrivacyContract() {
// #ifdef MP-WEIXIN
openWechatPrivacyContract().catch((error) => {
console.error('open privacy contract failed', error)
})
// #endif
},
handleAgreePrivacyAuthorization() {
this.showPrivacyPopup = false
if (!this.resolvePrivacyAuthorization) {
return
}
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
this.resolvePrivacyAuthorization = null
resolvePrivacyAuthorization({
buttonId: 'index-privacy-agree-btn',
event: 'agree'
})
},
handlePrivacyDisagree() {
this.showPrivacyPopup = false
if (!this.resolvePrivacyAuthorization) {
return
}
const resolvePrivacyAuthorization = this.resolvePrivacyAuthorization
this.resolvePrivacyAuthorization = null
resolvePrivacyAuthorization({
event: 'disagree'
})
},
getStoreUser() {
return (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
},
isCurrentLogin() {
const storeUser = this.getStoreUser()
if (storeUser && Object.keys(storeUser).length) {
return true
}
return !!getSessionSnapshot().isLogin
},
getStoreCheckinStatus() {
return !!(this.$store && this.$store.state && this.$store.state.vuex_checkin_status)
},
getStoreCurrentOrder() {
return (this.$store && this.$store.state && this.$store.state.vuex_current_order) || 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({
hasCheckedIn: false,
hasPendingApplication: false,
hasActiveReservationFlow: false,
flowList: []
})
}
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 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('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
},
fetchReservationFlowWithUserCode(userCode, options = {}) {
const { silent = true } = options
const storeUser = this.getStoreUser()
return orderSearchedReservationFlow({
userCode
}, this.$getLanguage())
.then((response) => {
const flowList = parseReservationFlowResponse(response)
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 {
hasCheckedIn,
hasPendingApplication,
hasActiveReservationFlow,
flowList
}
})
.catch((error) => {
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).then((flowState = {}) => {
this.sessionState.hasPendingApplication = !!flowState.hasPendingApplication
this.sessionState.hasActiveReservationFlow = !!flowState.hasActiveReservationFlow
})
]
if (!skipMessageBadge) {
tasks.push(this.refreshMessageBadgeCount())
}
return Promise.all(tasks).finally(() => {
this.refreshSessionState()
})
},
async handlePageLoad(options = {}) {
await this.refreshShellState(true, {
skipMessageBadge: options.pageId === 'message-center'
})
this.syncCurrentTab({
pageId: options.pageId,
index: Number(options.index || 0)
})
this.$nextTick(() => {
this.refreshTabPageState('select-room')
})
setTimeout(() => {
this.isAppInitLoading = false
}, 1500)
},
async handlePageShow() {
await this.refreshShellState(true, {
skipMessageBadge: this.currentPageId === 'message-center'
})
this.syncCurrentTab()
this.$nextTick(() => {
this.loadedPages.forEach(pageId => {
this.refreshTabPageState(pageId)
})
this.getTabbarItemInfo()
})
},
hasPage(id) {
return this.loadedPages.includes(id)
},
ensurePageLoaded(id) {
if (!id || this.loadedPages.includes(id)) return
this.loadedPages = [...this.loadedPages, id]
},
refreshSessionState() {
const session = getSessionSnapshot()
const storePendingContracts = this.getStorePendingContracts()
const storeUser = this.getStoreUser()
const storeHasCheckedIn = this.getStoreCheckinStatus()
const pendingContracts = storePendingContracts.length
? storePendingContracts
: (storeUser.pendingContracts || (Array.isArray(session.pendingContracts) ? session.pendingContracts : []))
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,
hasPendingApplication,
hasActiveReservationFlow,
pendingContracts
}
},
getCurrentUserCode() {
const storeUser = this.getStoreUser()
if (Object.keys(storeUser).length) {
return storeUser.code || storeUser.userCode || ''
}
const session = getSessionSnapshot()
const user = session.currentUser || session.registeredUser || session.resolvedUser || {}
return user.code || user.userCode || ''
},
getCurrentOrderNo() {
const storeCurrentOrder = this.getStoreCurrentOrder()
if (storeCurrentOrder && storeCurrentOrder.orderNo) {
return String(storeCurrentOrder.orderNo).trim()
}
const session = getSessionSnapshot()
const room = session.myRoomData || {}
const user = session.currentUser || session.registeredUser || session.resolvedUser || {}
const currentOrder = (user && user.currentOrder) || {}
return String(room.orderNo || currentOrder.orderNo || '').trim()
},
isPendingInspectionRecord(record) {
return countPendingInspectionRecords([record]) > 0
},
refreshMessageBadgeCount() {
if (!this.isCurrentLogin()) {
this.messageBadgeCount = 0
return Promise.resolve()
}
const tasks = [
notificationPage({
IsRead: false,
PageIndex: 1,
PageSize: 1,
Search: '',
IsExport: false
})
]
const userCode = this.getCurrentUserCode()
const orderNo = this.getCurrentOrderNo()
if (orderNo) {
tasks.push(checkRecordByChecked({
checkedUserCode: userCode,
orderNo
}))
}
return Promise.all(tasks)
.then(([notificationResponse, inspectionResponse]) => {
const remoteUnread = resolveNotificationUnreadTotal(notificationResponse)
const inspectionList = extractInspectionList(inspectionResponse)
const pendingInspectionCount = countPendingInspectionRecords(inspectionList)
this.messageBadgeCount = remoteUnread + pendingInspectionCount
})
.catch((error) => {
console.error('refresh message badge count failed', error)
this.messageBadgeCount = 0
})
},
handleMessageBadgeCountChange(count) {
this.messageBadgeCount = Math.max(Number(count) || 0, 0)
},
getMessageBadgeText() {
if (!this.messageBadgeCount) return ''
return this.messageBadgeCount > 99 ? '99+' : String(this.messageBadgeCount)
},
resolveTargetTab({ pageId, index = 0 } = {}) {
const list = this.currentTabbarList
if (!list.length) return null
if (pageId) {
const explicitMatchedTab = list.find(item => item.id === pageId)
if (explicitMatchedTab) return explicitMatchedTab
}
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 preferredPageId = getHomePageTabId()
if (preferredPageId) {
const matchedTab = list.find(item => item.id === preferredPageId)
if (matchedTab) return matchedTab
}
return list[index] || list[0]
},
syncCurrentTab({ pageId, index = 0 } = {}) {
const targetTab = this.resolveTargetTab({ pageId, index })
if (!targetTab) return
this.ensurePageLoaded(targetTab.id)
this.currentPageId = targetTab.id
saveHomePageTabId(targetTab.id)
},
getPageRef(refName) {
const ref = this.$refs[refName]
return Array.isArray(ref) ? ref[0] : ref
},
refreshTabPageState(pageId, retryCount = 0) {
const page = this.tabDefinitions.find(item => item.id === pageId)
if (!page || !page.refreshMethod) return
const pageVm = this.getPageRef(page.ref)
if (pageVm && typeof pageVm[page.refreshMethod] === 'function') {
pageVm[page.refreshMethod]()
return
}
if (retryCount < 8) {
setTimeout(() => {
this.refreshTabPageState(pageId, retryCount + 1)
}, 60)
}
},
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 })
},
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('stay-service')
this.syncCurrentTab()
this.$nextTick(() => {
this.refreshTabPageState('stay-service')
if (this.currentPageId && this.currentPageId !== 'stay-service') {
this.refreshTabPageState(this.currentPageId)
}
this.getTabbarItemInfo()
})
},
tabbarPageScrollLower(e) {
if (this.currentPageId === 'select-room') {
const selectRoomTab = this.getPageRef('selectRoomTab')
selectRoomTab && selectRoomTab.getRandomData && selectRoomTab.getRandomData()
}
},
// 获取底部元素的位置
getTabbarItemInfo() {
const view = uni.createSelectorQuery().in(this)
for (let i = 0; i < this.currentTabbarList.length; i++) {
view.select('#tabbar_item_' + i).boundingClientRect()
}
view.exec(res => {
if (!res || !res.length) {
if (!this._tabbarRetryCount) this._tabbarRetryCount = 0;
if (this._tabbarRetryCount < 20) {
this._tabbarRetryCount++;
setTimeout(() => {
this.getTabbarItemInfo()
}, 50)
}
return
}
// 将信息存入数组中
this.tabbarRectInfo = [];
res.map((item) => {
if (item) {
this.tabbarRectInfo.push({
left: item.left,
width: item.width
})
}
})
if (this.tabbarRectInfo[this.currentTabbarIndex]) {
this.updateHollowsPosition()
this.updateActiveBgPosition(true)
}
})
},
// 更新凹陷位置
updateHollowsPosition() {
if (!this.tabbarRectInfo[this.currentTabbarIndex]) return;
const {
width,
left
} = this.tabbarRectInfo[this.currentTabbarIndex]
// 计算掩模图片的宽高比
// const imageRatio = 200 / 92
// 计算定高的宽比
const imageFixedHeightWidthRatioValue = 300 * (uni.upx2px(20) / 92)
this.wrapMaskPositionLeft = left - ((imageFixedHeightWidthRatioValue - width) / 2)
},
// 更新激活时背景的位置
updateActiveBgPosition(init = false) {
if (!this.tabbarRectInfo[this.currentTabbarIndex]) return;
const {
width,
left
} = this.tabbarRectInfo[this.currentTabbarIndex]
const oldActiveBgPositionLeft = this.activeBgPositionLeft
this.activeBgPositionLeft = left + ((width - uni.upx2px(16)) / 2)
if (!init) {
const animation = uni.createAnimation({
duration: 160,
timingFunction: "linear"
})
animation.top(uni.upx2px(-60)).left(oldActiveBgPositionLeft + ((this.activeBgPositionLeft -
oldActiveBgPositionLeft) / 2)).scale(1).step()
animation.left(this.activeBgPositionLeft).top(uni.upx2px(-10)).scale(1).step()
this.activeBgAnimation = animation.export()
} else {
const animation = uni.createAnimation({
duration: 160,
timingFunction: "linear"
})
animation.left(this.activeBgPositionLeft).top(uni.upx2px(-10)).step()
this.activeBgAnimation = animation.export()
}
},
async changeTabbar(index, id) {
if (this.currentTabbarIndex === index && this.currentPageId === id) return;
if (this.isTabSwitching) return
// 兼容处理:防止 H5 等非小程序环境下找不到 wx 对象导致 JS 报错,从而中断后续页面切换逻辑
if (typeof uni !== 'undefined' && uni.vibrateShort) {
uni.vibrateShort({
fail: () => {} // 忽略 H5 环境下的不支持报错
});
}
this.isTabSwitching = true
try {
await this.refreshShellState(true, {
skipMessageBadge: id === 'message-center'
})
const targetTab = this.resolveTargetTab({
pageId: id,
index
})
const nextPageId = targetTab ? targetTab.id : id
this.ensurePageLoaded(nextPageId)
this.currentPageId = nextPageId
saveHomePageTabId(nextPageId)
this.$nextTick(() => {
this.refreshTabPageState(nextPageId)
this.updateHollowsPosition()
this.updateActiveBgPosition()
})
} finally {
this.isTabSwitching = false
}
},
},
// 分享到朋友
onShareAppMessage() {
return {
title: this.$tr('绿居公寓 - 国际化青年社区'),
path: '/pages/index/index'
}
},
// 分享到朋友圈
onShareTimeline() {
return {
title: this.$tr('绿居公寓 - 国际化青年社区')
}
}
}
</script>
<style lang="scss" scoped>
/* 页面初始化全屏遮罩 */
.init-loading-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #111111;
z-index: 99999;
}
.privacy-mask {
position: fixed;
inset: 0;
z-index: 100000;
background: rgba(17, 24, 39, 0.56);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.privacy-dialog {
width: 100%;
max-width: 640rpx;
background: #FFFFFF;
border-radius: 28rpx;
padding: 40rpx 32rpx 32rpx;
box-sizing: border-box;
box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, 0.16);
}
.privacy-title {
font-size: 34rpx;
font-weight: 700;
color: #1A2233;
text-align: center;
}
.privacy-content {
margin-top: 24rpx;
font-size: 28rpx;
line-height: 1.7;
color: #5C6B8A;
}
.privacy-link {
color: $brand-primary;
font-weight: 700;
}
.privacy-action-row {
display: flex;
margin-top: 32rpx;
}
.privacy-action-btn {
flex: 1;
height: 88rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: 700;
color: $brand-primary;
background: rgba($brand-primary, 0.08);
border: 1rpx solid rgba($brand-primary, 0.12);
box-sizing: border-box;
}
.privacy-action-btn + .privacy-action-btn {
margin-left: 20rpx;
}
button.privacy-action-btn {
padding: 0;
line-height: 88rpx;
}
button.privacy-action-btn::after {
border: none;
}
.privacy-action-btn.is-primary {
color: #FFFFFF;
background: linear-gradient(135deg, rgba($brand-primary, 0.92) 0%, $brand-primary 100%);
border-color: transparent;
}
.loading-logo {
animation: pulse 2s infinite ease-in-out;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.1); opacity: 1; }
100% { transform: scale(1); opacity: 0.8; }
}
/*整体样式 */
.index {
width: 100%;
height: 100vh;
position: relative;
.custom-tabbar-page {
width: 100%;
height: calc(100vh - 114rpx - constant(safe-area-inset-bottom));
height: calc(100vh - 114rpx - env(safe-area-inset-bottom));
box-sizing: border-box;
}
/* 底部导航 statr */
.tabbar {
width: 100%;
height: 114rpx;
height: calc(114rpx + constant(safe-area-inset-bottom));
height: calc(114rpx + env(safe-area-inset-bottom));
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: transparent;
z-index: 998;
/* 图鸟温馨提醒,裁剪式也生效的阴影样式,不用box-shadow*/
filter: drop-shadow(0rpx 0rpx 30rpx rgba(0, 0, 0, 0.08));
&__bg {
position: absolute;
width: 100%;
height: 100%;
bottom: 0;
left: 0;
background-color: #FFFFFF;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 61.5'%3E%3Cpath d='M100 0H0c32.9 0 49.3 61.5 100 61.5S167.1 0 200 0H100z'/%3E%3C/svg%3E"), linear-gradient(#000, #000);
-webkit-mask-size: auto 20rpx, cover;
-webkit-mask-position: center top;
-webkit-mask-repeat: no-repeat;
-webkit-mask-composite: xor;
/*只显示不重合的地方, chorem 、safari 支持*/
z-index: -1;
transition: 0.5s;
}
&__list {
position: absolute;
z-index: 999;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
&__item {
height: 100%;
width: 100%;
flex: 1;
text-align: center;
position: relative;
margin-top: 10rpx;
&--active {
.tabbar__item__icon {
// top: 10rpx;
color: $icon-tone-brand;
}
.tabbar__item__image {
top: -42rpx;
}
.tabbar__item__text {
opacity: 1;
color: $icon-tone-strong;
font-size: 20rpx;
}
}
&__icon {
font-size: 48rpx;
position: absolute;
left: 0;
right: 0;
top: 22rpx;
transition: 0.5s;
color: $icon-tone-muted;
}
&__icon-wrap {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 88rpx;
}
&__badge {
position: absolute;
top: 10rpx;
right: 50%;
margin-right: -42rpx;
min-width: 30rpx;
height: 30rpx;
padding: 0 8rpx;
border-radius: 999rpx;
background: #ff5b5b;
color: #ffffff;
font-size: 18rpx;
line-height: 30rpx;
text-align: center;
box-shadow: 0 8rpx 18rpx rgba(255, 91, 91, 0.25);
z-index: 2;
}
&__image {
width: 58rpx;
height: 58rpx;
position: absolute;
left: 0;
right: 0;
top: 20rpx;
transition: 0.5s;
margin: 0 auto;
}
&__text {
position: absolute;
left: 0;
right: 0;
bottom: 20rpx;
bottom: calc(20rpx + constant(safe-area-inset-bottom)*0.8);
bottom: calc(20rpx + env(safe-area-inset-bottom)*0.8);
transition: 0.5s;
font-size: 20rpx;
color: $icon-tone-muted;
}
}
&__select-active-bg {
position: absolute;
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background-color: $brand-primary;
border: 2rpx solid #FFFFFF;
// transition: 0.5s;
z-index: -1;
left: calc(64rpx);
box-shadow: 0rpx 10rpx 30rpx 0rpx rgba($brand-primary, 0.28);
// box-shadow: inset 0rpx 0rpx 50rpx 0rpx rgba(0, 0, 0, 0.05);
// box-shadow: 0rpx 10rpx 30rpx rgba(70,23,129, 0.07),
// 0rpx -8rpx 40rpx rgba(255, 255, 255, 0.07),
// inset 0rpx -10rpx 10rpx rgba(70,23,129, 0.07),
// inset 0rpx 10rpx 20rpx rgba(255, 255, 255, 1);
transition: box-shadow .2s ease-out;
&--hide {
top: calc(110rpx + 50rpx);
}
&--show {
top: -54rpx;
}
}
}
/* 底部导航 end */
/* 移动背景部分 start*/
.bg-contaniner {
position: fixed;
top: -0rpx;
left: -300rpx;
--text-color: hsl(0 95% 60%);
--bg-color: #F6F6F6;
--bg-size: 200px;
height: 100%;
display: grid;
place-items: center;
place-content: center;
overflow: hidden;
background-color: var(--bg-color);
z-index: -1;
}
.bg-contaniner::before {
--size: 150vmax;
content: "";
inline-size: var(--size);
block-size: var(--size);
background-image: url("https://cdn.nlark.com/yuque/0/2022/png/280373/1663568736856-assets/web-upload/97cab67d-6f17-4c6e-a3f9-ac81961d571a.png");
background-size: var(--bg-size);
background-repeat: repeat;
transform: rotate(45deg);
opacity: 0.1;
animation: bg 6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.bg-contaniner::before {
animation-duration: 0s;
}
}
@keyframes bg {
to {
background-position: 0 calc(var(--bg-size) * -1);
}
}
/* 移动背景部分 end*/
}
/* 毛玻璃*/
.dd-glass {
width: 100%;
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
}
</style>