Перетащите компоненты карты

Vue.js
Перетащите компоненты карты

预览图

предисловие

Я видел статью о Nuggets, в которой используется компонент перетаскивания карт, и после прочтения общей идеи я понял, что она очень ясна, и мне захотелось ее реализовать;

В процессе нашел много деталей.После доработки сравнил исходный авторский код,и нашел много мест,которые можно оптимизировать.Запишите сюда;

Ниже приведена демонстрация и адрес исходного кода реализации персонального обучения:

использовать

Получите файл dragCard.vue на складе и импортируйте его в проект, смотрите следующий пример

// app.js
<template>
  <div id="app">
    <DragCard
      :list="list"
      :col="4"
      :itemWidth="150"
      :itemHeight="150"
      @change="handleChange"
      @mouseUp="handleMouseUp">
    </DragCard>
  </div>
</template>

<script>
  import DragCard from './components/DragCard.vue'

  export default {
    name: 'app',
    components: {
      DragCard
    },
    data() {
      return {
        list: [
          {head: '标题0', content: "演示卡片0"},
          {head: '标题1', content: "演示卡片1"},
          {head: '标题2', content: "演示卡片2"}
        ],
      }
    },
    methods: {
      handleChange(data) {
        console.log(data);
      },
      handleMouseUp(data) {
        console.log(data);
      }
    }
  }
</script>

Давайте посмотрим на props и методы

Благодаря свойствам и методам компонента вы можете быстро понять, как использовать весь компонент;

Атрибуты

Атрибуты иллюстрировать Типы По умолчанию
list данные карты Array []
col сколько карт отображать в каждом ряду Number 3
itemWidth Ширина каждой карты (включая маржу) Number 150
itemHeight Высота каждой карты (включая поля) Number 150

метод

метод иллюстрировать возвращаемое значение
@change Срабатывает при изменении положения карты Возвращает массив номеров позиций для каждого элемента в массиве
@mouseUp Срабатывает, когда карту перетаскивают и отпускают То же

::: наконечник Возвращаемое значение — это набор номеров позиций каждого элемента в массиве; массив возвращаемых значенийindexа такжеlistсерединаindexНепротиворечивый; позже мы можем объединить эти два массива в[{ id: 'cardid1', seatid: '1' }...]Эта форма передается на сервер для изменения данных о местоположении карты, конечно, рекомендуется отправить запрос, когда лучше mouseUp; :::

слот слот

slotName иллюстрировать data
head Заголовок карты listItem
content Раздел содержимого карты listItem

::: наконечник Оба слота с областью действия имеют значения по умолчанию, если они не заполнены, заголовок будет отображатьсяlistсерединаheadСвойство, а содержимое будет отображатьсяcontentимущество; дваslotс текущей картойlistданные о товаре, содержимое карточки можно настроить более гибко; :::

Реализация

Главная идея

  • Карточка страницы принимаетabsoluteМакет, через настройкуleftа такжеtop, держите карты в порядке, чтобы входящиеlistдолжен быть в положительном порядке;
  • Инициализируйте стиль черезpropsС входящим значением мы можем рассчитать количество строк и столбцов, позицию карты и другую информацию;
  • Добавьте атрибут идентификации позиции к каждому элементу в массиве, и последующие обмены позициями могут быть расширены с помощью этой идентификационной метки, которая также является возвращаемым значением, переданным позже методом триггера родителю;
  • При нажатии мыши текущее положение мыши записывается как начальное, текущая карта включается параметрами и привязывается.mousemoveа такжеmouseupСобытие; в это время расстояние перемещения мыши соответствует расстоянию перемещения карты;
  • Когда карта перемещается, мы вычисляем, перемещена ли она в данный момент на другие позиции карты.Если это так, все карты в интервале перемещаются вперед или назад, вызывая родительский компонентchangeметод;
  • Когда мышь отпущена, карточка возвращается в целевое положение, запуская родительский компонент.mouseUpметод;

