предисловие
Я видел статью о 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()
Обмен карт делится на две ситуации;
- Когда движущаяся целевая позиция больше, чем текущая исходная позиция карты, когда карты и разнесенная задняя карта должны быть сдвинуты на определенное место;
- Когда целевая позиция меньше, чем исходная позиция текущей движущейся карты, и отделенная карта, и целевая карта должны переместиться вперед на одну позицию;
::: подсказка Примечание
- Когда мы перемещаемся, мы берем предыдущее или следующее значение, поэтому при обходе массива мы должны обращать внимание на обход от целевого значения;
- itemList является резервной копией списка.После того, как мы изменим dragCard_index карты, нам нужно синхронизировать его со списком;
- Анимация обмена карты 300 мс, в этот период карта не должна участвовать в обнаружении обмена, поэтому установите
isMoveing = true, и установите таймер на очистку через 300 мсisMoveing - В процессе обмена карты необходимо только поменять текущую карту
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