feat(fittingRoom): 替换轮播D堆叠效果

- 引图组件为3入新的 `hbxw-stack-carousel` 轮播图组件,支持3D堆叠、自动播放和手势滑动
- 更新页面结构与样式以适配新组件
- 增加点击事件处理函数 `handleItemClick` 和轮播切换事件 `handleChange`
- 添加动态更新轮播数据的功能,通过 `dian` 方法测试数据变更
- 调整图片展示模式为 `aspectFill` 并设定固定宽高
-优化样式布局,设置相对定位及层级控制

docs(readme): 添加接口文档链接

- 在 README 中补充项目相关接口文档的访问地址
```
This commit is contained in:
guigongli
2025-09-20 14:42:27 +08:00
parent 657631f5bf
commit f8dd924fa4
8 changed files with 759 additions and 26 deletions
@@ -0,0 +1,337 @@
<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>