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

相关推荐
雪碧聊技术25 分钟前
uniapp如何创建并使用组件?组件通过Props如何进行数据传递?
uni-app·创建组件·使用组件·props数据传递
中微子28 分钟前
🔥 React Context 面试必考!从源码到实战的完整攻略 | 99%的人都不知道的性能陷阱
前端·react.js
@Dream_Chaser29 分钟前
uniapp页面间通信
uni-app
中微子2 小时前
React 状态管理 源码深度解析
前端·react.js
加减法原则3 小时前
Vue3 组合式函数:让你的代码复用如丝般顺滑
前端·vue.js
yanlele3 小时前
我用爬虫抓取了 25 年 6 月掘金热门面试文章
前端·javascript·面试
lichenyang4533 小时前
React移动端开发项目优化
前端·react.js·前端框架
天若有情6733 小时前
React、Vue、Angular的性能优化与源码解析概述
vue.js·react.js·angular.js
你的人类朋友3 小时前
🍃Kubernetes(k8s)核心概念一览
前端·后端·自动化运维
web_Hsir3 小时前
vue3.2 前端动态分页算法
前端·算法