Vue 可拖拽組件Vue Smooth DnD的使用詳解
簡介和 Demo 展示
最近需要有個拖拽列表的需求,發(fā)現(xiàn)一個簡單好用的 Vue 可拖拽組件。安利一下~
Vue Smooth DnD 是一個快速、輕量級的拖放、可排序的 Vue.js 庫,封裝了 smooth-dnd 庫。
Vue Smooth DnD 主要包含了兩個組件,Container 和 Draggable,Container 包含可拖動的元素或組件,它的每一個子元素都應(yīng)該被 Draggable 包裹。每一個要被設(shè)置為可拖動的元素都需要被 Draggable 包裹。
安裝: npm i vue-smooth-dnd
一個簡單的 Demo ,展示組件的基礎(chǔ)用法,實現(xiàn)了可以拖拽的列表。
<template>
<div>
<div class="simple-page">
<Container @drop="onDrop">
<Draggable v-for="item in items" :key="item.id">
<div class="draggable-item">
{{item.data}}
</div>
</Draggable>
</Container>
</div>
</div>
</template>
<script>
import { Container, Draggable } from "vue-smooth-dnd";
const applyDrag = (arr, dragResult) => {
const { removedIndex, addedIndex, payload } = dragResult
console.log(removedIndex, addedIndex, payload)
if (removedIndex === null && addedIndex === null) return arr
const result = [...arr]
let itemToAdd = payload
if (removedIndex !== null) {
itemToAdd = result.splice(removedIndex, 1)[0]
}
if (addedIndex !== null) {
result.splice(addedIndex, 0, itemToAdd)
}
return result
}
const generateItems = (count, creator) => {
const result = []
for (let i = 0; i < count; i++) {
result.push(creator(i))
}
return result
}
export default {
name: "Simple",
components: { Container, Draggable },
data() {
return {
items: generateItems(50, i => ({ id: i, data: "Draggable " + i }))
};
},
methods: {
onDrop(dropResult) {
this.items = applyDrag(this.items, dropResult);
}
}
};
</script>
<style>
.draggable-item {
height: 50px;
line-height: 50px;
text-align: center;
display: block;
background-color: #fff;
outline: 0;
border: 1px solid rgba(0, 0, 0, .125);
margin-bottom: 2px;
margin-top: 2px;
cursor: default;
user-select: none;
}
</style>
效果

