Пойдем со мной, реализуй и инкапсулируй компоненты перетаскивания с 0

Vue.js
Пойдем со мной, реализуй и инкапсулируй компоненты перетаскивания с 0

Сначала продемонстрируйте окончательный эффект:

Эффекты плавного перетаскивания и смены положения, а также обновление данных в реальном времени.

效果演示1

Поддерживает настройку стиля и содержимого компонентов.

效果演示2

Это цикл статейСтатья 2, я инкапсулировалПеретащите компоненты карты, реализованные с помощью vueи публикуется в npm, который подробно записывает весь производственный процесс. Всего есть три статьи, в которых представлены идеи производства и проблемы, с которыми столкнулись компоненты, а также какие проблемы возникли и как их решить в процессе публикации в npm и загрузки для использования.


Сначала определите общие требования исходной функции, которую необходимо реализовать.:

  • Щелкните карточку мышью, чтобы переместить ее, и прокрутите мышью при прокрутке.
  • Меняйте местами при перемещении карты в соседнюю область над другой картой. При смене позиций карта в середине двух карт должна автоматически перемещаться вперед/назад.
  • Когда мышь отпускается, карта возвращается в исходное/новое положение.
  • Выставляйте свойства и события для вызовов родительских компонентов и создайте слоты

Я предлагаю, если вы увидите половину друзей, которые не знают, что я пишу, перейти прямоРепозиторий исходного кодаВзгляните на мой исходный код.Если вы просто хотите получить быстрое понимание, просто посмотрите на общую идею следующих вопросов!

Q1: Как реализовать движение карты?

вся идея:

  • Все карты одинаково принимают абсолютную компоновку и рассчитывают верхнюю и левую часть в соответствии с такими параметрами, как номер позиции и количество столбцов для отображения.
  • При нажатии на карту сначала определите, находится ли карта в состоянии перетаскивания. Если нет, перейдите к следующему шагу, получите данные и удалите стандартный переход карты.
  • После щелчка событие перемещения мыши отслеживается глобально, и карта перемещается так же, как перемещается мышь. В то же время вам нужно прослушивать событие прокрутки окна, чтобы выполнить ту же операцию.
  • При отпускании мыши все мониторы очищаются, а карта восстанавливается в позицию, рассчитанную в соответствии с номером позиции. Измените состояние клика на false

Реализация:

Прежде всего, нам нужно создать структуру карты и прочитать цикл данных для создания карты.

<!-- 外层的div是用于制定卡片的范围包括外面的margin -->
<div
  class="cardBorderBox"
  v-for="item of listData"
  :key="item.id"
  :id="item.id"
>
<!-- 里面的div是用于显示卡片本身的内容 -->
  <div class="cardInsideBox" >
    <div class="topWrapBox">
        <!-- 这里是标题栏,用于添加点击事件 -->
    </div>
    <div class="emptyContent">
        <!-- 这里是内容部分 -->
    </div>
  </div>
</div>

<script>
export default {
  //name记得一定要定义
  name: "cardDragger",
  data(){
    return {
      listData: [
        {
          positionNum: 1,        // 位置号码,卡片的位置根据这个计算生成
          name: "演示卡片1",      // 卡片标题
          id: "card1",           // 用于辨识的卡片ID
        },
      ]
    }
  },
}
</script>

Карту также необходимо настроить по положению и стилю.Другие необходимые параметры:

data(){
  return {
    colNum:2,                      //一行有多少列
    cardOutsideWidth:590,          //单个卡片的外范围宽度
    cardOutsideHeight:380,         //单个卡片的外范围高度
    cardInsideWidth:default:560,   //单个卡片的内容宽度
    cardInsideHeight:default:320,  //单个卡片的内容高度
    
    mousedownTimer: null           //用于记录卡片当前是否在过渡状态中的定时器 
  }
}

Раскладка карты с использованиемабсолютное позиционирование, для легкой анимации перехода. Для ширины и высоты используются заданные ширина и высота периферии карты.

<template>
  <div
    class="cardBorderBox"
    v-for="item of listData"
    :key="item.id"
    :id="item.id"
    :style="{ 
      width:cardOutsideWidth+'px', 
      height:cardOutsideHeight+'px'
    }"
  >
    <!-- 省略部分代码 -->
  <div>
