Files
QSY-lvjuxcx/pkgExplore/room_detail/components/RoomHero.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

384 lines
7.9 KiB
Vue

<template>
<view class="room-hero">
<view v-if="showVrPanel" class="hero-vr-panel" @click="openVR">
<image v-if="vrPoster" :src="vrPoster" mode="aspectFill" class="hero-image"></image>
<view v-else class="hero-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ $tr('暂无图片') }}</view>
<view class="hero-vr-glow"></view>
<view class="vr-center-entry">
<view class="vr-center-ring vr-center-ring--outer"></view>
<view class="vr-center-ring vr-center-ring--middle"></view>
<view class="vr-center-core">
<text class="tn-icon-vr vr-center-core-icon"></text>
</view>
<view class="vr-center-text">
<view class="vr-center-title">{{ $tr('VR预览') }}</view>
<view class="vr-center-desc">{{ $tr('切换到沉浸式看房模式。') }}</view>
</view>
</view>
</view>
<swiper
v-else-if="galleryList.length"
class="hero-swiper"
circular
autoplay
:interval="4200"
:duration="700"
@change="onSwiperChange"
>
<swiper-item v-for="(image, index) in galleryList" :key="`${image}-${index}`">
<image :src="image" mode="aspectFill" class="hero-image"></image>
</swiper-item>
</swiper>
<view v-else class="hero-empty tn-flex tn-flex-col-center tn-flex-row-center">
{{ $tr('暂无图片') }}
</view>
<view class="hero-mask"></view>
<view class="hero-count" v-if="!showVrPanel && galleryList.length">
{{ currentImageIndex + 1 }} / {{ galleryList.length }}
</view>
<view v-if="hasVR" class="media-tabs">
<view
class="media-tab"
:class="{ 'media-tab--active': currentMediaType === 'image' }"
@click="switchMedia('image')"
>
<text class="tn-icon-image media-tab-icon"></text>
<text>{{ $tr('图片') }}</text>
</view>
<view
class="media-tab"
:class="{ 'media-tab--active': currentMediaType === 'vr' }"
@click="switchMedia('vr')"
>
<text class="tn-icon-vr media-tab-icon"></text>
<text>{{ $tr('VR预览') }}</text>
</view>
</view>
<view class="hero-content">
<view class="hero-title">{{ detailData.name }}</view>
<view class="hero-subtitle" v-if="showEnglishName">{{ detailData.nameEnDisplay || detailData.nameEn }}</view>
</view>
<!-- 引导下滑箭头 -->
<view class="scroll-down-hint tn-flex tn-flex-direction-column tn-flex-col-center">
<text class="hint-text">{{ $tr('上滑查看详情') }}</text>
<text class="tn-icon-up arrow-icon"></text>
</view>
</view>
</template>
<script>
export default {
props: {
detailData: {
type: Object,
default: () => ({})
},
galleryList: {
type: Array,
default: () => []
},
hasVR: {
type: Boolean,
default: false
},
showVrPanel: {
type: Boolean,
default: false
},
vrPoster: {
type: String,
default: ''
},
showEnglishName: {
type: Boolean,
default: false
},
currentMediaType: {
type: String,
default: 'image'
},
language: {
type: String,
default: 'zh'
}
},
data() {
return {
currentImageIndex: 0
}
},
methods: {
onSwiperChange(e) {
this.currentImageIndex = e.detail.current;
this.$emit('swiper-change', e);
},
openVR() {
this.$emit('open-vr');
},
switchMedia(type) {
this.$emit('switch-media', type);
}
}
}
</script>
<style scoped lang="scss">
.room-hero {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.hero-swiper,
.hero-image,
.hero-empty {
width: 100%;
height: 100%;
}
.hero-empty {
background: linear-gradient(135deg, #2b2f35 0%, #15181d 100%);
color: rgba(255, 255, 255, 0.8);
font-size: 28rpx;
}
.hero-mask {
position: absolute;
inset: 0;
background:
linear-gradient(180deg, rgba(8, 11, 18, 0.14) 0%, rgba(8, 11, 18, 0.12) 42%, rgba(8, 11, 18, 0.78) 100%),
linear-gradient(135deg, rgba($brand-primary, 0.16) 0%, rgba(0, 0, 0, 0) 46%);
pointer-events: none;
}
.hero-count {
position: absolute;
right: 28rpx;
top: calc(110rpx + env(safe-area-inset-top));
z-index: 2;
padding: 10rpx 18rpx;
border-radius: 28rpx;
background: rgba(8, 11, 18, 0.42);
backdrop-filter: blur(12rpx);
color: rgba(255, 255, 255, 0.92);
font-size: 22rpx;
}
.hero-content {
position: absolute;
left: 32rpx;
right: 32rpx;
bottom: 120rpx;
z-index: 2;
color: #ffffff;
}
.media-tabs {
position: absolute;
left: 24rpx;
right: 24rpx;
bottom: 240rpx;
z-index: 3;
display: flex;
gap: 14rpx;
padding: 10rpx;
border-radius: 26rpx;
background: rgba(8, 11, 18, 0.28);
backdrop-filter: blur(14rpx);
border: 1rpx solid rgba(255, 255, 255, 0.14);
}
.media-tab {
flex: 1;
height: 74rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.78);
transition: all 0.18s ease;
}
.media-tab-icon {
font-size: 30rpx;
line-height: 1;
}
.media-tab--active {
background: rgba(255, 255, 255, 0.92);
color: #0f172a;
font-weight: 800;
}
.hero-vr-panel {
width: 100%;
height: 100%;
position: relative;
}
.hero-vr-glow {
position: absolute;
inset: 0;
background: radial-gradient(circle at 30% 30%, rgba($brand-primary, 0.24) 0%, rgba(0, 0, 0, 0) 55%);
pointer-events: none;
}
.vr-center-entry {
position: absolute;
left: 50%;
top: 46%;
transform: translate(-50%, -50%);
z-index: 3;
width: 280rpx;
height: 280rpx;
display: flex;
align-items: center;
justify-content: center;
}
.vr-center-ring {
position: absolute;
border-radius: 50%;
border: 2rpx solid rgba(255, 255, 255, 0.18);
}
.vr-center-ring--outer {
width: 280rpx;
height: 280rpx;
animation: vrPulseOuter 2.8s ease-out infinite;
}
.vr-center-ring--middle {
width: 220rpx;
height: 220rpx;
animation: vrPulseMiddle 2.2s ease-out infinite;
}
.vr-center-core {
width: 132rpx;
height: 132rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.14);
border: 2rpx solid rgba(255, 255, 255, 0.18);
backdrop-filter: blur(18rpx);
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.24);
display: flex;
align-items: center;
justify-content: center;
}
.vr-center-core-icon {
font-size: 56rpx;
color: #ffffff;
}
.vr-center-text {
position: absolute;
left: 50%;
bottom: -54rpx;
transform: translateX(-50%);
width: 320rpx;
text-align: center;
}
.vr-center-title {
font-size: 34rpx;
font-weight: 700;
color: #ffffff;
text-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.24);
}
.vr-center-desc {
margin-top: 10rpx;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.72);
}
@keyframes vrPulseOuter {
0% {
transform: scale(0.9);
opacity: 0.2;
}
70% {
transform: scale(1.08);
opacity: 0.65;
}
100% {
transform: scale(1.16);
opacity: 0;
}
}
@keyframes vrPulseMiddle {
0% {
transform: scale(0.92);
opacity: 0.18;
}
70% {
transform: scale(1.04);
opacity: 0.5;
}
100% {
transform: scale(1.1);
opacity: 0;
}
}
.hero-title {
font-size: 54rpx;
font-weight: 700;
line-height: 1.16;
text-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
}
.hero-subtitle {
margin-top: 12rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.72);
letter-spacing: 1rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.scroll-down-hint {
position: absolute;
bottom: 40rpx;
left: 0;
right: 0;
z-index: 3;
color: rgba(255, 255, 255, 0.8);
animation: bounce 2s infinite;
}
.hint-text {
font-size: 22rpx;
margin-bottom: 8rpx;
}
.arrow-icon {
font-size: 32rpx;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-16rpx);
}
60% {
transform: translateY(-8rpx);
}
}
</style>