1、安装vue-grid-layout
安装vue-grid-layout的3.0.0-beta1版本,执行命令:npm install vue-grid-layout@3.0.0-beta1
2、快速跑通demo
1)在main.js中引入vue-grid-layout
import VueGridLayout from 'vue-grid-layout' // 引入layout
app.use(VueGridLayout)
2)在页面中加入组件
template:
<template>
<div class="grid-box" >
<grid-layout ref="gridLayout" v-model:layout="layout" :col-num="12" :row-height="30" :is-draggable="true" :is-resizable="true" :is-mirrored="false" :vertical-compact="true" :margin="[10, 10]" :use-css-transforms="true">
<grid-item ref="gridItem" @resized="resizedEvent" v-for="item in layout" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" :key="item.i">
<span class=" close" @click="delItem(item)"><i class="iconfont icon-guanbi"></i></span>
{{item.i}}
</grid-item>
</grid-layout>
</div>
</template>
script:
<script setup >
import { onMounted, ref, getCurrentInstance, defineAsyncComponent } from 'vue'
const { proxy } = getCurrentInstance()
const layout = ref([
{ "x": 0, "y": 0, "w": 2, "h": 2, "i": "0" },
{ "x": 2, "y": 0, "w": 2, "h": 4, "i": "1" },
{ "x": 4, "y": 0, "w": 2, "h": 5, "i": "2" },
{ "x": 6, "y": 0, "w": 2, "h": 3, "i": "3" },
{ "x": 8, "y": 0, "w": 2, "h": 3, "i": "4" },
{ "x": 10, "y": 0, "w": 2, "h": 3, "i": "5" },
{ "x": 0, "y": 5, "w": 2, "h": 5, "i": "6" },
{ "x": 2, "y": 5, "w": 2, "h": 5, "i": "7" },
{ "x": 4, "y": 5, "w": 2, "h": 5, "i": "8" },
{ "x": 6, "y": 3, "w": 2, "h": 4, "i": "9" },
{ "x": 8, "y": 4, "w": 2, "h": 4, "i": "10" }
])
</script>
style:加了一个关闭图标
.close {
display: inline-block;
height: 16px;
width: 16px;
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
color: #fff;
}
3)效果:
3、从外部拖入元素
从布局外部拖入组件,并使用component标签和异步加载组件方法defineAsyncComponent动态引入组件,完整代码如下
<template>
<div class="container">
<!-- 自定义布局的部分 -->
<div class="grid-box" >
<grid-layout ref="gridLayout" v-model:layout="layout" :col-num="12" :row-height="30" :is-draggable="true" :is-resizable="true" :is-mirrored="false" :vertical-compact="true" :margin="[10, 10]" :use-css-transforms="true">
<grid-item ref="gridItem" @resized="resizedEvent" v-for="item in layout" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" :key="item.i">
<span class=" close" @click="delItem(item)"><i class="sky-iconfont icon-guanbi"></i></span>
<component :is="item.loadComp" />
</grid-item>
</grid-layout>
</div>
<!-- 可拖入的组件部分 -->
<div class="components-box">
<div class="ctrl-box" v-for="item in componentsInfo" :key="item.id" @drag="drag" @dragstart="dragstart($event, item)" @dragend="dragend" draggable="true">
{{item.des}}
</div>
</div>
</div>
</template>
<script setup >
import { onMounted, ref, getCurrentInstance, defineAsyncComponent } from 'vue'
const { proxy } = getCurrentInstance()
const layout = ref([])
const colNum = 12
let defaultH = 2
let defaultW = 2
let mouseXY = {
x: null,
y: null
}
let DragPos = {
x: null,
y: null,
w: null,
h: null,
i: null
}
const componentsInfo = [
{
id: '1-1',
title: '图表-年度统计仪表盘',
name: 'annualOutput',
component: '/components/Output1',
des: '图表-年度统计仪表盘',
w: 4,
h: 8
},
{
id: '1-2',
title: '图表-月度统计折线图',
name: 'MonthOutput',
component: '/components/Output2',
des: '图表-月度统计折线图',
w: 4,
h: 8
},
{
id: '1-3',
title: '图表-日统计柱状图',
name: 'dayOutput',
component: '/components/Output3',
des: '图表-日统计柱状图',
w: 4,
h: 8
}
]
let currentDragCom = null
onMounted(() => {
document.addEventListener('dragover', (e) => {
e.preventDefault()
mouseXY.x = e.clientX;
mouseXY.y = e.clientY;
}, false);
document.addEventListener('dragenter', function (event) {
// 阻止默认行为
event.preventDefault()
});
processLayout(layout.value)
})
// 处理布局数据中的组件
const processLayout = (layoutSetInfo) => {
for (let i = 0; i < layoutSetInfo.length; i++) {
let item = layoutSetInfo[i]
if (!item.component) {
continue
}
let resComp = loadComponent(item.component.component)
item.loadComp = resComp
}
}
// 引入组件
const loadComponent = (path) => {
return defineAsyncComponent(() =>
import(`@/${path}`)
)
}
const dragstart = (e, item) => {
e.dataTransfer.effectAllowed = 'move'
currentDragCom = item
defaultH = item.h
defaultW = item.w
}
const drag = (e, item) => {
e.preventDefault && e.preventDefault()
let parentRect = document.querySelector('.grid-box').getBoundingClientRect();
let mouseInGrid = false;
if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) {
mouseInGrid = true;
}
if (mouseInGrid === true && (layout.value.findIndex(item => item.i === 'drop')) === -1) {
layout.value.push({
x: (layout.value.length * 2) % (colNum || 12),
y: layout.value.length + (colNum || 12),
w: defaultW,
h: defaultH,
i: 'drop',
});
}
let index = layout.value.findIndex(item => item.i === 'drop');
if (index !== -1) {
try {
proxy.$refs.gridItem[layout.value.length - 1].$refs.item.style.display = "none";
} catch {
}
let el = proxy.$refs.gridItem[index];
if (el) {
el.dragging = { "top": mouseXY.y - parentRect.top, "left": mouseXY.x - parentRect.left };
let new_pos = el && el.calcXY(mouseXY.y - parentRect.top, mouseXY.x - parentRect.left);
if (mouseInGrid === true) {
// function dragEvent(eventName, id, x, y, h, w)
proxy.$refs.gridLayout.dragEvent('dragstart', 'drop', new_pos.x || 0, new_pos.y || 0, defaultH, defaultW);
DragPos.i = String(new Date().getTime());
DragPos.x = layout.value[index].x;
DragPos.y = layout.value[index].y;
}
if (mouseInGrid === false) {
proxy.$refs.gridLayout.dragEvent('dragend', 'drop', new_pos.x || 0, new_pos.y || 0, defaultH, defaultW);
layout.value = layout.value.filter(obj => obj.i !== 'drop');
}
}
}
}
const dragend = (e) => {
let parentRect = document.querySelector('.grid-box').getBoundingClientRect();
let mouseInGrid = false;
if (((mouseXY.x > parentRect.left) && (mouseXY.x < parentRect.right)) && ((mouseXY.y > parentRect.top) && (mouseXY.y < parentRect.bottom))) {
mouseInGrid = true;
}
if (mouseInGrid === true) {
proxy.$refs.gridLayout.dragEvent('dragend', 'drop', DragPos.x, DragPos.y, defaultH, defaultW);
let delIndex = layout.value.findIndex(item => item.i === 'drop')
layout.value.splice(delIndex, 1)
let loadComp = loadComponent(currentDragCom.component)
layout.value.push({
x: DragPos.x,
y: DragPos.y,
w: currentDragCom.w,
h: currentDragCom.h,
i: DragPos.i,
component: currentDragCom,
loadComp: loadComp
});
proxy.$refs.gridLayout.dragEvent('dragend', DragPos.i, DragPos.x, DragPos.y, currentDragCom.h, currentDragCom.w);
try {
proxy.$refs.gridItem[layout.value.length].$refs.item.style.display = "block";
} catch {
}
}
}
// 尺寸变更后,触发resize事件,使图表resize
const resizedEvent = e => {
if (document.createEvent) {
let ev = new Event('resize')
window.dispatchEvent(ev)
} else if (document.createEventObject) {
window.fireEvent('onresize')
}
}
// 删除item
const delItem = (item) => {
let delIndex = layout.value.findIndex(el => el.i === item.i)
layout.value.splice(delIndex, 1)
}
</script>
<style scoped lang="scss">
.container {
position: relative;
}
.grid-box {
width: calc(100% - 320px);
height: 100%;
}
.components-box {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 300px;
border: 1px solid rgba(66, 66, 66, 1);
padding: 12px 20px;
.ctrl-box {
height: 40px;
padding: 0 12px;
color: #fff;
display: flex;
align-items: center;
background: #2d2d2c;
border: 1px solid rgba(66, 66, 66, 1);
margin-top: 20px;
user-select: none;
-webkit-user-select: none;
}
}
</style>
<style lang="scss">
.vue-grid-layout {
height: 100% !important;
background: transparent;
border: 1px solid rgba(66, 66, 66, 1);
overflow: auto;
.vue-grid-item {
background: #2d2d2c;
border: 1px solid rgba(66, 66, 66, 1);
border-radius: 2px;
padding: 12px 20px;
}
.close {
display: inline-block;
height: 16px;
width: 16px;
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
color: rgba(255, 255, 255, 0.6);
i {
font-size: 20px;
}
&:hover {
color: #fff;
}
}
.vue-grid-item.vue-resizable.vue-grid-placeholder {
background: white !important;
}
}
</style>
4、解决布局紧缩的bug
完成上述工作之后,发现问题:删除元素时,下方元素没有自动上移,找到源码运行起来发现,vue3版本的vue-grid-layout确实有这个问题,所以把源码中的关于紧缩布局的代码拿过来改造了一点点,加入进来,解决问题。
在删除元素的方法中调用紧缩布局方法compact
// 删除item
const delItem = (item) => {
let delIndex = layout.value.findIndex(el => el.i === item.i)
layout.value.splice(delIndex, 1)
compact(layout.value)
}
// 处理布局
const compact = (layoutdata) => {
const compareWith = []
const sorted = sortLayoutItemsByRowCol(layoutdata);
const out = Array(sorted.length)
for (let i = 0, len = sorted.length; i < len; i++) {
let l = sorted[i]
l = compactItem(compareWith, l)
// 加入对比数组,检测冲突时比较其中的元素
compareWith.push(l);
// 放入输出的数组,维持原来的顺序
out[layoutdata.indexOf(l)] = l;
// Clear moved flag, if it exists.
// l.moved = false;
}
return out
}
// 根据行列对元素重新排列
const sortLayoutItemsByRowCol = (layoutdata) => {
return [].concat(layoutdata).sort(function (a, b) {
if (a.y === b.y && a.x === b.x) {
return 0;
}
if (a.y > b.y || (a.y === b.y && a.x > b.x)) {
return 1;
}
return -1;
});
}
// 判断每一个元素的冲突情况,无冲突则y=0,有冲突则y=冲突项的y+冲突项的h
const compactItem = (compareWith, item) => {
while (item.y > 0 && !getFirstCollision(compareWith, item)) {
item.y--;
}
let collides;
while ((collides = getFirstCollision(compareWith, item))) {
item.y = collides.y + collides.h;
}
return item
}
// 找到第一个冲突的元素
const getFirstCollision = (compareWith, item) => {
for (let i = 0, len = compareWith.length; i < len; i++) {
if (collides(compareWith[i], item)) return compareWith[i];
}
}
// 判断两个元素是否有冲突重叠
const collides = (item1, item2) => {
if (item1 === item2) return false; // 同一个item
if (item1.x + item1.w <= item2.x) return false; // item1 在 item2左边
if (item1.x >= item2.x + item2.w) return false; // item1 在 item2右边
if (item1.y + item1.h <= item2.y) return false; // item1 在 item2上边
if (item1.y >= item2.y + item2.h) return false; // item1 在 item2下边
return true; // 其他情况则是有冲突重叠
}