Files
gym-manage/gym-manage-uniapp/components/index/BannerSwiper.vue
T
2026-06-29 13:35:29 +08:00

255 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="banner-container">
<swiper
class="banner-swiper"
:circular="true"
:autoplay="true"
:interval="4000"
:duration="500"
:indicator-dots="false"
@change="onSwiperChange"
>
<swiper-item v-for="(banner, index) in banners" :key="index">
<view class="banner-content" @click="previewImage(index)">
<!-- 添加 lazy-load 属性实现懒加载 -->
<image
:src="banner.image"
mode="aspectFill"
class="banner-image"
lazy-load
:show-menu-by-longpress="false"
@load="onImageLoad(index)"
@error="onImageError(index)"
/>
<view class="banner-overlay"></view>
<view class="banner-text">
<text class="banner-title">{{ banner.title }}</text>
<text class="banner-subtitle">{{ banner.subtitle }}</text>
<text class="banner-desc">{{ banner.desc }}</text>
</view>
<!-- 可选添加加载占位符 -->
<view v-if="!imageLoaded[index]" class="image-placeholder">
<view class="loading-spinner"></view>
</view>
</view>
</swiper-item>
</swiper>
<view class="banner-dots">
<view
v-for="(_, index) in banners"
:key="index"
:class="['dot', { active: currentIndex === index }]"
></view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getActiveBanners } from '@/api/main.js'
// 默认静态数据(后端无数据时作为兜底)
const fallbackBanners = [
{
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=800&q=80',
title: '突破自我',
subtitle: '超越极限',
desc: '科学训练 · 遇见更好的自己'
},
{
image: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=800&q=80',
title: '专业指导',
subtitle: '高效训练',
desc: '私人定制 · 专属健身方案'
},
{
image: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=800&q=80',
title: '健康生活',
subtitle: '从这里开始',
desc: '全方位 · 打造完美体态'
}
]
// 先用回退数据初始化,确保 swiper 立即可见
const banners = ref([...fallbackBanners])
const currentIndex = ref(0)
const imageLoaded = ref(fallbackBanners.map(() => false))
const isFetching = ref(false)
async function fetchBanners() {
isFetching.value = true
try {
// 禁用缓存,确保每次获取最新数据
const res = await getActiveBanners({ cache: false })
// 处理后端返回数据:可能是数组或 { data: [...] }
const data = Array.isArray(res) ? res : (res?.data || [])
if (data.length > 0) {
// 映射后端字段到组件字段,替换回退数据
banners.value = data.map(item => ({
image: item.imageUrl || '',
title: item.title || '',
subtitle: item.subtitle || '',
desc: item.description || ''
}))
// 更新 imageLoaded 状态
imageLoaded.value = banners.value.map(() => false)
console.log('[BannerSwiper] 轮播图数据加载成功,共', data.length, '条')
} else {
console.log('[BannerSwiper] 后端无轮播图数据,使用回退数据')
// 保持已有的 fallbackBanners,无需重新赋值
imageLoaded.value = banners.value.map(() => false)
}
} catch (err) {
console.error('[BannerSwiper] 轮播图数据加载失败,使用回退数据:', err)
// 保持已有的 fallbackBanners,无需重新赋值
imageLoaded.value = banners.value.map(() => false)
} finally {
isFetching.value = false
}
}
onMounted(() => {
fetchBanners()
})
const onSwiperChange = (e) => {
currentIndex.value = e.detail.current
}
const previewImage = (index) => {
const urls = banners.map(banner => banner.image)
uni.previewImage({
urls: urls,
current: urls[index],
indicator: 'default',
loop: true
})
}
const onImageLoad = (index) => {
imageLoaded.value[index] = true
console.log(`图片 ${index} 加载完成`)
}
const onImageError = (index) => {
console.error(`图片 ${index} 加载失败`)
}
</script>
<style lang="scss" scoped>
.banner-container {
width: 100%;
padding: 0 24rpx;
box-sizing: border-box;
}
.banner-swiper {
width: 100%;
height: 360rpx;
border-radius: 20rpx;
overflow: hidden;
}
.banner-content {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.banner-image {
width: 100%;
height: 100%;
}
.image-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top: 4rpx solid #7AB5CC;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.banner-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.05) 50%, rgba(0,0,0,0.25) 100%);
z-index: 2;
}
.banner-text {
position: absolute;
left: 36rpx;
top: 50%;
transform: translateY(-50%);
z-index: 3;
}
.banner-title {
display: block;
font-size: 48rpx;
font-weight: 800;
color: #ffffff;
margin-bottom: 8rpx;
text-shadow: 0 4rpx 16rpx rgba(80, 150, 190, 0.4);
}
.banner-subtitle {
display: block;
font-size: 56rpx;
font-weight: 800;
color: #E0F0FA;
margin-bottom: 16rpx;
text-shadow: 0 4rpx 16rpx rgba(80, 150, 190, 0.4);
}
.banner-desc {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
}
.banner-dots {
display: flex;
justify-content: center;
gap: 16rpx;
padding: 20rpx 0 8rpx;
}
.dot {
width: 48rpx;
height: 8rpx;
border-radius: 9999rpx;
background: #D0E4EE;
transition: all 0.3s ease;
}
.dot.active {
width: 64rpx;
background: linear-gradient(90deg, #7AB5CC, #9CCFDF);
}
</style>