ExpandableContent.tsx

import React, { useEffect, useState, useRef, useMemo } from 'react';
import { View, Text, Image } from '@tarojs/components';
import Taro, { pxTransform } from '@tarojs/taro';
import arrowDown from '@/assets/image/common/arrow-down-grey.png';
import cn from 'classnames';
import './index.less';

interface ExpandableContentProps {
  /** 标题 */
  title: string;
  /** 唯一标识符,用于高度检测(可选,不传入时自动生成) */
  id?: string;
  /** 内容,支持任意React节点 */
  children: React.ReactNode;
  /** 每行的高度(px)*/
  lineHeight?: number;
  /** 收起状态的最大行数,默认4行 */
  maxLines?: number;
  /** 是否强制全部展示,不提供展开收起功能 */
  alwaysExpanded?: boolean;
}

/**
 * 可展开收起的内容组件
 * 支持智能检测内容高度,自动显示/隐藏展开按钮
 */
const ExpandableContent: React.FC<ExpandableContentProps> = ({
  title,
  id,
  children,
  lineHeight = 48,
  maxLines = 4,
  alwaysExpanded = false
}) => {
  const [isExpanded, setIsExpanded] = useState(true);
  const [needsExpansion, setNeedsExpansion] = useState(false);

  // 生成唯一 ID,使用 useRef 确保组件生命周期内保持稳定
  const contentId = useRef(id || `expandable-content-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`).current;

  // 计算最大高度
  const maxHeight = lineHeight * maxLines;

  // 使用Taro API检测内容是否超出限制高度
  useEffect(() => {
    if (alwaysExpanded) {
      return;
    }
    // 使用 nextTick 确保DOM已经渲染完成
    Taro.nextTick(() => {
      const query = Taro.createSelectorQuery();
      query.select(`#${contentId}`).boundingClientRect((rect) => {
        if (rect && rect.height) {
          const realHeight = rect.height*2;
          console.log('真实高度:', realHeight, '限制高度:', maxHeight);
          const shouldShowButton = realHeight > maxHeight;
          setNeedsExpansion(shouldShowButton);
          // 根据检测结果设置初始状态
          setIsExpanded(!shouldShowButton); // 如果不需要展开按钮,保持展开状态
        }
      }).exec();
    })

  }, [children, lineHeight, maxLines, alwaysExpanded, maxHeight]);

  // 是否显示展开按钮
  const shouldShowExpandButton = useMemo(() => {
    if (alwaysExpanded) return false;
    return needsExpansion;
  }, [alwaysExpanded, needsExpansion]);

  // 是否限制高度
  const shouldLimitHeight = useMemo(() => {
    if (alwaysExpanded) return false;
    return !isExpanded;
  }, [alwaysExpanded, isExpanded]);

  // 切换展开收起状态
  const toggleExpanded = () => {
    setIsExpanded(!isExpanded);
  };

  // 内容容器样式
  const contentStyle = shouldLimitHeight
    ? {
      maxHeight: pxTransform(maxHeight),
      overflow: 'hidden',
    }
    : {};
  return (
    <View className="expandable-content">
      {/* 标题 */}
  <View className="expandable-content__title">
    {title}
  </View>

{/* 内容区域 */}
<View
  id={contentId}
style={contentStyle}
  >
  {children}
  </View>
{/* 展开收起按钮 */}
{shouldShowExpandButton && (
  <View className="expandable-content__button" onClick={toggleExpanded}>
  <Text className="expandable-content__button-text">
    {isExpanded ? '收起' : '展开全部'}
 </Text>
   <Image src={arrowDown} className={cn("expandable-content__button-icon", {
            'expandable-content__button-icon-rotate': isExpanded
          })} />
        </View>
      )}
    </View>
  );
};

export default ExpandableContent; 

index.less

.expandable-content {
  padding: 32px 24px;
  background-color: #fff;
  border-radius: 12px;

  &__title {
    font-weight: 500;
    font-size: 32px;
    color: #333333;
    line-height: 44px;
    margin-bottom: 16px;
  }


  &__button {
    margin-top: 16px;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    gap: 4px;
    &-text {
      font-weight: 400;
      font-size: 24px;
      color: #999999;
      line-height: 36px;
    }

    &-icon {
      width: 24px;
      height: 24px;
    }

    &-icon-rotate {
      transform: rotate(180deg);
    }
  }

}