topfans/frontend/pages/square/components/BannerCarousel.vue
2026-04-28 16:05:55 +08:00

269 lines
5.6 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-carousel" @click.stop>
<swiper
class="banner-swiper"
:autoplay="true"
:interval="4000"
:duration="400"
:circular="true"
:indicator-dots="false"
@change="onSwiperChange"
>
<swiper-item @click.stop="$emit('top3Click')">
<BannerTop3 @dataLoaded="onTop3DataLoaded" />
</swiper-item>
<swiper-item
v-for="item in bannerActivities"
:key="item.id"
@click.stop="$emit('activityClick', item)"
>
<image
class="banner-activity-img"
:src="item.cover_image || '/static/avatar/1.jpeg'"
mode="aspectFill"
/>
</swiper-item>
</swiper>
<!-- 卡片层跟随第一个 swiper-item切走时隐藏 -->
<view
class="cards-overlay"
:style="{ opacity: currentIndex === 0 ? 1 : 0, pointerEvents: currentIndex === 0 ? 'auto' : 'none' }"
@click.stop="$emit('top3Click')"
>
<view
v-for="(item, index) in top3Items"
:key="item.asset_id || index"
class="card-wrapper"
:class="`card-pos-${index}`"
>
<view class="card-frame">
<view class="card-image-wrap">
<image
class="card-image"
:src="item.cover_url || '/static/avatar/1.jpeg'"
mode="aspectFill"
/>
<!-- 点赞数叠在图片底部 -->
<view class="card-footer">
<image class="card-heart" src="/static/icon/heart-icon.png" mode="aspectFit" />
<view class="card-likes-wrap">
<text class="card-likes">{{ formatLikes(item.like_count) }}</text>
</view>
</view>
</view>
<!-- 边框叠在整张卡片上 -->
<image class="card-frame-border" src="/static/square/gerenzhongxincangpinkuang.png" mode="aspectFill" />
</view>
</view>
<!-- 骨架屏 -->
<view v-if="loading" class="skeleton-wrap">
<view v-for="i in 3" :key="i" class="skeleton-card" :class="`skeleton-pos-${i - 1}`" />
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import BannerTop3 from '../../components/BannerTop3.vue'
defineProps({
bannerActivities: { type: Array, default: () => [] }
})
defineEmits(['activityClick', 'top3Click'])
const top3Items = ref([])
const loading = ref(true)
const currentIndex = ref(0)
const onSwiperChange = (e) => {
currentIndex.value = e.detail.current
}
const onTop3DataLoaded = (items) => {
const list = items.slice(0, 3)
while (list.length < 3) list.push({ asset_id: `placeholder-${list.length}` })
top3Items.value = list
loading.value = false
}
const formatLikes = (n) => {
if (!n || isNaN(n)) return '0'
if (n >= 10000) return (n / 10000).toFixed(1) + 'w'
if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
return String(n)
}
</script>
<style scoped>
.banner-carousel {
position: relative;
width: 100%;
z-index: 100;
padding: 0 8rpx;
box-sizing: border-box;
}
.banner-swiper {
width: 100%;
height: 360rpx;
border-radius: 24rpx;
}
.banner-activity-img {
width: 100%;
height: 100%;
display: block;
}
/* 卡片层:绝对定位叠在 swiper 上,可自由溢出 */
.cards-overlay {
position: absolute;
top: 0;
right: 8rpx;
width: 420rpx;
height: 360rpx;
pointer-events: auto;
z-index: 10;
}
.card-wrapper {
position: absolute;
width: 148rpx;
height: 220rpx;
}
.card-pos-0 {
left: 50rpx;
top: 40rpx;
transform: rotate(-6deg);
z-index: 3;
}
.card-pos-1 {
left: 140rpx;
top: -8rpx;
transform: rotate(6deg);
z-index: 4;
}
.card-pos-2 {
left: 240rpx;
top: 106rpx;
transform: rotate(16deg);
z-index: 5;
}
.card-frame {
width: 100%;
height: 100%;
border-radius: 16rpx;
position: relative;
overflow: visible;
}
.card-image-wrap {
width: 90%;
height: 92%;
position: relative;
border-radius: 16rpx;
overflow: hidden;
z-index: 5;
padding: 8rpx;
}
.card-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.card-frame-border {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 2;
pointer-events: none;
}
.card-footer {
position: absolute;
bottom: 8rpx;
left: 0;
right: 64rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 4rpx;
z-index: 3;
}
.card-bg-frame {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.card-heart {
width: 22rpx;
height: 22rpx;
flex-shrink: 0;
}
.card-likes-wrap {
background: linear-gradient(to bottom right,
#F0E4B1 0%,
#F08399 50%,
#B94E73 100%
);
border-radius: 8rpx;
padding: 2rpx 8rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4);
}
.card-likes {
font-size: 12rpx;
color: #ffffff;
font-weight: 700;
}
/* 骨架屏 */
.skeleton-wrap {
position: absolute;
inset: 0;
z-index: 5;
}
.skeleton-card {
position: absolute;
width: 148rpx;
height: 220rpx;
border-radius: 16rpx;
background: linear-gradient(90deg,
rgba(255,255,255,0.06) 25%,
rgba(255,255,255,0.14) 50%,
rgba(255,255,255,0.06) 75%);
background-size: 200% 100%;
animation: shimmer 1.4s infinite;
}
.skeleton-pos-0 { left: 50rpx; top: 40rpx; transform: rotate(-6deg); }
.skeleton-pos-1 { left: 140rpx; top: -40rpx; transform: rotate(6deg); }
.skeleton-pos-2 { left: 240rpx; top: 10rpx; transform: rotate(16deg); }
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>