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