前言
vue.draggable.next 是一款vue3的拖拽插件,是vue.draggable升级版本,同样是基于Sortable.js实现的,你可以用它来拖拽列表、菜单、工作台、选项卡等常见的工作场景。
包安装方式
yarn add vuedraggable@next
npm i -S vuedraggable@next
UMD浏览器引用JS方式
<script src="https://www.itxst.com/package/vue3/vue.global.js"></script>
<script src="https://www.itxst.com/package/sortable/Sortable.min.js"></script>
<script src="https://www.itxst.com/package/vuedraggablenext/vuedraggable.umd.min.js"></script>
左侧icon 区
<draggable
:list="item.componentList"
class="icon-list-box"
animation="300"
item-key="id"
@start="onStart"
@end="onEnd"
@filter="onFilter"
:clone="cloneDog"
:group="{ name: props.dragGroup, pull: 'clone', put: false }"
:touch-start-threshold="50"
:fallback-tolerance="50"
:sort="false"
>
<template #item="{ element }">
<div :class="{ 'list-item': true, draggableToolItem: true }">
<div class="icon-box">
<Icon :size="40" :icon="element.icon" />
<p class="icon-title">{{ element.title }}</p>
</div>
</div>
</template>
</draggable>
<script setup name="toolBar">
import { Tabs } from 'ant-design-vue';
import draggable from 'vuedraggable';
import { Icon } from '/@/components/Icon';
import { generateEditorItem } from '../../utils';
const $emits = defineEmits(['onFilter', 'onDragStart', 'onDragEnd']);
const TabPane = Tabs.TabPane;
const props = defineProps({
tools: {
type: Array,
default: () => [],
},
dragGroup: {
type: String,
default: '',
},
});
console.log(props.dragGroup, '-----');
//拖拽开始的事件
const onStart = () => {
console.log('--onStart---');
$emits('onDragStart');
};
const onFilter = () => {
console.log('--onFilter---');
$emits('onFilter');
};
//拖拽结束的事件
const onEnd = () => {
console.log('--onEnd---');
$emits('onDragEnd');
};
const cloneDog = (toolItem) => {
console.log('--cloneDog---', toolItem);
return generateEditorItem(toolItem);
};
</script>
中间显示区
<div ref="domScrollWrap" class="drag-area-wrap">
<draggable
ref="editDraggable"
:list="editComponentList"
@start="onStart"
@change="onDragChange"
:disabled="false"
item-key="id"
:component-data="{ name: 'fade' }"
draggable=".draggableItem"
handle=".draggableItem"
:group="{ name: 'componentsGroup', pull: true }"
:ghostClass="$style.ghost"
class="drag-area"
:animation="300"
:sort="true"
>
<template #item="{ element, index }">
<div class="draggableItem">
<div className="widget">{{ element.title }}</div>
<div @click="showPopover(element)" :class="{ active: state.id == element.id }">
<Popover
placement="right"
overlayClassName="draggable-popover"
:get-popup-container="getPopupContainer"
trigger="click"
v-model="visible"
>
<template #content>
<a-button
preIcon="ant-design:caret-up-outlined"
title="上移"
:disabled="index == 0"
@click="handleItemOperate({ item: element, command: 'moveUp' })"
/>
<a-button
preIcon="ant-design:caret-down-outlined"
title="下移"
:disabled="index == editComponentList.length - 1"
@click="handleItemOperate({ item: element, command: 'moveDown' })"
/>
<a-button
preIcon="ant-design:copy-outlined"
title="复制"
@click="handleItemOperate({ item: element, command: 'copy' })"
/>
<a-button
preIcon="ant-design:delete-outlined"
title="移除"
@click="handleItemOperate({ item: element, command: 'remove' })"
/>
</template>
<component
:is="group[element.name]"
:ref="element.name"
:form-data="element.componentValue"
/>
</Popover>
</div>
</div>
</template>
</draggable>
<div class="tip-box" v-if="editComponentList.length === 0">
<p>左边选择模块拖入该区域</p>
</div>
<script setup name="adornment">
import { ref, reactive, computed, onBeforeMount } from 'vue';
import { Input, Popover, Pagination, Empty } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import draggable from 'vuedraggable';
import { PageWrapper } from '/@/components/Page';
import ToolBar from './components/ToolBar/index.vue';
import tools from './data/tools';
import group from './components/group';
import defaultItems from './data/mock';
import * as arrayMethods from './array';
import { api2VmToolItem, generateEditorItem } from './utils';
import { editDecoratePageConfig, getDecoratePageConfigDetail } from '/@/api/sys/decorate';
import { useUserStore } from '/@/store/modules/user';
const { userInfo } = useUserStore();
const editDraggable = ref(null);
const domScrollWrap = ref(null);
const state = reactive({
formName: '',
id: '',
formData: {},
});
const editHeaderComponentList = reactive([]);
const editComponentList = reactive([]);
const editFooterComponentList = reactive([]);
const visible = ref(false);
//拖拽开始的事件
const onStart = (data) => {
console.log('开始拖拽');
};
// 新增和查询页面配置
editDecoratePageConfig({
pageName: '首页1',
remark: '111',
settingJson: JSON.stringify([
{
title: '搜索框',
description: '首页搜索框',
name: 'SearchBar',
orderNo: 1,
id: '1',
properties: {
link: '/pages/category/category',
},
},
{
title: '标题栏',
description: '首页标题栏',
name: 'TitleBar',
orderNo: 2,
id: '111',
properties: {
leftText: '爆款秒杀',
rightText: '查看更多',
},
},
{
title: '轮播图',
description: '首页轮播',
name: 'Swiper',
orderNo: 3,
id: '2',
properties: {
slideList: [
{
url: 'https://file.40017.cn/cvgfront-common/tc_travel_top_banner_new.png',
type: 'img',
},
{
url: 'https://pavo.elongstatic.com/i/ori/15sr7AutTyM.jpg',
type: 'img',
},
{
url: 'https://storage.360buyimg.com/jdc-article/welcomenutui.jpg',
type: 'img',
},
{
url: 'https://storage.360buyimg.com/jdc-article/fristfabu.jpg',
type: 'img',
},
],
},
},
]),
shopId: 0,
updateManager: userInfo.account,
});
getDecoratePageConfigDetail({ pageId: 1 });
const getPopupContainer = (trigger) => {
return trigger.parentElement;
};
const showPopover = (item) => {
console.log(item, '=========test======');
state.formData = item.componentValue;
state.formName = item.formName;
state.id = item.id;
visible.value = !visible.value;
};
onBeforeMount(() => {
const dataList = api2VmToolItem(tools, defaultItems);
dataList.forEach((toolItemData) => {
// 模拟拖入组件插入数据
const editorData = generateEditorItem(toolItemData);
editComponentList.push(editorData);
console.log(editComponentList, '----------');
});
});
//拖拽结束的事件
const onEnd = (data) => {
console.log('-----on----End----', data);
console.log('结束拖拽');
};
const onDragChange = (data) => {
console.log('-----onDragChange----', data);
};
const handleItemOperate = ({ item, command }) => {
const strategyMap = {
moveUp(target, arrayItem) {
return arrayMethods.moveUp(target, arrayItem);
},
moveDown(target, arrayItem) {
return arrayMethods.moveDown(target, arrayItem);
},
copy(target, arrayItem) {
const { componentValue, ...emptyPack } = arrayItem;
return target.splice(target.indexOf(arrayItem) + 1, 0, generateEditorItem(emptyPack));
},
remove(target, arrayItem) {
return arrayMethods.remove(target, arrayItem);
},
};
const curStrategy = strategyMap[command];
if (curStrategy) {
curStrategy.apply(this, [editComponentList, item]);
} else {
this.$message.error(`系统错误 - 未知的操作:[${command}]`);
}
};
const onSave = () => {
console.log('222222', editComponentList);
};
const onMove = (e) => {
console.log('-----on----Move----', e); //不允许停靠
// if (e.relatedContext.element.disabledPark == true) return false;
return true;
};
</script>
右侧组件配置区
{
"imgList": [
{
"id": "1",
"name": "苏宁",
"imgUrl": "https://gw.alicdn.com/imgextra/i1/1734167/O1CN01eXdllZ1geX1zdbBLa_!!1734167-0-lubanu.jpg_790x10000Q75.jpg_.webp",
"imgLink": "https://www.jd.com"
},
{
"id": "2",
"name": "苏宁",
"imgUrl": "https://img.alicdn.com/tps/i4///img.alicdn.com/tps/i4//TB1CDhKUvb2gK0jSZK9SuuEgFXa.jpg_790x10000Q75.jpg_.webp",
"imgLink": "https://www.jd.com"
}
]
}
<div class="card-list">
<draggable :list="props.formData.imgList" animation="300" :sort="true" item-key="id">
<template #item="{ element, index }">
<Card class="card-box" :bodyStyle="{ padding: '0' }">
<div style="overflow: hidden">
<div class="upload-box">
<Upload
name="file"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:action="uploadUrl"
:before-upload="beforeUpload"
@change="(file) => handleUploadChange(file, index)"
>
<img v-if="element.imgUrl" :src="element.imgUrl" alt="avatar" />
<div v-else>
<loading-outlined v-if="state.uploadLoading" />
<plus-outlined v-else />
<div class="ant-upload-text">添加图片</div>
</div>
</Upload>
</div>
<div>
<div>
<div class="mb12">
<span class="form-label">标题</span>
<Input style="width: 172px" v-model:value="element.name" />
</div>
<div>
<span class="form-label">链接</span>
<a-button type="link" style="padding: 0">Link</a-button>
</div>
</div>
</div>
</div>
<Icon
icon="ant-design:close-circle-outlined"
:size="20"
class="card-close"
style="display: none"
@click="() => removeItem(index)"
/>
</Card>
</template>
</draggable>
</div>
<script setup name="carouselImgForm">
import draggable from 'vuedraggable';
import { ref, defineComponent, reactive, toRefs, computed } from 'vue';
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue';
import { useGlobSetting } from '/@/hooks/setting';
import { useUserStore } from '/@/store/modules/user';
import { Card, Image, Upload, Input } from 'ant-design-vue';
import { Icon } from '/@/components/Icon';
import { genId } from '../../../utils';
const { uploadUrl } = useGlobSetting();
const props = defineProps({
formData: { type: Object },
});
const state = reactive({
uploadLoading: false,
});
const beforeUpload = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('You can only upload JPG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('Image must smaller than 2MB!');
}
return isJpgOrPng && isLt2M;
};
const handleUploadChange = (info, index) => {
if (info.file.status === 'uploading') {
state.uploadLoading = true;
return;
}
if (info.file.status === 'done') {
const { result } = info.file.response.data;
// eslint-disable-next-line vue/no-mutating-props
props.formData.imgList[index].imgUrl = result;
state.uploadLoading = false;
}
if (info.file.status === 'error') {
state.uploadLoading = false;
message.error('upload error');
}
};
const removeItem = (index) => {
if (props.formData.imgList.length < 2) {
message.error('最少保留一个图片');
return;
}
// eslint-disable-next-line vue/no-mutating-props
props.formData.imgList.splice(index, 1);
};
const addItem = () => {
// eslint-disable-next-line vue/no-mutating-props
props.formData.imgList.push({
id: genId(),
name: '',
img: {
imgUrl: '',
imgLink: '',
},
});
};
</script>
最终效果图