uniapp 左滑删除组件

0 阅读1分钟

image.png

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>