超详细记录vue+sortablejs拖拽心得,以及遇到的各种问题

2,189 阅读4分钟

一、前言

5.gif

【第一次在掘金上写文章,还不太熟悉,如有错误的地方,欢迎各位大佬指导~】

那是一个阳谷明媚的早上,我接了一杯滚烫的热水缓缓走进会议室。

在人员到齐后产品开始讲解这一期的需求~~~

我需要做的地方有很多,但总体来说难度so easy,心里窃喜脸上没表现出来,淡定的开完会后开始动工,然后折磨的旅途就此展开。。。

二、分析需求

其中的一个需求是要做一个横排,多个元素,可以拖拽排序的功能。

嘿,这个功能以前做过,内心窃喜,这不分分钟搞定。

立马翻开以前的代码,用的是vue-smooth-dnd插件,两三下就初步完成了一个可以拖拽的列表。哎嘿,可以提前摸鱼了。

不出意外的第一个坎就来了,vue-smooth-dnd只支持竖列的排序,不支持横着的排序。在不挣扎想换个插件是一天以后的事,期间查了api,找了度娘,甚至问了GPT大爹,最后悬着的心终于是死了。。。 

最后的最后甚至动起了歪脑筋,功能不支持我就通过样式去处理。在一番操作后能实现了一排之间互相拖动的功能,又一次沾沾自喜的时候发现如果一排装不下,换到了第二排拖动的时候,非本行进行拖拽的时候排序是乱的。。。

1.jpg

很明显,产品这关是过不了的,在实在没办法的情况下,我换了插件Sortable

三、sortablejs的使用

在打开Sortable官网介绍的时候,脑袋里面的想法是这玩意能救我狗命,里面各种操作和控制都有,充分满足我现在的需求,边看边干。

【用的vue2,以下代码均以vue2来完成】

1.npm安装

npm install sortablejs --save

2.vue导入

import Sortable from 'sortablejs';

3.开始使用

【html部分】
<div  ref="sortRef" class="cList">   
    <div v-for="(item,index) in list" :key="index" class="grid__item">     
        <div class="listTitle">{{ item.name }}</div>   
          <div class="isSwitch">      
              <div class="isTis">是否必填</div>      
              <el-switch        
              v-model="item.headerNotNull"         
              active-color="#13ce66"         
              inactive-color="#ff4949"         
              active-value="1"         
              inactive-value="0">      
              </el-switch>    
         </div>
     </div>
</div>

【JS部分】
data() {
  return {
      list:[
      {          
          id:'100',          
          name:'姓名',          
          headerNotNull:'1',        
       },       
       {          
           id:'101',          
           name:'年龄',          
           headerNotNull:'0',        
        },        
        {          
            id:'102',          
            name:'性别',          
            headerNotNull:'0',        
         },        
         {          
             id:'103',          
             name:'地址',          
             headerNotNull:'0',        
             },      
       ],  
     }
}

mounted() {
  this.sortDrag()},methods: {
   sortDrag(){      
       let that = this      
       this.$nextTick(() => {        
           let el = this.$refs.sortRef        
           new Sortable(el, {          
               animation: 100,          
               onEnd: (evt) => {            
                   that.list.splice(evt.newIndex, 0, that.list.splice(evt.oldIndex, 1)[0])}     
               })      
           })    
        },
} 

拖动是拖动了,但数据和视图完全各是各的,没有毛关系。。。

还能怎么了,接着改呗。。。仔细观察后发现,数据的顺序是对的,视图是乱的,抽象!真抽1象!

直接说出解决办法:在onEnd内手动赋一次值,解决视图更新乱的问题。

onEnd: (evt) => {  
    that.addWorkArray.splice(evt.newIndex, 0, that.addWorkArray.splice(evt.oldIndex, 1)[0])  
    let newArray = that.addWorkArray.slice(0)  
    that.addWorkArray = []  that.$nextTick(()=>{    that.addWorkArray = newArray  })
   }

完结 撒花~~~

四、功能迭代以及完整代码

怎么可能,在我沾沾自喜的时候,产品过来了,我们的需求有一点小小的变动!

