包阅导读总结
1. 动态菜单栏、滚动条、更多按钮、样式优化、优秀开发
2. 本文讲述公司导航菜单动态可配置,因样式问题需优化。作者提出动态菜单栏方案,即超出展示区域出现更多按钮,计算截断位置和重整样式,最终实现效果并被评为优秀开发,还分享了完整代码。
3.
– 背景
– 公司导航菜单动态可配置,页面菜单数量不一,多页采用滚动条展示,客户不满意。
– 方案提出
– 作者提出动态菜单栏方案,出现更多按钮来收纳多余菜单。
– 技术实现
– 开发通用组件AdaptiveMenuBar.vue,写基础样式和假数据。
– 实现更多按钮展示逻辑,计算截断位置。
– 重整样式,渲染两个菜单列。
– 完善基础功能,响应resize事件重新计算样式,给出完整代码和效果展示。
思维导图:
文章地址:https://juejin.cn/post/7384256110280802356
文章来源:juejin.cn
作者:石小石Orz
发布时间:2024/6/25 13:47
语言:中文
总字数:2893字
预计阅读时间:12分钟
评分:88分
标签:前端,CSS,Vue.js
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
精彩新文章:拿客户电脑,半小时完成轮播组件开发!被公司奖励500
背景
我们公司的导航菜单是动态可配置的,有的页面菜单数量比较多,有的比较少。
由于大多页面菜单都是比较少的,因此当菜单非常多时, 我们采用了朴实无华的滚动条:当横向超出的时候,滚动展示。
但很快,客户就打回来了:说我们的样式太丑,居然用滚动条!还质问我们产品这合理吗?产品斩钉截铁的告诉客户,我让开发去优化…
于是,领导让我们想解决方案。(我真谢谢产品!
)
很快,我想到一个方案(从其他地方看到的交互),我告诉领导:
我们可以做成动态菜单栏,如果展示不下了,出现一个更多按钮,多余的菜单都放到更多里面去:
领导说这个想法不错啊,那就你来实现吧!
好家伙,我只是随便说说,没想到,自己给自己挖了个大坑啊!
不过,我最后也是顺利的完成了这个效果的开发,还被评上了本季度优秀开发!分享一下自己的实现方案吧!
技术方案
基础组件样式开发
既然要开发这个效果,干脆就封装一个通用组件AdaptiveMenuBar.vue吧。我们先写一下基本样式,如图,灰色区域就是我们的组件内容,也就是我们菜单栏动态展示
的区域。
AdaptiveMenuBar.vue
<template> <div class="adaptive-menu-bar"> </div></template><style lang="less" scoped>.adaptive-menu-bar { width: 100%; height: 48px; background: gainsboro; display: flex; position: relative; overflow: hidden;}</style>
我们写点假数据
<template> <div class="adaptive-menu-bar"> <div class="origin-menu-item-wrap"> <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item"> {{ item.name }} </div> </div> <div>更多</div> </div></template><script setup>const menuOriginData = [ { name: '哆啦a梦', id: 1 }, { name: '宇智波佐助', id: 1 }, { name: '香蕉之王奥德彪', id: 1 }, { name: '漩涡鸣人', id: 1 }, { name: '雏田', id: 1 }, { name: '大雄', id: 1 }, { name: '源静香', id: 1 }, { name: '骨川小夫', id: 1 }, { name: '超级马里奥', id: 1 }, { name: '自来也', id: 1 }, { name: '孙悟空', id: 1 }, { name: '卡卡罗特', id: 1 }, { name: '万年老二贝吉塔', id: 1 }, { name: '小泽玛丽', id: 1 }];</script><style lang="less" scoped>.adaptive-menu-bar { width: 100%; height: 48px; background: gainsboro; display: flex; position: relative; overflow: hidden; .origin-menu-item-wrap{ width: 100%; display: flex; }}</style>
如图,由于菜单数量比较多,一部分已经隐藏在origin-menu-item-wrap
这个父元素里面了。
实现思路
那我们要如何才能让多余的菜单出现在【更多】按钮里呢?原理很简单,我们只要计算出哪个菜单超出展示区域即可。假设如图所示,第12个菜单被截断了,那我们前11个菜单就可以展示在显示区域,剩余的菜单就展示在【更多】按钮里。
更多按钮的展示逻辑
更多按钮只有在展示区域空间不够的时候出现,也就是origin-menu-item-wrap元素的滚动区域宽度scrollWidth 大于其宽度clientWidth的时候。
用代码展示大致如下
<template> <div ref="menuBarRef" class="origin-menu-item-wrap"> <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item"> <m-button type="default" size="small">{{ item.name }}</m-button> </div> </div></template><script setup>const menuOriginData = [ { name: '哆啦a梦', id: 1 }, { name: '宇智波佐助', id: 1 }, { name: '香蕉之王奥德彪', id: 1 }, { name: '漩涡鸣人', id: 1 }, { name: '雏田', id: 1 }, { name: '大雄', id: 1 }, { name: '源静香', id: 1 }, { name: '骨川小夫', id: 1 }, { name: '超级马里奥', id: 1 }, { name: '自来也', id: 1 }, { name: '孙悟空', id: 1 }, { name: '卡卡罗特', id: 1 }, { name: '万年老二贝吉塔', id: 1 }, { name: '小泽玛丽', id: 1 }];const showMoreBtn = ref(false);onMounted(() => { const menuWrapDom = menuBarRef.value; if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) { showMoreBtn.value = true; }});</script>
截断位置的计算
要计算截断位置,我们需要先渲染好菜单。
然后开始对menu-item元素宽度进行加和,当相加的宽度大于菜单展示区域的宽度clientWidth时,计算终止,此时的menu-item元素就是我们要截断的位置。
菜单截断的部分,我们此时放到更多里面展示就可以了。
大致代码如下:
<template> <div ref="menuBarRef" class="origin-menu-item-wrap"> <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item"> <m-button type="default" size="small">{{ item.name }}</m-button> </div> </div></template><script setup>const menuOriginData = [ { name: '哆啦a梦', id: 1 }, { name: '宇智波佐助', id: 1 }, { name: '香蕉之王奥德彪', id: 1 }, { name: '漩涡鸣人', id: 1 }, { name: '雏田', id: 1 }, { name: '大雄', id: 1 }, { name: '源静香', id: 1 }, { name: '骨川小夫', id: 1 }, { name: '超级马里奥', id: 1 }, { name: '自来也', id: 1 }, { name: '孙悟空', id: 1 }, { name: '卡卡罗特', id: 1 }, { name: '万年老二贝吉塔', id: 1 }, { name: '小泽玛丽', id: 1 }];const showMoreBtn = ref(false);onMounted(() => { const menuWrapDom = menuBarRef.value; if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) { showMoreBtn.value = true; } let sliceIndex = 0 const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item'); const nodeArray = Array.prototype.slice.call(menuItemNodeList); let addWidth = 0; for (let i = 0; i < nodeArray.length; i++) { const node = nodeArray[i]; addWidth += node.clientWidth + 12; if (addWidth + 76 > menuWrapDom.clientWidth) { sliceIndex.value = i; break; } else { sliceIndex.value = 0; } } });</script>
样式重整
当被截断的元素计算完毕时,我们需要重新进行样式渲染,但是注意,我们原先渲染的菜单列不能注销,因为每次浏览器尺寸变化时,我们都是基于原先渲染的菜单列进行计算的。
所以,我们实际需要渲染两个菜单列:一个原始的,一个样式重新排布后的。
如上图,黄色就是原始的菜单栏,用于计算重新排布的菜单栏,只不过,我们永远不在页面上展示给用户看!
<template> <div class="adaptive-menu-bar"> <div ref="menuBarRef" class="origin-menu-item-wrap"> <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item"> <m-button type="default" size="small">{{ item.name }}</m-button> </div> </div> <div v-for="(item, index) in menuList" :key="index" class="menu-item"> <m-button type="default" size="small">{{ item.name }}</m-button> </div> <div >更多</div> </div></template>
代码实现
基础功能完善
为了我们的菜单栏能动态的响应变化,我们需要再每次resize事件触发时,都重新计算样式
const menuOriginData = [ { name: '哆啦a梦', id: 1 }, { name: '宇智波佐助', id: 1 }, { name: '香蕉之王奥德彪', id: 1 }, { name: '漩涡鸣人', id: 1 }, { name: '雏田', id: 1 }, { name: '大雄', id: 1 }, { name: '源静香', id: 1 }, { name: '骨川小夫', id: 1 }, { name: '超级马里奥', id: 1 }, { name: '自来也', id: 1 }, { name: '孙悟空', id: 1 }, { name: '卡卡罗特', id: 1 }, { name: '万年老二贝吉塔', id: 1 }, { name: '小泽玛丽', id: 1 }];const showMoreBtn = ref(false);const setHeaderStyle = () => { }window.addEventListener('resize', () => setHeaderStyle());onMounted(() => { setHeaderStyle();});</script>
完整代码
完整代码剥离了一些第三方UI组件,便于大家理解。
<template> <div class="adaptive-menu-bar"> <div ref="menuBarRef" class="origin-menu-item-wrap"> <div v-for="(item, index) in menuOriginData" :key="index" class="menu-item"> {{ item.name }} </div> </div> <div v-for="(item, index) in menuList" :key="index" class="menu-item"> {{ item.name }} </div> <div v-if="showMoreBtn" class="dropdown-wrap"> <span>更多</span> <div class="menu-item-wrap"> <div v-for="(item, index) in menuOriginData.slice(menuList.length)" :key="index">{{ item.name }}</div> </div> </div> </div></template>
<script setup>import { IconMeriComponentArrowDown } from 'meri-icon';const menuBarRef = ref();const open = ref(false);const menuOriginData = [ { name: '哆啦a梦', id: 1 }, { name: '宇智波佐助', id: 1 }, { name: '香蕉之王奥德彪', id: 1 }, { name: '漩涡鸣人', id: 1 }, { name: '雏田', id: 1 }, { name: '大雄', id: 1 }, { name: '源静香', id: 1 }, { name: '骨川小夫', id: 1 }, { name: '超级马里奥', id: 1 }, { name: '自来也', id: 1 }, { name: '孙悟空', id: 1 }, { name: '卡卡罗特', id: 1 }, { name: '万年老二贝吉塔', id: 1 }, { name: '小泽玛丽', id: 1 }];const menuList = ref(menuOriginData);const showMoreBtn = ref(false);const setHeaderStyle = () => { const menuWrapDom = menuBarRef.value; if (!menuWrapDom) return; if (menuWrapDom.scrollWidth > menuWrapDom.clientWidth) { showMoreBtn.value = true; } else { showMoreBtn.value = false; } const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item'); if (menuItemNodeList) { let addWidth = 0, sliceIndex = 0; const nodeArray = Array.prototype.slice.call(menuItemNodeList); for (let i = 0; i < nodeArray.length; i++) { const node = nodeArray[i]; addWidth += node.clientWidth + 12; if (addWidth + 64 + 12 > menuWrapDom.clientWidth) { sliceIndex = i; break; } else { sliceIndex = 0; } } if (sliceIndex > 0) { menuList.value = menuOriginData.slice(0, sliceIndex); } else { menuList.value = menuOriginData; } }};window.addEventListener('resize', () => setHeaderStyle());onMounted(() => { setHeaderStyle();});</script>
<style lang="less" scoped>.adaptive-menu-bar { width: 100%; height: 48px; background: gainsboro; display: flex; position: relative; align-items: center; overflow: hidden; .origin-menu-item-wrap { width: 100%; display: flex; position: absolute; top: 49px; display: flex; align-items: center; left: 0; right: 0; bottom: 0; height: 48px; z-index: 9; } .menu-item { margin-left: 12px; } .dropdown-wrap { width: 64px; display: flex; align-items: center; cursor: pointer; justify-content: center; height: 28px; background: #fff; border-radius: 4px; overflow: hidden; border: 1px solid #c4c9cf; background: #fff; margin-left: 12px; .icon { width: 16px; height: 16px; margin-left: 4px; } }}</style>
代码效果
可以看到,非常丝滑!