uniapp 全端实现列表drag上下拖拽排序

拖拽中

介绍

子组件使用css tranisition实现动画效果,使用touchstart、touchmove、touchend实现拖拽事件。

*注意:该组件需要仅是上下拖拽,并且需要给出每个item统一固定高度,不满足您的需求请参考全网其他文章。

父组件使用

javascript 复制代码
<template>
  <m-drag :item-height="50" :list="list" @change="dragComplete">
    <template #default="{ item }">
      <view class="name">{{ item.name }}</view>
    </template>
  </m-drag>
</template>

<script setup>
  const list = [
    {
      name: '余额宝支付'
    },
    {
      name: '余额支付'
    },
    {
      name: '建设银行储蓄卡支付'
    },
    {
      name: '农业银行储蓄卡支付'
    }
  ]

  // 拖拽完成
  function dragComplete(newList, dragItem) {
    console.log(newList, dragItem)
  }
</script>

<style lang="scss" scoped>
  .name {
    display: flex;
    align-items: center;
    margin: 0 24rpx;
    height: 50px;
    color: #383838;
    font-size: 30rpx;
    border-bottom: 1px solid #f5f5f5;
  }
</style>

子组件

Vue3版本

javascript 复制代码
<template>
  <scroll-view class="m-drag" scroll-y :style="{ height: itemHeight * state.newList.length + 'px' }">
    <view
      v-for="(item, index) in state.newList"
      :key="index"
      class="m-drag-item"
      :class="{ active: state.currentIndex === index }"
      :style="{
        top: state.itemYList[index].top + 'px'
      }"
    >
      <slot :item="item" />
      <!-- css实现拖拽图标 -->
      <view class="icon"
        @touchstart="touchStart($event, index)"
        @touchmove="touchMove"
        @touchend="touchEnd">
        <i class="lines" />
      </view>
    </view>
  </scroll-view>
</template>

<script setup>
  import { reactive, watch } from 'vue'
  const emits = defineEmits(['change'])
  const props = defineProps({
    // 每一项item高度,必须
    itemHeight: {
      type: Number,
      required: true
    },
    // 数据列表,必须
    list: {
      type: Array,
      required: true
    },
    // 是否只读
    readonly: {
      type: Boolean,
      default: false
    }
  })

  const state = reactive({
    // 数据
    newList: [],
    // 记录所有item的初始坐标
    initialItemYList: [],
    // 坐标数据
    itemYList: [],
    // 记录当前手指的垂直方向的坐标
    touchY: 0,
    // 记录当前操作的item数据
    currentItemY: {},
    // 当前操作的item的下标
    currentIndex: -1
  })

  watch(
    () => props.list,
    (val) => {
      if (!val?.length) return
      // 获取数据列表
      state.newList = val
      // 获取所有item的初始坐标
      state.initialItemYList = getItemsY()
      // 初始化坐标
      state.itemYList = getItemsY()
    },
    {
      immediate: true
    }
  )

  /** @初始化各个item的坐标 **/
  function getItemsY() {
    return props.list.map((item, i) => {
      return {
        left: 0,
        top: i * props.itemHeight
      }
    })
  }

  /** @开始触摸 */
  function touchStart(event, index) {
    // 只读
    if (props.readonly) return
    // H5拖拽时,禁止触发ios回弹
    h5BodyScroll(false)
    const [{ pageY }] = event.touches

    // 记录数据
    state.currentIndex = index
    state.touchY = pageY
    state.currentItemY = state.itemYList[index]
  }

  /** @手指滑动 **/
  function touchMove(event) {
    // 只读
    if (props.readonly) return
    const [{ pageY }] = event.touches
    const current = state.itemYList[state.currentIndex]
    const prep = state.itemYList[state.currentIndex - 1]
    const next = state.itemYList[state.currentIndex + 1]
    // 获取移动差值
    state.itemYList[state.currentIndex] = {
      top: current.top + (pageY - state.touchY)
    }
    // 记录手指坐标
    state.touchY = pageY
    // 向下移动(超过下一个的1/2就进行换位)
    if (next && current.top > next.top - props.itemHeight / 2) {
      changePosition(state.currentIndex + 1)
    } else if (prep && current.top < prep.top + props.itemHeight / 2) {
      // 向上移动(超过上一个的1/2就进行换位)
      changePosition(state.currentIndex - 1)
    }
  }

  /** @手指松开 */
  function touchEnd() {
    // 只读
    if (props.readonly) return
    // 传给父组件新数据
    emits('change', state.newList, state.newList[state.currentIndex])
    // 将拖拽的item归位
    state.itemYList[state.currentIndex] = state.initialItemYList[state.currentIndex]
    state.currentIndex = -1
    // H5开启ios回弹
    h5BodyScroll(true)
  }

  /** @交换位置 **/
  // index 需要与第几个下标交换位置
  function changePosition(index) {
    console.log(index)
    // 记录当前拖拽的item数据
    const tempItem = state.newList[state.currentIndex]
    // 设置原来位置的item
    state.newList[state.currentIndex] = state.newList[index]
    // 将临时存放的数据设置好
    state.newList[index] = tempItem

    // 调整位置item
    state.itemYList[index] = state.itemYList[state.currentIndex]
    state.itemYList[state.currentIndex] = state.currentItemY

    // 改变当前操作的的下标
    state.currentIndex = index

    // 记录新位置的数据
    state.currentItemY = state.initialItemYList[state.currentIndex]
  }

  // h5 ios回弹
  function h5BodyScroll(flag) {
    // #ifdef H5
    document.body.style.overflow = flag ? 'initial' : 'hidden'
    // #endif
  }
