1204 lines
42 KiB
Vue
1204 lines
42 KiB
Vue
<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>
|