react固定容器标签超出n+展示
·
思路:
-
容器宽度监听
-
需要知道当前容器的宽度。
-
可以用
ResizeObserver来实时监听容器大小变化。
-
-
计算可展示的标签数量
-
遍历标签,测量每个
Tag占的宽度(包括 margin/padding)。 -
累计标签宽度,直到超出容器最大宽度。
-
一旦超出,就停止,记录下来能展示的数量。
-
-
预留空间给
+n标签-
因为可能会有超出的情况,需要给
+n预留宽度(比如 40px 左右)。
-
-
渲染逻辑
-
如果标签总数 ≤ 可展示数量 → 全部渲染。
-
如果标签总数 > 可展示数量 → 渲染前 N 个,剩余的合并为
+n。 -
+n上可以挂载Tooltip或Popover,用于展示完整的隐藏标签。
-
关键点:
-
如何测量单个标签的宽度?
常见做法是临时创建一个隐藏的 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>
更多推荐


所有评论(0)