Files
QSY-lvjuxcx/pages/index/component/SelectRoomAreaPanel.vue
2026-06-29 09:42:53 +08:00

829 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<view class="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>