uniapp 3端轮播

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"
/>
相关推荐
Memory沙漏2 小时前
IOS如何免费申请开发者证书(uniapp开发)
ios·uni-app
Fantasydg3 小时前
Request Response对象
前端
Wect3 小时前
学习React-DnD:核心组件与Hooks
前端
humors2213 小时前
前端开发案例(不定期更新)
前端·vue.js·elementui·ruoyi·若依
菠萝+冰3 小时前
npm中-d -g 和默认安装的区别
前端·npm·node.js
心随雨下3 小时前
Flutter Material 3设计语言详解
javascript·flutter·设计语言
一路向北North4 小时前
网页版预编译SQL转换工具
前端·javascript·sql
拿不拿铁194 小时前
Vite 5.x 开发模式启动流程分析
前端
fruge5 小时前
设计稿还原技巧:解决间距、阴影、字体适配的细节问题
前端·css