feat:增加骨架屏

This commit is contained in:
zerosaturation 2026-07-13 15:08:28 +08:00
parent 6a6d5f9deb
commit c4b8800ea7
3 changed files with 61 additions and 17 deletions

View File

@ -1,6 +1,11 @@
<template> <template>
<view class="banner-carousel" @click.stop> <view class="banner-carousel" @click.stop>
<swiper class="banner-swiper" :autoplay="true" :interval="4000" :duration="400" :circular="true" <!-- 骨架屏加载中或首次无数据时显示 -->
<view v-if="showSkeleton" class="banner-skeleton">
<view class="skeleton-banner"></view>
</view>
<swiper v-else class="banner-swiper" :autoplay="true" :interval="4000" :duration="400" :circular="true"
:indicator-dots="false" @change="onSwiperChange"> :indicator-dots="false" @change="onSwiperChange">
<!-- <swiper-item @click.stop="$emit('top3Click')" style="display: flex; <!-- <swiper-item @click.stop="$emit('top3Click')" style="display: flex;
align-items: center;"> align-items: center;">
@ -20,16 +25,18 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref, computed } from 'vue'
import BannerTop3 from '../../components/BannerTop3.vue' import BannerTop3 from '../../components/BannerTop3.vue'
defineProps({ const props = defineProps({
bannerActivities: { type: Array, default: () => [] }, bannerActivities: { type: Array, default: () => [] },
// //
banners: { banners: {
type: Array, type: Array,
default: () => [] default: () => []
} },
// useBanner
loading: { type: Boolean, default: false }
}) })
const emit = defineEmits(['activityClick', 'top3Click', 'bannerClick']) const emit = defineEmits(['activityClick', 'top3Click', 'bannerClick'])
@ -43,6 +50,13 @@ const onTop3DataLoaded = (items) => {
// BannerTop3 // BannerTop3
console.log('[BannerCarousel] top3 data loaded', items) console.log('[BannerCarousel] top3 data loaded', items)
} }
// banner
const showSkeleton = computed(() => {
if (props.loading) return true
const total = (props.bannerActivities?.length || 0) + (props.banners?.length || 0)
return total === 0
})
</script> </script>
<style scoped> <style scoped>
@ -95,4 +109,36 @@ const onTop3DataLoaded = (items) => {
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
} }
/* 骨架屏 */
.banner-skeleton {
width: 100%;
height: 392rpx;
border-radius: 24rpx;
overflow: hidden;
box-sizing: border-box;
}
.skeleton-banner {
width: 100%;
height: 100%;
border-radius: 24rpx;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0.1) 25%,
rgba(255, 255, 255, 0.25) 50%,
rgba(255, 255, 255, 0.1) 75%
);
background-size: 200% 100%;
animation: banner-skeleton-shimmer 1.5s infinite;
}
@keyframes banner-skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style> </style>

View File

@ -2,21 +2,13 @@ import { ref } from 'vue'
import { getActivityListApi, getMintingActivitiesApi } from '@/utils/api.js' import { getActivityListApi, getMintingActivitiesApi } from '@/utils/api.js'
import { getPreloadApi } from '@/utils/preloadApi/index' import { getPreloadApi } from '@/utils/preloadApi/index'
// 默认 banner(接口失败/无数据时兜底)
const FALLBACK_BANNER = [
{
image_url: '/static/sucai/image-05.png',
title: '这河非遇美学宇宙联名',
link_type: 'activity',
link_value: '1'
}
]
/** /**
* 模块级共享状态单例 * 模块级共享状态单例
*/ */
const bannerActivities = ref([]) const bannerActivities = ref([])
const banners = ref([]) const banners = ref([])
// 加载态:用于骨架屏展示(任一 fetch 未结束均为 true
const loading = ref(false)
// 内部:优先走预拉缓存,未命中/过期则手动 fetch // 内部:优先走预拉缓存,未命中/过期则手动 fetch
async function _tryPreloadThenFetch(key, fallbackFetch, processFn) { async function _tryPreloadThenFetch(key, fallbackFetch, processFn) {
@ -35,6 +27,7 @@ async function _tryPreloadThenFetch(key, fallbackFetch, processFn) {
} }
const loadBannerActivities = async () => { const loadBannerActivities = async () => {
loading.value = true
try { try {
await _tryPreloadThenFetch('banner.activities', await _tryPreloadThenFetch('banner.activities',
() => getActivityListApi(uni.getStorageSync('star_id') || null, 1, 10), () => getActivityListApi(uni.getStorageSync('star_id') || null, 1, 10),
@ -46,10 +39,13 @@ const loadBannerActivities = async () => {
) )
} catch (e) { } catch (e) {
console.error('[useBanner] 加载 banner 活动失败', e?.message ?? e) console.error('[useBanner] 加载 banner 活动失败', e?.message ?? e)
} finally {
loading.value = false
} }
} }
const loadBanners = async () => { const loadBanners = async () => {
loading.value = true
try { try {
await _tryPreloadThenFetch('banner.minting', await _tryPreloadThenFetch('banner.minting',
() => getMintingActivitiesApi(null, 1, 10), () => getMintingActivitiesApi(null, 1, 10),
@ -66,12 +62,12 @@ const loadBanners = async () => {
params: activity.params params: activity.params
})) }))
} }
if (banners.value.length === 0) banners.value = [...FALLBACK_BANNER]
} }
) )
} catch (e) { } catch (e) {
console.error('[useBanner] 加载运营 banner 失败', e?.message ?? e) console.error('[useBanner] 加载运营 banner 失败', e?.message ?? e)
banners.value = [...FALLBACK_BANNER] } finally {
loading.value = false
} }
} }
@ -79,6 +75,7 @@ export function useBanner() {
return { return {
bannerActivities, bannerActivities,
banners, banners,
loading,
loadBannerActivities, loadBannerActivities,
loadBanners loadBanners
} }

View File

@ -61,6 +61,7 @@
<BannerCarousel <BannerCarousel
:bannerActivities="bannerActivities" :bannerActivities="bannerActivities"
:banners="banners" :banners="banners"
:loading="bannerLoading"
@activityClick="handleActivityClick" @activityClick="handleActivityClick"
@top3Click="showRankingModal = true" @top3Click="showRankingModal = true"
@bannerClick="handleBannerClick" @bannerClick="handleBannerClick"
@ -168,7 +169,7 @@ const onScroll = (e) => {
}; };
// ========== Composables ========== // ========== Composables ==========
const { bannerActivities, banners, loadBannerActivities, loadBanners } = const { bannerActivities, banners, loading: bannerLoading, loadBannerActivities, loadBanners } =
useBanner(); useBanner();
// ========== Handlers ========== // ========== Handlers ==========