</script>

<style scoped lang="scss">
  .m-drag {
    position: relative;
    width: 100%;
    ::-webkit-scrollbar {
      display: none;
    }
    .m-drag-item {
      position: absolute;
      left: 0;
      right: 0;
      transition: all ease 0.25s;
      display: flex;
      align-items: center;
      > :deep(view:not(.icon)) {
        flex: 1;
      }
      .icon {
        padding: 30rpx;
        .lines {
          background: #e0e0e0;
          width: 20px;
          height: 2px;
          border-radius: 100rpx;
          margin-left: auto;
          position: relative;
          display: block;
          transition: all ease 0.25s;
          &::before,
          &::after {
            position: absolute;
            width: inherit;
            height: inherit;
            border-radius: inherit;
            background: #e0e0e0;
            transition: inherit;
            content: '';
            display: block;
          }
          &::before {
            top: -14rpx;
          }
          &::after {
            bottom: -14rpx;
          }
        }
      }
      // 拖拽中的元素,添加阴影、关闭动画、层级置顶
      &.active {
        box-shadow: 0 0 14rpx rgba(0, 0, 0, 0.08);
        transition: initial;
        z-index: 1;
        .icon .lines {
          background: #2e97f9;
          &::before,
          &::after {
            background: #2e97f9;
          }
        }
      }
    }
  }
</style>

Vue2版本

javascript 复制代码
<template>
  <scroll-view class="m-drag" scroll-y :style="{ height: itemHeight * newList.length + 'px' }">
    <view
      v-for="(item, index) in newList"
      :key="index"
      class="m-drag-item"
      :class="{ active: currentIndex === index }"
      :style="{
        top: itemYList[index].top + 'px'
      }"
    >
      <slot :item="item" />
      <!-- css实现拖拽图标 -->
      <view class="icon"
        @touchstart="touchStart($event, index)"
        @touchmove="touchMove"
        @touchend="touchEnd">
        <i class="lines" />
      </view>
    </view>
  </scroll-view>
