让你的面包屑自动响应适配

921 阅读2分钟

最近遇到一个业务需求,需要面包屑不换行展示;如果展示不下,则只展示第一个和最后两个面包屑,其余收至 ... 省略号按钮并悬浮展示

接到需求后先翻翻项目使用的蚂蚁组件能否满足需求,然而并不能; 接着搜索 responsive breadcrumbdynamic breadcrumb 也没找到符合要求的组件,所以只能自己写一个了

cry.jpeg

思考流程

静下来想了下思路 🤔 :本质是要通过 面包屑宽度父容器宽度 决定展示策略

假设遇到以下场景:

desc-1.png 父容器为灰色背景部分,面包屑列表比父容器长,一行放不下,按照咱们的需求此时应该只展示第一个和最后两个面包屑,其余省略展示,即:

desc-2.png 最终效果为:

desc-3.png

对应的流程为:

logic_flow.png

相关代码

流程捋顺了,咱们来写计算面包屑展示的方法:

    // 省略号按钮的宽度
    const MORE_BTN_WIDTH = 40
    
    /**
     * items: 每个面包屑宽度的数据列表
     * parentWidth: 父容器宽度
     */
    function calcBreadcrumbDisplay(items, parentWidth) {
        // 计算面包屑总宽度
        const totalWidth = items.reduce((acc, curr) => acc + curr.width, 0)
        if (totalWidth <= parentWidth) return items
        
        // 不超过 3 个面包屑,直接返回面包屑宽度的计算结果
        if (items.length <= 3) return calcItemWidth(items, totalWidth, parentWidth)
        
        // 分出展示组和剩余组
        const firstOne = items.shift()
        const lastOne = item.pop()
        const lastTwo = item.pop()
        const displayItems = [firstOne, lastTwo, lastOne]
        const displayWidth = displayItems.reduce((acc, curr) => acc + curr.width, 0)
        
        // 如果展示组加上省略按钮不能在父容器内完整展示
        if (displayWidth + MORE_BTN_WIDTH >= parentWidth) {
            // 添加 omit 标记,组件内会根据该标记将其收至省略列表
            items.forEach(item => (item.omit = true))
            calcItemWidth(displayItems, totalWidth, parentWidth - MORE_BTN_WIDTH)
            return [firstOne, ...items, lastTwo, lastOne]
        }
        
        // 父容器剩余的宽度
        const parentRemindWidth = parentWidth - displayWidth - MORE_BTN_WIDTH
        // 减去展示区宽度后,面包屑整体剩余的宽度
        let breadcrumbRestWidth = totalWidth - displayWidth
        // 计算剩余组哪些面包屑需要省略,哪些可以展示
        for (const item of items) {
            breadcrumbRestWidth -= item.width
            item.omit = true
            // 如果父容器剩余的空间能放得下剩下的面包屑,则结束循环
            if (breadcrumbRestWidth <= parentRemindWidth) break
        }
        
        return [firstOne, ...items, lastTwo, lastOne]
    }

calcItemWidth 的逻辑如下:

    // 面包屑最小展示宽度
    const MIN_DISPLAY_WIDTH = 80

    /**
     * items: 每个面包屑宽度的数据列表
     * itemsWidth: 面包屑整体宽度
     * parentWidth: 父容器宽度
     */
    function calcItemWidth(items, itemsWidth, parentWidth) {
        for (const item of items) {
            // 计算面包屑能削减的宽度
            const reduceWidth = item.width - MIN_DISPLAY_WIDTH
            // 如果面包屑宽度小于最小宽度,忽略,跳过计算
            if (reduceWidth <= 0) continue

            // 计算此时面包屑列表与父容器相差的宽度
            const gap = itemsWidth - parentEleWidth

            // 如果列表与父容器相差的宽度比面包屑能削减的宽度大
            if (gap > reduceWidth) {
                // 添加 idealWidth 标记,组件内会根据该标记设置宽度样式
                item.idealWidth = MIN_DISPLAY_WIDTH
                itemsWidth -= reduceWidth
            } else { // 否则将面包屑 idealWidth 设为能满足列表摆放的最小宽度并结束循环
                item.idealWidth = item.width - gap
                break
            }
        }

        return items
    }

面包屑组件的逻辑是: 根据传进来的 items 渲染一个 cloneList 列表,获取每个面包屑的实际宽度,再将数据传入 calcBreadcrumbDisplay 获取计算后的数据,最后展示计算后的面包屑并隐藏 cloneList

大致代码如下:

   imoprt { useRef, useEffect, useState } from 'react'
   
   function Breadcrumb({ items }) {
       const breadcrumbRef = useRef(null)
       const cloneList = useRef(null)
       const [calcedBreadcrumbList, setCalcedBreadcrumbList] = useState([])
       
       useEffect(() => {
           // 展示 clone-list 列表
           cloneListRef.current.style.display = 'block'
           calcBreadcrumbList()
       }, [items])
       
       function calcBreadcrumbList() {
           const parent = breadcrumbRef.current.parentElement
           const children = cloneListRef.current.children
           // 返回新数组,防止操作数据导致 items 被同步更改
           const breadcrumbList = items.concat()
           let breadcrumbItemDatas = []

           for (let i = 0; i < children.length; i ++) {
               breadcrumbItemDatas.push({ width: children[i].clientWidth })
           }
           
           breadcrumbItemDatas = calcBreadcrumb(breadcrumbItemDatas, parent.clientWidth)
           
           // 如果有需要省略的面包屑
           if (breadcrumbItemDatas.some(item => item.omit)) {
               // 将需要省略的面包屑移进 breadcrumbList 的 omitList 里
           }
           
           setCalcedBreadcrumbList(breadcrumbList)
           // 隐藏 clone-list 列表
           cloneListRef.current.style.display = 'none'
       }
       
       return (
           <div ref={breadcrumbRef}>
               // 初次渲染时通过 clone-list 获取面包屑元素宽度信息
               // 在计算结束后隐藏 clone-list 列表
               <div className="clone-list" ref={cloneList}>
                   {items.map(item => (
                       <div className="item" key={item.key}>{item.name}</div>
                   ))}
               </div>
               {calcedBreadcrumbList.map(item => {
                   if (Array.isArray(item.omitList)) {
                       // 展示省略按钮和下拉省略列表
                   }
                   // 否则直接展示
                   return (
                       <div
                         className="item"
                         key={item.key}
                         style={item.idealWidth && { width: idealWidth }}
                       >
                         {item.name}
                       </div>
                   )
               })}
           </div>
       )
   }

使用效果

demo.png

good.png

npm 地址

对了,npm 地址 在这里,欢迎大家使用和反馈哦 😁