chore: initial import
This commit is contained in:
292
pkgExplore/download/download.vue
Normal file
292
pkgExplore/download/download.vue
Normal file
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<view class="template-list">
|
||||
<!-- 顶部自定义导航 -->
|
||||
<tn-nav-bar fixed customBack :bottomShadow="false" backgroundColor="#FFFFFF">
|
||||
<view slot="back" class='tn-custom-nav-bar__back'
|
||||
@click="goBack">
|
||||
<text class='icon tn-icon-left'></text>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-col-center tn-flex-row-center ">
|
||||
<text class="tn-text-bold tn-text-lg tn-color-black">{{ t('navTitle') }}</text>
|
||||
</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="tn-search-fixed">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin" :style="{paddingTop: vuex_custom_bar_height + 'px'}">
|
||||
|
||||
<view class="justify-content-item align-content-item" style="width: 100vw;">
|
||||
<view class="tn-flex tn-flex-col-center" style="border: 1rpx solid #D2D2D2;border-radius: 100rpx;padding: 10rpx 20rpx 10rpx 20rpx;width: 90%;">
|
||||
<text class="tn-icon-search justify-content-item tn-padding-right-xs tn-color-gray tn-text-lg"></text>
|
||||
<input class="justify-content-item" :placeholder="t('searchPlaceholder')" name="input" placeholder-style="color:#AAAAAA" style="width: 90%;"></input>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="align-content-item">
|
||||
<view class="justify-content-item tn-text-center">
|
||||
<!-- 没输入前-->
|
||||
<tn-button backgroundColor="#3C97FF80" shape="round" padding="20rpx 20rpx" width="150rpx" @tap="">
|
||||
<text class="tn-color-white">{{ t('searchButton') }}</text>
|
||||
</tn-button>
|
||||
<!-- 输入后 -->
|
||||
<!-- <tn-button backgroundColor="#3C97FF" shape="round" padding="20rpx 20rpx" width="150rpx" @tap="">
|
||||
<text class="tn-color-white">搜 索</text>
|
||||
</tn-button> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tn-safe-area-inset-bottom" :style="{paddingTop: vuex_custom_bar_height + 60 + 'px'}">
|
||||
|
||||
<!-- 搜索为空 -->
|
||||
<!-- <view class="" style="padding: 5vh 20rpx;opacity: 0.8;">
|
||||
<view class="tn-text-center" style="font-size: 260rpx;padding-top: 30rpx;">
|
||||
<text class="tn-icon-search-list tn-color-gray--disabled"></text>
|
||||
</view>
|
||||
<view class="tn-text-center" style="color: #979797;">搜索内容为空</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 当前页面内容 -->
|
||||
<view class="tn-bg-white tn-padding-bottom-xs">
|
||||
<view class="list-bg tn-flex tn-flex-row-left tn-flex-col-center" v-for="(item,index) in list" :key="index" @click="tn('/homePages/details')">
|
||||
<view class="">
|
||||
<view class="type-pic">
|
||||
<view class="type-image" :style="'background-image:url(' + item.mainImage + ');background-size: cover;'">
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tn-padding-left clamp-text-2">
|
||||
<text class="">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import template_page_mixin from '@/libs/mixin/template_page_mixin.js'
|
||||
export default {
|
||||
name: 'List',
|
||||
mixins: [template_page_mixin],
|
||||
i18n: {
|
||||
zh: {
|
||||
navTitle: '资料图库',
|
||||
searchPlaceholder: '请输入搜索关键词',
|
||||
searchButton: '搜 索',
|
||||
galleryImage: '游戏攻略图',
|
||||
galleryImageLong: '游戏攻略图游戏攻略图游戏攻略图'
|
||||
},
|
||||
en: {
|
||||
navTitle: 'Gallery',
|
||||
searchPlaceholder: 'Enter search keywords',
|
||||
searchButton: 'Search',
|
||||
galleryImage: 'Guide Image',
|
||||
galleryImageLong: 'Guide Image Guide Image Guide Image'
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
list: [
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828216161-assets/web-upload/d6ecc4c6-a675-4e30-9681-45b105578b55.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828359865-assets/web-upload/9e232257-0b68-4169-a997-e23195b2e0aa.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828485984-assets/web-upload/dd7b313d-e16c-45f3-ad5a-8bb27c2913b8.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828486008-assets/web-upload/bbea1e86-0db3-4f39-b240-abafa8769d8b.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828617900-assets/web-upload/8a6c98e4-ed47-4308-92bc-84b224d31e0c.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828745355-assets/web-upload/7de060fc-d1fc-4890-b6c0-6982fb19b972.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828818144-assets/web-upload/7a3f1f98-149a-46f2-84cf-31aaa5b84b2e.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724828933196-assets/web-upload/35cda0e5-c495-4ebe-9ed9-3bc1ec3d1645.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724829158713-assets/web-upload/1c3b07ff-d64c-4fe3-8626-275044886400.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724829158748-assets/web-upload/2d83bd1a-1451-44a1-8115-55cadd636b5e.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724829158754-assets/web-upload/9e74ec62-2ae8-46d2-9199-b8fccb62b8a0.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724829158764-assets/web-upload/069cccaa-2439-4cb2-8b96-e53e8b442145.jpeg',
|
||||
},
|
||||
{
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1724830125897-assets/web-upload/e7e50a8b-202f-4cc5-9089-05d4b28dd2c8.jpeg',
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.syncList()
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
language() {
|
||||
this.syncList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgExplore.download.download', key, params)
|
||||
},
|
||||
syncList() {
|
||||
this.list = this.list.map((item, index) => ({
|
||||
...item,
|
||||
desc: index === 1 ? this.t('galleryImageLong') : this.t('galleryImage')
|
||||
}))
|
||||
},
|
||||
// 跳转
|
||||
tn(e) {
|
||||
uni.navigateTo({
|
||||
url: e,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 胶囊*/
|
||||
.tn-custom-nav-bar__back {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
color: #000000;
|
||||
font-size: 16px;
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
padding-left: 10rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.tn-search-fixed{
|
||||
position: fixed;
|
||||
top: 0rpx;
|
||||
width: 100%;
|
||||
transition: all 0.25s ease-out;
|
||||
z-index: 1;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.list-bg{
|
||||
margin: 30rpx;
|
||||
padding-top: 30rpx;
|
||||
}
|
||||
|
||||
.list-bg + .list-bg{
|
||||
border-top: 1rpx solid #F1F1F1;
|
||||
}
|
||||
|
||||
/* 图标容器15 start */
|
||||
.icon15 {
|
||||
&__item {
|
||||
width: 30%;
|
||||
background-color: #FFFFFF;
|
||||
padding: 30rpx;
|
||||
margin: 20rpx 10rpx;
|
||||
transform: scale(1);
|
||||
transition: transform 0.3s linear;
|
||||
transform-origin: center center;
|
||||
|
||||
&--icon {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
font-size: 60rpx;
|
||||
border-radius: 18rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
border-radius: inherit;
|
||||
opacity: 1;
|
||||
transform: scale(1, 1);
|
||||
background-size: 100% 100%;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 文件头像 start */
|
||||
.type-image {
|
||||
width: 102rpx;
|
||||
height: 120rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.type-pic {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
// background-attachment:fixed;
|
||||
background-position: center;
|
||||
// border: 1rpx solid rgba(255,255,255,0.05);
|
||||
// box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.15);
|
||||
border-radius: 18rpx;
|
||||
overflow: hidden;
|
||||
// background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 文字截取*/
|
||||
.clamp-text-1 {
|
||||
-webkit-line-clamp: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clamp-text-2 {
|
||||
-webkit-line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
333
pkgExplore/filter/filter.vue
Normal file
333
pkgExplore/filter/filter.vue
Normal file
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<view class="template-filter">
|
||||
<!-- 顶部自定义导航 -->
|
||||
<tn-nav-bar fixed customBack :bottomShadow="false" backgroundColor="#FFFFFF">
|
||||
<view slot="back" class='tn-custom-nav-bar__back'
|
||||
@click="goBack">
|
||||
<text class='icon tn-icon-left'></text>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-col-center tn-flex-row-center ">
|
||||
<text class="tn-text-bold tn-text-xl tn-color-black">{{ t('navTitle') }}</text>
|
||||
</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="top-fixed" :style="{paddingTop: vuex_custom_bar_height + 10 + 'px'}">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-left-sm tn-padding-right-sm">
|
||||
<view class="tn-flex tn-flex-col-center tn-padding-sm">
|
||||
<view class="tn-padding-right-xs">{{ t('sortDefault') }}</view>
|
||||
<!-- <view class="ri-bookmark-line tn-color-gray--dark"></view> -->
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-col-center tn-padding-sm" @tap="openTypeModal">
|
||||
<view class="tn-padding-right-xs">{{ t('typeLabel') }}</view>
|
||||
<view class="tn-icon-down-fill icon-active" v-if="showTypeModal"></view>
|
||||
<view class="tn-icon-down-fill tn-color-gray--dark" v-else></view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-col-center tn-padding-sm">
|
||||
<view class="tn-padding-right-xs">{{ t('salesLabel') }}</view>
|
||||
<view class="tn-icon-sort tn-color-gray--dark"></view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-col-center tn-padding-sm">
|
||||
<view class="tn-padding-right-xs">{{ t('priceLabel') }}</view>
|
||||
<view class="tn-icon-sort tn-color-gray--dark"></view>
|
||||
</view>
|
||||
<view class="tn-flex tn-flex-col-center tn-padding-sm" @tap="openSelectModal">
|
||||
<view class="tn-padding-right-xs">{{ t('comprehensiveLabel') }}</view>
|
||||
<view class="tn-icon-filter tn-color-gray--dark tn-text-sm"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="" :style="{paddingTop: vuex_custom_bar_height + 60 + 'px'}">
|
||||
|
||||
<!-- 商品 start-->
|
||||
<view class="tn-flex tn-flex-wrap" style="margin: 10rpx 14rpx 0 14rpx;">
|
||||
<block v-for="(item, index) in product" :key="index">
|
||||
<view class="" style="width: 50%;" @click="tn('/shopPages/product')">
|
||||
<view class="shop-shadow" style="margin: 0 16rpx 30rpx 16rpx;">
|
||||
<view class="image-pic" :style="'background-image:url(' + item.mainImage + ')'">
|
||||
<view class="image-product">
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tn-text-justify tn-padding-top-sm tn-padding-bottom-xs clamp-text-1 tn-text-bold" style="color: #333333;max-height: 60rpx;">
|
||||
<text class="tn-padding-left-sm tn-padding-right-sm">{{ item.desc }}</text>
|
||||
</view>
|
||||
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding-bottom-sm tn-padding-top-sm">
|
||||
<view class="justify-content-item tn-flex tn-flex-col-center tn-padding-left-sm tn-padding-right-sm" style="margin-left: -6rpx;color: #333333;">
|
||||
<text class="tn-text-sm">¥</text>
|
||||
<text class="tn-padding-right-sm tn-text-lg tn-text-bold">{{ item.price }}</text>
|
||||
<text class="tn-color-gray tn-text-sm" style="color: #C1CACF;"> {{ item.number }} {{ t('purchasedSuffix') }}</text>
|
||||
</view>
|
||||
<!-- <view class="justify-content-item tn-flex tn-flex-col-center">
|
||||
<tn-button size="sm" shape="icon" backgroundColor="#3e486400" fontColor="#333333" padding="10rpx 0 0 0">
|
||||
<text class="tn-icon-add-circle tn-text-lg"></text>
|
||||
</tn-button>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
<!-- 商品 end-->
|
||||
|
||||
<view class="tn-tabbar-height"></view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- popup会有延迟,采用这种方式来优化弹窗的优化体验-->
|
||||
<type-modal v-model="showTypeModal"></type-modal>
|
||||
|
||||
<!-- popup会有延迟,采用这种方式来优化弹窗的优化体验-->
|
||||
<select-modal v-model="showSelectModal"></select-modal>
|
||||
|
||||
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TypeModal from '@/components/filter/type.vue'
|
||||
import SelectModal from '@/components/filter/select.vue'
|
||||
import template_page_mixin from '@/libs/mixin/template_page_mixin.js'
|
||||
export default {
|
||||
components: { TypeModal , SelectModal },
|
||||
name: 'TemplateFilter',
|
||||
mixins: [template_page_mixin],
|
||||
i18n: {
|
||||
zh: {
|
||||
navTitle: '应用专区',
|
||||
sortDefault: '默认',
|
||||
typeLabel: '类型',
|
||||
salesLabel: '销量',
|
||||
priceLabel: '价格',
|
||||
comprehensiveLabel: '综合',
|
||||
purchasedSuffix: '人已购',
|
||||
product1: '蛋仔派对',
|
||||
product2: '保卫萝卜',
|
||||
product3: '旅行青蛙',
|
||||
product4: '辐射避难所',
|
||||
product5: '王国保卫战',
|
||||
product6: '崩坏 · 星穹铁道',
|
||||
product7: '纪念碑谷'
|
||||
},
|
||||
en: {
|
||||
navTitle: 'App Zone',
|
||||
sortDefault: 'Default',
|
||||
typeLabel: 'Type',
|
||||
salesLabel: 'Sales',
|
||||
priceLabel: 'Price',
|
||||
comprehensiveLabel: 'All',
|
||||
purchasedSuffix: 'purchased',
|
||||
product1: 'Eggy Party',
|
||||
product2: 'Carrot Defense',
|
||||
product3: 'Travel Frog',
|
||||
product4: 'Fallout Shelter',
|
||||
product5: 'Kingdom Rush',
|
||||
product6: 'Honkai: Star Rail',
|
||||
product7: 'Monument Valley'
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
/* 选择弹窗*/
|
||||
showTypeModal: false,
|
||||
|
||||
/* 选择弹窗*/
|
||||
showSelectModal: true,
|
||||
|
||||
product: [
|
||||
{
|
||||
price:'129',
|
||||
number:'231',
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1716013944651-assets/web-upload/32eafb0d-1e25-452b-9ad9-d14c18ec90f6.jpeg',
|
||||
},
|
||||
{
|
||||
price:'288',
|
||||
number:'157',
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/png/280373/1715960090731-assets/web-upload/156084cd-aa5a-41dc-98ca-eb681d4afcc5.png',
|
||||
},
|
||||
{
|
||||
price:'299',
|
||||
number:'876',
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1716014232876-assets/web-upload/5e1d7c68-048e-4b54-afda-68164a5bebd0.jpeg',
|
||||
},
|
||||
{
|
||||
price:'199',
|
||||
number:'29',
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1716025506668-assets/web-upload/b6d123a2-4f1d-46c9-9474-f62a87d05a74.jpeg',
|
||||
},
|
||||
{
|
||||
price:'389',
|
||||
number:'643',
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1716025891640-assets/web-upload/8b4440ce-710b-4d3c-afff-5d49e6c7a16a.jpeg',
|
||||
},
|
||||
{
|
||||
price:'298',
|
||||
number:'386',
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1716040399286-assets/web-upload/7a715327-f597-4a8e-ac3c-2ba7ab45b6b9.jpeg',
|
||||
},
|
||||
{
|
||||
price:'368',
|
||||
number:'439',
|
||||
desc: '',
|
||||
mainImage: 'https://cdn.nlark.com/yuque/0/2024/jpeg/280373/1716014593433-assets/web-upload/6e1ba4f3-4757-4693-b827-b8803072c5c9.jpeg',
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.syncProducts()
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
language() {
|
||||
this.syncProducts()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgExplore.filter.filter', key, params)
|
||||
},
|
||||
syncProducts() {
|
||||
const keys = ['product1', 'product2', 'product3', 'product4', 'product5', 'product6', 'product7']
|
||||
this.product = this.product.map((item, index) => ({
|
||||
...item,
|
||||
desc: this.t(keys[index] || 'product1')
|
||||
}))
|
||||
},
|
||||
// 打开选择弹框
|
||||
openTypeModal() {
|
||||
if (typeof uni !== 'undefined' && uni.vibrateShort) {
|
||||
uni.vibrateShort({ fail: () => {} });
|
||||
}
|
||||
this.showTypeModal = true
|
||||
},
|
||||
// 打开选择弹框
|
||||
openSelectModal() {
|
||||
if (typeof uni !== 'undefined' && uni.vibrateShort) {
|
||||
uni.vibrateShort({ fail: () => {} });
|
||||
}
|
||||
this.showSelectModal = true
|
||||
},
|
||||
// 跳转
|
||||
tn(e) {
|
||||
uni.navigateTo({
|
||||
url: e,
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 胶囊*/
|
||||
.tn-custom-nav-bar__back {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
// background-color: rgba(0, 0, 0, 0.15);
|
||||
// border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||
color: #000000;
|
||||
font-size: 18px;
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.top-fixed{
|
||||
position: fixed;
|
||||
background-color: rgba(255,255,255,1);
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.icon-active{
|
||||
animation:acitve 0.2s forwards 1;
|
||||
}
|
||||
|
||||
@keyframes acitve{
|
||||
from{
|
||||
|
||||
}
|
||||
to{
|
||||
-webkit-transform: rotate(-180deg);
|
||||
-ms-transform: rotate(-180deg);
|
||||
transform: rotate(-180deg);
|
||||
color: #000000;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义导航栏内容 end */
|
||||
.bg-radius{
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 16rpx;
|
||||
margin: 30rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.shop-shadow{
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.image-product{
|
||||
padding: 145rpx 0rpx;
|
||||
font-size: 16rpx;
|
||||
font-weight: 300;
|
||||
position: relative;
|
||||
}
|
||||
.image-pic{
|
||||
background-size: cover;
|
||||
background-repeat:no-repeat;
|
||||
// background-attachment:fixed;
|
||||
background-position:center;
|
||||
border-radius: 24rpx;
|
||||
border: 10rpx solid #FFFFFF;;
|
||||
}
|
||||
|
||||
/* 底部安全边距 start*/
|
||||
.tn-tabbar-height {
|
||||
min-height: 100rpx;
|
||||
height: calc(120rpx + env(safe-area-inset-bottom) / 2);
|
||||
height: calc(120rpx + constant(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 文字截取*/
|
||||
.clamp-text-1 {
|
||||
-webkit-line-clamp: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clamp-text-2 {
|
||||
-webkit-line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
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
532
pkgExplore/sign/sign.vue
Normal file
532
pkgExplore/sign/sign.vue
Normal file
@@ -0,0 +1,532 @@
|
||||
<template>
|
||||
<view class="template-spread">
|
||||
<!-- 顶部自定义导航 -->
|
||||
<tn-nav-bar fixed customBack :bottomShadow="false" backgroundColor="#FFFFFF00">
|
||||
<view slot="back" class='tn-custom-nav-bar__back'
|
||||
@click="goBack">
|
||||
<text class='icon tn-icon-left'></text>
|
||||
</view>
|
||||
<!-- <view class="tn-flex tn-flex-col-center tn-flex-row-center ">
|
||||
<text class="tn-text-xl tn-color-black">签到</text>
|
||||
</view> -->
|
||||
</tn-nav-bar>
|
||||
|
||||
<!-- 顶部渐变底色,需要的显示出来即可 -->
|
||||
<view class="top-bg">
|
||||
<view class="bg-pic" style="position: absolute;right: 40rpx;bottom: 120rpx;">
|
||||
<view class="bg-image" style="background-image:url('https://cdn.nlark.com/yuque/0/2024/png/280373/1719968962717-assets/web-upload/b6758518-c11e-400f-9d59-3874e2a74a12.png');background-size: cover;">
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="" :style="{paddingTop: vuex_custom_bar_height + 10 + 'px'}">
|
||||
|
||||
<view class="" style="position: relative;">
|
||||
|
||||
<view class="bg-notice">
|
||||
<tn-notice-bar :list="noticeList" mode="vertical" leftIconName="starry"></tn-notice-bar>
|
||||
</view>
|
||||
<view class="tn-margin tn-padding-top-sm" style="color: #001E50;font-size: 50rpx;">
|
||||
<text class="tn-text-bold">62,888</text>
|
||||
<text class="tn-padding-left-xs tn-text-xs tn-color-gray">{{ t('pointsUnit') }}</text>
|
||||
</view>
|
||||
<view class="tn-margin tn-text-sm" style="color: #001E50;">
|
||||
{{ t('pointsRecord') }} <text class="tn-icon-right tn-padding-left-xs tn-color-gray"></text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<!-- 组件对应可选项容器 -->
|
||||
<view class="" style="position: relative;">
|
||||
|
||||
<view class="tn-margin tn-padding-sm tn-bg-white" style="border-radius: 20rpx;">
|
||||
<view class="" style="border-bottom: 4rpx solid #ECF0F9;position: relative;top: 80rpx;width: 80%;margin: 0 auto;">
|
||||
</view>
|
||||
<!-- 方式12 start-->
|
||||
<view class="tn-flex tn-flex-wrap tn-flex-row-center tn-padding-top-lg tn-padding-bottom">
|
||||
<view v-for="(item, index) in 3" :key="index" style="width: 14%;">
|
||||
<view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
|
||||
<!-- icon -->
|
||||
<view class="icon12__item--icon tn-flex tn-flex-row-center tn-flex-col-center" style="border: 1rpx solid #E5BC81;color: #E5BC81;background-color: #F7EFE6;">
|
||||
<view class="tn-icon-check"></view>
|
||||
</view>
|
||||
<view class="tn-text-center tn-padding-bottom-sm" style="color: #95A3A8;">
|
||||
<text class="tn-text-ellipsis tn-text-sm">08/{{index + 13}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="(item, index) in 3" :key="index" style="width: 14%;">
|
||||
<view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
|
||||
<!-- 数字-->
|
||||
<view class="icon12__item--icon tn-flex tn-flex-row-center tn-flex-col-center">
|
||||
<view class="">+30</view>
|
||||
</view>
|
||||
<view class="tn-text-center tn-padding-bottom-sm" style="color: #95A3A8;">
|
||||
<text class="tn-text-ellipsis tn-text-sm">08/{{index + 16}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="(item, index) in 1" :key="index" style="width: 14%;">
|
||||
<view class="tn-flex tn-flex-direction-column tn-flex-row-center tn-flex-col-center">
|
||||
<!-- 图片形式 -->
|
||||
<view class="icon12__item--icon tn-flex tn-flex-row-center tn-flex-col-center" style="background-image:url('https://cdn.nlark.com/yuque/0/2024/png/280373/1719937310080-assets/web-upload/f979073b-e335-4e18-8bf2-757756236a7d.png');background-size: cover;">
|
||||
</view>
|
||||
<view class="tn-text-center tn-padding-bottom-sm" style="color: #95A3A8;">
|
||||
<text class="tn-text-ellipsis tn-text-sm">08/{{index + 19}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 方式12 end-->
|
||||
|
||||
<!-- 未签到-->
|
||||
<view class="tn-flex-1 justify-content-item tn-margin-xs tn-text-center">
|
||||
<tn-button shape="round" backgroundColor="$brand-primary" fontColor="#FFFFFF" padding="40rpx 0" width="100%" :fontSize="30" @tap="showLandscape">
|
||||
<text class="">{{ t('checkInNow') }}</text>
|
||||
</tn-button>
|
||||
</view>
|
||||
|
||||
<!-- 已签到-->
|
||||
<!-- <view class="tn-flex-1 justify-content-item tn-margin-xs tn-text-center">
|
||||
<tn-button shape="round" backgroundColor="#DFE4E8" fontColor="#95A3A8" padding="40rpx 0" width="100%" :fontSize="30" @click="tn('')">
|
||||
<text class="">今日已签到</text>
|
||||
</tn-button>
|
||||
</view> -->
|
||||
|
||||
<view class="tn-strip-bottom-min tn-padding" style="color: #95A3A8;">
|
||||
{{ t('checkInRewardHint') }}
|
||||
</view>
|
||||
|
||||
<view class="tn-padding-left tn-pright tn-padding-top tn-padding-bottom tn-text-sm" style="color: #95A3A8;">
|
||||
{{ t('warmTip') }}<br>
|
||||
{{ t('warmTipDesc') }}
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
<view class="tn-padding-top-sm">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-margin" @click="tn('')">
|
||||
<view class="justify-content-item tn-text-bold tn-text-lg tn-padding-left-xs">
|
||||
{{ t('taskSectionTitle') }}
|
||||
</view>
|
||||
<view class="justify-content-item tn-text-df" style="color: #C1CACF;">
|
||||
<text class="tn-padding-xs">{{ t('more') }}</text>
|
||||
<text class="tn-icon-right"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bg-shadow" style="background-color: #FFFFFF;border-radius: 24rpx;margin: 0 30rpx 30rpx 30rpx;" v-for="(item, index) in taskList" :key="index">
|
||||
<view class="tn-flex tn-flex-row-between tn-flex-col-center tn-padding" style="color: #333333;">
|
||||
<view class="" style="max-width: 50vw;">
|
||||
<view class="tn-text-bold">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<view class="tn-color-gray tn-text-sm tn-padding-top-xs">
|
||||
{{ item.desc }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="justify-content-item">
|
||||
<tn-button shape="round" size="sm" backgroundColor="$brand-primary" fontColor="#FFFFFF" padding="30rpx 0" width="160rpx">
|
||||
<text class="tn-text-bold tn-text-sm">{{ t('goComplete') }}</text>
|
||||
<text class="tn-icon-right-fill tn-padding-left-xs tn-text-lg"></text>
|
||||
</tn-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<view class="tn-tabbar-height"></view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 压屏窗-->
|
||||
<tn-landscape :show="show" @close="closeLandscape" closePosition="bottom">
|
||||
<view class="tn-text-center" style="padding-bottom: 10vh;">
|
||||
<view class="" style="z-index: 0;height: 420rpx;margin: 0 0 20rpx 0;">
|
||||
<image src="https://cdn.nlark.com/yuque/0/2024/png/280373/1719936825450-assets/web-upload/12a47786-b172-4b93-8e2a-eaec578e71de.png" mode="widthFix"></image>
|
||||
</view>
|
||||
|
||||
<view class="" style="background-color: #FFFFFF;border-radius: 20rpx;width: 70vw;margin: 0 auto;margin-top: -220rpx;position: relative;z-index: 999;">
|
||||
<view class="" style="position: absolute;top: -160rpx;left: 120rpx;">
|
||||
<image src="https://cdn.nlark.com/yuque/0/2024/png/280373/1719936460577-assets/web-upload/697ed6f1-6126-4734-bd80-bed28296e16d.png" mode="aspectFit" style="width: 320rpx;height: 330rpx;"></image>
|
||||
</view>
|
||||
<view class="tn-text-xl tn-text-bold" style="padding-top: 120rpx;">
|
||||
{{ t('checkInSuccess') }}
|
||||
</view>
|
||||
<view class="tn-padding tn-color-gray--dark">
|
||||
<text>{{ t('checkedInFor') }}</text>
|
||||
<text class="tn-color-orangered" style="padding-left: 6rpx;padding-right: 6rpx">3</text>
|
||||
<text>{{ t('daysAndEarned') }}</text>
|
||||
<text class="tn-color-orangered" style="padding-left: 6rpx;padding-right: 6rpx">20</text>
|
||||
<text>{{ t('pointsUnit') }}</text>
|
||||
</view>
|
||||
<view class=" tn-padding tn-color-gray">
|
||||
<text>{{ t('continuousCheckIn') }}</text>
|
||||
<text class="tn-color-orangered" style="padding-left: 6rpx;padding-right: 6rpx;">7</text>
|
||||
<text>{{ t('daysRewardHint') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</tn-landscape>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import template_page_mixin from '@/libs/mixin/template_page_mixin.js'
|
||||
export default {
|
||||
name: 'Sign',
|
||||
mixins: [template_page_mixin],
|
||||
i18n: {
|
||||
zh: {
|
||||
pointsUnit: '积分',
|
||||
pointsRecord: '积分记录',
|
||||
checkInNow: '立即签到',
|
||||
checkInRewardHint: '坚持每天连续签到可以获得多重奖励哦',
|
||||
warmTip: '温馨提示:',
|
||||
warmTipDesc: '每日签到可获得10积分奖励,连续完成7天签到可能获得额外奖励,连续签到满7天后将会重置签到记录。',
|
||||
taskSectionTitle: '做任务领金币',
|
||||
more: '更多',
|
||||
goComplete: '去完成',
|
||||
checkInSuccess: '签到成功',
|
||||
checkedInFor: '已连续签到',
|
||||
daysAndEarned: '天,获得',
|
||||
continuousCheckIn: '连续签到',
|
||||
daysRewardHint: '天可获得额外奖励哦!',
|
||||
notice1: '恭喜 付*其 连续7天签到,获得 300积分 奖励',
|
||||
notice2: '恭喜 徐*圆 连续7天签到,获得 300积分 奖励',
|
||||
notice3: '恭喜 蔡*东 连续7天签到,获得 300积分 奖励',
|
||||
taskName: '任务名称',
|
||||
taskDesc: '抓住那只猪吖'
|
||||
},
|
||||
en: {
|
||||
pointsUnit: 'Points',
|
||||
pointsRecord: 'Points History',
|
||||
checkInNow: 'Check In Now',
|
||||
checkInRewardHint: 'Daily continuous check-ins unlock multiple rewards',
|
||||
warmTip: 'Tips:',
|
||||
warmTipDesc: 'Daily check-in grants 10 points. Completing 7 consecutive days may unlock extra rewards, and the record resets after day 7.',
|
||||
taskSectionTitle: 'Complete Tasks for Coins',
|
||||
more: 'More',
|
||||
goComplete: 'Go',
|
||||
checkInSuccess: 'Check-in Successful',
|
||||
checkedInFor: 'Checked in for',
|
||||
daysAndEarned: 'days and earned',
|
||||
continuousCheckIn: 'Continuous check-in',
|
||||
daysRewardHint: 'days unlocks extra rewards!',
|
||||
notice1: 'Congrats to Fu*qi for 7 consecutive check-ins and earning 300 points',
|
||||
notice2: 'Congrats to Xu*yuan for 7 consecutive check-ins and earning 300 points',
|
||||
notice3: 'Congrats to Cai*dong for 7 consecutive check-ins and earning 300 points',
|
||||
taskName: 'Task Name',
|
||||
taskDesc: 'Catch That Pig'
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
show: true,
|
||||
maskCloseable: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
noticeList() {
|
||||
return [this.t('notice1'), this.t('notice2'), this.t('notice3')]
|
||||
},
|
||||
taskList() {
|
||||
return Array.from({ length: 6 }).map(() => ({
|
||||
title: this.t('taskName'),
|
||||
desc: this.t('taskDesc')
|
||||
}))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgExplore.sign.sign', key, params)
|
||||
},
|
||||
// 跳转
|
||||
tn(e) {
|
||||
uni.navigateTo({
|
||||
url: e,
|
||||
});
|
||||
},
|
||||
|
||||
// 弹出压屏窗
|
||||
showLandscape() {
|
||||
this.openLandscape()
|
||||
},
|
||||
// 打开压屏窗
|
||||
openLandscape() {
|
||||
this.show = true
|
||||
},
|
||||
// 关闭压屏窗
|
||||
closeLandscape() {
|
||||
this.show = false
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 胶囊*/
|
||||
.tn-custom-nav-bar__back {
|
||||
width: 38%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 1000rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||
color: #FFFFFF;
|
||||
font-size: 18px;
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
flex: 1;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
.template-spread{
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.icon12 {
|
||||
&__item {
|
||||
width: 30%;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 10rpx;
|
||||
padding: 30rpx;
|
||||
margin: 20rpx 10rpx;
|
||||
transform: scale(1);
|
||||
transition: transform 0.3s linear;
|
||||
transform-origin: center center;
|
||||
|
||||
&--icon {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
font-size: 30rpx;
|
||||
margin-bottom: 18rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #6A767D;
|
||||
border-radius: 100rpx;
|
||||
background-color: #ECF0F9;
|
||||
|
||||
&::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 20rpx;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
opacity: 1;
|
||||
transform: scale(1, 1);
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-bg{
|
||||
position: absolute;
|
||||
// background: linear-gradient(90deg, #d8feda, #F6F6F6);
|
||||
background: linear-gradient(120deg, #FEFDD3, #FAFCEF, #E5FDF5, #FEFBEA);
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 580rpx;
|
||||
transition: all 0.25s ease-out;
|
||||
z-index: 0;
|
||||
}
|
||||
.top-bg:before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 0;
|
||||
mask-image: linear-gradient(to bottom, transparent, black);
|
||||
background: linear-gradient(90deg, #F6F7F9, #F6F7F9);
|
||||
}
|
||||
|
||||
.bg-notice{
|
||||
color: #FFFFFF;
|
||||
background-color: #001E5020;
|
||||
padding: 4rpx;
|
||||
margin: 0 30rpx 30rpx 30rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
/* 日历 */
|
||||
.bg-image {
|
||||
width: 240rpx;
|
||||
height: 192rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-pic {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
.tag-border-cyan {
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
margin: 0 15rpx 10rpx 0;
|
||||
border-radius: 10rpx;
|
||||
background-color: #2DE8BD08;
|
||||
color: #2DE8BD;
|
||||
border: 1rpx solid #2DE8BD;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
.tag-border-yellow {
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
margin: 0 15rpx 10rpx 0;
|
||||
border-radius: 10rpx;
|
||||
background-color: #FFD10008;
|
||||
color: #FFD100;
|
||||
border: 1rpx solid #FFD100;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
.tag-border-orange {
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
margin: 0 15rpx 10rpx 0;
|
||||
border-radius: 10rpx;
|
||||
background-color: #FEA50008;
|
||||
color: #FEA500;
|
||||
border: 1rpx solid #FEA500;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
.tag-border-green {
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
margin: 0 15rpx 10rpx 0;
|
||||
border-radius: 10rpx;
|
||||
background-color: #1FC77308;
|
||||
color: #1FC773;
|
||||
border: 1rpx solid #1FC773;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
.tag-bg {
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
margin: 0 15rpx 10rpx 0;
|
||||
border-radius: 10rpx;
|
||||
background-color: #E6E9EE;
|
||||
color: #001E50;
|
||||
border: 1rpx solid #001E5080;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
|
||||
/* 品牌 */
|
||||
.logo-image {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 100rpx;
|
||||
border: 1rpx solid #DFE4E8;
|
||||
}
|
||||
|
||||
.logo-image-yes {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 100rpx;
|
||||
border: 1rpx solid #1FC773;
|
||||
}
|
||||
|
||||
.logo-image-no {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 100rpx;
|
||||
border: 1rpx solid #FF0033;
|
||||
}
|
||||
|
||||
.logo-pic {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
// box-shadow: 0rpx 0rpx 80rpx 0rpx rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
// background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 商品 */
|
||||
.image-product {
|
||||
border-radius: 8rpx;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
position: relative;
|
||||
border: 1rpx solid #DFE4E8;
|
||||
}
|
||||
|
||||
.image-pic{
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background-size: cover;
|
||||
background-repeat:no-repeat;
|
||||
background-position:center;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
/* 间隔线 start*/
|
||||
.tn-strip-bottom-min {
|
||||
width: 100%;
|
||||
border-bottom: 1rpx solid #F8F9FB;
|
||||
}
|
||||
|
||||
.tn-strip-bottom {
|
||||
width: 100%;
|
||||
border-bottom: 20rpx solid rgba(241, 241, 241, 0.8);
|
||||
}
|
||||
/* 间隔线 end*/
|
||||
|
||||
/* 文字截取*/
|
||||
.clamp-text-1 {
|
||||
-webkit-line-clamp: 1;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clamp-text-2 {
|
||||
-webkit-line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 底部安全边距 start*/
|
||||
.tn-tabbar-height {
|
||||
min-height: 40rpx;
|
||||
height: calc(60rpx + env(safe-area-inset-bottom) / 2);
|
||||
height: calc(60rpx + constant(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
</style>
|
||||
75
pkgExplore/store_detail/components/FacilityList.vue
Normal file
75
pkgExplore/store_detail/components/FacilityList.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<view class="section-panel" v-if="facilityList && facilityList.length">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ t('sectionTitle') }}</text>
|
||||
</view>
|
||||
<view class="facility-grid tn-flex tn-flex-wrap">
|
||||
<view class="facility-item tn-flex tn-flex-direction-column tn-flex-col-center" v-for="(item, index) in facilityList" :key="index">
|
||||
<text :class="item.icon" class="facility-icon"></text>
|
||||
<text class="facility-name">{{ item.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
facilityList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgExplore.store_detail.store_detail', key)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.section-panel {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
||||
margin: 24rpx 32rpx 0;
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.facility-grid {
|
||||
gap: 32rpx 0;
|
||||
}
|
||||
|
||||
.facility-item {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.facility-icon {
|
||||
font-size: 52rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.facility-name {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
</style>
|
||||
239
pkgExplore/store_detail/components/RoomList.vue
Normal file
239
pkgExplore/store_detail/components/RoomList.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<view class="section-panel room-section">
|
||||
<view class="section-header tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<text class="section-title">{{ t('roomSectionTitle', { count: roomTypes.length }) }}</text>
|
||||
</view>
|
||||
<view class="room-list-vertical">
|
||||
<view
|
||||
class="room-card-v"
|
||||
v-for="room in roomTypes"
|
||||
:key="room.code"
|
||||
@click="goToRoomDetail(room)"
|
||||
>
|
||||
<view class="room-img-v">
|
||||
<image class="img" :src="room.images && room.images[0] ? room.images[0] : ''" mode="aspectFill"></image>
|
||||
<view class="pay-method-tag" v-if="room.payMethod">{{ getPayMethodText(room.payMethod) }}</view>
|
||||
</view>
|
||||
<view class="room-info-v">
|
||||
<view class="room-header-v tn-flex tn-flex-row-between tn-flex-col-center">
|
||||
<view class="room-name-v tn-text-ellipsis">{{ getRoomName(room) }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="room.tags && room.tags.length" class="room-tags-v tn-flex tn-flex-wrap">
|
||||
<view class="tag-v" v-for="(tag, index) in room.tags.slice(0, 3)" :key="index">{{ getDisplayTag(tag) }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="getRoomDescription(room)" class="room-desc-v tn-text-ellipsis">{{ getRoomDescription(room) }}</view>
|
||||
|
||||
<view class="room-bottom-v tn-flex tn-flex-row-between tn-flex-col-bottom">
|
||||
<view class="room-price-v">
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-val">{{ formatPriceValue(room.price) || t('pricePending') }}</text>
|
||||
<text class="price-unit">{{ t('priceUnit') }}</text>
|
||||
</view>
|
||||
<view class="go-btn-v tn-flex tn-flex-col-center tn-flex-row-center">
|
||||
<text>{{ t('viewDetails') }}</text>
|
||||
<text class="tn-icon-right"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { localizeHousingTag, localizePayMethodText } from '../../../utils/select-room-normalizers'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
roomTypes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params = {}) {
|
||||
return this.$tm('pkgExplore.store_detail.store_detail', key, params)
|
||||
},
|
||||
formatPriceValue(priceText) {
|
||||
if (!priceText) {
|
||||
return ''
|
||||
}
|
||||
const matched = String(priceText).match(/(\d+(\.\d+)?)/)
|
||||
return matched ? matched[1] : ''
|
||||
},
|
||||
getPayMethodText(payMethod = '') {
|
||||
return localizePayMethodText(payMethod, this.language)
|
||||
},
|
||||
getDisplayTag(tag = '') {
|
||||
return localizeHousingTag(tag, this.language)
|
||||
},
|
||||
getRoomName(room = {}) {
|
||||
if (this.language === 'en') {
|
||||
return room.nameEnDisplay || room.nameEn || room.name || ''
|
||||
}
|
||||
return room.name || room.nameEn || ''
|
||||
},
|
||||
getRoomDescription(room = {}) {
|
||||
if (this.language === 'en') {
|
||||
return room.descriptionEnDisplay || room.descriptionEn || room.description || ''
|
||||
}
|
||||
return room.description || room.descriptionEn || ''
|
||||
},
|
||||
goToRoomDetail(room) {
|
||||
this.$emit('go-detail', room);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.section-panel {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
||||
margin: 24rpx 32rpx 0;
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.room-list-vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.room-card-v {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.room-img-v {
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.pay-method-tag {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
left: 12rpx;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8rpx);
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.room-info-v {
|
||||
flex: 1;
|
||||
padding: 16rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.room-header-v {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-name-v {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.room-tags-v {
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.tag-v {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.room-desc-v {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: auto; /* 将底部价格推下去 */
|
||||
}
|
||||
|
||||
.room-bottom-v {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.room-price-v {
|
||||
color: #ffb8b8;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 20rpx;
|
||||
font-weight: bold;
|
||||
margin-right: 2rpx;
|
||||
}
|
||||
|
||||
.price-val {
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.price-unit {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-left: 4rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.go-btn-v {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 30rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.go-btn-v .tn-icon-right {
|
||||
font-size: 20rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
</style>
|
||||
199
pkgExplore/store_detail/components/StoreInfo.vue
Normal file
199
pkgExplore/store_detail/components/StoreInfo.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<view class="store-info-panel">
|
||||
<!-- 核心信息卡片 -->
|
||||
<view class="info-card tn-flex tn-flex-row-between tn-flex-col-center" @click="openMap">
|
||||
<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>
|
||||
<text class="address-title tn-text-ellipsis">{{ shortAddress }}</text>
|
||||
</view>
|
||||
<view class="address-right tn-flex tn-flex-col-center">
|
||||
<text class="nav-text">{{ t('navigate') }}</text>
|
||||
<text class="tn-icon-right nav-arrow"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 小区简介 -->
|
||||
<view class="section-panel" v-if="displayDescription">
|
||||
<view class="section-header">
|
||||
<text class="section-title">{{ t('storeIntro') }}</text>
|
||||
</view>
|
||||
<view class="store-desc" :class="{'store-desc--collapsed': isDescCollapsed}">
|
||||
{{ displayDescription }}
|
||||
</view>
|
||||
<view 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: {
|
||||
storeData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'zh'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDescCollapsed: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayDescription() {
|
||||
if (this.language === 'en') {
|
||||
return this.storeData.descriptionEnDisplay || this.storeData.descriptionEn || this.storeData.description || ''
|
||||
}
|
||||
return this.storeData.description || this.storeData.descriptionEn || ''
|
||||
},
|
||||
shortAddress() {
|
||||
const addr = this.language === 'en'
|
||||
? (this.storeData.addressEnDisplay || this.storeData.addressEn || this.storeData.address || '')
|
||||
: (this.storeData.address || this.storeData.addressEn || '')
|
||||
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.store_detail.store_detail', key)
|
||||
},
|
||||
toggleDesc() {
|
||||
this.isDescCollapsed = !this.isDescCollapsed;
|
||||
},
|
||||
openMap() {
|
||||
this.$emit('open-map');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.store-info-panel {
|
||||
padding: 0 32rpx;
|
||||
margin-top: 24rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.address-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 16rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.location-icon {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.address-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address-right {
|
||||
flex-shrink: 0;
|
||||
padding-left: 20rpx;
|
||||
border-left: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.section-panel {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
|
||||
margin-top: 24rpx;
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.store-desc {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.store-desc--collapsed {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
margin-left: 8rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
613
pkgExplore/store_detail/store_detail.vue
Normal file
613
pkgExplore/store_detail/store_detail.vue
Normal file
@@ -0,0 +1,613 @@
|
||||
<template>
|
||||
<view class="store-detail-page">
|
||||
<!-- 底层:全屏轮播背景 -->
|
||||
<view class="fullscreen-bg">
|
||||
<swiper
|
||||
v-if="storeGalleryList.length"
|
||||
class="bg-swiper"
|
||||
circular
|
||||
autoplay
|
||||
:interval="5000"
|
||||
:duration="1000"
|
||||
@change="onHeroSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="(image, index) in storeGalleryList" :key="`${image}-${index}`">
|
||||
<image class="bg-image" :src="image" mode="aspectFill"></image>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view v-else class="bg-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ t('noImage') }}</view>
|
||||
<!-- 渐变暗化遮罩,确保上层文字卡片清晰 -->
|
||||
<view class="bg-mask"></view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部导航栏 -->
|
||||
<tn-nav-bar
|
||||
:isBack="true"
|
||||
:bottomShadow="false"
|
||||
backgroundColor="transparent"
|
||||
:customBackStyle="navBarBackStyle"
|
||||
backIconColor="#FFFFFF"
|
||||
:fixed="true"
|
||||
class="transparent-nav-bar"
|
||||
>
|
||||
<view class="nav-title" :style="{ opacity: navTitleOpacity }">{{ displayStoreName }}</view>
|
||||
</tn-nav-bar>
|
||||
|
||||
<!-- 上层:透明可滚动内容区 -->
|
||||
<scroll-view
|
||||
class="content-scroll"
|
||||
scroll-y
|
||||
:show-scrollbar="false"
|
||||
@scroll="handleScroll"
|
||||
enable-back-to-top
|
||||
>
|
||||
<view class="scroll-inner">
|
||||
<!-- 留出足够的顶部空间,让背景图展示出来 -->
|
||||
<view class="hero-space tn-flex tn-flex-direction-column tn-flex-col-bottom">
|
||||
<!-- 照片计数器移到店名上方,避免与导航栏重叠 -->
|
||||
<view v-if="storeGalleryList.length > 1" class="hero-counter tn-flex tn-flex-col-center">
|
||||
<text class="tn-icon-image counter-icon"></text>
|
||||
<text>{{ currentImageIndex + 1 }} / {{ storeGalleryList.length }}</text>
|
||||
</view>
|
||||
|
||||
<view class="store-title-wrap">
|
||||
<view class="store-title">{{ displayStoreName }}</view>
|
||||
<view v-if="storeTags.length" class="store-tags tn-flex tn-flex-wrap">
|
||||
<view class="tag-item" v-for="(tag, index) in storeTags" :key="index">{{ tag }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 悬浮毛玻璃信息流 -->
|
||||
<view class="floating-content-wrap">
|
||||
<store-info
|
||||
:storeData="storeData"
|
||||
:language="language"
|
||||
@open-map="openMap"
|
||||
/>
|
||||
|
||||
<facility-list
|
||||
:facilityList="facilityList"
|
||||
:language="language"
|
||||
/>
|
||||
|
||||
<room-list
|
||||
v-if="roomTypes.length"
|
||||
:roomTypes="roomTypes"
|
||||
:language="language"
|
||||
@go-detail="goToRoomDetail"
|
||||
/>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 悬浮操作栏 (固定在底部,透明风格) -->
|
||||
<view class="action-bar-wrap tn-safe-area-inset-bottom">
|
||||
<view class="action-bar">
|
||||
<button class="action-btn action-btn--glass" open-type="share">
|
||||
<text class="tn-icon-share action-btn__icon"></text>
|
||||
<text>{{ t('share') }}</text>
|
||||
</button>
|
||||
<button class="action-btn action-btn--primary-glass" @click="handleCall">
|
||||
<text class="tn-icon-phone action-btn__icon"></text>
|
||||
<text>{{ contactButtonText }}</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StoreInfo from './components/StoreInfo.vue';
|
||||
import FacilityList from './components/FacilityList.vue';
|
||||
import RoomList from './components/RoomList.vue';
|
||||
import { facilityManagers } from '@/api/index.js'
|
||||
import { localizeFacilityName, localizeHousingTag } from '../../utils/select-room-normalizers'
|
||||
|
||||
function parseCoordinate(...values) {
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
const value = values[i]
|
||||
if (value === '' || value === null || typeof value === 'undefined') {
|
||||
continue
|
||||
}
|
||||
const parsed = Number(value)
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StoreInfo,
|
||||
FacilityList,
|
||||
RoomList
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
storeData: {},
|
||||
roomTypes: [],
|
||||
areaManagers: [],
|
||||
areaManager: null,
|
||||
isAreaManagerLoading: false,
|
||||
scrollTop: 0,
|
||||
currentImageIndex: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.$getLanguage()
|
||||
},
|
||||
storeGalleryList() {
|
||||
if (Array.isArray(this.storeData.images) && this.storeData.images.length) {
|
||||
return this.storeData.images.filter(Boolean)
|
||||
}
|
||||
if (this.heroImage) {
|
||||
return [this.heroImage]
|
||||
}
|
||||
return []
|
||||
},
|
||||
heroImage() {
|
||||
if (this.storeData.images && this.storeData.images[0]) {
|
||||
return this.storeData.images[0]
|
||||
}
|
||||
if (this.roomTypes.length > 0 && this.roomTypes[0].images && this.roomTypes[0].images[0]) {
|
||||
return this.roomTypes[0].images[0]
|
||||
}
|
||||
return ''
|
||||
},
|
||||
storeTags() {
|
||||
const tags = Array.isArray(this.storeData.tags) ? this.storeData.tags.filter(Boolean) : []
|
||||
return tags.map((tag) => localizeHousingTag(tag, this.language))
|
||||
},
|
||||
displayStoreName() {
|
||||
if (this.language === 'en') {
|
||||
return this.storeData.nameEnDisplay || this.storeData.nameEn || this.storeData.name || this.t('apartmentName')
|
||||
}
|
||||
return this.storeData.name || this.storeData.nameEn || this.t('apartmentName')
|
||||
},
|
||||
displayStoreAddress() {
|
||||
if (this.language === 'en') {
|
||||
return this.storeData.addressEnDisplay || this.storeData.addressEn || this.storeData.address || ''
|
||||
}
|
||||
return this.storeData.address || this.storeData.addressEn || ''
|
||||
},
|
||||
facilityList() {
|
||||
const rawList = this.storeData.facilities
|
||||
if (!Array.isArray(rawList)) {
|
||||
return []
|
||||
}
|
||||
return rawList
|
||||
.map((item) => {
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
if (typeof item === 'string') {
|
||||
return { name: item, icon: 'tn-icon-shop' }
|
||||
}
|
||||
return {
|
||||
name: localizeFacilityName(item.name || item.label || '', this.language),
|
||||
icon: item.icon || 'tn-icon-shop'
|
||||
}
|
||||
})
|
||||
.filter((item) => item && item.name)
|
||||
},
|
||||
phoneNumber() {
|
||||
return this.storeData.phone || this.storeData.tel || this.storeData.mobile || ''
|
||||
},
|
||||
areaManagerName() {
|
||||
const source = this.areaManager || {}
|
||||
return source.displayName || source.name || source.userName || source.nickname || ''
|
||||
},
|
||||
areaManagerOptions() {
|
||||
if (!Array.isArray(this.areaManagers)) {
|
||||
return []
|
||||
}
|
||||
return this.areaManagers.filter((item) => item && item.phone)
|
||||
},
|
||||
contactPhoneNumber() {
|
||||
const source = this.areaManager || {}
|
||||
return source.phone || source.mobile || source.tel || source.managerPhone || this.phoneNumber || ''
|
||||
},
|
||||
contactButtonText() {
|
||||
if (this.isAreaManagerLoading) {
|
||||
return this.t('loading')
|
||||
}
|
||||
if (this.areaManagerOptions.length > 1) {
|
||||
return this.t('selectContact')
|
||||
}
|
||||
if (this.areaManagerName) {
|
||||
return this.t('contactManager')
|
||||
}
|
||||
return this.t('phoneConsult')
|
||||
},
|
||||
navBarBackStyle() {
|
||||
const isDarkBg = this.scrollTop < 100;
|
||||
return {
|
||||
transition: 'all 0.3s',
|
||||
background: isDarkBg ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.15)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
borderRadius: '50%',
|
||||
width: '64rpx',
|
||||
height: '64rpx',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '12rpx',
|
||||
border: '1rpx solid rgba(255, 255, 255, 0.25)'
|
||||
};
|
||||
},
|
||||
navTitleOpacity() {
|
||||
return Math.min(Math.max((this.scrollTop - 80) / 100, 0), 1);
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options.storeData) {
|
||||
Promise.resolve(options.storeData)
|
||||
.then(decodeURIComponent)
|
||||
.then(JSON.parse)
|
||||
.then((data) => {
|
||||
this.applyStoreData(data)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(this.t('parseStoreDataFailed'), e);
|
||||
})
|
||||
return
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: this.displayStoreName || this.t('shareTitle'),
|
||||
path: `/pkgExplore/store_detail/store_detail?storeData=${encodeURIComponent(JSON.stringify(this.storeData || {}))}`,
|
||||
imageUrl: this.heroImage || ''
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: this.displayStoreName || this.t('shareTitle'),
|
||||
query: `storeData=${encodeURIComponent(JSON.stringify(this.storeData || {}))}`,
|
||||
imageUrl: this.heroImage || ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key) {
|
||||
return this.$tm('pkgExplore.store_detail.store_detail', key)
|
||||
},
|
||||
applyStoreData(data) {
|
||||
this.storeData = data && typeof data === 'object' ? data : {}
|
||||
this.roomTypes = this.storeData.roomTypes || []
|
||||
this.loadAreaManager()
|
||||
},
|
||||
loadAreaManager() {
|
||||
const areaCode = this.storeData.code || this.storeData.areaCode || ''
|
||||
if (!areaCode) {
|
||||
this.areaManagers = []
|
||||
this.areaManager = null
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
this.isAreaManagerLoading = true
|
||||
return facilityManagers({
|
||||
FacilityType: 10,
|
||||
FacilityCode: areaCode
|
||||
}, this.language)
|
||||
.then((res) => {
|
||||
const rawData = res && res.data && typeof res.data === 'object' ? res.data : res
|
||||
const managerList = Array.isArray(rawData)
|
||||
? rawData
|
||||
: (Array.isArray(rawData && rawData.data)
|
||||
? rawData.data
|
||||
: (Array.isArray(rawData && rawData.managers) ? rawData.managers : []))
|
||||
const normalizedManagers = managerList
|
||||
.map((item) => this.normalizeManager(item))
|
||||
.filter(Boolean)
|
||||
this.areaManagers = normalizedManagers
|
||||
this.areaManager = normalizedManagers.find((item) => item.phone) || normalizedManagers[0] || null
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('查询区域宿管失败', error)
|
||||
this.areaManagers = []
|
||||
this.areaManager = null
|
||||
})
|
||||
.finally(() => {
|
||||
this.isAreaManagerLoading = false
|
||||
})
|
||||
},
|
||||
normalizeManager(manager) {
|
||||
if (!manager || typeof manager !== 'object') {
|
||||
return null
|
||||
}
|
||||
const phoneSource = manager.phone || manager.mobile || manager.tel || manager.managerPhone || ''
|
||||
const phone = typeof phoneSource === 'string' ? phoneSource.trim() : phoneSource
|
||||
const userCode = manager.userCode || manager.code || ''
|
||||
const name = manager.name || manager.userName || manager.nickname || userCode || ''
|
||||
return {
|
||||
...manager,
|
||||
phone,
|
||||
userCode,
|
||||
displayName: name
|
||||
}
|
||||
},
|
||||
getManagerActionText(manager) {
|
||||
if (!manager) {
|
||||
return this.t('phoneConsult')
|
||||
}
|
||||
if (manager.displayName && manager.phone) {
|
||||
return `${manager.displayName}(${manager.phone})`
|
||||
}
|
||||
return manager.displayName || manager.phone || this.t('phoneConsult')
|
||||
},
|
||||
makePhoneCall(phoneNumber) {
|
||||
if (!phoneNumber) {
|
||||
uni.showToast({ title: this.t('noServicePhone'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.makePhoneCall({
|
||||
phoneNumber
|
||||
});
|
||||
},
|
||||
onHeroSwiperChange(e) {
|
||||
this.currentImageIndex = e.detail.current;
|
||||
},
|
||||
handleScroll(e) {
|
||||
this.scrollTop = e.detail.scrollTop;
|
||||
},
|
||||
openMap() {
|
||||
const latitude = parseCoordinate(this.storeData.latitude, this.storeData.lat)
|
||||
const longitude = parseCoordinate(this.storeData.longitude, this.storeData.lng)
|
||||
if (latitude === null || longitude === null) {
|
||||
uni.showToast({
|
||||
title: this.t('noLocation'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.openLocation({
|
||||
latitude,
|
||||
longitude,
|
||||
name: this.displayStoreName || this.t('apartmentName'),
|
||||
address: this.displayStoreAddress || '',
|
||||
scale: 18
|
||||
});
|
||||
},
|
||||
goToRoomDetail(room) {
|
||||
uni.navigateTo({
|
||||
url: `/pkgExplore/room_detail/room_detail?roomTypeData=${encodeURIComponent(JSON.stringify(room))}&areaData=${encodeURIComponent(JSON.stringify({
|
||||
code: this.storeData.code,
|
||||
name: this.storeData.name,
|
||||
nameEn: this.storeData.nameEn || this.storeData.name,
|
||||
nameEnDisplay: this.storeData.nameEnDisplay || this.storeData.nameEn || this.storeData.name,
|
||||
address: this.storeData.address,
|
||||
addressEn: this.storeData.addressEn || this.storeData.address,
|
||||
addressEnDisplay: this.storeData.addressEnDisplay || this.storeData.addressEn || this.storeData.address,
|
||||
latitude: this.storeData.latitude || this.storeData.lat || '',
|
||||
longitude: this.storeData.longitude || this.storeData.lng || ''
|
||||
}))}`
|
||||
});
|
||||
},
|
||||
handleCall() {
|
||||
if (this.isAreaManagerLoading && !this.contactPhoneNumber) {
|
||||
uni.showToast({ title: this.t('loadingManagerInfo'), icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const managerOptions = this.areaManagerOptions
|
||||
if (managerOptions.length > 1) {
|
||||
uni.showActionSheet({
|
||||
itemList: managerOptions.map((item) => this.getManagerActionText(item)),
|
||||
success: ({ tapIndex }) => {
|
||||
const selectedManager = managerOptions[tapIndex]
|
||||
if (selectedManager && selectedManager.phone) {
|
||||
this.makePhoneCall(selectedManager.phone)
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.makePhoneCall(this.contactPhoneNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.store-detail-page {
|
||||
height: 100vh;
|
||||
background-color: #000; /* 全屏黑色底色,防止滑动露白 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
transition: opacity 0.3s;
|
||||
max-width: 420rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transparent-nav-bar ::v-deep .tn-nav-bar {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 底层背景 */
|
||||
.fullscreen-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bg-swiper, .bg-image, .bg-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bg-empty {
|
||||
background: #111;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.bg-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.4) 60%, rgba(0,0,0,0.85) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 上层滚动内容区 */
|
||||
.content-scroll {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-scroll ::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.scroll-inner {
|
||||
min-height: 101%;
|
||||
}
|
||||
|
||||
/* 留出背景展示区 */
|
||||
.hero-space {
|
||||
width: 100%;
|
||||
height: 35vh; /* 进一步降低留白高度,确保底部房型列表至少透出一条数据 */
|
||||
padding: 0 32rpx 40rpx;
|
||||
justify-content: flex-end; /* 内容沉底 */
|
||||
}
|
||||
|
||||
.hero-counter {
|
||||
height: 52rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(14px);
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: 24rpx;
|
||||
align-self: flex-end; /* 靠右对齐 */
|
||||
}
|
||||
|
||||
.counter-icon {
|
||||
margin-right: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.store-title-wrap {
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.store-title {
|
||||
font-size: 64rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 24rpx;
|
||||
text-shadow: 0 4rpx 16rpx rgba(0,0,0,0.6);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.store-tags {
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* 悬浮毛玻璃信息流 */
|
||||
.floating-content-wrap {
|
||||
position: relative;
|
||||
padding-bottom: 180rpx;
|
||||
}
|
||||
|
||||
/* 底部操作栏 - 毛玻璃悬浮 */
|
||||
.action-bar-wrap {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 18rpx 32rpx calc(18rpx + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0) 100%);
|
||||
z-index: 100;
|
||||
pointer-events: none; /* 让渐变层不阻挡点击 */
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
pointer-events: auto; /* 恢复按钮区域点击 */
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
padding: 0;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
line-height: 1;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
button.action-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn__icon {
|
||||
margin-right: 12rpx;
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
.action-btn--glass {
|
||||
flex: 0 0 200rpx;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-btn--primary-glass {
|
||||
flex: 1;
|
||||
background: rgba($brand-primary, 0.85);
|
||||
color: #fff;
|
||||
border-color: rgba($brand-primary, 0.5);
|
||||
box-shadow: 0 12rpx 32rpx rgba($brand-primary, 0.3);
|
||||
}
|
||||
|
||||
.bottom-spacer {
|
||||
height: calc(160rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
</style>
|
||||
297
pkgExplore/vr_preview/vr_preview.vue
Normal file
297
pkgExplore/vr_preview/vr_preview.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<view class="vr-preview-page">
|
||||
<tn-nav-bar
|
||||
:isBack="true"
|
||||
:bottomShadow="false"
|
||||
backgroundColor="transparent"
|
||||
:customBackStyle="navBarBackStyle"
|
||||
backIconColor="#FFFFFF"
|
||||
:fixed="true"
|
||||
>
|
||||
</tn-nav-bar>
|
||||
|
||||
<view class="vr-stage">
|
||||
<image v-if="poster" :src="poster" mode="aspectFill" class="vr-poster"></image>
|
||||
<view v-else class="vr-empty tn-flex tn-flex-col-center tn-flex-row-center">{{ t('emptyPreview') }}</view>
|
||||
<view class="vr-mask"></view>
|
||||
|
||||
<view class="vr-mode-chip">{{ t('modeChip') }}</view>
|
||||
|
||||
<view class="vr-center-indicator" @click="openPosterPreview">
|
||||
<view class="indicator-ring"></view>
|
||||
<view class="indicator-core">
|
||||
<text class="tn-icon-vr indicator-icon"></text>
|
||||
</view>
|
||||
<text class="indicator-text">{{ poster ? t('viewFullscreen') : t('waitingAsset') }}</text>
|
||||
</view>
|
||||
|
||||
<view class="vr-bottom-bar tn-safe-area-inset-bottom">
|
||||
<view class="vr-title-area">
|
||||
<view class="vr-title">{{ resolvedPageTitle }}</view>
|
||||
<view class="vr-subtitle">{{ poster ? t('posterSubtitle') : t('placeholderSubtitle') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="vr-action-circle" @click="openPosterPreview">
|
||||
<text :class="poster ? 'tn-icon-image' : 'tn-icon-vr'"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
i18n: {
|
||||
zh: {
|
||||
defaultTitle: 'VR看房',
|
||||
emptyPreview: '暂无VR预览画面',
|
||||
modeChip: 'VR预览模式',
|
||||
viewFullscreen: '查看全屏预览',
|
||||
waitingAsset: '等待素材接入',
|
||||
posterSubtitle: '当前展示 VR 预览海报,可点击查看大图。',
|
||||
placeholderSubtitle: '当前房型已保留 VR 入口,后续可继续接入更完整的沉浸式资源。',
|
||||
noAsset: '暂无VR预览素材'
|
||||
},
|
||||
en: {
|
||||
defaultTitle: 'VR Viewing',
|
||||
emptyPreview: 'No VR preview available',
|
||||
modeChip: 'VR Preview',
|
||||
viewFullscreen: 'View Fullscreen',
|
||||
waitingAsset: 'Waiting for assets',
|
||||
posterSubtitle: 'The current VR preview poster is displayed. Tap to view the full image.',
|
||||
placeholderSubtitle: 'This room type keeps its VR entry reserved for richer immersive assets later.',
|
||||
noAsset: 'No VR preview asset available'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageTitleZh: '',
|
||||
pageTitleEn: '',
|
||||
rawPageTitle: '',
|
||||
poster: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resolvedPageTitle() {
|
||||
if (this.$getLanguage() === 'en') {
|
||||
return this.pageTitleEn || this.pageTitleZh || this.rawPageTitle || this.t('defaultTitle')
|
||||
}
|
||||
return this.pageTitleZh || this.pageTitleEn || this.rawPageTitle || this.t('defaultTitle')
|
||||
},
|
||||
navBarBackStyle() {
|
||||
return {
|
||||
background: 'rgba(0, 0, 0, 0.4)',
|
||||
borderRadius: '50%',
|
||||
width: '64rpx',
|
||||
height: '64rpx',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '12rpx'
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.pageTitleZh = options && (options.titleZh || options.title) ? decodeURIComponent(options.titleZh || options.title) : ''
|
||||
this.pageTitleEn = options && options.titleEn ? decodeURIComponent(options.titleEn) : ''
|
||||
this.rawPageTitle = options && options.title ? decodeURIComponent(options.title) : ''
|
||||
this.poster = options.poster ? decodeURIComponent(options.poster) : ''
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: this.resolvedPageTitle,
|
||||
path: `/pkgExplore/vr_preview/vr_preview?title=${encodeURIComponent(this.resolvedPageTitle)}&titleZh=${encodeURIComponent(this.pageTitleZh || this.resolvedPageTitle)}&titleEn=${encodeURIComponent(this.pageTitleEn || this.resolvedPageTitle)}&poster=${encodeURIComponent(this.poster || '')}`,
|
||||
imageUrl: this.poster || ''
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
return {
|
||||
title: this.resolvedPageTitle,
|
||||
query: `title=${encodeURIComponent(this.resolvedPageTitle)}&titleZh=${encodeURIComponent(this.pageTitleZh || this.resolvedPageTitle)}&titleEn=${encodeURIComponent(this.pageTitleEn || this.resolvedPageTitle)}&poster=${encodeURIComponent(this.poster || '')}`,
|
||||
imageUrl: this.poster || ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t(key, params) {
|
||||
return this.$tm('pkgExplore.vr_preview.vr_preview', key, params)
|
||||
},
|
||||
openPosterPreview() {
|
||||
if (!this.poster) {
|
||||
uni.showToast({
|
||||
title: this.t('noAsset'),
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.previewImage({
|
||||
urls: [this.poster],
|
||||
current: this.poster
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vr-preview-page {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background: #000000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vr-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.vr-poster,
|
||||
.vr-empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.vr-empty {
|
||||
background: #111111;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.vr-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0) 40%, rgba(0,0,0,0.8) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vr-mode-chip {
|
||||
position: absolute;
|
||||
left: 32rpx;
|
||||
top: calc(120rpx + env(safe-area-inset-top));
|
||||
z-index: 10;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.22);
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 2rpx;
|
||||
backdrop-filter: blur(12rpx);
|
||||
}
|
||||
|
||||
.vr-center-indicator {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 45%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.indicator-ring {
|
||||
position: absolute;
|
||||
top: 14rpx;
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
animation: vrRipple 2.5s cubic-bezier(0.25, 0.8, 0.25, 1) infinite;
|
||||
}
|
||||
|
||||
.indicator-core {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(12rpx);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.indicator-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.indicator-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
letter-spacing: 4rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
@keyframes vrRipple {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.6);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vr-bottom-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
padding: 0 40rpx calc(60rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.vr-title-area {
|
||||
flex: 1;
|
||||
padding-right: 40rpx;
|
||||
}
|
||||
|
||||
.vr-title {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
margin-bottom: 12rpx;
|
||||
text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.5);
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.vr-subtitle {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
letter-spacing: 1rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.vr-action-circle {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.4);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 40rpx;
|
||||
color: #000000;
|
||||
margin-left: 6rpx; /* 微调播放图标居中 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user