</template>
<script>
  export default {
    props: {
      // 每一项item高度,必须
      itemHeight: {
        type: Number,
        required: true
      },
      // 数据列表,必须
      list: {
        type: Array,
        required: true
      },
      // 是否只读
      readonly: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        // 数据
        newList: [],
        // 记录所有item的初始坐标
        initialItemYList: [],
        // 坐标数据
        itemYList: [],
        // 记录当前手指的垂直方向的坐标
        touchY: 0,
        // 记录当前操作的item数据
        currentItemY: {},
        // 当前操作的item的下标
        currentIndex: -1
      }
    },
    watch: {
      list: {
        handler(val) {
          if (!val?.length) return
          // 获取数据列表
          this.newList = val
          // 获取所有item的初始坐标
          this.initialItemYList = this.getItemsY()
          // 初始化坐标
          this.itemYList = this.getItemsY()
        },
        immediate: true
      }
    },
    created() {},
    methods: {
      /** @初始化各个item的坐标 **/
      getItemsY() {
        return this.list.map((item, i) => {
          return {
            left: 0,
            top: i * this.itemHeight
          }
        })
      },
      /** @开始触摸 */
      touchStart(event, index) {
        // 只读
        if (this.readonly) return
        // H5拖拽时,禁止触发ios回弹
        this.h5BodyScroll(false)
        const [{ pageY }] = event.touches

        // 记录数据
        this.currentIndex = index
        this.touchY = pageY
        this.currentItemY = this.itemYList[index]
      },
      /** @手指滑动 **/
      touchMove(event) {
        // 只读
        if (this.readonly) return
        const [{ pageY }] = event.touches
        const current = this.itemYList[this.currentIndex]
        const prep = this.itemYList[this.currentIndex - 1]
        const next = this.itemYList[this.currentIndex + 1]
        // 获取移动差值
        this.itemYList[this.currentIndex] = {
          top: current.top + (pageY - this.touchY)
        }
        // 记录手指坐标
        this.touchY = pageY
        // 向下移动(超过下一个的1/2就进行换位)
        if (next && current.top > next.top - this.itemHeight / 2) {
          this.changePosition(this.currentIndex + 1)
        } else if (prep && current.top < prep.top + this.itemHeight / 2) {
          // 向上移动(超过上一个的1/2就进行换位)
          this.changePosition(this.currentIndex - 1)
        }
      },
      /** @手指松开 */
      touchEnd() {
        // 只读
        if (this.readonly) return
        // 传给父组件新数据
        this.$emit('change', this.newList, this.newList[this.currentIndex])
        // 将拖拽的item归位
        this.itemYList[this.currentIndex] = this.initialItemYList[this.currentIndex]
        this.currentIndex = -1
        // H5开启ios回弹
        this.h5BodyScroll(true)
      },
      /** @交换位置 **/
      // index 需要与第几个下标交换位置
      changePosition(index) {
        // 记录当前拖拽的item数据
        const tempItem = this.newList[this.currentIndex]
        // 设置原来位置的item
        this.newList[this.currentIndex] = this.newList[index]
        // 将临时存放的数据设置好
        this.newList[index] = tempItem

        // 调整位置item
        this.itemYList[index] = this.itemYList[this.currentIndex]
        this.itemYList[this.currentIndex] = this.currentItemY

        // 改变当前操作的的下标
        this.currentIndex = index

        // 记录新位置的数据
        this.currentItemY = this.initialItemYList[this.currentIndex]
      },
      // h5 ios回弹
      h5BodyScroll(flag) {
        // #ifdef H5
        document.body.style.overflow = flag ? 'initial' : 'hidden'
        // #endif
      }
    }
  }
</script>

<style scoped lang="scss">
  .m-drag {
    position: relative;
    width: 100%;
    ::-webkit-scrollbar {
      display: none;
    }
    .m-drag-item {
      position: absolute;
      left: 0;
      right: 0;
      transition: all ease 0.25s;
      display: flex;
      align-items: center;
      > :deep(view:not(.icon)) {
        flex: 1;
      }
      .icon {
        padding: 30rpx;
        .lines {
          background: #e0e0e0;
          width: 20px;
          height: 2px;
          border-radius: 100rpx;
          margin-left: auto;
          position: relative;
          display: block;
          transition: all ease 0.25s;
          &::before,
          &::after {
            position: absolute;
            width: inherit;
            height: inherit;
            border-radius: inherit;
            background: #e0e0e0;
            transition: inherit;
            content: '';
            display: block;
          }
          &::before {
            top: -14rpx;
          }
          &::after {
            bottom: -14rpx;
          }
        }
      }
      // 拖拽中的元素,添加阴影、关闭动画、层级置顶
      &.active {
        box-shadow: 0 0 14rpx rgba(0, 0, 0, 0.08);
        transition: initial;
        z-index: 1;
        .icon .lines {
          background: #2e97f9;
          &::before,
          &::after {
            background: #2e97f9;
          }
        }
      }
    }
  }
</style>

iOS 回弹

在ios设备上,拖拽会触发设备的回弹,页面会跟着拖拽滚动,导致拖拽体验不好,我们需要禁用iOS回弹效果。

微信小程序

在pages.json中,添加属性 "disabledScroll": true

javascript 复制代码
{
  "path": "pages/a/index",
  "style": {
    "navigationBarTitleText": "页面标题",
    "disabledScroll": true
  }
}

支付宝小程序

在pages.json中,添加属性 "allowsBounceVertical": "NO"

javascript 复制代码
{
  "path": "pages/a/index",
  "style": {
    "navigationBarTitleText": "页面标题",
    "allowsBounceVertical": "NO"
  }
}

App

在pages.json中,添加属性 "bounce": "none"

javascript 复制代码
{
  "path": "pages/a/index",
  "style": {
    "navigationBarTitleText": "页面标题",
    "app-plus": {
      "bounce": "none"
    }
  }
}

H5

子组件已经对iOS H5页面回弹,作了处理,具体见 h5BodyScroll

相关推荐
修炼的dance4 分钟前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M10 分钟前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写12 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李14 分钟前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易28 分钟前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马39 分钟前
前沿视界
前端
kyriewen44 分钟前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒1 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端
Flynt2 小时前
别写拖拽行为,只声明"能拖到哪":UIST 最佳论文的拖拽方案我实测了
前端·svg·preact