思路:
  • 容器宽度监听

    • 需要知道当前容器的宽度。

    • 可以用 ResizeObserver 来实时监听容器大小变化。

  • 计算可展示的标签数量

    • 遍历标签,测量每个 Tag 占的宽度(包括 margin/padding)。

    • 累计标签宽度,直到超出容器最大宽度。

    • 一旦超出,就停止,记录下来能展示的数量。

  • 预留空间给 +n 标签

    • 因为可能会有超出的情况,需要给 +n 预留宽度(比如 40px 左右)。

  • 渲染逻辑

    • 如果标签总数 ≤ 可展示数量 → 全部渲染。

    • 如果标签总数 > 可展示数量 → 渲染前 N 个,剩余的合并为 +n

    • +n 上可以挂载 TooltipPopover,用于展示完整的隐藏标签。

关键点:
  • 如何测量单个标签的宽度?
    常见做法是临时创建一个隐藏的 DOM 元素,把标签内容放进去测量 clientWidth
    也可以直接渲染在页面中,用 getBoundingClientRect() 获取。

  • 如何保证响应式?
    ResizeObserver 会在容器大小变化时重新计算,不需要手动监听 window.onresize

  • 性能优化

    • 标签数如果很多,可以提前缓存测量结果。

    • 只在标签数据或容器宽度变化时重新计算。

初始化 → 监听容器宽度 → 计算可展示数量
       ↓
 遍历标签 → 累计宽度 → 超出则停止
       ↓
   渲染:前 N 个标签 + (超出的 +n 标签)

版本一代码:
import React, { useRef, useState, useEffect } from "react";
import { Tag, Tooltip } from "antd";

// 标签列表组件
const TagList = ({ data = [] }) => {
  const containerRef = useRef(null);
  const [visibleCount, setVisibleCount] = useState(data.length);

  useEffect(() => {
    if (!containerRef.current) return;

    const resizeObserver = new ResizeObserver(() => {
      calcVisibleCount();
    });

    resizeObserver.observe(containerRef.current);

    return () => {
      resizeObserver.disconnect();
    };
  }, [data]);

  const calcVisibleCount = () => {
    const container = containerRef.current;
    if (!container) return;

    const containerWidth = container.clientWidth;
    let total = 0;
    let count = data.length;

    // 临时创建元素测量宽度
    const temp = document.createElement("div");
    temp.style.position = "absolute";
    temp.style.visibility = "hidden";
    temp.style.whiteSpace = "nowrap";
    document.body.appendChild(temp);

    for (let i = 0; i < data.length; i++) {
      temp.innerHTML = `<span class="ant-tag">${data[i]}</span>`;
      const w = temp.clientWidth + 8; // 8px = margin-right 近似
      if (total + w > containerWidth - 40) {
        // 40 给 +N 预留宽度
        count = i;
        break;
      }
      total += w;
    }

    document.body.removeChild(temp);
    setVisibleCount(count);
  };

  const showTags = data.slice(0, visibleCount);
  const hiddenTags = data.slice(visibleCount);

  return (
    <div
      ref={containerRef}
      style={{ width: "100%", overflow: "hidden", whiteSpace: "nowrap" }}
    >
      {showTags.map((item, index) => (
        <Tag key={index}>{item}</Tag>
      ))}

      {hiddenTags.length > 0 && (
        <Tooltip
          title={
            <div style={{ display: "flex", flexWrap: "wrap", gap: 4 }}>
              {hiddenTags.map((item, idx) => (
                <Tag key={idx}>{item}</Tag>
              ))}
            </div>
          }
        >
          <Tag>+{hiddenTags.length}</Tag>
        </Tooltip>
      )}
    </div>
  );
};

export default TagList;
版本二(优化后的):
import React, { useRef, useState, useEffect } from "react";
import { Tag, Tooltip } from "antd";
import SDIcon from "Components/SDIcon";
import s from "./style.module.scss";

