|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
import { useEffect, useRef, type ReactNode } from 'react' /** * 与 index.html / postcss-pxtorem 一致: * - 设计稿 375 * - html fontSize = (width / 375) * 100 * - 设计稿 px → rem:px / 100 */ const DESIGN_ROOT_VALUE = 100 const MIN_DESIGN_FONT_SIZE = 8 const FIT_FRAME_BUDGET_MS = 6 const FIT_EPSILON_PX = 0.25 type FitTask = () => void const activeFitTasks = new Set<FitTask>() const pendingFitTasks = new Set<FitTask>() let fitFrameId = 0 let resizeListening = false let fontsReadyListening = false const flushFitTasks = () => { fitFrameId = 0 const startedAt = performance.now() for (const task of pendingFitTasks) { pendingFitTasks.delete(task) task() if (performance.now() - startedAt >= FIT_FRAME_BUDGET_MS) break } if (pendingFitTasks.size > 0) { fitFrameId = window.requestAnimationFrame(flushFitTasks) } } const scheduleFitTask = (task: FitTask) => { pendingFitTasks.add(task) if (!fitFrameId) { fitFrameId = window.requestAnimationFrame(flushFitTasks) } } const scheduleAllFitTasks = () => { activeFitTasks.forEach(scheduleFitTask) } const subscribeFitTask = (task: FitTask) => { activeFitTasks.add(task) scheduleFitTask(task) if (!resizeListening) { resizeListening = true window.addEventListener('resize', scheduleAllFitTasks, false) } if (!fontsReadyListening && document.fonts?.ready) { fontsReadyListening = true void document.fonts.ready.then(scheduleAllFitTasks) } return () => { activeFitTasks.delete(task) pendingFitTasks.delete(task) if (activeFitTasks.size === 0 && resizeListening) { resizeListening = false window.removeEventListener('resize', scheduleAllFitTasks, false) } } } interface AutoFitTextProps { children: ReactNode className?: string } const designPxToRem = (designPx: number) => `${designPx / DESIGN_ROOT_VALUE}rem` /** * 取所有可观测宽度的最小值: * - 元素真实布局宽度 * - 父容器扣除 padding 后的内容宽度 * * 父容器使用浮点 DOMRect 扣除 padding / border,避免 clientWidth * 在单字符窄容器里取整后把正常内容误判为溢出。 */ const resolveConstraintWidth = (el: HTMLElement): number => { const parent = el.parentElement if (!parent) return 0 const widths: number[] = [] const ownWidth = el.getBoundingClientRect().width if (ownWidth > 0) widths.push(ownWidth) const parentStyle = window.getComputedStyle(parent) const parentRect = parent.getBoundingClientRect() const parentContentWidth = parentRect.width - (parseFloat(parentStyle.borderLeftWidth) || 0) - (parseFloat(parentStyle.borderRightWidth) || 0) - (parseFloat(parentStyle.paddingLeft) || 0) - (parseFloat(parentStyle.paddingRight) || 0) if (parentContentWidth > 0) widths.push(parentContentWidth) if (widths.length === 0) return 0 return Math.max(0, Math.min(...widths)) } /** Range 保留文字宽度的小数,避免 scrollWidth/clientWidth 整数取整误判。 */ const measureContentWidth = (el: HTMLElement): number => { const range = document.createRange() range.selectNodeContents(el) return range.getBoundingClientRect().width } /** * 默认继承父级字号;超出后按设计稿字号逐步 -1 缩小, * 最小 8px;仍超出再省略号。 */ export const AutoFitText = ({ children, className }: AutoFitTextProps) => { const ref = useRef<HTMLSpanElement>(null) useEffect(() => { const el = ref.current if (!el) return let cancelled = false const fit = () => { if (cancelled) return el.style.fontSize = '' el.style.maxWidth = '' el.classList.remove('is-ellipsis') const constraint = resolveConstraintWidth(el) if (constraint <= 0) return el.style.maxWidth = `${constraint}px` if (measureContentWidth(el) <= constraint + FIT_EPSILON_PX) { el.style.maxWidth = '' return } const rootPx = parseFloat( window.getComputedStyle(document.documentElement).fontSize, ) || DESIGN_ROOT_VALUE const inheritedCssPx = parseFloat( window.getComputedStyle(el).fontSize, ) if (!(inheritedCssPx > 0)) { el.classList.add('is-ellipsis') return } let designSize = Math.round((inheritedCssPx / rootPx) * DESIGN_ROOT_VALUE) while ( designSize > MIN_DESIGN_FONT_SIZE && measureContentWidth(el) > constraint + FIT_EPSILON_PX ) { designSize -= 1 el.style.fontSize = designPxToRem(designSize) } if (measureContentWidth(el) > constraint + FIT_EPSILON_PX) { el.classList.add('is-ellipsis') } else { // 缩到能放下后去掉 maxWidth,避免多余裁切 el.style.maxWidth = '' } } const unsubscribe = subscribeFitTask(fit) return () => { cancelled = true unsubscribe() } }, [children]) return ( <span ref={ref} className={`trade-order-bottom__auto-fit-text${ className ? ` ${className}` : '' }`} > {children} </span> ) } |