Сначала посмотрите на структуру страницы

  <div class="dragCard">
    <div
      class="dragCard_warpper"
      ref="dragCard_warpper"
      :style="dragCardWarpperStyle">
      <div
        v-for="(item, index) in list"
        :key="index"
        class="dragCard_item"
        :style="initItemStyle(index)"
        :ref="item.dragCard_id">
        <div class="dragCard_content">
          <div
            class="dragCard_head"
            @mousedown="touchStart($event, item)">
            <slot name="head" :item="item" >
              <div class="dragCard_head-defaut">
                {{ item.head ? item.head : `卡片标题${index + 1}` }}
              </div>
            </slot>
          </div>
          <div class="dragCard_body">
            <slot name="content" :item="item">
              <div class="dragCard_body-defaut">
                {{ item.content ? item.content : `暂无数据` }}
              </div>
            </slot>
          </div>
        </div>
      </div>
    </div>
  </div>
  • Щелкните заголовок, чтобы перетащить карточку, чтобы@mousedownустановить вdragCard_head, для этого положимslotделится на две части, однаheadРаздел заголовка, показанный по умолчаниюitem.content;одинcontentРаздел контента, дисплей по умолчаниюitem.head, ; пользователь может пройтиslotнестандартная карта;очки знаний слота
// app.js  使用自定义卡片样式
<template>
  <div id="app">
    <DragCard
      :list="list"
      :col="4"
      :itemWidth="150"
      :itemHeight="150"
      @change="handleChange"
      @mouseUp="handleMouseUp">
      <template v-slot:head="{ item }">
        <div class="dragHead">{{item.head}}</div>
      </template>
      <template v-slot:content="{ item }">
        <div class="dragContent">{{item.content}}</div>
      </template>
    </DragCard>
  </div>
</template>
  • dragCardWarpperStyle — это стиль контейнера, а ширина и высота контейнера вычисляются через значения, переданные пропсами, должны вычисляться при инициализации компонента и подключении с помощью init();
// ... 
 created() {
   this.init();
 },
 methods: {
   init () {
     // 根据数组的长度length和每行个数col,可以计算出需要多少行row,超出不满一行算一行,用ceil向上取整;
     this.row = Math.ceil(this.list.length / this.col);
     // 计算出容器的宽高
     this.dragCardWarpperStyle = `width: ${this.col * this.itemWidth}px; height:${this.row * this.itemHeight}px`;
     /*
     * 这里处理下数组,引入两个重要的属性:
     * dragCard_id:
     *   给每一个卡片创建一个唯一id,作为ref值,后续通过this.$refs[dragCard_id]获取卡片的dom
     * dragCard_index:
     *   这是每个卡片的位置序号,用于记录卡片当前位置
     * */
     this.list.forEach((item, index) => {
       this.$set(item, 'dragCard_index', index);
       this.$set(item, 'dragCard_id', 'dragCard_id' + index);
     });
   },
   // 通过index计算出每个卡片的left和right
   initItemStyle(INDEX) {
     return {
        width: this.itemWidth + 'px',
        height: this.itemHeight + 'px',
        left: (INDEX < this.col ? INDEX : (INDEX % this.col)) * this.itemWidth + 'px',
        top: Math.floor(INDEX / this.col) * this.itemHeight + 'px'
     };
   }
 }
  • Конечно, данные нашей карты передаются от родителя, поэтомуlistВ сцене обязательно будут изменения, в это время нам нужно пересчитать количество строк и столбцов, пересчитать ширину и высоту контейнера, по сути, перевыполнитьinitФункция; поэтому нам нужно слушатьlist;
  watch: {
    list: {
      handler: function(newVal, oldVal) {
        this.init();
      },
      immediate: true // 定义的时候就执行一次,所以created的时候就不需要执行init了
    }
  },

handleMousedown()

существуетhandleMousedown()прямо определитьhandleMousemove()а такжеhandleMouseUp()событие, иhandleMouseUp()удаленный;

Во-первых, это еще несколько важных переменных и методов.

  • itemList:listскопировать и добавить свойства, которые необходимо использовать позжеdom(информация узла текущей карты, полученная через реф),isMoveing(чтобы отметить, движется ли текущая карта),left, top,

  • curItem: Текущая карта используется много, поэтому вынимается здесь отдельно, а при перемещении следует убрать эффект перехода одиночной лицевой карты, иначе движение застрянет, иz-indexдолжен быть на более высоком уровне

  • targetItem: объект карты, позиция которого будет заменена, начиная сnull

  • mousePosition: начальная позиция мыши, позиция движущейся мыши минус начальная позиция — это смещение движения карты;

  • handleMousemove(): движение мыши

  • cardDetect(): Обнаружение движения карты, следует ли выполнять обмен позициями

  • swicthPosition(): поменять местами карты

  • handleMouseUp(): Мышь вверх

