antd 的展开收起计算太耗时导致渲染卡顿, 可能是兼容的东西更多
https://github.com/bricksjs/ui/commit/3bb3d2bffb7dc27bd17dbd7b84d55835ac90ecef
结合 useResizeRect
import React, { Fragment, useCallback, useEffect, useMemo, useState, useRef } from 'react';import { isEqual, isObject, toNumber } from 'lodash';import { Tooltip } from 'antd';import type { TooltipProps } from 'antd/lib/tooltip';import CopyOutlined from '@ant-design/icons/CopyOutlined';import CheckOutlined from '@ant-design/icons/CheckOutlined';import copy from 'copy-to-clipboard';import useResizeRect from '../hooks/useResizeRect';import useSetState from '../hooks/useSetState';import usePrevious from '../hooks/usePrevious';import HighLightWord from '../HighLightWord';import type { HighlightProps } from '../HighLightWord';import { getPrefixCls } from '../utils';import './index.less';/** 展开配置 */export type ExpandableType = {/** 展开时节点 */openText?: React.ReactNode;/** 收起时节点 */closeText?: React.ReactNode;};/** 复制操作 */interface CopyConfig {text?: string;onCopy?: () => void;// icon?: React.ReactNode;// tooltips?: boolean | React.ReactNode;}/** 记录当前文本 & 操作: 文本 | 展开操作 | 后缀 | 复制 */type MapStateTypes = 'data' | 'action' | 'suffix' | 'copyable';export interface ExpandIProps {className?: string;style?: React.CSSProperties;/** 展示仅支持字符串模式 */data: string;/** 后缀 */suffix?: string | false;high?: Omit<HighlightProps, 'text'>;/** 复制操作 */copyable?: boolean | CopyConfig;/** Tooltip */tip?: boolean | TooltipProps;/** 是否可展开 */expandable?: boolean | ExpandableType | ((opened: boolean) => string); // React.ReactNode 暂仅支持字符串prefix?: string;/** 多行 */lines?: number;/** 超过长度隐藏 */count?: number;/** 默认设置的 React 节点 */renderContent?: (dom: string | JSX.Element, texts: string) => JSX.Element;}const DEFAULT_ACTION_TEXTS: ExpandableType = {openText: '展开',closeText: '收起',};/** 展开收起文案 */const getActionContent = (expandable: ExpandIProps['expandable'], open: boolean) => {if (typeof expandable === 'boolean' && expandable) {return open ? DEFAULT_ACTION_TEXTS.closeText : DEFAULT_ACTION_TEXTS.openText;}/** 自定义函数渲染 */if (typeof expandable === 'function') {return expandable(open);}if (isObject(expandable)) {const actionTexts = { ...DEFAULT_ACTION_TEXTS, ...expandable };return open ? actionTexts.closeText : actionTexts.openText;}return null;};const Expand = React.memo<ExpandIProps>((props) => {const {tip,style,high,className,data = '',copyable,lines = 1,expandable = true,suffix = '...',renderContent,prefix: customizePrefixCls,} = props;const prefixCls = getPrefixCls('expand', customizePrefixCls);/** 默认为收起状态 */const [map, setMap] = useState(new Map<MapStateTypes, any>([['data', data],['action', null],['suffix', '...'],['copyable', null],]),);/** 控制文本长度 */const [sizes, setSizes] = useSetState({/** 允许显示文本的长度 */allow: 0,/** 文本总长度 */total: 0,});const [texts, setTexts] = useState('');/** 复制操作 */const [copies, setCopies] = useSetState({copied: false,title: '复制',});/** 定时关闭复制操作 */const copyRef = useRef<number>();/** 是否有操作及后缀: 允许长度大于总长度时 不展示后缀 和操作 */const allowActionAndSuffix = useMemo(() => sizes.allow > sizes.total, [JSON.stringify(sizes)]);const [open, setOpen] = useState(false);/** 指定外层容器 */const [containerRef, rect] = useResizeRect<HTMLDivElement>();/** 计算内容文本 */useEffect(() => {if (containerRef.current) {const textLen = data.length;const fontSize = toNumber(window.getComputedStyle(containerRef.current).fontSize?.replace('px', ''),);const containerWidth = rect.width || containerRef.current.getBoundingClientRect().width;/** 计算可容纳的字符长度 */const allowSize = containerWidth / fontSize;const lineSize = allowSize * lines;setSizes({allow: lineSize,total: textLen,});/** 大于总长度时 直接显示文本 */if (allowSize > textLen) {setTexts(data);return;}if (allowSize <= textLen && !open) {const sliceText = data.slice(0, lineSize - 6);setTexts(sliceText);}}}, [containerRef, rect, data, lines]);/** 更新操作文案 */useEffect(() => {/** 如果 false */if (!expandable || allowActionAndSuffix) {map.set('action', null);} else {const actiondom = (<aclassName={`${prefixCls}-action`}onClick={() => {setOpen((prevOpen) => !prevOpen);}}>{getActionContent(expandable, open)}</a>);map.set('action', actiondom);}setMap(new Map(map));}, [open, setOpen, expandable, allowActionAndSuffix]);const onCopy = useCallback((evt: any) => {evt.preventDefault();clearTimeout(copyRef.current);const copyConfig: CopyConfig = {...(typeof copyable === 'object' ? copyable : null),};if (copyConfig.text === undefined) {copyConfig.text = data ?? '';}const copied = copy(copyConfig.text ?? '');setCopies({ copied });/** 复制返回值 */if (copied) {copyConfig?.onCopy?.();} else {console.error('复制错了');}copyRef.current = window.setTimeout(() => {setCopies({ copied: false });}, 2000);},[map, data, setCopies],);useEffect(() => {if (copyable) {const copyCls = copies.copied? `${prefixCls}-copy-success ${prefixCls}-copy`: `${prefixCls}-copy`;map.set('copyable',<Tooltip title={copies.copied ? '复制成功' : '复制'}><span className={copyCls} onClick={onCopy}>{!copies.copied ? <CopyOutlined /> : <CheckOutlined />}</span></Tooltip>,);} else {map.set('copyable', null);}setMap(new Map(map));}, [copyable, copies, prefixCls]);/** 更新后缀 */useEffect(() => {map.set('suffix',open || allowActionAndSuffix ? null : (<span className={`${prefixCls}-suffix`}>{suffix}</span>),);setMap(new Map(map));}, [allowActionAndSuffix, suffix, prefixCls, open]);const prevTexts = usePrevious(texts);useEffect(() => {const getDataDom = (str: string) => {const baseDom = isObject(high) ? <HighLightWord {...high} text={str} /> : str;if (tip && !open) {const tipConfig = isObject(tip) ? tip : {};return (<TooltipgetPopupContainer={() => containerRef.current}placement="topLeft"title={data}{...tipConfig}><span>{baseDom}</span></Tooltip>);}return baseDom;};const fillStr = open ? data : texts;let dom = getDataDom(fillStr);if (typeof renderContent === 'function') {/** @todo: 若返回操作后的字符比原来的长, 其他操作将失效 */dom = renderContent(dom, fillStr);}map.set('data', dom);setMap(new Map(map));}, [texts, prevTexts, open, data, tip, high, renderContent]);useEffect(() => {return () => {clearTimeout(copyRef.current);};}, []);const cls = useMemo(() => (prefixCls ? prefixCls : `${prefixCls} ${className}`),[className, prefixCls],);return (<div ref={containerRef} style={style} className={cls} title={tip ? null : data}>{Array.from(map).map(([key, value]) => {return <Fragment key={key}>{value}</Fragment>;})}</div>);},(prev, next) => isEqual(prev, next),);export default Expand;
@import '../style/themes/index.less';
@tsz-expand-prefix-cls: ~'@{tsz-prefix}-expand';
@tsz-expand-copy-prefix-cls: ~'@{tsz-expand-prefix-cls}-copy';
.@{tsz-expand-prefix-cls} {
&-action {
color: #1890ff;
cursor: pointer;
}
.@{tsz-expand-copy-prefix-cls} {
margin-left: 4px;
color: #1890ff;
cursor: pointer;
&-success {
color: #52c41a;
}
}
}
参考
- webkit-line-clamp: css 实现多行省略
- 或者使用 before after 定位方案