</template>
<script>
//整体就是按列数的限定,从左往右一行一行地排列数据
computeLeft(num) {
  //left为(位置号码-1)%列数*卡片外围宽度
  return (num-1) % this.colNum * this.cardOutsideWidth;
},
computeTop(num) {
  //top为(位置号码/列数)向上取整,减去1,再乘以卡片外围高度
  return (Math.ceil(num / this.colNum) - 1) * this.cardOutsideHeight;
}
</script>
<!-- 省略部分样式代码 -->

При первой загрузке карты и отслеживании изменения количества карт нужно пересчитывать верхнюю и левую части сгенерированной карты по ее собственному номеру позиции. Убедитесь, что асинхронная загрузка данных также может быть прочитана.

//判断卡片的selectState是否存在,不存在则添加false
  methods:{
    addCardStyle(){
      this.$nextTick(()=>{
        this.listData.forEach(item=>{
          document.querySelector('#'+item.id).style.top = this.computeTop(item.positionNum)+'px'
          document.querySelector('#'+item.id).style.left = this.computeLeft(item.positionNum)+'px'
        })
      })
    }
  },
  watch:{
    listData:{
      handler:function(){
        this.addCardStyle()     
      },
      immediate: true
    }
  }

Затем нам нужно обернуть еще один слой div в самую внешнюю часть всего содержимого, а затем добавитьposition:relative, установите ширину и высоту div в соответствии с количеством listData.

<!-- 
    首先,absolute是根据第一个父元素不为static 定位的元素进行定位
    其次,确定宽高是因为将卡片移动的的时候,宽高会根据内容自适应,这里不需要宽高自适应。
    宽度为:列数*卡片外围宽度
    高度为:最后一个卡片的top+卡片外围的高度
-->
<div
  :style="{
    position:'relative',
    height:computeTop(listData.length)+cardOutsideHeight+'px',
    width:cardOutsideWidth*colNum+'px'}"
>
<!-- computeTop()方法是上面计算卡片top的方法 -->

   <!-- 卡片代码 -->
</div>

Затем добавляем в заголовок обычной карточкисобытие щелчка, когда мышь щелкает по строке заголовка, сначала определить, пустой ли таймер анимации перехода, и если он пустой, вернуться напрямую. Если не пусто, выполните событие щелчка.

<div
  class="cardBorderBox"
  v-for="item of listData"
  :key="item.id"
  :id="item.id"
>
  <div class="cardInsideBox" >
    <div @mousedown="touchStart($event,item.id)" class="topWrapBox">
        <!-- 标题栏添加点击事件 -->
    </div>
    <div class="emptyContent">
        <!-- 这里是内容部分 -->
    </div>
  </div>
</div>

methods: {
  //event为鼠标的点击事件,selectId是当前数据的id
  touchStart(event, selectId) {
      
      //其他卡片正在动画中的时候不可以再次点击,否则动画和数据会出错。
      if (this.mousedownTimer) {
        return false;
      }
      
      const that = this;
      //选中的卡片的dom和数据
      let selectDom = document.getElementById(selectId);
      let selectMenuData = this.data.find(item => {
        return item.id === selectId;
      });
      //获取屏幕滚动条位置
      let originTop = document.body.scrollTop === 0 ?
                      document.documentElement.scrollTop : document.body.scrollTop;
      let scrolTop = originTop;
      //记录卡片的top和left
      let moveTop 
      let moveLeft 
      //记录起始选中位置
      let OriginObjPosition = {
        left: 0,
        top: 0,
        originNum: -1
      };
      //起始鼠标信息
      let OriginMousePosition = {
        x: 0,
        y: 0
      };
      //记录交换位置的号码
      let OldPositon = null;
      let NewPositon = null;
    
      
      //1.保存点击的起始鼠标位置
      OriginMousePosition.x = event.screenX;
      OriginMousePosition.y = event.screenY;
      
      //2.给选中卡片一个transition:none的class,去除默认过渡
      selectDom.classList.add('d_moveBox')
      
      //3.保存现在卡片的top和left
      moveLeft = OriginObjPosition.left = parseInt(
        //这里获取到的left是带单位的字符串,要转换成纯数字
        selectDom.style.left.slice(0, selectDom.style.left.length - 2)
      );
      moveTop = OriginObjPosition.top = parseInt(
        selectDom.style.top.slice(0, selectDom.style.top.length - 2)
      );
      
      //4.添加其他鼠标事件
      document.addEventListener("mousemove", mouseMoveListener);
      document.addEventListener("mouseup", mouseUpListener);
      document.addEventListener("scroll", mouseScroll);

      
      //省略部分代码 
  }
}

