You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React MUI中如何根据背景图高度动态设置元素高度保持原图宽高比

React+MUI GalleryItem 动态高度适配图片比例方案

核心思路

硬编码ButtonBase高度会导致不同比例的背景图被拉伸,解决方案是先获取图片原始宽高比,再根据容器实时渲染宽度按比例计算对应高度,让容器比例和图片完全匹配,彻底避免变形。

修改点说明

  • 补充React Hook依赖:引入useRef、useEffect,配合MUI的useTheme获取断点值
  • 新增状态存储三类数据:图片原始宽高、容器实时宽度、图片加载状态
  • 图片预加载逻辑:拿到图片地址后通过原生Image对象预加载,加载完成后读取原生宽高属性,兜底默认图走相同加载逻辑
  • 容器尺寸监听:通过ResizeObserver监听ButtonBase的宽度变化,响应式布局下宽度变动时自动重算高度
  • 动态高度计算:按高度 = 容器宽度 * (图片原始高度/图片原始宽度)公式计算,图片未加载完成时沿用原有硬编码高度作为兜底,避免布局抖动
  • 移除styled组件中写死的height配置,将动态计算的高度通过内联样式绑定到ButtonBase

完整修改后代码

import React, { useState, useRef, useEffect } from 'react';
import {
  Box,
  ButtonBase as MuiButtonBase,
  IconButton,
  Typography,
  styled,
  useTheme
} from '@mui/material';

import Label from './Label';
import Favorite from './Favorite';
import MintingContextButton from './MintingContextButton';

import StatusLabel from 'src/components/StatusLabel';
import PublicLabel from 'src/components/PublicLabel';

import { IMAGINE_STATUS } from 'src/constants';
import { authDomain } from 'src/config';

const ButtonBase = styled(MuiButtonBase)(({ theme }) => ({
  position: 'relative',
  width: '100%',
  borderRadius: 32,
  [theme.breakpoints.down('xs')]: {
    width: '100% !important',
  },
  '&:hover': {
    '& .imageTitle': {
      opacity: 1,
      transition: theme.transitions.create('opacity'),
      zIndex: 1,
    },
    '& .imageBackdrop': {
      opacity: 0.7,
    },
    '& .MuiIconButton-favorite': {
      opacity: 1,
      transition: theme.transitions.create('opacity'),
      zIndex: 12,
    },
    '& .MuiButton-minting': {
      opacity: 1,
      transition: theme.transitions.create('opacity'),
      zIndex: 12,
    },
  },
}));

const ContextContainer = styled('span')(() => ({
  position: 'absolute',
  width: '100%',
  height: '100%',
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'space-between',
  padding: 16,
  zIndex: 1,
}));

const StyledStatusLabel = styled(StatusLabel)(() => ({
  position: 'absolute',
  left: 16,
  top: 16,
  zIndex: 1,
}));

const MintIconButton = styled(IconButton)(() => ({
  position: 'absolute',
  left: 16,
  top: 16,
  zIndex: 1,
  background: 'rgba(0, 0, 0, 0.7)',
  '&:hover': {
    background: 'rgba(128, 58, 254, 0.9)',
  },
}));

const ImageTitle = styled('span')(({ theme }) => ({
  opacity: 0,
  textAlign: 'left',
}));

const ImageBackdrop = styled('span')(({ theme }) => ({
  position: 'absolute',
  left: 0,
  right: 0,
  top: 0,
  bottom: 0,
  backgroundColor: theme.palette.common.black,
  opacity: 0,
  transition: theme.transitions.create('opacity'),
  borderRadius: 32,
}));

const ImageImagine = styled('span')(() => ({
  position: 'absolute',
  left: 0,
  right: 0,
  top: 0,
  bottom: 0,
  backgroundSize: 'cover',
  backgroundPosition: 'center 40%',
  borderRadius: 32,
}));

