vue实战场景之轻量级拖拽组件封装

在vue中想实现拖拽,可能会去使用vue-draggable,而vue-draggable是基于sortable.js作二次封装,兼容了移动端,但是在我们实际业务中,类似这种需求功能,我们并不希望频繁引入第三方库到生产环境中。

本文实现一个轻量级pc端vue拖拽组件,毕竟它在很多业务场景都是很常见的。

效果展示

vue拖拽 https://www.zhihu.com/video/1428213666806730752

实现的代码如下所示

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: {