改进菜单栏动态展示样式,我被评上优秀开发!

简介: 【8月更文挑战第24天】改进菜单栏动态展示样式,我被评上优秀开发!

背景

我们公司的导航菜单是动态可配置的,有的页面菜单数量比较多,有的比较少。
image.png
由于大多页面菜单都是比较少的,因此当菜单非常多时, 我们采用了朴实无华的滚动条:当横向超出的时候,滚动展示。
但很快,客户就打回来了:说我们的样式太丑,居然用滚动条!还质问我们产品这合理吗?产品斩钉截铁的告诉客户,我让开发去优化...
于是,领导让我们想解决方案。(我真谢谢产品)
很快,我想到一个方案(从其他地方看到的交互),我告诉领导:
我们可以做成动态菜单栏,如果展示不下了,出现一个更多按钮,多余的菜单都放到更多里面去:
444444.gif
领导说这个想法不错啊,那就你来实现吧!
好家伙,我只是随便说说,没想到,自己给自己挖了个大坑啊!
不过,我最后也是顺利的完成了这个效果的开发,还被评上了本季度优秀开发!分享一下自己的实现方案吧!

技术方案

基础组件样式开发

既然要开发这个效果,干脆就封装一个通用组件AdaptiveMenuBar.vue吧。我们先写一下基本样式,如图,灰色区域就是我们的组件内容,也就是我们菜单栏动态展示的区域。
image.png
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这个父元素里面了。
image.png

实现思路

那我们要如何才能让多余的菜单出现在【更多】按钮里呢?原理很简单,我们只要计算出哪个菜单超出展示区域即可。假设如图所示,第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>

截断位置的计算

要计算截断位置,我们需要先渲染好菜单。
image.png
然后开始对menu-item元素宽度进行加和,当相加的宽度大于菜单展示区域的宽度clientWidth时,计算终止,此时的menu-item元素就是我们要截断的位置。
image.png
菜单截断的部分,我们此时放到更多里面展示就可以了。
大致代码如下:

<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
    // 获取menu-item元素dom的集合
    const menuItemNodeList = menuWrapDom.querySelectorAll('.menu-item');
    // 将NodeList转换成数组
    const nodeArray = Array.prototype.slice.call(menuItemNodeList);
    let addWidth = 0;
    for (let i = 0; i < nodeArray.length; i++) {
        const node = nodeArray[i];
        // clientWidth不包含菜单的margin边距,因此我们手动补上12px
        addWidth += node.clientWidth + 12;
        // 76是更多按钮的宽度,我们也要计算进去
        if (addWidth + 76 > middleDom.clientWidth) {
            sliceIndex.value = i;
            break;
        } else {
            sliceIndex.value = 0;
        }
      }

});
</script>

样式重整

当被截断的元素计算完毕时,我们需要重新进行样式渲染,但是注意,我们原先渲染的菜单列不能注销,因为每次浏览器尺寸变化时,我们都是基于原先渲染的菜单列进行计算的。
所以,我们实际需要渲染两个菜单列,一个原始的,一个样式重新排布后的。
image.png
如上图,黄色就是原始的菜单栏,用于计算重新排布的菜单栏,只不过,我们永远不在页面上展示给用户看!

<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;
        // 将NodeList转换成数组
        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>

代码效果

可以看到,非常丝滑!
444444.gif

相关文章
|
JavaScript
【Vue3从零开始-实战】S16:详情页样式优化及tab栏内容联动功能实现
【Vue3从零开始-实战】S16:详情页样式优化及tab栏内容联动功能实现
291 0
【Vue3从零开始-实战】S16:详情页样式优化及tab栏内容联动功能实现
|
1月前
|
JavaScript
avaScript如何实现 选项卡功能
avaScript如何实现 选项卡功能
19 1
|
1月前
|
前端开发
前端如何制作简易的菜单多级导航栏
前端如何制作简易的菜单多级导航栏
40 0
|
3月前
|
数据可视化 前端开发 JavaScript
可视化图表与源代码显示配置项及页面的动态调整功能分析
本篇文章对可视化图表与源代码显示配置项及页面的动态调整进行了一个详细的功能分析,我将文章内容分为四个部分(分析图表源代码;分析源代码显示功能;分析源代码显示及动态调整;分析代码编辑器及运行效果显示)。对此,我会一一为大家解释代码的结构,功能的组成;且文章出现的所有代码,为了方便小白也能够读懂,我都做了详细的注释
52 0
可视化图表与源代码显示配置项及页面的动态调整功能分析
|
6月前
【最全最详细】使用publiccms实现动态可维护的导航菜单栏
【最全最详细】使用publiccms实现动态可维护的导航菜单栏
|
6月前
若依框架---如何实现翻页保留选择?如何调整首页左侧菜单栏宽度?
若依框架---如何实现翻页保留选择?如何调整首页左侧菜单栏宽度?
257 3
|
JavaScript 前端开发
24EasyUI 数据网格- 创建复杂工具栏
24EasyUI 数据网格- 创建复杂工具栏
47 0
|
前端开发 数据库 容器
ivx页面(4)下拉菜单的页面
ivx页面(4)下拉菜单的页面
93 0
Echarts实战案例代码(24):柱图数据顶部显示图片的解决方案
Echarts实战案例代码(24):柱图数据顶部显示图片的解决方案
304 0
Echarts实战案例代码(24):柱图数据顶部显示图片的解决方案
|
小程序 算法 前端开发
小程序之移花宫-自定义底部标签图标---【浅入深出系列005】
小程序之移花宫-自定义底部标签图标---【浅入深出系列005】