chore: initial import

This commit is contained in:
liangjian
2026-06-29 09:42:53 +08:00
commit 9fea127de1
241 changed files with 82767 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,828 @@
<template>
<view class="panel-container">
<view
v-if="bannerList.length"
id="content-start"
class="compact-banner-wrap"
:style="{ top: 0 }"
>
<!-- 收缩轮播图 -->
<view class="compact-swiper-container">
<swiper
class="compact-swiper"
circular
:current="currentBannerIndex"
@change="handleBannerChange"
>
<swiper-item v-for="(item, index) in bannerList" :key="index">
<image :src="item.image" mode="aspectFill" class="compact-swiper__image"></image>
<view class="compact-swiper__mask"></view>
</swiper-item>
</swiper>
</view>
<!-- 搜索栏 -->
<view class="search-header tn-flex tn-flex-row-between tn-flex-col-center">
<view class="back-btn tn-flex tn-flex-col-center tn-flex-row-center" @click="$emit('collapse-to-hero')">
<text class="tn-icon-up nav-icon"></text>
</view>
<view class="search-box tn-flex tn-flex-col-center">
<text class="tn-icon-search search-icon"></text>
<text class="search-placeholder">{{ t('searchApartment') }}</text>
</view>
<view
class="sort-trigger tn-flex tn-flex-col-center tn-flex-row-center"
:class="{ 'sort-trigger--active': selectedSortKey !== 'default' }"
@click="showSortActionSheet"
>
<text class="sort-trigger__text">{{ currentSortLabel }}</text>
<text class="tn-icon-down sort-trigger__arrow"></text>
</view>
</view>
<view class="quick-filter-card">
<view class="quick-filter-head tn-flex tn-flex-row-between tn-flex-col-center">
<view class="quick-filter-title-box tn-flex tn-flex-col-center">
<view class="quick-filter-dot"></view>
<text class="quick-filter-title">{{ t('tagFilter') }}</text>
</view>
<text class="quick-filter-current">{{ displayTag(currentTagLabel) }}</text>
</view>
<!-- 快捷标签栏 (横向滚动) -->
<scroll-view class="quick-tags-scroll" scroll-x :show-scrollbar="false">
<view class="quick-tags-wrap tn-flex tn-flex-col-center">
<view
class="quick-tag"
:class="{ 'quick-tag--active': !selectedTag }"
@click="$emit('select-tag', '')"
>
{{ t('all') }}
</view>
<view
v-for="tag in availableFilterTags"
:key="tag"
class="quick-tag"
:class="{ 'quick-tag--active': selectedTag === tag }"
@click="$emit('select-tag', tag)"
>
{{ displayTag(tag) }}
</view>
</view>
</scroll-view>
</view>
</view>
<view class="content-section">
<view class="content-panel">
<view
v-if="isAreaLoading || roomLoadError || areaLoadError"
class="room-status-card tn-margin-bottom-lg"
>
<view class="room-status-card__title">
{{ isAreaLoading ? t('loadingAreaStatus') : t('areaStatus') }}
</view>
<view class="room-status-card__desc">
{{ roomLoadError || areaLoadError || t('areaStatusDesc') }}
</view>
</view>
<view v-if="!isAreaLoading && !displayAreaSections.length" class="empty-state-card tn-margin-bottom-lg">
<view class="empty-state-card__title">{{ bannerList.length ? t('emptyAreaTitle') : t('emptyBannerTitle') }}</view>
<view class="empty-state-card__desc">
{{ bannerList.length ? t('emptyAreaDesc') : t('emptyBannerDesc') }}
</view>
</view>
<!-- 小区/区域卡片 -->
<view v-for="area in displayAreaSections" :key="area.code" class="store-card">
<!-- 小区头部信息 -->
<view class="store-header" @click="openStoreDetail(area)">
<view class="store-title-bar tn-flex tn-flex-row-between tn-flex-col-center">
<view class="store-title tn-text-ellipsis">{{ getAreaName(area) }}</view>
<view class="store-detail-btn tn-flex tn-flex-col-center">
<text class="btn-text">{{ t('viewCommunity') }}</text>
<text class="tn-icon-right btn-icon"></text>
</view>
</view>
<view class="store-tags tn-flex tn-flex-wrap">
<view v-for="tag in getDisplayTags(area)" :key="tag" class="store-tag">{{ displayTag(tag) }}</view>
</view>
<view class="store-location-info tn-flex tn-flex-row-between tn-flex-col-center">
<view class="store-address tn-flex tn-flex-col-center tn-text-ellipsis">
<view class="address-icon-box tn-flex tn-flex-col-center tn-flex-row-center">
<text class="tn-icon-location location-icon"></text>
</view>
<text class="address-text">{{ getAreaAddress(area) || defaultAreaAddress(getAreaName(area)) }}</text>
</view>
<view class="store-distance">{{ t('distancePlaceholder') }}</view>
</view>
</view>
<!-- 横向滚动房型列表 -->
<scroll-view class="room-scroll" scroll-x :show-scrollbar="false">
<view class="room-list-wrap tn-flex">
<view
v-for="(roomType, idx) in area.roomTypes"
:key="roomType.code"
class="room-item"
@click="$emit('open-room-type', area, roomType)"
>
<view class="room-image-box">
<image
:src="roomType.images && roomType.images.length ? roomType.images[0] : ''"
mode="aspectFill"
class="room-image"
></image>
<view class="room-tag-overlay" v-if="roomType.payMethod">{{ getPayMethodText(roomType.payMethod) }}</view>
</view>
<view class="room-info">
<view class="room-name tn-text-ellipsis">{{ formatRoomTypeName(getRoomTypeName(roomType)) }}</view>
<view class="room-price-row tn-flex tn-flex-row-between tn-flex-col-bottom">
<view class="price-content">
<text class="price-symbol">¥</text>
<text class="price-val">{{ formatDisplayPrice(roomType.price) || t('pricePending') }}</text>
<text class="price-unit">{{ t('priceUnit') }}</text>
</view>
<view class="room-go-icon tn-flex tn-flex-col-center tn-flex-row-center">
<text class="tn-icon-right"></text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部安全区占位,防止最后一张卡片被 tabBar 遮挡 -->
<view class="safe-area-bottom-padding"></view>
</view>
</view>
</view>
</template>
<script>
import { localizeHousingTag, localizePayMethodText } from '../../../utils/select-room-normalizers'
export default {
name: 'SelectRoomAreaPanel',
props: {
isBannerLoading: {
type: Boolean,
default: false
},
bannerLoadError: {
type: String,
default: ''
},
bannerLoadedFromApi: {
type: Boolean,
default: false
},
currentBanner: {
type: Object,
default: () => ({})
},
selectedAreaCode: {
type: String,
default: ''
},
areaList: {
type: Array,
default: () => []
},
currentAreaLabel: {
type: String,
default: ''
},
selectedTag: {
type: String,
default: ''
},
currentTagLabel: {
type: String,
default: '全部标签'
},
availableFilterTags: {
type: Array,
default: () => []
},
selectedSortKey: {
type: String,
default: 'default'
},
currentSortLabel: {
type: String,
default: ''
},
isAreaLoading: {
type: Boolean,
default: false
},
roomLoadError: {
type: String,
default: ''
},
areaLoadError: {
type: String,
default: ''
},
isLogin: {
type: Boolean,
default: false
},
userInfo: {
type: Object,
default: null
},
displayAreaSections: {
type: Array,
default: () => []
},
bannerList: {
type: Array,
default: () => []
},
currentBannerIndex: {
type: Number,
default: 0
},
language: {
type: String,
default: 'zh'
},
compactBannerTop: {
type: Number,
default: 88
}
},
data() {
return {}
},
methods: {
displayTag(tag = '') {
const text = String(tag || '').trim()
if (text === '全部标签' && this.language === 'en') {
return 'All Tags'
}
return localizeHousingTag(text, this.language)
},
t(key) {
return this.$tm('pages.index.component.SelectRoomTab', key)
},
formatRoomTypeName(name) {
const normalizedName = String(name || '')
return normalizedName.replace(/^[^·]+·/, '')
},
getAreaName(area = {}) {
if (this.language === 'en') {
return area.nameEnDisplay || area.nameEn || area.name || ''
}
return area.name || ''
},
getAreaAddress(area = {}) {
if (this.language === 'en') {
return area.addressEnDisplay || area.addressEn || area.address || ''
}
return area.address || ''
},
getRoomTypeName(roomType = {}) {
if (this.language === 'en') {
return roomType.nameEnDisplay || roomType.nameEn || roomType.name || ''
}
return roomType.name || ''
},
getPayMethodText(payMethod = '') {
return localizePayMethodText(payMethod, this.language)
},
formatDisplayPrice(price) {
if (!price) {
return ''
}
const normalizedPrice = String(price).replace(/,/g, '')
const matched = normalizedPrice.match(/(\d+(?:\.\d+)?)/)
return matched ? matched[1] : ''
},
getDisplayTags(area) {
const areaTags = Array.isArray(area.tags) ? area.tags : []
if (areaTags.length) {
return areaTags.slice(0, 4)
}
const roomTypeTags = (area.roomTypes || [])
.flatMap((roomType) => roomType.tags || [])
.filter(Boolean)
return [...new Set(roomTypeTags)].slice(0, 4)
},
showTagActionSheet() {
const tagOptions = [this.t('allTags'), ...this.availableFilterTags]
if (!tagOptions.length) {
return
}
uni.showActionSheet({
itemList: tagOptions.map((tag) => this.displayTag(tag)),
success: ({ tapIndex }) => {
const selectedTag = tapIndex === 0 ? '' : tagOptions[tapIndex]
this.$emit('select-tag', selectedTag)
}
})
},
showSortActionSheet() {
const sortOptions = [
{ label: this.t('sortDefault'), value: 'default' },
{ label: this.t('sortPriceAsc'), value: 'price-asc' },
{ label: this.t('sortPriceDesc'), value: 'price-desc' },
{ label: this.t('sortNameAsc'), value: 'name-asc' }
]
uni.showActionSheet({
itemList: sortOptions.map((item) => item.label),
success: ({ tapIndex }) => {
const selected = sortOptions[tapIndex]
if (selected) {
this.$emit('select-sort', selected.value)
}
}
})
},
handleBannerChange(e) {
this.$emit('banner-change', e.detail.current)
},
openStoreDetail(area) {
uni.navigateTo({
url: `/pkgExplore/store_detail/store_detail?storeData=${encodeURIComponent(JSON.stringify(area))}`
})
},
defaultAreaAddress(areaName = '') {
if (this.language === 'en') {
return `${this.t('defaultAreaAddressPrefix')}${areaName} CBD`
}
return `${this.t('defaultAreaAddressPrefix')}${areaName}核心商圈`
}
}
}
</script>
<style scoped lang="scss">
.panel-container {
min-height: 100vh;
background-color: transparent;
}
.content-panel {
padding: 24rpx;
box-sizing: border-box;
}
/* 顶部搜索栏包装 */
.compact-banner-wrap {
position: sticky;
z-index: 99;
padding: 0 0 24rpx;
overflow: hidden;
background-color: #fff;
}
.compact-swiper-container {
width: 100%;
height: 420rpx;
position: relative;
}
.compact-swiper {
width: 100%;
height: 100%;
}
.compact-swiper__image {
width: 100%;
height: 100%;
}
.compact-swiper__mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom,
rgba(0,0,0,0.15) 0%,
rgba(0,0,0,0) 30%,
rgba(255,255,255,0) 60%,
rgba(255,255,255,1) 100%
);
}
.search-header {
position: relative;
z-index: 100;
margin-top: -96rpx;
min-height: 92rpx;
background: rgba(255, 255, 255, 0.96);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 46rpx;
padding: 0 36rpx;
box-sizing: border-box;
margin-left: 24rpx;
margin-right: 24rpx;
border: 1rpx solid rgba(255, 255, 255, 0.8);
box-shadow:
0 10rpx 30rpx rgba(0, 0, 0, 0.05),
0 4rpx 10rpx rgba($brand-primary, 0.05);
}
.back-btn {
width: 60rpx;
height: 60rpx;
flex-shrink: 0;
color: #333;
margin-right: 20rpx;
background: #f8f9fa;
border-radius: 50%;
border: 1rpx solid rgba(0, 0, 0, 0.03);
}
.nav-icon {
font-size: 28rpx;
font-weight: bold;
}
.search-box {
flex: 1;
color: #333;
}
.search-icon {
font-size: 34rpx;
margin-right: 16rpx;
line-height: 1;
color: $brand-primary;
}
.search-placeholder {
font-size: 28rpx;
color: #a0a7b5;
letter-spacing: 1rpx;
font-weight: 400;
}
.sort-trigger {
max-width: 200rpx;
min-width: 144rpx;
height: 64rpx;
padding: 0 24rpx;
flex-shrink: 0;
border-radius: 32rpx;
border: 1rpx solid rgba($brand-primary, 0.12);
background: #f0f7ff;
color: #0C6E8B;
box-sizing: border-box;
transition: all 0.2s ease;
}
.sort-trigger--active {
background: $brand-primary;
color: #ffffff;
border-color: $brand-primary;
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.2);
}
.sort-trigger__text {
max-width: 128rpx;
font-size: 24rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sort-trigger__arrow {
margin-left: 10rpx;
font-size: 20rpx;
line-height: 1;
opacity: 0.8;
}
.quick-filter-card {
margin: 18rpx 24rpx 0;
padding: 22rpx 0 18rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.96);
border: 1rpx solid rgba(255, 255, 255, 0.88);
box-shadow:
0 12rpx 28rpx rgba(15, 23, 42, 0.05),
0 6rpx 16rpx rgba($brand-primary, 0.06);
}
.quick-filter-head {
padding: 0 24rpx 14rpx;
}
.quick-filter-title-box {
color: #1E293B;
}
.quick-filter-dot {
width: 12rpx;
height: 12rpx;
margin-right: 12rpx;
border-radius: 50%;
background: linear-gradient(135deg, $brand-primary 0%, #76B8FF 100%);
box-shadow: 0 0 0 6rpx rgba($brand-primary, 0.08);
}
.quick-filter-title {
font-size: 24rpx;
font-weight: 600;
color: #334155;
letter-spacing: 1rpx;
}
.quick-filter-current {
max-width: 220rpx;
font-size: 22rpx;
color: #94A3B8;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 横向快捷标签 */
.quick-tags-scroll {
background-color: transparent;
padding: 0;
width: 100%;
}
.quick-tags-wrap {
padding: 0 24rpx;
}
.quick-tag {
flex-shrink: 0;
min-width: 96rpx;
text-align: center;
background: #F8FBFF;
color: #475569;
font-size: 24rpx;
padding: 12rpx 26rpx;
border-radius: 999rpx;
margin-right: 16rpx;
font-weight: 500;
border: 1rpx solid rgba($brand-primary, 0.08);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.8);
}
.quick-tag--active {
background: linear-gradient(135deg, rgba($brand-primary, 0.16) 0%, rgba(118, 184, 255, 0.24) 100%);
color: #0C6E8B;
border-color: rgba($brand-primary, 0.22);
box-shadow: 0 8rpx 18rpx rgba($brand-primary, 0.10);
}
/* 小区卡片整体 */
.store-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
border-radius: 32rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.08);
border: 1rpx solid rgba(255, 255, 255, 1);
}
/* 小区头部信息 */
.store-header {
margin-bottom: 32rpx;
}
.store-title-bar {
min-width: 0;
margin-bottom: 20rpx;
}
.store-title {
font-size: 38rpx;
font-weight: 700;
color: #1A1D24;
max-width: 75%;
letter-spacing: 1rpx;
}
.store-detail-btn {
background: rgba($brand-primary, 0.08);
padding: 8rpx 20rpx;
border-radius: 24rpx;
color: $brand-primary;
.btn-text {
font-size: 22rpx;
font-weight: 600;
}
.btn-icon {
font-size: 20rpx;
margin-left: 4rpx;
}
}
.store-tags {
margin-bottom: 24rpx;
gap: 12rpx;
}
.store-tag {
color: #0C6E8B;
background: #F0F9FF;
font-size: 22rpx;
padding: 6rpx 18rpx;
border-radius: 12rpx;
font-weight: 500;
border: 1rpx solid rgba($brand-primary, 0.1);
}
.store-location-info {
color: #64748B;
font-size: 24rpx;
background: #F8FAFC;
padding: 16rpx 24rpx;
border-radius: 20rpx;
border: 1rpx solid rgba(0, 0, 0, 0.02);
}
.store-address {
flex: 1;
min-width: 0;
margin-right: 24rpx;
}
.address-icon-box {
width: 44rpx;
height: 44rpx;
background: #fff;
border-radius: 12rpx;
margin-right: 12rpx;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.03);
}
.location-icon {
font-size: 26rpx;
color: $brand-primary;
}
.address-text {
min-width: 0;
color: #475569;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.store-distance {
color: #1E293B;
font-weight: 600;
font-size: 22rpx;
}
/* 横向滚动房型列表 */
.room-scroll {
width: 100%;
}
.room-list-wrap {
gap: 16rpx;
padding-bottom: 12rpx;
}
.room-item {
width: 240rpx;
flex-shrink: 0;
background: #ffffff;
border-radius: 16rpx;
padding-bottom: 16rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.03);
overflow: hidden;
border: 1rpx solid #F1F5F9;
transition: all 0.2s ease;
}
.room-item:active {
transform: translateY(-2rpx);
box-shadow: 0 10rpx 24rpx rgba($brand-primary, 0.1);
}
.room-image-box {
width: 100%;
height: 180rpx;
position: relative;
overflow: hidden;
}
.room-image {
width: 100%;
height: 100%;
background-color: #F8FAFC;
transition: transform 0.4s ease;
}
.room-tag-overlay {
position: absolute;
top: 12rpx;
right: 12rpx;
max-width: calc(100% - 24rpx);
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8rpx);
color: #fff;
font-size: 18rpx;
padding: 4rpx 10rpx;
border-radius: 999rpx;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.room-info {
width: 100%;
padding: 16rpx 16rpx 0;
}
.room-name {
font-size: 26rpx;
color: #1E293B;
font-weight: 600;
margin-bottom: 12rpx;
}
.room-price-row {
width: 100%;
}
.price-symbol {
font-size: 20rpx;
color: #EF4444;
font-weight: 700;
margin-right: 2rpx;
}
.price-val {
color: #EF4444;
font-size: 32rpx;
font-weight: 800;
line-height: 1;
}
.price-unit {
color: #94A3B8;
font-size: 18rpx;
margin-left: 4rpx;
font-weight: 500;
}
.room-go-icon {
width: 32rpx;
height: 32rpx;
background: #F8FAFC;
border-radius: 50%;
color: #94A3B8;
font-size: 18rpx;
border: 1rpx solid #F1F5F9;
}
.safe-area-bottom-padding {
height: calc(140rpx + env(safe-area-inset-bottom));
width: 100%;
}
.room-status-card,
.empty-state-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10rpx);
border-radius: 20rpx;
padding: 32rpx;
text-align: center;
border: 1rpx solid rgba($brand-primary, 0.1);
box-shadow: 0 4rpx 16rpx rgba($brand-primary, 0.04);
}
.room-status-card__title,
.empty-state-card__title {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.room-status-card__desc,
.empty-state-card__desc {
font-size: 26rpx;
color: #666;
}
</style>

View File

@@ -0,0 +1,459 @@
<template>
<view>
<view v-if="bannerList.length" class="hero-shell">
<swiper
class="hero-swiper"
circular
:autoplay="true"
interval="6000"
duration="800"
:current="currentBannerIndex"
@change="handleBannerChange"
>
<swiper-item v-for="item in bannerList" :key="item.id">
<image :src="item.image" mode="aspectFill" class="hero-image"></image>
</swiper-item>
</swiper>
<view class="hero-mask"></view>
<view class="hero-content">
<!-- 顶部横向 Tabs -->
<view class="hero-tabs tn-flex tn-flex-row-center" :class="{ 'hero-tabs--en': language === 'en' }">
<view
v-for="(item, index) in bannerList"
:key="item.id"
class="hero-tab-item tn-flex tn-flex-direction-column tn-flex-col-center"
:class="{
'hero-tab-item--active': currentBannerIndex === index,
'hero-tab-item--en': language === 'en'
}"
@click="switchBanner(index)"
>
<text class="tab-title" :class="{ 'tab-title--en': language === 'en' }">{{ displayTabTitle(item) }}</text>
<text class="tab-subtitle" :class="{ 'tab-subtitle--en': language === 'en' }">{{ displayTabSubtitle(item) }}</text>
</view>
</view>
<!-- 居中文案区 -->
<view
class="hero-typography tn-flex tn-flex-direction-column tn-flex-col-center"
:class="{ 'hero-typography--no-logo': !(currentBanner.logoType === 'image' && currentBanner.logoImage) && !currentBanner.logoIconClass }"
>
<image
v-if="currentBanner.logoType === 'image' && currentBanner.logoImage"
:src="currentBanner.logoImage"
mode="aspectFit"
class="hero-logo-image"
></image>
<text
v-else-if="currentBanner.logoIconClass"
:class="[currentBanner.logoIconClass, 'hero-logo-icon']"
></text>
<view class="hero-eng-title" :class="{ 'hero-eng-title--en': language === 'en' }">{{ getBannerText(currentBanner, 'subtitle') }}</view>
<view class="hero-title" :class="{ 'hero-title--en': language === 'en' }">{{ getBannerText(currentBanner, 'title') }}</view>
<view class="hero-desc" :class="{ 'hero-desc--en': language === 'en' }">{{ getBannerText(currentBanner, 'desc') }}</view>
</view>
<!-- 底部操作区 -->
<view class="hero-bottom tn-flex tn-flex-direction-column tn-flex-col-center">
<view class="hero-actions tn-flex tn-flex-row-between" :class="{ 'hero-actions--en': language === 'en' }">
<view class="action-btn outline-btn" :class="{ 'action-btn--en': language === 'en' }" @click="$emit('consult')">{{ t('apartmentDetail') }}</view>
<view class="action-btn primary-btn" :class="{ 'action-btn--en': language === 'en' }" @click="$emit('submit')">{{ t('bookViewing') }}</view>
</view>
<view class="scroll-hint tn-flex tn-flex-direction-column tn-flex-col-center" @click="$emit('collapse')">
<text class="scroll-hint__text">{{ t('learnMore') }}</text>
<view class="scroll-hint__double-icon">
<text class="tn-icon-down scroll-hint__icon"></text>
<text class="tn-icon-down scroll-hint__icon scroll-hint__icon--second"></text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'SelectRoomHero',
props: {
bannerList: {
type: Array,
default: () => []
},
currentBanner: {
type: Object,
default: () => ({})
},
currentBannerIndex: {
type: Number,
default: 0
},
language: {
type: String,
default: 'zh'
},
isHeroCollapsed: {
type: Boolean,
default: false
},
compactBannerTop: {
type: Number,
default: 88
}
},
data() {
return {}
},
methods: {
getBannerText(item = {}, field = '') {
const key = String(field || '').trim()
if (!key) {
return ''
}
const zhText = item[key]
const enText = item[`${key}En`]
if (this.language === 'en') {
return enText || zhText || ''
}
return zhText || ''
},
getBannerDisplayText(item = {}, field = '') {
const key = String(field || '').trim()
if (!key) {
return ''
}
if (this.language === 'en') {
return item[`${key}EnDisplay`] || item[`${key}En`] || item[key] || ''
}
return item[key] || ''
},
displayTabTitle(item = {}) {
return this.formatTabTitle(this.getBannerDisplayText(item, 'title'))
},
displayTabSubtitle(item = {}) {
return this.getBannerDisplayText(item, 'tab')
},
t(key) {
return this.$tm('pages.index.component.SelectRoomTab', key)
},
formatTabTitle(title) {
const normalizedTitle = String(title || '')
return normalizedTitle
.replace(/^[^·]+·/, '')
.replace(/^\s*CBD/, '')
.trim()
},
handleBannerChange(e) {
this.$emit('banner-change', e.detail.current)
},
switchBanner(index) {
this.$emit('banner-switch', index)
}
}
}
</script>
<style scoped lang="scss">
.hero-shell {
position: relative;
height: 100vh;
min-height: 1100rpx;
overflow: hidden;
}
.hero-swiper,
.hero-image {
width: 100%;
height: 100%;
}
.hero-mask {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0.7) 100%);
}
.hero-content {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 180rpx 0 40rpx;
pointer-events: none; /* 让事件穿透到 swiper */
}
.hero-tabs, .hero-typography, .hero-bottom {
pointer-events: auto; /* 恢复子元素事件 */
}
/* 顶部横向 Tabs */
.hero-tabs {
padding: 0 40rpx;
gap: 60rpx;
align-items: flex-start;
}
.hero-tabs--en {
padding: 0 24rpx;
gap: 16rpx;
}
.hero-tab-item {
opacity: 0.6;
transition: all 0.3s ease;
max-width: 240rpx;
min-width: 0;
}
.hero-tab-item--en {
flex: 1;
max-width: none;
min-width: 0;
}
.hero-tab-item--active {
opacity: 1;
}
.tab-title {
display: block;
width: 100%;
color: #ffffff;
font-size: 34rpx;
font-weight: bold;
letter-spacing: 2rpx;
margin-bottom: 8rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-subtitle {
display: block;
width: 100%;
color: #ffffff;
font-size: 22rpx;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-title--en {
font-size: 24rpx;
letter-spacing: 0.5rpx;
line-height: 1.25;
min-height: 60rpx;
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: unset;
}
.tab-subtitle--en {
font-size: 18rpx;
letter-spacing: 0.5rpx;
line-height: 1.2;
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
text-overflow: unset;
}
/* 居中文案区 */
.hero-typography {
position: relative;
margin-top: -100rpx; /* 微调垂直居中位置 */
padding: 0 48rpx;
}
.hero-logo-icon {
font-size: 80rpx;
color: $icon-tone-luxury;
margin-bottom: 24rpx;
text-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.28);
}
.hero-logo-image {
width: 160rpx;
height: 80rpx;
margin-bottom: 24rpx;
}
.hero-typography--no-logo {
margin-top: -36rpx;
}
.hero-typography--no-logo::before {
content: '';
display: block;
width: 88rpx;
height: 2rpx;
margin: 0 auto 28rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.75) 50%, rgba(255, 255, 255, 0) 100%);
}
.hero-eng-title {
color: rgba(255, 255, 255, 0.8);
font-size: 22rpx;
letter-spacing: 4rpx;
text-transform: uppercase;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.hero-eng-title--en {
font-size: 20rpx;
letter-spacing: 3rpx;
}
.hero-typography--no-logo .hero-eng-title {
margin-bottom: 20rpx;
}
.hero-title {
color: #ffffff;
font-size: 56rpx;
font-weight: bold;
letter-spacing: 6rpx;
margin-bottom: 24rpx;
text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
}
.hero-title--en {
font-size: 48rpx;
letter-spacing: 1rpx;
text-align: center;
}
.hero-typography--no-logo .hero-title {
margin-bottom: 28rpx;
}
.hero-desc {
color: #ffffff;
font-size: 30rpx;
letter-spacing: 4rpx;
max-width: 560rpx;
text-align: center;
line-height: 1.7;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
}
.hero-desc--en {
font-size: 26rpx;
letter-spacing: 1rpx;
max-width: 600rpx;
}
/* 底部操作区 */
.hero-bottom {
width: 100%;
/* 避开首页自定义 tabbar(114rpx),同时别抬得太高 */
padding-bottom: calc(132rpx + env(safe-area-inset-bottom));
}
.hero-actions {
width: 100%;
padding: 0 120rpx;
margin-bottom: 60rpx;
gap: 40rpx;
}
.hero-actions--en {
padding: 0 92rpx;
gap: 24rpx;
}
.action-btn {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
border: 2rpx solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 500;
letter-spacing: 2rpx;
line-height: 1;
white-space: nowrap;
transition: all 0.3s ease;
}
.action-btn--en {
font-size: 28rpx;
letter-spacing: 1rpx;
}
.action-btn:active {
background: rgba(255, 255, 255, 0.2);
}
.outline-btn {
border-color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.primary-btn {
border-color: transparent;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(244, 248, 255, 0.92) 100%);
color: #0f172a;
box-shadow: 0 14rpx 30rpx rgba(0, 0, 0, 0.18);
}
.primary-btn:active {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.88) 0%, rgba(235, 242, 252, 0.84) 100%);
}
.scroll-hint {
cursor: pointer;
}
.scroll-hint__text {
color: rgba(255, 255, 255, 0.8);
font-size: 24rpx;
letter-spacing: 4rpx;
margin-bottom: 8rpx;
}
.scroll-hint__double-icon {
display: flex;
flex-direction: column;
align-items: center;
margin-top: -4rpx;
}
.scroll-hint__icon {
color: $icon-tone-overlay;
font-size: 32rpx;
animation: bounce 2s infinite;
}
.scroll-hint__icon--second {
margin-top: -14rpx;
opacity: 0.8;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(12rpx); }
60% { transform: translateY(6rpx); }
}
</style>