handleMousedown(e, optionItem) {
  e.preventDefault();
  let that = this;
  if (this.timer) return false; // timer为全局的定时器,表示当前有卡片正在移动,直接返回;
  
  // 拷贝一份list,并加上后续要使用的属性;
  let itemList = that.list.map(item => {
    // 如果ref是动态赋的值,存入$refs中会是一个数组;
    let dom = this.$refs[item.dragCard_id][0];
    let left = parseInt(dom.style.left.slice(0, dom.style.left.length - 2));
    let top = parseInt(dom.style.top.slice(0, dom.style.top.length - 2));
    let isMoveing = false; // 标记正在移动的卡片,正在移动的卡片不参与碰撞检测
    return {...item, dom, left, top, isMoveing};
  });

  // 当前卡片对象用的比较多,用一个别名curItem把他存起来;
  let curItem = itemList.find(item => item.dragCard_id === optionItem.dragCard_id);
  curItem.dom.style.transition = 'none';
  curItem.dom.style.zIndex = '100';
  curItem.dom.childNodes[0].style.boxShadow = '0 0 5px rgba(0, 0, 0, 0.1)';
  curItem.startLeft = curItem.left; // 起始的left
  curItem.startTop = curItem.top; // 起始的top
  curItem.OffsetLeft = 0; // left的偏移量
  curItem.OffsetTop = 0; // top的偏移量

  // 即将交换位置的对象
  let targetItem = null;

  // 记录鼠标起始位置
  let mousePosition = {
    startX: e.screenX,
    startY: e.screenY
  };

  document.addEventListener("mousemove", handleMousemove);
  document.addEventListener("mouseup", handleMouseUp);


  // 鼠标移动
  function handleMousemove(e) {}
  // 卡片交换检测
  function cardDetect() {}
  // 卡片交换 
  function swicthPosition() {}
  // 鼠标抬起
  function handleMouseUp() {}
}

handleMousemove(e)

Текущие координаты мыши минус запускающие координаты являются смещение текущей карты;

Обнаружение обмена карт может выполняться во время процесса перемещения.Для повышения производительности выполняется следующее регулирование: один раз каждые 200 мс;

  // 鼠标移动
  function handleMousemove(e) {
    curItem.OffsetLeft = parseInt(e.screenX - mousePosition.startX);
    curItem.OffsetTop = parseInt(e.screenY - mousePosition.startY);
    // 改变当前卡片对应的style
    curItem.dom.style.left = curItem.startLeft + curItem.OffsetLeft + 'px';
    curItem.dom.style.top = curItem.startTop + curItem.OffsetTop + 'px';
    // 卡片交换检测,做一下节流
    if (!DectetTimer) {
      DectetTimer = setTimeout(() => {
        cardDetect();
        clearTimeout(DectetTimer);
        DectetTimer = null;
      }, 200)
    }
  }

cardDetect()

Сначала я думал сделать это с обнаружением столкновений, перебирая весь itemList, а затем сравнивая расстояние между текущей картой и каждым элементом, когда оно меньше установленного значенияgap, выполнятьswicthPosition();

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

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

  // 卡片移动检测
  function cardDetect() {
    // 根据移动的距离计算出移动到哪一个位置
    let colNum = Math.round((curItem.OffsetLeft / that.itemWidth));
    let rowNum = Math.round((curItem.OffsetTop / that.itemHeight));
    // 这里的dragCard_index需要用到最初点击卡片的位置,因为curItem在后续的卡片交换中dragCard_index已经改变;
    let targetItemDragCardIndex = optionItem.dragCard_index + colNum + (rowNum * that.col);

    // 超出行列,目标位置不变或不存在都直接return;
    if(Math.abs(colNum) >= that.col
      || Math.abs(rowNum) >= that.row
      || Math.abs(colNum) >= that.col
      || Math.abs(rowNum) >= that.row
      || targetItemDragCardIndex === curItem.dragCard_index
      || targetItemDragCardIndex < 0
      || targetItemDragCardIndex > that.list.length - 1) return false;

    let item = itemList.find(item => item.dragCard_index === targetItemDragCardIndex);
    item.isMoveing = true;
    // 将目标卡片拷贝一份,主要是为了松开鼠标的时候赋值给当前卡片;
    targetItem = {...item};
    swicthPosition();
  }

swicthPosition()

Обмен карт делится на две ситуации;

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

