Skip to content

Draggable 拖拽

也许你需要拖拽排序

组件注册

js
import { FDraggable } from '@fesjs/fes-design';

app.use(FDraggable);

代码演示

垂直方向

我们经常在垂直方向上拖动排序

play
<template>
    <FForm labelWidth="100px">
        <FFormItem label="是否禁用:">
            <FSwitch v-model="disabled" />
        </FFormItem>
    </FForm>
    <FSpace>
        <FButton type="primary" @click="doInsertItem">动态新增</FButton>
        <FButton type="primary" @click="doDeleteItem">动态删减</FButton>
        <FButton type="primary" @click="doRandomUpdateItem"> 随机更新 </FButton>
    </FSpace>

    <FDivider />

    <div class="container">
        <FDraggable
            v-model="list"
            :beforeDragend="beforeDragend"
            :disabled="disabled"
        >
            <template #default="{ item }">
                <div class="sort-item">{{ item }}</div>
            </template>
        </FDraggable>
    </div>
</template>

<script>
import { ref } from 'vue';
import { FMessage } from '@fesjs/fes-design';

let seed = 6;
export default {
    setup() {
        const disabled = ref(false);

        const list = ref([1, 2, 3, 4, 5]);
        const doInsertItem = () => {
            list.value.push(`new item ${seed++}`);
        };
        const doDeleteItem = () => {
            list.value.splice(list.value.length - 1, 1);
        };

        const doRandomUpdateItem = () => {
            let op = Number.parseInt(Math.random() * 10) % 2;
            let index = Number.parseInt(Math.random() * 100) % list.value.length;
            if (list.value.length === 0) {
                op = 0;
                index = 0;
            }
            if (op === 0) {
                // 新增
                FMessage.success(`在位置${index + 1}新增`);
                list.value = list.value
                    .slice(0, index)
                    .concat([`new item ${seed++}`])
                    .concat(list.value.slice(index));
            } else {
                // 删除
                FMessage.success(`在位置${index + 1}删除`);
                list.value.splice(index, 1);
            }
        };

        const beforeDragend = () => {
            // return new Promise((resolve, reject) => {
            //     setTimeout(() => {
            //         reject();
            //     }, 3000);
            // });
            return true;
        };

        return {
            disabled,
            list,
            doInsertItem,
            doDeleteItem,
            doRandomUpdateItem,
            beforeDragend,
        };
    },
};
</script>

<style>
.fes-draggable-item::before {
    display: none;
}
</style>

<style scoped>
.container {
    background: #eee;
    padding: 50px 20px;
}
.sort-item {
    line-height: 50px;
    background: #fff;
    margin: 1px 0;
    padding-left: 20px;
}
</style>

水平方向

还可能在水平方向上拖动

play
<template>
    <FForm labelWidth="100px">
        <FFormItem label="是否禁用:">
            <FSwitch v-model="disabled" />
        </FFormItem>
    </FForm>

    <FDivider />

    <div class="container">
        <FDraggable
            v-model="hlist"
            class="horizontal"
            :beforeDragend="beforeDragend"
            :disabled="disabled"
        >
            <template #default="{ item }">
                <div class="sort-horizontal-item">{{ item }}</div>
            </template>
        </FDraggable>
    </div>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const disabled = ref(false);
        const hlist = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);

        const beforeDragend = (item, index) => {
            console.log(
                '[draggable.horizontal] [beforeDragend] item:',
                item,
                ' index:',
                index,
            );
            return true;
        };

        return {
            disabled,
            hlist,
            beforeDragend,
        };
    },
};
</script>

<style scoped>
.container {
    background: #eee;
    padding: 50px 20px;
}
.sort-horizontal-item {
    line-height: 100px;
    width: 100px;
    background: #fff;
    margin: 1px;
    text-align: center;
}

.horizontal {
    display: flex;
    flex-wrap: wrap;
}
</style>

多个容器

或许你需要由一个容器拖拽到另一个容器

