- 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;