我们需要在设置模块,每个模块下是可以拖动的小字段。模块可以新增,也可以删除,但不能删除第一个模块,模块后能需要增加一个开关,模块可以自己命名。怎么样,很简单吧!

王德发??听到“很简单”这几个字就头皮发麻!

事实上确实,这玩意咋看简单,实际开发中插件自带的一些特性和本次的需求组合成了强力地雷,给我炸的体无完肤!

先贴出修复后的完整代码和实例图,在说说这次遇到的坑

【html部分】

<div class="listN" v-for="(item, index) in listArray" :key="index" :data-list="index" ref="sortableLists">
  <!-- 输入框区域 -->
  <div class="listNTop">
    <i class="el-icon-circle-close delIcon" v-if="index != 0" @click="delList(index)"></i>
    <el-input style="width: 300px;" v-model="item.groupName" placeholder="请输入模块名称,无内容则不显示"></el-input>
    <el-select style="width: 100px; margin-left: 20px;" v-model="item.groupType" placeholder="请选择">
      <el-option v-for="item in textOrSwitch" :key="item.value" :label="item.label" :value="item.value">
      </el-option>
    </el-select>
    <div class="switchTip" v-if="item.groupType == '1'">(在模板中开启状态才能填写当前板块下内容)</div>
  </div>
  <!-- 字段区域 -->
  <div v-for="(it, ind) in item.templateList" :key="ind" :data-id="it.id" class="item">
    <div class="listTitle">{{ it.name }}</div>
    <div class="isSwitch">
      <div class="isTis">是否必填</div>
      <el-switch v-model="it.headerNotNull" active-color="#13ce66" inactive-color="#ff4949" active-value="1"
        inactive-value="0">
      </el-switch>
    </div>
  </div>
</div>

【JS部分】
data() {
    return {
      open:false,
      id:'',
      listArray:[
        {
          id:'0',
          groupName:'基础信息',
          groupType:0,
          templateList:[
            {
              id:'100',
              name:'姓名',
              headerNotNull:'1',
            },
            {
              id:'101',
              name:'年龄',
              headerNotNull:'0',
            },
            {
              id:'102',
              name:'性别',
              headerNotNull:'0',
            },
            {
              id:'103',
              name:'地址',
              headerNotNull:'0',
            },
          ]
        }
      ],
      // 文本或开关
      textOrSwitch:[
        {
          label:'文本',
          value:0,
        },
        {
          label:'开关',
          value:1,
        },
      ],
    };
  },
  mounted() {
    this.newSortable()
  },
  
  methods: {
    // 初始化拖拽
    newSortable(){
      const options = {
        group: 'shared',
        preventOnFilter: false, //阻止事件穿透,否则输入框无法输入
        filter:'.listNTop',
        animation: 100,
        onEnd: (evt) => {
          this.updateListOrder(evt);
        }
      };
      this.$nextTick(() => {
        this.$refs.sortableLists.forEach(list => {
          Sortable.create(list, { ...options, dataIdAttr: 'data-list' });
        });
      });
    },
    updateListOrder(evt) {
        // 获取拖动前和拖动后的索引及列表索引
        // 原数组内索引
        const fromIndex = evt.oldIndex -1;
        // 新数组内索引
        const toIndex = evt.newIndex -1;
        // 原列表索引
        const fromListIndex = parseInt(evt.from.getAttribute('data-list'));
        // 新列表索引
        const toListIndex = parseInt(evt.to.getAttribute('data-list'));
        // 获取拖动前和拖动后的列表
        const fromList = this.listArray[fromListIndex].templateList;
        const toList = this.listArray[toListIndex].templateList;
        console.log('原数组内索引', fromIndex, '新数组内索引', toIndex,'原列表索引',fromListIndex,'新列表索引',toListIndex,'拖动前列表','拖动后列表')
        if (toIndex == -1) {
            // 不允许拖动
            if (fromListIndex === toListIndex) {
                // 拖动到同一列表内
                this.listArray[fromListIndex].templateList.splice(toIndex, 0, this.listArray[fromListIndex].templateList.splice(fromIndex, 1)[0])
                let newData = this.listArray[fromListIndex].templateList.slice(0)
                this.listArray[fromListIndex].templateList = []
                this.$nextTick(() => {
                  this.listArray[fromListIndex].templateList = newData
                });
            } else {
                let newData1 = this.listArray[fromListIndex].templateList.splice(fromIndex, 1)[0]
                this.listArray[toListIndex].templateList.splice(toIndex, 0, newData1)
                let newData = this.listArray[fromListIndex].templateList.slice(0)
                this.listArray[fromListIndex].templateList = []
                this.$nextTick(() => {
                  this.listArray[fromListIndex].templateList = newData
                });
            }
        } else {
            if (fromListIndex === toListIndex) {
                // 拖动到同一列表内
                this.listArray[fromListIndex].templateList.splice(toIndex, 0, this.listArray[fromListIndex].templateList.splice(fromIndex, 1)[0])
                let newData = this.listArray[fromListIndex].templateList.slice(0)
                this.listArray[fromListIndex].templateList = []
                this.$nextTick(() => {
                  this.listArray[fromListIndex].templateList = newData
                });
            } else {
                let newData1 = this.listArray[fromListIndex].templateList.splice(fromIndex, 1)[0]
                this.listArray[toListIndex].templateList.splice(toIndex, 0, newData1)
                let newData = this.listArray[fromListIndex].templateList.slice(0)
                this.listArray[fromListIndex].templateList = []
                this.$nextTick(() => {
                  this.listArray[fromListIndex].templateList = newData
                });
            }
        }
    },
    // 新增内容
    listArrayData(){
      this.listArray.push({
        groupName:'',
        groupType:0,
        id:null,
        templateList:[]
      })
      this.$nextTick(() => {
        this.newSortable()
      });
    },
    // 删除内容
    delList(i){
      if(this.listArray[i].templateList.length > 0){
        this.$modal.msgError('当前板模块存在内容,需将内容移除后才能删除该模块!')
        return
      }
      if(this.listArray[i].id != null){
        this.form.deleteGroupIdList.push(this.listArray[i].id)
      }
      this.listArray.splice(i,1)
    },
  }

