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