unnapp 小程序长按拖动排序

一、单列排序

1.组件 sort.vue

<template>
  <view @touchmove.stop.prevent>
    <scroll-view
      :scroll-y="isTouchMove"
      :style="{
        height: windowHeight + 'px',
      }"
    >
      <view
        class="listItem"
        @longtap="longtap($event, index)"
        @touchstart="onTouchstart($event, index)"
        @touchmove="onTouchmove"
        @touchend="onTouchend"
        v-for="(item, index) in listArray"
        :key="index"
        :style="{
          height: lineHeight + 'px',
          'margin-bottom': '15px',
          top: listPosition[index].top + 'px',
          transition: curretnItemIndex === index ? 'initial' : '.3s',
        }"
        :class="{ activeClass: index == curretnItemIndex }"
      >
        {{ item.name }}
      </view>
    </scroll-view>
  </view>
</template>

<script>
export default {
  props: {
    //列表
    list: {
      type: Array,
      default: [],
    },
    // 列表每行高度
    lineHeight: {
      type: Number,
      default: 80,
    },
  },
  data() {
    return {
      listArray: [],
      // 所有元素定位位置
      listPosition: [],
      // 记录拖动前元素定位位置
      initListPosition: [],
      // 记录当前拖动元素的下标
      curretnItemIndex: -1,
      // 记录拖动前的位置
      recordPosition: {
        y: 0,
      },
      // 记录拖动前的定位位置
      recordCurrentPositionItem: {
        top: 0,
      },
      // 是否正在交换位置
      isChange: false,
      isDrag: false,
      isTouchMove: true,
      windowHeight: 0,
    };
  },
  created() {
    this.init();

    let windowHeight = uni.getSystemInfoSync().windowHeight;
    // 状态栏高度
    let statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
    const custom = wx.getMenuButtonBoundingClientRect();

    let navigationBarHeight =
      custom.height + (custom.top - statusBarHeight) * 2;

    let navHeight = navigationBarHeight + statusBarHeight;

    this.windowHeight = windowHeight - navHeight;
  },
  methods: {
    init() {
      this.listArray = [...this.list];
      const query = uni.createSelectorQuery().in(this);
      query.selectAll(".listItem").fields(
        {
          rect: true,
          size: true,
        },
        (data) => {
          data.forEach((item, index) => {
            this.listPosition.push({
              height: item.height,
              top: (item.height + 15) * index,
            });
          });
          this.initListPosition = [...this.listPosition];
        }
      );
      query.exec(); //执行所有请求
    },
    longtap(event, index) {
      this.isTouchMove = false;
      this.isDrag = true;

      const { pageY } = event.touches[0];

      // 记录当前拖动元素的下标
      this.curretnItemIndex = index;
      // 记录拖动前的位置
      this.recordPosition = {
        y: pageY,
      };
      // 记录拖动前的定位位置
      this.recordCurrentPositionItem = this.listPosition[index];
    },
    onTouchstart(event, index) {
      //   const { pageY } = event.touches[0];
      //   // 记录当前拖动元素的下标
      //   this.curretnItemIndex = index;
      //   // 记录拖动前的位置
      //   this.recordPosition = {
      //     y: pageY,
      //   };
      //   // 记录拖动前的定位位置
      //   this.recordCurrentPositionItem = this.listPosition[index];
    },
    onTouchmove(event) {
      if (!this.isDrag) {
        return;
      }
      const { pageY } = event.touches[0];

      // 获取移动的差
      this.$set(this.listPosition, this.curretnItemIndex, {
        top:
          this.listPosition[this.curretnItemIndex].top +
          (pageY - this.recordPosition.y),
      });
      // 记录位置
      this.recordPosition = {
        y: pageY,
      };
      // 向下
      if (
        this.listPosition[this.curretnItemIndex].top >=
        this.listPosition[this.curretnItemIndex + 1]?.top -
          this.initListPosition[0].height / 2
      ) {
        if (this.isChange) return;
        this.isChange = true;
        let temp = this.listArray[this.curretnItemIndex];
        console.log(temp);
        this.listArray[this.curretnItemIndex] =
          this.listArray[this.curretnItemIndex + 1];
        this.listArray[this.curretnItemIndex + 1] = temp;
        this.listPosition[this.curretnItemIndex + 1] =
          this.listPosition[this.curretnItemIndex];
        this.listPosition[this.curretnItemIndex] =
          this.recordCurrentPositionItem;
        this.curretnItemIndex = this.curretnItemIndex + 1;
        this.recordCurrentPositionItem =
          this.initListPosition[this.curretnItemIndex];
        this.isChange = false;
      }
      // 向上
      if (
        this.listPosition[this.curretnItemIndex].top <=
        this.listPosition[this.curretnItemIndex - 1]?.top +
          this.initListPosition[0].height / 2
      ) {
        if (this.isChange) return;
        this.isChange = true;
        let temp = this.listArray[this.curretnItemIndex];
        console.log(temp);
        this.listArray[this.curretnItemIndex] =
          this.listArray[this.curretnItemIndex - 1];
        this.listArray[this.curretnItemIndex - 1] = temp;
        this.listPosition[this.curretnItemIndex - 1] =
          this.listPosition[this.curretnItemIndex];
        this.listPosition[this.curretnItemIndex] =
          this.recordCurrentPositionItem;
        this.curretnItemIndex = this.curretnItemIndex - 1;
        this.recordCurrentPositionItem =
          this.initListPosition[this.curretnItemIndex];
        this.isChange = false;
      }
    },
    onTouchend(event) {
      if (!this.isDrag) {
        return;
      }
      this.isTouchMove = true;
      this.isDrag = false;
      // 拖动元素归位
      this.listPosition[this.curretnItemIndex] =
        this.initListPosition[this.curretnItemIndex];
      this.curretnItemIndex = -1;
      this.$emit("change", [...this.listArray]);
    },
  },
};
</script>

