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

如何使用Material UI为Modal内的图片裁剪功能设置样式?

解决MUI Modal内裁剪组件的布局问题

我具备基础的CSS页面布局能力,但在Modal内进行布局时感到困惑。曾尝试用MUI的Dialog组件包裹裁剪器但无效,因此改用了Modal组件。希望将其做成仅包含裁剪区域、取消和裁剪按钮的样式,按钮需位于裁剪区域外且背景为白色。目前Modal功能正常,但样式不够理想,以下是我的代码和CSS:

原React组件代码

// import React, {useRef, useState} from 'react'
// import style from '../App.scss.json'
// import ImageCropper from '../components/ImageCropper'


// const Uploader = () => {
//     const [image, setImage] = useState(null)
//     const [fileName, setFileName] = useState("No profile photo selected")
    
//     return (
//         <form onClick={() => document.querySelector<HTMLElement>(".input-field").click()}>
//             {image ? <img src={image} alt={fileName} className={style.imageUpload}  /> : <img src="/static/images/upload-icon.svg" style={{height: '100px', width: '100px'}} className={style.imageUpload} />}
//             <input 
//                 accept="image/*" 
//                 type="file" 
//                 hidden 
//                 className="input-field" 
                // onChange={({target: {files}}) => {
                //     files[0] && setFileName(files[0].name)
                //     if (files){

                //         setImage(URL.createObjectURL(files[0]))
                //     }
                // }}
//             />
//         </form>
//     )
// }

// export default Uploader

import React, {useRef, useState, useCallback} from 'react'
import style from '../App.scss.json'
import Cropper from 'react-easy-crop'
import Slider from "@material-ui/core/Slider";
import {Dialog, DialogActions, Button, Box, Typography, Modal} from '@mui/material'
import getCroppedImg from '../components/CroppedImage'


const Uploader = () => {
    const [image, setImage] = useState(null)
    const [fileName, setFileName] = useState("No profile photo selected")
    const [croppedArea, setCroppedArea] = useState(null)
    const [crop, setCrop] = useState({x:0, y:0})
    const [zoom, setZoom] = useState(1)

    //modal
    const [open, setOpen] = React.useState(false);
    const handleOpen = () => {
        setOpen(true);
        triggerFileSelectPopUp()
    }
    const handleClose = () => setOpen(false);

    const inputRef = useRef<HTMLInputElement>()
    const triggerFileSelectPopUp = () => inputRef.current.click()

    const onCropComplete = (croppedAreaPercentage, croppedArea) => {
        setCroppedArea(croppedArea);
    };
    
    const showCroppedImage = useCallback(async () => {
        try {
        const croppedImage = await getCroppedImg(
            image,
            croppedArea
        )
        console.log('donee', { croppedImage })
        setCroppedArea(croppedImage)
        handleClose()
        // setOpenCrop(false)
        // setCroppedArea(URL.createObjectURL(blobUrl))
        } catch (e) {
      console.error(e)
    }
    }, [image, croppedArea])
    
    const onSelectFile = (event) => {
        if (event.target.files && event.target.files.length > 0) {
            const reader = new FileReader();
            reader.readAsDataURL(event.target.files[0]);
            reader.addEventListener("load", () => {
                setImage(reader.result);
            });
        }
    }

    const onCropCancel = () => {
        setImage(null)
        handleClose()
    }

    const zoomPercent = (value) => {
        return `${Math.round(value * 100)}%`;
    };

    return (
        <>
            {image ? ( 
                <Modal open={open} onClose={handleClose}>
                    <div className={style.modal_container}>
                        <div className={style.cropper}>
                            <Cropper 
                                image={image} 
                                crop={crop} 
                                zoom={zoom} 
                                aspect={3/5}
                                onCropChange={setCrop} 
                                onZoomChange={setZoom} 
                                onCropComplete={onCropComplete} 
                            />
                        </div>
                        <div className={style.slider_container}>
                            {/* <Box sx={{ width: '100%', mb: 1 }}> */}
                                <Typography>Zoom: {zoomPercent(zoom)}</Typography>
                                <div className={style.slider}>
                                    <Slider 
                                        min={1}
                                        max={3}
                                        step={0.1}
                                        value={zoom}
                                        onChange={(e, zoom:any) => setZoom(zoom)}/>
                                </div>
                            {/* </Box> */}
                        </div>
                        <div className={style.button_container}>
                            <Button onClick={onCropCancel}>Cancel</Button>
                            <Button onClick={showCroppedImage}>Crop</Button>
                        </div>
                    </div>
                </Modal>
            ) : null}
                    <div className={style.image_container}>
                        {image ? <img src={croppedArea ? croppedArea : image} alt={fileName} className={style.imageUpload} onClick={handleOpen} /> : <img src="/static/images/upload-icon.svg" style={{height: '100px', width: '100px'}} className={style.imageUpload} onClick={triggerFileSelectPopUp} />}
                        <input 
                            accept="image/*" 
                            type="file" 
                            hidden 
                            ref={inputRef}
                            className="input-field" 
                            onChange={onSelectFile}
                            onClick={handleOpen}
                        />
                    </div>
                    {/* {croppedArea ? (
                        <img alt="Cropped" style={{ maxWidth: '100%' }} src={croppedArea} />
                    ) : null} */}
        </>
    )
}

export default Uploader

原CSS代码

.container {
  height: 100vh;
  width: 70vw;
}

