web-base-h5/components/simple-vertical-swiper.vue

586 lines
14 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="simple-vertical-swiper" :style="{ height: height + 'px' }">
<view
class="swiper-wrapper"
@touchstart.stop="onTouchStart"
@touchmove.stop.prevent="onTouchMove"
@touchend.stop="onTouchEnd"
>
<view
v-for="(item, index) in items"
:key="index"
class="swiper-slide"
:class="{ 'swiper-slide-active': index === currentIndex }"
:style="getSlideStyle(index)"
>
<!-- 公告类型 -->
<view v-if="type === 'notice'" class="notice-item">
<text class="notice-icon">📢</text>
<text class="notice-text">{{ item.content || item }}</text>
</view>
<!-- 商品类型 -->
<view v-else-if="type === 'product'" class="product-item">
<image
class="product-image"
:src="item.image"
mode="aspectFill"
></image>
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<text class="product-price">¥{{ item.price }}</text>
</view>
</view>
<!-- 内容类型 -->
<view v-else-if="type === 'content'" class="content-item">
<text class="content-title">{{ item.title }}</text>
<text class="content-desc">{{ item.description }}</text>
</view>
<!-- 文本类型 -->
<view v-else-if="type === 'text'" class="text-item">
<text class="text-content">{{ item.text || item }}</text>
</view>
<!-- 自定义插槽 -->
<slot v-else :item="item" :index="index">
<view class="default-item">
<text>{{ item.text || item.content || item }}</text>
</view>
</slot>
</view>
</view>
<!-- 指示器 -->
<view v-if="showIndicators && items.length > 1" class="indicators">
<view
v-for="(item, index) in items"
:key="index"
class="indicator"
:class="{ active: index === currentIndex }"
></view>
</view>
</view>
</template>
<script>
export default {
name: 'SimpleVerticalSwiper',
props: {
// 轮播数据
items: {
type: Array,
default: () => [],
},
// 数据类型notice(公告), product(商品), content(内容), text(文本), custom(自定义)
type: {
type: String,
default: 'text',
},
// 组件高度
height: {
type: Number,
default: 60,
},
// 是否自动轮播
autoplay: {
type: Boolean,
default: true,
},
// 自动轮播间隔时间(ms)
interval: {
type: Number,
default: 3000,
},
// 动画持续时间(ms)
duration: {
type: Number,
default: 300,
},
// 是否显示指示器
showIndicators: {
type: Boolean,
default: false,
},
// 是否循环轮播
circular: {
type: Boolean,
default: true,
},
},
data() {
return {
currentIndex: 0,
timer: null,
isTransitioning: false,
// 触摸相关
touchStartY: 0,
touchStartTime: 0,
isTouching: false,
}
},
computed: {
itemsLength() {
return this.items.length
},
},
watch: {
items: {
handler() {
this.initSwiper()
},
immediate: true,
},
autoplay(newVal) {
if (newVal) {
this.startAutoplay()
} else {
this.clearTimer()
}
},
},
mounted() {
this.initSwiper()
},
beforeDestroy() {
this.clearTimer()
},
// uniapp 生命周期
onReady() {
this.initSwiper()
},
onUnload() {
this.clearTimer()
},
methods: {
// 获取滑块样式 - 解决循环轮播动画连续性问题
getSlideStyle(index) {
if (this.itemsLength <= 1) {
return {
transform: 'translateY(0px)',
opacity: 1,
zIndex: 1,
}
}
let translateY = 0
let opacity = 0.6
let zIndex = 1
if (index === this.currentIndex) {
// 当前激活项
translateY = 0
opacity = 1
zIndex = 10
} else {
// 计算偏移量
let offset = index - this.currentIndex
// 循环轮播的关键:选择最短路径
if (this.circular && this.itemsLength > 2) {
const halfLength = this.itemsLength / 2
// 如果偏移量的绝对值大于一半,说明走相反方向更近
if (offset > halfLength) {
offset = offset - this.itemsLength // 从负方向走
} else if (offset < -halfLength) {
offset = offset + this.itemsLength // 从正方向走
}
}
translateY = offset * this.height
// 控制可见性,只显示相邻的项目
const absOffset = Math.abs(offset)
if (absOffset <= 1) {
opacity = absOffset === 1 ? 0.3 : 0.6
zIndex = absOffset === 1 ? 5 : 1
} else {
opacity = 0
zIndex = 0
}
}
const transition =
this.isTransitioning && !this.isTouching
? `all ${this.duration}ms cubic-bezier(0.25, 0.46, 0.45, 0.94)`
: 'none'
return {
transform: `translateY(${translateY}px)`,
opacity: opacity,
zIndex: zIndex,
transition: transition,
}
},
// 初始化轮播
initSwiper() {
console.log(
'初始化轮播, items数量:',
this.itemsLength,
'autoplay:',
this.autoplay
)
if (this.itemsLength === 0) return
this.currentIndex = 0
if (this.autoplay && this.itemsLength > 1) {
this.startAutoplay()
}
},
// 开始自动轮播
startAutoplay() {
this.clearTimer()
if (this.autoplay && this.itemsLength > 1) {
console.log('开始自动轮播,间隔:', this.interval)
this.timer = setInterval(() => {
this.next()
}, this.interval)
}
},
// 清除定时器
clearTimer() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
// 下一页
next() {
if (this.isTransitioning || this.itemsLength <= 1) return
console.log('切换到下一页,当前:', this.currentIndex)
this.isTransitioning = true
const nextIndex = this.circular
? (this.currentIndex + 1) % this.itemsLength
: Math.min(this.currentIndex + 1, this.itemsLength - 1)
this.currentIndex = nextIndex
setTimeout(() => {
this.isTransitioning = false
this.$emit('change', {
current: this.currentIndex,
currentItem: this.items[this.currentIndex],
})
}, this.duration)
},
// 上一页
prev() {
if (this.isTransitioning || this.itemsLength <= 1) return
this.isTransitioning = true
const prevIndex = this.circular
? (this.currentIndex - 1 + this.itemsLength) % this.itemsLength
: Math.max(this.currentIndex - 1, 0)
this.currentIndex = prevIndex
setTimeout(() => {
this.isTransitioning = false
this.$emit('change', {
current: this.currentIndex,
currentItem: this.items[this.currentIndex],
})
}, this.duration)
},
// 跳转到指定页
goToSlide(index) {
if (index === this.currentIndex || this.isTransitioning) return
if (index < 0 || index >= this.itemsLength) return
this.isTransitioning = true
this.currentIndex = index
setTimeout(() => {
this.isTransitioning = false
this.$emit('change', {
current: this.currentIndex,
currentItem: this.items[this.currentIndex],
})
}, this.duration)
},
// 触摸开始
onTouchStart(e) {
if (this.itemsLength <= 1) return
console.log('触摸开始')
this.clearTimer() // 停止自动轮播
this.touchStartY = e.touches[0].clientY
this.touchStartTime = Date.now()
this.isTouching = true
},
// 触摸移动
onTouchMove(e) {
if (this.isTransitioning || !this.isTouching || this.itemsLength <= 1)
return
const deltaY = e.touches[0].clientY - this.touchStartY
console.log('触摸移动, deltaY:', deltaY)
// 事件已通过修饰符处理,这里只做逻辑判断
// 可以在这里添加实时跟随手指的效果
},
// 触摸结束
onTouchEnd(e) {
if (!this.isTouching || this.itemsLength <= 1) return
this.isTouching = false
const currentY = e.changedTouches[0].clientY
const deltaY = currentY - this.touchStartY
const deltaTime = Date.now() - this.touchStartTime
const velocity = Math.abs(deltaY) / deltaTime
console.log('触摸结束, deltaY:', deltaY, 'velocity:', velocity)
// 判断是否需要切换
const threshold = this.height * 0.25 // 降低阈值,更容易触发
const velocityThreshold = 0.3
const shouldSwitch =
Math.abs(deltaY) > threshold || velocity > velocityThreshold
if (shouldSwitch) {
if (deltaY > 0) {
// 向下滑动,显示上一项
console.log('手势触发:上一页')
this.prev()
} else {
// 向上滑动,显示下一项
console.log('手势触发:下一页')
this.next()
}
} else {
console.log('手势未达到切换阈值,不切换')
}
// 重新开始自动轮播
setTimeout(() => {
if (this.autoplay) {
this.startAutoplay()
}
}, 1000)
},
},
}
</script>
<style lang="scss" scoped>
.simple-vertical-swiper {
position: relative;
overflow: hidden;
// 防止触摸时选中文本
user-select: none;
-webkit-user-select: none;
// App端需要的设置
-webkit-overflow-scrolling: touch;
.swiper-wrapper {
position: relative;
width: 100%;
height: 100%;
// uniapp App端触摸控制
touch-action: none;
// 防止触摸时的默认行为
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
// App端滚动穿透防护
overflow: hidden;
}
.swiper-slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
// 防止子元素被选中
user-select: none;
-webkit-user-select: none;
&.swiper-slide-active {
opacity: 1 !important;
z-index: 10 !important;
}
}
// 公告样式
.notice-item {
display: flex;
align-items: center;
padding: 0 15px;
width: calc(100% - 30px);
height: 80%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 6px;
.notice-icon {
font-size: 16px;
margin-right: 8px;
color: #fff;
}
.notice-text {
color: #fff;
font-size: 14px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
// 商品样式
.product-item {
display: flex;
align-items: center;
padding: 10px 15px;
width: calc(100% - 30px);
height: calc(100% - 20px);
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.product-image {
width: 60px;
height: 60px;
border-radius: 6px;
margin-right: 12px;
background-color: #f0f0f0;
}
.product-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.product-name {
font-size: 14px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
font-size: 16px;
color: #ff6b6b;
font-weight: 600;
}
}
}
// 内容样式
.content-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 15px;
width: calc(100% - 30px);
height: calc(100% - 30px);
text-align: center;
background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
border-radius: 8px;
.content-title {
font-size: 16px;
color: #fff;
font-weight: 600;
margin-bottom: 6px;
}
.content-desc {
font-size: 12px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.3;
}
}
// 文本样式
.text-item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
width: calc(100% - 30px);
height: 80%;
background: linear-gradient(135deg, #fd79a8 0%, #e84393 100%);
border-radius: 6px;
.text-content {
color: #fff;
font-size: 14px;
font-weight: 500;
text-align: center;
}
}
// 默认样式
.default-item {
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
width: calc(100% - 30px);
height: 80%;
background-color: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
text {
color: #333;
font-size: 14px;
text-align: center;
}
}
// 指示器
.indicators {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: 3px;
z-index: 20;
.indicator {
width: 3px;
height: 12px;
background-color: rgba(255, 255, 255, 0.4);
border-radius: 2px;
transition: all 0.3s ease;
&.active {
background-color: rgba(255, 255, 255, 0.9);
height: 16px;
}
}
}
}
</style>