Files
medical-mall/pages/main/index.uvue

4635 lines
106 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- pages/main/index.uvue -->
<template>
<view class="medic-home">
<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"
: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"
: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"
@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>
</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 { 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 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 buildMockServiceProducts(): Array<HomeCareServiceProductType> {
// TODO: 后续替换为服务首页专用接口,当前仅在真实服务目录为空时兜底。
return [
{
id: 'svc-001',
title: '基础上门照护',
subtitle: '协助起居、日常陪护、健康观察',
categoryId: 'basic_care',
price: 99,
unit: '次',
tags: ['平台认证', '可预约'],
salesText: '已服务230+',
imageText: '护',
coverGradient: getServiceGradient('basic_care'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-001&mode=booking'
},
{
id: 'svc-002',
title: '居家康复指导',
subtitle: '术后恢复、动作训练、康复评估',
categoryId: 'rehab',
price: 129,
unit: '次',
tags: ['康复指导', '上门服务'],
salesText: '已服务180+',
imageText: '康',
coverGradient: getServiceGradient('rehab'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-002&mode=booking'
},
{
id: 'svc-mock-escort',
title: '陪诊陪护服务',
subtitle: '挂号陪同、检查陪同、取药协助',
categoryId: 'escort',
price: 168,
unit: '次',
tags: ['陪诊服务', '安心陪护'],
salesText: '已服务320+',
imageText: '陪',
coverGradient: getServiceGradient('escort'),
detailPath: '',
bookingPath: ''
},
{
id: 'svc-003',
title: '慢病随访服务',
subtitle: '血压血糖记录、健康建议、定期回访',
categoryId: 'chronic',
price: 79,
unit: '次',
tags: ['慢病管理', '健康随访'],
salesText: '已服务150+',
imageText: '访',
coverGradient: getServiceGradient('chronic'),
detailPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003',
bookingPath: '/pages/mall/consumer/home-service/service-detail?id=svc-003&mode=booking'
}
]
}
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()
if (catalog.length > 0) {
allServiceProducts.value = buildServiceProductsFromCatalog(catalog)
} else {
allServiceProducts.value = buildMockServiceProducts()
}
} catch (error) {
console.error('加载服务首页数据失败', error)
allServiceProducts.value = buildMockServiceProducts()
} 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 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([
'neo2无人机',
'华为mate50',
'钙片',
'感冒药',
'维生素C',
])
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
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 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 buildSimpleCategoryChannels(categoryId: string): SimpleCategoryChannel[] {
return []
}
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 = buildVisibleRecommendChannels()
categorySimpleChannels.value = []
return
}
marketingChannels.value = []
categorySimpleChannels.value = buildSimpleCategoryChannels(categoryId)
}
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.getProductsByCategory(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 loadCategories = async (): Promise<void> => {
try {
const categoriesData = await supabaseService.getParentCategories()
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.getSubCategories(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 '精选'
}
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 matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id == nextCategoryId)
if (matchedItem != null) {
await refreshHomeCategory(matchedItem)
return true
}
await refreshHomeCategory({
id: nextCategoryId,
name: getCategoryLabelById(nextCategoryId)
})
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('调用 getProductsBySales')
return await supabaseService.getProductsBySales(page, limit)
case 'price':
console.log('调用 getProductsByPrice, 升序:', priceAscending.value)
return await supabaseService.getProductsByPrice(page, limit, priceAscending.value)
case 'new':
console.log('调用 getProductsByNewest')
return await supabaseService.getProductsByNewest(page, limit)
case 'recommend':
console.log('调用 getSmartRecommendations')
return await supabaseService.getSmartRecommendations(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('调用默认 getProductsBySales')
return await supabaseService.getProductsBySales(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)
hasMore.value = result.hasmore
currentPage.value = page
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
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.getSubCategories(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 {
let result: PaginatedResponse<Product>
switch (activeSort.value) {
case 'sales':
result = await supabaseService.getProductsBySales(1, defaultLoadLimit)
break
case 'price':
result = await supabaseService.getProductsByPrice(1, defaultLoadLimit, priceAscending.value)
break
case 'new':
result = await supabaseService.getProductsByNewest(1, defaultLoadLimit)
break
case 'discount': {
const products = await supabaseService.getDiscountProducts(defaultLoadLimit)
result = {
data: products,
total: products.length,
page: 1,
limit: defaultLoadLimit,
hasmore: false
}
break
}
case 'recommend':
default:
result = await supabaseService.getSmartRecommendations(1, defaultLoadLimit)
break
}
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
currentPage.value = 1
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
hasMore.value = false
}
} else {
try {
loading.value = true
const result = await supabaseService.getProductsByCategory(categoryId, 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
}
}
}
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.getSmartRecommendations(1, defaultLoadLimit)
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('加载推荐商品失败:', error)
hotProducts.value = []
hasMore.value = false
} finally {
loading.value = false
}
return
}
try {
const subData = await supabaseService.getSubCategories(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.getProductsByCategory(item.id, 1, defaultLoadLimit)
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
hotProducts.value = []
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> => {
recommendedProducts.value = await supabaseService.getRecommendedProducts(limit)
}
// 加载热搜词
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)}`
})
}
// 初始化数据
const initData = async () => {
logSupaConfig()
console.log('[consumer-db] 首页开始加载数据')
// 首先确保用户资料已加载
try {
await getCurrentUser()
console.log('主页初始化:用户资料加载完成')
} catch (error) {
console.error('加载用户资料失败:', error)
}
await loadCategories()
await loadBrands()
await loadHotKeywords()
await loadServiceHomeData()
if (await consumeSelectedCategoryFromStorage()) {
await loadRecommendedProducts(defaultLoadLimit)
return
}
await loadCategoryGoods(currentCategory.value)
await loadRecommendedProducts(defaultLoadLimit)
}
// 家庭常备药
const familyItems = [
{
id: 'family1',
name: '创可贴',
desc: '伤口护理',
icon: '🩹',
color: '#FF5722',
categoryId: 'external'
},
{
id: 'family2',
name: '体温计',
desc: '健康监测',
icon: '🌡️',
color: '#2196F3',
categoryId: 'device'
},
{
id: 'family3',
name: '消毒酒精',
desc: '环境消毒',
icon: '🧪',
color: '#ff5000',
categoryId: 'external'
},
{
id: 'family4',
name: '口罩',
desc: '日常防护',
icon: '😷',
color: '#607D8B',
categoryId: 'device'
},
{
id: 'family5',
name: '退热贴',
desc: '物理降温',
icon: '🧊',
color: '#00BCD4',
categoryId: 'cold'
},
{
id: 'family6',
name: '棉签纱布',
desc: '伤口处理',
icon: '🩹',
color: '#FF9800',
categoryId: 'external'
}
]
// 初始化页面
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()
initData()
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()
})
// 处理滚动事件
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 服务处理
loadHotProducts(1, defaultLoadLimit)
}
// 切换筛选器
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()
} 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 nextPage = currentPage.value + 1
const currentCount = hotProducts.value.length
console.log('开始加载更多,当前数量:', currentCount, '页码:', nextPage, '分类:', currentFeedCategoryId.value)
if (currentFeedCategoryId.value === 'recommend') {
const result = await fetchSortedProductsPage(nextPage, defaultLoadLimit)
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.getProductsByCategory(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;
}
.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>