components/SwipeCard.vue:
<template>
<!-- 外层容器 -->
<view class="swipe-card" :style="customStyle">
<!-- 删除区域(右侧隐藏,滑动时显示) -->
<view class="swipe-action-area" :style="{ width: actionWidth + 'px' }">
<slot name="action"></slot>
</view>
<!-- 内容区域(可滑动,使用自定义样式) -->
<view class="swipe-card-content" :style="{
transform: `translateX(${offsetX}px)`,
transition: isAnimating ? 'transform 0.25s ease' : 'none'
}" @touchstart="onTouchStart" @touchmove="onTouchMove"
@touchend="onTouchEnd" @tap="onContentTap">
<slot name="default"></slot>
</view>
</view>
</template>
<script>
export default {
name: 'SwipeCard',
props: {
// 当前卡片索引(用于控制哪个展开)
index: Number,
// 当前打开的卡片索引
openIndex: Number,
// 滑块活动区域宽度,等于删除区域宽度
actionWidth: {
type: Number,
default: 100
},
// 固定滑动阈值(单位:px)
thresholdPx: {
type: Number,
default: 20
},
// 最外层容器样式
customStyle: {
type: String,
default: ''
}
},
data() {
return {
startX: 0, // 当前滑动中的参考点(用于平滑滑动)
touchStartX: 0, // 记录手指按下时真实位置(用于判断滑动总距离)
offsetX: 0,
isAnimating: false
};
},
watch: {
// 如果外部 openIndex 变化,并且不是当前卡片,就收起
openIndex(newVal) {
if (newVal !== this.index) {
this.close();
}
}
},
methods: {
// 手指按下,记录起始坐标
onTouchStart(e) {
this.startX = e.touches[0].clientX; // 用于拖动
this.touchStartX = e.touches[0].clientX; // 用于判断总滑动距离
this.isAnimating = false;
},
// 手指移动,计算偏移量
onTouchMove(e) {
const currentX = e.touches[0].clientX;
const deltaX = currentX - this.startX;
let target = deltaX + this.offsetX;
// 限制滑动范围:最多向左滑 actionWidth,不能右滑超过 0
if (target < -this.actionWidth) {
target = -this.actionWidth;
} else if (target > 0) {
target = 0;
}
this.offsetX = target;
this.startX = currentX;
},
// 手指松开,判断是否超过固定像素阈值
onTouchEnd(e) {
this.isAnimating = true;
const touchEndX = e.changedTouches[0].clientX;
const deltaX = touchEndX - this.touchStartX; // 本次手势的真实滑动距离
if (Math.abs(deltaX) >= this.thresholdPx) {
// 滑动足够:判断方向
if (deltaX < 0) {
// 向左滑:展开
this.open();
this.$emit('opened', this.index);
} else {
// 向右滑:收回
this.close();
this.$emit('closed', this.index);
}
} else {
// 滑动不够:回到原状态
if (this.offsetX < -this.actionWidth / 2) {
this.open();
this.$emit('opened', this.index);
} else {
this.close();
this.$emit('closed', this.index);
}
}
},
// 点击内容区域收回展开的删除按钮
onContentTap() {
if (this.offsetX < 0) {
this.close();
this.$emit('closed', this.index);
}
},
// 展开删除区域
open() {
this.offsetX = -this.actionWidth;
this.isAnimating = true;
this.$emit('opened', this.index);
},
// 收起删除区域
close() {
this.offsetX = 0;
this.isAnimating = true;
},
// 删除按钮点击事件
emitDelete() {
this.$emit('delete', this.index);
}
}
}
</script>
<style scoped>
/* 外层容器 */
.swipe-card {
position: relative;
overflow: hidden;
width: 100%;
}
/* 删除区域(背景固定,右对齐) */
.swipe-action-area {
position: absolute;
right: 0;
top: 0;
bottom: 0;
height: 100%;
z-index: 0;
}
/* 内容区域(可滑动) */
.swipe-card-content {
position: relative;
z-index: 1;
width: 100%;
will-change: transform;
/* 提前告诉浏览器此元素会位移,提升动画性能 */
}
</style>
父组件使用:
<template>
<view>
<SwipeCard
v-for="(item, index) in list"
:key="item.id"
:index="index"
:openIndex="openIndex"
:actionWidth="64"
customStyle="margin-top: 16rpx;border-radius: 12rpx;"
@opened="handleOpened"
@closed="handleClosed"
@delete="handleDelete"
>
<template #default>
<view class="card-content">
{{ item.name }}
</view>
</template>
<template #action>
<view class="card-delete" @click.stop="handleDelete(index)">
删除
</view>
</template>
</SwipeCard>
</view>
</template>
<script>
import SwipeCard from '@/components/SwipeCard.vue'
export default {
components: {
SwipeCard
},
data() {
return {
list: [
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橘子' }
],
openIndex: null,
}
},
onLoad() {},
onShow() {},
methods: {
handleOpened(index) {
this.openIndex = index;
},
handleClosed(index) {
if (this.openIndex === index) this.openIndex = null;
},
handleDelete(index) {
this.list.splice(index, 1);
this.openIndex = null;
}
}
}
</script>
<style lang="scss" scoped>
.card-content {
width: 100%;
height: 292rpx;
background: #F4F4F4;
padding: 24rpx 16rpx;
box-sizing: border-box;
}
.card-delete {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: #F34646;
font-size: 32rpx;
color: #FFFFFF;
}
</style>