Bootstrap

React-Cropper (#^.^#) 特制裁剪图片组件^_^+腾讯云

  • ImgCropper.tsx
import React, { useRef, useState, useEffect } from 'react';
import Cropper from 'react-cropper';
import 'cropperjs/dist/cropper.css';
import { Button, Row, Col, notification } from 'antd';
import {
  ZoomInOutlined,
  SwapOutlined,
  ZoomOutOutlined,
  RedoOutlined,
  UndoOutlined,
  ReloadOutlined,
  UploadOutlined,
} from '@ant-design/icons';
import COS from 'cos-js-sdk-v5';

interface ImgCropperProps {
  secretId: string;
  secretKey: string;
  bucket: string;
  region: string;
  file: File | null;
  width?: number;
  height?: number;
  centerWidth?: number;
  onImageChanged?: () => void;
  onUploadStarted?: () => void;
  onUploadFailed?: (err: any) => void;
  onUploaded?: (url: string) => void;
  setIsUrlGenerated: (value: boolean) => void; // 新增的 prop
}

const ImgCropper: React.FC<ImgCropperProps> = ({
                                                 secretId,
                                                 secretKey,
                                                 bucket,
                                                 region,
                                                 file,
                                                 width = 555,
                                                 height = 555,
                                                 centerWidth = 450,
                                                 onImageChanged,
                                                 onUploadStarted,
                                                 onUploadFailed,
                                                 onUploaded,
                                                 setIsUrlGenerated, // 新增的 prop
                                               }) => {
  const [src, setSrc] = useState<string>('');
  const cropperRef = useRef<Cropper>(null);
  const [flipXValue, setFlipXValue] = useState<number>(1);
  const [flipYValue, setFlipYValue] = useState<number>(1);
  const [uploadedImageUrl, setUploadedImageUrl] = useState<string>('');
  const [cos, setCos] = useState<COS | null>(null);
  const cropWidth = 280;
  const cropHeight = 136;

  useEffect(() => {
    const cosInstance = new COS({
      SecretId: secretId,
      SecretKey: secretKey,
    });
    setCos(cosInstance);
  }, [secretId, secretKey]);

  useEffect(() => {
    if (file) {
      const imageUrl = URL.createObjectURL(file);
      setSrc(imageUrl);
      onImageChanged && onImageChanged();
    }
  }, [file, onImageChanged]);

  useEffect(() => {
    if (src) {
      const resetTimeout = setTimeout(() => reset(), 100);
      return () => clearTimeout(resetTimeout);
    }
  }, [src]);

  const zoom = (scale: number) => {
    cropperRef.current?.cropper.zoom(scale);
  };

  const flipX = () => {
    const newFlipXValue = flipXValue === 1 ? -1 : 1;
    setFlipXValue(newFlipXValue);
    cropperRef.current?.cropper.scaleX(newFlipXValue);
  };

  const flipY = () => {
    const newFlipYValue = flipYValue === 1 ? -1 : 1;
    setFlipYValue(newFlipYValue);
    cropperRef.current?.cropper.scaleY(newFlipYValue);
  };

  const rotate = (degrees: number) => {
    cropperRef.current?.cropper.rotate(degrees);
  };

  const reset = () => {
    if (!cropperRef.current) return;

    const cropper = cropperRef.current.cropper;
    cropper.reset();

    const imageData = cropper.getImageData();
    const newHeight = (centerWidth / imageData.naturalWidth) * imageData.naturalHeight;
    cropper.setCanvasData({
      width: centerWidth,
      height: newHeight,
      left: (width - centerWidth) / 2,
      top: (height - newHeight) / 2,
    });
    cropper.setCropBoxData({
      left: (width - cropWidth) / 2,
      top: (height - cropHeight) / 2,
      width: cropWidth,
      height: cropHeight,
    });
    setFlipXValue(1);
    setFlipYValue(1);
  };

  const resetImage = () => {
    setSrc('');
    onImageChanged && onImageChanged();
  };

  const uploadImageToCos = () => {
    if (!cropperRef.current || !cos) return;

    setUploadedImageUrl('');
    onUploadStarted && onUploadStarted();

    cropperRef.current.cropper
      .getCroppedCanvas({ width: cropWidth, height: cropHeight })
      .toBlob((blob) => {
        if (blob) {
          const timestamp = Date.now();
          const fileName = `cropped_image_${timestamp}.png`;
          const croppedFile = new File([blob], fileName, { type: 'image/png' });
          cos.uploadFile(
            {
              Bucket: bucket,
              Region: region,
              Key: fileName,
              Body: croppedFile,
              SliceSize: 1024 * 1024,
            },
            (err: any, data: any) => {
              if (err) {
                notification.error({
                  message: '生成失败',
                  description: '图片生成封面URL失败,请重试。',
                  duration: 2,
                });
                onUploadFailed && onUploadFailed(err);
              } else {
                const imageUrl = `https://${data.Location}`;
                setUploadedImageUrl(imageUrl);
                setIsUrlGenerated(true); // 设置为已生成
                notification.success({
                  message: '生成成功',
                  description: '图片已成功上传至腾讯云。',
                  duration: 2,
                });
                onUploaded && onUploaded(imageUrl);
                resetImage();
              }
            },
          );
        }
      });
  };

  return (
    <div>
      {src && (
        <>
          <Row className="cropper-wrapper">
            <Col span={24}>
              <Cropper
                src={src}
                style={{ width: `${width}px`, height: `${height}px` }}
                dragMode="move"
                viewMode={1}
                cropBoxResizable={false}
                toggleDragModeOnDblclick={false}
                ref={cropperRef}
              />
            </Col>
          </Row>

          <Row className="button-row"  style={{ marginTop: 16 }}>
            <Button.Group>
              <Button
                onClick={() => zoom(0.2)}
                size="middle"
                icon={<ZoomInOutlined />}
                style={{ backgroundColor: '#ffcc80',
                  borderColor: '#ffcc80' ,
                  color: '#fff',marginRight:'10px',borderRadius: 5,width: 44}}
              />
              <Button
                onClick={() => zoom(-0.2)}
                size="middle"
                icon={<ZoomOutOutlined />}
                style={{ backgroundColor: '#ffcc80',
                  borderColor: '#ffcc80' ,
                  color: '#fff',marginRight:'10px',borderRadius: 5,width: 44}}
              />
              <Button
                onClick={flipX}
                size="middle"
                icon={<SwapOutlined rotate={90} />}
                style={{ backgroundColor: '#bbd0f8',
                  borderColor: '#bbd0f8' ,
                  color: '#fff',marginRight:'10px',borderRadius: 5,width: 44}}
              />
              <Button
                onClick={flipY}
                size="middle"
                icon={<SwapOutlined />}
                style={{ backgroundColor: '#bbd0f8',
                  borderColor: '#bbd0f8' ,
                  color: '#fff',marginRight:'10px',borderRadius: 5,width: 44}}
              />
              <Button
                onClick={() => rotate(90)}
                size="middle"
                icon={<RedoOutlined />}
                style={{  backgroundColor: '#a5d6a7',
                  borderColor: '#a5d6a7',
                  color: '#fff',marginRight:'10px',borderRadius: 5,width: 44}}
              />
              <Button
                onClick={() => rotate(-90)}
                size="middle"
                icon={<UndoOutlined />}
                style={{  backgroundColor: '#a5d6a7',
                  borderColor: '#a5d6a7',
                  color: '#fff',marginRight:'10px',borderRadius: 5,width: 44}}
              />
              <Button
                onClick={reset}
                size="middle"
                icon={<ReloadOutlined />}
                style={{ backgroundColor: '#D3D3D3', borderColor: '#D3D3D3' ,marginRight:'10px',borderRadius: 5,width: 44}}
              />
              <Button
                onClick={uploadImageToCos}
                size="middle"
                icon={<UploadOutlined />}
                type="primary"
                style={{ backgroundColor: '#FF8A80', borderColor: '#FF8A80' ,marginRight:'10px',borderRadius: 5}}
              >
                生成封面URL
              </Button>
            </Button.Group>
          </Row>
        </>
      )}
    </div>
  );
};

export default ImgCropper;

使用该组件

import React, { useState } from 'react';
import { Button, Modal } from 'antd';
import ImgCropper from './ImgCropper'; // 假设 ImgCropper 组件在同一目录下

const App: React.FC = () => {
  const [isModalVisible, setIsModalVisible] = useState(false);
  const [file, setFile] = useState<File | null>(null);
  const [coverUrl, setCoverUrl] = useState<string>('');

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = event.target.files?.[0];
    if (selectedFile) {
      setFile(selectedFile);
      setIsModalVisible(true);
    }
  };

  const handleUploadComplete = (url: string) => {
    setCoverUrl(url);
    setIsModalVisible(false);
  };

  return (
    <div style={{ padding: 20 }}>
      <h1>图片裁剪示例</h1>
      <input type="file" accept="image/*" onChange={handleFileChange} />
      {coverUrl && (
        <div>
          <h3>生成的封面URL:</h3>
          <a href={coverUrl} target="_blank" rel="noopener noreferrer">
            {coverUrl}
          </a>
        </div>
      )}
      <Modal
        title="裁剪图片"
        visible={isModalVisible}
        footer={null}
        onCancel={() => setIsModalVisible(false)}
        width={600}
      >
        {file && (
          <ImgCropper
            secretId="your-secret-id" // 替换为您的腾讯云 SecretId
            secretKey="your-secret-key" // 替换为您的腾讯云 SecretKey
            bucket="your-bucket-name" // 替换为您的腾讯云存储桶名称
            region="your-region" // 替换为您的腾讯云区域
            file={file}
            onUploaded={handleUploadComplete}
            onUploadFailed={() => setIsModalVisible(false)}
            setIsUrlGenerated={() => {}}
          />
        )}
      </Modal>
    </div>
  );
};

export default App;

;