
cpp
<template>
<view class="swiper-card-box" :style="{ height: pageHeight + 'rpx' }">
<view class="swiper-card-container" @touchstart="handleTouchStart" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd">
<view class="swiper-card-item" v-for="(item, index) in loopBannerList" @click="onCardClick(index)" :key="index" :style="cardStyles[index]">
<image :src="item" mode="aspectFill" class="swiper-card-image" />
</view>
</view>
</view>
</template>
<script>
export default {
name: 'SwiperCard',
props: {
pageHeight: {
type: Number,
default: 920 // rpx
},
banners: {
type: Array,
default: () => []
},
rotateDeg: {
type: Number,
default: 14
},
flyDistance: {
type: Number,
default: 120 // 单位 vw
},
cardWidth: {
type: Number,
default: 568 // rpx
},
cardHeight: {
type: Number,
default: 820 // rpx
},
// ✅ 新增:自动轮播相关配置
autoPlay: { type: Boolean, default: true }, // 是否自动播放
autoPlayInterval: { type: Number, default: 3000 } // 自动播放间隔 ms
},
data() {
return {
loopBannerList: [],
cardStyles: [],
currentIndex: 0,
oldIndex: 0,
startX: 0,
startY: 0,
isAnimating: false,
slideDirection: '',
allowNewCardTop: false,
autoTimer: null // ✅ 自动播放定时器
};
},
mounted() {
this.initLoopList();
this.updateCardStyles();
this.startAutoPlay(); // ✅ 启动自动轮播
},
beforeDestroy() {
this.stopAutoPlay(); // ✅ 组件销毁前清理
},
methods: {
initLoopList() {
console.log(this.banners.length, '>>>>');
if (this.banners.length === 0) return;
this.loopBannerList = [...this.banners.slice(-1), ...this.banners, ...this.banners.slice(0, 1)];
this.currentIndex = 1;
this.oldIndex = this.currentIndex;
},
// ✅ 自动轮播定时器
startAutoPlay() {
if (!this.autoPlay || this.banners.length <= 1) return;
this.stopAutoPlay();
this.autoTimer = setInterval(() => {
if (!this.isAnimating) {
this.slideDirection = 'left';
this.triggerSlide();
}
}, this.autoPlayInterval);
},
stopAutoPlay() {
if (this.autoTimer) {
clearInterval(this.autoTimer);
this.autoTimer = null;
}
},
// ✅ 封装触发动画逻辑(原滑动复用)
triggerSlide(direction = this.slideDirection) {
if (this.isAnimating) return;
const len = this.banners.length;
this.isAnimating = true;
this.oldIndex = this.currentIndex;
this.slideDirection = direction;
this.currentIndex += direction === 'left' ? 1 : -1;
this.updateCardStyles();
setTimeout(() => {
this.allowNewCardTop = true;
this.updateCardStyles();
}, 300);
setTimeout(() => {
this.currentIndex = ((((this.currentIndex - 1) % len) + len) % len) + 1;
this.isAnimating = false;
this.allowNewCardTop = false;
this.updateCardStyles();
this.$emit('change', this.currentIndex - 1);
}, 800);
},
onCardClick(index) {
// ✅ 当前展示的卡片才允许点击
if (index === this.currentIndex) {
const realIndex = (this.currentIndex - 1 + this.banners.length) % this.banners.length;
this.$emit('click', realIndex); // 向父组件传当前 index
}
},
updateCardStyles() {
this.cardStyles = this.loopBannerList.map((item, index) => {
const diff = index - this.currentIndex;
let transform = 'rotate(0deg) translateX(0rpx) translateY(0rpx)';
let opacity = 1;
let zIndex = 0;
let transition = 'none';
// 飞出动画
if (this.isAnimating && index === this.oldIndex) {
transition = 'all 0.4s ease';
zIndex = this.allowNewCardTop ? 3 : 9;
opacity = 0;
if (this.slideDirection === 'left') {
transform = `rotate(-${this.rotateDeg}deg) translateX(-${this.flyDistance * 7.5}rpx) translateY(-${(this.flyDistance / 4) * 7.5}rpx)`;
} else if (this.slideDirection === 'right') {
transform = `rotate(${this.rotateDeg}deg) translateX(${this.flyDistance * 7.5}rpx) translateY(-${(this.flyDistance / 4) * 7.5}rpx)`;
}
}
// 堆叠三张
else if (Math.abs(diff) <= 1) {
transition = this.isAnimating ? 'all 0.25s ease' : 'none';
opacity = diff === 0 ? 1 : 0.8;
if (diff === 0) {
zIndex = this.allowNewCardTop ? 7 : 8;
transform = 'rotate(0deg) translateX(0rpx) translateY(0rpx)';
} else if (diff === -1) {
transform = `rotate(-${this.rotateDeg / 3}deg) translateX(-10rpx) translateY(10rpx)`;
zIndex = 2;
} else if (diff === 1) {
transform = `rotate(${this.rotateDeg / 3}deg) translateX(10rpx) translateY(10rpx)`;
zIndex = 2;
}
}
// 隐藏备用卡
else {
opacity = 0;
zIndex = 0;
if (this.slideDirection === 'left' && index === this.currentIndex + 2) {
transform = `rotate(${this.rotateDeg / 3}deg) translateX(100rpx) translateY(10rpx)`;
} else if (this.slideDirection === 'right' && index === this.currentIndex - 2) {
transform = `rotate(-${this.rotateDeg / 3}deg) translateX(-100rpx) translateY(10rpx)`;
}
}
return `transform:${transform};opacity:${opacity};z-index:${zIndex};transition:${transition};width:${this.cardWidth}rpx;height:${this.cardHeight}rpx;`;
});
},
handleTouchStart(e) {
this.stopAutoPlay(); // ✅ 手动滑动时暂停自动轮播
if (this.isAnimating) return;
this.startX = e.touches[0].clientX;
this.startY = e.touches[0].clientY; // ✅ 记录Y坐标
this.oldIndex = this.currentIndex;
this.allowNewCardTop = false;
},
handleTouchEnd(e) {
if (this.isAnimating) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const diffX = endX - this.startX;
const diffY = endY - this.startY; // ✅ 垂直偏移量
const minSwipeX = 30; // 横向切换最小滑动距离
const maxSwipeY = 60; // 垂直滑动容忍度(超过就视为上下滑)
const len = this.banners.length;
// ✅ 判断:横向滑动明显大于纵向滑动,且距离足够
if (Math.abs(diffX) > minSwipeX && Math.abs(diffX) > Math.abs(diffY)) {
this.slideDirection = diffX < 0 ? 'left' : 'right';
this.currentIndex += diffX < 0 ? 1 : -1;
this.isAnimating = true;
this.updateCardStyles();
setTimeout(() => {
this.allowNewCardTop = true;
this.updateCardStyles();
}, 300);
setTimeout(() => {
this.currentIndex = ((((this.currentIndex - 1) % len) + len) % len) + 1;
this.isAnimating = false;
this.allowNewCardTop = false;
this.updateCardStyles();
this.$emit('change', this.currentIndex - 1);
}, 800);
} else {
// ✅ 否则忽略这次滑动(例如上下滑或滑动太短)
this.slideDirection = '';
}
// ✅ 重新启动自动轮播
this.startAutoPlay();
}
}
};
</script>
<style scoped>
.swiper-card-box {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.swiper-card-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.swiper-card-item {
position: absolute;
border-radius: 25rpx;
transform-origin: center center;
background: #fff;
}
.swiper-card-image {
width: 100%;
height: 100%;
border-radius: inherit;
object-fit: cover;
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.25);
}
</style>
使用
cpp
<swiper-card
:banners="[
'/static/banner1.jpg',
'/static/banner2.jpg',
'/static/banner3.jpg'
]"
:autoPlay="true"
:autoPlayInterval="4000"
@click="onBannerClick"
@change="onBannerChange"
/>