Также были добавлены события перемещения, отпускания и колеса мыши. Осталось только усовершенствовать содержание каждого события. прежде всегособытие перемещения мыши, нам нужно следить за текущим положением мыши и сравнивать его с исходным положением, а затем корректировать верхнюю и левую части текущей карты, чтобы завершить эффект щелчка по карте и перемещения карты.

methods: {
  //所有其他函数都添加在touchStart方法里,共同使用点击事件的数据
  touchStart(event, selectId) {
    //省略部分代码
    
    function mouseMoveListener(event) {
      //在原来的top和left基础上,加上鼠标的偏移量
      moveTop = OriginObjPosition.top + ( event.screenY - OriginMousePosition.y );
      moveLeft = OriginObjPosition.left + ( event.screenX - OriginMousePosition.x );
        
      document.querySelector(".d_moveBox").style.left = moveLeft + "px";
      document.querySelector(".d_moveBox").style.top = moveTop + (scrolTop - originTop) + "px";  //这里要加上滚动的高度
    }
  }
}

событие колеса мышиЭто почти то же самое, послушайте особенности свитка и измените положение карты.

function mouseScroll(event) {
    scrolTop = document.body.scrollTop === 0
               ? document.documentElement.scrollTop
               : document.body.scrollTop;

    document.querySelector(".d_moveBox").style.top = moveTop + scrolTop - originTop + "px";
  }

Q2: Как обнаружить и обменять карты?

вся идея:

  • При перемещении карты вызовите функцию, которая вычисляет, к какому номеру позиции принадлежит текущая позиция карты, если он совпадает с существующим номером и не является собственным номером, позиция будет заменена.
  • При обмене сравните, изменился ли номер позиции с малого на большой или с большого на маленький, а число в середине двух случаев сдвинуто вперед на один бит или на один бит назад.

Реализация:

надсобытие перемещения мыши, мы вызываем функцию обнаружения, чтобы определить, есть ли карта ниже текущей движущейся позиции, но функцию обнаружения необходимо проверить.дросселирование, в противном случае частота обнаружения слишком высока, чтобы повлиять на производительность. Когда карта перемещается в определенном направлении другой карты более чем на 50% расстояния, позиция меняется. (Обнаружение здесь рассчитывается на основе периферийной ширины и высоты карты)

methods: {
  touchStart(event, selectId) {
    //用于保存检测位置的定时器
    let DectetTimer = null;
    //省略部分代码...

    function mouseMoveListener(event) {
      //省略部分代码...
      
      //在鼠标移动的监听中添加如下代码
      if (!DectetTimer) {
        DectetTimer = setTimeout(()=>{
          //节流调用检测函数,传入当前位置信息
          cardDetect(moveTop + (scrolTop - originTop),moveLeft) 
          //调用结束清空定时器
          DectetTimer = null;
        }, 200);
      }     
    }
    
    function cardDetect(moveItemTop, moveItemLeft){
      //计算当前移动卡片位于卡片的哪一行哪一列
      let newWidthNum = Math.round((moveItemLeft/ that.cardOutsideWidth))+1
      let newHeightNum = Math.round((moveItemTop/ that.cardOutsideHeight))
      
      //如果移动卡片至范围外则不会有任何操作,直接返回
      if(newHeightNum>(Math.ceil(that.listData.length / that.colNum) - 1)||
        newHeightNum<0||
        newWidthNum<=0||
        newWidthNum>that.colNum){
        return false
      }
      
      //将计算的行列转换为位置号码
      const newPositionNum = (newWidthNum) + newHeightNum * that.colNum
      if(newPositionNum!==selectMenuData.positionNum){
        //寻找当前位置号码有没有卡片数据
        let newItem = that.listData.find(item=>{
          return item.positionNum === newPositionNum
        })
        //有卡片数据的话就进行交换
        if( newItem ){
          swicthPosition(newItem, selectMenuData);
        }
      }      
    }
  }
}

Когда номер обнаруженной позиции совпадает с номером других существующих обычных карт, определяется, что позицию необходимо заменить. Ситуация обмена делится на две ситуации: номер позиции перемещается от малого к большему, и ситуация перемещается от большого к меньшему.

