5108 lines
121 KiB
Plaintext
5108 lines
121 KiB
Plaintext
<!-- pages/main/index.uvue -->
|
||
<template>
|
||
<view class="medic-home">
|
||
<view :class="['home-main-stage', pageLoading ? 'home-main-stage-hidden' : 'home-main-stage-visible']">
|
||
<view class="jd-header-fixed">
|
||
<JdLikeHomeHeader
|
||
:status-bar-height="statusBarHeight"
|
||
:capsule-right="navBarRight"
|
||
:modules="topModules"
|
||
:active-module="activeTopModule"
|
||
:search-keyword="searchKeyword"
|
||
:placeholder="headerSearchPlaceholder"
|
||
@change-module="handleTopModuleChange"
|
||
@update:searchKeyword="handleSearchKeywordUpdate"
|
||
@search="handleHeaderSearch"
|
||
@focusSearch="handleSearchFocus"
|
||
></JdLikeHomeHeader>
|
||
|
||
<view v-if="activeTopModule == 'home'" class="category-wrapper">
|
||
<view :class="['category-bar-wrap', { 'category-bar-wrap-hidden': showCategoryPanel }]">
|
||
<scroll-view
|
||
class="category-scroll"
|
||
direction="horizontal"
|
||
:scroll-x="true"
|
||
:show-scrollbar="false"
|
||
:scroll-with-animation="true"
|
||
:scroll-into-view="categoryScrollIntoView"
|
||
>
|
||
<view
|
||
v-for="(item, index) in categoryList"
|
||
:key="buildListItemKey('top-category', item.id, index)"
|
||
:id="'cat-' + item.id"
|
||
:class="['category-item', { 'category-item-active': currentCategory === item.id }]"
|
||
@tap="handleCategoryTabClick(item)"
|
||
>
|
||
<text :class="['category-item-text', { 'category-item-text-active': currentCategory === item.id, 'category-item-text-accent': shouldHighlightCategory(item.name) && currentCategory !== item.id }]">{{ getCategoryTabDisplayName(item.name) }}</text>
|
||
<view v-if="currentCategory === item.id" class="category-active-line"></view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<view class="category-expand-btn" @tap="toggleCategoryPanel">
|
||
<text class="category-expand-icon">{{ showCategoryPanel ? '∧' : '∨' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="jd-header-placeholder" :style="{ height: (activeTopModule == 'home' ? headerPlaceholderHeight : navbarTotalHeight) + 'px' }"></view>
|
||
|
||
<scroll-view
|
||
direction="vertical"
|
||
class="main-scroll"
|
||
refresher-enabled
|
||
:refresher-triggered="refreshing"
|
||
:lower-threshold="50"
|
||
@refresherrefresh="onRefresh"
|
||
@scrolltolower="handleMainScrollToLower"
|
||
@scroll="handleScroll"
|
||
>
|
||
<HomeMallContent
|
||
v-if="activeTopModule == 'home'"
|
||
:current-category="currentCategory"
|
||
:page-loading="pageLoading"
|
||
:selected-sub-category-id="selectedSubCategoryId"
|
||
:secondary-category-display="secondaryCategoryDisplay"
|
||
:marketing-channels="marketingChannels"
|
||
:category-simple-channels="categorySimpleChannels"
|
||
:hot-products="hotProducts"
|
||
:loading="loading"
|
||
:has-more="hasMore"
|
||
:show-load-more="showLoadMore"
|
||
:empty-state-title="homeEmptyStateTitle"
|
||
:empty-state-description="homeEmptyStateDescription"
|
||
@secondary-category-click="handleSecondaryCategoryClick"
|
||
@select-channel="navigateToChannel"
|
||
@select-simple-channel="navigateToSimpleChannel"
|
||
@select-product="navigateToProduct"
|
||
></HomeMallContent>
|
||
|
||
<view v-else class="service-home-section">
|
||
<view class="service-hero-banner">
|
||
<view class="service-hero-content">
|
||
<text class="service-hero-tag">康养到家</text>
|
||
<text class="service-hero-title">专业康养服务到家</text>
|
||
<text class="service-hero-subtitle">护理照护|康复指导|陪诊陪护</text>
|
||
<view class="service-hero-tags">
|
||
<text class="service-hero-chip">平台认证</text>
|
||
<text class="service-hero-chip">上门服务</text>
|
||
<text class="service-hero-chip">服务可追溯</text>
|
||
</view>
|
||
</view>
|
||
<view class="service-hero-visual-wrap">
|
||
<view class="service-hero-visual service-hero-visual-primary">
|
||
<text class="service-hero-visual-text">护</text>
|
||
</view>
|
||
<view class="service-hero-visual service-hero-visual-secondary">
|
||
<text class="service-hero-visual-subtext">康</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="service-category-card">
|
||
<view
|
||
v-for="(item, index) in serviceCategories"
|
||
:key="buildListItemKey('service-category', item.id, index)"
|
||
class="service-category-item"
|
||
@tap="handleServiceCategoryClick(item)"
|
||
>
|
||
<view
|
||
:class="['service-category-icon', selectedServiceCategory == item.id ? 'service-category-icon-active' : '']"
|
||
:style="{ backgroundColor: item.color }"
|
||
>
|
||
<text class="service-category-icon-text">{{ item.iconText }}</text>
|
||
</view>
|
||
<text :class="['service-category-name', selectedServiceCategory == item.id ? 'service-category-name-active' : '']">{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="service-shortcut-row">
|
||
<view
|
||
v-for="(item, index) in serviceShortcuts"
|
||
:key="buildListItemKey('service-shortcut', item.id, index)"
|
||
class="service-shortcut-card"
|
||
@tap="handleServiceShortcut(item.id)"
|
||
>
|
||
<text class="service-shortcut-icon">{{ item.iconText }}</text>
|
||
<view class="service-shortcut-body">
|
||
<text class="service-shortcut-title">{{ item.title }}</text>
|
||
<text class="service-shortcut-desc">{{ item.desc }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="service-products-section">
|
||
<view class="service-section-title-row">
|
||
<view>
|
||
<text class="service-section-title">推荐服务</text>
|
||
<text class="service-section-subtitle">{{ serviceSelectedCategoryLabel }}</text>
|
||
</view>
|
||
<text class="service-section-more" @tap="goServiceHall">更多</text>
|
||
</view>
|
||
|
||
<view v-if="serviceLoading" class="service-state-card">
|
||
<text class="service-state-title">正在加载服务...</text>
|
||
<text class="service-state-desc">请稍候,正在整理适合居家康养的服务内容。</text>
|
||
</view>
|
||
|
||
<view v-else-if="serviceProducts.length > 0" class="service-products-grid">
|
||
<view
|
||
v-for="(item, index) in serviceProducts"
|
||
:key="buildListItemKey('service-product', item.id, index)"
|
||
class="service-product-card"
|
||
@tap="goServiceDetail(item)"
|
||
>
|
||
<view class="service-product-cover" :style="{ background: item.coverGradient }">
|
||
<text class="service-product-cover-badge">到家服务</text>
|
||
<text class="service-product-cover-text">{{ item.imageText }}</text>
|
||
</view>
|
||
<view class="service-product-body">
|
||
<text class="service-product-title">{{ item.title }}</text>
|
||
<text class="service-product-subtitle">{{ item.subtitle }}</text>
|
||
<view class="service-product-tags">
|
||
<text
|
||
v-for="(tag, tagIndex) in item.tags"
|
||
:key="buildListItemKey(item.id + '-tag', tag, tagIndex)"
|
||
class="service-product-tag"
|
||
>
|
||
{{ tag }}
|
||
</text>
|
||
</view>
|
||
<view class="service-product-price-row">
|
||
<text class="service-product-price-symbol">¥</text>
|
||
<text class="service-product-price">{{ item.price }}</text>
|
||
<text class="service-product-unit">起 / {{ item.unit }}</text>
|
||
</view>
|
||
<text class="service-product-sales">{{ item.salesText }}</text>
|
||
<view class="service-product-action-row">
|
||
<view class="service-product-secondary-btn" @tap.stop="showServicePreview(item)">查看详情</view>
|
||
<view class="service-product-primary-btn" @tap.stop="goServiceDetail(item)">立即预约</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else class="service-state-card">
|
||
<text class="service-state-title">该分类服务正在完善</text>
|
||
<text class="service-state-desc">可先查看全部服务,或进入服务大厅了解更多上门服务。</text>
|
||
<view class="service-state-action" @tap="goServiceHall">进入服务大厅</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="safe-area" :style="{ height: bottomSafeArea + 88 + 'px' }"></view>
|
||
</scroll-view>
|
||
|
||
<view
|
||
v-if="activeTopModule == 'home' && showCategoryPanel"
|
||
class="category-panel-mask"
|
||
:style="{ top: navbarTotalHeight + 'px' }"
|
||
@tap="toggleCategoryPanel"
|
||
></view>
|
||
<view
|
||
v-if="activeTopModule == 'home' && showCategoryPanel"
|
||
class="category-panel"
|
||
:style="{ top: (navbarTotalHeight - 5) + 'px' }"
|
||
>
|
||
<view class="category-panel-header">
|
||
<text class="category-panel-title">全部分类</text>
|
||
<view class="category-panel-close-btn" @tap="toggleCategoryPanel">
|
||
<text class="category-panel-close-text">收起</text>
|
||
<text class="category-panel-close-arrow">∧</text>
|
||
</view>
|
||
</view>
|
||
<view class="category-panel-grid">
|
||
<view
|
||
v-for="(item, index) in categoryList"
|
||
:key="buildListItemKey('panel-category', item.id, index)"
|
||
:class="['category-panel-item', { 'category-panel-item-active': currentCategory === item.id }]"
|
||
@tap="selectCategoryFromPanel(item)"
|
||
>
|
||
<text :class="['category-panel-item-text', { 'category-panel-item-text-active': currentCategory === item.id }]">{{ item.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
v-if="showHomeSkeleton"
|
||
:class="['home-skeleton-overlay', pageLoading ? 'home-skeleton-overlay-visible' : 'home-skeleton-overlay-leaving']"
|
||
>
|
||
<HomeSkeleton
|
||
:status-bar-height="statusBarHeight"
|
||
:capsule-right="navBarRight"
|
||
:bottom-safe-area="bottomSafeArea"
|
||
:show-category-bar="true"
|
||
:category-count="7"
|
||
:channel-count="4"
|
||
:product-count="8"
|
||
></HomeSkeleton>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
|
||
import { onShow, onLoad, onHide } from '@dcloudio/uni-app'
|
||
import HomeMallContent from '@/components/home/HomeMallContent.uvue'
|
||
import JdLikeHomeHeader from '@/components/home/JdLikeHomeHeader.uvue'
|
||
import HomeSkeleton from '@/components/home-skeleton/home-skeleton.uvue'
|
||
import { fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
|
||
import type { HomeServiceCatalogType } from '@/types/home-service.uts'
|
||
import supabaseService from '@/utils/supabaseService.uts'
|
||
import type { Product, Category, Brand, PaginatedResponse } from '@/utils/supabaseService.uts'
|
||
import { getRecommendMarketingChannels } from '@/utils/mockChannelData.uts'
|
||
import type { MarketingChannel, ChannelProduct, SimpleCategoryChannel } from '@/utils/mockChannelData.uts'
|
||
import { getCurrentUser } from '@/utils/store.uts'
|
||
import { goToLogin } from '@/utils/utils.uts'
|
||
import { logSupaConfig } from '@/ak/config.uts'
|
||
|
||
// 响应式数据
|
||
const statusBarHeight = ref(0)
|
||
const bottomSafeArea = ref(20)
|
||
const scrollHeight = ref(0)
|
||
const refreshing = ref(false)
|
||
const loading = ref(false)
|
||
const isFirstShow = ref(true)
|
||
const hasMore = ref(true)
|
||
const pageLoading = ref(true)
|
||
const goodsLoaded = ref(false)
|
||
const categoryLoaded = ref(false)
|
||
const bannerLoaded = ref(false)
|
||
const skeletonExiting = ref(false)
|
||
const activeSort = ref('recommend') // 默认展示智能推荐
|
||
const activeFilter = ref('recommend')
|
||
const currentPage = ref(1)
|
||
const priceAscending = ref(true) // 价格排序方向:true=升序,false=降序
|
||
|
||
// 小程序胶囊按钮信息类型
|
||
type CapsuleButtonInfo = {
|
||
left: number,
|
||
top: number,
|
||
right: number,
|
||
bottom: number,
|
||
width: number,
|
||
height: number
|
||
}
|
||
|
||
// 小程序胶囊按钮信息
|
||
const capsuleButtonInfo = ref<CapsuleButtonInfo | null>(null)
|
||
const navBarRight = ref(0)
|
||
const navBarHeight = ref(44)
|
||
const navbarTotalHeight = ref(88)
|
||
const categoryBarHeightPx = ref(40)
|
||
const headerPlaceholderHeight = ref(128)
|
||
|
||
const headerStyle = ref('')
|
||
const searchRowStyle = ref('')
|
||
const activeTopModule = ref('home')
|
||
const searchKeyword = ref('')
|
||
|
||
type HeaderModuleItem = {
|
||
key: string
|
||
label: string
|
||
}
|
||
|
||
const topModules: Array<HeaderModuleItem> = [
|
||
{ key: 'home', label: '首页' },
|
||
{ key: 'service', label: '服务' }
|
||
]
|
||
|
||
const headerSearchPlaceholder = computed((): string => {
|
||
if (activeTopModule.value == 'service') {
|
||
return '居家护理 / 康复照护 / 血压计 / 助餐服务'
|
||
}
|
||
return currentPlaceholderKeyword.value != '' ? currentPlaceholderKeyword.value : '血压计 / 血糖仪 / 制氧机 / 维生素'
|
||
})
|
||
|
||
type HomeCareCategoryType = {
|
||
id: string
|
||
name: string
|
||
iconText: string
|
||
color: string
|
||
}
|
||
|
||
type HomeCareServiceProductType = {
|
||
id: string
|
||
title: string
|
||
subtitle: string
|
||
categoryId: string
|
||
price: number
|
||
unit: string
|
||
tags: Array<string>
|
||
salesText: string
|
||
imageText: string
|
||
coverGradient: string
|
||
detailPath: string
|
||
bookingPath: string
|
||
}
|
||
|
||
type ServiceShortcutType = {
|
||
id: string
|
||
title: string
|
||
desc: string
|
||
iconText: string
|
||
}
|
||
|
||
const serviceCategories: Array<HomeCareCategoryType> = [
|
||
{ id: 'basic_care', name: '基础照护', iconText: '护', color: '#EAFBF7' },
|
||
{ id: 'rehab', name: '康复指导', iconText: '康', color: '#EFF6FF' },
|
||
{ id: 'escort', name: '陪诊服务', iconText: '陪', color: '#FFF7ED' },
|
||
{ id: 'nursing', name: '上门护理', iconText: '医', color: '#F0FDFA' },
|
||
{ id: 'chronic', name: '慢病随访', iconText: '访', color: '#F5F3FF' },
|
||
{ id: 'assessment', name: '健康评估', iconText: '评', color: '#ECFEFF' },
|
||
{ id: 'elderly', name: '适老改造', iconText: '老', color: '#FEF3C7' },
|
||
{ id: 'cleaning', name: '居家保洁', iconText: '洁', color: '#F1F5F9' },
|
||
{ id: 'medicine', name: '用药提醒', iconText: '药', color: '#FCE7F3' },
|
||
{ id: 'all', name: '全部服务', iconText: '全', color: '#F3F4F6' }
|
||
]
|
||
|
||
const serviceShortcuts: Array<ServiceShortcutType> = [
|
||
{ id: 'guarantee', title: '服务保障', desc: '认证机构与过程留痕', iconText: '保' },
|
||
{ id: 'hall', title: '服务大厅', desc: '查看全部居家服务', iconText: '厅' },
|
||
{ id: 'tracking', title: '服务单跟踪', desc: '预约进度随时可查', iconText: '单' }
|
||
]
|
||
|
||
const serviceLoading = ref(false)
|
||
const selectedServiceCategory = ref('all')
|
||
const allServiceProducts = ref<Array<HomeCareServiceProductType>>([])
|
||
|
||
const serviceProducts = computed((): Array<HomeCareServiceProductType> => {
|
||
if (selectedServiceCategory.value == 'all') {
|
||
return allServiceProducts.value
|
||
}
|
||
const result: Array<HomeCareServiceProductType> = []
|
||
for (let i = 0; i < allServiceProducts.value.length; i++) {
|
||
const item = allServiceProducts.value[i]
|
||
if (item.categoryId == selectedServiceCategory.value) {
|
||
result.push(item)
|
||
}
|
||
}
|
||
return result
|
||
})
|
||
|
||
const serviceSelectedCategoryLabel = computed((): string => {
|
||
for (let i = 0; i < serviceCategories.length; i++) {
|
||
if (serviceCategories[i].id == selectedServiceCategory.value) {
|
||
return serviceCategories[i].name
|
||
}
|
||
}
|
||
return '全部服务'
|
||
})
|
||
|
||
function getServiceGradient(categoryId: string): string {
|
||
if (categoryId == 'basic_care') {
|
||
return 'linear-gradient(135deg, #e0f7f5 0%, #f0fdfb 100%)'
|
||
}
|
||
if (categoryId == 'rehab') {
|
||
return 'linear-gradient(135deg, #e8f1ff 0%, #f5f9ff 100%)'
|
||
}
|
||
if (categoryId == 'escort') {
|
||
return 'linear-gradient(135deg, #fff1e4 0%, #fffaf5 100%)'
|
||
}
|
||
if (categoryId == 'nursing') {
|
||
return 'linear-gradient(135deg, #def7f3 0%, #eefcf9 100%)'
|
||
}
|
||
if (categoryId == 'chronic') {
|
||
return 'linear-gradient(135deg, #efe9ff 0%, #f8f5ff 100%)'
|
||
}
|
||
if (categoryId == 'assessment') {
|
||
return 'linear-gradient(135deg, #ebfbff 0%, #f6fdff 100%)'
|
||
}
|
||
return 'linear-gradient(135deg, #eef4f8 0%, #f7fafc 100%)'
|
||
}
|
||
|
||
function mapServiceCatalogCategory(category: string): string {
|
||
if (category == '日常照护') {
|
||
return 'basic_care'
|
||
}
|
||
if (category == '康复支持') {
|
||
return 'rehab'
|
||
}
|
||
if (category == '健康管理') {
|
||
return 'chronic'
|
||
}
|
||
return 'all'
|
||
}
|
||
|
||
function buildServiceSalesText(serviceId: string): string {
|
||
if (serviceId == 'svc-001') {
|
||
return '已服务230+'
|
||
}
|
||
if (serviceId == 'svc-002') {
|
||
return '已服务180+'
|
||
}
|
||
if (serviceId == 'svc-003') {
|
||
return '已服务150+'
|
||
}
|
||
return '已服务99+'
|
||
}
|
||
|
||
function buildServiceImageText(categoryId: string): string {
|
||
if (categoryId == 'basic_care') {
|
||
return '护'
|
||
}
|
||
if (categoryId == 'rehab') {
|
||
return '康'
|
||
}
|
||
if (categoryId == 'escort') {
|
||
return '陪'
|
||
}
|
||
if (categoryId == 'nursing') {
|
||
return '医'
|
||
}
|
||
if (categoryId == 'chronic') {
|
||
return '访'
|
||
}
|
||
if (categoryId == 'assessment') {
|
||
return '评'
|
||
}
|
||
return '服'
|
||
}
|
||
|
||
function buildServiceProductsFromCatalog(catalog: Array<HomeServiceCatalogType>): Array<HomeCareServiceProductType> {
|
||
const result: Array<HomeCareServiceProductType> = []
|
||
for (let i = 0; i < catalog.length; i++) {
|
||
const item = catalog[i]
|
||
const categoryId = mapServiceCatalogCategory(item.category)
|
||
result.push({
|
||
id: item.id,
|
||
title: item.name,
|
||
subtitle: item.summary,
|
||
categoryId,
|
||
price: item.price,
|
||
unit: '次',
|
||
tags: item.tags.length > 0 ? item.tags.slice(0, 2) : ['平台认证', '可预约'],
|
||
salesText: buildServiceSalesText(item.id),
|
||
imageText: buildServiceImageText(categoryId),
|
||
coverGradient: getServiceGradient(categoryId),
|
||
detailPath: '/pages/mall/consumer/home-service/service-detail?id=' + encodeURIComponent(item.id),
|
||
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=' + encodeURIComponent(item.id) + '&mode=booking'
|
||
} as HomeCareServiceProductType)
|
||
}
|
||
return result
|
||
}
|
||
|
||
async function loadServiceHomeData(): Promise<void> {
|
||
serviceLoading.value = true
|
||
try {
|
||
const catalog = await fetchHomeServiceCatalog()
|
||
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
|
||
} catch (error) {
|
||
console.error('加载服务首页数据失败', error)
|
||
allServiceProducts.value = [] as Array<HomeCareServiceProductType>
|
||
} finally {
|
||
serviceLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleServiceCategoryClick(item: HomeCareCategoryType): void {
|
||
selectedServiceCategory.value = item.id
|
||
if (item.id == 'all') {
|
||
return
|
||
}
|
||
let hasMatched = false
|
||
for (let i = 0; i < allServiceProducts.value.length; i++) {
|
||
if (allServiceProducts.value[i].categoryId == item.id) {
|
||
hasMatched = true
|
||
break
|
||
}
|
||
}
|
||
if (!hasMatched) {
|
||
uni.showToast({
|
||
title: '该类服务正在完善',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
function handleServiceShortcut(shortcutId: string): void {
|
||
if (shortcutId == 'hall') {
|
||
goServiceHall()
|
||
return
|
||
}
|
||
if (shortcutId == 'tracking') {
|
||
uni.navigateTo({ url: '/pages/mall/consumer/home-service/index' })
|
||
return
|
||
}
|
||
uni.showToast({
|
||
title: '服务保障体系建设中',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
function showServicePreview(item: HomeCareServiceProductType): void {
|
||
if (item.detailPath != '') {
|
||
uni.navigateTo({ url: item.detailPath })
|
||
return
|
||
}
|
||
uni.showToast({
|
||
title: '服务详情建设中',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
function goServiceDetail(item: HomeCareServiceProductType): void {
|
||
if (item.bookingPath != '') {
|
||
uni.navigateTo({ url: item.bookingPath })
|
||
return
|
||
}
|
||
uni.showToast({
|
||
title: '服务详情建设中',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
|
||
function goServiceHall(): void {
|
||
uni.navigateTo({ url: '/pages/mall/consumer/home-service/index' })
|
||
}
|
||
|
||
// 分类标签栏相关
|
||
type CategoryItem = {
|
||
id: string
|
||
name: string
|
||
}
|
||
|
||
const categoryList = ref<CategoryItem[]>([{ id: 'recommend', name: '推荐' }])
|
||
const currentCategory = ref('recommend')
|
||
const showCategoryPanel = ref(false)
|
||
const categoryScrollIntoView = ref('')
|
||
const secondaryCategoryDisplay = ref<Category[]>([])
|
||
const selectedSubCategoryId = ref('')
|
||
const marketingChannels = ref<MarketingChannel[]>([])
|
||
const categorySimpleChannels = ref<SimpleCategoryChannel[]>([])
|
||
const isNavigatingChannel = ref(false)
|
||
const currentFeedCategoryId = ref('recommend')
|
||
const failedProductImageIds = ref<string[]>([])
|
||
const hotProducts = ref<Product[]>([])
|
||
const recommendedProducts = ref<Product[]>([])
|
||
const hotKeywords = ref<string[]>([])
|
||
const defaultLoadLimit: number = 6
|
||
const recommendChannelLoadLimit: number = 16
|
||
const categoryChannelLoadLimit: number = 12
|
||
|
||
// 屏幕尺寸检测
|
||
const isMobile = ref(false)
|
||
const showLoadMore = ref(false)
|
||
|
||
// 导航栏显示控制
|
||
const showNavbar = ref(true)
|
||
const lastScrollTop = ref(0)
|
||
const scrollThreshold = 30 // 降低滚动阈值,使其更灵敏
|
||
const scrollingUp = ref(false)
|
||
|
||
// 分类数据 - 从Supabase获取
|
||
const categoryTab = ref<string>('category')
|
||
const categories = ref<Category[]>([])
|
||
const brands = ref<Brand[]>([])
|
||
|
||
// 一级分类和二级分类
|
||
const parentCategories = ref<Category[]>([])
|
||
const subCategories = ref<Category[]>([])
|
||
const selectedParentCategory = ref<Category | null>(null)
|
||
const showSubCategories = ref(false)
|
||
|
||
// 首页占位词轮播相关
|
||
const placeholderKeywords = ref([
|
||
'血压计',
|
||
'血糖仪',
|
||
'体温计',
|
||
'制氧机',
|
||
'口罩',
|
||
'创可贴',
|
||
'维生素',
|
||
'钙片',
|
||
'康复护具',
|
||
'护理床',
|
||
|
||
])
|
||
const currentPlaceholderIndex = ref(0)
|
||
const currentPlaceholderKeyword = ref(placeholderKeywords.value.length > 0 ? placeholderKeywords.value[0] : '')
|
||
const nextPlaceholderKeyword = ref(placeholderKeywords.value.length > 1 ? placeholderKeywords.value[1] : currentPlaceholderKeyword.value)
|
||
const placeholderAnimating = ref(false)
|
||
let placeholderTimer: number = 0
|
||
let placeholderAnimationTimer: number = 0
|
||
let skeletonTimer: number = 0
|
||
|
||
const showHomeSkeleton = computed((): boolean => {
|
||
return pageLoading.value || skeletonExiting.value
|
||
})
|
||
|
||
const homeEmptyStateTitle = computed((): string => {
|
||
if (currentFeedCategoryId.value == 'recommend') {
|
||
return '康养医疗商品正在完善中'
|
||
}
|
||
return '当前分类暂无商品'
|
||
})
|
||
|
||
const homeEmptyStateDescription = computed((): string => {
|
||
if (currentFeedCategoryId.value == 'recommend') {
|
||
return '可先查看居家服务或稍后再试'
|
||
}
|
||
return ''
|
||
})
|
||
|
||
const syncNextPlaceholderKeyword = () => {
|
||
const total = placeholderKeywords.value.length
|
||
if (total <= 1) {
|
||
nextPlaceholderKeyword.value = currentPlaceholderKeyword.value
|
||
return
|
||
}
|
||
const nextIndex = (currentPlaceholderIndex.value + 1) % total
|
||
nextPlaceholderKeyword.value = placeholderKeywords.value[nextIndex]
|
||
}
|
||
|
||
const playNextPlaceholderAnimation = () => {
|
||
const total = placeholderKeywords.value.length
|
||
if (total <= 1 || placeholderAnimating.value) {
|
||
return
|
||
}
|
||
const nextIndex = (currentPlaceholderIndex.value + 1) % total
|
||
nextPlaceholderKeyword.value = placeholderKeywords.value[nextIndex]
|
||
placeholderAnimating.value = true
|
||
if (placeholderAnimationTimer != 0) {
|
||
clearTimeout(placeholderAnimationTimer)
|
||
placeholderAnimationTimer = 0
|
||
}
|
||
placeholderAnimationTimer = setTimeout(() => {
|
||
currentPlaceholderIndex.value = nextIndex
|
||
currentPlaceholderKeyword.value = placeholderKeywords.value[nextIndex]
|
||
placeholderAnimating.value = false
|
||
syncNextPlaceholderKeyword()
|
||
placeholderAnimationTimer = 0
|
||
}, 320)
|
||
}
|
||
|
||
function stopPlaceholderScroll(): void {
|
||
try {
|
||
if (placeholderTimer != 0) {
|
||
clearInterval(placeholderTimer)
|
||
placeholderTimer = 0
|
||
}
|
||
if (placeholderAnimationTimer != 0) {
|
||
clearTimeout(placeholderAnimationTimer)
|
||
placeholderAnimationTimer = 0
|
||
}
|
||
placeholderAnimating.value = false
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function resetInitialHomeLoadFlags(): void {
|
||
goodsLoaded.value = false
|
||
categoryLoaded.value = false
|
||
bannerLoaded.value = false
|
||
}
|
||
|
||
function clearSkeletonTimer(): void {
|
||
if (skeletonTimer != 0) {
|
||
clearTimeout(skeletonTimer)
|
||
skeletonTimer = 0
|
||
}
|
||
}
|
||
|
||
function beginHomeSkeleton(): void {
|
||
clearSkeletonTimer()
|
||
pageLoading.value = true
|
||
skeletonExiting.value = false
|
||
}
|
||
|
||
function finishHomeSkeleton(): void {
|
||
if (!pageLoading.value) {
|
||
return
|
||
}
|
||
pageLoading.value = false
|
||
skeletonExiting.value = true
|
||
clearSkeletonTimer()
|
||
skeletonTimer = setTimeout(() => {
|
||
skeletonExiting.value = false
|
||
skeletonTimer = 0
|
||
}, 280)
|
||
}
|
||
|
||
async function loadHomeBannerData(): Promise<void> {
|
||
return
|
||
}
|
||
|
||
function startPlaceholderScroll(): void {
|
||
try {
|
||
if (placeholderTimer != 0) {
|
||
clearInterval(placeholderTimer)
|
||
placeholderTimer = 0
|
||
}
|
||
if (placeholderAnimationTimer != 0) {
|
||
clearTimeout(placeholderAnimationTimer)
|
||
placeholderAnimationTimer = 0
|
||
}
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
currentPlaceholderIndex.value = 0
|
||
currentPlaceholderKeyword.value = placeholderKeywords.value.length > 0 ? placeholderKeywords.value[0] : ''
|
||
placeholderAnimating.value = false
|
||
syncNextPlaceholderKeyword()
|
||
if (placeholderKeywords.value.length <= 1) {
|
||
return
|
||
}
|
||
placeholderTimer = setInterval(() => {
|
||
playNextPlaceholderAnimation()
|
||
}, 2500)
|
||
}
|
||
|
||
const handleHomeSearchClick = () => {
|
||
const kw = currentPlaceholderKeyword.value != null ? currentPlaceholderKeyword.value : ''
|
||
try {
|
||
uni.navigateTo({ url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(kw)}&source=placeholder` })
|
||
} catch (e) {
|
||
console.error('跳转搜索页失败', e)
|
||
}
|
||
}
|
||
|
||
function handleSearchKeywordUpdate(keyword: string) {
|
||
searchKeyword.value = keyword
|
||
}
|
||
|
||
function handleSearchFocus() {
|
||
if (searchKeyword.value == '' && activeTopModule.value == 'home') {
|
||
searchKeyword.value = currentPlaceholderKeyword.value
|
||
}
|
||
}
|
||
|
||
function handleHeaderSearch(keyword: string) {
|
||
const inputKeyword = keyword != '' ? keyword : searchKeyword.value
|
||
const fallbackKeyword = headerSearchPlaceholder.value != '' ? headerSearchPlaceholder.value : '居家护理'
|
||
const finalKeyword = inputKeyword != '' ? inputKeyword : fallbackKeyword
|
||
searchKeyword.value = finalKeyword
|
||
try {
|
||
uni.navigateTo({ url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(finalKeyword)}&source=home_header` })
|
||
} catch (error) {
|
||
console.error('首页搜索跳转失败', error)
|
||
}
|
||
}
|
||
|
||
function handleTopModuleChange(moduleKey: string) {
|
||
activeTopModule.value = moduleKey
|
||
showCategoryPanel.value = false
|
||
if (moduleKey == 'service' && allServiceProducts.value.length == 0 && !serviceLoading.value) {
|
||
void loadServiceHomeData()
|
||
}
|
||
}
|
||
|
||
function handleMainScrollToLower() {
|
||
if (activeTopModule.value == 'home') {
|
||
void loadMore()
|
||
}
|
||
}
|
||
|
||
function toEventJsonObject(value: any | null): UTSJSONObject | null {
|
||
if (value == null) {
|
||
return null
|
||
}
|
||
if (value instanceof UTSJSONObject) {
|
||
return value as UTSJSONObject
|
||
}
|
||
const raw = JSON.stringify(value)
|
||
if (raw == '' || raw == 'null') {
|
||
return null
|
||
}
|
||
const parsed = JSON.parse(raw)
|
||
if (parsed == null) {
|
||
return null
|
||
}
|
||
return parsed as UTSJSONObject
|
||
}
|
||
|
||
function getEventDetailObject(event: any | null): UTSJSONObject | null {
|
||
const eventObj = toEventJsonObject(event)
|
||
if (eventObj == null) {
|
||
return null
|
||
}
|
||
return toEventJsonObject(eventObj.get('detail'))
|
||
}
|
||
|
||
// 同步 swiper 占位词索引到 currentPlaceholderKeyword
|
||
const handleKeywordChange = (e: any) => {
|
||
try {
|
||
const detail = getEventDetailObject(e)
|
||
if (detail == null) return
|
||
const nextIndex = detail.getNumber('current') ?? 0
|
||
if (nextIndex >= 0 && nextIndex < placeholderKeywords.value.length) {
|
||
currentPlaceholderIndex.value = nextIndex
|
||
currentPlaceholderKeyword.value = placeholderKeywords.value[nextIndex]
|
||
placeholderAnimating.value = false
|
||
syncNextPlaceholderKeyword()
|
||
}
|
||
} catch (err) {
|
||
currentPlaceholderIndex.value = 0
|
||
currentPlaceholderKeyword.value = placeholderKeywords.value.length > 0 ? placeholderKeywords.value[0] : ''
|
||
placeholderAnimating.value = false
|
||
syncNextPlaceholderKeyword()
|
||
}
|
||
}
|
||
|
||
const toggleCategoryPanel = (): void => {
|
||
if (activeTopModule.value != 'home') {
|
||
return
|
||
}
|
||
showCategoryPanel.value = !showCategoryPanel.value
|
||
}
|
||
|
||
const isImageIcon = (icon: string): boolean => {
|
||
if (icon == null || icon === '') return false
|
||
return icon.indexOf('http') === 0 || icon.indexOf('/') === 0
|
||
}
|
||
|
||
function resolveCategoryDisplayIcon(category: Category): string {
|
||
const icon = category.icon ?? ''
|
||
if (icon !== '' && !isImageIcon(icon)) {
|
||
return icon
|
||
}
|
||
const name = category.name ?? ''
|
||
if (name !== '') {
|
||
return name.substring(0, 1)
|
||
}
|
||
return '品'
|
||
}
|
||
|
||
const getProductCover = (product: Product): string => {
|
||
if (failedProductImageIds.value.indexOf(product.id) !== -1) {
|
||
return '/static/images/default.png'
|
||
}
|
||
if (product.main_image_url != null && product.main_image_url !== '') {
|
||
return product.main_image_url
|
||
}
|
||
if (product.images != null && product.images.length > 0 && product.images[0] !== '') {
|
||
return product.images[0]
|
||
}
|
||
if (product.image_url != null && product.image_url !== '') {
|
||
return product.image_url
|
||
}
|
||
return '/static/images/default.png'
|
||
}
|
||
|
||
const handleProductImageError = (productId: string): void => {
|
||
if (productId === '') {
|
||
return
|
||
}
|
||
if (failedProductImageIds.value.indexOf(productId) === -1) {
|
||
failedProductImageIds.value.push(productId)
|
||
}
|
||
}
|
||
|
||
const getProductTitle = (product: Product): string => {
|
||
if (product.short_title != null && product.short_title !== '') {
|
||
return product.short_title
|
||
}
|
||
if (product.name != null && product.name !== '') {
|
||
return product.name
|
||
}
|
||
return product.id
|
||
}
|
||
|
||
function getSimpleChannelCoverToken(category: Category): string {
|
||
const icon = category.icon ?? ''
|
||
if (icon !== '') {
|
||
return icon
|
||
}
|
||
const name = category.name ?? ''
|
||
if (name !== '') {
|
||
return name.substring(0, 1)
|
||
}
|
||
return '品'
|
||
}
|
||
|
||
function buildSimpleChannelCoverImages(startIndex: number): string[] {
|
||
const covers: string[] = []
|
||
for (let i = startIndex; i < secondaryCategoryDisplay.value.length && covers.length < 2; i++) {
|
||
covers.push(getSimpleChannelCoverToken(secondaryCategoryDisplay.value[i]))
|
||
}
|
||
while (covers.length < 2) {
|
||
covers.push('/static/images/default.png')
|
||
}
|
||
return covers
|
||
}
|
||
|
||
function getRealProductImage(product: Product): string {
|
||
if (product.main_image_url != null && product.main_image_url !== '') {
|
||
return product.main_image_url
|
||
}
|
||
if (product.images != null && product.images.length > 0 && product.images[0] !== '') {
|
||
return product.images[0]
|
||
}
|
||
if (product.image_url != null && product.image_url !== '') {
|
||
return product.image_url
|
||
}
|
||
return '/static/images/default.png'
|
||
}
|
||
|
||
function getRealSalePrice(product: Product): number {
|
||
return product.base_price ?? product.price ?? 0
|
||
}
|
||
|
||
|
||
function getRealMarketPrice(product: Product): number {
|
||
return product.market_price ?? product.original_price ?? 0
|
||
}
|
||
|
||
function toChannelProduct(product: Product, labelPrefix: string): ChannelProduct {
|
||
const salePrice = getRealSalePrice(product)
|
||
const marketPrice = getRealMarketPrice(product)
|
||
const shortName = product.short_title != null && product.short_title !== ''
|
||
? product.short_title
|
||
: (product.name != null && product.name !== '' ? product.name : product.id)
|
||
return {
|
||
id: product.id,
|
||
name: product.name != null && product.name !== '' ? product.name : product.id,
|
||
shortName,
|
||
image: getRealProductImage(product),
|
||
price: salePrice,
|
||
marketPrice,
|
||
tag: labelPrefix
|
||
} as ChannelProduct
|
||
}
|
||
|
||
function getProductDiscountScore(product: Product): number {
|
||
const salePrice = getRealSalePrice(product)
|
||
const marketPrice = getRealMarketPrice(product)
|
||
if (marketPrice <= salePrice || marketPrice <= 0) {
|
||
return 0
|
||
}
|
||
const discountValue = marketPrice - salePrice
|
||
const discountRate = discountValue / marketPrice
|
||
return discountRate * 100000 + discountValue
|
||
}
|
||
|
||
function getProductQualityScore(product: Product): number {
|
||
let score = 0
|
||
if (product.is_featured == true) {
|
||
score = score + 100000
|
||
}
|
||
if (product.is_hot == true) {
|
||
score = score + 50000
|
||
}
|
||
score = score + (product.sale_count ?? 0)
|
||
return score
|
||
}
|
||
|
||
function getProductHotScore(product: Product): number {
|
||
let score = product.sale_count ?? 0
|
||
if (product.is_hot == true) {
|
||
score = score + 100000
|
||
}
|
||
if (product.is_featured == true) {
|
||
score = score + 50000
|
||
}
|
||
score = score + getProductDiscountScore(product)
|
||
return score
|
||
}
|
||
|
||
function cloneProductArray(source: Array<Product>): Array<Product> {
|
||
const result: Array<Product> = []
|
||
for (let i = 0; i < source.length; i++) {
|
||
result.push(source[i])
|
||
}
|
||
return result
|
||
}
|
||
|
||
function sortProductsByScoreDesc(source: Array<Product>, scoreType: string): Array<Product> {
|
||
const result = cloneProductArray(source)
|
||
for (let i = 0; i < result.length; i++) {
|
||
for (let j = i + 1; j < result.length; j++) {
|
||
let leftScore = 0
|
||
let rightScore = 0
|
||
if (scoreType == 'discount') {
|
||
leftScore = getProductDiscountScore(result[i])
|
||
rightScore = getProductDiscountScore(result[j])
|
||
} else if (scoreType == 'quality') {
|
||
leftScore = getProductQualityScore(result[i])
|
||
rightScore = getProductQualityScore(result[j])
|
||
} else {
|
||
leftScore = getProductHotScore(result[i])
|
||
rightScore = getProductHotScore(result[j])
|
||
}
|
||
if (rightScore > leftScore) {
|
||
const temp = result[i]
|
||
result[i] = result[j]
|
||
result[j] = temp
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
function sortProductsByPriceAsc(source: Array<Product>): Array<Product> {
|
||
const result = cloneProductArray(source)
|
||
for (let i = 0; i < result.length; i++) {
|
||
for (let j = i + 1; j < result.length; j++) {
|
||
const leftPrice = getRealSalePrice(result[i])
|
||
const rightPrice = getRealSalePrice(result[j])
|
||
if (rightPrice < leftPrice) {
|
||
const temp = result[i]
|
||
result[i] = result[j]
|
||
result[j] = temp
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
function filterProductsByMode(source: Array<Product>, mode: string): Array<Product> {
|
||
const result: Array<Product> = []
|
||
for (let i = 0; i < source.length; i++) {
|
||
const item = source[i]
|
||
const salePrice = getRealSalePrice(item)
|
||
const marketPrice = getRealMarketPrice(item)
|
||
if (mode == 'discount' && marketPrice > salePrice) {
|
||
result.push(item)
|
||
continue
|
||
}
|
||
if (mode == 'quality' && (item.is_featured == true || item.is_hot == true)) {
|
||
result.push(item)
|
||
continue
|
||
}
|
||
if (mode == 'cheap-9' && salePrice > 0 && salePrice <= 9.9) {
|
||
result.push(item)
|
||
continue
|
||
}
|
||
if (mode == 'cheap-19' && salePrice > 0 && salePrice <= 19.9) {
|
||
result.push(item)
|
||
continue
|
||
}
|
||
if (mode == 'live' && (item.is_hot == true || (item.sale_count ?? 0) > 0)) {
|
||
result.push(item)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
function mergeUniqueProductLists(first: Array<Product>, second: Array<Product>, third: Array<Product>): Array<Product> {
|
||
const result: Array<Product> = []
|
||
const seenIds: Array<string> = []
|
||
const sources: Array<Array<Product>> = [first, second, third]
|
||
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
|
||
const source = sources[sourceIndex]
|
||
for (let i = 0; i < source.length; i++) {
|
||
const item = source[i]
|
||
const productId = item.id ?? ''
|
||
if (productId != '' && seenIds.indexOf(productId) != -1) {
|
||
continue
|
||
}
|
||
if (productId != '') {
|
||
seenIds.push(productId)
|
||
}
|
||
result.push(item)
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
function appendChannelProducts(source: Array<Product>, result: Array<Product>, selectedIds: Array<string>, desiredCount: number, allowRepeat: boolean): void {
|
||
for (let i = 0; i < source.length; i++) {
|
||
if (result.length >= desiredCount) {
|
||
return
|
||
}
|
||
const item = source[i]
|
||
const productId = item.id ?? ''
|
||
let existsInResult = false
|
||
for (let j = 0; j < result.length; j++) {
|
||
if (result[j].id == productId) {
|
||
existsInResult = true
|
||
break
|
||
}
|
||
}
|
||
if (existsInResult) {
|
||
continue
|
||
}
|
||
if (!allowRepeat && productId != '' && selectedIds.indexOf(productId) != -1) {
|
||
continue
|
||
}
|
||
result.push(item)
|
||
if (!allowRepeat && productId != '') {
|
||
selectedIds.push(productId)
|
||
}
|
||
}
|
||
}
|
||
|
||
function selectChannelProducts(primary: Array<Product>, secondary: Array<Product>, fallback: Array<Product>, selectedIds: Array<string>, desiredCount: number): Array<Product> {
|
||
const result: Array<Product> = []
|
||
appendChannelProducts(primary, result, selectedIds, desiredCount, false)
|
||
appendChannelProducts(secondary, result, selectedIds, desiredCount, false)
|
||
appendChannelProducts(fallback, result, selectedIds, desiredCount, false)
|
||
appendChannelProducts(primary, result, selectedIds, desiredCount, true)
|
||
appendChannelProducts(secondary, result, selectedIds, desiredCount, true)
|
||
appendChannelProducts(fallback, result, selectedIds, desiredCount, true)
|
||
return result
|
||
}
|
||
|
||
function buildChannelFromTemplate(template: MarketingChannel, products: Array<Product>, labelPrefix: string): MarketingChannel {
|
||
const mappedProducts: Array<ChannelProduct> = []
|
||
for (let i = 0; i < products.length; i++) {
|
||
mappedProducts.push(toChannelProduct(products[i], labelPrefix))
|
||
}
|
||
return {
|
||
id: template.id,
|
||
title: template.title,
|
||
subtitle: template.subtitle,
|
||
badge: template.badge,
|
||
themeColor: template.themeColor,
|
||
bgColor: template.bgColor,
|
||
routeType: template.routeType,
|
||
layoutType: template.layoutType,
|
||
products: mappedProducts,
|
||
moreProducts: mappedProducts
|
||
} as MarketingChannel
|
||
}
|
||
|
||
function logChannelProducts(channelTitle: string, products: Array<Product>): void {
|
||
for (let i = 0; i < products.length; i++) {
|
||
const item = products[i]
|
||
console.log('[home-channel] ' + channelTitle + ' product:', item.id, item.name ?? '', getRealProductImage(item), getRealSalePrice(item), getRealMarketPrice(item))
|
||
}
|
||
}
|
||
|
||
function buildRealRecommendMarketingChannels(products: Array<Product>): MarketingChannel[] {
|
||
console.log('[home-channel] buildRealRecommendMarketingChannels input count:', products.length)
|
||
const templates = getRecommendMarketingChannels()
|
||
if (products.length == 0 || templates.length == 0) {
|
||
console.log('[home-channel] fallback to mock channel data')
|
||
return templates
|
||
}
|
||
const uniqueProducts = dedupeProducts(products)
|
||
if (uniqueProducts.length == 0) {
|
||
console.log('[home-channel] fallback to mock channel data')
|
||
return templates
|
||
}
|
||
const selectedIds: Array<string> = []
|
||
const discountCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'discount'), 'discount')
|
||
const qualityCandidates = sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'quality'), 'quality')
|
||
const cheapCandidates = mergeUniqueProductLists(
|
||
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-9')),
|
||
sortProductsByPriceAsc(filterProductsByMode(uniqueProducts, 'cheap-19')),
|
||
sortProductsByPriceAsc(uniqueProducts)
|
||
)
|
||
const liveCandidates = mergeUniqueProductLists(
|
||
sortProductsByScoreDesc(filterProductsByMode(uniqueProducts, 'live'), 'hot'),
|
||
sortProductsByScoreDesc(discountCandidates, 'discount'),
|
||
sortProductsByScoreDesc(uniqueProducts, 'hot')
|
||
)
|
||
const hotFallback = sortProductsByScoreDesc(uniqueProducts, 'hot')
|
||
const cheapFallback = sortProductsByPriceAsc(uniqueProducts)
|
||
|
||
const subsidyProducts = selectChannelProducts(discountCandidates, hotFallback, hotFallback, selectedIds, 2)
|
||
const qualityProducts = selectChannelProducts(qualityCandidates, hotFallback, hotFallback, selectedIds, 2)
|
||
const cheapProducts = selectChannelProducts(cheapCandidates, cheapFallback, hotFallback, selectedIds, 2)
|
||
const liveProducts = selectChannelProducts(liveCandidates, discountCandidates, hotFallback, selectedIds, 2)
|
||
|
||
logChannelProducts('百亿补贴', subsidyProducts)
|
||
logChannelProducts('品质生活', qualityProducts)
|
||
logChannelProducts('9.9包邮', cheapProducts)
|
||
logChannelProducts('直播低价', liveProducts)
|
||
|
||
const mappedChannels: Array<MarketingChannel> = []
|
||
for (let i = 0; i < templates.length; i++) {
|
||
const template = templates[i]
|
||
if (template.id == 'subsidy') {
|
||
mappedChannels.push(buildChannelFromTemplate(template, subsidyProducts, '补贴价'))
|
||
continue
|
||
}
|
||
if (template.id == 'quality-life') {
|
||
mappedChannels.push(buildChannelFromTemplate(template, qualityProducts, '实惠'))
|
||
continue
|
||
}
|
||
if (template.id == 'cheap-mail') {
|
||
const cheapMappedProducts: Array<ChannelProduct> = []
|
||
for (let j = 0; j < cheapProducts.length; j++) {
|
||
const cheapProduct = cheapProducts[j]
|
||
const label = getRealSalePrice(cheapProduct) <= 9.9 ? '9.9包邮' : '特价'
|
||
cheapMappedProducts.push(toChannelProduct(cheapProduct, label))
|
||
}
|
||
mappedChannels.push({
|
||
id: template.id,
|
||
title: template.title,
|
||
subtitle: template.subtitle,
|
||
badge: template.badge,
|
||
themeColor: template.themeColor,
|
||
bgColor: template.bgColor,
|
||
routeType: template.routeType,
|
||
layoutType: template.layoutType,
|
||
products: cheapMappedProducts,
|
||
moreProducts: cheapMappedProducts
|
||
} as MarketingChannel)
|
||
continue
|
||
}
|
||
if (template.id == 'live-low-price') {
|
||
mappedChannels.push(buildChannelFromTemplate(template, liveProducts, '直播价'))
|
||
continue
|
||
}
|
||
mappedChannels.push(template)
|
||
}
|
||
return mappedChannels
|
||
}
|
||
|
||
function buildSimpleCategoryChannels(categoryId: string, products: Array<Product> = []): SimpleCategoryChannel[] {
|
||
const dedupedProducts = dedupeProducts(products)
|
||
if (dedupedProducts.length == 0) {
|
||
return [] as Array<SimpleCategoryChannel>
|
||
}
|
||
const hotProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'hot')
|
||
const qualityProductsForCategory = sortProductsByScoreDesc(dedupedProducts, 'quality')
|
||
const firstChannelCovers: Array<string> = []
|
||
const secondChannelCovers: Array<string> = []
|
||
for (let i = 0; i < hotProductsForCategory.length && firstChannelCovers.length < 2; i++) {
|
||
firstChannelCovers.push(getRealProductImage(hotProductsForCategory[i]))
|
||
}
|
||
for (let i = 0; i < qualityProductsForCategory.length && secondChannelCovers.length < 2; i++) {
|
||
secondChannelCovers.push(getRealProductImage(qualityProductsForCategory[i]))
|
||
}
|
||
while (firstChannelCovers.length < 2) {
|
||
firstChannelCovers.push('/static/images/default.png')
|
||
}
|
||
while (secondChannelCovers.length < 2) {
|
||
secondChannelCovers.push('/static/images/default.png')
|
||
}
|
||
return [
|
||
{
|
||
id: categoryId + '-rank',
|
||
title: '热销榜',
|
||
subtitle: '真实商品热度精选',
|
||
routeType: 'rank',
|
||
icon: '热',
|
||
coverImages: firstChannelCovers,
|
||
categoryId
|
||
} as SimpleCategoryChannel,
|
||
{
|
||
id: categoryId + '-quality',
|
||
title: '品质优选',
|
||
subtitle: '真实好物口碑推荐',
|
||
routeType: 'quality',
|
||
icon: '精',
|
||
coverImages: secondChannelCovers,
|
||
categoryId
|
||
} as SimpleCategoryChannel
|
||
]
|
||
}
|
||
|
||
async function loadCategoryChannelCards(categoryId: string): Promise<void> {
|
||
try {
|
||
const channelResult = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, categoryChannelLoadLimit)
|
||
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId, channelResult.data)
|
||
} catch (error) {
|
||
console.error('[home-channel] 加载分类频道卡片失败', categoryId, error)
|
||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||
}
|
||
}
|
||
|
||
function buildVisibleRecommendChannels(): MarketingChannel[] {
|
||
const source = getRecommendMarketingChannels()
|
||
const visible: MarketingChannel[] = []
|
||
for (let i = 0; i < source.length; i++) {
|
||
const channel = source[i]
|
||
if (channel.title == '排行榜' || channel.title == '品质优选') {
|
||
continue
|
||
}
|
||
visible.push(channel)
|
||
}
|
||
return visible
|
||
}
|
||
|
||
function applyChannelDisplay(categoryId: string): void {
|
||
if (categoryId === 'recommend') {
|
||
marketingChannels.value = [] as Array<MarketingChannel>
|
||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||
return
|
||
}
|
||
marketingChannels.value = [] as Array<MarketingChannel>
|
||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||
}
|
||
|
||
function buildChannelDetailUrl(channelId: string, routeType: string, categoryId: string): string {
|
||
return '/pages/mall/consumer/channel-detail'
|
||
+ '?channelId=' + encodeURIComponent(channelId)
|
||
+ '&routeType=' + encodeURIComponent(routeType)
|
||
+ '&categoryId=' + encodeURIComponent(categoryId)
|
||
}
|
||
|
||
function safeNavigateTo(url: string): void {
|
||
let pageStackLength = 0
|
||
try {
|
||
const pages = getCurrentPages()
|
||
pageStackLength = pages.length
|
||
} catch (error) {
|
||
console.error('读取页面栈失败', error)
|
||
}
|
||
|
||
const redirectSafely = (): void => {
|
||
uni.redirectTo({
|
||
url,
|
||
success: () => {
|
||
console.log('channel redirectTo success', url)
|
||
},
|
||
fail: (error) => {
|
||
console.error('channel redirectTo fail', url, error)
|
||
uni.showToast({ title: '页面打开失败', icon: 'none' })
|
||
},
|
||
complete: () => {
|
||
isNavigatingChannel.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
if (pageStackLength >= 9) {
|
||
console.log('channel redirectTo fallback by stack length', pageStackLength, url)
|
||
redirectSafely()
|
||
return
|
||
}
|
||
|
||
let fallbackTriggered = false
|
||
uni.navigateTo({
|
||
url,
|
||
success: () => {
|
||
console.log('channel navigateTo success', url)
|
||
},
|
||
fail: (error) => {
|
||
fallbackTriggered = true
|
||
console.error('channel navigateTo fail', url, error)
|
||
redirectSafely()
|
||
},
|
||
complete: () => {
|
||
if (!fallbackTriggered) {
|
||
isNavigatingChannel.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
const getProductCardTags = (product: Product): string[] => {
|
||
if (product.card_tags != null && product.card_tags.length > 0) {
|
||
return product.card_tags.slice(0, 2)
|
||
}
|
||
const fallback: string[] = []
|
||
if (product.is_hot === true) fallback.push('热卖')
|
||
if (product.is_new === true && fallback.length < 2) fallback.push('新品')
|
||
if (product.is_featured === true && fallback.length < 2) fallback.push('精选')
|
||
return fallback
|
||
}
|
||
|
||
const getProductServiceTags = (product: Product): string[] => {
|
||
if (product.service_tags != null && product.service_tags.length > 0) {
|
||
return product.service_tags.slice(0, 3)
|
||
}
|
||
return [] as string[]
|
||
}
|
||
|
||
const getProductHighlight = (product: Product): string => {
|
||
if (product.selling_points != null && product.selling_points.length > 0 && product.selling_points[0] !== '') {
|
||
return product.selling_points[0]
|
||
}
|
||
if (product.subtitle != null && product.subtitle !== '') {
|
||
return product.subtitle
|
||
}
|
||
return ''
|
||
}
|
||
|
||
const formatSaleCountText = (saleCount: number): string => {
|
||
if (saleCount >= 100000) return '已售10万+'
|
||
if (saleCount >= 10000) return '已售' + (saleCount / 10000).toFixed(1) + '万件'
|
||
if (saleCount > 0) return '已售' + saleCount.toString() + '件'
|
||
return ''
|
||
}
|
||
|
||
const getProductSalesText = (product: Product): string => {
|
||
if (product.display_sales_text != null && product.display_sales_text !== '') {
|
||
return product.display_sales_text
|
||
}
|
||
const saleCount = product.sale_count ?? 0
|
||
return formatSaleCountText(saleCount)
|
||
}
|
||
|
||
const formatProductPrice = (product: Product): string => {
|
||
const price = product.base_price ?? product.price ?? 0
|
||
return parseFloat(price.toString()).toFixed(2)
|
||
}
|
||
|
||
const formatMarketPrice = (product: Product): string => {
|
||
const price = product.market_price ?? product.original_price ?? 0
|
||
return parseFloat(price.toString()).toFixed(2)
|
||
}
|
||
|
||
const showMarketPrice = (product: Product): boolean => {
|
||
const marketPrice = product.market_price ?? product.original_price ?? 0
|
||
const salePrice = product.base_price ?? product.price ?? 0
|
||
return marketPrice > 0 && marketPrice > salePrice
|
||
}
|
||
|
||
const buildSecondaryCategoryDisplay = (categoryId: string): Category[] => {
|
||
if (categoryId === 'recommend') {
|
||
return parentCategories.value.slice(0, 10)
|
||
}
|
||
if (subCategories.value.length > 0) {
|
||
return subCategories.value.slice(0, 10)
|
||
}
|
||
const fallbackParent = parentCategories.value.find((item: Category): boolean => item.id === categoryId)
|
||
if (fallbackParent != null) {
|
||
return [fallbackParent]
|
||
}
|
||
return []
|
||
}
|
||
|
||
const buildMarketingChannels = (categoryId: string): MarketingChannel[] => {
|
||
if (categoryId === 'recommend') {
|
||
return getRecommendMarketingChannels()
|
||
}
|
||
return []
|
||
}
|
||
|
||
function navigateToChannel(channel: MarketingChannel): void {
|
||
if (isNavigatingChannel.value) {
|
||
return
|
||
}
|
||
isNavigatingChannel.value = true
|
||
const categoryId = currentCategory.value
|
||
safeNavigateTo(buildChannelDetailUrl(channel.id, channel.routeType, categoryId))
|
||
}
|
||
|
||
function navigateToSimpleChannel(channel: SimpleCategoryChannel): void {
|
||
if (isNavigatingChannel.value) {
|
||
return
|
||
}
|
||
isNavigatingChannel.value = true
|
||
safeNavigateTo(buildChannelDetailUrl(channel.id, channel.routeType, channel.categoryId))
|
||
}
|
||
|
||
function dedupeProducts(products: Product[]): Product[] {
|
||
const deduped: Product[] = []
|
||
const seenIds: string[] = []
|
||
for (let i = 0; i < products.length; i++) {
|
||
const product = products[i]
|
||
const productId = product.id ?? ''
|
||
if (productId !== '') {
|
||
if (seenIds.indexOf(productId) !== -1) {
|
||
continue
|
||
}
|
||
seenIds.push(productId)
|
||
}
|
||
deduped.push(product)
|
||
}
|
||
return deduped
|
||
}
|
||
|
||
function setHotProducts(products: Product[]): void {
|
||
hotProducts.value = dedupeProducts(products)
|
||
}
|
||
|
||
function appendHotProducts(products: Product[]): void {
|
||
const merged = hotProducts.value.concat(products)
|
||
hotProducts.value = dedupeProducts(merged)
|
||
}
|
||
|
||
const handleSecondaryCategoryClick = async (category: Category): Promise<void> => {
|
||
selectedSubCategoryId.value = category.id
|
||
currentFeedCategoryId.value = category.id
|
||
currentPage.value = 1
|
||
hasMore.value = true
|
||
loading.value = true
|
||
try {
|
||
const result = await supabaseService.getMedicalMallProductsByCategory(category.id, 1, defaultLoadLimit)
|
||
failedProductImageIds.value = []
|
||
setHotProducts(result.data)
|
||
hasMore.value = result.hasmore
|
||
} catch (e) {
|
||
console.error('二级分类商品加载失败', e)
|
||
hotProducts.value = []
|
||
hasMore.value = false
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
type SortTab = {
|
||
id: string
|
||
name: string
|
||
}
|
||
|
||
// 排序标签
|
||
const sortTabs: SortTab[] = [
|
||
{ id: 'recommend', name: '智能推荐' },
|
||
{ id: 'sales', name: '销量' },
|
||
{ id: 'price', name: '价格' },
|
||
{ id: 'new', name: '新品' },
|
||
{ id: 'discount', name: '特价' }
|
||
]
|
||
|
||
|
||
// 健康资讯
|
||
const healthNews = [
|
||
{
|
||
id: 'news1',
|
||
title: '秋季流感预防指南,科学防护健康过冬',
|
||
tag: '健康科普',
|
||
image: '/static/images/default.png'
|
||
},
|
||
{
|
||
id: 'news2',
|
||
title: '家庭常备药清单,为家人健康保驾护航',
|
||
tag: '家庭用药',
|
||
image: '/static/images/default.png'
|
||
},
|
||
{
|
||
id: 'news3',
|
||
title: '慢性病科学管理,提高生活质量',
|
||
tag: '健康管理',
|
||
image: '/static/images/default.png'
|
||
}
|
||
]
|
||
|
||
// 获取一级分类数据
|
||
const loadMedicalMallCategories = async (): Promise<void> => {
|
||
try {
|
||
const categoriesData = await supabaseService.getMedicalMallParentCategories()
|
||
parentCategories.value = categoriesData
|
||
// 兼容其他使用 categories 的地方
|
||
categories.value = categoriesData
|
||
console.log('一级分类数据:', JSON.stringify(parentCategories.value))
|
||
// 构建横向标签栏数据(推荐 + 一级分类)
|
||
const items: CategoryItem[] = [{ id: 'recommend', name: '推荐' }]
|
||
for (let i = 0; i < categoriesData.length; i++) {
|
||
items.push({ id: categoriesData[i].id, name: categoriesData[i].name })
|
||
}
|
||
categoryList.value = items
|
||
} catch (error) {
|
||
console.error('加载分类数据失败:', error)
|
||
parentCategories.value = []
|
||
categories.value = []
|
||
categoryList.value = [{ id: 'recommend', name: '推荐' }]
|
||
}
|
||
}
|
||
|
||
// 获取二级分类数据
|
||
async function loadSubCategories(parentId: string): Promise<void> {
|
||
try {
|
||
console.log('[loadSubCategories] 开始加载二级分类, parentId:', parentId)
|
||
const subData = await supabaseService.getMedicalMallSubCategories(parentId)
|
||
console.log('[loadSubCategories] 获取到二级分类数量:', subData.length)
|
||
console.log('[loadSubCategories] 二级分类数据:', JSON.stringify(subData))
|
||
subCategories.value = subData
|
||
} catch (error) {
|
||
console.error('加载子分类数据失败:', error)
|
||
subCategories.value = []
|
||
}
|
||
}
|
||
|
||
// 点击一级分类
|
||
const onParentCategoryClick = async (category: Category): Promise<void> => {
|
||
console.log('[onParentCategoryClick] 点击一级分类:', category.name, 'id:', category.id)
|
||
|
||
// 如果已经选中,则切换显示/隐藏二级分类
|
||
if (selectedParentCategory.value != null && selectedParentCategory.value.id === category.id) {
|
||
console.log('[onParentCategoryClick] 切换显示状态')
|
||
showSubCategories.value = !showSubCategories.value
|
||
return
|
||
}
|
||
|
||
// 选中新的分类
|
||
selectedParentCategory.value = category
|
||
showSubCategories.value = true
|
||
console.log('[onParentCategoryClick] showSubCategories 设置为 true')
|
||
|
||
// 加载二级分类
|
||
await loadSubCategories(category.id)
|
||
|
||
// 如果没有二级分类,直接跳转到分类页
|
||
if (subCategories.value.length == 0) {
|
||
console.log('[onParentCategoryClick] 没有二级分类,切换首页分类商品流')
|
||
void refreshHomeCategory({ id: category.id, name: category.name })
|
||
}
|
||
}
|
||
|
||
// 点击二级分类
|
||
const onSubCategoryClick = (category: Category): void => {
|
||
void refreshHomeCategory({ id: category.id, name: category.name })
|
||
}
|
||
|
||
// 获取品牌数据
|
||
const loadBrands = async (): Promise<void> => {
|
||
try {
|
||
const brandsData = await supabaseService.getBrands()
|
||
brands.value = brandsData
|
||
} catch (e) {
|
||
console.error('加载品牌失败:', e)
|
||
brands.value = []
|
||
}
|
||
}
|
||
|
||
// 根据品牌名称获取图标
|
||
function getBrandIcon(name: string): string {
|
||
if (name == null || name === '') {
|
||
return '🏢'
|
||
}
|
||
// 常见品牌图标映射(使用数组方式避免 Object.keys 问题)
|
||
const iconKeys = [
|
||
'感冒', '发烧', '咳嗽', '消炎', '维生素', '钙片', '胃药', '止痛', '过敏', '皮肤', '眼药水', '口腔', '血压', '血糖', '血脂', '保健', '养生', '减肥', '美容', '母婴', '儿童', '老人', '男性', '女性', '维生素C', '维生素D', '蛋白粉', '鱼油', '蜂胶', '阿胶', '红枣', '枸杞', '菊花', '金银花', '口罩', '消毒液', '体温计', '创可贴', '棉签',
|
||
'九芝堂', '同仁堂', '云南白药', '东阿阿胶', '太极', '江中', '三九', '华素制药', '汤臣倍健', '白云山', '修正', '葵花', '哈药', '扬子江', '恒瑞', '复星', '辉瑞', '阿斯利康', '罗氏', '默沙东', '赛诺菲', '诺华', '雅培', '雀巢', '蒙牛', '伊利', '海尔', '美的', '飞利浦', '西门子', '松下', '苏泊尔', '九阳', '华为', '小米', '苹果', '三星'
|
||
]
|
||
const iconValues = [
|
||
'💊', '🌡️', '😷', '🔬', '💊', '🦴', '🫁', '💉', '🌸', '🧴', '👁️', '🦷', '❤️', '🩸', '💓', '🧬', '🍵', '⚖️', '💅', '👶', '🧒', '👴', '♂️', '♀️', '🍊', '☀️', '🥛', '🐟', '🐝', '🍯', '🫘', '🌿', '🌼', '🌸', '😷', '🧴', '🌡️', '🩹', '🧺',
|
||
'📜', '🏛️', '⛰️', '🍯', '☯️', '🌿', '9️⃣', '💊', '💪', '⛰️', '🩹', '🌻', '🧪', '🚢', '🔬', '⭐', '🧬', '🧪', '🧬', '💊', '🧬', '🔬', '🏥', '🥣', '🐄', '🥛', '🏠', '❄️', '🪒', '⚡', '🔋', '🍳', '🥛', '📱', '🍚', '🍎', '📱'
|
||
]
|
||
|
||
// 尝试精确匹配
|
||
for (let i = 0; i < iconKeys.length; i++) {
|
||
if (name === iconKeys[i]) {
|
||
return iconValues[i]
|
||
}
|
||
}
|
||
// 尝试模糊匹配
|
||
for (let i = 0; i < iconKeys.length; i++) {
|
||
if (name.indexOf(iconKeys[i]) !== -1) {
|
||
return iconValues[i]
|
||
}
|
||
}
|
||
// 默认返回品牌图标
|
||
return '🏢'
|
||
}
|
||
|
||
function getCategoryDisplayIcon(category: Category): string {
|
||
const icon = category.icon ?? ''
|
||
if (icon !== '' && !isImageIcon(icon)) {
|
||
return icon
|
||
}
|
||
const name = category.name ?? ''
|
||
if (name !== '') {
|
||
return name.substring(0, 1)
|
||
}
|
||
return '品'
|
||
}
|
||
|
||
function shouldHighlightCategory(name: string): boolean {
|
||
if (name === '') {
|
||
return false
|
||
}
|
||
return name.indexOf('明天达') !== -1 || name.indexOf('快') !== -1
|
||
}
|
||
|
||
function getCategoryTabDisplayName(name: string): string {
|
||
if (shouldHighlightCategory(name)) {
|
||
return '⚡' + name
|
||
}
|
||
return name
|
||
}
|
||
|
||
function buildListItemKey(prefix: string, id: string, index: number): string {
|
||
const normalizedId = id !== '' ? id : 'empty'
|
||
return prefix + '-' + normalizedId + '-' + index.toString()
|
||
}
|
||
|
||
function getCategoryLabelById(categoryId: string): string {
|
||
if (categoryId === 'recommend') {
|
||
return '推荐'
|
||
}
|
||
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id === categoryId)
|
||
if (matchedItem != null) {
|
||
return matchedItem.name
|
||
}
|
||
return '精选'
|
||
}
|
||
|
||
function normalizeSelectedMedicalCategoryId(categoryId: string): string {
|
||
if (categoryId == 'device') return 'med_device'
|
||
if (categoryId == 'external') return 'home_care_daily'
|
||
if (categoryId == 'cold') return 'otc_medicine'
|
||
if (categoryId == 'medicine') return 'otc_medicine'
|
||
if (categoryId == 'care') return 'home_care_daily'
|
||
return categoryId
|
||
}
|
||
|
||
async function consumeSelectedCategoryFromStorage(): Promise<boolean> {
|
||
if (activeTopModule.value != 'home' || categoryList.value.length == 0) {
|
||
return false
|
||
}
|
||
const savedCategoryId = uni.getStorageSync('selectedCategory')
|
||
if (savedCategoryId == null) {
|
||
return false
|
||
}
|
||
const nextCategoryId = String(savedCategoryId)
|
||
if (nextCategoryId === '') {
|
||
return false
|
||
}
|
||
uni.removeStorageSync('selectedCategory')
|
||
const normalizedCategoryId = normalizeSelectedMedicalCategoryId(nextCategoryId)
|
||
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id == normalizedCategoryId)
|
||
if (matchedItem != null) {
|
||
await refreshHomeCategory(matchedItem)
|
||
return true
|
||
}
|
||
await refreshHomeCategory({
|
||
id: normalizedCategoryId,
|
||
name: getCategoryLabelById(normalizedCategoryId)
|
||
})
|
||
return true
|
||
}
|
||
|
||
function formatChannelPrice(price: number): string {
|
||
const rounded = Math.round(price)
|
||
if (Math.abs(price - rounded) < 0.001) {
|
||
return rounded.toString()
|
||
}
|
||
return price.toFixed(1)
|
||
}
|
||
|
||
function getChannelProductImage(product: ChannelProduct): string {
|
||
if (product.image != null && product.image !== '') {
|
||
return product.image
|
||
}
|
||
return '/static/images/default.png'
|
||
}
|
||
|
||
function buildProductLoopKey(product: Product, index: number): string {
|
||
const productId = product.id ?? ''
|
||
return buildListItemKey('hot-product', productId, index)
|
||
}
|
||
|
||
const fetchSortedProductsPage = async (page: number, limit: number): Promise<PaginatedResponse<Product>> => {
|
||
console.log('加载热销商品,当前排序方式:', activeSort.value, 'page:', page, 'limit:', limit)
|
||
switch (activeSort.value) {
|
||
case 'sales':
|
||
console.log('调用 getMedicalMallSmartRecommendations(sales-fallback)')
|
||
return await supabaseService.getMedicalMallSmartRecommendations(page, limit)
|
||
case 'price':
|
||
console.log('调用 getMedicalMallSmartRecommendations(price-fallback), 升序:', priceAscending.value)
|
||
return await supabaseService.getMedicalMallSmartRecommendations(page, limit)
|
||
case 'new':
|
||
console.log('调用 getMedicalMallSmartRecommendations(new-fallback)')
|
||
return await supabaseService.getMedicalMallSmartRecommendations(page, limit)
|
||
case 'recommend':
|
||
console.log('调用 getMedicalMallSmartRecommendations')
|
||
return await supabaseService.getMedicalMallSmartRecommendations(page, limit)
|
||
case 'discount': {
|
||
console.log('调用 getDiscountProducts')
|
||
const products = await supabaseService.getDiscountProducts(limit)
|
||
return {
|
||
data: products,
|
||
total: products.length,
|
||
page,
|
||
limit,
|
||
hasmore: false
|
||
}
|
||
}
|
||
default:
|
||
console.log('调用默认 getMedicalMallSmartRecommendations')
|
||
return await supabaseService.getMedicalMallSmartRecommendations(page, limit)
|
||
}
|
||
}
|
||
|
||
async function loadHotProducts(page: number, limit: number): Promise<void> {
|
||
try {
|
||
const result = await fetchSortedProductsPage(page, limit)
|
||
const products = result.data
|
||
if (page <= 1) {
|
||
failedProductImageIds.value = []
|
||
}
|
||
console.log('加载到的商品数量:', products.length)
|
||
if (products.length > 0) {
|
||
console.log('Sample Product Merchant IDs:')
|
||
for (let i = 0; i < Math.min(products.length, 3); i++) {
|
||
const p = products[i]
|
||
console.log(` - Product: ${p.name}, MerchantID: ${p.merchant_id}`)
|
||
}
|
||
}
|
||
setHotProducts(products)
|
||
if (currentFeedCategoryId.value === 'recommend' && page <= 1) {
|
||
marketingChannels.value = buildRealRecommendMarketingChannels(products)
|
||
}
|
||
hasMore.value = result.hasmore
|
||
currentPage.value = page
|
||
} catch (error) {
|
||
console.error('加载热销商品失败:', error)
|
||
hotProducts.value = []
|
||
if (currentFeedCategoryId.value === 'recommend') {
|
||
console.log('[home-channel] fallback to mock channel data')
|
||
marketingChannels.value = buildVisibleRecommendChannels()
|
||
}
|
||
hasMore.value = false
|
||
}
|
||
}
|
||
|
||
async function syncCategoryLayout(categoryId: string): Promise<void> {
|
||
selectedSubCategoryId.value = ''
|
||
if (categoryId === 'recommend') {
|
||
subCategories.value = []
|
||
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(categoryId)
|
||
applyChannelDisplay(categoryId)
|
||
return
|
||
}
|
||
|
||
try {
|
||
const subData = await supabaseService.getMedicalMallSubCategories(categoryId)
|
||
subCategories.value = subData
|
||
} catch (error) {
|
||
console.error('加载子分类数据失败:', error)
|
||
subCategories.value = []
|
||
}
|
||
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(categoryId)
|
||
applyChannelDisplay(categoryId)
|
||
}
|
||
|
||
async function loadCategoryGoods(categoryId: string): Promise<void> {
|
||
currentFeedCategoryId.value = categoryId
|
||
currentPage.value = 1
|
||
hasMore.value = true
|
||
await syncCategoryLayout(categoryId)
|
||
if (categoryId === 'recommend') {
|
||
try {
|
||
const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
|
||
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
|
||
failedProductImageIds.value = []
|
||
setHotProducts(result.data)
|
||
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
|
||
hasMore.value = result.hasmore
|
||
currentPage.value = 1
|
||
} catch (error) {
|
||
console.error('加载热销商品失败:', error)
|
||
hotProducts.value = []
|
||
console.log('[home-channel] fallback to mock channel data')
|
||
marketingChannels.value = buildVisibleRecommendChannels()
|
||
hasMore.value = false
|
||
}
|
||
} else {
|
||
try {
|
||
loading.value = true
|
||
const result = await supabaseService.getMedicalMallProductsByCategory(categoryId, 1, defaultLoadLimit)
|
||
failedProductImageIds.value = []
|
||
setHotProducts(result.data)
|
||
await loadCategoryChannelCards(categoryId)
|
||
hasMore.value = result.hasmore
|
||
} catch (e) {
|
||
console.error('分类商品加载失败', e)
|
||
hotProducts.value = []
|
||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||
hasMore.value = false
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
async function refreshHomeCategory(item: CategoryItem): Promise<void> {
|
||
currentCategory.value = item.id
|
||
currentFeedCategoryId.value = item.id
|
||
currentPage.value = 1
|
||
hotProducts.value = []
|
||
hasMore.value = true
|
||
selectedSubCategoryId.value = ''
|
||
showCategoryPanel.value = false
|
||
categoryScrollIntoView.value = 'cat-' + item.id
|
||
failedProductImageIds.value = []
|
||
loading.value = true
|
||
|
||
if (item.id === 'recommend') {
|
||
subCategories.value = []
|
||
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
|
||
applyChannelDisplay(item.id)
|
||
try {
|
||
const result = await supabaseService.getMedicalMallSmartRecommendations(1, recommendChannelLoadLimit)
|
||
console.log('[home-channel] 推荐商品接口返回数量:', result.data.length)
|
||
setHotProducts(result.data)
|
||
marketingChannels.value = buildRealRecommendMarketingChannels(result.data)
|
||
hasMore.value = result.hasmore
|
||
} catch (error) {
|
||
console.error('加载推荐商品失败:', error)
|
||
hotProducts.value = []
|
||
console.log('[home-channel] fallback to mock channel data')
|
||
marketingChannels.value = buildVisibleRecommendChannels()
|
||
hasMore.value = false
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
return
|
||
}
|
||
|
||
try {
|
||
const subData = await supabaseService.getMedicalMallSubCategories(item.id)
|
||
subCategories.value = subData
|
||
} catch (error) {
|
||
console.error('加载子分类数据失败:', error)
|
||
subCategories.value = []
|
||
}
|
||
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
|
||
applyChannelDisplay(item.id)
|
||
try {
|
||
const result = await supabaseService.getMedicalMallProductsByCategory(item.id, 1, defaultLoadLimit)
|
||
setHotProducts(result.data)
|
||
await loadCategoryChannelCards(item.id)
|
||
hasMore.value = result.hasmore
|
||
} catch (error) {
|
||
console.error('分类商品加载失败', error)
|
||
hotProducts.value = []
|
||
categorySimpleChannels.value = [] as Array<SimpleCategoryChannel>
|
||
hasMore.value = false
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 分类标签栏交互
|
||
function handleCategoryTabClick(item: CategoryItem): void {
|
||
void refreshHomeCategory(item)
|
||
}
|
||
|
||
function selectCategoryFromPanel(item: CategoryItem): void {
|
||
void refreshHomeCategory(item)
|
||
}
|
||
|
||
const loadRecommendedProducts = async (limit: number): Promise<void> => {
|
||
const result = await supabaseService.getMedicalMallSmartRecommendations(1, limit)
|
||
recommendedProducts.value = result.data
|
||
}
|
||
|
||
// 加载热搜词
|
||
const loadHotKeywords = async (): Promise<void> => {
|
||
try {
|
||
const keywords = await supabaseService.getHotKeywords(10)
|
||
hotKeywords.value = keywords
|
||
console.log('加载热搜词:', keywords.length, '个')
|
||
} catch (error) {
|
||
console.error('加载热搜词失败:', error)
|
||
hotKeywords.value = []
|
||
}
|
||
}
|
||
|
||
// 点击热搜词进行搜索
|
||
const searchByKeyword = (keyword: string): void => {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(keyword)}`
|
||
})
|
||
}
|
||
|
||
// 初始化数据
|
||
type InitDataOptions = {
|
||
showSkeleton: boolean
|
||
}
|
||
|
||
const initData = async (options: InitDataOptions = { showSkeleton: false }) => {
|
||
logSupaConfig()
|
||
console.log('[consumer-db] 首页开始加载数据')
|
||
resetInitialHomeLoadFlags()
|
||
if (options.showSkeleton) {
|
||
beginHomeSkeleton()
|
||
}
|
||
// 首先确保用户资料已加载
|
||
try {
|
||
await getCurrentUser()
|
||
console.log('主页初始化:用户资料加载完成')
|
||
} catch (error) {
|
||
console.error('加载用户资料失败:', error)
|
||
}
|
||
const categoryTask = (async (): Promise<void> => {
|
||
try {
|
||
await loadMedicalMallCategories()
|
||
} finally {
|
||
categoryLoaded.value = true
|
||
}
|
||
})()
|
||
const bannerTask = (async (): Promise<void> => {
|
||
try {
|
||
await loadHomeBannerData()
|
||
} finally {
|
||
bannerLoaded.value = true
|
||
}
|
||
})()
|
||
const goodsTask = (async (): Promise<void> => {
|
||
try {
|
||
await categoryTask
|
||
if (!(await consumeSelectedCategoryFromStorage())) {
|
||
await loadCategoryGoods(currentCategory.value)
|
||
}
|
||
} finally {
|
||
goodsLoaded.value = true
|
||
}
|
||
})()
|
||
const supplementTask = (async (): Promise<void> => {
|
||
await Promise.all([
|
||
loadBrands(),
|
||
loadHotKeywords(),
|
||
loadServiceHomeData()
|
||
])
|
||
try {
|
||
await loadRecommendedProducts(defaultLoadLimit)
|
||
} catch (error) {
|
||
console.error('加载推荐频道商品失败:', error)
|
||
recommendedProducts.value = []
|
||
}
|
||
})()
|
||
try {
|
||
await Promise.all([categoryTask, goodsTask, bannerTask, supplementTask])
|
||
} finally {
|
||
if (options.showSkeleton && categoryLoaded.value && goodsLoaded.value && bannerLoaded.value) {
|
||
finishHomeSkeleton()
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// 家庭常备药
|
||
const familyItems = [
|
||
{
|
||
id: 'family1',
|
||
name: '创可贴',
|
||
desc: '伤口护理',
|
||
icon: '🩹',
|
||
color: '#FF5722',
|
||
categoryId: 'wound_care'
|
||
},
|
||
{
|
||
id: 'family2',
|
||
name: '体温计',
|
||
desc: '健康监测',
|
||
icon: '🌡️',
|
||
color: '#2196F3',
|
||
categoryId: 'thermometer'
|
||
},
|
||
{
|
||
id: 'family3',
|
||
name: '消毒酒精',
|
||
desc: '环境消毒',
|
||
icon: '🧪',
|
||
color: '#ff5000',
|
||
categoryId: 'disinfectant'
|
||
},
|
||
{
|
||
id: 'family4',
|
||
name: '口罩',
|
||
desc: '日常防护',
|
||
icon: '😷',
|
||
color: '#607D8B',
|
||
categoryId: 'mask'
|
||
},
|
||
{
|
||
id: 'family5',
|
||
name: '退热贴',
|
||
desc: '物理降温',
|
||
icon: '🧊',
|
||
color: '#00BCD4',
|
||
categoryId: 'cold_fever'
|
||
},
|
||
{
|
||
id: 'family6',
|
||
name: '棉签纱布',
|
||
desc: '伤口处理',
|
||
icon: '🩹',
|
||
color: '#FF9800',
|
||
categoryId: 'dressing_tools'
|
||
}
|
||
]
|
||
|
||
// 初始化页面
|
||
const initPage = () => {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
statusBarHeight.value = systemInfo.statusBarHeight != null ? systemInfo.statusBarHeight : 20
|
||
const searchContentHeight = Math.round(68 * systemInfo.screenWidth / 750)
|
||
const searchTopGap = Math.round(14 * systemInfo.screenWidth / 750)
|
||
const headerBottomPadding = Math.round(10 * systemInfo.screenWidth / 750)
|
||
const moduleRowHeight = Math.round(62 * systemInfo.screenWidth / 750)
|
||
const categoryRowHeight = Math.round(56 * systemInfo.screenWidth / 750)
|
||
|
||
let menuInfo: CapsuleButtonInfo | null = null
|
||
// #ifdef MP-WEIXIN || MP-ALIPAY
|
||
try {
|
||
menuInfo = uni.getMenuButtonBoundingClientRect()
|
||
capsuleButtonInfo.value = menuInfo
|
||
} catch (e) {
|
||
console.log('获取胶囊按钮信息失败', e)
|
||
}
|
||
// #endif
|
||
|
||
if (menuInfo != null && menuInfo.top > 0) {
|
||
const navHeight = (menuInfo.top - statusBarHeight.value) * 2 + menuInfo.height
|
||
navBarHeight.value = navHeight
|
||
|
||
const rightReserve = systemInfo.screenWidth - menuInfo.left + 8
|
||
navBarRight.value = rightReserve
|
||
const searchRowTotalH = statusBarHeight.value + Math.max(navHeight, moduleRowHeight) + searchTopGap + searchContentHeight + headerBottomPadding
|
||
searchRowStyle.value = `padding-right:${rightReserve}px;`
|
||
navbarTotalHeight.value = searchRowTotalH
|
||
} else {
|
||
navBarHeight.value = 44
|
||
navBarRight.value = 0
|
||
const searchRowTotalH = statusBarHeight.value + Math.max(44, moduleRowHeight) + searchTopGap + searchContentHeight + headerBottomPadding
|
||
searchRowStyle.value = ``
|
||
navbarTotalHeight.value = searchRowTotalH
|
||
}
|
||
|
||
headerStyle.value = ``
|
||
categoryBarHeightPx.value = categoryRowHeight
|
||
headerPlaceholderHeight.value = navbarTotalHeight.value + categoryBarHeightPx.value
|
||
const safeBottom = systemInfo.safeArea != null ? systemInfo.screenHeight - systemInfo.safeArea.bottom : 20
|
||
bottomSafeArea.value = safeBottom > 0 ? safeBottom : 20
|
||
|
||
const screenWidth = systemInfo.screenWidth
|
||
isMobile.value = screenWidth < 768
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
initPage()
|
||
void initData({ showSkeleton: true })
|
||
startPlaceholderScroll()
|
||
})
|
||
|
||
// 页面显示时重置状态
|
||
onShow(() => {
|
||
console.log('=== index页面onShow被调用 ===')
|
||
console.log('主页重新显示,重置页面状态')
|
||
startPlaceholderScroll()
|
||
if (categoryList.value.length > 0) {
|
||
void consumeSelectedCategoryFromStorage()
|
||
}
|
||
|
||
// 重置导航栏显示状态
|
||
showNavbar.value = true
|
||
lastScrollTop.value = 0
|
||
|
||
// 重置滚动位置到顶部
|
||
// 注意:这里不能直接操作scroll-view的滚动位置
|
||
// 但可以重置一些页面状态
|
||
|
||
// 兼容旧分类桥接页:如果首页是从旧入口返回的,前面已经同步读取并清理 selectedCategory
|
||
// 这里不再额外处理,避免重复触发分类刷新
|
||
|
||
// 每次页面显示时尝试更新用户资料
|
||
if (!isFirstShow.value) {
|
||
getCurrentUser().then(profile => {
|
||
if (profile != null) {
|
||
console.log('主页onShow:用户资料更新成功')
|
||
} else {
|
||
console.log('主页onShow:用户资料为空,可能未登录')
|
||
}
|
||
}).catch(error => {
|
||
console.error('主页onShow:加载用户资料失败:', error)
|
||
})
|
||
} else {
|
||
isFirstShow.value = false
|
||
console.log('主页首次显示,跳过onShow中的用户资料检查,交由initData处理')
|
||
}
|
||
|
||
console.log('=== index页面onShow执行完成 ===')
|
||
})
|
||
|
||
// 页面隐藏时停止轮播
|
||
onHide(() => {
|
||
stopPlaceholderScroll()
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopPlaceholderScroll()
|
||
clearSkeletonTimer()
|
||
})
|
||
|
||
// 处理滚动事件
|
||
const handleScroll = (event: any) => {
|
||
try {
|
||
const detail = getEventDetailObject(event)
|
||
if (detail == null) return
|
||
const scrollTop = detail.getNumber('scrollTop') ?? 0
|
||
const currentTime = Date.now()
|
||
|
||
// 判断滚动方向
|
||
if (scrollTop > lastScrollTop.value) {
|
||
// 向下滚动
|
||
scrollingUp.value = false
|
||
// 向下滚动超过阈值时隐藏导航栏
|
||
if (scrollTop > scrollThreshold && showNavbar.value) {
|
||
showNavbar.value = false
|
||
}
|
||
} else if (scrollTop < lastScrollTop.value) {
|
||
// 向上滚动
|
||
scrollingUp.value = true
|
||
// 向上滚动时显示导航栏
|
||
if (!showNavbar.value) {
|
||
showNavbar.value = true
|
||
}
|
||
}
|
||
|
||
// 滚动到顶部时强制显示导航栏
|
||
if (scrollTop <= 10) {
|
||
showNavbar.value = true
|
||
}
|
||
|
||
lastScrollTop.value = scrollTop
|
||
|
||
// 调试信息(开发时可启用)
|
||
// console.log(`Scroll: ${scrollTop}, ShowNavbar: ${showNavbar.value}, ScrollingUp: ${scrollingUp.value}`)
|
||
} catch (e) {
|
||
// 忽略滚动事件处理错误
|
||
}
|
||
}
|
||
|
||
// 重置导航栏显示状态(例如点击回到顶部时)
|
||
const resetNavbar = () => {
|
||
showNavbar.value = true
|
||
lastScrollTop.value = 0
|
||
}
|
||
|
||
// 切换分类 - 跳转到分类页面并传递分类ID
|
||
const switchCategory = (category: any) => {
|
||
console.log('=== switchCategory函数开始执行 ===')
|
||
|
||
// 将 category 转换为 UTSJSONObject 以访问属性
|
||
const catObj = (category instanceof UTSJSONObject) ? (category as UTSJSONObject) : (JSON.parse(JSON.stringify(category)) as UTSJSONObject)
|
||
const categoryId = catObj.getString('id') ?? ''
|
||
const categoryName = catObj.getString('name') ?? ''
|
||
|
||
console.log('分类ID:', categoryId, '分类名称:', categoryName)
|
||
if (categoryId === '') {
|
||
return
|
||
}
|
||
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id == categoryId)
|
||
if (matchedItem != null) {
|
||
void refreshHomeCategory(matchedItem)
|
||
return
|
||
}
|
||
void refreshHomeCategory({
|
||
id: categoryId,
|
||
name: categoryName !== '' ? categoryName : getCategoryLabelById(categoryId)
|
||
})
|
||
}
|
||
|
||
const switchBrand = (brand: Brand) => {
|
||
// 假设跳转到搜索结果页或者分类页带 filter
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(brand.name)}&type=brand&brandId=${brand.id}`
|
||
})
|
||
}
|
||
|
||
// 切换排序
|
||
const switchSort = (sortId: string) => {
|
||
// 如果点击的是价格排序,切换升序/降序
|
||
if (sortId === 'price' && activeSort.value === 'price') {
|
||
priceAscending.value = !priceAscending.value
|
||
console.log('切换价格排序方向,升序:', priceAscending.value)
|
||
} else {
|
||
// 切换到其他排序时,重置价格排序为升序
|
||
if (sortId !== 'price') {
|
||
priceAscending.value = true
|
||
}
|
||
activeSort.value = sortId
|
||
}
|
||
hasMore.value = true // 重置加载更多状态
|
||
// 重新加载热销商品,排序由 Supabase 服务处理
|
||
const nextLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
|
||
loadHotProducts(1, nextLimit)
|
||
}
|
||
|
||
// 切换筛选器
|
||
const switchFilter = (filterId: string) => {
|
||
activeFilter.value = filterId
|
||
// 重新加载推荐商品,筛选由 Supabase 服务处理
|
||
loadRecommendedProducts(defaultLoadLimit)
|
||
}
|
||
|
||
// 查看新闻详情
|
||
const viewNewsDetail = (news: any) => {
|
||
uni.navigateTo({
|
||
url: `/pages/news/detail?id=${news.id}`
|
||
})
|
||
}
|
||
|
||
// 下拉刷新
|
||
const onRefresh = async () => {
|
||
refreshing.value = true
|
||
|
||
try {
|
||
// 重新加载数据
|
||
await initData({ showSkeleton: false })
|
||
} catch (e) {
|
||
console.error('刷新数据失败:', e)
|
||
} finally {
|
||
// 延迟关闭刷新动画,确保用户能看到刷新过程
|
||
setTimeout(() => {
|
||
refreshing.value = false
|
||
// 延迟显示提示,避免与动画冲突
|
||
setTimeout(() => {
|
||
uni.showToast({
|
||
title: '刷新成功',
|
||
icon: 'success'
|
||
})
|
||
}, 200)
|
||
}, 800)
|
||
}
|
||
}
|
||
|
||
// 加载更多
|
||
const loadMore = async () => {
|
||
console.log('=== 触发触底事件 ===')
|
||
if (loading.value || !hasMore.value) {
|
||
console.log('正在加载中,跳过')
|
||
return
|
||
}
|
||
|
||
showLoadMore.value = true
|
||
loading.value = true
|
||
try {
|
||
const pageLimit = currentFeedCategoryId.value === 'recommend' ? recommendChannelLoadLimit : defaultLoadLimit
|
||
const nextPage = currentPage.value + 1
|
||
const currentCount = hotProducts.value.length
|
||
console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage, '分类:', currentFeedCategoryId.value)
|
||
|
||
if (currentFeedCategoryId.value === 'recommend') {
|
||
const result = await fetchSortedProductsPage(nextPage, pageLimit)
|
||
const newProducts = result.data
|
||
|
||
if (newProducts.length == 0) {
|
||
hasMore.value = false
|
||
} else {
|
||
const existingIds: string[] = hotProducts.value.map((item: Product): string => item.id)
|
||
const appendedProducts: Product[] = []
|
||
for (let i = 0; i < newProducts.length; i++) {
|
||
const nextProduct = newProducts[i]
|
||
if (existingIds.indexOf(nextProduct.id) === -1) {
|
||
appendedProducts.push(nextProduct)
|
||
existingIds.push(nextProduct.id)
|
||
}
|
||
}
|
||
if (appendedProducts.length === 0) {
|
||
hasMore.value = false
|
||
} else {
|
||
appendHotProducts(appendedProducts)
|
||
hasMore.value = result.hasmore
|
||
}
|
||
currentPage.value = nextPage
|
||
}
|
||
} else {
|
||
const result = await supabaseService.getMedicalMallProductsByCategory(currentFeedCategoryId.value, nextPage, defaultLoadLimit)
|
||
if (result.data.length == 0) {
|
||
hasMore.value = false
|
||
} else {
|
||
appendHotProducts(result.data)
|
||
currentPage.value = nextPage
|
||
hasMore.value = result.hasmore
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载更多失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
// 稍微延迟隐藏加载条,让用户看到
|
||
setTimeout(() => {
|
||
showLoadMore.value = false
|
||
}, 500)
|
||
}
|
||
}
|
||
|
||
// 添加到购物车
|
||
const addToCart = async (product: any) => {
|
||
const userId = supabaseService.getCurrentUserId()
|
||
if (userId == null || userId === '') {
|
||
goToLogin('/pages/main/index')
|
||
return
|
||
}
|
||
uni.showLoading({ title: '检查商品...' })
|
||
try {
|
||
// 将 product 转换为 UTSJSONObject 以访问属性
|
||
const prodObj = (product instanceof UTSJSONObject) ? (product as UTSJSONObject) : (JSON.parse(JSON.stringify(product)) as UTSJSONObject)
|
||
const productId = prodObj.getString('id') ?? ''
|
||
const merchantId = prodObj.getString('merchant_id') ?? ''
|
||
|
||
// 检查商品是否有SKU
|
||
const skus = await supabaseService.getProductSkus(productId)
|
||
uni.hideLoading()
|
||
|
||
if (skus.length > 0) {
|
||
// 有规格,提示并跳转到商品详情页选择规格
|
||
uni.showToast({
|
||
title: '请选择规格',
|
||
icon: 'none'
|
||
})
|
||
setTimeout(() => {
|
||
uni.navigateTo({
|
||
url: '/pages/mall/consumer/product-detail?id=' + productId
|
||
})
|
||
}, 500)
|
||
} else {
|
||
// 无规格,直接加入购物车
|
||
uni.showLoading({ title: '添加中...' })
|
||
const success = await supabaseService.addToCart(productId, 1, '', merchantId)
|
||
uni.hideLoading()
|
||
if (success) {
|
||
uni.showToast({
|
||
title: '已添加到购物车',
|
||
icon: 'success'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: '添加失败,请先登录',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('添加到购物车异常', e)
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '操作异常',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 扫码功能
|
||
const onScan = (): void => {
|
||
uni.scanCode({
|
||
success: (res: ScanCodeSuccess) => {
|
||
console.log('扫码成功:', res)
|
||
uni.showToast({
|
||
title: '扫码成功: ' + res.result,
|
||
icon: 'none'
|
||
})
|
||
},
|
||
fail: (err) => {
|
||
console.error('扫码失败:', err)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 相机功能
|
||
const onCamera = (): void => {
|
||
uni.chooseImage({
|
||
count: 1,
|
||
sourceType: ['camera'],
|
||
success: (res: ChooseImageSuccess) => {
|
||
console.log('相机拍摄成功:', res.tempFilePaths[0])
|
||
uni.showToast({
|
||
title: '已拍摄,正在识别...',
|
||
icon: 'loading'
|
||
})
|
||
setTimeout(() => {
|
||
uni.showToast({
|
||
title: '识别成功',
|
||
icon: 'success'
|
||
})
|
||
}, 1000)
|
||
},
|
||
fail: (err) => {
|
||
console.error('相机调用失败:', err)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 导航函数
|
||
const navigateToSearch = (): void => { uni.navigateTo({ url: '/pages/mall/consumer/search' }) }
|
||
const navigateToNews = (): void => { uni.navigateTo({ url: '/pages/news/list' }) }
|
||
const navigateToProduct = (product: any) => {
|
||
// 将 product 转换为 UTSJSONObject 以访问属性
|
||
const prodObj = (product instanceof UTSJSONObject) ? (product as UTSJSONObject) : (JSON.parse(JSON.stringify(product)) as UTSJSONObject)
|
||
|
||
// 使用productId(如果存在)作为跳转的商品ID,否则使用id
|
||
const productId = prodObj.getString('productId') ?? prodObj.getString('id') ?? ''
|
||
const name = prodObj.getString('name') ?? ''
|
||
// 使用 main_image_url
|
||
const image = prodObj.getString('main_image_url') ?? prodObj.getString('image') ?? '/static/images/default.png'
|
||
const price = (prodObj.getNumber('base_price') ?? prodObj.getNumber('price') ?? 0).toString()
|
||
const marketPrice = prodObj.getNumber('market_price') ?? prodObj.getNumber('original_price') ?? (parseFloat(price) * 1.2)
|
||
const originalPrice = marketPrice.toString()
|
||
|
||
// 手动构建URL,避免双重编码问题
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/product-detail?id=${productId}&price=${price}&originalPrice=${originalPrice}&name=${encodeURIComponent(name)}&image=${encodeURIComponent(image)}`
|
||
})
|
||
}
|
||
const navigateToCategory = (item: any) => {
|
||
uni.navigateTo({
|
||
url: `/pages/mall/consumer/search?keyword=${encodeURIComponent(item.name)}&type=family`
|
||
})
|
||
}
|
||
const navigateToConsultation = () => uni.navigateTo({ url: '/pages/medicine/consultation' })
|
||
const navigateToPrescription = () => uni.navigateTo({ url: '/pages/medicine/prescription' })
|
||
const navigateToOTC = () => uni.navigateTo({ url: '/pages/medicine/otc' })
|
||
const navigateToHealthTools = () => uni.navigateTo({ url: '/pages/medicine/tools' })
|
||
const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders' })
|
||
</script>
|
||
|
||
<style>
|
||
/* 全局重置 removed - uniapp-x does not support * selector */
|
||
/* .medic-home * {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
} */
|
||
|
||
.medic-home {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
overflow: hidden;
|
||
background: #f5f5f5;
|
||
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||
line-height: 1.5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.home-main-stage {
|
||
flex: 1;
|
||
min-height: 0;
|
||
transition: opacity 260ms ease;
|
||
}
|
||
|
||
.home-main-stage-hidden {
|
||
opacity: 0;
|
||
}
|
||
|
||
.home-main-stage-visible {
|
||
opacity: 1;
|
||
}
|
||
|
||
.home-skeleton-overlay {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 1200;
|
||
background: #f6f8fb;
|
||
transition: opacity 260ms ease;
|
||
}
|
||
|
||
.home-skeleton-overlay-visible {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.home-skeleton-overlay-leaving {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.jd-header-fixed {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1000;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.jd-header-placeholder {
|
||
width: 100%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.main-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.service-home-section {
|
||
background-color: #f4f8fb;
|
||
padding: 16rpx 16rpx 32rpx;
|
||
box-sizing: border-box;
|
||
min-height: 100%;
|
||
}
|
||
|
||
.service-hero-banner {
|
||
min-height: 240rpx;
|
||
border-radius: 28rpx;
|
||
padding: 28rpx;
|
||
background: linear-gradient(135deg, #e0f7f5 0%, #eff6ff 55%, #fff7ed 100%);
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
overflow: hidden;
|
||
margin-bottom: 18rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-hero-content {
|
||
flex: 1;
|
||
flex-direction: column;
|
||
padding-right: 12rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-hero-tag {
|
||
width: 128rpx;
|
||
height: 38rpx;
|
||
line-height: 38rpx;
|
||
text-align: center;
|
||
border-radius: 999rpx;
|
||
background-color: #ffffff;
|
||
color: #0f766e;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.service-hero-title {
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
color: #16324f;
|
||
margin-bottom: 10rpx;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.service-hero-subtitle {
|
||
font-size: 24rpx;
|
||
color: #64748b;
|
||
margin-bottom: 16rpx;
|
||
line-height: 34rpx;
|
||
}
|
||
|
||
.service-hero-tags {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
margin-right: -8rpx;
|
||
margin-bottom: -8rpx;
|
||
}
|
||
|
||
.service-hero-chip {
|
||
font-size: 22rpx;
|
||
color: #0f766e;
|
||
background-color: rgba(255, 255, 255, 0.86);
|
||
border-radius: 999rpx;
|
||
padding: 6rpx 14rpx;
|
||
margin-right: 8rpx;
|
||
margin-bottom: 8rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-hero-visual-wrap {
|
||
width: 156rpx;
|
||
height: 156rpx;
|
||
position: relative;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.service-hero-visual {
|
||
position: absolute;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 999rpx;
|
||
background-color: rgba(255, 255, 255, 0.72);
|
||
box-shadow: 0 12rpx 24rpx rgba(14, 165, 164, 0.12);
|
||
}
|
||
|
||
.service-hero-visual-primary {
|
||
width: 128rpx;
|
||
height: 128rpx;
|
||
right: 0;
|
||
top: 0;
|
||
}
|
||
|
||
.service-hero-visual-secondary {
|
||
width: 74rpx;
|
||
height: 74rpx;
|
||
left: 0;
|
||
bottom: 6rpx;
|
||
background-color: rgba(15, 118, 110, 0.12);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.service-hero-visual-text {
|
||
font-size: 56rpx;
|
||
font-weight: 700;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.service-hero-visual-subtext {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.service-category-card {
|
||
background-color: #ffffff;
|
||
border-radius: 28rpx;
|
||
padding: 22rpx 12rpx 10rpx;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 18rpx;
|
||
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.04);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-category-item {
|
||
width: 20%;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-category-icon {
|
||
width: 76rpx;
|
||
height: 76rpx;
|
||
border-radius: 24rpx;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 10rpx;
|
||
border-width: 2rpx;
|
||
border-style: solid;
|
||
border-color: transparent;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-category-icon-active {
|
||
border-color: #14b8a6;
|
||
box-shadow: 0 8rpx 18rpx rgba(20, 184, 166, 0.14);
|
||
}
|
||
|
||
.service-category-icon-text {
|
||
font-size: 30rpx;
|
||
font-weight: 700;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.service-category-name {
|
||
font-size: 23rpx;
|
||
color: #334155;
|
||
text-align: center;
|
||
line-height: 30rpx;
|
||
padding: 0 4rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-category-name-active {
|
||
color: #0f766e;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.service-shortcut-row {
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
|
||
.service-shortcut-card {
|
||
width: 32%;
|
||
background-color: #ffffff;
|
||
border-radius: 22rpx;
|
||
padding: 18rpx 16rpx;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||
min-height: 116rpx;
|
||
}
|
||
|
||
.service-shortcut-icon {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
line-height: 52rpx;
|
||
text-align: center;
|
||
border-radius: 18rpx;
|
||
background-color: #e8f7f6;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #0f766e;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.service-shortcut-body {
|
||
flex: 1;
|
||
margin-left: 12rpx;
|
||
min-width: 0;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.service-shortcut-title {
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #16324f;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.service-shortcut-desc {
|
||
margin-top: 6rpx;
|
||
font-size: 20rpx;
|
||
color: #64748b;
|
||
line-height: 28rpx;
|
||
}
|
||
|
||
.service-products-section {
|
||
background-color: #f4f8fb;
|
||
}
|
||
|
||
.service-section-title-row {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 14rpx;
|
||
}
|
||
|
||
.service-section-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #16324f;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.service-section-subtitle {
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: #94a3b8;
|
||
line-height: 30rpx;
|
||
}
|
||
|
||
.service-section-more {
|
||
font-size: 24rpx;
|
||
color: #94a3b8;
|
||
padding: 8rpx 0 8rpx 12rpx;
|
||
}
|
||
|
||
.service-products-grid {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.service-product-card {
|
||
width: 49%;
|
||
background-color: #ffffff;
|
||
border-radius: 22rpx;
|
||
overflow: hidden;
|
||
margin-bottom: 14rpx;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.05);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-product-cover {
|
||
height: 220rpx;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
padding-top: 28rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-product-cover-badge {
|
||
position: absolute;
|
||
top: 14rpx;
|
||
left: 14rpx;
|
||
font-size: 20rpx;
|
||
color: #0f766e;
|
||
background-color: rgba(255, 255, 255, 0.82);
|
||
border-radius: 999rpx;
|
||
padding: 4rpx 10rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-product-cover-text {
|
||
font-size: 64rpx;
|
||
font-weight: 700;
|
||
color: #0f766e;
|
||
}
|
||
|
||
.service-product-body {
|
||
padding: 18rpx;
|
||
flex-direction: column;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-product-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #1f2937;
|
||
line-height: 36rpx;
|
||
min-height: 72rpx;
|
||
max-height: 72rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.service-product-subtitle {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #64748b;
|
||
line-height: 32rpx;
|
||
min-height: 64rpx;
|
||
max-height: 64rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.service-product-tags {
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin-top: 10rpx;
|
||
margin-right: -6rpx;
|
||
margin-bottom: -6rpx;
|
||
}
|
||
|
||
.service-product-tag {
|
||
font-size: 20rpx;
|
||
color: #0f766e;
|
||
background-color: #e0f2f1;
|
||
border-radius: 8rpx;
|
||
padding: 4rpx 8rpx;
|
||
margin-right: 6rpx;
|
||
margin-bottom: 6rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-product-price-row {
|
||
margin-top: 12rpx;
|
||
flex-direction: row;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.service-product-price-symbol {
|
||
font-size: 22rpx;
|
||
color: #e1251b;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.service-product-price {
|
||
font-size: 36rpx;
|
||
color: #e1251b;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.service-product-unit {
|
||
font-size: 22rpx;
|
||
color: #64748b;
|
||
margin-left: 4rpx;
|
||
margin-bottom: 4rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.service-product-sales {
|
||
margin-top: 6rpx;
|
||
font-size: 21rpx;
|
||
color: #94a3b8;
|
||
line-height: 30rpx;
|
||
}
|
||
|
||
.service-product-action-row {
|
||
margin-top: 14rpx;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.service-product-secondary-btn,
|
||
.service-product-primary-btn {
|
||
height: 58rpx;
|
||
border-radius: 999rpx;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
padding: 0 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-product-secondary-btn {
|
||
width: 47%;
|
||
border-width: 1rpx;
|
||
border-style: solid;
|
||
border-color: #cbd5e1;
|
||
color: #476072;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.service-product-primary-btn {
|
||
width: 49%;
|
||
background-color: #16a085;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.service-state-card {
|
||
background-color: #ffffff;
|
||
border-radius: 24rpx;
|
||
padding: 28rpx 24rpx;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.service-state-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #16324f;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.service-state-desc {
|
||
margin-top: 10rpx;
|
||
font-size: 22rpx;
|
||
color: #64748b;
|
||
line-height: 32rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.service-state-action {
|
||
margin-top: 18rpx;
|
||
height: 68rpx;
|
||
line-height: 68rpx;
|
||
padding: 0 28rpx;
|
||
border-radius: 999rpx;
|
||
background-color: #16a085;
|
||
color: #ffffff;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
text-align: center;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.pdd-home-header {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background-color: #ffffff;
|
||
z-index: 1000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.pdd-header-shell {
|
||
background-color: #ffffff;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
|
||
overflow: visible;
|
||
}
|
||
|
||
.pdd-search-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
padding-left: 18rpx;
|
||
padding-right: 18rpx;
|
||
box-sizing: border-box;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.pdd-search-box {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 60rpx;
|
||
background-color: #f3f3f3;
|
||
border-radius: 32rpx;
|
||
overflow: hidden;
|
||
padding: 0 18rpx 0 22rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.pdd-search-icon {
|
||
color: #b8b8b8;
|
||
font-size: 28rpx;
|
||
margin-right: 10rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.pdd-keyword-text {
|
||
flex: 1;
|
||
height: 60rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
justify-content: flex-start;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.pdd-keyword-track {
|
||
width: 100%;
|
||
height: 120rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.pdd-keyword-track-animating {
|
||
transition: transform 0.32s ease;
|
||
transform: translateY(-60rpx);
|
||
}
|
||
|
||
.pdd-keyword-slide {
|
||
width: 100%;
|
||
height: 60rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.pdd-keyword-placeholder {
|
||
font-size: 25rpx;
|
||
color: #9b9b9b;
|
||
width: 100%;
|
||
line-height: 60rpx;
|
||
vertical-align: middle;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
lines: 1;
|
||
}
|
||
|
||
.pdd-camera-icon {
|
||
color: #b8b8b8;
|
||
font-size: 28rpx;
|
||
margin-left: 10rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.pdd-header-placeholder {
|
||
width: 100%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 主内容区域 */
|
||
.main-scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
padding: 0 12rpx 16px;
|
||
width: 100%;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 智能健康卡片 */
|
||
.smart-health-card {
|
||
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
/* margin-top 由 style 动态控制 */
|
||
color: white;
|
||
}
|
||
|
||
.health-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* gap: 12px; removed for uniapp-x support */
|
||
}
|
||
|
||
.health-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* gap: 4px; removed for uniapp-x support */
|
||
margin-bottom: 12px; /* acts as gap for health-content */
|
||
}
|
||
|
||
.health-title {
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
margin-bottom: 4px; /* acts as gap for health-header */
|
||
}
|
||
|
||
.health-subtitle {
|
||
font-size: 14px;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.health-tips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
/* gap: 12px; removed for uniapp-x support */
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.tip-item {
|
||
font-size: 13px;
|
||
padding: 6px 12px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 20px;
|
||
/* backdrop-filter: blur(10px); removed for uniapp-x support */
|
||
margin-right: 12px;
|
||
margin-bottom: 12px; /* acts as gap for health-tips */
|
||
}
|
||
|
||
/* 智能分类网格 */
|
||
/* ====== 分类展示层容器 (固定定位,分类栏 + 内联面板) ====== */
|
||
.category-wrapper {
|
||
position: relative;
|
||
z-index: 4;
|
||
background-color: #ffffff;
|
||
height: 64rpx;
|
||
}
|
||
|
||
.category-bar-wrap {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
background-color: #ffffff;
|
||
padding: 0;
|
||
height: 64rpx;
|
||
border-bottom-width: 1rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #f3f3f3;
|
||
}
|
||
|
||
.category-bar-wrap-hidden {
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
}
|
||
|
||
.category-scroll {
|
||
flex: 1;
|
||
height: 64rpx;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
flex-wrap: nowrap;
|
||
padding-left: 12rpx;
|
||
padding-right: 4rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.category-item {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 64rpx;
|
||
padding: 0 14rpx;
|
||
margin-right: 6rpx;
|
||
border-radius: 0;
|
||
background-color: transparent;
|
||
flex-shrink: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.category-item-active {
|
||
background-color: transparent;
|
||
}
|
||
|
||
.category-item-text {
|
||
font-size: 26rpx;
|
||
color: #333333;
|
||
white-space: nowrap;
|
||
line-height: 30rpx;
|
||
font-weight: 500;
|
||
transform: translateY(10rpx);
|
||
}
|
||
|
||
.category-item-text-active {
|
||
color: #e02e24;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.category-item-text-accent {
|
||
color: #14b85a;
|
||
}
|
||
|
||
.category-active-line {
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 2rpx;
|
||
width: 60rpx;
|
||
height: 4rpx;
|
||
border-radius: 4rpx;
|
||
background-color: #e02e24;
|
||
margin-left: -30rpx;
|
||
}
|
||
|
||
.category-expand-btn {
|
||
width: 60rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
border-left: 1rpx solid #f3f3f3;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.category-expand-icon {
|
||
font-size: 22rpx;
|
||
color: #999999;
|
||
line-height: 22rpx;
|
||
height: 22rpx;
|
||
transform: translateY(-10rpx);
|
||
}
|
||
|
||
.category-panel-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.08);
|
||
z-index: 999;
|
||
}
|
||
|
||
.category-panel {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1002;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #ffffff;
|
||
border-bottom-left-radius: 24rpx;
|
||
border-bottom-right-radius: 24rpx;
|
||
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.07);
|
||
overflow: hidden;
|
||
padding-bottom: 16rpx;
|
||
border-top-width: 0;
|
||
}
|
||
|
||
.category-panel-header {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 64rpx;
|
||
padding: 0 0 0 20rpx;
|
||
box-sizing: border-box;
|
||
border-bottom-width: 1rpx;
|
||
border-bottom-style: solid;
|
||
border-bottom-color: #f4f4f4;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.category-panel-title {
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
color: #2f2f2f;
|
||
line-height: 1;
|
||
transform: translateY(-6rpx);
|
||
}
|
||
|
||
.category-panel-close-btn {
|
||
min-width: 96rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
border-left-width: 1rpx;
|
||
border-left-style: solid;
|
||
border-left-color: #f4f4f4;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.category-panel-close-text {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
transform: translateY(-6rpx);
|
||
}
|
||
|
||
.category-panel-close-arrow {
|
||
font-size: 18rpx;
|
||
color: #666666;
|
||
margin-left: 6rpx;
|
||
line-height: 1;
|
||
transform: translateY(-6rpx);
|
||
}
|
||
|
||
.category-panel-grid {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
padding: 10rpx 12rpx 0 12rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.category-panel-item {
|
||
width: 25%;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 6rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.category-panel-item-active {
|
||
|
||
}
|
||
|
||
.category-panel-item-text {
|
||
width: 100%;
|
||
height: 62rpx;
|
||
background-color: #f6f6f6;
|
||
border-radius: 14rpx;
|
||
font-size: 25rpx;
|
||
color: #555555;
|
||
text-align: center;
|
||
line-height: 62rpx;
|
||
}
|
||
|
||
.category-panel-item-text-active {
|
||
color: #e02e24;
|
||
font-weight: 700;
|
||
background-color: #fff2f2;
|
||
border-width: 1rpx;
|
||
border-style: solid;
|
||
border-color: #f1c4c4;
|
||
}
|
||
|
||
/* ====== 热销区保留的 section-header 样式 ====== */
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #666;
|
||
}
|
||
|
||
.section-desc {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 健康资讯 */
|
||
.health-news {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.news-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.news-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.news-more {
|
||
font-size: 14px;
|
||
color: #ff5000;
|
||
/* cursor: pointer; removed for uvue support */
|
||
}
|
||
|
||
.news-swiper {
|
||
height: 200px;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.news-content {
|
||
position: relative;
|
||
height: 100%;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.news-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
}
|
||
|
||
.news-caption {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
line-height: 1.4;
|
||
display: flex;
|
||
}
|
||
|
||
/* 智能服务 */
|
||
.smart-services {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.services-grid {
|
||
display: flex;
|
||
flex-direction: row; /* Ensure items are in row */
|
||
flex-wrap: wrap;
|
||
/* gap: 20px; removed for uniapp-x support */
|
||
margin: 0 -1.5%;
|
||
}
|
||
|
||
.service-card {
|
||
width: 47%; /* 50 - 3 */
|
||
margin: 0 1.5% 20px 1.5%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.service-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.service-icon {
|
||
width: 60px;
|
||
height: 60px;
|
||
border-radius: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.service-icon-text {
|
||
font-size: 28px;
|
||
color: white;
|
||
}
|
||
|
||
.service-name {
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.service-desc {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 热搜词区域 */
|
||
.hot-keywords-section {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.keywords-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.keyword-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 16px;
|
||
background: #f5f5f5;
|
||
border-radius: 20px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.keyword-item:hover {
|
||
background: #fff0f0;
|
||
}
|
||
|
||
.keyword-rank {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
color: #999;
|
||
background: #eee;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.keyword-rank.top-three {
|
||
background: #ff4757;
|
||
color: white;
|
||
}
|
||
|
||
.keyword-text {
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
/* 首页分类流骨架 */
|
||
.category-feed-shell {
|
||
padding: 12rpx 0 24rpx;
|
||
}
|
||
|
||
.secondary-category-panel {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
padding: 12rpx 8rpx 8rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.secondary-category-item {
|
||
width: 20%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 10rpx 4rpx 8rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.secondary-category-item-active .secondary-category-name {
|
||
color: #e1251b;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.secondary-category-icon-wrap {
|
||
width: 68rpx;
|
||
height: 68rpx;
|
||
border-radius: 22rpx;
|
||
background: #f6f6f6;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 10rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.secondary-category-image {
|
||
width: 68rpx;
|
||
height: 68rpx;
|
||
border-radius: 22rpx;
|
||
}
|
||
|
||
.secondary-category-icon-text {
|
||
font-size: 30rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.secondary-category-name {
|
||
font-size: 22rpx;
|
||
color: #333333;
|
||
line-height: 1.2;
|
||
text-align: center;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.recommend-channel-section {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
padding: 8rpx 0 18rpx;
|
||
}
|
||
|
||
.recommend-channel-card {
|
||
width: 49%;
|
||
border-radius: 22rpx;
|
||
padding: 16rpx;
|
||
min-height: 236rpx;
|
||
margin-bottom: 14rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
box-sizing: border-box;
|
||
box-shadow: 0 3rpx 12rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.recommend-channel-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.recommend-channel-title-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.recommend-channel-title {
|
||
font-size: 30rpx;
|
||
color: #202020;
|
||
font-weight: 800;
|
||
line-height: 1.2;
|
||
margin-right: 8rpx;
|
||
}
|
||
|
||
.recommend-channel-badge {
|
||
font-size: 18rpx;
|
||
border-width: 1rpx;
|
||
border-style: solid;
|
||
border-radius: 6rpx;
|
||
padding: 2rpx 6rpx;
|
||
line-height: 1.2;
|
||
background: rgba(255, 255, 255, 0.72);
|
||
}
|
||
|
||
.recommend-channel-subtitle {
|
||
font-size: 22rpx;
|
||
color: #777777;
|
||
margin-top: 6rpx;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.recommend-channel-products {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
margin-top: 12rpx;
|
||
}
|
||
|
||
.recommend-channel-product {
|
||
width: 48%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.recommend-channel-product-image {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
border-radius: 12rpx;
|
||
background-color: #ffffff;
|
||
}
|
||
|
||
.recommend-channel-product-name {
|
||
margin-top: 6rpx;
|
||
font-size: 20rpx;
|
||
color: #333333;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.recommend-channel-price-row {
|
||
margin-top: 4rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.recommend-channel-product-tag {
|
||
font-size: 18rpx;
|
||
margin-right: 4rpx;
|
||
}
|
||
|
||
.recommend-channel-product-price {
|
||
font-size: 23rpx;
|
||
font-weight: 800;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.recommend-channel-market-price {
|
||
margin-top: 4rpx;
|
||
font-size: 18rpx;
|
||
color: #999999;
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.category-simple-channel-section {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
background: transparent;
|
||
padding: 0;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.simple-channel-card {
|
||
width: 49%;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-sizing: border-box;
|
||
padding: 22rpx 18rpx;
|
||
min-height: 148rpx;
|
||
background: #ffffff;
|
||
border-radius: 22rpx;
|
||
box-shadow: 0 3rpx 12rpx rgba(0, 0, 0, 0.03);
|
||
}
|
||
|
||
.simple-channel-card-with-divider {
|
||
border-right-width: 0;
|
||
}
|
||
|
||
.simple-channel-left {
|
||
flex: 1;
|
||
padding-right: 12rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.simple-channel-title-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
.simple-channel-icon {
|
||
font-size: 24rpx;
|
||
line-height: 1;
|
||
color: #202020;
|
||
margin-right: 6rpx;
|
||
}
|
||
|
||
.simple-channel-title {
|
||
font-size: 30rpx;
|
||
line-height: 1.2;
|
||
color: #202020;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.simple-channel-subtitle {
|
||
font-size: 22rpx;
|
||
line-height: 1.2;
|
||
color: #a0a0a0;
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.simple-channel-cover-wrap {
|
||
width: 116rpx;
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.simple-channel-cover-slot {
|
||
width: 54rpx;
|
||
height: 54rpx;
|
||
border-radius: 14rpx;
|
||
overflow: hidden;
|
||
background: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.simple-channel-cover-image {
|
||
width: 54rpx;
|
||
height: 54rpx;
|
||
border-radius: 14rpx;
|
||
}
|
||
|
||
.simple-channel-cover-fallback {
|
||
width: 54rpx;
|
||
height: 54rpx;
|
||
border-radius: 14rpx;
|
||
background: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.simple-channel-cover-text {
|
||
font-size: 22rpx;
|
||
line-height: 1;
|
||
color: #555555;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.feed-divider {
|
||
height: 20rpx;
|
||
background: #f3f4f6;
|
||
border-radius: 16rpx;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
/* 热销药品 */
|
||
.hot-products {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
flex-direction: column; /* 标题和筛选器垂直排列 */
|
||
align-items: flex-start;
|
||
margin-bottom: 20px;
|
||
width: 100%;
|
||
}
|
||
|
||
.title-section {
|
||
display: flex;
|
||
align-items: center;
|
||
/* gap: 8px; removed */
|
||
width: 100%;
|
||
}
|
||
|
||
.section-icon {
|
||
font-size: 20px;
|
||
color: #ff5000;
|
||
margin-right: 8px; /* Replacement for gap */
|
||
}
|
||
|
||
.sort-tabs {
|
||
display: flex;
|
||
flex-direction: row; /* UVUE 显式设置 row */
|
||
/* gap: 8px; removed */
|
||
align-items: center;
|
||
flex-wrap: wrap; /* 允许换行,实现自适应 */
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.sort-tab {
|
||
font-size: 13px;
|
||
color: #666;
|
||
padding: 8px 12px; /* 增加左右内边距 */
|
||
border-radius: 20px;
|
||
border: 1px solid #e0e0e0;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
flex: 1; /* 均分宽度 */
|
||
min-width: 70px; /* 设置最小宽度防止过窄 */
|
||
text-align: center;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-right: 8px; /* Replacement for gap */
|
||
}
|
||
|
||
.sort-tab.active {
|
||
background: #ff5000;
|
||
color: white;
|
||
border-color: #ff5000;
|
||
}
|
||
|
||
.sort-tab:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.sort-tab.active:hover {
|
||
background: #e64a00;
|
||
}
|
||
|
||
/* 产品网格 */
|
||
.products-grid {
|
||
display: flex; /* 替换 block 为 flex */
|
||
flex-direction: row; /* 确保横向排列 */
|
||
flex-wrap: wrap; /* 确保网格布局 */
|
||
/* gap: 10px; removed for uniapp-x support */
|
||
justify-content: space-between; /* use space-between instead of gap */
|
||
margin-top: 0;
|
||
min-height: 0;
|
||
padding-bottom: 20rpx;
|
||
}
|
||
|
||
.product-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #fff;
|
||
border-radius: 18rpx;
|
||
overflow: hidden;
|
||
width: 49%;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.products-grid-dense .product-card {
|
||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.product-image-wrapper {
|
||
width: 100%;
|
||
padding-bottom: 100%;
|
||
position: relative;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.product-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 18rpx 18rpx 0 0;
|
||
}
|
||
|
||
.product-image-wrapper-fixed {
|
||
padding-bottom: 100%;
|
||
background: #f2f2f2;
|
||
}
|
||
|
||
.product-card-skeleton {
|
||
background: #ffffff;
|
||
}
|
||
|
||
.product-placeholder-body {
|
||
padding: 14rpx 14rpx 16rpx;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.product-card-tags {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.product-card-tag {
|
||
height: 28rpx;
|
||
line-height: 28rpx;
|
||
padding: 0 8rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 18rpx;
|
||
font-weight: 700;
|
||
color: #fff7d1;
|
||
background: #e1251b;
|
||
margin-right: 6rpx;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.product-skeleton-title {
|
||
font-size: 24rpx;
|
||
color: #232323;
|
||
line-height: 1.35;
|
||
height: 64rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.product-highlight-text {
|
||
font-size: 20rpx;
|
||
line-height: 28rpx;
|
||
color: #7a7a7a;
|
||
margin-top: 6rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.product-service-tags {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex-wrap: wrap;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.product-service-tag {
|
||
height: 28rpx;
|
||
line-height: 28rpx;
|
||
padding: 0 8rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 18rpx;
|
||
font-weight: 600;
|
||
color: #12b76a;
|
||
background: #ecfdf3;
|
||
margin-right: 6rpx;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.product-price-row {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: flex-end;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.product-price-value {
|
||
font-size: 34rpx;
|
||
line-height: 1;
|
||
color: #ff1030;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.product-market-price {
|
||
font-size: 20rpx;
|
||
line-height: 1;
|
||
color: #9a9a9a;
|
||
text-decoration: line-through;
|
||
margin-left: 8rpx;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.product-sales-text {
|
||
font-size: 20rpx;
|
||
line-height: 28rpx;
|
||
color: #8f8f8f;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
.feed-loading-state,
|
||
.feed-empty-state,
|
||
.feed-end-state,
|
||
.load-more-status {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
padding: 28rpx 0 36rpx;
|
||
}
|
||
|
||
.feed-empty-state {
|
||
background: #ffffff;
|
||
border-radius: 18rpx;
|
||
}
|
||
|
||
.feed-empty-title {
|
||
font-size: 26rpx;
|
||
color: #333333;
|
||
font-weight: 600;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.feed-empty-desc,
|
||
.feed-end-text,
|
||
.loading-text {
|
||
font-size: 22rpx;
|
||
color: #8c8c8c;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: 13px;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
line-height: 1.4;
|
||
height: 36px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.product-bottom {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 8px 8px;
|
||
}
|
||
|
||
.product-price {
|
||
font-size: 15px;
|
||
color: #ff5000;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.product-add-btn {
|
||
width: 24px;
|
||
height: 24px;
|
||
background-color: #ff5000;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.add-icon {
|
||
color: #fff;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.cart-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
/* gap: 6px; removed */
|
||
background: #ff5000;
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
font-weight: bold;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.cart-btn:hover {
|
||
background: #388E3C;
|
||
}
|
||
|
||
.cart-icon {
|
||
font-size: 14px;
|
||
margin-right: 6px; /* Replacement for gap */
|
||
}
|
||
|
||
.cart-text {
|
||
font-size: 13px;
|
||
}
|
||
|
||
/* 家庭常备药 */
|
||
.family-medicine {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.section-subtitle {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-left: 12px;
|
||
}
|
||
|
||
.family-grid {
|
||
display: flex;
|
||
flex-direction: row; /* Ensure items are in row */
|
||
flex-wrap: wrap;
|
||
/* gap: 16px; removed for uniapp-x support */
|
||
margin: 0 -1.5%;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.family-item {
|
||
width: 47%; /* 50 - 3 */
|
||
margin: 0 1.5% 16px 1.5%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 16px;
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.family-item:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.family-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.family-icon-text {
|
||
font-size: 20px;
|
||
color: white;
|
||
}
|
||
|
||
.family-name {
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.family-desc {
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 智能推荐 */
|
||
.smart-recommend {
|
||
background: white;
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.recommend-filters {
|
||
display: flex;
|
||
flex-direction: row; /* UVUE 显式设置 row */
|
||
/* gap: 8px; removed for uniapp-x support */
|
||
align-items: center;
|
||
flex-wrap: wrap; /* 允许换行,实现自适应 */
|
||
justify-content: flex-start;
|
||
width: 100%;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.filter-item {
|
||
font-size: 13px;
|
||
color: #666;
|
||
padding: 8px 12px; /* 增加左右内边距 */
|
||
border-radius: 20px;
|
||
border: 1px solid #e0e0e0;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.2s ease;
|
||
white-space: nowrap;
|
||
flex: 1; /* 均分宽度 */
|
||
min-width: 80px; /* 设置最小宽度防止过窄 */
|
||
text-align: center;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-right: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.filter-item.active {
|
||
background: #ff5000;
|
||
color: white;
|
||
border-color: #ff5000;
|
||
}
|
||
|
||
.filter-item:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.filter-item.active:hover {
|
||
background: #e64a00;
|
||
}
|
||
|
||
.recommend-grid {
|
||
display: flex;
|
||
flex-direction: row; /* Ensure items are in row */
|
||
flex-wrap: wrap;
|
||
/* gap: 20px; removed for uniapp-x support */
|
||
margin: 0 -1.5%;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.recommend-product {
|
||
width: 97%; /* 1 col */
|
||
margin: 0 1.5% 20px 1.5%;
|
||
background: #f8f9fa;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.3s ease;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.recommend-product:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.product-image-container {
|
||
position: relative;
|
||
width: 100%;
|
||
padding-bottom: 100%;
|
||
}
|
||
|
||
.product-image-container .product-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: white;
|
||
}
|
||
|
||
.product-tags {
|
||
position: absolute;
|
||
top: 12px;
|
||
left: 12px;
|
||
display: flex;
|
||
flex-direction: row;
|
||
/* gap: 8px; removed */
|
||
}
|
||
|
||
.product-tag, .featured-tag {
|
||
padding: 4px 10px;
|
||
border-radius: 10px;
|
||
font-size: 11px;
|
||
font-weight: bold;
|
||
color: white;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.product-tag {
|
||
background: rgba(76, 175, 80, 0.9);
|
||
}
|
||
|
||
.featured-tag {
|
||
background: rgba(255, 87, 34, 0.9);
|
||
}
|
||
|
||
.product-details {
|
||
padding: 16px;
|
||
}
|
||
|
||
.product-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
line-height: 1.4;
|
||
display: flex;
|
||
}
|
||
|
||
.product-specification {
|
||
font-size: 13px;
|
||
color: #666;
|
||
margin-bottom: 12px;
|
||
display: flex;
|
||
}
|
||
|
||
.product-rating {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
/* gap: 8px; removed */
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.rating-stars {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
/* gap: 4px; removed */
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.star-icon {
|
||
font-size: 14px;
|
||
color: #FFC107;
|
||
margin-right: 2px;
|
||
}
|
||
|
||
.rating-value {
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.reviews-count {
|
||
color: #666;
|
||
}
|
||
|
||
.product-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.add-to-cart {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 18px;
|
||
background: #ff5000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.add-to-cart:hover {
|
||
background: #e64a00;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.cart-icon {
|
||
font-size: 16px;
|
||
color: white;
|
||
}
|
||
|
||
/* 健康提醒 */
|
||
.health-reminder {
|
||
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%);
|
||
border-radius: 16px;
|
||
padding: 16px 20px;
|
||
margin-bottom: 20px;
|
||
color: white;
|
||
}
|
||
|
||
.reminder-content {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
/* gap: 12px; removed */
|
||
}
|
||
|
||
.reminder-icon {
|
||
font-size: 24px;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.reminder-text {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* gap: 4px; removed */
|
||
}
|
||
|
||
.reminder-title {
|
||
font-size: 15px;
|
||
font-weight: bold;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.reminder-desc {
|
||
font-size: 13px;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.reminder-action {
|
||
padding: 6px 16px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 20px;
|
||
/* cursor: pointer; removed for uvue support */
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.reminder-action:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.action-text {
|
||
font-size: 13px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading-state {
|
||
padding: 40px 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 3px solid #f0f0f0;
|
||
border-top-color: #ff5000;
|
||
border-radius: 16px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.no-more {
|
||
padding: 30px 0;
|
||
text-align: center;
|
||
border-top: 1px solid #f0f0f0;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.no-more-text {
|
||
font-size: 13px;
|
||
color: #999;
|
||
}
|
||
|
||
/* 安全区域 */
|
||
.safe-area {
|
||
height: 20px;
|
||
width: 100%;
|
||
}
|
||
|
||
/* ===== 响应式设计 ===== */
|
||
|
||
/* 小屏手机 (小于414px) */
|
||
@media screen and (max-width: 414px) {
|
||
.search-container {
|
||
padding: 0 12px;
|
||
height: 44px;
|
||
}
|
||
|
||
.search-box {
|
||
padding: 0 4px 0 12px;
|
||
margin: 0;
|
||
}
|
||
|
||
.main-scroll {
|
||
padding: 0 12px 12px;
|
||
}
|
||
|
||
.category-grid {
|
||
margin: 0 -1%;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.category-grid .category-card {
|
||
width: 18%;
|
||
margin: 0 1% 6px 1%;
|
||
padding: 4px 0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
border: none;
|
||
}
|
||
|
||
.category-card:hover {
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.card-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 18px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.card-icon-text {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.card-name {
|
||
font-size: 10px;
|
||
font-weight: normal;
|
||
color: #333;
|
||
}
|
||
|
||
.card-desc {
|
||
display: none;
|
||
}
|
||
|
||
.services-grid .service-card {
|
||
width: 23%; /* 4 cols */
|
||
margin: 0 1% 8px 1%;
|
||
}
|
||
|
||
.service-card {
|
||
padding: 10px 4px;
|
||
background: #f8f9fa; /* 保持淡色背景 */
|
||
}
|
||
|
||
.service-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 20px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.service-icon-text {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.service-name {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.service-desc {
|
||
display: none; /* 隐藏描述 */
|
||
}
|
||
|
||
.load-more-status {
|
||
padding: 20px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
}
|
||
|
||
.loading-text {
|
||
color: #888;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.products-grid {
|
||
/* column-count: 2; removed for flex */
|
||
/* column-gap: 8px; removed */
|
||
}
|
||
|
||
.product-card {
|
||
width: 48%;
|
||
}
|
||
|
||
.recommend-grid .recommend-product {
|
||
width: 48%;
|
||
margin: 0 1% 8px 1%;
|
||
}
|
||
|
||
.product-info,
|
||
.product-details {
|
||
padding: 6px; /* 极小内边距 */
|
||
}
|
||
|
||
.product-name,
|
||
.product-title {
|
||
font-size: 13px; /* 调整字体大小 */
|
||
height: 36px; /* 限制高度,防止参差不齐 */
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
/* display: webkit-box removed for compatibility */
|
||
display: flex;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.product-image-wrapper {
|
||
padding-bottom: 100%;
|
||
}
|
||
|
||
/* 手机端商品卡片极简模式(热销 & 推荐) */
|
||
.hot-products .product-spec,
|
||
.hot-products .manufacturer,
|
||
.hot-products .original-price,
|
||
.hot-products .cart-text,
|
||
.hot-products .sales-info,
|
||
.hot-products .product-action, /* 隐藏热销区加购按钮 */
|
||
.smart-recommend .product-specification,
|
||
.smart-recommend .product-rating,
|
||
.smart-recommend .original-price,
|
||
.smart-recommend .product-actions /* 隐藏推荐区加购按钮 */
|
||
{
|
||
display: none;
|
||
}
|
||
|
||
.hot-products .product-info,
|
||
.smart-recommend .product-details {
|
||
padding: 6px; /* 极小内边距 */
|
||
}
|
||
|
||
.hot-products .product-image-wrapper,
|
||
.hot-products .product-image-container,
|
||
.smart-recommend .product-image-wrapper,
|
||
.smart-recommend .product-image-container {
|
||
padding-bottom: 100%;
|
||
}
|
||
|
||
.hot-products .product-name,
|
||
.smart-recommend .product-title {
|
||
margin-bottom: 2px;
|
||
font-size: 12px;
|
||
line-height: 1.3;
|
||
height: 32px; /* 限制2行高度 */
|
||
}
|
||
|
||
.hot-products .price-section,
|
||
.smart-recommend .price-section {
|
||
margin-bottom: 0;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.hot-products .price-symbol,
|
||
.smart-recommend .price-symbol {
|
||
font-size: 10px;
|
||
color: #FF5722;
|
||
}
|
||
|
||
.hot-products .price-value,
|
||
.smart-recommend .price-value {
|
||
font-size: 14px; /* 字体变小 */
|
||
font-weight: bold;
|
||
}
|
||
|
||
.family-grid .family-item {
|
||
width: 23%; /* 4 cols */
|
||
margin: 0 1% 8px 1%;
|
||
padding: 8px 4px;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
|
||
|
||
.family-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 18px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.family-icon-text {
|
||
font-size: 18px;
|
||
}
|
||
|
||
.family-name {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.family-desc {
|
||
display: none;
|
||
}
|
||
|
||
.news-swiper {
|
||
height: 160px;
|
||
}
|
||
|
||
.sort-tabs,
|
||
.recommend-filters {
|
||
/* gap: 8px; removed */
|
||
justify-content: flex-start; /* 保持左对齐 */
|
||
/* overflow-x: auto; REMOVED for uniapp-x support - use generic view wrapping instead */
|
||
/* flex-wrap: nowrap; REMOVED to allow wrapping on mobile since overflow-x not supported on view */
|
||
padding-bottom: 4px; /* 滚动条空间 */
|
||
}
|
||
|
||
.sort-tab,
|
||
.filter-item {
|
||
padding: 5px 12px;
|
||
font-size: 12px;
|
||
flex-shrink: 0; /* 防止被压缩 */
|
||
min-width: 0; /* CHANGED from auto to 0 */
|
||
flex: 0 0 auto; /* 取消均分 */
|
||
margin-right: 8px;
|
||
margin-bottom: 8px; /* Added spacing for wrapped items */
|
||
}
|
||
|
||
.section-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
/* gap: 12px; removed */
|
||
}
|
||
|
||
.title-section {
|
||
justify-content: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.sort-tabs,
|
||
.recommend-filters {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
/* 中屏手机/小平板 (415px-768px) */
|
||
@media screen and (min-width: 415px) and (max-width: 768px) {
|
||
.search-container {
|
||
padding: 0 16px;
|
||
height: 44px;
|
||
}
|
||
|
||
.main-scroll {
|
||
/* 移除 margin-top */
|
||
}
|
||
|
||
.category-grid .category-card {
|
||
width: 30.33%;
|
||
}
|
||
|
||
.services-grid .service-card {
|
||
width: 47%;
|
||
}
|
||
|
||
.products-grid {
|
||
/* column-count: 2; removed */
|
||
}
|
||
|
||
.product-card {
|
||
width: 48%;
|
||
}
|
||
|
||
.recommend-grid .recommend-product {
|
||
width: 47%;
|
||
}
|
||
|
||
.family-grid .family-item {
|
||
width: 30.33%;
|
||
}
|
||
|
||
.sort-tabs,
|
||
.recommend-filters {
|
||
/* gap: 10px; removed */
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.sort-tab,
|
||
.filter-item {
|
||
padding: 6px 14px;
|
||
font-size: 12px;
|
||
flex-grow: 0;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.section-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
/* gap: 12px; removed */
|
||
}
|
||
|
||
.title-section {
|
||
justify-content: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.sort-tabs,
|
||
.recommend-filters {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
/* 平板设备 (769px-1024px) */
|
||
@media screen and (min-width: 769px) and (max-width: 1024px) {
|
||
.search-container {
|
||
padding: 0 24px;
|
||
height: 44px;
|
||
}
|
||
|
||
.nav-search-tools .nav-tool-item {
|
||
display: none;
|
||
}
|
||
|
||
.main-scroll {
|
||
padding: 0 24px 20px;
|
||
}
|
||
|
||
.category-grid .category-card {
|
||
width: 22%;
|
||
}
|
||
|
||
.services-grid .service-card {
|
||
width: 22%;
|
||
}
|
||
|
||
.product-card {
|
||
width: 32%;
|
||
}
|
||
|
||
.recommend-grid .recommend-product {
|
||
width: 47%;
|
||
}
|
||
|
||
.family-grid .family-item {
|
||
width: 30.33%;
|
||
}
|
||
|
||
.news-swiper {
|
||
height: 240px;
|
||
}
|
||
}
|
||
|
||
/* 桌面端 (1025px以上) */
|
||
@media screen and (min-width: 1025px) {
|
||
.search-container {
|
||
padding: 0 32px;
|
||
height: 44px;
|
||
}
|
||
|
||
.main-scroll {
|
||
padding: 0 32px 24px;
|
||
}
|
||
|
||
.category-grid .category-card {
|
||
width: 13.66%;
|
||
}
|
||
|
||
.services-grid .service-card {
|
||
width: 22%;
|
||
}
|
||
|
||
.product-card {
|
||
width: 23%;
|
||
}
|
||
|
||
.recommend-grid .recommend-product {
|
||
width: 30.33%;
|
||
}
|
||
|
||
.family-grid .family-item {
|
||
width: 30.33%;
|
||
}
|
||
|
||
.news-swiper {
|
||
height: 260px;
|
||
}
|
||
}
|
||
|
||
/* 大桌面端 (1400px以上) */
|
||
@media screen and (min-width: 1400px) {
|
||
.category-grid {
|
||
margin-right: -24px;
|
||
}
|
||
.category-grid .category-card {
|
||
width: 17%;
|
||
margin: 0 1.5% 24px 1.5%;
|
||
}
|
||
|
||
.product-card {
|
||
width: 18%;
|
||
}
|
||
|
||
.recommend-grid .recommend-product {
|
||
width: 22%;
|
||
}
|
||
}
|
||
|
||
/* 暗黑模式适配 */
|
||
@media (prefers-color-scheme: dark) {
|
||
.medic-home {
|
||
background: #121212;
|
||
}
|
||
|
||
.smart-categories,
|
||
.health-news,
|
||
.smart-services,
|
||
.hot-products,
|
||
.family-medicine,
|
||
.smart-recommend {
|
||
background: #1e1e1e;
|
||
border-color: #333;
|
||
}
|
||
|
||
.nav-search-box {
|
||
background: rgba(30, 30, 30, 0.95);
|
||
border-color: #444;
|
||
}
|
||
|
||
.category-card,
|
||
.service-card,
|
||
.product-card,
|
||
.family-item,
|
||
.recommend-product {
|
||
background: #2d2d2d;
|
||
border-color: #444;
|
||
}
|
||
|
||
.section-title,
|
||
.card-name,
|
||
.service-name,
|
||
.product-name,
|
||
.family-name,
|
||
.product-title,
|
||
.section-desc,
|
||
.card-desc,
|
||
.service-desc,
|
||
.product-spec,
|
||
.product-specification,
|
||
.family-desc {
|
||
color: #aaa;
|
||
}
|
||
|
||
.sort-tab,
|
||
.filter-item {
|
||
background: #333;
|
||
border-color: #444;
|
||
color: #ccc;
|
||
}
|
||
|
||
.sort-tab.active,
|
||
.filter-item.active {
|
||
background: #ff5000;
|
||
color: white;
|
||
}
|
||
|
||
.sort-tab:hover,
|
||
.filter-item:hover {
|
||
background: #444;
|
||
}
|
||
|
||
.sort-tab.active:hover,
|
||
.filter-item.active:hover {
|
||
background: #e64a00;
|
||
}
|
||
|
||
.nav-tool-item {
|
||
background: rgba(255, 80, 0, 0.2);
|
||
color: #ff5000;
|
||
}
|
||
|
||
.manufacturer,
|
||
.sales-count,
|
||
.reviews-count,
|
||
.original-price {
|
||
color: #888;
|
||
}
|
||
}
|
||
</style>
|