.modal_container {
  position: relative;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 500px;
  height: 70%;
  background-color: whitesmoke;
  box-shadow: 24px;
  padding: 4px;
}

.image_container {
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.cropper{
  background-color: red;
}

.slider {
  height: 10%;
}

.slider_container{
  background: white;
  display:flex;
  justify-content: end;
  align-items: center;
}

.disable_cropper {
  display: none;
}

.button_container {
  position: absolute;
  
}

调整后的布局与样式优化

针对需求,通过调整Modal容器的布局结构、优化CSS定位和弹性布局来实现预期样式:

修改后的React组件代码

import React, {useRef, useState, useCallback} from 'react'
import style from '../App.scss.json'
import Cropper from 'react-easy-crop'
import Slider from "@material-ui/core/Slider";
import {Button, Modal, Typography} from '@mui/material'
import getCroppedImg from '../components/CroppedImage'


const Uploader = () => {
    const [image, setImage] = useState(null)
    const [fileName, setFileName] = useState("No profile photo selected")
    const [croppedArea, setCroppedArea] = useState(null)
    const [crop, setCrop] = useState({x:0, y:0})
    const [zoom, setZoom] = useState(1)

    //modal
    const [open, setOpen] = React.useState(false);
    const handleOpen = () => {
        setOpen(true);
        triggerFileSelectPopUp()
    }
    const handleClose = () => setOpen(false);

    const inputRef = useRef<HTMLInputElement>()
    const triggerFileSelectPopUp = () => inputRef.current.click()

    const onCropComplete = (croppedAreaPercentage, croppedArea) => {
        setCroppedArea(croppedArea);
    };
    
    const showCroppedImage = useCallback(async () => {
        try {
        const croppedImage = await getCroppedImg(
            image,
            croppedArea
        )
        console.log('donee', { croppedImage })
        setCroppedArea(croppedImage)
        handleClose()
        } catch (e) {
      console.error(e)
    }
    }, [image, croppedArea])
    
    const onSelectFile = (event) => {
        if (event.target.files && event.target.files.length > 0) {
            const reader = new FileReader();
            reader.readAsDataURL(event.target.files[0]);
            reader.addEventListener("load", () => {
                setImage(reader.result);
            });
        }
    }

    const onCropCancel = () => {
        setImage(null)
        handleClose()
    }

    const zoomPercent = (value) => {
        return `${Math.round(value * 100)}%`;
    };

    return (
        <>
            {image ? ( 
                <Modal open={open} onClose={handleClose}>
                    <div className={style.modal_container}>
                        {/* 裁剪区域 */}
                        <div className={style.cropper_wrap}>
                            <Cropper 
                                image={image} 
                                crop={crop} 
                                zoom={zoom} 
                                aspect={3/5}
                                onCropChange={setCrop} 
                                onZoomChange={setZoom} 
                                onCropComplete={onCropComplete} 
                            />
                        </div>
                        {/* 底部操作栏:缩放滑块 + 按钮 */}
                        <div className={style.modal_footer}>
                            <div className={style.slider_container}>
                                <Typography>Zoom: {zoomPercent(zoom)}</Typography>
                                <Slider 
                                    min={1}
                                    max={3}
                                    step={0.1}
                                    value={zoom}
                                    onChange={(e, zoom:any) => setZoom(zoom)}/>
                            </div>
                            <div className={style.button_container}>
                                <Button onClick={onCropCancel}>Cancel</Button>
                                <Button onClick={showCroppedImage} variant="contained">Crop</Button>
                            </div>
                        </div>
                    </div>
                </Modal>
            ) : null}
            <div className={style.image_container}>
                {image ? (
                    <img 
                        src={croppedArea ? croppedArea : image} 
                        alt={fileName} 
                        className={style.imageUpload} 
                        onClick={handleOpen} 
                    />
                ) : (
                    <img 
                        src="/static/images/upload-icon.svg" 
                        style={{height: '100px', width: '100px'}} 
                        className={style.imageUpload} 
                        onClick={triggerFileSelectPopUp} 
                    />
                )}
                <input 
                    accept="image/*" 
                    type="file" 
                    hidden 
                    ref={inputRef}
                    className="input-field" 
                    onChange={onSelectFile}
                />
            </div>
        </>
    )
}

export default Uploader

修改后的CSS代码

.container {
  height: 100vh;
  width: 70vw;
}

.modal_container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 500px;
  max-height: 80vh;
  background-color: #fff;
  box-shadow: 0 8px 32px rgba(0,0,0,0.15);
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* 裁剪区域占满容器剩余空间 */
.cropper_wrap {
  flex: 1;
  background-color: #f5f5f5;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}

.image_container {
  display: flex;
  justify-content: flex-start;
  align-items: center;
}

.slider_container {
  padding: 12px 16px;
  border-bottom: 1px solid #eee;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.modal_footer {
  background-color: #fff;
}

.button_container {
  padding: 12px 16px;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

关键修改说明

  • Modal容器布局:将modal_container改为flex-direction: column,让裁剪区域自动占满剩余空间,底部操作栏固定在下方
  • 裁剪区域优化:给裁剪区域添加浅灰色背景,用flex布局居中裁剪组件,保证视觉效果
  • 底部操作栏拆分:将滑块和按钮放在统一的白色底部栏内,按钮靠右排列,符合常规交互习惯
  • 样式细节优化:添加圆角、阴影、内边距,提升整体视觉质感;移除不必要的绝对定位,避免布局混乱

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 05:38:09