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

基于Next.js+TypeScript的可复用图片上传裁剪组件需求

集成Cropper的可复用Dropzone组件实现(Next.js + TypeScript)

1. 安装依赖

首先安装所需的核心库及类型定义:

npm install react-dropzone react-cropper cropperjs
npm install -D @types/cropperjs

2. 封装可复用组件 ImageCropperDropzone.tsx

创建客户端组件(Next.js 13+ 需添加'use client'指令),完整实现如下:

'use client';

import { useState, useRef } from 'react';
import { useDropzone } from 'react-dropzone';
import Cropper from 'react-cropper';
import 'cropperjs/dist/cropper.css';

// 组件Props定义
interface ImageCropperDropzoneProps {
  onCropComplete: (croppedFile: File) => void;
  accept?: string;
  cropAspectRatio?: number;
  dropzoneText?: string;
}

export default function ImageCropperDropzone({
  onCropComplete,
  accept = 'image/*',
  cropAspectRatio = 1 / 1,
  dropzoneText = '拖拽图片到此处或点击上传',
}: ImageCropperDropzoneProps) {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [cropModalOpen, setCropModalOpen] = useState(false);
  const cropperRef = useRef<Cropper>(null);

  // Dropzone核心逻辑
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    accept,
    multiple: false,
    onDrop: (acceptedFiles) => {
      if (acceptedFiles.length > 0) {
        setSelectedFile(acceptedFiles[0]);
        setCropModalOpen(true);
      }
    },
  });

  // 裁剪完成处理
  const handleCropSave = () => {
    const cropper = cropperRef.current?.cropper;
    if (!cropper || !selectedFile) return;

    cropper.getCroppedCanvas().toBlob((blob) => {
      if (!blob) return;
      const croppedFile = new File([blob], selectedFile.name, {
        type: selectedFile.type,
      });
      onCropComplete(croppedFile);
      // 重置状态
      setCropModalOpen(false);
      setSelectedFile(null);
    });
  };

  // 关闭模态框时重置状态
  const handleModalClose = () => {
    setCropModalOpen(false);
    setSelectedFile(null);
  };

  return (
    <>
      {/* Dropzone上传区域 */}
      <div
        {...getRootProps()}
        style={{
          border: '2px dashed #ccc',
          borderRadius: '8px',
          padding: '40px',
          textAlign: 'center',
          cursor: 'pointer',
          backgroundColor: isDragActive ? '#f0f0f0' : 'white',
        }}
      >
        <input {...getInputProps()} />
        <p>{dropzoneText}</p>
      </div>

      {/* 裁剪模态框 */}
      {cropModalOpen && (
        <div style={{
          position: 'fixed',
          top: 0,
          left: 0,
          width: '100%',
          height: '100%',
          backgroundColor: 'rgba(0,0,0,0.8)',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          zIndex: 1000,
        }}>
          <div style={{ width: '80%', maxWidth: '800px', position: 'relative' }}>
            {selectedFile && (
              <Cropper
                ref={cropperRef}
                src={URL.createObjectURL(selectedFile)}
                aspectRatio={cropAspectRatio}
                viewMode={1}
                dragMode='move'
                autoCropArea={0.8}
                restore={false}
                guides={true}
                center={true}
                highlight={true}
                cropBoxMovable={true}
                cropBoxResizable={true}
                toggleDragModeOnDblclick={true}
                style={{ width: '100%', height: '450px' }}
              />
            )}
            <div style={{
              display: 'flex',
              gap: '16px',
              marginTop: '20px',
              justifyContent: 'flex-end',
            }}>
              <button
                onClick={handleModalClose}
                style={{ padding: '8px 16px', borderRadius: '4px', border: 'none', cursor: 'pointer' }}
              >
                取消
              </button>
              <button
                onClick={handleCropSave}
                style={{ padding: '8px 16px', borderRadius: '4px', border: 'none', backgroundColor: '#0070f3', color: 'white', cursor: 'pointer' }}
              >
                确认裁剪
              </button>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

3. 组件使用示例

在任意Next.js页面或组件中导入并使用:

'use client';

import ImageCropperDropzone from '@/components/ImageCropperDropzone';

export default function UploadPage() {
  const handleCropComplete = (croppedFile: File) => {
    // 此处处理裁剪后的图片,比如上传到后端、预览等
    console.log('裁剪完成的文件:', croppedFile);
    // 示例:创建预览URL
    const previewUrl = URL.createObjectURL(croppedFile);
    console.log('预览URL:', previewUrl);
  };

  return (
    <div style={{ maxWidth: '600px', margin: '40px auto' }}>
      <h2>图片上传与裁剪</h2>
      <ImageCropperDropzone
        onCropComplete={handleCropComplete}
        cropAspectRatio={16/9}
        dropzoneText='拖拽图片到此处,或点击选择图片'
      />
    </div>
  );
}

4. 关键逻辑说明

  • Dropzone交互:通过react-dropzone监听文件拖拽/上传事件,选中文件后自动打开裁剪模态框
  • Cropper配置:默认设置1:1比例,可通过cropAspectRatio props自定义;启用裁剪框拖拽、缩放等基础交互
  • 裁剪结果处理:通过getCroppedCanvas()获取裁剪后的画布,转为File对象后通过onCropComplete回调传递给父组件
  • 状态清理:关闭模态框时自动重置选中文件状态,避免内存泄漏

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 10:10:03