feat:增加骨架屏
This commit is contained in:
parent
6a6d5f9deb
commit
c4b8800ea7
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 ==========
|
||||
|
||||
Loading…
Reference in New Issue
Block a user