如何实现两个联动的react grid layout?支持跨布局拖拽等交互
实现双联动React Grid Layout方案
核心思路
基于react-grid-layout的基础布局能力,结合HTML5原生拖拽API实现跨布局元素迁移,通过状态管理维护左右布局的元素数据,同时添加可拖动分隔线实现布局宽度自由调整。
关键实现步骤
1. 状态管理
- 分别维护
leftItems和rightItems数组,存储左右布局的元素数据(包含唯一ID、内容、宽高参数) - 用
leftWidth变量控制左侧布局宽度,支持分隔线拖动调整
2. 左侧布局配置
- 设置
resizeHandles={[]},完全禁止元素调整尺寸 - 给每个元素添加
draggable="true"属性,在onDragStart事件中将元素数据存入dataTransfer,供右侧布局接收 - 布局项的
data-grid中设置y: Infinity,让元素自动向下排列,无需手动管理位置
3. 右侧布局配置
- 开启
resizeHandles={['se', 'sw', 'ne', 'nw']},允许元素从四个方向调整尺寸 - 监听
onDragOver和onDrop事件处理跨布局拖拽:onDragOver需阻止默认行为,设置dropEffect为move以激活放置状态onDrop中解析拖拽数据,从左侧数组移除对应元素,同时添加到右侧数组(注意给右侧元素ID加前缀避免冲突)
- 通过
onLayoutChange监听右侧元素的位置/尺寸变化,可用于持久化布局状态(如存入本地存储或后端)
4. 可拖动分隔线
- 用窄div作为分隔线,设置
cursor: col-resize提示可拖动 - 监听
mousedown事件,在鼠标移动时计算并更新左侧布局宽度,同时限制宽度范围(10%-90%)避免布局异常
完整代码示例
import React, { useState, useRef } from 'react'; import { Responsive, WidthProvider } from 'react-grid-layout'; const ResponsiveGridLayout = WidthProvider(Responsive); const DualGrid = () => { // 左侧初始元素数据 const [leftItems, setLeftItems] = useState([ { id: 'item1', content: '左侧元素1', w: 2, h: 1 }, { id: 'item2', content: '左侧元素2', w: 2, h: 1 }, { id: 'item3', content: '左侧元素3', w: 2, h: 1 }, ]); // 右侧初始元素数据 const [rightItems, setRightItems] = useState([ { id: 'right-item4', content: '右侧元素1', w: 3, h: 2 }, ]); // 左侧布局宽度 const [leftWidth, setLeftWidth] = useState('40%'); const separatorRef = useRef(null); const containerRef = useRef(null); // 左侧元素拖拽开始事件 const handleDragStart = (e, item) => { e.dataTransfer.setData('text/plain', JSON.stringify(item)); e.dataTransfer.effectAllowed = 'move'; }; // 右侧布局拖拽进入事件 const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; // 右侧布局放置事件 const handleDrop = (e) => { e.preventDefault(); const draggedItem = JSON.parse(e.dataTransfer.getData('text/plain')); // 从左侧移除元素 setLeftItems(prev => prev.filter(item => item.id !== draggedItem.id)); // 添加到右侧,ID加前缀避免冲突 setRightItems(prev => [...prev, { ...draggedItem, id: `right-${draggedItem.id}` }]); }; // 分隔线拖动调整宽度 const handleSeparatorMouseDown = (e) => { e.preventDefault(); const containerRect = containerRef.current.getBoundingClientRect(); const startX = e.clientX; const startLeftWidth = parseFloat(leftWidth); const handleMouseMove = (moveEvent) => { const deltaX = moveEvent.clientX - startX; const newWidth = ((startLeftWidth / 100) * containerRect.width + deltaX) / containerRect.width * 100; // 限制宽度范围,避免布局异常 if (newWidth > 10 && newWidth < 90) { setLeftWidth(`${newWidth}%`); } }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }; // 右侧布局变化回调 const handleRightLayoutChange = (newLayout) => { // 可在此处将布局状态存入localStorage或后端 console.log('右侧布局更新:', newLayout); }; return ( <div ref={containerRef} style={{ display: 'flex', height: '100vh', gap: '0' }}> {/* 左侧布局容器 */} <div style={{ width: leftWidth, overflow: 'auto' }}> <ResponsiveGridLayout className="layout" breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} cols={{ lg: 4, md: 3, sm: 2, xs: 1, xxs: 1 }} rowHeight={60} resizeHandles={[]} // 禁止调整尺寸 > {leftItems.map(item => ( <div key={item.id} data-grid={{ w: item.w, h: item.h, x: 0, y: Infinity }} className="grid-item" draggable="true" onDragStart={(e) => handleDragStart(e, item)} style={{ backgroundColor: '#f0f0f0' }} > <div className="grid-item-content">{item.content}</div> </div> ))} </ResponsiveGridLayout> </div> {/* 分隔线 */} <div ref={separatorRef} style={{ width: '2px', backgroundColor: '#ccc', cursor: 'col-resize', alignSelf: 'stretch', }} onMouseDown={handleSeparatorMouseDown} /> {/* 右侧布局容器 */} <div style={{ flex: 1, overflow: 'auto' }} onDragOver={handleDragOver} onDrop={handleDrop}> <ResponsiveGridLayout className="layout" breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} cols={{ lg: 6, md: 4, sm: 3, xs: 2, xxs: 1 }} rowHeight={60} resizeHandles={['se', 'sw', 'ne', 'nw']} // 允许四向调整尺寸 onLayoutChange={handleRightLayoutChange} > {rightItems.map(item => ( <div key={item.id} data-grid={{ w: item.w, h: item.h, x: 0, y: Infinity }} className="grid-item" style={{ backgroundColor: '#e0e0e0' }} > <div className="grid-item-content">{item.content}</div> </div> ))} </ResponsiveGridLayout> </div> </div> ); }; // 基础样式注入 const styles = ` .layout { padding: 10px; } .grid-item { padding: 10px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); } .grid-item-content { height: 100%; display: flex; align-items: center; justify-content: center; } `; const styleSheet = document.createElement("style"); styleSheet.textContent = styles; document.head.appendChild(styleSheet); export default DualGrid;
注意事项
- 元素ID必须唯一,拖拽到右侧后建议添加前缀避免冲突
- 若需要精确控制元素在右侧布局的放置位置,可在
onDrop中计算鼠标位置对应的网格坐标,修改元素的x和y值 - 可根据需求调整布局的
breakpoints和cols配置,适配不同屏幕尺寸 - 若需要持久化布局状态,可在
onLayoutChange中将布局数据存入本地存储或后端
内容的提问来源于stack exchange,提问作者change world
相关产品推荐
相关产品推荐

