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

460 lines
11 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>
<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>