vue实战场景之轻量级拖拽组件封装
在vue中想实现拖拽,可能会去使用vue-draggable,而vue-draggable是基于sortable.js作二次封装,兼容了移动端,但是在我们实际业务中,类似这种需求功能,我们并不希望频繁引入第三方库到生产环境中。
本文实现一个轻量级pc端vue拖拽组件,毕竟它在很多业务场景都是很常见的。
效果展示

实现的代码如下所示
drag.vue
<template>
<TransitionGroup name="group-list" tag="ul">
v-for="(item, index) in list"
:key="item[config.uuidField]"
:draggable="item.draggable"
:class="[
'list-item',
'is-dragover': index === dropIndex && item.draggable && config.exchange
@dragstart="onDragstart($event,index)"
@dragenter="onDragenter(index)"
@dragover.prevent="onDragover(index)"
@dragleave="onDragleave"
@dragend="onDragend"
@drop="onDrop"
<slot :item="item" />
</TransitionGroup>
</template>
<script>
export default {
name: 'Draggable',
props: {
list: {
type: Array,
default: () => [],
config: {
type: Object,
default: () => ({
name: '',
uuidField: 'id',
push: true,
pull: true,
exchange: true,
data() {
return {
dragIndex: null,
dropIndex: null,
computed: {
isPush() {
const { dropIndex, dragIndex } = this;
return dropIndex !== null && dragIndex === null;
isExchange() {
const { dropIndex, dragIndex } = this;
return dragIndex !== null && dropIndex !== null;
pushCbName() {
const { config: { name } } = this;
return `${name}-push-callback`;
methods: {
onDragstart(e, i) {
const {
list,
config: { name },
transferData,
} = this;
this.dragIndex = i;
if (name) {
transferData({ e, key: name, type: 'set', data: list[i] });
} else {
throw new Error('缺少配置关联名name');
this.$emit('drag-start', i);
onDragenter(i) {
this.dropIndex = i;
this.$emit('drag-enter', i);
onDragover(i) {
const { dragIndex, dropIndex } = this;
if (i === dragIndex || i === dropIndex) return;
this.dropIndex = i;
this.$emit('drag-over', i);
onDragleave() {
this.dropIndex = null;
onDrop(e) {
const {
list
,
dropIndex,
dragIndex,
config: { name, push: enablePush, exchange },
isPush,
isExchange,
pushCbName,
storage,
resetIndex,
transferData,
} = this;
if (dropIndex === dragIndex || !exchange) return;
if (isPush) {
if (!enablePush) {
resetIndex();
return;
if (name) {
list.splice(dropIndex, 0, transferData({ e, key: name, type: 'get' }));
storage('set', pushCbName, true);
} else {
resetIndex();
throw new Error('缺少配置关联属性name');
resetIndex();
return;
if (isExchange) {
const drapItem = list[dragIndex];
const dropItem = list[dropIndex];
list.splice(dropIndex, 1, drapItem);
list.splice(dragIndex, 1, dropItem);
resetIndex();
onDragend() {
const {
list,
dragIndex,
config: { pull: enablePull },
pushCbName,
storage,
resetIndex,
} = this;
if (enablePull) {
const isPushSuccess = storage('get', pushCbName);
if (isPushSuccess) {
list.splice(dragIndex, 1);
resetIndex();
storage('remove', pushCbName);
this.$emit('drag-end');
storage(type, key, value) {
return {
get() {
return JSON.parse(localStorage.getItem(key));
set() {
localStorage.setItem(key, JSON.stringify(value));
remove() {
localStorage.removeItem(key);
}[type]();
resetIndex() {
this.dropIndex = null;
this.dragIndex = null;
transferData({ e, key, type, data } = {}) {
if (type === 'get') {
return JSON.parse(e.dataTransfer.getData(`${key}-drag-key`));
if (type === 'set') {
e.dataTransfer.setData(`${key}-drag-key`, JSON.stringify(data));
</script>
<style lang="scss" scoped>
ul {
list-style: none;
margin: 0;
padding: 0;
.list-item {
list-style: none;
position: relative;
background-color: #fff;
cursor: move;
.list-item.is-dragover::before {
content: "";
position: absolute;
bottom: -7px;
left: 0;
width: 100%;
height: 4px;
background-color: #0c6bc9;
.list-item.is-dragover::after {
content: "";
position: absolute;
bottom: -11px;
left: -6px;
border: 3px solid #0c6bc9;
border-radius: 50%;
width: 6px;
height: 6px;
background-color: #fff;
.group-list-move {
transition: transform .8s;
</style>
使用方式:
demo.vue
<template>
<div id="app" class="app">
style="width: 200px"
:list="list1"
:config="config1"
<template v-slot="{ item }">
<div class="item">
{{ item.name }}
</template>
</Drag>
style="width: 200px"
:list="list2"
:config="config2"
<template v-slot="{ item }">
<div class="item">
{{ item.name }}
</template>
</Drag>
</template>
<script>
import Drag from './components/drag.vue';
export default {
name: 'App',
components: {
Drag,
data() {
return {
list1: new Array(10).fill(0).map((_, i) => ({
name: `列表1 - ${i + 1}`,
draggable: true,
config1: {
name: 'test',
push: true,
pull: true,
exchange: true,
list2: new Array(10).fill(0).map((_, i) => ({
name: `列表2 - ${i + 1}`,
draggable: true,
config2: {
name: 'test',
push: true,
pull: true,
exchange: true,
</script>
<style lang="scss" scoped>
.app {
display: flex;
justify-content: center;
column-gap: 20px;
.item {
border: 1px solid #ccc;
width: 200px;
height: 30px;
margin-bottom: 20px;
text-align: center;
</style>
参数:
list: 渲染列表
config: {