需求
M*N矩阵中有大小相同的格子,实现单元格的行/列的合并与拆分,如下图:
实现方法
可以通过Antd Table表格行/列合并实现功能,但是对需求来说使用table有点重,探索用更轻量的方法实现。
调研发现,CSS Grid网格布局引入了二维网格布局系统,不熟悉的同学可以先学习下Grid布局,二维网格布局系统对合并单元格来说有天然的优势。
组件入参
javascript
/*
参数 说明 类型
row 行数 Number
col 列数 Number
dataSource 外部数据源 Object[]
cellStyle 单个单元格样式 Object{}
cellRender 单元格渲染函数 Function(cellItem)
onChange 单元格发生变化后触发 Function(allCells)
*/
export default function CellMerger({
row=3,
col=3,
cellStyle={
width: "100px",
height: "100px",
gap: "2px",
},
cellRender,
dataSource,
onChange,
}) {
......
}
画矩阵
以3*3为例,首先需要生成9个网格
ini
// 定义数组存储单元格信息
const [grids, setGrids] = useState([]);
// 初始化单元格数据。有外部传入直接使用,没有传入使用row、col自动生成
useEffect(() => {
if (dataSource) {
setGrids(dataSource)
} else {
generateGridData();
}
}, []);
// 使用row、col自动生成
const generateGridData = () => {
const data = [];
for (let i = 1; i <= row; i++) {
for (let j = 1; j <= col; j++) {
data.push({
id: generateUniqueId(),
gridRowStart: i,
gridRowEnd: i + 1,
gridColumnStart: j,
gridColumnEnd: j + 1,
});
}
}
onGridChange(data);
};
// 唯一ID
const generateUniqueId = () => {
const randomNumber = Math.random().toString(36).substring(2, 5);
const timestamp = Date.now().toString(36);
return randomNumber + timestamp;
};
渲染矩阵
根据单元格数据循环渲染单元格
css
const renderGridItem = (item) => {
return (
<div
key={item.id}
className={styles.gridItem}
style={{
/* 单元格位置 */
gridRowStart: item.gridRowStart,
gridRowEnd: item.gridRowEnd,
gridColumnStart: item.gridColumnStart,
gridColumnEnd: item.gridColumnEnd,
}}
>
<div className={styles.gridContainer}>
<div className={styles.gridContent}>
{/* 用户自定义渲染 */}
{cellRender(item)}
</div>
{/* 渲染当前单元格是否显示四个方向合并按钮 */}
{renderLinkButton(item)}
{/* 渲染当前单元格是否显示拆分按钮 */}
{renderUnlinkButton(item)}
</div>
</div>
);
};
return (
<div>
<div
className={styles.gridContainer}
style={{
/* 声明行的高度 */
gridTemplateRows: `repeat(${row}, ${cellStyle.height}`,
/* 声明列的宽度 */
gridTemplateColumns: `repeat(${col}, ${cellStyle.width})`,
/* 设置格子之间的间隔 */
gap: cellStyle.gap || "2px",
}}
>
{grids.map((item) => {
return renderGridItem(item);
})}
</div>
</div>
);
判断单元格是否显示四个方向合并按钮
显然不是所有单元四个方向都是可合并的,那在什么情况下才显示合并按钮呢?
- 不在边的单元格;
- 横向合并要保证横向相邻单元格的高度一致;
- 竖向合并要保证竖向相邻单元格的宽度一致;
ini
const renderLinkButton = (curGrid) => {
let doms = [];
let enableLeftBtn = false;
for (let item of grids) {
// 判断左边可不可以合并,元素必须和当当前元素高度一致并首尾相连
if (
item.gridRowStart === curGrid.gridRowStart &&
item.gridRowEnd === curGrid.gridRowEnd &&
item.gridColumnEnd === curGrid.gridColumnStart
) {
enableLeftBtn = true;
}
// 边上按钮不显示
if (curGrid.gridColumnStart <= 1) {
enableLeftBtn = false;
}
// 同理其他三个方向
.....
}
if (enableLeftBtn) {
doms.push(
<button
key={`${curGrid.id}-left`}
className={`${styles.leftButton} ${styles.hoverBtn}`}
// 处理合并事件
onClick={() => mergeCell(DIRECTION_TYPES.LEFT, curGrid)}
>
+
</button>
);
}
}
/* Css部分 */
.gridItem:hover {
.hoverBtn {
display: block;
}
}
.hoverBtn {
display: none;
}
注意合并按钮只有hover时才显示。
处理合并事件
以左合并为例:
- 去除row相同 & col相同的数据(当前元素);
- 去除row相同 & 左边col的数据(被合并的元素),并记录该元素左边位置;
- 添加新合并后单元格:row保持不变,col从被合并元素左边开始到当前元素的右边;
ini
const mergeCell = (direction, curGrid) => {
let resGrid = [];
// 向左合并: 去除row相同 & 左边col的数据,去除row相同 & col相同的数据
if (direction === DIRECTION_TYPES.LEFT) {
let start = null;
for (let item of grids) {
if ( // 去除当前元素
item.gridRowStart === curGrid.gridRowStart &&
item.gridRowEnd === curGrid.gridRowEnd &&
item.gridColumnEnd === curGrid.gridColumnEnd &&
item.gridColumnStart === curGrid.gridColumnStart
) {
continue;
} else if ( // 去除row相同 & 左边col的数据,并记录起始点
item.gridRowStart === curGrid.gridRowStart &&
item.gridRowEnd === curGrid.gridRowEnd &&
item.gridColumnEnd === curGrid.gridColumnStart
) {
start = item.gridColumnStart;
continue;
} else {
resGrid.push(item);
}
}
// 添加合并后的新格子
resGrid.push({
id: generateUniqueId(),
gridRowStart: curGrid.gridRowStart,
gridRowEnd: curGrid.gridRowEnd,
gridColumnStart: start,
gridColumnEnd: curGrid.gridColumnEnd,
});
}
// 其他三个方向同理
onGridChange(resGrid);
};
// 单元格数据变化,需要触发回调
const onGridChange = (grids) => {
setGrids(grids);
onChange(grids);
};
判断是否显示拆分按钮
是否可以拆分只需要判断当前单元格的长宽是否大于1即可。
ini
const renderUnlinkButton = (curGrid) => {
if (
curGrid.gridColumnEnd - curGrid.gridColumnStart > 1 ||
curGrid.gridRowEnd - curGrid.gridRowStart > 1
) {
return (
<button
className={`${styles.unlinkBtn} ${styles.hoverBtn}`}
onClick={() => unlinkCell(curGrid)}
>
解除连接
</button>
);
}
return null;
};
处理拆分事件
- 去除row相同 & col相同的数据(当前元素);
- 遍历当前元素row和col,添加单元格
ini
const unlinkCell = (curGrid) => {
let resGrid = [];
for (let item of grids) {
if (
item.gridRowStart === curGrid.gridRowStart &&
item.gridRowEnd === curGrid.gridRowEnd &&
item.gridColumnEnd === curGrid.gridColumnEnd &&
item.gridColumnStart === curGrid.gridColumnStart
) {
continue;
} else {
resGrid.push(item);
}
}
for (let i = curGrid.gridRowStart; i < curGrid.gridRowEnd; i++) {
for (let j = curGrid.gridColumnStart; j < curGrid.gridColumnEnd; j++) {
resGrid.push({
id: generateUniqueId(),
gridRowStart: i,
gridRowEnd: i + 1,
gridColumnStart: j,
gridColumnEnd: j + 1,
});
}
}
// 通知组件更新
onGridChange(resGrid);
};