|
1 | | -import React, { useState, useLayoutEffect, useRef } from 'react'; |
| 1 | +import React, { useMemo } from 'react'; |
2 | 2 | import { Tag } from 'antd'; |
| 3 | +import { useOverflowItems } from '@kne/overflow-items'; |
3 | 4 | import style from './style.module.scss'; |
4 | 5 |
|
| 6 | +const TAG_GAP = 8; |
| 7 | + |
5 | 8 | /** |
6 | | - * 标签溢出内部渲染组件 |
7 | | - * 根据 innerWidth 计算标签显示数量 |
| 9 | + * 标签溢出:用 @kne/overflow-items 按容器宽度计算可见数量,预留 +N。 |
8 | 10 | */ |
9 | | -const TagOverflowInner = ({ value, innerWidth, labelKey, valueKey, onRemove, overflowTagWidth = 45, iconSpace = 20 }) => { |
10 | | - const [visibleCount, setVisibleCount] = useState(value.length); |
11 | | - |
12 | | - const measureRef = useRef(null); |
13 | | - |
14 | | - // 测量每个标签的实际宽度并计算可见数量 |
15 | | - useLayoutEffect(() => { |
16 | | - // innerWidth 为 0 时,保持当前状态,等待正确的宽度 |
17 | | - if (!measureRef.current || innerWidth === 0 || value.length === 0) { |
18 | | - setVisibleCount(value.length); |
19 | | - return; |
20 | | - } |
21 | | - |
22 | | - const tagElements = measureRef.current.querySelectorAll('[data-tag-measure]'); |
23 | | - if (tagElements.length === 0) return; |
24 | | - |
25 | | - // 获取每个标签的实际占用宽度(包含 margin) |
26 | | - const tagWidths = Array.from(tagElements).map(el => { |
27 | | - const style = window.getComputedStyle(el); |
28 | | - const marginLeft = parseFloat(style.marginLeft) || 0; |
29 | | - const marginRight = parseFloat(style.marginRight) || 0; |
30 | | - return el.offsetWidth + marginLeft + marginRight; |
31 | | - }); |
32 | | - |
33 | | - const availableWidth = innerWidth - iconSpace; |
34 | | - |
35 | | - let totalWidth = 0; |
36 | | - let count = 0; |
37 | | - |
38 | | - for (let i = 0; i < tagWidths.length; i++) { |
39 | | - // 如果不是最后一个,需要预留 +N 标签空间 |
40 | | - const needOverflowSpace = i < tagWidths.length - 1; |
41 | | - const requiredWidth = tagWidths[i] + (needOverflowSpace ? overflowTagWidth : 0); |
| 11 | +const TagOverflowInner = ({ value, innerWidth, labelKey, valueKey, onRemove, iconSpace = 20 }) => { |
| 12 | + const list = Array.isArray(value) ? value : []; |
| 13 | + const availableWidth = Math.max((innerWidth || 0) - iconSpace, 0); |
| 14 | + const maxTagWidth = innerWidth > 0 ? Math.min(150, Math.max(60, innerWidth - 60)) : 150; |
42 | 15 |
|
43 | | - if (totalWidth + requiredWidth <= availableWidth) { |
44 | | - totalWidth += tagWidths[i]; |
45 | | - count++; |
46 | | - } else { |
47 | | - break; |
48 | | - } |
49 | | - } |
| 16 | + const shareItems = useMemo( |
| 17 | + () => |
| 18 | + list.map(item => ({ |
| 19 | + key: item?.[valueKey], |
| 20 | + label: item?.[labelKey] |
| 21 | + })), |
| 22 | + [list, labelKey, valueKey] |
| 23 | + ); |
50 | 24 |
|
51 | | - // 确保至少显示一个标签 |
52 | | - setVisibleCount(Math.max(1, count)); |
53 | | - }, [value, innerWidth, overflowTagWidth, iconSpace]); |
| 25 | + const { setContainerRef, setMeasureRef, setMoreMeasureRef, visibleCount, ready, hiddenCount, shouldMeasure } = useOverflowItems({ |
| 26 | + itemCount: list.length, |
| 27 | + items: shareItems, |
| 28 | + enabled: list.length > 0 && availableWidth > 0, |
| 29 | + gap: TAG_GAP, |
| 30 | + beforeReady: 'all', |
| 31 | + debounce: 0, |
| 32 | + itemSelector: '[data-tag-measure]' |
| 33 | + }); |
54 | 34 |
|
55 | | - // 计算单个标签最大宽度:min(150px, innerWidth - 60),初始状态默认 150px |
56 | | - const maxTagWidth = innerWidth > 0 ? Math.min(150, Math.max(60, innerWidth - 60)) : 150; |
57 | | - const displayTags = value.slice(0, visibleCount); |
58 | | - const hiddenCount = value.length - visibleCount; |
| 35 | + const displayCount = ready ? visibleCount : list.length; |
| 36 | + const displayTags = list.slice(0, displayCount); |
| 37 | + const showOverflow = ready && hiddenCount > 0; |
59 | 38 |
|
60 | 39 | return ( |
61 | | - <> |
62 | | - {/* 隐藏的测量层 */} |
63 | | - <div ref={measureRef} className={style['measure-layer']}> |
64 | | - {value.map(item => ( |
65 | | - <Tag key={item[valueKey]} data-tag-measure closable bordered={false} className={style['tag-item']} style={{ '--max-tag-width': `${maxTagWidth}px` }}> |
66 | | - <span className={style['tag-label']}>{item[labelKey]}</span> |
| 40 | + <div ref={setContainerRef} className={style['tag-overflow']} style={{ width: availableWidth || '100%', maxWidth: '100%' }}> |
| 41 | + {shouldMeasure ? ( |
| 42 | + <div ref={setMeasureRef} className={style['measure-layer']} aria-hidden style={{ gap: TAG_GAP }}> |
| 43 | + {list.map(item => ( |
| 44 | + <div key={item[valueKey]} data-tag-measure> |
| 45 | + <Tag closable bordered={false} className={style['tag-item']} style={{ '--max-tag-width': `${maxTagWidth}px`, margin: 0 }}> |
| 46 | + <span className={style['tag-label']}>{item[labelKey]}</span> |
| 47 | + </Tag> |
| 48 | + </div> |
| 49 | + ))} |
| 50 | + <div ref={setMoreMeasureRef}> |
| 51 | + <Tag className={style['overflow-tag']} style={{ margin: 0 }}> |
| 52 | + +99 |
| 53 | + </Tag> |
| 54 | + </div> |
| 55 | + </div> |
| 56 | + ) : null} |
| 57 | + <div className={style['tag-overflow-content']} style={{ gap: TAG_GAP }}> |
| 58 | + {displayTags.map(item => ( |
| 59 | + <Tag |
| 60 | + key={item[valueKey]} |
| 61 | + closable |
| 62 | + bordered={false} |
| 63 | + className={style['tag-item']} |
| 64 | + style={{ '--max-tag-width': `${maxTagWidth}px`, margin: 0 }} |
| 65 | + onClose={e => { |
| 66 | + e.preventDefault(); |
| 67 | + onRemove(item); |
| 68 | + }} |
| 69 | + > |
| 70 | + <span className={style['tag-label']} title={item[labelKey]}> |
| 71 | + {item[labelKey]} |
| 72 | + </span> |
67 | 73 | </Tag> |
68 | 74 | ))} |
| 75 | + {showOverflow ? <Tag className={style['overflow-tag']}>+{hiddenCount}</Tag> : null} |
69 | 76 | </div> |
70 | | - {/* 实际显示的标签 */} |
71 | | - {displayTags.map(item => ( |
72 | | - <Tag |
73 | | - key={item[valueKey]} |
74 | | - closable |
75 | | - bordered={false} |
76 | | - className={style['tag-item']} |
77 | | - style={{ '--max-tag-width': `${maxTagWidth}px` }} |
78 | | - onClose={e => { |
79 | | - e.preventDefault(); |
80 | | - onRemove(item); |
81 | | - }} |
82 | | - > |
83 | | - <span className={style['tag-label']} title={item[labelKey]}> |
84 | | - {item[labelKey]} |
85 | | - </span> |
86 | | - </Tag> |
87 | | - ))} |
88 | | - {hiddenCount > 0 && <Tag className={style['overflow-tag']}>+{hiddenCount}</Tag>} |
89 | | - </> |
| 77 | + </div> |
90 | 78 | ); |
91 | 79 | }; |
92 | 80 |
|
|
0 commit comments