vue 实现左滑删除

Fork Me On Github

更新

支持 :index 为数值或字符串

演示效果

源码

实现原理

  1. 需要记录初始位置,滑动方向,是否是滑动(点击不触发touchmove事件)
ts
             

oldLeft: 0,       // 记录初始位置 > 0 显示左边控件 < 0 显示右边控件  0 原始状态
left: 0,         // 控制滑动位置
startX: 0,       // 记录滑动的初始位置
isTouch: false,  //判断是否是滑动


touchStart(e: TouchEvent) {
    this.oldLeft = this.left;
    this.isTouch = false;
    this.startX = e.targetTouches[0].clientX;
},
12345678910111213
  1. 滑动中不能超过可显示的隐藏区域宽度
ts
                                

touchMove(e: TouchEvent) {
    this.isTouch = true;
    // 获取滑动距离
    const diff = e.targetTouches[0].clientX - this.startX;
    if (this.oldLeft == 0) {
        if (diff < 0) {
            // 左滑显示右边控件
            this.left = Math.max(diff, -this.getRightWidth());
            return;
        }
        // 右滑显示左边控件
        this.left = Math.min(diff, this.getLeftWidth());
        return;
    }
    if (this.oldLeft > 0) {
        if (diff > 0) {
            // 已显示左边控件,不能继续右滑
            return;
        }
        // 左滑隐藏左边控件
        this.left =  Math.max(this.oldLeft + diff, 0);
        return;
    }
    if (diff < 0) {
        // 已显示右边控件,不能继续左滑
        return;
    }
    // 右滑隐藏右边控件
    this.left = Math.min(this.oldLeft + diff, 0);
}
1234567891011121314151617181920212223242526272829303132
  1. 滑动结束,判断总滑动距离,不超过1/3自动复原
ts
                    

touchEnd(e: TouchEvent) {
    if (!this.isTouch) {
        // 点击,并复原
        this.animation(this.left, 0);
        this.$emit('click');
        return;
    }
    if (this.left == 0) {
        return;
    }
    if (this.left > 0) {
        const width = this.getLeftWidth();
        this.animation(this.left, this.left * 3 > width ? width : 0);
        return;
    }
    const width = - this.getRightWidth();
    this.animation(this.left, this.left * 3 < width ? width : 0);
}
1234567891011121314151617181920
  1. 点击事件需要复原

关于操作一个隐藏其他实现设想

  1. 通过父控件记录所有子控件引用,并标记子空间顺序,在父控件调用子元素复原方法或子控件内部调用

  2. 子控件在初始化的时刻,生成一个自增唯一标识,然后根据这个挂载到父控件或全局属性上

完整代码

html
                                                                                                                                                     

<template>
    <div class="swipe-row" :style="{left: left + 'px'}">
        <div class="actions-left" ref="left">
            <slot name="left"></slot>
        </div>
        <div :class="['swipe-content', name]"  
            @touchstart='touchStart'
            @touchmove='touchMove'
            @touchend='touchEnd'>
            <slot></slot>
        </div>
        <div class="actions-right" ref="right">
            <slot name="right">
                <i class="fa fa-trash" @click="tapRemove"></i>
            </slot>
        </div>
    </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from 'vue-property-decorator';

@Component
export default class SwipeRow extends Vue {
    @Prop([String, Array]) readonly name!: string| string[];
    @Prop([Number, String]) readonly index!: number|string;
    oldLeft: number = 0;
    left = 0;
    startX = 0;
    isTouch = false;
    leftBox: HTMLDivElement | null = null;
    rightBox: HTMLDivElement | null = null;

    mounted() {
        this.leftBox = this.$refs.left as HTMLDivElement;
        this.rightBox = this.$refs.right as HTMLDivElement;
    }

    getLeftWidth(): number {
        if (!this.leftBox) {
            return 0;
        }
        return this.leftBox.clientWidth || this.leftBox.offsetWidth;
    }

    getRightWidth(): number {
        if (!this.rightBox) {
            return 0;
        }
        return this.rightBox.clientWidth || this.rightBox.offsetWidth;
    }

