Posted in

改进菜单栏动态展示样式,我被评上优秀开发! – 掘金_AI阅读总结 — 包阅AI

包阅导读总结

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>

代码效果

可以看到,非常丝滑!