API: Container
屬性
| 屬性 | 類型 | 默認(rèn)值 | 描述 |
|---|---|---|---|
| :orientation | string | vertical | 容器的方向,可以為 horizontal 或 vertical |
| :behaviour | string | move | 描述被拖動的元素被移動或復(fù)制到目標(biāo)容器。 可以為 move 或 copy 或 drop-zone 或 contain 。move 可以在容器間互相移動,copy 是可以將元素復(fù)制到其他容器,但本容器內(nèi)元素不可變,drop-zone 可以在容器間移動,但是容器內(nèi)元素的順序是固定的。contain 只能在容器內(nèi)移動。 |
| :tag | string, NodeDescription | div | 容器的元素標(biāo)簽,默認(rèn)是 div ,可以是字符串如 tag="table" 也可以是包含 value和 props 屬性的對象 :tag="{value: 'table', props: {class: 'my-table'}}" |
| :group-name | string | undefined | 可拖動元素可以在具有相同組名的容器之間移動。如果未設(shè)置組名容器將不接受來自外部的元素。 這種行為可以被 shouldAcceptDrop 函數(shù)覆蓋。 見下文。 |
| :lock-axis | string | undefined | 鎖定拖動的移動軸??捎弥?nbsp;x, y 或 undefined。 |
| :drag-handle-selector | string | undefined | 用于指定可以開啟拖拽的 CSS 選擇器,如果不指定的話則元素內(nèi)部任意位置都可抓取。 |
| :non-drag-area-selector | string | undefined | 禁止拖動的 CSS 選擇器,優(yōu)先于 dragHandleSelector. |
| :drag-begin-delay | number | 0(觸控設(shè)備為 200) | 單位毫秒。表示點擊元素持續(xù)多久后可以開始拖動。在此之前移動光標(biāo)超過 5px 將取消拖動。 |
| :animation-duration | number | 250 | 單位毫秒。表示放置元素和重新排序的動畫持續(xù)時間。 |
| :auto-scroll-enabled | boolean | true | 如果拖動項目接近邊界,第一個可滾動父項將自動滾動。(這個屬性沒看懂= =) |
| :drag-class | string | undefined | 元素被拖動中的添加的類(不會影響拖拽結(jié)束后元素的顯示)。 |
| :drop-class | string | undefined | 從拖拽元素被放置到被添加到頁面過程中添加的類。 |
| :remove-on-drop-out | boolean | undefined | 如果設(shè)置為 true,在被拖拽元素沒有被放置到任何相關(guān)容器時,使用元素索引作為 removedIndex 調(diào)用 onDrop() |
| :drop-placeholder | boolean,object | undefined | 占位符的選項。包含 className, animationDuration, showOnTop |
關(guān)于 drag-class,drop-class 和 drop-placeholder.className 的效果演示
<Container # 省略其它屬性...
:animation-duration="1000" # 放置元素后動畫延時
drag-class="card-ghost"
drop-class="card-ghost-drop"
:drop-placeholder="{
className: 'drop-preview', # 占位符的樣式
animationDuration: '1000', # 占位符的動畫延遲
showOnTop: true # 是否在其它元素的上面顯示 設(shè)置為false會被其他的拖拽元素覆蓋
}"
>
<!-- 一些可拖拽元素 -->
<Draggable>....</Draggable>
</Container>
類對應(yīng)樣式
.card-ghost {
transition: transform 0.18s ease;
transform: rotateZ(35deg);
background: red !important;
}
.card-ghost-drop {
transition: transform 1s cubic-bezier(0,1.43,.62,1.56);
transform: rotateZ(0deg);
background: green !important;
}
.drop-preview {
border: 1px dashed #abc;
margin: 5px;
background: yellow !important;
}
實際效果(我這優(yōu)秀的配色?。?/p>

生命周期
一次拖動的生命周期通過一系列回調(diào)和事件進行描述和控制,下面以包含 3 個容器的示例為例進行說明
(直接復(fù)制了文檔沒有翻譯,API 詳細(xì)解釋可以看后面介紹。):
Mouse Calls Callback / Event Parameters Notes
down o Initial click
move o Initial drag
|
| get-child-payload() index Function should return payload
|
| 3 x should-accept-drop() srcOptions, payload Fired for all containers
|
| 3 x drag-start dragResult Fired for all containers
|
| drag-enter
v
move o Drag over containers
|
| n x drag-leave Fired as draggable leaves container
| n x drag-enter Fired as draggable enters container
v
up o Finish drag
should-animate-drop() srcOptions, payload Fires once for dropped container
3 x drag-end dragResult Fired for all containers
n x drop dropResult Fired only for droppable containers
請注意,應(yīng)在每次 drag-start 之前和每次 drag-end 之前觸發(fā) should-accept-drop,但為了清晰起見,此處已省略。
其中 dragResult 參數(shù)的格式:
dragResult: {
payload, # 負(fù)載 可以理解為用來記錄被拖動的對象
isSource, # 是否是被拖動的容器本身
willAcceptDrop, # 是否可以被放置
}
其中 dropResult 參數(shù)的格式:
dropResult: {
addedIndex, # 被放置的新添加元素的下標(biāo),沒有則為 null
removedIndex, # 將被移除的元素下標(biāo),沒有則為 null
payload, # 拖動的元素對象,可通過 getChildPayload 指定
droppedElement, # 放置的 DOM 元素
}
回調(diào)
回調(diào)在用戶交互之前和期間提供了額外的邏輯和檢查。
get-child-payload(index)自定義傳給onDrop()的payload對象。should-accept-drop(sourceContainerOptions, payload)用來確定容器是否可被放置,會覆蓋group-name屬性。should-animate-drop(sourceContainerOptions, payload)返回false則阻止放置動畫。get-ghost-parent()返回幽靈元素(拖動時顯示的元素)應(yīng)該添加到的元素,默認(rèn)是父元素,某些情況定位會出現(xiàn)問題,則可以選擇自定義,如返回document.body。
事件
@drag-start在拖動開始時由所有容器發(fā)出的事件。參數(shù)dragResult。@drag-end所有容器在拖動結(jié)束時調(diào)用的函數(shù)。 在@drop事件之前調(diào)用。參數(shù)dragResult。@drag-enter每當(dāng)拖動的項目在拖動時進入其邊界時,相關(guān)容器要發(fā)出的事件。@drag-leave每當(dāng)拖動的項目在拖動時離開其邊界時,相關(guān)容器要發(fā)出的事件。@drop-ready當(dāng)容器中可能放置位置的索引發(fā)生變化時,被拖動的容器將調(diào)用的函數(shù)?;旧?,每次容器中的可拖動對象滑動以打開拖動項目的空間時都會調(diào)用它。參數(shù)dropResult。@drop放置結(jié)束時所有相關(guān)容器會發(fā)出的事件(放置動畫結(jié)束后)。源容器和任何可以接受放置的容器都被認(rèn)為是相關(guān)的。參數(shù)dropResult。
API: Draggable
tag
同容器的 tag 指定可拖拽元素的 DOM 元素標(biāo)簽。
實戰(zhàn)
實現(xiàn)一個簡單的團隊協(xié)作任務(wù)管理器。
<template>
<div class="card-scene">
<Container
orientation="horizontal"
@drop="onColumnDrop($event)"
drag-handle-selector=".column-drag-handle"
>
<Draggable v-for="column in taskColumnList" :key="column.name">
<div class="card-container">
<div class="card-column-header">
<span class="column-drag-handle">☰</span>
{{ column.name }}
</div>
<Container
group-name="col"
@drop="(e) => onCardDrop(column.id, e)"
:get-child-payload="getCardPayload(column.id)"
drag-class="card-ghost"
drop-class="card-ghost-drop"
:drop-placeholder="dropPlaceholderOptions"
class="draggable-container"
>
<Draggable v-for="task in column.list" :key="task.id">
<div class="task-card">
<div class="task-title">{{ task.name }}</div>
<div class="task-priority" :style="{ background: priorityMap[task.priority].color }">
{{ priorityMap[task.priority].label }}
</div>
</div>
</Draggable>
</Container>
</div>
</Draggable>
</Container>
</div>
</template>
<script>
import { Container, Draggable } from "vue-smooth-dnd";
const applyDrag = (arr, dragResult) => {
const { removedIndex, addedIndex, payload } = dragResult
console.log(removedIndex, addedIndex, payload)
if (removedIndex === null && addedIndex === null) return arr
const result = [...arr]
let itemToAdd = payload
if (removedIndex !== null) {
itemToAdd = result.splice(removedIndex, 1)[0]
}
if (addedIndex !== null) {
result.splice(addedIndex, 0, itemToAdd)
}
return result
}
const taskList = [
{
name: '首頁',
priority: 'P1',
status: '待開發(fā)',
id: 1,
},
{
name: '流程圖開發(fā)',
priority: 'P3',
status: '待評審',
id: 2,
},
{
name: '統(tǒng)計圖展示',
priority: 'P0',
status: '開發(fā)中',
id: 3,
},
{
name: '文件管理',
priority: 'P1',
status: '開發(fā)中',
id: 4,
}
]
const statusList = ['待評審', '待開發(fā)', '開發(fā)中', '已完成']
const taskColumnList = statusList.map((status, index) => {
return {
name: status,
list: taskList.filter(item => item.status === status),
id: index
}
})
const priorityMap = {
'P0': {
label: '最高優(yōu)',
color: '#ff5454',
},
'P1': {
label: '高優(yōu)',
color: '#ff9a00',
},
'P2': {
label: '中等',
color: '#ffd139',
},
'P3': {
label: '較低',
color: '#1ac7b5',
},
}
export default {
name: 'Cards',
components: {Container, Draggable},
data () {
return {
taskColumnList,
priorityMap,
dropPlaceholderOptions: {
className: 'drop-preview',
animationDuration: '150',
showOnTop: true
}
}
},
methods: {
onColumnDrop (dropResult) {
this.taskColumnList = applyDrag(this.taskColumnList, dropResult)
},
onCardDrop (columnId, dropResult) {
let { removedIndex, addedIndex, payload } = dropResult
if (removedIndex !== null || addedIndex !== null) {
const column = taskColumnList.find(p => p.id === columnId)
if (addedIndex !== null && payload) { // 更新任務(wù)狀態(tài)
dropResult.payload = {
...payload,
status: column.name,
}
}
column.list = applyDrag(column.list, dropResult)
}
},
getCardPayload (columnId) {
return index =>
this.taskColumnList.find(p => p.id === columnId).list[index]
},
}
}
</script>
<style>
* {
margin: 0;
padding: 0;
font-family: 'Microsoft YaHei','PingFang SC','Helvetica Neue',Helvetica,sans-serif;
line-height: 1.45;
color: rgba(0,0,0,.65);
}
.card-scene {
user-select: none;
display: flex;
height: 100%;
margin: 20px;
}
.card-container {
display: flex;
flex-direction: column;
width: 260px;
min-width: 260px;
border-radius: 12px;
background-color: #edeff2;
margin-right: 16px;
height: calc(100vh - 40px);
}
.card-column-header {
display: flex;
height: 50px;
margin: 0 16px;
align-items: center;
flex-shrink: 0;
font-weight: 500;
font-size: 16px;
}
.draggable-container {
flex-grow: 1;
overflow: auto;
}
.column-drag-handle {
cursor: move;
padding: 5px;
}
.task-card {
margin: 10px;
background-color: white;
padding: 15px 10px;
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.12);
cursor: pointer;
display: flex;
justify-content: space-between;
}
.task-title {
color: #333333;
font-size: 14px;
}
.task-priority {
width: 60px;
line-height: 20px;
border-radius: 12px;
text-align: center;
color: #fff;
font-size: 12px;
}
.card-ghost {
transition: transform 0.18s ease;
transform: rotateZ(5deg)
}
.card-ghost-drop {
transition: transform 0.18s ease-in-out;
transform: rotateZ(0deg)
}
.drop-preview {
background-color: rgba(150, 150, 200, 0.1);
border: 1px dashed #abc;
margin: 5px;
}
</style>
效果

