feat(fittingRoom): 替换轮播D堆叠效果 - 引图组件为3入新的 `hbxw-stack-carousel` 轮播图组件,支持3D堆叠、自动播放和手势滑动 - 更新页面结构与样式以适配新组件 - 增加点击事件处理函数 `handleItemClick` 和轮播切换事件 `handleChange` - 添加动态更新轮播数据的功能,通过 `dian` 方法测试数据变更 - 调整图片展示模式为 `aspectFill` 并设定固定宽高 -优化样式布局,设置相对定位及层级控制 docs(readme): 添加接口文档链接 - 在 README 中补充项目相关接口文档的访问地址 ```
338 lines
9.0 KiB
Vue
338 lines
9.0 KiB
Vue
<template>
|
|
<view class="stacked-carousel">
|
|
<view class="carousel-container" :style="containerStyle" @touchstart="handleTouchStart" @touchend="handleTouchEnd">
|
|
<view v-for="(item, index) in displayList" :key="index" class="carousel-item"
|
|
:class="{ 'active': index === activeIndex, 'no-transition': isResetting }" :style="getItemStyle(index)"
|
|
@click="handleItemClick(index)" @transitionend="handleTransitionEnd">
|
|
<slot :item="item" :index="index">
|
|
<image :src="item.image" mode="aspectFill" />
|
|
</slot>
|
|
</view>
|
|
<view v-if="showIndicator" class="carousel-indicators">
|
|
<slot name="indicator" :current-index="getRealIndex(activeIndex)" :total="list.length"
|
|
:handle-click="handleIndicatorClick">
|
|
<view v-for="(_, index) in list" :key="index" :class="[
|
|
indicatorStyle === 'dot' ? 'indicator-dot' : 'indicator-line',
|
|
{ 'active': getRealIndex(activeIndex) === index }
|
|
]" @click="handleIndicatorClick(index)" />
|
|
</slot>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
/**
|
|
* 3D堆叠轮播图组件
|
|
* @description 一个支持3D堆叠效果的轮播图组件,具有自动播放、无缝循环、触摸滑动等功能
|
|
*/
|
|
export default {
|
|
name: 'hbxw-stacked-carousel',
|
|
props: {
|
|
// 轮播图数据列表,每个元素必须包含image属性
|
|
list: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
// 组件宽度,支持数字(单位rpx)或字符串
|
|
width: {
|
|
type: [String, Number],
|
|
default: '750rpx'
|
|
},
|
|
// 组件高度,支持数字(单位rpx)或字符串
|
|
height: {
|
|
type: [String, Number],
|
|
default: '400rpx'
|
|
},
|
|
// 是否自动播放
|
|
autoplay: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
// 自动播放间隔时间(毫秒)
|
|
interval: {
|
|
type: Number,
|
|
default: 3000
|
|
},
|
|
current: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
// 是否启用无缝循环
|
|
loop: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
// 是否显示指示器
|
|
showIndicator: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
// 指示器样式,支持dot(圆点)和line(线条)
|
|
indicatorStyle: {
|
|
type: String,
|
|
default: 'dot',
|
|
validator: function (value) {
|
|
return ['dot', 'line'].includes(value)
|
|
}
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
activeIndex: 0, // 当前激活的轮播图索引
|
|
timer: null, // 自动播放定时器
|
|
touchStartX: 0, // 触摸开始位置
|
|
touchEndX: 0, // 触摸结束位置
|
|
displayList: [], // 实际显示的轮播图列表(包含首尾过渡图片)
|
|
isResetting: false // 是否正在重置位置
|
|
}
|
|
},
|
|
computed: {
|
|
// 计算容器样式,处理宽高单位
|
|
containerStyle() {
|
|
return {
|
|
width: typeof this.width === 'number' ? this.width + 'rpx' : this.width,
|
|
height: typeof this.height === 'number' ? this.height + 'rpx' : this.height
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.initDisplayList()
|
|
this.startAutoplay()
|
|
},
|
|
beforeDestroy() {
|
|
this.stopAutoplay()
|
|
},
|
|
methods: {
|
|
// 初始化显示列表,处理无缝循环逻辑
|
|
initDisplayList() {
|
|
if (!this.list.length) return
|
|
|
|
if (this.loop && this.list.length > 1) {
|
|
// 在首尾添加额外的图片副本以实现无缝效果
|
|
this.displayList = [
|
|
this.list[this.list.length - 1],
|
|
...this.list,
|
|
this.list[0]
|
|
]
|
|
this.activeIndex = 1 // 从真实的第一张图片开始
|
|
} else {
|
|
this.displayList = [...this.list]
|
|
this.activeIndex = 0
|
|
}
|
|
},
|
|
// 计算每个轮播图项的样式,实现3D堆叠效果
|
|
getItemStyle(index) {
|
|
const diff = index - this.activeIndex
|
|
const total = this.displayList.length
|
|
let scale = 1
|
|
let zIndex = 1
|
|
let translateX = '0'
|
|
|
|
if (diff === 0) {
|
|
scale = 1
|
|
zIndex = 3
|
|
} else if (Math.abs(diff) === 1) {
|
|
scale = 0.8
|
|
zIndex = 2
|
|
translateX = diff > 0 ? '60%' : '-60%'
|
|
} else {
|
|
scale = 0.6
|
|
zIndex = 1
|
|
translateX = diff > 0 ? '120%' : '-120%'
|
|
}
|
|
|
|
return {
|
|
transform: `translateX(${translateX}) scale(${scale})`,
|
|
zIndex: zIndex
|
|
}
|
|
},
|
|
// 处理轮播图过渡结束事件,实现无缝循环
|
|
handleTransitionEnd() {
|
|
if (!this.loop || this.displayList.length <= 1) return
|
|
|
|
// 处理无缝轮播的首尾过渡
|
|
if (this.activeIndex === 0 || this.activeIndex === this.displayList.length - 1) {
|
|
this.isResetting = true
|
|
|
|
// 立即重置位置
|
|
const targetIndex = this.activeIndex === 0 ? this.displayList.length - 2 : 1
|
|
this.activeIndex = targetIndex
|
|
|
|
// 在下一帧恢复过渡效果
|
|
this.$nextTick(() => {
|
|
setTimeout(() => {
|
|
this.isResetting = false
|
|
}, 50)
|
|
})
|
|
}
|
|
},
|
|
// 处理轮播图项点击事件
|
|
handleItemClick(index) {
|
|
this.$emit('click', this.getRealIndex(index), this.displayList[index])
|
|
// if (index === this.activeIndex) return
|
|
// const oldIndex = this.getRealIndex(this.activeIndex)
|
|
// this.activeIndex = index
|
|
// this.$emit('change', this.getRealIndex(index), oldIndex)
|
|
// this.resetAutoplay()
|
|
},
|
|
// 处理轮播图切换,direction: 1向后,-1向前
|
|
handleSlideChange(direction) {
|
|
if (!this.displayList.length) return
|
|
|
|
const oldIndex = this.getRealIndex(this.activeIndex)
|
|
let newIndex
|
|
|
|
if (this.loop && this.displayList.length > 1) {
|
|
// 处理向前切换时的特殊情况
|
|
if (direction === -1 && this.activeIndex === 1) {
|
|
this.isResetting = true
|
|
this.activeIndex = this.displayList.length - 2
|
|
this.$nextTick(() => {
|
|
setTimeout(() => {
|
|
this.isResetting = false
|
|
}, 50)
|
|
})
|
|
} else {
|
|
newIndex = (this.activeIndex + direction + this.displayList.length) % this.displayList.length
|
|
this.activeIndex = newIndex
|
|
}
|
|
} else {
|
|
newIndex = (this.activeIndex + direction + this.list.length) % this.list.length
|
|
this.activeIndex = newIndex
|
|
}
|
|
|
|
this.$emit('change', this.getRealIndex(this.activeIndex), oldIndex)
|
|
},
|
|
// 开始自动播放
|
|
startAutoplay() {
|
|
if (!this.autoplay) return
|
|
this.timer = setInterval(() => {
|
|
this.handleSlideChange(1)
|
|
}, this.interval)
|
|
},
|
|
// 停止自动播放
|
|
stopAutoplay() {
|
|
if (this.timer) {
|
|
clearInterval(this.timer)
|
|
this.timer = null
|
|
}
|
|
},
|
|
// 重置自动播放定时器
|
|
resetAutoplay() {
|
|
this.stopAutoplay()
|
|
this.startAutoplay()
|
|
},
|
|
// 处理触摸开始事件
|
|
handleTouchStart(e) {
|
|
this.touchStartX = e.touches[0].clientX
|
|
this.stopAutoplay()
|
|
},
|
|
// 处理触摸结束事件,判断滑动方向
|
|
handleTouchEnd(e) {
|
|
this.touchEndX = e.changedTouches[0].clientX
|
|
const diff = this.touchEndX - this.touchStartX
|
|
|
|
if (Math.abs(diff) > 50) { // 设置最小滑动距离阈值
|
|
if (diff > 0) {
|
|
// 向右滑,显示上一张
|
|
this.handleSlideChange(-1)
|
|
} else {
|
|
// 向左滑,显示下一张
|
|
this.handleSlideChange(1)
|
|
}
|
|
}
|
|
|
|
this.startAutoplay()
|
|
},
|
|
|
|
// 获取真实的轮播图索引(考虑无缝循环的情况)
|
|
getRealIndex(displayIndex) {
|
|
if (this.loop && this.list.length > 1) {
|
|
if (displayIndex === 0) return this.list.length - 1
|
|
if (displayIndex === this.displayList.length - 1) return 0
|
|
return displayIndex - 1
|
|
}
|
|
return displayIndex
|
|
},
|
|
|
|
// 处理指示器点击事件
|
|
handleIndicatorClick(index) {
|
|
const currentRealIndex = this.getRealIndex(this.activeIndex)
|
|
if (currentRealIndex === index) return
|
|
|
|
const targetDisplayIndex = this.loop ? index + 1 : index
|
|
this.handleItemClick(targetDisplayIndex)
|
|
}
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.stacked-carousel {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.carousel-container {
|
|
position: relative;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.carousel-item {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
transition: all 0.5s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.carousel-item.no-transition {
|
|
transition: none;
|
|
}
|
|
|
|
.carousel-item image {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 20rpx;
|
|
}
|
|
|
|
.carousel-indicators {
|
|
position: absolute;
|
|
bottom: 20rpx;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
gap: 16rpx;
|
|
z-index: 3;
|
|
}
|
|
|
|
.indicator-dot {
|
|
width: 16rpx;
|
|
height: 16rpx;
|
|
border-radius: 50%;
|
|
background-color: rgba(255, 255, 255, 0.5);
|
|
transition: all 0.5s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.indicator-dot.active {
|
|
background-color: #ffffff;
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.indicator-line {
|
|
width: 24rpx;
|
|
height: 8rpx;
|
|
border-radius: 4rpx;
|
|
background-color: rgba(255, 255, 255, 0.5);
|
|
transition: all 0.5s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.indicator-line.active {
|
|
width: 32rpx;
|
|
background-color: #ffffff;
|
|
}
|
|
</style>
|