580 lines
18 KiB
Vue
580 lines
18 KiB
Vue
<template>
|
|
<view class="select-room-page tn-safe-area-inset-bottom">
|
|
<tn-nav-bar
|
|
v-if="currentScreen === 0"
|
|
:isBack="false"
|
|
:bottomShadow="false"
|
|
:backgroundColor="navBarBg"
|
|
:fixed="true"
|
|
:customBackStyle="navBarTransition"
|
|
>
|
|
<view class="custom-nav tn-flex tn-flex-col-center">
|
|
<view class="nav-left">
|
|
<view class="lang-text-toggle tn-flex tn-flex-col-center" @click="toggleLang">
|
|
<text class="lang-text" :class="{ 'lang-text--active': currentLang === 'zh' }">中</text>
|
|
<text class="lang-split">/</text>
|
|
<text class="lang-text" :class="{ 'lang-text--active': currentLang === 'en' }">EN</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</tn-nav-bar>
|
|
|
|
<swiper
|
|
class="page-swiper"
|
|
vertical
|
|
:current="currentScreen"
|
|
@change="handleScreenChange"
|
|
:disable-touch="currentScreen === 1"
|
|
>
|
|
<swiper-item>
|
|
<SelectRoomHero
|
|
:language="currentLang"
|
|
:banner-list="bannerList"
|
|
:current-banner="currentBanner"
|
|
:current-banner-index="currentBannerIndex"
|
|
@banner-change="handleBannerChange"
|
|
@banner-switch="switchBanner"
|
|
@consult="handleConsult"
|
|
@submit="handleSubmit"
|
|
@collapse="collapseToTopBanner"
|
|
/>
|
|
</swiper-item>
|
|
|
|
<swiper-item>
|
|
<scroll-view
|
|
class="panel-scroll"
|
|
scroll-y
|
|
scroll-with-animation
|
|
:show-scrollbar="false"
|
|
>
|
|
<SelectRoomAreaPanel
|
|
:language="currentLang"
|
|
:banner-list="bannerList"
|
|
:current-banner-index="currentBannerIndex"
|
|
:compact-banner-top="compactBannerTop"
|
|
:is-banner-loading="isBannerLoading"
|
|
:banner-load-error="bannerLoadError"
|
|
:banner-loaded-from-api="bannerLoadedFromApi"
|
|
:current-banner="currentBanner"
|
|
:selected-area-code="selectedAreaCode"
|
|
:area-list="areaList"
|
|
:current-area-label="currentAreaLabel"
|
|
:selected-tag="selectedTag"
|
|
:current-tag-label="currentTagLabel"
|
|
:available-filter-tags="availableFilterTags"
|
|
:selected-sort-key="selectedSortKey"
|
|
:current-sort-label="currentSortLabel"
|
|
:is-area-loading="isAreaLoading"
|
|
:room-load-error="roomLoadError"
|
|
:area-load-error="areaLoadError"
|
|
:is-login="isLogin"
|
|
:user-info="userInfo"
|
|
:display-area-sections="displayAreaSections"
|
|
@retry-banner="retryLoadBanner"
|
|
@select-area="selectArea"
|
|
@select-tag="selectTag"
|
|
@select-sort="selectSort"
|
|
@open-room-type="openRoomTypeEntry"
|
|
@banner-change="handleBannerChange"
|
|
@collapse-to-hero="collapseToHero"
|
|
/>
|
|
</scroll-view>
|
|
</swiper-item>
|
|
</swiper>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { getSessionSnapshot } from '../../../utils/app-state'
|
|
import { ensureBookingAccess } from '../../../utils/booking-guard'
|
|
import { getStoredUser } from '../../../utils/flow'
|
|
import { areaPagination, bannerAll } from '@/api/index.js'
|
|
import { normalizeAreaItem, normalizeBannerItem } from '../../../utils/select-room-normalizers'
|
|
import SelectRoomHero from './SelectRoomHero.vue'
|
|
import SelectRoomAreaPanel from './SelectRoomAreaPanel.vue'
|
|
|
|
const SORT_LABEL_MAP = {
|
|
zh: {
|
|
default: '默认排序',
|
|
'price-asc': '价格从低到高',
|
|
'price-desc': '价格从高到低',
|
|
'name-asc': '名称排序'
|
|
},
|
|
en: {
|
|
default: 'Default',
|
|
'price-asc': 'Price: Low to High',
|
|
'price-desc': 'Price: High to Low',
|
|
'name-asc': 'Name'
|
|
}
|
|
}
|
|
|
|
export default {
|
|
name: 'SelectRoomTab',
|
|
components: {
|
|
SelectRoomHero,
|
|
SelectRoomAreaPanel
|
|
},
|
|
data() {
|
|
return {
|
|
isLogin: false,
|
|
userInfo: null,
|
|
currentBannerIndex: 0,
|
|
currentScreen: 0,
|
|
isHeroCollapsed: false,
|
|
isBannerLoading: false,
|
|
bannerLoadedFromApi: false,
|
|
bannerLoadError: '',
|
|
isAreaLoading: false,
|
|
areaLoadError: '',
|
|
areaList: [],
|
|
selectedAreaCode: '',
|
|
selectedTag: '',
|
|
selectedSortKey: 'default',
|
|
roomLoadError: '',
|
|
navBarBg: 'transparent',
|
|
navBarTransition: { transition: 'background-color 0.3s ease' },
|
|
bannerList: []
|
|
}
|
|
},
|
|
computed: {
|
|
currentLang() {
|
|
return this.$getLanguage()
|
|
},
|
|
currentBanner() {
|
|
return this.bannerList[this.currentBannerIndex] || {}
|
|
},
|
|
compactBannerTop() {
|
|
if (this.currentScreen === 0) {
|
|
return this.vuex_custom_bar_height || 88
|
|
}
|
|
return (this.vuex_status_bar_height || 20) + 12
|
|
},
|
|
currentAreaLabel() {
|
|
const matchedArea = this.areaList.find((item) => item.code === this.resolveBannerAreaCode())
|
|
if (matchedArea) {
|
|
return this.getLocalizedAreaText(matchedArea)
|
|
}
|
|
if (this.currentLang === 'en') {
|
|
return this.currentBanner.areaLabelEnDisplay || this.currentBanner.areaLabelEn || this.currentBanner.areaLabel || this.$tr('全部区域')
|
|
}
|
|
return this.currentBanner.areaLabel || this.$tr('全部区域')
|
|
},
|
|
currentTagLabel() {
|
|
return this.selectedTag || this.$tr('全部标签')
|
|
},
|
|
availableFilterTags() {
|
|
const preferredOrder = ['押一付一', '生活便利', '独立厨卫', '商圈环绕', '民水民电', '拎包入住', '近地铁', '智能家居']
|
|
const tagSet = new Set()
|
|
|
|
this.areaList.forEach((area) => {
|
|
;(area.tags || []).forEach((tag) => tagSet.add(tag))
|
|
;(area.roomTypes || []).forEach((roomType) => {
|
|
;(roomType.tags || []).forEach((tag) => tagSet.add(tag))
|
|
})
|
|
})
|
|
|
|
const preferredTags = preferredOrder.filter((tag) => tagSet.has(tag))
|
|
const extraTags = [...tagSet].filter((tag) => !preferredOrder.includes(tag))
|
|
|
|
return [...preferredTags, ...extraTags]
|
|
},
|
|
currentSortLabel() {
|
|
const labelMap = SORT_LABEL_MAP[this.currentLang] || SORT_LABEL_MAP.zh
|
|
return labelMap[this.selectedSortKey] || labelMap.default
|
|
},
|
|
displayAreaSections() {
|
|
const filteredAreas = this.areaList
|
|
.map((area) => ({
|
|
...area,
|
|
roomTypes: this.filterAndSortRoomTypes(area)
|
|
}))
|
|
.filter((area) => (area.code || area.name) && area.roomTypes.length)
|
|
|
|
return this.sortAreaSections(filteredAreas)
|
|
}
|
|
},
|
|
watch: {
|
|
currentLang() {
|
|
this.loadBannerList()
|
|
this.refreshSelectRoomState()
|
|
}
|
|
},
|
|
async mounted() {
|
|
this.checkLoginStatus()
|
|
await this.loadBannerList()
|
|
},
|
|
methods: {
|
|
getLocalizedBannerText(banner = {}, field = '') {
|
|
const key = String(field || '').trim()
|
|
if (!key) {
|
|
return ''
|
|
}
|
|
const zhText = banner[key]
|
|
const enText = banner[`${key}En`]
|
|
if (this.currentLang === 'en') {
|
|
return enText || zhText || ''
|
|
}
|
|
return zhText || ''
|
|
},
|
|
getLocalizedAreaText(area = {}) {
|
|
if (this.currentLang === 'en') {
|
|
return area.nameEnDisplay || area.nameEn || area.name || ''
|
|
}
|
|
return area.name || ''
|
|
},
|
|
getLocalizedRoomTypeText(roomType = {}) {
|
|
if (this.currentLang === 'en') {
|
|
return roomType.nameEnDisplay || roomType.nameEn || roomType.name || ''
|
|
}
|
|
return roomType.name || ''
|
|
},
|
|
toggleLang() {
|
|
this.$toggleLocale()
|
|
},
|
|
checkLoginStatus() {
|
|
const session = getSessionSnapshot()
|
|
this.isLogin = session.isLogin
|
|
this.userInfo = this.isLogin ? getStoredUser() : null
|
|
},
|
|
hasActiveReservationFlow() {
|
|
const storeUser = (this.$store && this.$store.state && this.$store.state.vuex_user) || {}
|
|
if (storeUser.hasActiveReservationFlow === true) {
|
|
return true
|
|
}
|
|
if (storeUser.hasActiveReservationFlow === false) {
|
|
return false
|
|
}
|
|
const session = getSessionSnapshot()
|
|
const user = session.currentUser || session.registeredUser || session.resolvedUser || {}
|
|
if (user.hasActiveReservationFlow === true) {
|
|
return true
|
|
}
|
|
if (user.hasActiveReservationFlow === false) {
|
|
return false
|
|
}
|
|
return false
|
|
},
|
|
refreshSelectRoomState() {
|
|
if (!this.isLogin || !this.hasActiveReservationFlow()) {
|
|
this.loadAreaList()
|
|
}
|
|
},
|
|
loadBannerList() {
|
|
this.isBannerLoading = true
|
|
this.bannerLoadError = ''
|
|
|
|
return bannerAll(this.currentLang)
|
|
.then((res) => {
|
|
return Array.isArray(res?.data)
|
|
? res.data
|
|
: Array.isArray(res)
|
|
? res
|
|
: []
|
|
})
|
|
.catch((e) => {
|
|
console.warn('bannerAll API failed, using fallback', e)
|
|
return []
|
|
})
|
|
.then((rawList) => {
|
|
const enabledList = rawList
|
|
.filter((item) => item && item.enabled !== false)
|
|
.sort((a, b) => (a.sort || 0) - (b.sort || 0))
|
|
.map((item, index) => normalizeBannerItem(item, index))
|
|
|
|
this.bannerList = enabledList
|
|
this.bannerLoadedFromApi = true
|
|
this.currentBannerIndex = 0
|
|
})
|
|
.finally(() => {
|
|
this.isBannerLoading = false
|
|
})
|
|
},
|
|
loadAreaList() {
|
|
this.isAreaLoading = true
|
|
this.areaLoadError = ''
|
|
|
|
return areaPagination({ PageIndex: 1, PageSize: 10000 }, this.currentLang)
|
|
.then((res) => {
|
|
return Array.isArray(res?.data?.data)
|
|
? res.data.data
|
|
: Array.isArray(res?.data)
|
|
? res.data
|
|
: []
|
|
})
|
|
.catch((e) => {
|
|
console.warn('areaPagination API failed, using fallback', e)
|
|
return []
|
|
})
|
|
.then((rawList) => {
|
|
if (rawList && rawList.length) {
|
|
this.areaList = rawList.map((item) => normalizeAreaItem(item))
|
|
} else {
|
|
this.areaList = []
|
|
}
|
|
})
|
|
.finally(() => {
|
|
this.isAreaLoading = false
|
|
})
|
|
},
|
|
resolveBannerAreaCode() {
|
|
if (!this.areaList.length) {
|
|
return ''
|
|
}
|
|
|
|
if (this.currentBanner.areaCode) {
|
|
const codeMatched = this.areaList.find((item) => item.code === this.currentBanner.areaCode)
|
|
if (codeMatched) {
|
|
return codeMatched.code
|
|
}
|
|
}
|
|
|
|
const byName = this.areaList.find((item) => {
|
|
const labels = [
|
|
item.name,
|
|
item.nameEn
|
|
].filter(Boolean)
|
|
return labels.includes(this.currentBanner.areaLabel) || labels.includes(this.currentBanner.areaLabelEn)
|
|
})
|
|
if (byName) {
|
|
return byName.code
|
|
}
|
|
|
|
const fuzzy = this.areaList.find((item) => {
|
|
const labels = [
|
|
this.currentBanner.areaLabel,
|
|
this.currentBanner.areaLabelEn
|
|
].filter(Boolean)
|
|
return labels.some((label) => label.includes(item.name) || label.includes(item.nameEn || ''))
|
|
})
|
|
return fuzzy ? fuzzy.code : ''
|
|
},
|
|
applyBannerFilters() {
|
|
const resolvedAreaCode = this.resolveBannerAreaCode()
|
|
this.selectedAreaCode = resolvedAreaCode
|
|
},
|
|
selectArea(areaCode = '') {
|
|
this.selectedAreaCode = areaCode
|
|
},
|
|
selectTag(tag = '') {
|
|
this.selectedTag = this.selectedTag === tag ? '' : tag
|
|
},
|
|
selectSort(sortKey = 'default') {
|
|
this.selectedSortKey = sortKey
|
|
},
|
|
getPriceValue(priceText) {
|
|
if (!priceText) {
|
|
return Number.MAX_SAFE_INTEGER
|
|
}
|
|
const matched = String(priceText).match(/(\d+(\.\d+)?)/)
|
|
return matched ? Number(matched[1]) : Number.MAX_SAFE_INTEGER
|
|
},
|
|
getRoomTypePriceValue(roomType = {}) {
|
|
if (Number.isFinite(roomType.priceValue)) {
|
|
return roomType.priceValue
|
|
}
|
|
return this.getPriceValue(roomType.price)
|
|
},
|
|
isTagMatched(tags = [], tag = '') {
|
|
if (!tag) {
|
|
return true
|
|
}
|
|
return Array.isArray(tags) && tags.includes(tag)
|
|
},
|
|
filterAndSortRoomTypes(area = {}) {
|
|
const areaMatched = this.isTagMatched(area.tags, this.selectedTag)
|
|
const roomTypes = areaMatched || !this.selectedTag
|
|
? (area.roomTypes || [])
|
|
: (area.roomTypes || []).filter((roomType) => this.isTagMatched(roomType.tags, this.selectedTag))
|
|
|
|
return this.sortRoomTypes(roomTypes)
|
|
},
|
|
sortRoomTypes(roomTypes = []) {
|
|
const list = [...roomTypes]
|
|
if (this.selectedSortKey === 'price-asc') {
|
|
return list.sort((a, b) => this.getRoomTypePriceValue(a) - this.getRoomTypePriceValue(b))
|
|
}
|
|
if (this.selectedSortKey === 'price-desc') {
|
|
return list.sort((a, b) => this.getRoomTypePriceValue(b) - this.getRoomTypePriceValue(a))
|
|
}
|
|
if (this.selectedSortKey === 'name-asc') {
|
|
return list.sort((a, b) => String(this.getLocalizedRoomTypeText(a) || '').localeCompare(String(this.getLocalizedRoomTypeText(b) || ''), this.currentLang === 'en' ? 'en' : 'zh-Hans-CN'))
|
|
}
|
|
return list
|
|
},
|
|
getAreaSortPrice(area = {}, mode = 'min') {
|
|
const values = (area.roomTypes || [])
|
|
.map((roomType) => this.getRoomTypePriceValue(roomType))
|
|
.filter((value) => Number.isFinite(value) && value !== Number.MAX_SAFE_INTEGER)
|
|
|
|
if (!values.length) {
|
|
return Number.MAX_SAFE_INTEGER
|
|
}
|
|
|
|
return mode === 'max' ? Math.max(...values) : Math.min(...values)
|
|
},
|
|
sortAreaSections(areas = []) {
|
|
const list = [...areas]
|
|
if (this.selectedSortKey === 'price-asc') {
|
|
return list.sort((a, b) => this.getAreaSortPrice(a, 'min') - this.getAreaSortPrice(b, 'min'))
|
|
}
|
|
if (this.selectedSortKey === 'price-desc') {
|
|
return list.sort((a, b) => this.getAreaSortPrice(b, 'max') - this.getAreaSortPrice(a, 'max'))
|
|
}
|
|
if (this.selectedSortKey === 'name-asc') {
|
|
return list.sort((a, b) => String(this.getLocalizedAreaText(a) || '').localeCompare(String(this.getLocalizedAreaText(b) || ''), this.currentLang === 'en' ? 'en' : 'zh-Hans-CN'))
|
|
}
|
|
return list
|
|
},
|
|
openServiceEntry({ path, title, desc, key, payload }) {
|
|
uni.navigateTo({
|
|
url: `${path}?title=${encodeURIComponent(title)}&desc=${encodeURIComponent(desc)}&key=${encodeURIComponent(key)}&payload=${encodeURIComponent(JSON.stringify(payload || {}))}`
|
|
})
|
|
},
|
|
handleBannerChange(index) {
|
|
this.currentBannerIndex = index
|
|
},
|
|
switchBanner(index) {
|
|
this.currentBannerIndex = index
|
|
},
|
|
collapseToTopBanner() {
|
|
this.currentScreen = 1
|
|
},
|
|
collapseToHero() {
|
|
this.currentScreen = 0
|
|
},
|
|
handleScreenChange(e) {
|
|
this.currentScreen = e.detail.current
|
|
this.isHeroCollapsed = this.currentScreen === 1
|
|
this.navBarBg = this.isHeroCollapsed ? 'rgba(17, 17, 17, 0.92)' : 'transparent'
|
|
},
|
|
retryLoadBanner() {
|
|
this.loadBannerList()
|
|
},
|
|
openRoomTypeEntry(area, roomType) {
|
|
uni.navigateTo({
|
|
url: `/pkgExplore/room_detail/room_detail?roomTypeData=${encodeURIComponent(JSON.stringify(roomType))}&areaData=${encodeURIComponent(JSON.stringify({
|
|
code: area.code,
|
|
name: area.name,
|
|
nameEn: area.nameEn || '',
|
|
address: area.address
|
|
,
|
|
addressEn: area.addressEn || ''
|
|
}))}`
|
|
})
|
|
},
|
|
handleConsult() {
|
|
// 从 areaList 中匹配当前 banner 对应的小区信息
|
|
const resolvedAreaCode = this.resolveBannerAreaCode()
|
|
const area = this.areaList.find(item => item.code === resolvedAreaCode)
|
|
|
|
if (area) {
|
|
uni.navigateTo({
|
|
url: `/pkgExplore/store_detail/store_detail?storeData=${encodeURIComponent(JSON.stringify(area))}`
|
|
})
|
|
} else {
|
|
// 容错处理
|
|
uni.showToast({
|
|
title: this.$tr('该区域暂未开放'),
|
|
icon: 'none'
|
|
})
|
|
}
|
|
},
|
|
handleSubmit() {
|
|
const bookingUrl = `/pkgBooking/book_viewing/book_viewing?payload=${encodeURIComponent(JSON.stringify({
|
|
sourceType: 'banner',
|
|
bannerTitle: this.getLocalizedBannerText(this.currentBanner, 'title'),
|
|
bannerTitleEn: this.currentBanner.titleEn || '',
|
|
bannerSubtitle: this.getLocalizedBannerText(this.currentBanner, 'subtitle'),
|
|
bannerSubtitleEn: this.currentBanner.subtitleEn || '',
|
|
bannerDesc: this.getLocalizedBannerText(this.currentBanner, 'desc'),
|
|
bannerDescEn: this.currentBanner.descEn || '',
|
|
areaLabel: this.currentAreaLabel,
|
|
areaLabelEn: this.currentLang === 'en'
|
|
? this.currentAreaLabel
|
|
: ((this.areaList.find((item) => item.code === this.resolveBannerAreaCode()) || {}).nameEn || this.currentBanner.areaLabelEn || ''),
|
|
areaCode: this.resolveBannerAreaCode()
|
|
}))}`
|
|
if (!ensureBookingAccess({
|
|
language: this.currentLang,
|
|
userInfo: this.userInfo,
|
|
loginRedirectUrl: bookingUrl
|
|
})) {
|
|
return
|
|
}
|
|
uni.navigateTo({
|
|
url: bookingUrl
|
|
})
|
|
}
|
|
}
|
|
,
|
|
onShareAppMessage() {
|
|
return {
|
|
title: this.$tr('绿居公寓 - 国际化青年社区'),
|
|
path: '/pages/index/index'
|
|
}
|
|
},
|
|
onShareTimeline() {
|
|
return {
|
|
title: this.$tr('绿居公寓 - 国际化青年社区')
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.select-room-page {
|
|
height: 100vh;
|
|
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.page-swiper {
|
|
height: 100vh;
|
|
}
|
|
|
|
.panel-scroll {
|
|
height: calc(100vh - env(safe-area-inset-bottom));
|
|
background: linear-gradient(180deg, #F0F6FF 0%, #F8FAFF 100%);
|
|
}
|
|
|
|
/* 隐藏整个页面的滚动条 */
|
|
::-webkit-scrollbar {
|
|
display: none;
|
|
width: 0 !important;
|
|
height: 0 !important;
|
|
-webkit-appearance: none;
|
|
background: transparent;
|
|
}
|
|
|
|
.custom-nav {
|
|
width: 100%;
|
|
padding: 0 40rpx;
|
|
height: 100%;
|
|
}
|
|
|
|
.lang-text-toggle {
|
|
color: rgba(255, 255, 255, 0.6);
|
|
font-size: 24rpx;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
letter-spacing: 2rpx;
|
|
}
|
|
|
|
.lang-text {
|
|
transition: all 0.3s ease;
|
|
padding: 10rpx 0;
|
|
}
|
|
|
|
.lang-text--active {
|
|
color: #ffffff;
|
|
font-weight: 700;
|
|
text-shadow: 0 0 12rpx rgba(255, 255, 255, 0.4);
|
|
}
|
|
|
|
.lang-split {
|
|
margin: 0 12rpx;
|
|
opacity: 0.3;
|
|
font-size: 20rpx;
|
|
}
|
|
</style>
|