// 标签列表组件
const TagList = ({ data = [], label = "label", value = "value" }) => {
  const containerRef = useRef(null);
  const [visibleCount, setVisibleCount] = useState(data.length);

  useEffect(() => {
    if (!containerRef.current) return;

    const resizeObserver = new ResizeObserver(() => {
      debounce(calcVisibleCount(), 16);
    });

    resizeObserver.observe(containerRef.current);

    return () => {
      resizeObserver.disconnect();
    };
  }, [data]);

  // 工具:单例离屏画布,用来测文本像素宽度
  const calcWidth = (() => {
    let canvas = null;
    return (
      text,
      font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto'
    ) => {
      if (!canvas) {
        canvas = document.createElement("canvas");
        canvas.context2d = canvas.getContext("2d");
      }
      canvas.context2d.font = font;
      return canvas.context2d.measureText(text).width + 8; // 8 = 右边距
    };
  })();

  const calcVisibleCount = () => {
    const container = containerRef.current;
    if (!container) return;
    const containerWidth = container.clientWidth;

    let low = 0,
      high = data.length;
    while (low < high) {
      const mid = (low + high + 1) >> 1;
      const total = data
        .slice(0, mid)
        .reduce((sum, d) => sum + calcWidth(`${d[label]}:${d[value]}`), 0);
      if (total <= containerWidth - 40) low = mid; // 40 留给 +N
      else high = mid - 1;
    }
    setVisibleCount(low);
  };

  const showTags = data.slice(0, visibleCount);
  const hiddenTags = data.slice(visibleCount);

  return (
    <div
      ref={containerRef}
      style={{ width: "100%", overflow: "hidden", whiteSpace: "nowrap" }}
    >
      {showTags.map((item, index) => (
        <Tag key={index} color="rgba(28, 28, 35, 0.06)">
          <div className={s.tagItem}>
            {item[label]}:{item[value]}
          </div>
        </Tag>
      ))}

      {hiddenTags.length > 0 && (
        <Tooltip
          color="rgba(255, 255, 255, 1)"
          placement="bottom"
          arrow={false}
          overlayStyle={{ maxWidth: 500 }}
          title={
            <div
              style={{
                display: "flex",
                flexWrap: "wrap",
                columnGap: 6, // 左右
                rowGap: 12,
              }}
            >
              {hiddenTags.map((item, idx) => (
                <Tag
                  key={idx}
                  color="rgba(28, 28, 35, 0.06)"
                  style={{
                    whiteSpace: "normal",
                    wordBreak: "break-all",
                    height: "auto",
                    lineHeight: "22px",
                  }}
                >
                  <div className={s.tagItem}>
                    {item[label]}:{item[value]}
                  </div>
                </Tag>
              ))}
            </div>
          }
          overlayInnerStyle={{
            maxWidth: 500, // 把默认 250px 覆盖掉
            padding: "16px", // 内边距
            borderRadius: 8,
            maxHeight: 500,
            overflowY: "auto",
          }}
        >
          <Tag color="rgba(28, 28, 35, 0.06)">
            <div className={s.tagMore}>
              <span>+{hiddenTags.length}</span>
              <SDIcon
                style={{
                  width: 10,
                  height: 10,
                  color: "rgba(28, 28, 35, 0.45)",
                }}
                type="icon-you"
              />
            </div>
          </Tag>
        </Tooltip>
      )}
    </div>
  );
};

export default TagList;

/* ---------- 防抖工具 ---------- */
function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

css代码(优化后的):

.tagItem {
  color: rgba(28, 28, 35, 0.85);
}
.tagMore {
  display: flex;
  align-items: center;
  column-gap: 4px;
  color: rgba(28, 28, 35, 0.85);
}
使用:
      <div
        style={{
          width: 300,
          maxWidth: 300,
        }}
      >
        <TagList data={tags} />
      </div>

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