最近接到一个需求,要做一个数据面板,展示可视化数据,具体的布局大概是如下这样的

面板有8个模块,展示不同的可视化内容,布局横向平分,纵向,1、2、5、6高度为1x,3、4、7、8为2x,同时,这8个模块带有权限控制,用户会给分配1-8数量不等的权限。与需求方交流后大概了解到需求方的意图,同时给了以下几种可能出现的布局,以此类推。

可以看出,并不是一个非常常规的按序布局,大概含义是如果后面有可以插空到前面的,需要进行插空布局。
首先拿到需求,我们先看一下主要要处理的东西,我们能拿到的直观的是用户有权限的模块的逻辑顺序,比如用户有1、3、5、8的权限,或者有2、3、4、7、8的权限,那么这个直接形成数组就是用户的权限逻辑顺序。
从这个逻辑顺序,我们要能找到规律形成一个界面顺序的数组,比如需求里面第二个布局,我们先看行,1、3、4、5这个可以被当作一整行,而当我们放置元素时,按照顺序先放置1,然后放置3,这时候,因为放置3后右边的高度比左边高了,所以我们先找到下一个元素4补放到左边,然后再放置3,这时候左边比右边高了,我们找到下一个元素5,放置到右边,这时候左右一样高,当前行放置完成了。这时候另起一行,后面的6、7、8也是一样,我们先将6放置到左边,然后找到下一个7,试图放到右边,此时发现如果放置了7,右边就比左边高了,这时候我们找到下一个元素8补放到左边,放置结束。于是分析一下就找到了如下逻辑。
- 首先,当前行放置结束的标识是左右两边高度相等
- 任何新的一行时,先放置元素到左边,
- 然后开启一个while循环,只要左右高度不相等,而且没有将所有元素都放完的情况下,就持续循环
- 在这个循环中,判断接下来要放置的元素,如果放之前左边高那就往右放,如果右边高,就往左放
- 循环放置时,如果此时放的是左边的元素,直接放置就可以。如果放置的是右边的元素,此时需要判断一下,如果右边放完之后,右边高度不超过左边,那么直接放就ok;如果右边放置完高度超过左边,那么先停止放置,找到下一个元素(如果有下一个元素的话)放到左边,然后再放之前的元素到右边,之后因为这时候右边已经不会比左边高度高了,直接继续循环下一次
在这个过程中,如需求2被转化为1、4、3、5、6、8、7这样一个界面顺序,同时在我们进行上述逻辑的时候,往左放还是往右放,这时候会在界面顺序进行属性标记,增加一个isLeft属性,左边就是true,右边则为false;同时,还会有个属性size记录高度,比如1x的模块size=1,2x的模块size=2。
至此我们完成了从逻辑顺序到界面顺序的转变,得到了这个界面顺序数组。然后分析一下我们怎么从界面顺序形成真正的布局顺序呢?
思考一下布局,貌似左右切分为两个div,然后左边右边各有一个数组,我们从界面顺序数组里依次拿出来分别放到左边和右边各一个div就好了。但是这里有个问题,组件的逻辑关系是我们再写html时候就决定的了,不可能根据最后这两个界面数组再去反过来推算谁在前谁在后,除非动态注册子组件,但这未免也太复杂而且没有必要。
换一个角度思考,我们能不能用绝对定位来解决,8个模块div在html中还是1-8的顺序,我们通过给父容器一个position:relative,然后每个模块子容器position:absolute,然后在通过left和top进行绝对定位。
这样思考感觉是可以的,我们依旧定义两个数组leftModules,rightModules,从界面顺序数组里遍历分别往里面放置,isLeft:true的就放到leftModules且left:0,isLeft:false的就放到rightModules且left:‘50%’,至于top,根据放置的元素大小累加高度就可以了。比如上面说到的需求第二种布局的元素6就是left:0,top:3x;元素5就是left:‘50%’,top:2x(x是元素1的高度,比如x为50,3x为150px,2x为100px)。
由此就能完成需求要求的布局,当然上面的分析也不是一蹴而就思考得来,也是边写边分析,遇到问题解决,遇到情况进行归类得来。
如下为具体的代码实现,感兴趣的小伙伴可以到我的git项目
去查看,里面是完整代码实现。
首先所有模块数组:
mPermission: {
m1: { display: true, size: 1 },
m2: { display: true, size: 1 },
m3: { display: true, size: 2 },
m4: { display: true, size: 2 },
m5: { display: true, size: 1 },
m6: { display: true, size: 1 },
m7: { display: true, size: 2 },
m8: { display: true, size: 2 }
},
这里面display属性代表当前用户是否有权限查看这个模块,实际应用中我会从权限中获取到每一个,如果没有权限display置false,客官可以直接更改属性值,来查看布局效果。
通过如下代码后得到了逻辑顺序数组logicOrder
this.allModules.forEach(m => {
if (this.mPermission[m].display) {
this.logicOrder.push({ name: m, size: this.mPermission[m].size })
}
})
下面代码是逻辑顺序到界面顺序uiOrder的代码,这就是上面列出的1-5步骤的具体代码实现
let moduleCount = this.logicOrder.length // 总共的模块数
let locateNum = 0 // 已经放置的模块数
let finishCurrentRow = true // 已经填满了某一横行
while (locateNum < moduleCount) {
if (finishCurrentRow) { // 新的一行
let nextModule = this.locateNextModule()
this.uiOrder.push({ name: nextModule.name, size: nextModule.size, isLeft: true })
locateNum++
finishCurrentRow = false
} else { // 放右边元素
let leftSize = this.uiOrder[locateNum - 1].size
let rightSize = 0
while (locateNum < moduleCount && leftSize !== rightSize) { // 左右高度不相等时,进行放置,相等则完成此行放置
let nextModule = this.locateNextModule()
if (leftSize < rightSize) { // 左边放,直接放
this.uiOrder.push({ name: nextModule.name, size: nextModule.size, isLeft: true })
locateNum++
leftSize += nextModule.size
} else { // 右边放
if (leftSize >= rightSize + nextModule.size) { // 右边放完后若没有超出左边高度,直接放
this.uiOrder.push({ name: nextModule.name, size: nextModule.size, isLeft: false })
locateNum++
rightSize += nextModule.size
} else { // 右边放完后超出了左边高度,这时候先找到下一个放到左边,再放右边
if (locateNum + 1 < moduleCount) { // 如果有下一个元素才找,如果此时已经是最后一个元素那就不用找了
let nextLeftModule = this.locateNextModule(nextModule)
this.uiOrder.push({ name: nextLeftModule.name, size: nextLeftModule.size, isLeft: true })
locateNum++
leftSize += nextLeftModule.size
}
this.uiOrder.push({ name: nextModule.name, size: nextModule.size, isLeft: false })
locateNum++
rightSize += nextModule.size
}
}
}
finishCurrentRow = true
}
}
值得注意的是locateNextModule方法,传参与不传参有区别,正常查找不传参,直接能够定位到下一个元素,而当循环放置右边元素发现放置后右边高度超出时,这时找下一个元素补位到左边,就需要把当前即将放到右边的这个元素抛开去,于是把右边元素传入过滤掉
locateNextModule (exceptOne) { // exceptOne:需要额外排除,放右边之前需要先向左边放一个时
let nextModule = {}
for (let i = 0; i < this.logicOrder.length; i++) {
const isAlreadyLocated = this.uiOrder.find(uM => {
return uM.name === this.logicOrder[i].name
})
if (!isAlreadyLocated) {
if (exceptOne && exceptOne.name === this.logicOrder[i].name) {
continue
}
nextModule = this.logicOrder[i]
break
}
}
return nextModule
}
下面将界面顺序转化为布局顺序leftModules和rightModules,这里面我将1x的高度定位50,主要为了方便缩小浏览器截图显示效果,实际项目中不是这个高度
this.accLeftHeight = 0
this.accRightHeight = 0
this.uiOrder.forEach(uO => {
if (uO.isLeft) {
let leftModule = uO
leftModule.top = this.accLeftHeight
this.accLeftHeight += leftModule.size * 50
this.leftModules.push(leftModule)
} else {
let rightModule = uO
rightModule.top = this.accRightHeight
this.accRightHeight += rightModule.size * 50
this.rightModules.push(rightModule)
}
})
template的元素写了8个div,并且给了颜色和1-8的文字方便看效果,实际项目里是8个子组件引用进来,分别为1-8的可视化模块:
<div class="group-module">
<div v-if="isExist('m1')" :style="{position:'absolute',width:'50%',height:'50px','line-height':'50px','font-size':'30px',left:`${getLeft('m1')}`,top:`${getTop('m1')}px`,'background-color':'#556B2F'}">{{1}}</div>
<div v-if="isExist('m2')" :style="{position:'absolute',width:'50%',height:'50px','line-height':'50px','font-size':'30px',left:`${getLeft('m2')}`,top:`${getTop('m2')}px`,'background-color':'#FF8C00'}">{{2}}</div>
<div v-if="isExist('m3')" :style="{position:'absolute',width:'50%',height:'100px','line-height':'100px','font-size':'30px',left:`${getLeft('m3')}`,top:`${getTop('m3')}px`,'background-color':'#E9967A'}">{{3}}</div>
<div v-if="isExist('m4')" :style="{position:'absolute',width:'50%',height:'100px','line-height':'100px','font-size':'30px',left:`${getLeft('m4')}`,top:`${getTop('m4')}px`,'background-color':'#8FBC8B'}">{{4}}</div>
<div v-if="isExist('m5')" :style="{position:'absolute',width:'50%',height:'50px','line-height':'50px','font-size':'30px',left:`${getLeft('m5')}`,top:`${getTop('m5')}px`,'background-color':'#00CED1'}">{{5}}</div>
<div v-if="isExist('m6')" :style="{position:'absolute',width:'50%',height:'50px','line-height':'50px','font-size':'30px',left:`${getLeft('m6')}`,top:`${getTop('m6')}px`,'background-color':'#9400D3'}">{{6}}</div>
<div v-if="isExist('m7')" :style="{position:'absolute',width:'50%',height:'100px','line-height':'100px','font-size':'30px',left:`${getLeft('m7')}`,top:`${getTop('m7')}px`,'background-color':'#696969'}">{{7}}</div>
<div v-if="isExist('m8')" :style="{position:'absolute',width:'50%',height:'100px','line-height':'100px','font-size':'30px',left:`${getLeft('m8')}`,top:`${getTop('m8')}px`,'background-color':'#1E90FF'}">{{8}}</div>
</div>
其中isExist根据权限判断是否有当前模块,getLeft和getTop是从最终布局顺序数组中获取相应模块的位置。
尝试更改mPermission的display属性看一下需求给的几种布局是不是都实现了:
布局2:

布局3:

布局4:

完美!
本文介绍了一种用于动态调整数据面板布局的算法,该算法能够根据用户权限控制显示不同的可视化内容,确保即使在模块数量变化时,布局依然保持整齐有序。
569

被折叠的 条评论
为什么被折叠?



