Files
QSY-lvjuxcx/pages/index/component/SelectRoomHero.vue
liangjian 6e721ccc7e refactor: 国际化文本迁移至全局 locales/messages.js
将组件级 i18n 对象重构为统一由全局 locales/messages.js 管理,确保中英文语言切换时正确显示对应文本。

- 新增 utils/i18n.js 国际化工具函数
- 更新 store/$t.mixin.js 翻译混入逻辑
- 迁移 pkgService、pkgBooking、pkgAccount、pkgExplore、pages 等共 58 个组件的中英文文本至全局消息表
2026-07-24 13:43:41 +08:00

457 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')">{{ $tr('公寓详情') }}</view>
<view class="action-btn primary-btn" :class="{ 'action-btn--en': language === 'en' }" @click="$emit('submit')">{{ $tr('预约看房') }}</view>
</view>
<view class="scroll-hint tn-flex tn-flex-direction-column tn-flex-col-center" @click="$emit('collapse')">
<text class="scroll-hint__text">{{ $tr('了解更多') }}</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')
},
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>