背景:项目中本来使用的是multi-columns多列布局,但是后来随着业务的增加,布局中的元素的高度不是固定的了,而是增加了一个Antd的Collapse的展开折叠操作。这种情况下,使用纵向的multi-columns布局就使得元素高度变化比较大时,就会随着高度自动重排,有时会换列,这样的话操作复杂性就很高,不利于用户使用,故修改为横向瀑布流的方式。以下对两种方式进行一个简单的介绍和记录。
一、 multi-columns多栏布局---纵向瀑布流
1. 优点:
纯css写法,省时省力
2. 缺点:
排列顺序只能从上到下,再从左到右。只能用于数据固定, 无法动态的加载追加,对于滚动到底部加载新数据则无法实现。
3. 两个重要属性:
column-count : 定义列数
column-gap :列与列之间的间隔
4. 注意点:
有时候页面会出现在前几列的最后一个元素的内容被自动断开,一部分在当前列尾,一部分在下一列的列头。这时候子元素可以用 break-inside设置为不被截断 avoid来控制。
break-inside: avoid; // 不被截断 默认值是auto,会被截断
5. 实现代码:
.card{
column-count: 3; // 定义三列
column-gap: 20px; // 列与列的距离为20px
.card-item{
text-align: center;
width: 216px;
border-radius: 16px;
grid-row-start: auto;
margin-bottom: 20px;
break-inside: avoid; // 不被截断
}
}
二、 横向瀑布流---泳道设计
1. 优点:
横向布局,更符合使用习惯
2. 缺点:
实现复杂度较高,元素较多时,计算量较大,需要考虑重复渲染的问题
3. 实现思路:
用flex弹性布局+计算元素高度实现布局。第一列元素排列之后,每次元素的追加都选择当前高度最小的那一列。
4. 算法思路:通过上面的分析则能了解瀑布流的思路了
- 设计要分成的列数
- 设置每列的 宽度一致
- 每次插入的位置选择所有列高度最小 的位置,依次循环
5. 代码实现思路:
- 由于是使用Collapse折叠结构,则计算时直接利用其子元素的个数来进行高度比较
- 多少列则定义多少新的空数组,然后根据瀑布流的思路依次插入到空的数组即可
6. 代码实现
结构:
<div className={styles.flexLeftList}>
<div className={styles.columnItem}>
{generateDOM(columnListLeft.current)}
</div>
<div className={styles.columnItem}>
{generateDOM(columnListCenter.current)}
</div>
<div className={styles.columnItem}>
{generateDOM(columnListRight.current)}
</div>
</div>
const generateDOM = (list) => (
list.map((item) => {
return (
<div className={styles.module} key={item.id} id={item.id}>
{/* 二级菜单名称 */}
<div className={`${styles.title}>
{item.name}
</div>
{/* 三级菜单 */}
{item?.children?.map((childItem) => {
return (
<div style={{display: 'block'}}>
<Item
type={ItemType}
item={childItem}
key={childItem.id}
/>
</div>
);
})}
</div>
);
})
)
js:
// 计算flex布局每列的数据项
const caLFlex = (mList) => {
if(!mList.length) return;
let arrLeft: IMenuItem[] = []; // 第一列的数据
let arrCenter: IMenuItem[] = []; // 第二列的数据
let arrRight: IMenuItem[] = []; // 第二列的数据
let countArray_left: number[] = []; // 根据数量判断高度
let countArray_center: number[] = [];
let countArray_right: number[] = [];
mList.forEach((item,index) =>{
if(index === 0){ // 第一行中的元素无需判断,直接加到新的数组中
countArray_left.push(item?.children?.length ?? 0);
arrLeft.push(item);
return;
}
if(index === 1){
countArray_center.push(item?.children?.length ?? 0);
arrCenter.push(item);
return;
}
if(index === 2){
countArray_right.push(item?.children?.length ?? 0);
arrRight.push(item);
return;
}
// 累计各行元素数目
const countTotal_left = countArray_left.length ? Array.from(countArray_left).reduce(( acc, cur ) => acc + cur) : 0; // 第一列的总个数
const countTotal_center = countArray_center.length ? Array.from(countArray_center).reduce(( acc, cur ) => acc + cur) : 0; // 第二列的总个数
const countTotal_right = countArray_right.length ? Array.from(countArray_right).reduce(( acc, cur ) => acc + cur) : 0; // 第三列的总个数
// 找到最小值
let minNumber = Math.min(countTotal_left,countTotal_center,countTotal_right);
switch (minNumber) {
case countTotal_left:
countArray_left.push(item?.children?.length ?? 0);
arrLeft.push(item);
break;
case countTotal_center:
countArray_center.push(item?.children?.length ?? 0);
arrCenter.push(item);
break;
case countTotal_right:
countArray_right.push(item?.children?.length ?? 0);
arrRight.push(item);
break;
}
});
// 重新将数据赋值给各列数组
columnListLeft.current = arrLeft;
columnListCenter.current = arrCenter;
columnListRight.current = arrRight;
};
css:
// flex横向布局
.flex-left-list {
width: 100%;
min-height: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
.column-item{
width: calc(100% / 3);
.module{
display: flex;
flex-direction: column;
height: fit-content;
padding: 0 16px 30px 16px;
.title {
font-size: 16px;
color: rgba(0,0,0,0.85);
margin-bottom: 10px;
padding-left: 14px;
}
}
}
}
参考链接:https://blog.csdn.net/Smile_666666/article/details/124728121 (大哥讲解更详细,有需要可以去看,同时多谢大哥提供思路~)