Files
medical-mall/pages/main/index.uvue
2026-05-14 17:30:19 +08:00

3801 lines
87 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"
:categories="headerCategories"
:active-category="activeCategory"
@changeModule="handleTopModuleChange"
@update:searchKeyword="handleSearchKeywordUpdate"
@search="handleHeaderSearch"
@changeCategory="handleHeaderCategoryChange"
@focusSearch="handleSearchFocus"
></JdLikeHomeHeader>
</view>
<view class="jd-header-placeholder" :style="{ height: headerPlaceholderHeight + '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>
<HomeServiceContent
v-else
:active-category="activeCategory"
:categories="serviceCategories"
:entries="serviceEntries"
@change-category="handleHeaderCategoryChange"
@select-entry="handleServiceEntrySelect"
></HomeServiceContent>
<view class="safe-area" :style="{ height: bottomSafeArea + 88 + 'px' }"></view>
</scroll-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 HomeServiceContent from '@/components/home/HomeServiceContent.uvue'
import JdLikeHomeHeader from '@/components/home/JdLikeHomeHeader.uvue'
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 activeCategory = ref('recommend')
const searchKeyword = ref('')
type HeaderModuleItem = {
key: string
label: string
}
type ServiceHomeCategoryItem = {
id: string
name: string
}
type ServiceEntryItem = {
id: string
title: string
desc: string
icon: string
tone: string
category: string
linkText: string
route: string
}
const topModules: Array<HeaderModuleItem> = [
{ key: 'home', label: '首页' },
{ key: 'service', label: '服务' }
]
const serviceCategories: Array<ServiceHomeCategoryItem> = [
{ id: 'all', name: '推荐' },
{ id: 'nursing', name: '居家护理' },
{ id: 'rehab', name: '康复照护' },
{ id: 'assist', name: '适老用品' },
{ id: 'health', name: '健康管理' },
{ id: 'life', name: '生活服务' }
]
const serviceEntries: Array<ServiceEntryItem> = [
{ id: 'apply', title: '服务申请', desc: '快速发起上门护理与健康随访申请。', icon: '📝', tone: 'red', category: 'nursing', linkText: '立即申请', route: '/pages/mall/consumer/home-service/apply' },
{ id: 'service-home', title: '服务大厅', desc: '查看服务项目、适用对象与预约说明。', icon: '🏠', tone: 'blue', category: 'all', linkText: '查看大厅', route: '/pages/mall/consumer/home-service/index' },
{ id: 'progress', title: '服务进度', desc: '追踪评估、方案、执行和验收节点。', icon: '📍', tone: 'green', category: 'health', linkText: '查看工单', route: '/pages/mall/consumer/home-service/order-detail?id=case-001' },
{ id: 'assessment', title: '上门评估', desc: '了解评估前准备事项和风险核对重点。', icon: '🩺', tone: 'blue', category: 'rehab', linkText: '查看说明', route: '/pages/mall/consumer/home-service/assessment?id=case-001' },
{ id: 'plan', title: '服务方案', desc: '查看护理频次、服务周期和执行建议。', icon: '📋', tone: 'green', category: 'rehab', linkText: '方案说明', route: '/pages/mall/consumer/home-service/service-plan?id=case-001' },
{ id: 'execution', title: '上门执行', desc: '同步执行记录、签到留痕与异常处理。', icon: '🚪', tone: 'orange', category: 'life', linkText: '查看进展', route: '/pages/mall/consumer/home-service/order-detail?id=case-002' },
{ id: 'feedback', title: '验收反馈', desc: '服务完成后提交验收意见和满意度反馈。', icon: '✅', tone: 'red', category: 'nursing', linkText: '去反馈', route: '/pages/mall/consumer/home-service/feedback?id=case-001' },
{ id: 'settlement', title: '结算归档', desc: '查看归档说明、账单结果和服务闭环。', icon: '🗂️', tone: 'green', category: 'assist', linkText: '查看说明', route: '/pages/mall/consumer/home-service/settlement-archive?id=case-001' }
]
const headerCategories = computed((): Array<ServiceHomeCategoryItem | CategoryItem> => {
return activeTopModule.value == 'home' ? categoryList.value : serviceCategories
})
const headerSearchPlaceholder = computed((): string => {
if (activeTopModule.value == 'service') {
return '居家护理 / 康复照护 / 血压计 / 助餐服务'
}
return currentPlaceholderKeyword.value != '' ? currentPlaceholderKeyword.value : '感冒药 / 康复护理 / 居家护理 / 血压计'
})
// 分类标签栏相关
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
if (moduleKey == 'home') {
activeCategory.value = currentCategory.value
} else {
activeCategory.value = serviceCategories.length > 0 ? serviceCategories[0].id : 'all'
}
}
function handleHeaderCategoryChange(categoryId: string) {
activeCategory.value = categoryId
if (activeTopModule.value == 'home') {
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id == categoryId)
if (matchedItem != null) {
handleCategoryTabClick(matchedItem)
}
}
}
function handleServiceEntrySelect(entry: ServiceEntryItem) {
if (entry.route == '') {
uni.showToast({ title: '该入口下一步补页面', icon: 'none' })
return
}
uni.navigateTo({ url: entry.route })
}
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 => {
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[] {
if (categoryId === 'recommend') {
return []
}
return [
{
id: 'rank',
title: '排行榜',
subtitle: '每日更新',
routeType: 'rank',
icon: '榜',
coverImages: buildSimpleChannelCoverImages(0),
categoryId
},
{
id: 'quality',
title: '品质优选',
subtitle: '精选好货',
routeType: 'quality',
icon: '选',
coverImages: buildSimpleChannelCoverImages(2),
categoryId
}
]
}
function applyChannelDisplay(categoryId: string): void {
if (categoryId === 'recommend') {
marketingChannels.value = getRecommendMarketingChannels()
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] 没有二级分类,直接跳转到分类页')
uni.setStorageSync('selectedCategory', category.id)
uni.switchTab({
url: '/pages/main/category'
})
}
}
// 点击二级分类
const onSubCategoryClick = (category: Category): void => {
// 跳转到分类页面
uni.setStorageSync('selectedCategory', category.id)
const timestamp = Date.now()
const randomParam = Math.random().toString(36).substring(2, 8)
const url = `/pages/main/category?categoryId=${category.id}&name=${encodeURIComponent(category.name)}&timestamp=${timestamp}&random=${randomParam}`
uni.switchTab({
url: '/pages/main/category'
})
}
// 获取品牌数据
const loadBrands = async (): Promise<void> => {
try {
const brandsData = await supabaseService.getBrands()
brands.value = brandsData
} catch (e) {
console.error('加载品牌失败:', e)
brands.value = []
}
}
// 根据品牌名称获取图标
function getBrandIcon(name: string): string {
if (name == null || name === '') {
return '🏢'
}
// 常见品牌图标映射(使用数组方式避免 Object.keys 问题)
const iconKeys = [
'感冒', '发烧', '咳嗽', '消炎', '维生素', '钙片', '胃药', '止痛', '过敏', '皮肤', '眼药水', '口腔', '血压', '血糖', '血脂', '保健', '养生', '减肥', '美容', '母婴', '儿童', '老人', '男性', '女性', '维生素C', '维生素D', '蛋白粉', '鱼油', '蜂胶', '阿胶', '红枣', '枸杞', '菊花', '金银花', '口罩', '消毒液', '体温计', '创可贴', '棉签',
'九芝堂', '同仁堂', '云南白药', '东阿阿胶', '太极', '江中', '三九', '华素制药', '汤臣倍健', '白云山', '修正', '葵花', '哈药', '扬子江', '恒瑞', '复星', '辉瑞', '阿斯利康', '罗氏', '默沙东', '赛诺菲', '诺华', '雅培', '雀巢', '蒙牛', '伊利', '海尔', '美的', '飞利浦', '西门子', '松下', '苏泊尔', '九阳', '华为', '小米', '苹果', '三星'
]
const iconValues = [
'💊', '🌡️', '😷', '🔬', '💊', '🦴', '🫁', '💉', '🌸', '🧴', '👁️', '🦷', '❤️', '🩸', '💓', '🧬', '🍵', '⚖️', '💅', '👶', '🧒', '👴', '♂️', '♀️', '🍊', '☀️', '🥛', '🐟', '🐝', '🍯', '🫘', '🌿', '🌼', '🌸', '😷', '🧴', '🌡️', '🩹', '🧺',
'📜', '🏛️', '⛰️', '🍯', '☯️', '🌿', '9⃣', '💊', '💪', '⛰️', '🩹', '🌻', '🧪', '🚢', '🔬', '⭐', '🧬', '🧪', '🧬', '💊', '🧬', '🔬', '🏥', '🥣', '🐄', '🥛', '🏠', '❄️', '🪒', '⚡', '🔋', '🍳', '🥛', '📱', '🍚', '🍎', '📱'
]
// 尝试精确匹配
for (let i = 0; i < iconKeys.length; i++) {
if (name === iconKeys[i]) {
return iconValues[i]
}
}
// 尝试模糊匹配
for (let i = 0; i < iconKeys.length; i++) {
if (name.indexOf(iconKeys[i]) !== -1) {
return iconValues[i]
}
}
// 默认返回品牌图标
return '🏢'
}
function getCategoryDisplayIcon(category: Category): string {
const icon = category.icon ?? ''
if (icon !== '' && !isImageIcon(icon)) {
return icon
}
const name = category.name ?? ''
if (name !== '') {
return name.substring(0, 1)
}
return '品'
}
function shouldHighlightCategory(name: string): boolean {
if (name === '') {
return false
}
return name.indexOf('明天达') !== -1 || name.indexOf('快') !== -1
}
function getCategoryTabDisplayName(name: string): string {
if (shouldHighlightCategory(name)) {
return '⚡' + name
}
return name
}
function buildListItemKey(prefix: string, id: string, index: number): string {
const normalizedId = id !== '' ? id : 'empty'
return prefix + '-' + normalizedId + '-' + index.toString()
}
function getCategoryLabelById(categoryId: string): string {
if (categoryId === 'recommend') {
return '推荐'
}
const matchedItem = categoryList.value.find((item: CategoryItem): boolean => item.id === categoryId)
if (matchedItem != null) {
return matchedItem.name
}
return '精选'
}
function 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
}
}
}
// 分类标签栏交互
function handleCategoryTabClick(item: CategoryItem): void {
currentCategory.value = item.id
categoryScrollIntoView.value = 'cat-' + item.id
void (async (): Promise<void> => {
currentFeedCategoryId.value = item.id
currentPage.value = 1
hasMore.value = true
selectedSubCategoryId.value = ''
if (item.id === 'recommend') {
subCategories.value = []
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
const result = await supabaseService.getSmartRecommendations(1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
hasMore.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 {
loading.value = true
const result = await supabaseService.getProductsByCategory(item.id, 1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
hotProducts.value = []
hasMore.value = false
} finally {
loading.value = false
}
})()
}
function selectCategoryFromPanel(item: CategoryItem): void {
currentCategory.value = item.id
showCategoryPanel.value = false
categoryScrollIntoView.value = 'cat-' + item.id
void (async (): Promise<void> => {
currentFeedCategoryId.value = item.id
currentPage.value = 1
hasMore.value = true
selectedSubCategoryId.value = ''
if (item.id === 'recommend') {
subCategories.value = []
secondaryCategoryDisplay.value = buildSecondaryCategoryDisplay(item.id)
applyChannelDisplay(item.id)
try {
const result = await supabaseService.getSmartRecommendations(1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('加载热销商品失败:', error)
hotProducts.value = []
hasMore.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 {
loading.value = true
const result = await supabaseService.getProductsByCategory(item.id, 1, defaultLoadLimit)
failedProductImageIds.value = []
setHotProducts(result.data)
hasMore.value = result.hasmore
} catch (error) {
console.error('分类商品加载失败', error)
hotProducts.value = []
hasMore.value = false
} finally {
loading.value = false
}
})()
}
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('[IndexInit] 准备加载首页数据,确认当前请求将使用上述 SUPA_URL')
// 首先确保用户资料已加载
try {
await getCurrentUser()
console.log('主页初始化:用户资料加载完成')
} catch (error) {
console.error('加载用户资料失败:', error)
}
await loadCategories()
await loadBrands()
await loadHotKeywords()
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 categoryTopGap = Math.round(12 * 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 + categoryTopGap + categoryRowHeight + headerBottomPadding
searchRowStyle.value = `padding-top:${statusBarHeight.value}px;height:${searchRowTotalH}px;padding-right:${rightReserve}px;`
navbarTotalHeight.value = searchRowTotalH
} else {
navBarHeight.value = 44
navBarRight.value = 0
const searchRowTotalH = statusBarHeight.value + Math.max(44, moduleRowHeight) + searchTopGap + searchContentHeight + categoryTopGap + categoryRowHeight + headerBottomPadding
searchRowStyle.value = `padding-top:${statusBarHeight.value}px;height:${searchRowTotalH}px;`
navbarTotalHeight.value = searchRowTotalH
}
headerStyle.value = ``
categoryBarHeightPx.value = categoryRowHeight
headerPlaceholderHeight.value = navbarTotalHeight.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()
// 重置导航栏显示状态
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)
// 使用Storage传递参数确保switchTab后能被读取
uni.setStorageSync('selectedCategory', categoryId)
// 生成唯一的时间戳和随机参数,确保每次跳转都是新的页面
const timestamp = Date.now()
const randomParam = Math.random().toString(36).substring(2, 8)
// 构建带参数的URL直接通过URL传递分类信息
const url = `/pages/main/category?categoryId=${categoryId}&name=${encodeURIComponent(categoryName)}&timestamp=${timestamp}&random=${randomParam}`
uni.switchTab({
url: '/pages/main/category',
success: () => {
// 通过 Storage 传递参数已在上面设置
console.log('跳转分类页面成功categoryId:', 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%;
}
.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(-5rpx);
}
.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: 72rpx;
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;
}
.category-panel-close-btn {
min-width: 96rpx;
height: 72rpx;
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;
}
.category-panel-close-arrow {
font-size: 18rpx;
color: #666666;
margin-left: 6rpx;
line-height: 1;
}
.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>