View File

@@ -0,0 +1,617 @@
<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
},
i18n: {
zh: {
shareTitle: '绿居公寓 - 国际化青年社区',
all: '全部',
allAreas: '全部区域',
allTags: '全部标签',
apartmentDetail: '公寓详情',
areaStatus: '区域状态',
areaStatusDesc: '暂无符合筛选条件的区域',
areaUnavailableTitle: '该区域暂未开放',
bookViewing: '预约看房',
defaultAreaAddressPrefix: '位于',
distancePlaceholder: '附近',
emptyAreaDesc: '当前筛选条件下暂无可用房源',
emptyAreaTitle: "暂无匹配房源",
emptyBannerDesc: "暂无可展示的轮播图",
emptyBannerTitle: "暂无轮播图",
learnMore: "了解更多",
loadingAreaStatus: "区域加载中",
pricePending: "待定",
priceUnit: "元/月起",
searchApartment: "搜索区域、公寓或房型",
sortDefault: "默认排序",
sortNameAsc: "名称排序",
sortPriceAsc: "价格从低到高",
sortPriceDesc: "价格从高到低",
tagFilter: "标签筛选",
viewCommunity: "查看详情"
},
en: {
shareTitle: 'Luju Apartments - International Youth Community',
all: "All",
allAreas: "All Areas",
allTags: "All Tags",
apartmentDetail: "Apartment Detail",
areaStatus: "Area Status",
areaStatusDesc: "No areas matched the current filter.",
areaUnavailableTitle: "Area information unavailable",
bookViewing: "Book Viewing",
defaultAreaAddressPrefix: "Located at ",
distancePlaceholder: "Nearby",
emptyAreaDesc: "No rooms available under the current filters.",
emptyAreaTitle: "No matched rooms",
emptyBannerDesc: "No active banners available.",
emptyBannerTitle: "No banners",
learnMore: "Learn More",
loadingAreaStatus: "Loading areas",
pricePending: "TBD",
priceUnit: "/month",
searchApartment: "Search by area, apartment or room type",
sortDefault: "Default",
sortNameAsc: "Name",
sortPriceAsc: "Price: Low to High",
sortPriceDesc: "Price: High to Low",
tagFilter: "Tags",
viewCommunity: "View Details"
}
},
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.t('allAreas')
}
return this.currentBanner.areaLabel || this.t('allAreas')
},
currentTagLabel() {
return this.selectedTag || this.t('allTags')
},
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.loadAreaList()
}
},
async mounted() {
this.checkLoginStatus()
await Promise.all([this.loadBannerList(), this.loadAreaList()])
},
methods: {
t(key) {
return this.$tm('pages.index.component.SelectRoomTab', key)
},
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
},
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.t('areaUnavailableTitle'),
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.t('shareTitle'),
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: this.t('shareTitle')
}
}
}
</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>

File diff suppressed because it is too large Load Diff