Сначала продемонстрируйте окончательный эффект:
Эффекты плавного перетаскивания и смены положения, а также обновление данных в реальном времени.
Поддерживает настройку стиля и содержимого компонентов.
Это цикл статейСтатья 2, я инкапсулировалПеретащите компоненты карты, реализованные с помощью vueи публикуется в npm, который подробно записывает весь производственный процесс. Всего есть три статьи, в которых представлены идеи производства и проблемы, с которыми столкнулись компоненты, а также какие проблемы возникли и как их решить в процессе публикации в 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. Какие оптимизации были сделаны со времени выхода первой статьи?
Переписано определение положения, переписано перетаскивание, удалено много бесполезного кода. Исправлена ошибка, из-за которой асинхронные данные не загружались. Сейчас я чувствую себя намного лучше, чем в начале! Пожалуйста, используйте его с уверенностью!
😃 Выше описан весь процесс изготовления этого компонента.Должно быть много мест, которые можно оптимизировать.Добро пожаловать, чтобы меня поправить. Если вам это интересно, не забудьте поставить лайк~