首页 前端知识 基于vue3和vue-grid-layout实现自定义布局和动态渲染组件

基于vue3和vue-grid-layout实现自定义布局和动态渲染组件

2024-09-10 23:09:23 前端知识 前端哥 599 264 我要收藏

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; // 其他情况则是有冲突重叠
}

转载请注明出处或者链接地址:https://www.qianduange.cn//article/18121.html
标签
评论
发布的文章

jQuery 选择器

2024-05-12 00:05:34

cdn引入前端插件

2024-10-13 20:10:14

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!