//省略部分代码
 function swicthPosition(newItem, originItem) {
    OldPositon = originItem.positionNum;
    NewPositon = newItem.positionNum;

    that.$emit('swicthPosition',OldPositon,NewPositon,originItem)

    //位置号码从小移动到大
    if (NewPositon > OldPositon) {
      let changeArray = [];
      //从小移动到大,那小的号码就会空出来,其余卡片应往前移动一位 
      //找出两个号码中间对应的卡片数据
      for (let i = OldPositon + 1; i <= NewPositon; i++) {
        let pushData = that.data.find(item => {
          return item.positionNum === i;
        });
        changeArray.push(pushData);
      }
      
      for (let item of changeArray) {
        //vue的$set实时更改数据
        that.$set(item, "positionNum", item.positionNum - 1);
        //原生js调整卡片动画
        document.querySelector('#'+item.id).style.top = that.computeTop(item.positionNum)+'px'
        document.querySelector('#'+item.id).style.left = that.computeLeft(item.positionNum)+'px'
      }
      //正在拖动的卡片就不需要动画了
      that.$set(originItem, "positionNum", NewPositon);
    }

    //位置号码从大移动到小
    if (NewPositon < OldPositon) {
      let changeArray = [];
      //从大移动到小,那大的号码就会空出来,其余卡片应往后移动一位 
      //找出两个号码中间对应的卡片数据
      for (let i = OldPositon - 1; i >= NewPositon; i--) {
        let pushData = that.data.find(item => {
          return item.positionNum === i;
        });
        changeArray.push(pushData);
      }

      for (let item of changeArray) {
        that.$set(item, "positionNum", item.positionNum + 1);
        document.querySelector('#'+item.id).style.top = that.computeTop(item.positionNum)+'px'
        document.querySelector('#'+item.id).style.left = that.computeLeft(item.positionNum)+'px'
      }
      that.$set(originItem, "positionNum", NewPositon);

    }
  }  

Q3: После отпускания мыши она возвращается в исходное положение?

вся идея:

  • Когда мышь отпускают, сначала сбрасывается таймер определения положения, а затем выполняется последнее определение положения.
  • Восстановите карту на позицию, соответствующую номеру позиции, и заодно добавьте таймер с такой же длительностью, что и переход карты, очистите таймер в таймере и удалите другие классы карты. Таймер пуст, прежде чем можно будет сделать следующий щелчок.

Реализация:

function mouseUpListener() {
    /*首先清除位置检测的定时器,
      因为位置检测的定时器,会在鼠标松开事件结束后执行,
      会导致拖拽卡片都已经回到原位置并隐藏了,还会发生位置交换导致报错。
      应该调整为,先清楚定时器,直接检测,再添加卡片返回原处的动画*/
    clearTimeout(DectetTimer)
    DectetTimer = null
    
    //对鼠标松开位置直接进行最后一次位置检测
    cardDetect(moveTop + (scrolTop - originTop),moveLeft)

    //设置卡片当前位置号码计算生成的宽高,并添加transition进行过渡
    document.querySelector(".d_moveBox").classList.add('d_transition');
    document.querySelector(".d_moveBox").style.top = that.computeTop(selectMenuData.positionNum) + "px";
    document.querySelector(".d_moveBox").style.left = that.computeLeft(selectMenuData.positionNum) + "px";
    that.$emit('finishDrag',OldPositon,NewPositon,selectMenuData)

    that.mousedownTimer = setTimeout(() => {
      /*mousedownTimer是一个全局定时器,默认为空。详情可看仓库源码。
        若鼠标松开,卡片过渡动画开始时后则激活定时器,
        时间到了的话就清空定时器内容。
        保证在过渡动画执行期间,不能点击其他卡片。
        mousedownTimer在点击事件开始时进行判断,若不为空则直接返回跳出点击事件
      */
      document.querySelector(".d_moveBox").classList.remove('d_transition')
      document.querySelector(".d_moveBox").classList.remove('d_moveBox')
      clearTimeout(that.mousedownTimer);
      that.mousedownTimer = null;
    }, 300);
    
    //移除所有监听
    document.removeEventListener("mousemove", mouseMoveListener);
    document.removeEventListener("mouseup", mouseUpListener);
    document.removeEventListener("scroll", mouseScroll);
}

Q4: Как сделать слоты компонентов и настроить свойства и события?

вся идея:

  • Атрибуты: поместите данные данных в реквизит, чтобы определить и установить значения по умолчанию.
  • Событие: Пока $emit вызывается в какой-либо функции компонента, слушайте, когда он используется.
  • Слоты: обновлено в 2.6.0 с vueименованный слотДля производства