play
<template>
    <FForm labelWidth="120px">
        <FFormItem label="禁用容器:">
            <FCheckboxGroup
                v-model="disableds"
                :options="[
                    { label: '左侧容器', value: 'left' },
                    { label: '中间容器', value: 'center' },
                    { label: '右侧容器', value: 'right' },
                ]"
            />
        </FFormItem>
        <FFormItem label="可以放置容器:">
            <FCheckboxGroup
                v-model="droppables"
                :options="[
                    { label: '左侧容器', value: 'left' },
                    { label: '中间容器', value: 'center' },
                    { label: '右侧容器', value: 'right' },
                ]"
            />
        </FFormItem>
    </FForm>

    <FDivider />

    <div class="container">
        <FDraggable
            v-model="mlist"
            class="draggable-wrapper"
            :disabled="disableds.includes('left')"
            :droppable="droppables.includes('left')"
            @dragstart="handleDargStart"
            @dragend="handleDargEnd"
        >
            <template #default="{ item }">
                <div class="sort-item2">{{ item }}</div>
            </template>
        </FDraggable>
        <FDraggable
            v-model="mlist2"
            class="draggable-wrapper"
            :disabled="disableds.includes('center')"
            :droppable="droppables.includes('center')"
            @dragstart="handleDargStart2"
            @dragend="handleDargEnd2"
        >
            <template #default="{ item }">
                <div class="sort-item2">{{ item }}</div>
            </template>
        </FDraggable>
        <FDraggable
            v-model="mlist3"
            class="draggable-wrapper"
            :disabled="disableds.includes('right')"
            :droppable="droppables.includes('right')"
        >
            <template #default="{ item }">
                <div class="sort-item2">{{ item }}</div>
            </template>
        </FDraggable>
    </div>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const disableds = ref([]);
        const droppables = ref(['left', 'center', 'right']);

        const mlist = ref([1, 2, 3, 4]);
        const mlist2 = ref([5, 6, 7]);
        const mlist3 = ref([8, 9]);

        function handleDargStart(event, item, setting) {
            console.log(
                '[draggable.container] [handleDargStart] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }
        function handleDargStart2(event, item, setting) {
            console.log(
                '[draggable.container] [handleDargStart2] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }
        function handleDargEnd(event, item, setting) {
            console.log(
                '[draggable.container] [handleDargEnd] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }
        function handleDargEnd2(event, item, setting) {
            console.log(
                '[draggable.container] [handleDargEnd2] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }

        return {
            disableds,
            droppables,
            mlist,
            mlist2,
            mlist3,
            handleDargStart,
            handleDargStart2,
            handleDargEnd,
            handleDargEnd2,
        };
    },
};
</script>

<style scoped>
.container {
    background: #eee;
    padding: 50px 20px;
    display: flex;
    justify-content: space-around;
}

.draggable-wrapper {
    height: 300px;
    width: 200px;
    overflow: auto;
    border: 1px dashed #ccc;
    box-sizing: border-box;
}

.sort-item2 {
    line-height: 50px;
    background: #fff;
    margin: 1px 0;
    padding-left: 20px;
}
</style>

阻止拖拽

当需要检查拖拽结果是否符合要求时,使用 beforeDragend,返回 false、Promise.resolve(false)、Promise.reject()时,拖拽会恢复之前的状态;

ts
type BeforeDragEnd = (
    drag: {
        // 拖拽信息
        item: unknown;
        index: number;
        list: unknown[];
        resultList: unknown[]; // 拖拽结束预期结果
    },
    drop: {
        // 放置信息
        item: unknown;
        index: number;
        list: unknown[];
        resultList: unknown[];
    },
) => Promise<boolean> | boolean;
play
<template>
    <div class="container" style="display: flex; justify-content: space-around">
        <FDraggable
            v-model="mlist"
            droppable
            class="draggable-wrapper"
            :beforeDragend="beforeDragend"
            @dragstart="handleDargStart"
            @dragend="handleDargEnd"
        >
            <template #default="{ item }">
                <div class="sort-item2">{{ item }}</div>
            </template>
        </FDraggable>
        <FDraggable
            v-model="mlist2"
            droppable
            class="draggable-wrapper"
            :beforeDragend="beforeDragend2"
            @dragstart="handleDargStart2"
            @dragend="handleDargEnd2"
        >
            <template #default="{ item }">
                <div class="sort-item2">{{ item }}</div>
            </template>
        </FDraggable>
        <FDraggable v-model="mlist3" droppable class="draggable-wrapper">
            <template #default="{ item }">
                <div class="sort-item2">{{ item }}</div>
            </template>
        </FDraggable>
    </div>
</template>

<script>
import { ref } from 'vue';
import { FMessage } from '@fesjs/fes-design';

export default {
    setup() {
        const mlist = ref([1, 2, 3, 4]);
        const mlist2 = ref([5, 6, 7]);
        const mlist3 = ref([8, 9]);

        function handleDargStart(event, item, setting) {
            console.log(
                '[draggable.checkDragEnd] [handleDargStart] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }
        function handleDargStart2(event, item, setting) {
            console.log(
                '[draggable.checkDragEnd] [handleDargStart2] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }
        function handleDargEnd(event, item, setting) {
            console.log(
                '[draggable.checkDragEnd] [handleDargEnd] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }
        function handleDargEnd2(event, item, setting) {
            console.log(
                '[draggable.checkDragEnd] [handleDargEnd2] event:',
                event,
                ' item:',
                item,
                ' setting:',
                setting,
            );
        }

        const beforeDragend = (drag, drop) => {
            console.log(
                '[draggable.checkDragEnd] [beforeDragend] drag:',
                drag,
                ' drop:',
                drop,
            );
            FMessage.error('拖拽阻止!');
            return false;
        };

        const beforeDragend2 = (drag, drop) => {
            console.log(
                '[draggable.checkDragEnd] [beforeDragend2] drag:',
                drag,
                ' drop:',
                drop,
            );

            FMessage.error('拖拽阻止!');
            return false;
        };

        return {
            mlist,
            mlist2,
            mlist3,
            handleDargStart,
            handleDargStart2,
            handleDargEnd,
            handleDargEnd2,
            beforeDragend,
            beforeDragend2,
        };
    },
};
</script>

<style scoped>
.container {
    background: #eee;
    padding: 50px 20px;
}

.draggable-wrapper {
    height: 300px;
    width: 200px;
    overflow: auto;
    border: 1px dashed #ccc;
    box-sizing: border-box;
}

.sort-item2 {
    line-height: 50px;
    background: #fff;
    margin: 1px 0;
    padding-left: 20px;
}
</style>

拖拽指令-垂直方向

用指令更简单

play
<template>
    <div class="container">
        <div v-drag:[dragArg]="vlist">
            <div
                v-for="i in vlist"
                :key="i"
                class="sort-item"
                style="opacity: 1"
            >
                <span>{{ i }}</span>
            </div>
        </div>
    </div>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const vlist = ref([]);
        setTimeout(() => {
            vlist.value = [1, 2, 3, 4, 5];
        }, 1000);

        const dragArg = {
            onDragstart(event, item, setting) {
                console.log(
                    '[draggable.instruction] [onDragstart] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            onDragend(event, item, setting) {
                console.log(
                    '[draggable.instruction] [onDragend] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            beforeDragend(item, start, end) {
                console.log(
                    '[draggable.instruction] [beforeDragend] item:',
                    item,
                    ' start:',
                    start,
                    ' end:',
                    end,
                );
                return true;
            },
        };

        return {
            vlist,
            dragArg,
        };
    },
};
</script>

<style scoped>
.container {
    background: #eee;
    padding: 50px 20px;
}
.sort-item {
    line-height: 50px;
    background: #fff;
    margin: 1px 0;
    padding-left: 20px;
}
</style>

拖拽指令-水平方向

指令也可以支持水平方向的

play
<template>
    <div class="container">
        <div v-drag="vhlist" class="horizontal">
            <div v-for="i in vhlist" :key="i" class="sort-horizontal-item">
                <span>{{ i }}</span>
            </div>
        </div>
    </div>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const vhlist = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
        return {
            vhlist,
        };
    },
};
</script>

<style scoped>
.container {
    background: #eee;
    padding: 50px 20px;
}
.sort-horizontal-item {
    line-height: 100px;
    width: 100px;
    background: #fff;
    margin: 1px;
    text-align: center;
}

.horizontal {
    display: flex;
    flex-wrap: wrap;
}
</style>

拖拽指令-多个容器

指令当然也支持由一个容器拖拽到另一个容器

play
<template>
    <div class="container" style="display: flex; justify-content: space-around">
        <div>
            <span class="words">禁止拖拽、不可放置</span>
            <div v-drag:[dragArg].disabled="mlist" class="draggable-wrapper">
                <div v-for="i in mlist" :key="i" class="sort-item2">
                    <span>{{ i }}</span>
                </div>
            </div>
        </div>
        <div>
            <span class="words">可拖拽、可放置</span>
            <div v-drag:[dragArg2].droppable="mlist2" class="draggable-wrapper">
                <div v-for="i in mlist2" :key="i" class="sort-item2">
                    <span>{{ i }}</span>
                </div>
            </div>
        </div>
        <div>
            <span class="words">可拖拽、不可放置</span>
            <div v-drag="mlist3" class="draggable-wrapper">
                <div v-for="i in mlist3" :key="i" class="sort-item2">
                    <span>{{ i }}</span>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import { ref } from 'vue';

export default {
    setup() {
        const disableds = ref([]);
        const droppables = ref(['left', 'center', 'right']);

        const mlist = ref([1, 2, 3, 4]);
        const mlist2 = ref([5, 6, 7]);
        const mlist3 = ref([8, 9]);

        const dragArg = {
            onDragstart(event, item, setting) {
                console.log(
                    '[draggable.instructionContainer] [onDragstart] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            onDragend(event, item, setting) {
                console.log(
                    '[draggable.instructionContainer] [onDragend] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
        };

        const dragArg2 = {
            onDragstart(event, item, setting) {
                console.log(
                    '[draggable.instructionContainer] [onDragstart] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            onDragend(event, item, setting) {
                console.log(
                    '[draggable.instructionContainer] [onDragend] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
        };

        return {
            disableds,
            droppables,
            mlist,
            mlist2,
            mlist3,
            dragArg,
            dragArg2,
        };
    },
};
</script>

<style scoped>
.container {
    background: #eee;
    padding: 30px 20px 50px;
    display: flex;
    justify-content: space-around;
}

.words {
    display: block;
    margin-bottom: 10px;
}

.draggable-wrapper {
    height: 300px;
    width: 200px;
    overflow: auto;
    border: 1px dashed #ccc;
    box-sizing: border-box;
}

.sort-item2 {
    line-height: 50px;
    background: #fff;
    margin: 1px 0;
    padding-left: 20px;
}
</style>

拖拽指令-阻止拖拽

play
<template>
    <div class="container" style="display: flex; justify-content: space-around">
        <div v-drag:[dragArg].droppable="mlist">
            <div v-for="i in mlist" :key="i" class="sort-item2">
                <span>{{ i }}</span>
            </div>
        </div>
        <div v-drag:[dragArg2].droppable="mlist2">
            <div v-for="i in mlist2" :key="i" class="sort-item2">
                <span>{{ i }}</span>
            </div>
        </div>
        <div v-drag.droppable="mlist3">
            <div v-for="i in mlist3" :key="i" class="sort-item2">
                <span>{{ i }}</span>
            </div>
        </div>
    </div>
</template>

<script>
import { ref } from 'vue';
import { FMessage } from '@fesjs/fes-design';

export default {
    setup() {
        const mlist = ref([]);
        setTimeout(() => {
            mlist.value = [1, 2, 3, 4];
        }, 1000);
        const mlist2 = ref([5, 6, 7]);
        const mlist3 = ref([8, 9]);

        const dragArg = {
            onDragstart(event, item, setting) {
                console.log(
                    '[draggable.instructionCheckDragEnd] [onDragstart] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            onDragend(event, item, setting) {
                console.log(
                    '[draggable.instructionCheckDragEnd] [onDragend] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            beforeDragend(item, start, end) {
                console.log(
                    '[draggable.instructionCheckDragEnd] [beforeDragend] item:',
                    item,
                    ' start:',
                    start,
                    ' end:',
                    end,
                );
                FMessage.error('拖拽阻止!');
                return false;
            },
        };

        const dragArg2 = {
            onDragstart(event, item, setting) {
                console.log(
                    '[draggable.instructionCheckDragEnd] [onDragstart] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            onDragend(event, item, setting) {
                console.log(
                    '[draggable.instructionCheckDragEnd] [onDragend] event:',
                    event,
                    ' item:',
                    item,
                    ' setting:',
                    setting,
                );
            },
            beforeDragend(item, start, end) {
                console.log(
                    '[draggable.instructionCheckDragEnd] [beforeDragend] item:',
                    item,
                    ' start:',
                    start,
                    ' end:',
                    end,
                );
                FMessage.error('拖拽阻止!');
                return false;
            },
        };

        return {
            mlist,
            mlist2,
            mlist3,
            dragArg,
            dragArg2,
        };
    },
};
</script>

<style scoped>
.container {
    background: #eee;
    padding: 50px 20px;
}

.sort-item2 {
    line-height: 50px;
    background: #fff;
    margin: 1px 0;
    padding-left: 20px;
    width: 150px;
}
</style>

Draggable Props

属性说明类型默认值
v-model绑定值Array[]
tag指定 root dom 类型stringdiv
disabled是否禁止拖拽booleanfalse
droppable是否可以放置,设置为 droppable 的容器都可以相互拖拽放置booleanfalse
beforeDragend拖拽结束之前回调,返回 false、Promise.resolve(false)、Promise.reject()时,拖拽会恢复之前的状态BeforeDragEnd() => true

Draggable Events

事件名称说明回调参数
dragstart拖拽开始触发(event, item, index) => void
dragend拖拽结束触发(event, item, index) => void

Draggable Slots

名称说明属性
defaultitem 内容{ item, index }

Draggable Directive

说明
指令名称v-drag
绑定值 Array 类型
修饰符 droppable是否可以放置其他容器的拖拽目标
修饰符 disabled是否禁用
参数Object,指令参数
-- beforeDragend拖拽结束之前回调,BeforeDragEnd
-- onDragstart拖拽开始触发
-- onDragend拖拽结束触发