完成的效果就是这样:

5、细说遇到的问题以及解决思路:

1.新增后不能拖动:是因为拖动模块在mounted的时候就初始化了,所以新增的数据也需要初始化。

this.$nextTick(() => {   this.newSortable() });

2.新增后的输入框这个区域是不能被拖动,或者被改变位置。

为了解决这个问题,前面滋生出N个问题,这个插件是根据代码的层级去决定是否可以拖动。

在最开始的时候我在字段区域包裹了一层div方便写样式。结果是字段区域只能整块拖动。

不能拖动用的是api里面的方法:.listNTop是输入框区域的class名

filter:'.listNTop',

然而不让拖动的问题只是视图上得到了解决! 在拖动后虽然拖不上去但数据的结构发生了变化!具体表现为:拖动后回到原来的位置,但报错了,因为他把输入框区域认为是数组内的一个元素,实际上不是,所以报错了。

好心累!

我做了如下的两个操作

a.拿到的索引需要-1,为了处理后面的数据,否则对不上。

// 原数组内索引 const fromIndex = evt.oldIndex -1; 

// 新数组内索引 const toIndex = evt.newIndex -1; 

b.对索引进行判断,如果索引是-1,那就对数据进行处理

拖动的视图与数据都没问题,正准备长舒一口气的时候,嘿 问题又来了,输入框没办法输东西!

3.输入框无法输入

在仔细观察后发现输入框不是无法输入,是没有获取到被点击选中的焦点。出现这个问题的原因是点击的时候焦点被拖动截取了。真是不让人省心啊,又接着看api,看了半天没看出啥有用的结果,只能问gpt大哥了,大哥给我说用这一句魔法就能解决我当前的困境。

preventOnFilter: false;

尝试以后解决了问题。在回顾api的时候发现,真怪人家,人家写出来了

只能说是急火攻心了,api没仔细看。。。

6.png

自此本期使用到sortablejs插件遇到的几个值得记录问题记录完毕,完结撒花~~