chore: initial import

This commit is contained in:
liangjian
2026-06-29 09:42:53 +08:00
commit 9fea127de1
241 changed files with 82767 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

532
pkgExplore/sign/sign.vue Normal file
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>