添加首页加载skeleton

This commit is contained in:
2026-05-26 17:04:04 +08:00
parent 9680276b3f
commit 2f528c049f
10 changed files with 1329 additions and 410 deletions

View File

@@ -1,6 +1,7 @@
<!-- pages/main/index.uvue -->
<template>
<view class="medic-home">
<view :class="['home-main-stage', pageLoading ? 'home-main-stage-hidden' : 'home-main-stage-visible']">
<view class="jd-header-fixed">
<JdLikeHomeHeader
:status-bar-height="statusBarHeight"
@@ -59,6 +60,7 @@
<HomeMallContent
v-if="activeTopModule == 'home'"
:current-category="currentCategory"
:page-loading="pageLoading"
:selected-sub-category-id="selectedSubCategoryId"
:secondary-category-display="secondaryCategoryDisplay"
:marketing-channels="marketingChannels"
@@ -220,6 +222,22 @@
</view>
</view>
</view>
</view>
<view
v-if="showHomeSkeleton"
:class="['home-skeleton-overlay', pageLoading ? 'home-skeleton-overlay-visible' : 'home-skeleton-overlay-leaving']"
>
<HomeSkeleton
:status-bar-height="statusBarHeight"
:capsule-right="navBarRight"
:bottom-safe-area="bottomSafeArea"
:show-category-bar="true"
:category-count="7"
:channel-count="4"
:product-count="8"
></HomeSkeleton>
</view>
</view>
</template>
@@ -228,6 +246,7 @@ import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
import { onShow, onLoad, onHide } from '@dcloudio/uni-app'
import HomeMallContent from '@/components/home/HomeMallContent.uvue'
import JdLikeHomeHeader from '@/components/home/JdLikeHomeHeader.uvue'
import HomeSkeleton from '@/components/home-skeleton/home-skeleton.uvue'
import { fetchHomeServiceCatalog } from '@/services/homeServiceService.uts'
import type { HomeServiceCatalogType } from '@/types/home-service.uts'
import supabaseService from '@/utils/supabaseService.uts'
@@ -246,6 +265,11 @@ const refreshing = ref(false)
const loading = ref(false)
const isFirstShow = ref(true)
const hasMore = ref(true)
const pageLoading = ref(true)
const goodsLoaded = ref(false)
const categoryLoaded = ref(false)
const bannerLoaded = ref(false)
const skeletonExiting = ref(false)
const activeSort = ref('recommend') // 默认展示智能推荐
const activeFilter = ref('recommend')
const currentPage = ref(1)
@@ -598,6 +622,11 @@ const nextPlaceholderKeyword = ref(placeholderKeywords.value.length > 1 ? placeh
const placeholderAnimating = ref(false)
let placeholderTimer: number = 0
let placeholderAnimationTimer: number = 0
let skeletonTimer: number = 0
const showHomeSkeleton = computed((): boolean => {
return pageLoading.value || skeletonExiting.value
})
const homeEmptyStateTitle = computed((): string => {
if (currentFeedCategoryId.value == 'recommend') {
@@ -660,6 +689,42 @@ function stopPlaceholderScroll(): void {
}
}
function resetInitialHomeLoadFlags(): void {
goodsLoaded.value = false
categoryLoaded.value = false
bannerLoaded.value = false
}
function clearSkeletonTimer(): void {
if (skeletonTimer != 0) {
clearTimeout(skeletonTimer)
skeletonTimer = 0
}
}
function beginHomeSkeleton(): void {
clearSkeletonTimer()
pageLoading.value = true
skeletonExiting.value = false
}
function finishHomeSkeleton(): void {
if (!pageLoading.value) {
return
}
pageLoading.value = false
skeletonExiting.value = true
clearSkeletonTimer()
skeletonTimer = setTimeout(() => {
skeletonExiting.value = false
skeletonTimer = 0
}, 280)
}
async function loadHomeBannerData(): Promise<void> {
return
}
function startPlaceholderScroll(): void {
try {
if (placeholderTimer != 0) {
@@ -1910,9 +1975,17 @@ const searchByKeyword = (keyword: string): void => {
}
// 初始化数据
const initData = async () => {
type InitDataOptions = {
showSkeleton: boolean
}
const initData = async (options: InitDataOptions = { showSkeleton: false }) => {
logSupaConfig()
console.log('[consumer-db] 首页开始加载数据')
resetInitialHomeLoadFlags()
if (options.showSkeleton) {
beginHomeSkeleton()
}
// 首先确保用户资料已加载
try {
await getCurrentUser()
@@ -1920,16 +1993,50 @@ const initData = async () => {
} catch (error) {
console.error('加载用户资料失败:', error)
}
await loadMedicalMallCategories()
await loadBrands()
await loadHotKeywords()
await loadServiceHomeData()
if (await consumeSelectedCategoryFromStorage()) {
await loadRecommendedProducts(defaultLoadLimit)
return
const categoryTask = (async (): Promise<void> => {
try {
await loadMedicalMallCategories()
} finally {
categoryLoaded.value = true
}
})()
const bannerTask = (async (): Promise<void> => {
try {
await loadHomeBannerData()
} finally {
bannerLoaded.value = true
}
})()
const goodsTask = (async (): Promise<void> => {
try {
await categoryTask
if (!(await consumeSelectedCategoryFromStorage())) {
await loadCategoryGoods(currentCategory.value)
}
} finally {
goodsLoaded.value = true
}
})()
const supplementTask = (async (): Promise<void> => {
await Promise.all([
loadBrands(),
loadHotKeywords(),
loadServiceHomeData()
])
try {
await loadRecommendedProducts(defaultLoadLimit)
} catch (error) {
console.error('加载推荐频道商品失败:', error)
recommendedProducts.value = []
}
})()
try {
await Promise.all([categoryTask, goodsTask, bannerTask, supplementTask])
} finally {
if (options.showSkeleton && categoryLoaded.value && goodsLoaded.value && bannerLoaded.value) {
finishHomeSkeleton()
}
}
await loadCategoryGoods(currentCategory.value)
await loadRecommendedProducts(defaultLoadLimit)
}
@@ -2035,7 +2142,7 @@ const initPage = () => {
// 生命周期
onMounted(() => {
initPage()
initData()
void initData({ showSkeleton: true })
startPlaceholderScroll()
})
@@ -2085,6 +2192,7 @@ onHide(() => {
onUnmounted(() => {
stopPlaceholderScroll()
clearSkeletonTimer()
})
// 处理滚动事件
@@ -2202,7 +2310,7 @@ const onRefresh = async () => {
try {
// 重新加载数据
await initData()
await initData({ showSkeleton: false })
} catch (e) {
console.error('刷新数据失败:', e)
} finally {
@@ -2432,6 +2540,41 @@ const navigateToReminders = () => uni.navigateTo({ url: '/pages/user/reminders'
flex-direction: column;
}
.home-main-stage {
flex: 1;
min-height: 0;
transition: opacity 260ms ease;
}
.home-main-stage-hidden {
opacity: 0;
}
.home-main-stage-visible {
opacity: 1;
}
.home-skeleton-overlay {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1200;
background: #f6f8fb;
transition: opacity 260ms ease;
}
.home-skeleton-overlay-visible {
opacity: 1;
pointer-events: auto;
}
.home-skeleton-overlay-leaving {
opacity: 0;
pointer-events: none;
}
.jd-header-fixed {
position: fixed;
top: 0;