::: подсказка Примечание

  1. Когда мы перемещаемся, мы берем предыдущее или следующее значение, поэтому при обходе массива мы должны обращать внимание на обход от целевого значения;
  2. itemList является резервной копией списка.После того, как мы изменим dragCard_index карты, нам нужно синхронизировать его со списком;
  3. Анимация обмена карты 300 мс, в этот период карта не должна участвовать в обнаружении обмена, поэтому установитеisMoveing = true, и установите таймер на очистку через 300 мсisMoveing
  4. В процессе обмена карты необходимо только поменять текущую картуitemListсвойства в , менять не нужноlistВ, пока, наконец, выпустите мышь, не было синхронизированоlistсередина :::
  function swicthPosition() {
    const dragCardIndexList = itemList.map(item => item.dragCard_index);
    // 目标卡片位置大于当前卡片位置;
    if (targetItem.dragCard_index > curItem.dragCard_index) {
      for (let i = targetItem.dragCard_index; i >= curItem.dragCard_index + 1; i--) {
        let item = itemList[dragCardIndexList.indexOf(i)];
        let preItem = itemList[dragCardIndexList.indexOf(i - 1)];
        item.isMoveing = true;
        item.left = preItem.left;
        item.top = preItem.top;
        item.dom.style.left = item.left + 'px';
        item.dom.style.top = item.top + 'px';
        item.dragCard_index = that.list[dragCardIndexList.indexOf(i)].dragCard_index -= 1;
        setTimeout(() => {
          item.isMoveing = false;
        }, 300)
      }
    }
    // 目标卡片位置小于当前卡片位置;
    if (targetItem.dragCard_index < curItem.dragCard_index) {
      for (let i = targetItem.dragCard_index; i <= curItem.dragCard_index - 1; i++) {
        let item = itemList[dragCardIndexList.indexOf(i)];
        let nextItem = itemList[dragCardIndexList.indexOf(i + 1)];
        item.isMoveing = true;
        item.left = nextItem.left;
        item.top = nextItem.top;
        item.dom.style.left = item.left + 'px';
        item.dom.style.top = item.top + 'px';
        item.dragCard_index = that.list[dragCardIndexList.indexOf(i)].dragCard_index += 1;
        setTimeout(() => {
          item.isMoveing = false;
        }, 300)
      }
    }
    curItem.left = targetItem.left;
    curItem.top = targetItem.top;
    curItem.dragCard_index =  targetItem.dragCard_index;
    // 派发change事件通知父组件
    that.$emit('change', itemList.map(item => item.dragCard_index));
  }

handleMouseUp()

  • При поднятии мышки следует судить, есть ли целевая карта, если есть, вернуться к целевой карте, если нет, вернуться в исходное положение;
  • Текущая карта удаляет эффект перехода при щелчке мыши, и эффект перехода должен быть добавлен обратно при поднятии мыши; потому чтоtransitionсуществуетcssустанавливается вstyleпросто ясно
  function handleMouseUp() {
    //移除所有监听
    document.removeEventListener("mousemove", handleMousemove);
    document.removeEventListener("mouseup", handleMouseUp);

    // 清除检测的定时器并做最后一次碰撞检测
    clearTimeout(DectetTimer);
    DectetTimer = null;
    cardDetect();
    // 把过渡效果加回去
    curItem.dom.style.transition = '';
    // 同步dragCard_index到list中;
    that.list.find(item => item.dragCard_id === optionItem.dragCard_id).dragCard_index = curItem.dragCard_index;
    curItem.dom.style.left = curItem.left + 'px';
    curItem.dom.style.top = curItem.top + 'px';    
    // 派发mouseUp事件通知父组件
    that.$emit('mouseUp', that.list.map(item => item.dragCard_index));
    that.timer = setTimeout(() => {
      curItem.dom.style.zIndex = '';
      curItem.dom.childNodes[0].style.boxShadow = 'none';
      clearTimeout(that.timer);
      that.timer = null;
    }, 300);
  }

написать на обороте

Этот компонент здесь, чтобы завершить его!

Последняя публикация из裂泉Ссылка на оригинальную статью:Пойдем со мной, реализуй и инкапсулируй компоненты перетаскивания с 0; это пока цикл статей,todoВ будущем я также поделюсь, как загружать компоненты вnpm;

dranein@163.com

адрес:GitHub.com/D тогда вам а/в UE...