基于 vue-grid-layout 实现的可拖拽网格布局组件
# Vue Grid Layout 组件 基于 `vue-grid-layout` 实现的可拖拽网格布局组件。 ## 文件说明 | 文件 | 说明 |
|------|------|
| `grid.vue` | 独立演示组件,包含完整的拖拽功能 |
| `gridLayout.vue` | 可复用组件,支持禁用模式,需父组件控制拖拽逻辑 | ### grid.vue ``` Displayed as [x, y, w, h]: {{ item.i }}</b >: [{{ item.x }}, {{ item.y }}, {{ item.w }}, {{ item.h }}] <div @drag="drag" @dragend="dragend" class="droppable-element" draggable="true" unselectable="on" > Droppable Element (Drag me!) <grid-layout ref="gridlayout" v-model:layout="layout" :col-num="state.colCount" :row-height="state.rowHeight" :maxRows="state.rowCount" :margin="state.margin" :is-draggable="true" :is-resizable="true" :vertical-compact="true" :use-css-transforms="true" > <grid-item :key="item.i" v-for="item in layout" ref="gridItems" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" > {{ item.i }} x import { ref, onMounted, onUnmounted } from 'vue' interface GridItem { x: number y: number w: number h: number i: string
} interface State { colCount: number rowCount: number rowHeight: number margin: number[]
} const state = ref({ colCount: 24, rowHeight: 10, rowCount: 24, margin: [10, 10]
}) let mouseXY: any = { x: null, y: null }
let DragPos: any = { x: null, y: null, w: 1, h: 1, i: null } // refs
const content = ref()
const gridlayout = ref()
const gridItems = ref([]) const layout = ref([ { x: 0, y: 0, w: 2, h: 6, i: '0' }, { x: 2, y: 0, w: 2, h: 6, i: '1' }, { x: 4, y: 0, w: 2, h: 8, i: '2' }, { x: 6, y: 0, w: 2, h: 10, i: '3' }, { x: 8, y: 0, w: 2, h: 12, i: '4' }, { x: 10, y: 0, w: 2, h: 4, i: '5' }, { x: 0, y: 5, w: 2, h: 6, i: '6' }, { x: 2, y: 5, w: 2, h: 8, i: '7' }, { x: 4, y: 5, w: 2, h: 12, i: '8' }, { x: 5, y: 10, w: 4, h: 4, i: '9' }
]) const setRowHeight = () => { const el = content.value if (!el) return state.value.rowHeight = el.clientHeight / state.value.rowCount - state.value.margin[1]
} onMounted(() => { setRowHeight() window.addEventListener('resize', setRowHeight) window.addEventListener('orientationchange', setRowHeight)
}) onUnmounted(() => { window.removeEventListener('resize', setRowHeight) window.removeEventListener('orientationchange', setRowHeight)
}) onMounted(() => { document.addEventListener( 'dragover', function (e) { mouseXY.x = e.clientX mouseXY.y = e.clientY }, false )
}) const removeItem = (val) => { const index = layout.value.map((item) => item.i).indexOf(val) layout.value.splice(index, 1)
} // 拖拽开始
const drag = (e) => { let parentRect = document.getElementById('content')!.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) % state.value.colCount, y: layout.value.length + state.value.colCount, // puts it at the bottom w: 8, h: 8, i: 'drop' }) } let index = layout.value.findIndex((item) => item.i === 'drop') if (index !== -1) { try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'none' } catch {} let el: any = gridItems.value[index] el.dragging = { top: mouseXY.y - parentRect.top, left: mouseXY.x - parentRect.left } let new_pos = el.calcXY( mouseXY.y - parentRect.top, mouseXY.x - parentRect.left ) if (mouseInGrid === true) { gridlayout.value.dragEvent( 'dragstart', 'drop', new_pos.x, new_pos.y, 8, 8 ) DragPos.i = String(index) DragPos.x = layout.value[index].x DragPos.y = layout.value[index].y } if (mouseInGrid === false) { gridlayout.value.dragEvent('dragend', 'drop', new_pos.x, new_pos.y, 1, 1) layout.value = layout.value.filter((obj) => obj.i !== 'drop') } }
} // 拖放结束
const dragend = (e) => { let parentRect = document.getElementById('content')!.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) { // alert( // `Dropped element props:\n${JSON.stringify( // DragPos, // ['x', 'y', 'w', 'h'], // 2 // )}` // ) gridlayout.value.dragEvent('dragend', 'drop', DragPos.x, DragPos.y, 1, 1) layout.value = layout.value.filter((obj) => obj.i !== 'drop') // UNCOMMENT below if you want to add a grid-item layout.value.push({ x: DragPos.x, y: DragPos.y, w: 8, h: 8, i: DragPos.i }) gridlayout.value.dragEvent('dragend', DragPos.i, DragPos.x, DragPos.y, 1, 1) try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'block' } catch {} }
} .droppable-element { width: 150px; text-align: center; background: #fdd; border: 1px solid black; margin: 10px 0; padding: 10px;
} .layoutJSON { background: #ddd; border: 1px solid black; margin-top: 10px; padding: 10px;
} .columns { -moz-columns: 120px; -webkit-columns: 120px; columns: 120px;
} /*************************************/ #content { width: 60vw; height: 60vh; background-color: #eee;
} .remove { position: absolute; right: 2px; top: 0; cursor: pointer;
} .vue-grid-layout { background: #eee;
} .vue-grid-item:not(.vue-grid-placeholder) { background: #ccc; border: 1px solid black;
} .vue-grid-item .resizing { opacity: 0.9;
} .vue-grid-item .static { background: #cce;
} .vue-grid-item .text { font-size: 24px; text-align: center; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; height: 100%; width: 100%;
} .vue-grid-item .no-drag { height: 100%; width: 100%;
} .vue-grid-item .minMax { font-size: 12px;
} .vue-grid-item .add { cursor: pointer;
} .vue-draggable-handle { position: absolute; width: 20px; height: 20px; top: 0; left: 0; background: url("data:image/svg+xml;utf8,") no-repeat; background-position: bottom right; padding: 0 8px 8px 0; background-repeat: no-repeat; background-origin: content-box; box-sizing: border-box; cursor: pointer;
} :deep(.vue-grid-item.vue-grid-placeholder) { background: red !important;
} ``` ### gridLayout.vue ``` <!-- <div @drag="drag" @dragend="dragend" class="droppable-element" draggable="true" unselectable="on" > Droppable Element (Drag me!) --> <grid-layout ref="gridlayout" v-model:layout="layout" :col-num="state.colCount" :row-height="state.rowHeight" :maxRows="state.rowCount" :margin="state.margin" :is-draggable="true" :is-resizable="true" :vertical-compact="true" :use-css-transforms="true" v-bind="disabledAttrs" > <grid-item :key="item.i" v-for="item in layout" ref="gridItems" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" > {{ item.i }} <span v-if="!disabled" class="remove" @click="removeItem(item.i)" >x</span > import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' interface GridItem { x: number y: number w: number h: number i: string
} interface State { colCount: number rowCount: number rowHeight: number margin: number[]
} const props = withDefaults( defineProps<{ disabled?: boolean }>(), { disabled: false }
) const disabledAttrs = computed(() => props.disabled ? { isDraggable: false, isResizable: false } : {}
) const state = ref({ colCount: 24, rowHeight: 10, rowCount: 24, margin: [10, 10]
}) let mouseXY: any = { x: null, y: null }
let DragPos: any = { x: null, y: null, w: 24, h: 8, i: null } // refs
const content = ref()
const gridlayout = ref()
const gridItems = ref([]) const layout = ref([ { x: 0, y: 0, w: 24, h: 4, i: '0' }, { x: 0, y: 0, w: 12, h: 6, i: '1' }, { x: 12, y: 0, w: 12, h: 8, i: '2' }
]) const setRowHeight = () => { const el = content.value if (!el) return state.value.rowHeight = el.clientHeight / state.value.rowCount - state.value.margin[1]
} onMounted(() => { setRowHeight() window.addEventListener('resize', setRowHeight) window.addEventListener('orientationchange', setRowHeight)
}) onUnmounted(() => { window.removeEventListener('resize', setRowHeight) window.removeEventListener('orientationchange', setRowHeight)
}) onMounted(() => { document.addEventListener( 'dragover', function (e) { mouseXY.x = e.clientX mouseXY.y = e.clientY }, false )
}) const removeItem = (val) => { const index = layout.value.map((item) => item.i).indexOf(val) layout.value.splice(index, 1)
} // 拖拽开始
const drag = (e) => { let parentRect = content.value!.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) % state.value.colCount, y: layout.value.length + state.value.colCount, // puts it at the bottom w: DragPos.w, h: DragPos.h, i: 'drop' }) } let index = layout.value.findIndex((item) => item.i === 'drop') nextTick(() => { if (index !== -1) { try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'none' } catch {} let el: any = gridItems.value[index] el.dragging = { top: mouseXY.y - parentRect.top, left: mouseXY.x - parentRect.left } let new_pos = el.calcXY( mouseXY.y - parentRect.top, mouseXY.x - parentRect.left ) if (mouseInGrid === true) { gridlayout.value.dragEvent( 'dragstart', 'drop', new_pos.x, new_pos.y, DragPos.h, DragPos.w ) DragPos.i = String(index) DragPos.x = layout.value[index].x DragPos.y = layout.value[index].y } if (mouseInGrid === false) { gridlayout.value.dragEvent( 'dragend', 'drop', new_pos.x, new_pos.y, DragPos.h, DragPos.w ) layout.value = layout.value.filter((obj) => obj.i !== 'drop') } } })
} // 拖放结束
const dragend = (e) => { let parentRect = content.value!.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) { gridlayout.value.dragEvent( 'dragend', 'drop', DragPos.x, DragPos.y, DragPos.h, DragPos.w ) layout.value = layout.value.filter((obj) => obj.i !== 'drop') // UNCOMMENT below if you want to add a grid-item layout.value.push({ x: DragPos.x, y: DragPos.y, w: DragPos.w, h: DragPos.h, i: DragPos.i }) try { ;( gridItems.value[layout.value.length - 1] as any ).$refs.item.style.display = 'block' } catch {} }
} defineExpose({ drag, dragend
}) .grid-layout { width: 60vw; height: 60vh; background-color: #eee;
} .remove { position: absolute; right: 2px; top: 0; cursor: pointer;
} .vue-grid-layout { // background: #eee;
} .vue-grid-item:not(.vue-grid-placeholder) { background: #ccc; border: 1px solid black;
} .vue-grid-item .resizing { opacity: 0.9;
} .vue-grid-item .static { background: #cce;
} .vue-grid-item .text { font-size: 24px; text-align: center; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; height: 100%; width: 100%;
} .vue-grid-item .no-drag { height: 100%; width: 100%;
} .vue-grid-item .minMax { font-size: 12px;
} .vue-grid-item .add { cursor: pointer;
} .vue-draggable-handle { position: absolute; width: 20px; height: 20px; top: 0; left: 0; background: url("data:image/svg+xml;utf8,") no-repeat; background-position: bottom right; padding: 0 8px 8px 0; background-repeat: no-repeat; background-origin: content-box; box-sizing: border-box; cursor: pointer;
} :deep(.vue-grid-item.vue-grid-placeholder) { background: red !important;
} ``` ## 功能特性 - 24x24 网格系统
- 支持从外部拖拽元素到网格
- 网格项可拖拽重排
- 网格项可调整大小
- 支持删除网格项
- 响应式行高自适应
- 支持禁用模式(只读) ## 依赖 - vue 3
- vue-grid-layout ## 使用方式 ### grid.vue(独立演示) 直接作为页面组件使用,包含完整的拖拽演示功能。 ### gridLayout.vue(可复用组件) 父组件通过 ref 引用调用暴露的 `drag` 和 `dragend` 方法控制拖拽行为: ```vue <div @drag="handleDrag" @dragend="handleDragEnd" class="droppable-element" draggable="true" > 拖拽我到网格中 import { ref } from 'vue'
import GridLayout from './gridLayout.vue' const gridRef = ref()
const isReadonly = ref(false) // 拖拽过程中调用
const handleDrag = (e: DragEvent) => { gridRef.value?.drag(e)
} // 拖拽结束时调用
const handleDragEnd = (e: DragEvent) => { gridRef.value?.dragend(e)
} .droppable-element { width: 150px; text-align: center; background: #fdd; border: 1px solid black; margin: 10px 0; padding: 10px; cursor: grab;
} ``` **控制流程:** ```
父组件拖拽元素 → @drag/@dragend 事件 → gridRef.value.drag(e)/dragend(e) → 子组件处理布局
``` ## Props | 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| disabled | boolean | false | 禁用拖拽和调整大小 | ## 暴露方法 | 方法 | 说明 |
|------|------|
| drag(e) | 处理拖拽过程 |
| dragend(e) | 处理拖拽结束 |