<style scoped lang="scss">
.listItem {
  width: 100%;
  align-items: center;
  box-sizing: border-box;
  background-color: #fff;
}

.activeClass {
  box-shadow: 0 0px 50rpx #cfcfcf;
  z-index: 999;
}
</style>

2.使用 index.vue

<template>
  <view>
    <sort
      ref="sort"
      v-if="sortList.length > 0"
      :list="sortList"
      :lineHeight="lineHeight"
      @change="changeSort"
    />
  </view>
</template>
  
  <script>
import sort from "./components/sort.vue";
export default {
  components: {
    sort,
  },
  data() {
    return {
      sortList: [{ name: "1" }, { name: "2" }, { name: "3" }],
      lineHeight: 108,
    };
  },
  methods: {
    changeSort(e) {
      console.log(e, "修改后的新排序");
    },
  },
};
</script>

二、多列排序

1.sort.vue

<template>
  <view>
    <scroll-view
      :scroll-y="isEdit"
      :style="{
        height: windowHeight + 'px',
      }"
    >
      <movable-area
        id="dragSortArea"
        :style="{ height: boxHeight + 'px' }"
      >
        <block v-for="(item, index) in cloneList" :key="item.id">
          <movable-view
            class="dragSort-view"
            direction="all"
            :class="{ 'is-touched': item.isTouched }"
            :x="item.x"
            :y="item.y"
            :damping="40"
            :disabled="!isEdit"
            @longtap="longtap($event, index)"
            @change="onChange($event, item)"
            @touchstart="onTouchstart(item)"
            @touchend="onTouchend(item, index)"
            :style="{
              width: columnWidth + 'px',
              height: rpxTopx(rowHeight) + 'px',
              zIndex: item.zIndex,
            }"
          >
            {{ item[label] }}
          </movable-view>
        </block>
      </movable-area>
    </scroll-view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      cloneList: [], //用来展示的数据列表
      cacheList: [], //用来在点击“编辑”文字按钮的时候,将当前list的数据缓存,以便在取消的时候用到
      positionList: [], //用来存储xy坐标定位的列表
      columnWidth: 0, //列宽,单位px
      rowNum: 1, //行数
      boxHeight: 10, //可拖动区域的高度,单位px
      windowWidth: 750, //系统获取到的窗口宽度,单位px
      curTouchPostionIndex: 0, //当前操作的移动块在positionList队列里的索引
      xMoveUnit: 0, //沿x轴移动时的单位距离,单位px
      yMoveUnit: 0, //沿y轴移动时的单位距离,单位px
      clearT: "", //onChange事件中使用
      clearF: "", //点击“完成”文字按钮时使用
      isEdit: false, //是否在编辑状态
      windowHeight: 0,
    };
  },
  props: {
    //props里属性Number的单位都为rpx,在操作的时候需要用rpxTopx进行转换
    list: {
      type: Array,
      default() {
        return [];
      },
    },
    label: {
      //list队列中的对象中要用来展示的key名
      type: String,
      default: "name",
    },
    rowHeight: {
      //行高,单位rpx
      type: Number,
      default: 60,
    },
    rowSpace: {
      //行间距,单位rpx
      type: Number,
      default: 15,
    },
    columnSpace: {
      //列间距,单位rpx
      type: Number,
      default: 15,
    },
    columnNum: {
      //列数
      type: Number,
      default: 4,
    },
    zIndex: {
      //可移动项的默认z-index
      type: Number,
      default: 1,
    },
  },
  created() {
    this.windowWidth = uni.getSystemInfoSync().windowWidth;
    let windowHeight = uni.getSystemInfoSync().windowHeight;
    // 状态栏高度
    let statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
    const custom = wx.getMenuButtonBoundingClientRect();

    let navigationBarHeight =
      custom.height + (custom.top - statusBarHeight) * 2;

    let navHeight = navigationBarHeight + statusBarHeight;

    this.windowHeight = windowHeight - navHeight;
  },

  mounted() {
    const query = uni.createSelectorQuery().in(this);
    query
      .select("#dragSortArea")
      .boundingClientRect((data) => {
        this.columnWidth =
          (data.width - (this.columnNum - 1) * this.rpxTopx(this.columnSpace)) /
          this.columnNum;
        this.handleListData();

        this.toggleEdit("edit");
      })
      .exec();
  },
  methods: {
    /* 切换编辑状态
     * [type] String 参数状态
     */
    toggleEdit(type) {
      if (type == "finish") {
        //点击“完成”
        this.isEdit = false;
        this.$emit("newDishList", this.getSortedIdArr());
      } else if (type == "cancel") {
        //点击“取消”,将数据恢复到最近一次编辑时的状态
        this.isEdit = false;
        this.updateList(this.cacheList);
      } else if (type == "edit") {
        //点击“编辑”
        this.isEdit = true;
        this.cacheList = JSON.parse(JSON.stringify(this.list));
      }
    },

    /* 更新父组件的list,并重新渲染布局
     * 有改变数组长度的操作内才需要调用此方法进行重新渲染布局进行更新,
     * 否则直接$emit('update:list')进行更新,无须调用此方法
     */
    updateList(arr) {
      this.$emit("update:list", arr);
      setTimeout(() => {
        this.handleListData();
      }, 100);
    },

    /* 处理源数据列表,生成展示用的cloneList和positionList布局位置信息 */
    handleListData() {
      this.cloneList = JSON.parse(JSON.stringify(this.list));
      this.positionList = [];
      this.rowNum = Math.ceil(this.cloneList.length / this.columnNum);
      this.boxHeight =
        this.rowNum * this.rpxTopx(this.rowHeight) +
        (this.rowNum - 1) * this.rpxTopx(this.rowSpace);
      this.xMoveUnit = this.columnWidth + this.rpxTopx(this.columnSpace);
      this.yMoveUnit =
        this.rpxTopx(this.rowHeight) + this.rpxTopx(this.rowSpace);
      this.cloneList.forEach((item, index) => {
        item.sortNumber = index;
        item.zIndex = this.zIndex;
        item.x = this.xMoveUnit * (index % this.columnNum); //单位px
        item.y = Math.floor(index / this.columnNum) * this.yMoveUnit; //单位px
        this.positionList.push({
          x: item.x,
          y: item.y,
          id: item.id,
        });
      });
    },

    /* 找到id在位置队列positionList里对应的索引 */
    findPositionIndex(id) {
      var resultIndex = 0;
      for (var i = 0, len = this.positionList.length; i < len; i++) {
        var item = this.positionList[i];
        if (item.id == id) {
          resultIndex = i;
          break;
        }
      }
      return resultIndex;
    },

    /* 触摸开始 */
    onTouchstart(obj) {
      if (!this.isEdit) {
        return false;
      }

      this.curTouchPostionIndex = this.findPositionIndex(obj.id);

      // 将当前拖动的模块zindex调成当前队列里的最大
      this.cloneList.forEach((item, index) => {
        if (item.id == obj.id) {
          item.zIndex = item.zIndex + 50;
          item.isTouched = true;
        } else {
          item.zIndex = this.zIndex + index + 1;
          item.isTouched = false;
        }
      });
      this.$set(this.cloneList, 0, this.cloneList[0]);
    },

    /* 触摸结束 */
    onTouchend(obj) {
      if (!this.isEdit) {
        return false;
      }
      this.startSort(this.curTouchPostionIndex, "onTouchend"); //再次调用并传参数‘onTouchend’,使拖动后且没有找到目标位置的滑块归位
    },

    /* 移动过程中触发的事件(所有移动块只要一有移动都会触发) */
    onChange(e, obj) {
      if (!this.isEdit) {
        return false;
      }
      var theX = e.detail.x,
        theY = e.detail.y,
        curCenterX = theX + this.columnWidth / 2,
        curCenterY = theY + this.rpxTopx(this.rowHeight) / 2;

      if (e.detail.source === "touch") {
        //表示由“拖动”触发
        var targetIndex = this.findTargetPostionIndex({
          curCenterX,
          curCenterY,
        });

        clearTimeout(this.clearT);
        this.clearT = setTimeout(() => {
          this.$nextTick(() => {
            this.startSort(targetIndex); //根据targetIndex将队列进行排序
          });
        }, 100);
      }
    },
    longtap() {
      this.isEdit = true;
    },
    /* 根据targetIndex将cloneList进行排序
     * [targetIndex] Number 当前拖动的模块拖动到positionList队列里的目标位置的索引
     * [type] String 值为onTouchend时,再次调用set方法
     */
    startSort(targetIndex, type) {
      var curTouchId = this.positionList[this.curTouchPostionIndex].id;
      if (this.curTouchPostionIndex < targetIndex) {
        for (var i = 0, len = this.positionList.length; i < len; i++) {
          var curItem = this.positionList[i];
          var nextItem =
            this.positionList[i + 1] ||
            this.positionList[this.positionList.length - 1];
          if (i >= this.curTouchPostionIndex && i <= targetIndex) {
            //找到要进行位移的索引集
            if (i == targetIndex) {
              curItem.id = curTouchId;
            } else {
              curItem.id = nextItem.id;
            }
          }
        }
      } else {
        var clonePostionList = JSON.parse(JSON.stringify(this.positionList));
        for (var i = 0, len = this.positionList.length; i < len; i++) {
          var curItem = this.positionList[i];
          var preItem = this.positionList[i - 1] || this.positionList[0];
          if (i >= targetIndex && i <= this.curTouchPostionIndex) {
            //找到要进行位移的索引集
            if (i == targetIndex) {
              curItem.id = curTouchId;
            } else {
              curItem.id = clonePostionList[i - 1].id;
            }
          }
        }
      }

      this.cloneList.forEach((item) => {
        item.x += 0.001;
        item.y += 0.001;
      });

      if (type == "onTouchend") {
        this.$set(this.cloneList, 0, this.cloneList[0]);
      }

      this.$nextTick(() => {
        this.cloneList.forEach((item) => {
          for (var i = 0, len = this.positionList.length; i < len; i++) {
            var item02 = this.positionList[i];
            if (item.id == item02.id) {
              item.x = item02.x;
              item.y = item02.y;
            }
          }
        });
        this.$set(this.cloneList, 0, this.cloneList[0]);
        this.curTouchPostionIndex = targetIndex;

        this.handleEmitData(); //需要在onChange事件里发射信息出去最稳妥,因为在快速拖动释放鼠标的时候该事件会再onTouchend后执行
      });
    },

    /* 处理要发射出去的数据队列,将排序后的结果同步到父组件的list */
    handleEmitData() {
      var idArr = this.getSortedIdArr();
      var emitList = [];
      idArr.forEach((id) => {
        for (var i = 0, len = this.list.length; i < len; i++) {
          var item = this.list[i];
          if (id == item.id) {
            emitList.push(item);
            break;
          }
        }
      });
      this.$emit("update:list", emitList);
    },

    /* 获取最后的排序完的id队列集 */
    getSortedIdArr() {
      return this.positionList.map((item) => item.id);
    },

    /* 找出拖动到positionList队列里的哪个目标索引
     * [curObj.curCenterX], Number 当前拖动的模块的中心点x轴坐标
     * [curObj.curCenterY], Number 当前拖动的模块的中心点y轴坐标
     * return 返回拖动到的目标索引
     */
    findTargetPostionIndex(curObj) {
      var resultIndex = this.curTouchPostionIndex;
      for (var i = 0, len = this.positionList.length; i < len; i++) {
        var item = this.positionList[i];
        if (
          curObj.curCenterX >= item.x &&
          curObj.curCenterX <= item.x + this.columnWidth
        ) {
          if (
            curObj.curCenterY >= item.y &&
            curObj.curCenterY <= item.y + this.rpxTopx(this.rowHeight)
          ) {
            resultIndex = i;
            break;
          }
        }
      }
      return resultIndex;
    },

    /* prx转换成px,返回值还是不带px单位的Number */
    rpxTopx(v) {
      return (this.windowWidth * v) / 750;
    },
  },
};
</script>

<style scoped>
.dragSort-view {
  background-color: #ffffff;
  text-align: center;
  border-radius: 4rpx;
}

.dragSort-view.is-touched {
  opacity: 0.9;
}
</style>

2.使用 index.vue

<template>
  <view>
    <sort
      :list="dataArray"
      label="name"
      :columnNum="2"
      :columnSpace="30"
      :rowHeight="72"
      :rowSpace="30"
      @newDishList="newDishList"
      ref="dragSortNew"
    />
  </view>
</template>
  
  <script>
import sort from "./components/sort.vue";
export default {
  components: {
    sort,
  },
  data() {
    return {
      dataArray: [{ name: "1" }, { name: "2" }, { name: "3" }],
    };
  },
  methods: {
    newDishList(e) {
      console.log(e, "修改后的新排序");
    },
  },
};
</script> 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值