    tapRemove(item: any) {
        this.$emit('remove', item);
    }

    touchStart(e: TouchEvent) {
        this.resetOther();
        this.oldLeft = this.left;
        this.isTouch = false;
        this.startX = e.targetTouches[0].clientX;
    }

    touchMove(e: TouchEvent) {
        this.isTouch = true;
        const diff = e.targetTouches[0].clientX - this.startX;
        if (this.oldLeft == 0) {
            if (diff < 0) {
                this.left = Math.max(diff, -this.getRightWidth());
                return;
            }
            this.left = Math.min(diff, this.getLeftWidth());
            return;
        }
        if (this.oldLeft > 0) {
            if (diff > 0) {
                return;
            }
            this.left =  Math.max(this.oldLeft + diff, 0);
            return;
        }
        if (diff < 0) {
            return;
        }
        this.left = Math.min(this.oldLeft + diff, 0);
    }

    touchEnd(e: TouchEvent) {
        if (!this.isTouch) {
            this.animation(this.left, 0);
            this.$emit('click');
            return;
        }
        //const diff = e.changedTouches[0].clientX - this.startX;
        if (this.left == 0) {
            return;
        }
        if (this.left > 0) {
            const width = this.getLeftWidth();
            this.animation(this.left, this.left * 3 > width ? width : 0);
            return;
        }
        const width = - this.getRightWidth();
        this.animation(this.left, this.left * 3 < width ? width : 0);
    }
    // 补间动画
    animation(
        start: number, end: number, endHandle?: Function) {
        const diff = start > end ? -1 : 1;
        let step = 1;
        let handle = setInterval(() => {
            start += (step ++) * diff;
            if ((diff > 0 && start >= end) || (diff < 0 && start <= end)) {
                clearInterval(handle);
                this.left = end;
                endHandle && endHandle();
                return;
            }
            this.left = start;
        }, 16);
    }

    // 暴露出来给外部调用
    public reset() {
        if (this.left === 0) {
            return;
        }
        this.animation(this.left, 0);
    }

    // 复原其他
    resetOther() {
        if (typeof this.index == 'undefined') {
            return;
        }
        const items: SwipeRow[] = this.$parent.$refs.swiperow as SwipeRow[];
        if (!items || items.length < 1) {
            return;
        }
        for (let i = 0; i < items.length; i++) {
            if (items[i].index == this.index) {
                continue;
            }
            items[i].reset();
        }
    }
}
</script>
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149

使用

html
        
<div class="swipe-box">
    <SwipeRow v-for="(item, index) in items" :key="index" @remove="tapRemove(item)" :index="index" ref="swiperow">
        <div>
            这是内容
        </div>
    </SwipeRow>
</div>
12345678

样式

scss
                                               

.swipe-box {
    overflow: hidden;
    .swipe-row {
        width: 100%;
        position: relative;
        height: 5rem; 
        padding: 0;
        margin: 0;
        transition: left .5s;
        .swipe-content {
            width: 100%;
            display: block;
            height: 5rem;
        }
        .actions-left,
        .actions-right {
            position: absolute;
            height: 5rem; 
            min-height: 5rem;
            max-height: 5rem;
            text-align: center;
            font-size: 1.375rem;
            top: 0;
            white-space: nowrap;
            .fa {
                font-size: 1.875rem;
                padding: 1.5625rem 0.9375rem;
            }
            a {
                color: #fff;
                text-decoration: none;
            }
        }
        .actions-left {
            color: #fff;
            background-color: rgb(0, 187, 72);
            right: 100%;
        }
        .actions-right {
            color: #fff;
            background-color: #BB0000;
            left: 100%;
        }
    }
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
点击查看全文
标签: vue
0 577 0
升级vue3记录
2021年08月
最近有空折腾一下vue3,把以前的vue项目从vue2升级到vue3
滚动加载的页面默认无法后退保存之前的滚动位置,这么解决
玩转vue和小程序
Vuex 使用心得
2019年03月
Vuex 使用心得,typescript版。