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>
<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">
<!-- <swiper-item @click.stop="$emit('top3Click')" style="display: flex;
align-items: center;">
@ -20,16 +25,18 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import BannerTop3 from '../../components/BannerTop3.vue'
defineProps({
const props = defineProps({
bannerActivities: { type: Array, default: () => [] },
//
banners: {
type: Array,
default: () => []
}
},
// useBanner
loading: { type: Boolean, default: false }
})
const emit = defineEmits(['activityClick', 'top3Click', 'bannerClick'])
@ -43,6 +50,13 @@ const onTop3DataLoaded = (items) => {
// BannerTop3
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>
<style scoped>
@ -95,4 +109,36 @@ const onTop3DataLoaded = (items) => {
color: #fff;
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>

View File

@ -2,21 +2,13 @@ import { ref } from 'vue'
import { getActivityListApi, getMintingActivitiesApi } from '@/utils/api.js'
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 banners = ref([])
// 加载态:用于骨架屏展示(任一 fetch 未结束均为 true
const loading = ref(false)
// 内部:优先走预拉缓存,未命中/过期则手动 fetch
async function _tryPreloadThenFetch(key, fallbackFetch, processFn) {
@ -35,6 +27,7 @@ async function _tryPreloadThenFetch(key, fallbackFetch, processFn) {
}
const loadBannerActivities = async () => {
loading.value = true
try {
await _tryPreloadThenFetch('banner.activities',
() => getActivityListApi(uni.getStorageSync('star_id') || null, 1, 10),
@ -46,10 +39,13 @@ const loadBannerActivities = async () => {
)
} catch (e) {
console.error('[useBanner] 加载 banner 活动失败', e?.message ?? e)
} finally {
loading.value = false
}
}
const loadBanners = async () => {
loading.value = true
try {
await _tryPreloadThenFetch('banner.minting',
() => getMintingActivitiesApi(null, 1, 10),
@ -66,12 +62,12 @@ const loadBanners = async () => {
params: activity.params
}))
}
if (banners.value.length === 0) banners.value = [...FALLBACK_BANNER]
}
)
} catch (e) {
console.error('[useBanner] 加载运营 banner 失败', e?.message ?? e)
banners.value = [...FALLBACK_BANNER]
} finally {
loading.value = false
}
}
@ -79,6 +75,7 @@ export function useBanner() {
return {
bannerActivities,
banners,
loading,
loadBannerActivities,
loadBanners
}

View File

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