到此這篇關(guān)于Vue 可拖拽組件Vue Smooth DnD的使用詳解的文章就介紹到這了,更多相關(guān)Vue 可拖拽組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- vue拖拽組件 vuedraggable API options實現(xiàn)盒子之間相互拖拽排序
- Vue拖拽組件列表實現(xiàn)動態(tài)頁面配置功能
- vue拖拽組件使用方法詳解
- Vue拖拽組件開發(fā)實例詳解
- vue 實現(xiàn)拖拽動態(tài)生成組件的需求
- vue使用Split封裝通用拖拽滑動分隔面板組件
- vue開發(fā)拖拽進度條滑動組件
- vue draggable resizable 實現(xiàn)可拖拽縮放的組件功能
- 利用Vue-draggable組件實現(xiàn)Vue項目中表格內(nèi)容的拖拽排序
- Vue組件Draggable實現(xiàn)拖拽功能
- Vue實現(xiàn)可拖拽組件的方法
相關(guān)文章
Element-UI結(jié)合遞歸組件實現(xiàn)后臺管理系統(tǒng)左側(cè)菜單
在Vue.js中使用遞歸組件可以方便地構(gòu)建多層級的菜單結(jié)構(gòu),遞歸組件適用于處理具有嵌套關(guān)系的數(shù)據(jù),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-09-09
Vue實現(xiàn)模糊查詢-Mysql數(shù)據(jù)庫數(shù)據(jù)
這篇文章主要介紹了基于Vue實現(xiàn)Mysql數(shù)據(jù)庫數(shù)據(jù)模糊查詢,下面文章我們主要實現(xiàn)的是輸入框中輸入數(shù)據(jù),根據(jù)輸入的結(jié)果模糊搜索數(shù)據(jù)庫對應(yīng)內(nèi)容,實現(xiàn)模糊查詢,感興趣的小伙伴可以進入文章我們一起學(xué)習(xí)2021-12-12
vue3+element-plus?Dialog對話框的使用與setup?寫法的用法
這篇文章主要介紹了vue3+element-plus?Dialog對話框的使用?與?setup?寫法的使用,本文通過兩種方式結(jié)合實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04