const GalleryItem = ({
  imagine,
  imagine: {
    id,
    title,
    status,
    isMyFavorite,
    public: publicStatus,
    context: imgContext,
    thumb_url: imgUrl,
    count_favorites: countFavorites,
  },
  onOpen,
  onClose,
  ...props
}) => {
  const theme = useTheme();
  const buttonRef = useRef(null);
  const [isHoverFavorite, setHoverFavorite] = useState(false);
  // 存储图片原始尺寸
  const [imgNaturalSize, setImgNaturalSize] = useState({ width: 0, height: 0 });
  // 存储容器实时宽度
  const [containerWidth, setContainerWidth] = useState(0);
  // 存储图片加载状态
  const [imgLoaded, setImgLoaded] = useState(false);

  // 预加载图片获取原始宽高
  useEffect(() => {
    const currentImgUrl = imgUrl ?? `${authDomain}/images/glitchcat.gif`;
    const tempImg = new Image();
    tempImg.src = currentImgUrl;
    tempImg.onload = () => {
      setImgNaturalSize({
        width: tempImg.naturalWidth,
        height: tempImg.naturalHeight
      });
      setImgLoaded(true);
    };
    tempImg.onerror = () => {
      // 加载失败用1:1比例兜底
      setImgNaturalSize({ width: 1, height: 1 });
      setImgLoaded(true);
    };
  }, [imgUrl]);

  // 监听容器宽度变化
  useEffect(() => {
    const currentNode = buttonRef.current;
    if (!currentNode) return;

    // 初始化获取宽度
    setContainerWidth(currentNode.offsetWidth);
    // 监听尺寸变动
    const resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        setContainerWidth(entry.contentRect.width);
      }
    });
    resizeObserver.observe(currentNode);

    return () => resizeObserver.disconnect();
  }, []);

  // 计算动态高度
  const dynamicHeight = (() => {
    // 未加载完成时用原有兜底高度
    if (!imgLoaded || !imgNaturalSize.width || !containerWidth) {
      return window.innerWidth < theme.breakpoints.values.xs ? 326 : 342;
    }
    const aspectRatio = imgNaturalSize.height / imgNaturalSize.width;
    return containerWidth * aspectRatio;
  })();

  return (
    <>
      <ButtonBase
        ref={buttonRef}
        className="MuiButtonBase-gallery"
        disableRipple
        focusRipple
        onClick={(event) => onOpen(event, id)}
        style={{ height: dynamicHeight }}
      >
        <ContextContainer>
          <Box display="flex" justifyContent="space-between">
            <Box
              display="flex"
              visibility={status === IMAGINE_STATUS.COMPLETE && 'hidden'}
            >
              <StyledStatusLabel status={status} />
            </Box>
            {status === IMAGINE_STATUS.COMPLETE && (
              <React.Fragment>
                <Favorite
                  imagineId={id}
                  isMyFavorite={isMyFavorite}
                  isHoverFavorite={isHoverFavorite}
                  isHidden
                  onHoverFavorite={setHoverFavorite}
                  count={countFavorites}
                />
              </React.Fragment>
            )}
          </Box>
          <ImageTitle className="imageTitle">
            <Typography color="textPrimary" variant="subtitle1" noWrap>
              {title}
            </Typography>
            <Box
              display="flex"
              justifyContent="space-between"
              alignItems="center"
            >
              {imgContext && <Label>{imgContext}</Label>}
              <PublicLabel publicStatus={publicStatus} />
            </Box>
          </ImageTitle>
        </ContextContainer>
        <ImageImagine
          style={{
            backgroundImage: `url(${
              imgUrl ?? `${authDomain}/images/glitchcat.gif`
            })`,
          }}
        />
        <ImageBackdrop className="imageBackdrop" />
      </ButtonBase>
    </>
  );
};

export default GalleryItem;

补充说明

  • 该方案不需要后端返回图片宽高字段,前端通过预加载即可拿到原始尺寸,兼容性更好
  • ResizeObserver可以精准响应容器宽度变化,不需要硬编码响应式断点的高度值,适配任意屏幕尺寸
  • 图片加载完成前保留原有兜底高度,不会出现页面布局塌陷抖动
  • 如果后端接口已经返回图片的宽高字段,可以直接跳过预加载步骤,直接用接口返回的尺寸计算比例,性能更好

内容的提问来源于stack exchange,提问作者belegic

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 00:51:17