chore: initial import
This commit is contained in:
1080
pages/index/component/MessageCenterTab.vue
Normal file
1080
pages/index/component/MessageCenterTab.vue
Normal file
File diff suppressed because it is too large
Load Diff
1421
pages/index/component/MyApplicationTab.vue
Normal file
1421
pages/index/component/MyApplicationTab.vue
Normal file
File diff suppressed because it is too large
Load Diff
1031
pages/index/component/ProfileCenterTab.vue
Normal file
1031
pages/index/component/ProfileCenterTab.vue
Normal file
File diff suppressed because it is too large
Load Diff
828
pages/index/component/SelectRoomAreaPanel.vue
Normal file
828
pages/index/component/SelectRoomAreaPanel.vue
Normal 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>
|
||||
459
pages/index/component/SelectRoomHero.vue
Normal file
459
pages/index/component/SelectRoomHero.vue
Normal 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>
|
||||
617
pages/index/component/SelectRoomTab.vue
Normal file
617
pages/index/component/SelectRoomTab.vue
Normal 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>
|
||||
2227
pages/index/component/StayServiceTab.vue
Normal file
2227
pages/index/component/StayServiceTab.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user