chore: initial import
This commit is contained in:
179
pkgExplore/room_detail/components/RoomAssets.vue
Normal file
179
pkgExplore/room_detail/components/RoomAssets.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<view v-if="assetList.length" class="section-panel">
|
||||
<view class="section-header tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="title-wrap tn-flex tn-flex-col-center">
|
||||
<view class="title-accent"></view>
|
||||
<text class="section-title">{{ t('sectionTitle') }}</text>
|
||||
</view>
|
||||
<text class="section-subtitle">{{ t('sectionSubtitle', { count: assetList.length }) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="asset-grid">
|
||||
<view
|
||||
v-for="item in assetList"
|
||||
:key="item.key"
|
||||
class="asset-item tn-flex tn-flex-col-center"
|
||||
>
|
||||
<view class="asset-icon-box tn-flex tn-flex-row-center tn-flex-col-center">
|
||||
<text
|
||||
v-if="item.iconClass"
|
||||
:class="[item.iconClass, 'asset-remix-icon', 'asset-icon-font']"
|
||||
></text>
|
||||
<image
|
||||
v-else-if="item.icon"
|
||||
:src="item.icon"
|
||||
mode="aspectFit"
|
||||
class="asset-icon-image"
|
||||
></image>
|
||||
<text v-else class="tn-icon-shop asset-icon-font"></text>
|
||||
|
||||
<!-- 数量角标:仅大于1时显示 -->
|
||||
<view v-if="item.defaultQuantity > 1" class="asset-qty-badge">
|
||||
x{{ item.defaultQuantity }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="asset-name tn-text-ellipsis">{{ item.assetName }}</text>
|
||||
<text v-if="item.brand" class="asset-brand tn-text-ellipsis">{{ item.brand }}</text>
|
||||
<text v-else class="asset-brand-placeholder"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
assetList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params = {}) {
|
||||
return this.$tm('pkgExplore.room_detail.room_detail', key, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.section-panel {
|
||||
background: #ffffff;
|
||||
padding: 48rpx 32rpx;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.title-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title-accent {
|
||||
width: 0rpx;
|
||||
height: 0rpx;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
position: relative;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #1D2129;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: #86909C;
|
||||
background: #F2F3F5;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.asset-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 40rpx 16rpx;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.asset-icon-box {
|
||||
position: relative;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.05) 0%, rgba($brand-primary, 0.12) 100%);
|
||||
margin-bottom: 16rpx;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.asset-icon-box:active {
|
||||
transform: scale(0.95);
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.08) 0%, rgba($brand-primary, 0.16) 100%);
|
||||
}
|
||||
|
||||
.asset-icon-image {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.asset-icon-font {
|
||||
font-size: 44rpx;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.asset-remix-icon {
|
||||
font-family: 'remixicon' !important;
|
||||
}
|
||||
|
||||
.asset-qty-badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -14rpx;
|
||||
background: #FF5722;
|
||||
color: #FFFFFF;
|
||||
font-size: 20rpx;
|
||||
font-weight: bold;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 4rpx solid #FFFFFF;
|
||||
line-height: 1.2;
|
||||
font-family: 'DIN Alternate', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
z-index: 2;
|
||||
box-shadow: 0 4rpx 8rpx rgba(255, 87, 34, 0.2);
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1D2129;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.asset-brand {
|
||||
font-size: 22rpx;
|
||||
color: #86909C;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.asset-brand-placeholder {
|
||||
height: 30rpx; /* Placeholder to keep grid aligned if some have brand and some don't */
|
||||
}
|
||||
</style>
|
||||
386
pkgExplore/room_detail/components/RoomHero.vue
Normal file
386
pkgExplore/room_detail/components/RoomHero.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<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">{{ t('noImage') }}</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">{{ t('vrPreview') }}</view>
|
||||
<view class="vr-center-desc">{{ t('vrPreviewDesc') }}</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">
|
||||
{{ t('noImage') }}
|
||||
</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>{{ t('imageTab') }}</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>{{ t('vrPreview') }}</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">{{ t('swipeUpHint') }}</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: {
|
||||
t(key) {
|
||||
return this.$tm('pkgExplore.room_detail.room_detail', key)
|
||||
},
|
||||
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>
|
||||
174
pkgExplore/room_detail/components/RoomInfo.vue
Normal file
174
pkgExplore/room_detail/components/RoomInfo.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<view class="room-info-wrapper">
|
||||
<view class="section-panel" v-if="basicInfoRows.length">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ t('basicInfo') }}</text>
|
||||
</view>
|
||||
<view class="info-list">
|
||||
<view v-for="item in basicInfoRows" :key="item.label" class="info-row tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<text class="info-label">{{ item.label }}</text>
|
||||
<text class="info-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<view class="section-panel" v-if="displayDescriptions.length">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ isRoomType ? t('roomTypeDesc') : t('roomDesc') }}</text>
|
||||
</view>
|
||||
<view class="desc-wrap" :class="{ 'desc-wrap--collapsed': isDescCollapsed }">
|
||||
<view
|
||||
v-for="(item, index) in displayDescriptions"
|
||||
:key="`${item}-${index}`"
|
||||
class="desc-text"
|
||||
>
|
||||
<rich-text :nodes="item"></rich-text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="shouldShowDescToggle"
|
||||
class="expand-btn tn-flex tn-flex-row-center tn-flex-col-center"
|
||||
@click="toggleDesc"
|
||||
>
|
||||
<text>{{ isDescCollapsed ? t('expandAll') : t('collapse') }}</text>
|
||||
<text :class="isDescCollapsed ? 'tn-icon-down' : 'tn-icon-up'" class="expand-icon"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
basicInfoRows: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
displayDescriptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isRoomType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
shouldShowDescToggle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDescCollapsed: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgExplore.room_detail.room_detail', key)
|
||||
},
|
||||
toggleDesc() {
|
||||
this.isDescCollapsed = !this.isDescCollapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.room-info-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.section-panel {
|
||||
background: #ffffff;
|
||||
padding: 48rpx 32rpx;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
position: relative;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #1D2129;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
gap: 24rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 160rpx;
|
||||
font-size: 26rpx;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: #1D2129;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.desc-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
transition: max-height 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
max-height: 5000rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desc-wrap--collapsed {
|
||||
max-height: 180rpx;
|
||||
}
|
||||
|
||||
.desc-wrap--collapsed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.desc-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
color: #4E5969;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
margin-top: 22rpx;
|
||||
color: $brand-primary;
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
margin-left: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
101
pkgExplore/room_detail/components/RoomRules.vue
Normal file
101
pkgExplore/room_detail/components/RoomRules.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<view class="section-panel" v-if="bookingRuleRows.length">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ isRoomType ? t('bookingRules') : t('bookingInfo') }}</text>
|
||||
</view>
|
||||
<view class="rule-list">
|
||||
<view v-for="item in bookingRuleRows" :key="item.label" class="rule-item tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="rule-left tn-flex tn-flex-col-center">
|
||||
<text class="tn-icon-success-circle-fill rule-icon"></text>
|
||||
<text class="rule-label">{{ item.label }}</text>
|
||||
</view>
|
||||
<text class="rule-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
bookingRuleRows: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isRoomType: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgExplore.room_detail.room_detail', key)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.section-panel {
|
||||
background: #ffffff;
|
||||
padding: 48rpx 32rpx;
|
||||
margin: 0;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
position: relative;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #1D2129;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.rule-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.rule-item {
|
||||
min-height: 80rpx;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-bottom: 1rpx dashed #E5E6EB;
|
||||
}
|
||||
|
||||
.rule-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rule-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rule-icon {
|
||||
margin-right: 16rpx;
|
||||
font-size: 28rpx;
|
||||
color: #1D2129;
|
||||
}
|
||||
|
||||
.rule-label {
|
||||
font-size: 26rpx;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
.rule-value {
|
||||
margin-left: 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #1D2129;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
297
pkgExplore/room_detail/components/RoomSummary.vue
Normal file
297
pkgExplore/room_detail/components/RoomSummary.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<view class="summary-card">
|
||||
<!-- 价格与标签区 -->
|
||||
<view class="price-header tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="price-block tn-flex tn-flex-col-bottom">
|
||||
<template v-if="hasPrice">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ detailData.priceValue }}</text>
|
||||
<text class="price-unit">{{ t('priceUnit') }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="price-empty">{{ t('pricePending') }}</text>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 支付方式作为高亮小签 -->
|
||||
<view class="pay-method-tag" v-if="detailData.payMethod">
|
||||
<text class="tn-icon-trusty-fill tag-icon"></text>
|
||||
<text>{{ displayPayMethod }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标签列表区 -->
|
||||
<view class="tags-row tn-flex tn-flex-wrap" v-if="summaryBadges && summaryBadges.length">
|
||||
<view v-for="(item, index) in summaryBadges" :key="index" class="badge-item">
|
||||
{{ item }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 地址导航区 -->
|
||||
<view class="address-banner" v-if="displayAddress" @click="handleAddressClick">
|
||||
<view class="address-bg-decoration">
|
||||
<text class="tn-icon-map decoration-icon"></text>
|
||||
</view>
|
||||
<view class="address-content tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="address-left tn-flex tn-flex-col-center">
|
||||
<view class="icon-wrap tn-flex tn-flex-row-center tn-flex-col-center">
|
||||
<text class="tn-icon-location-fill location-icon"></text>
|
||||
</view>
|
||||
<view class="address-info">
|
||||
<view class="address-text tn-text-ellipsis">{{ shortAddress }}</view>
|
||||
<view class="address-sub tn-flex tn-flex-col-center">
|
||||
<text class="address-sub-text">{{ t('addressHint') }}</text>
|
||||
<text class="tn-icon-right address-sub-icon"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="address-right">
|
||||
<view class="nav-btn tn-flex tn-flex-row-center tn-flex-col-center">
|
||||
<text class="tn-icon-navigation-fill"></text>
|
||||
<text class="nav-text">{{ t('navigate') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { localizePayMethodText } from '../../../utils/select-room-normalizers'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
detailData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
hasPrice: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
summaryBadges: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayPayMethod() {
|
||||
return localizePayMethodText(this.detailData.payMethod, this.language)
|
||||
},
|
||||
displayAddress() {
|
||||
if (this.language === 'en') {
|
||||
return this.detailData.addressEnDisplay || this.detailData.addressEn || this.detailData.address || ''
|
||||
}
|
||||
return this.detailData.address || this.detailData.addressEn || ''
|
||||
},
|
||||
shortAddress() {
|
||||
const addr = this.displayAddress;
|
||||
if (!addr) {
|
||||
return this.t('noAddress')
|
||||
}
|
||||
if (this.language === 'en') {
|
||||
return addr
|
||||
}
|
||||
const short = addr.replace(/^(?:.*?省|.*?自治区)?(?:.*?市|.*?自治州)?/, '');
|
||||
return short.trim() ? short : (addr || this.t('noAddress'));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgExplore.room_detail.room_detail', key)
|
||||
},
|
||||
handleAddressClick() {
|
||||
this.$emit('open-map', this.detailData)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.summary-card {
|
||||
background: #ffffff;
|
||||
padding: 48rpx 32rpx 32rpx;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
}
|
||||
|
||||
/* 价格区域 */
|
||||
.price-header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.price-block {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
color: #1D2129;
|
||||
margin-right: 6rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 72rpx;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
color: #1D2129;
|
||||
font-family: 'DIN Alternate', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
letter-spacing: -2rpx;
|
||||
}
|
||||
|
||||
.price-unit {
|
||||
font-size: 24rpx;
|
||||
color: #86909C;
|
||||
margin-left: 8rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.price-empty {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
.pay-method-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, rgba($brand-primary, 0.1) 0%, rgba($brand-primary, 0.2) 100%);
|
||||
color: $brand-primary;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 100rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
|
||||
.tag-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签区 */
|
||||
.tags-row {
|
||||
gap: 16rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.badge-item {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #F2F3F5;
|
||||
color: #4E5969;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 地址导航区 */
|
||||
.address-banner {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, #F0F6FF 0%, #F8FAFF 100%);
|
||||
border-radius: 20rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
border: 1rpx solid rgba($brand-primary, 0.15);
|
||||
box-shadow: 0 8rpx 20rpx rgba($brand-primary, 0.05);
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background: linear-gradient(135deg, #E8F2FF 0%, #F0F6FF 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.address-bg-decoration {
|
||||
position: absolute;
|
||||
right: -20rpx;
|
||||
top: -20rpx;
|
||||
opacity: 0.04;
|
||||
pointer-events: none;
|
||||
|
||||
.decoration-icon {
|
||||
font-size: 160rpx;
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.address-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.address-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 6rpx 16rpx rgba($brand-primary, 0.12);
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.location-icon {
|
||||
font-size: 36rpx;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.address-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.address-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.address-sub {
|
||||
font-size: 22rpx;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.address-sub-text {
|
||||
margin-right: 4rpx;
|
||||
}
|
||||
|
||||
.address-sub-icon {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.address-right {
|
||||
flex-shrink: 0;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
height: 60rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 100rpx;
|
||||
background: #ffffff;
|
||||
color: $brand-primary;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 4rpx 12rpx rgba($brand-primary, 0.08);
|
||||
border: 1rpx solid rgba($brand-primary, 0.2);
|
||||
|
||||
text.tn-icon-navigation-fill {
|
||||
margin-right: 6rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1493
pkgExplore/room_detail/room_detail.vue
Normal file
1493
pkgExplore/room_detail/room_detail.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user