Реализация:

Первоначальная потребность в данных позволяет пользователям настраивать использованиеАтрибуты, меняются наprops, и присвойте значение по умолчанию

//组件中:
  props:{
    data:{
      type:Array,
      //设定默认值,返回空数组
      default: function () {
        return []
      }
    },
    colNum:{
      type:Number,
      default:2
    },
    cardOutsideWidth:{
      type:Number,
      default:590      
    },
    cardOutsideHeight:{
      type:Number,
      default:380      
    },
    cardInsideWidth:{
      type:Number,
      default:560      
    },
    cardInsideHeight:{
      type:Number,
      default:320      
    }
  },
 
//使用时: 
<cardDragger 
  :data="componentData"
  :colNum="3"
  :cardOutsideWidth="360"
  :cardInsideWidth="320"
  :cardOutsideHeight="250"
  :cardInsideHeight="210"
>  

Инкапсуляция событий также очень проста, просто позвоните туда, где это необходимо.пользовательское событие, например, я вызывал событие освобождения мыши:

//组件中$emit事件名+要传递的数据
function mouseUpListener() {
  that.$emit('finishDrag',OldPositon,NewPositon,that.selectMenuData)
}
//使用时
<cardDragger 
  :data="componentData"
  @finishDrag="finishDrag"
>
export default {
  methods: {
    finishDrag(OldPositon,NewPositon,originItem){
      console.log(OldPositon,NewPositon,originItem)
    }
  }
}

Для создания слота сначала определите, какой контент вам нужно создать в слоте. Я говорю здесь о том, чтобы добавить содержимое строки заголовка и содержимое карты в слот, используя vueименованный слот. Поместите исходный контент, который нужно заменить на слот, в слот и используйте его как контент по умолчанию.

<div
  class="d_cardBorderBox"
  v-for="item of listData"
  :key="item.id"
  :id="item.id"
>
  <div 
    class="d_cardInsideBox" 
    v-if="item.selectState===false"
  >
   <!--保留标题栏添加事件内容的div里添加slot,保留点击事件-->
    <div @mousedown="touchStart($event,item.id)" class="d_topWrapBox">
      <!--原来这里应该是标题栏的内容,将slot添加至slot的默认值即可-->
      <slot name="header" v-bind:item="item">
        <div class="d_topMenuBox" >
          <div class="d_menuTitle" >{{item.name}}</div>
        </div>
      </slot>
    </div>

    <slot name="content" v-bind:item="item" >
      <div class="d_emptyContent">
        卡片暂无内容
      </div>
    </slot>
  </div>
</div>

также используетсяслот с прицеломПусть содержимое слота может получить доступ к данным в подкомпонент. И я сделал несколько суждений. Если компонентдата в данных данных существует, используйте VuecomponentПоказать приоритет. Я не буду здесь вдаваться в подробности.

Q5: С какими проблемами вы столкнулись в производстве?

1. Почему бы не использовать перетаскивание?

Перетаскивание h5 не используется, потому что стиль мыши станет запрещенным символом и станет прозрачным при перетаскивании. Не соответствует моим потребностям в стилях перетаскивания.

2. Когда карты перетаскивания добавляют переход?

При отображении карт и движущихся карт, переходы не могут быть добавлены, в противном случае будет задержка в перетаскивании. Только после того, как мышь будет выпущена, переход добавляется, чтобы сделать карту вернуться в исходное положение. И поскольку карта перетаскивания отображается с V-iF, переход был разрушен при отображении карты перетаскивания в следующий раз.

3. Что делать, если я быстро нажимаю другую карточку и сообщаю об ошибке до окончания анимации?

Добавлен глобальный таймер, если отпустить кнопку мыши, таймер сработает при запуске анимации перехода карты, а после окончания содержимое таймера будет очищено. Событие нажатия на карту сначала оценивает, пусто ли содержимое таймера, а затем выполняет его.

4. Какие оптимизации были сделаны со времени выхода первой статьи?

Переписано определение положения, переписано перетаскивание, удалено много бесполезного кода. Исправлена ​​ошибка, из-за которой асинхронные данные не загружались. Сейчас я чувствую себя намного лучше, чем в начале! Пожалуйста, используйте его с уверенностью!


😃 Выше описан весь процесс изготовления этого компонента.Должно быть много мест, которые можно оптимизировать.Добро пожаловать, чтобы меня поправить. Если вам это интересно, не забудьте поставить лайк~