修改商详情页UI

This commit is contained in:
2026-06-04 18:32:08 +08:00
parent 714e63e12a
commit fc808bd562
14 changed files with 6440 additions and 1605 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,9 +21,14 @@
<text v-for="tag in summaryGuaranteeTags" :key="tag.id" class="summary-tag">{{ tag.label }}</text>
</view>
<view class="summary-price-row">
<text class="summary-price-prefix">¥</text>
<text class="summary-price">{{ minPriceText }}</text>
<text class="summary-price-unit">起</text>
<view class="summary-price-main">
<text class="summary-price-prefix">¥</text>
<text class="summary-price">{{ minPriceText }}</text>
<text class="summary-price-unit">起</text>
</view>
<view class="summary-favorite-btn" @click="toggleServiceFavorite">
<text class="summary-favorite-btn-text">{{ isFavorite ? '已收藏' : '收藏服务' }}</text>
</view>
</view>
</view>
@@ -303,6 +308,7 @@ import { createHomeServiceApplication, fetchHomeServiceCatalog, fetchHomeService
import { shouldUseCareTaskPath } from '@/services/serviceOrderService.uts'
import { HomeServiceApplicationDraftType, HomeServiceCatalogType, HomeServicePackageType, HomeServiceSelectedAddressType } from '@/types/home-service.uts'
import { getCurrentUser, getCurrentUserId } from '@/utils/store.uts'
import { supabaseService } from '@/utils/supabaseService.uts'
import { goToLogin } from '@/utils/utils.uts'
import {
HomeServiceAgencyType,
@@ -382,6 +388,7 @@ const serviceValidity = ref('预约后 30 天内可服务')
const serviceSuitableFor = ref('行动不便、术后恢复、慢病随访老人')
const serviceImageText = ref('照护')
const serviceExcludeText = ref('高风险处置、住院陪护、急诊陪诊')
const isFavorite = ref(false)
const bookingDays = ref<Array<BookingDayOptionType>>([])
const bookingSlots = ref<Array<BookingTimeSlotType>>([])
@@ -782,6 +789,7 @@ async function loadData() {
}
if (matchedService == null) {
setUnavailableServiceState()
await syncFavoriteState()
return
}
serviceTitle.value = matchedService.name
@@ -796,11 +804,34 @@ async function loadData() {
servicePackages.value = mapServicePackages(packages)
if (servicePackages.value.length == 0) {
setUnavailableServiceState()
await syncFavoriteState()
return
}
const firstPackage = servicePackages.value[0]
servicePrice.value = firstPackage.price
serviceDuration.value = firstPackage.duration
await syncFavoriteState()
}
async function syncFavoriteState(): Promise<void> {
const userId = getCurrentUserId()
if (userId == '') {
isFavorite.value = false
return
}
isFavorite.value = await supabaseService.checkFavorite(serviceId.value, 'service')
}
async function toggleServiceFavorite(): Promise<void> {
if (!(await ensureLogin())) {
return
}
const nextState = await supabaseService.toggleFavorite(serviceId.value, 'service')
isFavorite.value = nextState
uni.showToast({
title: nextState ? '已收藏服务' : '已取消收藏',
icon: nextState ? 'success' : 'none'
})
}
async function ensureLogin(): Promise<boolean> {
@@ -1105,6 +1136,7 @@ function refreshBookingSlots(): void {
onShow(() => {
loadCachedSelectedAddress()
syncFavoriteState()
const now = new Date()
const day = selectedDay.value
if (day != null) {
@@ -1358,6 +1390,12 @@ onUnload(() => {
.summary-price-row {
margin-top: 12rpx;
justify-content: space-between;
align-items: flex-end;
}
.summary-price-main {
flex-direction: row;
align-items: flex-end;
}
@@ -1377,6 +1415,26 @@ onUnload(() => {
margin-bottom: 6rpx;
}
.summary-favorite-btn {
min-width: 132rpx;
height: 56rpx;
padding: 0 22rpx;
border-radius: 999rpx;
background: #fff3ee;
border-width: 1rpx;
border-style: solid;
border-color: #ffd0c2;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.summary-favorite-btn-text {
font-size: 22rpx;
color: #d85b34;
font-weight: 600;
}
.form-item {
margin-bottom: 24rpx;
}

View File

@@ -1,33 +1,57 @@
<!-- 消费者端 - 商品详情页 -->
<template>
<view class="product-detail-page">
<scroll-view class="page-scroll" direction="vertical">
<view class="hero-section">
<view class="custom-header" :style="`height:${customHeaderHeight}px;`">
<view class="header-fade-layer" :style="getHeaderFadeStyle()">
<view class="header-main-row">
<view class="header-back-space" :style="`width:${navButtonHeight + 20}px;`"></view>
<view class="header-search-box" @click="goToSearch">
<text class="header-search-icon">⌕</text>
<text class="header-search-placeholder">{{ getDisplayTitle() }}</text>
</view>
<view class="header-capsule-reserve" :style="`width:${navRightReserve}px;`"></view>
</view>
<view class="header-tabs-row">
<view class="header-tab-item" @click="scrollToSection('goods')">
<text :class="activeTab == 'goods' ? 'header-tab-text active' : 'header-tab-text'">宝贝</text>
<view v-if="activeTab == 'goods'" class="header-tab-line"></view>
</view>
<view class="header-tab-item" @click="scrollToSection('comments')">
<text :class="activeTab == 'comments' ? 'header-tab-text active' : 'header-tab-text'">评论</text>
<view v-if="activeTab == 'comments'" class="header-tab-line"></view>
</view>
<view class="header-tab-item" @click="scrollToSection('detail')">
<text :class="activeTab == 'detail' ? 'header-tab-text active' : 'header-tab-text'">详情</text>
<view v-if="activeTab == 'detail'" class="header-tab-line"></view>
</view>
<view class="header-tab-item" @click="scrollToSection('recommend')">
<text :class="activeTab == 'recommend' ? 'header-tab-text active' : 'header-tab-text'">推荐</text>
<view v-if="activeTab == 'recommend'" class="header-tab-line"></view>
</view>
</view>
</view>
</view>
<view class="persistent-back-btn" :style="getBackButtonStyle()" @click="goBack">
<text class="persistent-back-text"></text>
</view>
<scroll-view class="page-scroll" direction="vertical" @scroll="onProductScroll" :scroll-top="targetScrollTop" scroll-with-animation="true">
<view id="section-goods" class="hero-section">
<swiper class="hero-swiper" :indicator-dots="false" :autoplay="false" @change="onSwiperChange">
<swiper-item v-for="(image, index) in getProductImages()" :key="index">
<image :src="image" class="hero-image" mode="aspectFill" @error="handleProductImageError(index)" @click="previewImage(index)" />
</swiper-item>
</swiper>
<view class="hero-indicator">{{ currentImageIndex + 1 }}/{{ getProductImages().length }}</view>
<view class="floating-nav left-nav" :style="getNavLeftStyle()">
<view class="floating-btn" @click="goBack">
<text class="floating-btn-text"></text>
</view>
<view class="floating-btn" @click="goToSearch">
<text class="floating-btn-text">⌕</text>
</view>
</view>
<view class="floating-nav right-nav" :style="getNavRightStyle()">
<view class="floating-btn" @click="openActionPanel">
<text class="floating-btn-text">...</text>
</view>
</view>
</view>
<view class="content-wrap">
<view class="info-card price-sales-card">
<view v-if="isProductOffShelf()" class="off-shelf-tip">
<text class="off-shelf-tip-text">商品已下架,当前仅支持查看,暂不支持购买</text>
</view>
<view v-if="memberDiscount > 0 && memberPrice > 0 && memberPrice < product.price" class="vip-row">
<view class="vip-badge">
<text class="vip-badge-text">VIP</text>
@@ -126,7 +150,7 @@
<text class="enter-shop">进店 ></text>
</view>
<view class="info-card review-card">
<view id="section-comments" class="info-card review-card">
<view class="card-header-row">
<text class="card-title">商品评论</text>
<text class="card-link-text" @click="viewAllReviews">查看全部 ></text>
@@ -151,7 +175,7 @@
</view>
</view>
<view class="info-card detail-card">
<view id="section-detail" class="info-card detail-card">
<view class="card-header-row">
<text class="card-title">商品详情</text>
<text class="card-link-text" @click="showParamsModal">商品参数 ></text>
@@ -163,7 +187,7 @@
</view>
<view class="detail-images" v-if="getDetailImages().length > 0">
<image v-for="(img, index) in getDetailImages()" :key="'detail-' + index" :src="img" class="detail-image" mode="widthFix" @error="handleDetailImageError(index)" @click="previewDetailImage(index)" />
<image v-for="(img, index) in getDetailImages()" :key="'detail-' + index" :src="img" class="detail-image" mode="widthFix" @error="handleDetailImageError(index)" @click="previewDetailImage(index)" @load="onDetailImageLoad" />
</view>
<text v-else-if="getDetailDescription() != ''" class="description-text">{{ getDetailDescription() }}</text>
@@ -172,6 +196,15 @@
<text class="empty-detail-text">暂无商品详情</text>
</view>
</view>
<view id="section-recommend" class="info-card recommend-card">
<view class="card-header-row">
<text class="card-title">推荐</text>
</view>
<view class="empty-recommend-state">
<text class="empty-recommend-text">暂无推荐商品</text>
</view>
</view>
</view>
</scroll-view>
@@ -195,8 +228,8 @@
</view>
<view class="btn-group">
<button class="cart-btn" @click="addToCart">加入购物车</button>
<button class="buy-btn" @click="buyNow">立即购买</button>
<button :class="['cart-btn', { 'btn-disabled': isProductOffShelf() }]" @click="addToCart">{{ isProductOffShelf() ? '已下架' : '加入购物车' }}</button>
<button :class="['buy-btn', { 'btn-disabled': isProductOffShelf() }]" @click="buyNow">{{ isProductOffShelf() ? '暂不可购买' : '立即购买' }}</button>
</view>
</view>
@@ -441,7 +474,19 @@ export default {
reviewPreview: [] as Array<ProductReviewPreviewType>,
reviewLoading: false,
productGuarantees: [] as Array<ProductGuaranteeType>,
detailImages: [] as Array<string>
detailImages: [] as Array<string>,
currentScrollTop: 0 as number,
headerOpacity: 0 as number,
activeTab: 'goods' as string,
targetScrollTop: 0 as number,
customHeaderHeight: 0 as number,
sectionOffsets: new UTSJSONObject(),
isProgrammaticScrolling: false as boolean,
capsuleLeft: 0 as number,
capsuleBottom: 0 as number,
headerMainRowHeight: 44 as number,
headerTabsRowHeight: 40 as number,
searchRightReserve: 16 as number
}
},
onLoad(options: any) {
@@ -473,7 +518,7 @@ export default {
}
this.productGuarantees = this.getDefaultGuarantees()
this.reviewPreview = this.getMockReviews()
this.reviewPreview = [] as Array<ProductReviewPreviewType>
if (productId != null) {
this.loadProductDetail(productId, {
@@ -485,6 +530,10 @@ export default {
this.checkFavoriteStatus(productId)
this.saveFootprint(productId)
}
setTimeout(() => {
this.measureSections()
}, 600)
},
onShow() {
this.refreshCartBadgeCount()
@@ -589,13 +638,25 @@ export default {
},
initImmersiveLayout() {
const systemInfo = uni.getSystemInfoSync()
this.statusBarHeight = systemInfo.statusBarHeight ?? 0
let windowWidth = 375
try {
const windowInfo = uni.getWindowInfo()
this.statusBarHeight = windowInfo.statusBarHeight ?? 0
windowWidth = windowInfo.windowWidth ?? 375
} catch (e) {
this.statusBarHeight = 0
}
this.capsuleTop = this.statusBarHeight > 0 ? this.statusBarHeight : 12
this.capsuleHeight = 32
this.navButtonTop = this.capsuleTop
this.navButtonHeight = this.capsuleHeight
this.navRightReserve = 16
this.capsuleLeft = windowWidth - 100
this.capsuleBottom = this.capsuleTop + this.capsuleHeight
this.headerMainRowHeight = 44
this.headerTabsRowHeight = 40
this.searchRightReserve = 16
this.customHeaderHeight = this.statusBarHeight + this.headerMainRowHeight + this.headerTabsRowHeight
// #ifdef MP-WEIXIN
try {
@@ -605,13 +666,18 @@ export default {
this.capsuleHeight = menuBtn.height > 0 ? menuBtn.height : 32
this.navButtonTop = menuBtn.top
this.navButtonHeight = this.capsuleHeight
const screenWidth = systemInfo.screenWidth ?? systemInfo.windowWidth
this.navRightReserve = (screenWidth - menuBtn.left) + 12
this.navRightReserve = (windowWidth - menuBtn.left) + 12
this.capsuleLeft = menuBtn.left
this.capsuleBottom = menuBtn.bottom ?? (menuBtn.top + menuBtn.height)
this.searchRightReserve = menuBtn.left - 8
}
} catch (e) {
this.navButtonTop = this.statusBarHeight > 0 ? this.statusBarHeight + 6 : 16
this.navButtonHeight = 32
this.navRightReserve = 16
this.capsuleLeft = windowWidth - 100
this.capsuleBottom = this.navButtonTop + 32
this.searchRightReserve = 16
}
// #endif
@@ -624,14 +690,106 @@ export default {
if (this.navRightReserve < 16) {
this.navRightReserve = 16
}
if (this.searchRightReserve < 16) {
this.searchRightReserve = 16
}
this.customHeaderHeight = this.statusBarHeight + this.headerMainRowHeight + this.headerTabsRowHeight
},
getNavLeftStyle(): string {
return `top:${this.navButtonTop}px;height:${this.navButtonHeight}px;`
getHeaderFadeStyle(): string {
const opacity = this.headerOpacity
const translateY = (1 - opacity) * (-6)
const h = this.statusBarHeight + this.headerMainRowHeight + this.headerTabsRowHeight
const pt = this.statusBarHeight
const pointerEvents = opacity < 0.05 ? 'none' : 'auto'
return `opacity:${opacity};transform:translateY(${translateY}px);height:${h}px;padding-top:${pt}px;pointer-events:${pointerEvents};`
},
getNavRightStyle(): string {
return `top:${this.navButtonTop}px;height:${this.navButtonHeight}px;right:${this.navRightReserve}px;`
getBackButtonStyle(): string {
const top = this.navButtonTop
const height = this.navButtonHeight
const bgOpacity = this.headerOpacity > 0.8 ? 0 : 0.88
return `top:${top}px;height:${height}px;background-color:rgba(255,255,255,${bgOpacity});`
},
onProductScroll(e: { detail: { scrollTop: number } }) {
const current = e.detail.scrollTop
this.currentScrollTop = current
const fadeStart = 16
const fadeEnd = 128
const progress = (current - fadeStart) / (fadeEnd - fadeStart)
this.headerOpacity = Math.max(0, Math.min(1, progress))
if (!this.isProgrammaticScrolling) {
const offsets = this.sectionOffsets
if (offsets != null) {
const goodsOffset = offsets.getNumber('goods') ?? 0
const commentsOffset = offsets.getNumber('comments') ?? 999999
const detailOffset = offsets.getNumber('detail') ?? 999999
const recommendOffset = offsets.getNumber('recommend') ?? 999999
const visualTop = current + this.customHeaderHeight + 20
let nextTab = 'goods'
if (visualTop >= recommendOffset && recommendOffset < 999999) {
nextTab = 'recommend'
} else if (visualTop >= detailOffset && detailOffset < 999999) {
nextTab = 'detail'
} else if (visualTop >= commentsOffset && commentsOffset < 999999) {
nextTab = 'comments'
} else {
nextTab = 'goods'
}
if (nextTab != this.activeTab) {
this.activeTab = nextTab
}
}
}
},
scrollToSection(tabKey: string) {
const offsets = this.sectionOffsets
if (offsets == null) return
const offset = offsets.getNumber(tabKey)
if (offset != null && offset >= 0) {
let target = tabKey == 'goods' ? 0 : Math.max(0, offset - this.customHeaderHeight)
this.isProgrammaticScrolling = true
this.targetScrollTop = -1
setTimeout(() => {
this.targetScrollTop = target
setTimeout(() => { this.isProgrammaticScrolling = false }, 400)
}, 20)
this.activeTab = tabKey
}
},
measureSections() {
const sectionIds = ['section-goods', 'section-comments', 'section-detail', 'section-recommend']
const query = uni.createSelectorQuery().in(this)
for (let i = 0; i < sectionIds.length; i++) {
query.select('#' + sectionIds[i]).boundingClientRect()
}
query.exec((res: any) => {
if (res == null || !Array.isArray(res)) return
const offsets = new UTSJSONObject()
for (let i = 0; i < res.length; i++) {
const rect = res[i]
if (rect != null && rect.top != null) {
const key = sectionIds[i].replace('section-', '')
offsets.set(key, rect.top + this.currentScrollTop)
}
}
this.sectionOffsets = offsets
this.customHeaderHeight = this.statusBarHeight + this.headerMainRowHeight + this.headerTabsRowHeight
})
},
onDetailImageLoad() {
setTimeout(() => {
this.measureSections()
}, 300)
},
goBack() {
@@ -671,7 +829,7 @@ export default {
}
})
const footprintData = uni.getStorageSync('footprints') as string | null
const footprintData = uni.getStorageSync('footprints_v2') as string | null
let footprints: Array<FootprintItemType> = []
if (footprintData != null && footprintData !== '') {
@@ -685,27 +843,16 @@ export default {
const productIdStr = productId
footprints = footprints.filter(function(item) {
const itemObj = item as UTSJSONObject
const itemId = itemObj.getString('id') ?? ''
const itemId = itemObj.getString('productId') ?? itemObj.getString('id') ?? ''
return itemId != productIdStr
})
const productImage = this.product.images.length > 0 ? this.product.images[0] : this.defaultImage
footprints.unshift({
id: this.product.id,
name: this.product.name,
price: this.product.price,
original_price: this.product.original_price,
image: productImage,
sales: this.product.sales,
shopId: this.merchant.id,
shopName: this.merchant.shop_name,
viewTime: Date.now()
})
footprints.unshift(this.getFootprintCachePayload())
if (footprints.length > 50) {
footprints = footprints.slice(0, 50)
if (footprints.length > 200) {
footprints = footprints.slice(0, 200)
}
uni.setStorageSync('footprints', JSON.stringify(footprints))
uni.setStorageSync('footprints_v2', JSON.stringify(footprints))
},
async loadProductDetail(productId: string, options: any = {}) {
@@ -997,20 +1144,14 @@ export default {
this.reviewCount = parsedReviews.length
}
} else {
this.reviewPreview = this.getMockReviews()
if (this.reviewCount <= 0) {
this.reviewCount = this.reviewPreview.length
}
this.reviewPreview = [] as Array<ProductReviewPreviewType>
if (this.reviewRating <= 0) {
this.reviewRating = 5
}
}
} catch (e) {
console.error('加载商品评论失败:', e)
this.reviewPreview = this.getMockReviews()
if (this.reviewCount <= 0) {
this.reviewCount = this.reviewPreview.length
}
this.reviewPreview = [] as Array<ProductReviewPreviewType>
if (this.reviewRating <= 0) {
this.reviewRating = 5
}
@@ -1195,6 +1336,10 @@ export default {
if (trimmed.indexOf('https://picsum.photos/') == 0) return this.defaultImage
if (trimmed.indexOf('http://picsum.photos/') == 0) return this.defaultImage
if (trimmed.indexOf('blob:') == 0) return this.defaultImage
if (trimmed.indexOf('/static/service/') == 0) return this.defaultImage
if (trimmed.indexOf('/static/images/product/') == 0) return this.defaultImage
if (trimmed.indexOf('__tmp__/') >= 0) return this.defaultImage
if (trimmed.indexOf('http://') == 0) return this.defaultImage
return trimmed
},
@@ -1427,6 +1572,30 @@ export default {
this.selectedSpec = this.getSkuSpecText(sku)
},
isProductOffShelf(): boolean {
return this.product.status != null && this.product.status !== 1
},
showOffShelfToast() {
uni.showToast({ title: '该商品已下架', icon: 'none' })
},
getFootprintCachePayload(): UTSJSONObject {
const payload = new UTSJSONObject()
const productImage = this.product.images.length > 0 ? this.product.images[0] : this.defaultImage
payload.set('footprintId', '')
payload.set('productId', this.product.id)
payload.set('name', this.product.name)
payload.set('price', this.product.price)
payload.set('originalPrice', this.product.original_price ?? 0)
payload.set('image', productImage)
payload.set('merchantId', this.merchant.id != '' ? this.merchant.id : this.product.merchant_id)
payload.set('viewTime', Date.now())
payload.set('saleStatus', this.product.status ?? 0)
payload.set('isOffShelf', this.isProductOffShelf())
return payload
},
getSkuSpecText(sku: ProductSkuType): string {
if (sku.specifications != null) {
const specs = sku.specifications as UTSJSONObject
@@ -1451,9 +1620,13 @@ export default {
},
async addToCart() {
if (this.isProductOffShelf()) {
this.showOffShelfToast()
return
}
const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/product-detail?id=${this.product.id}`)
goToLogin(`/pages/mall/consumer/product-detail?productId=${this.product.id}`)
return
}
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
@@ -1489,9 +1662,13 @@ export default {
},
buyNow() {
if (this.isProductOffShelf()) {
this.showOffShelfToast()
return
}
const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/product-detail?id=${this.product.id}`)
goToLogin(`/pages/mall/consumer/product-detail?productId=${this.product.id}`)
return
}
if (this.productSkus.length > 0 && (this.selectedSkuId == null || this.selectedSkuId === '')) {
@@ -1535,7 +1712,7 @@ export default {
if (this.product.id == '') return
const userId = supabaseService.getCurrentUserId()
if (userId == null || userId === '') {
goToLogin(`/pages/mall/consumer/product-detail?id=${this.product.id}`)
goToLogin(`/pages/mall/consumer/product-detail?productId=${this.product.id}`)
return
}
uni.showLoading({ title: '处理中' })
@@ -1838,7 +2015,7 @@ export default {
copyLink() {
this.hideSharePopup()
const shareLink = `pages/mall/consumer/product-detail?id=${this.product.id}`
const shareLink = `pages/mall/consumer/product-detail?productId=${this.product.id}`
uni.setClipboardData({
data: shareLink,
success: () => {
@@ -1928,46 +2105,142 @@ export default {
font-size: 24rpx;
}
.floating-nav {
.custom-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 30;
}
.header-fade-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
z-index: 1;
display: flex;
flex-direction: column;
}
.header-main-row {
display: flex;
flex-direction: row;
align-items: center;
z-index: 20;
flex: 1;
padding-left: 0;
padding-right: 0;
}
.left-nav {
left: 24rpx;
.header-back-space {
flex-shrink: 0;
}
.right-nav {
right: 24rpx;
}
.floating-btn {
min-width: 72rpx;
height: 100%;
padding: 0 20rpx;
.header-search-box {
flex: 1;
height: 64rpx;
background-color: #f5f6f8;
border-radius: 999rpx;
background-color: rgba(255, 255, 255, 0.88);
display: flex;
flex-direction: row;
align-items: center;
padding: 0 20rpx;
}
.header-search-icon {
font-size: 28rpx;
color: #9aa1ab;
margin-right: 8rpx;
}
.header-search-placeholder {
font-size: 26rpx;
color: #9aa1ab;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-capsule-reserve {
flex-shrink: 0;
}
.header-tabs-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
height: 80rpx;
padding-bottom: 4rpx;
}
.header-tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-right: 14rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
position: relative;
height: 100%;
padding: 0 20rpx;
}
.right-nav .floating-btn {
margin-right: 0;
}
.floating-btn-text {
.header-tab-text {
font-size: 28rpx;
color: #5f6670;
}
.header-tab-text.active {
color: #ff4d24;
font-weight: 700;
}
.header-tab-line {
position: absolute;
bottom: 8rpx;
left: 50%;
transform: translateX(-50%);
width: 32rpx;
height: 4rpx;
background-color: #ff4d24;
border-radius: 999rpx;
}
.persistent-back-btn {
position: fixed;
left: 16rpx;
width: 72rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 31;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.persistent-back-text {
font-size: 36rpx;
color: #1f2329;
font-weight: 700;
}
.recommend-card {
margin-bottom: 20rpx;
}
.empty-recommend-state {
padding: 40rpx 0;
display: flex;
align-items: center;
justify-content: center;
}
.empty-recommend-text {
font-size: 24rpx;
color: #9aa1ab;
}
.content-wrap {
margin-top: -20rpx;
padding: 0 20rpx 180rpx;
@@ -1987,6 +2260,19 @@ export default {
background: linear-gradient(180deg, #fff8f4 0%, #ffffff 72%);
}
.off-shelf-tip {
margin-bottom: 18rpx;
padding: 14rpx 18rpx;
border-radius: 18rpx;
background-color: #fff1f0;
}
.off-shelf-tip-text {
font-size: 22rpx;
color: #d4380d;
font-weight: 600;
}
.vip-row {
display: flex;
flex-direction: row;
@@ -2653,6 +2939,11 @@ export default {
color: #ffffff;
}
.btn-disabled {
background: #d8dbe1 !important;
color: #ffffff !important;
}
.spec-modal,
.params-modal,
.popup-mask,
@@ -3067,6 +3358,3 @@ export default {
font-weight: 600;
}
</style>