/** * KRA - Image Cropper Component * Provides image cropping functionality with preview */ import React, { useState, useRef, useCallback } from 'react'; import { Modal, Button, Upload, Select, Space, Tooltip, message, Slider } from 'antd'; import { RotateLeftOutlined, RotateRightOutlined, ZoomInOutlined, ZoomOutOutlined, UploadOutlined, } from '@ant-design/icons'; import Cropper from 'react-cropper'; import 'cropperjs/dist/cropper.css'; import { uploadFile } from '@/services/kratos/fileUpload'; import { useToken } from '@/models'; import type { UploadFile, RcFile } from 'antd/es/upload/interface'; interface CropperImageProps { /** Class ID for file categorization */ classId?: number; /** Callback when upload succeeds */ onSuccess?: (url: string) => void; /** Button text */ buttonText?: string; /** Max file size in MB */ maxSize?: number; } interface RatioOption { label: string; value: number[]; } const ratioOptions: RatioOption[] = [ { label: '1:1', value: [1, 1] }, { label: '16:9', value: [16, 9] }, { label: '9:16', value: [9, 16] }, { label: '4:3', value: [4, 3] }, { label: '自由比例', value: [] }, ]; const CropperImage: React.FC = ({ classId = 0, onSuccess, buttonText = '裁剪上传', maxSize = 8, }) => { const token = useToken(); const cropperRef = useRef(null); const [visible, setVisible] = useState(false); const [imgSrc, setImgSrc] = useState(''); const [uploading, setUploading] = useState(false); const [currentRatio, setCurrentRatio] = useState(4); // Default to free ratio const [previewUrl, setPreviewUrl] = useState(''); const [zoom, setZoom] = useState(1); // Get aspect ratio based on selection const getAspectRatio = useCallback(() => { const ratio = ratioOptions[currentRatio].value; if (ratio.length === 0) return NaN; // Free ratio return ratio[0] / ratio[1]; }, [currentRatio]); // Handle file selection const handleBeforeUpload = (file: RcFile) => { const isImage = file.type.startsWith('image/'); if (!isImage) { message.error('请选择图片文件'); return false; } if (file.size / 1024 / 1024 > maxSize) { message.error(`文件大小不能超过${maxSize}MB!`); return false; } const reader = new FileReader(); reader.onload = (e) => { setImgSrc(e.target?.result as string); setVisible(true); }; reader.readAsDataURL(file); return false; // Prevent auto upload }; // Rotate image const handleRotate = (degree: number) => { const cropper = cropperRef.current?.cropper; if (cropper) { cropper.rotate(degree); } }; // Zoom image const handleZoom = (ratio: number) => { const cropper = cropperRef.current?.cropper; if (cropper) { cropper.zoom(ratio); } }; // Handle ratio change const handleRatioChange = (value: number) => { setCurrentRatio(value); const cropper = cropperRef.current?.cropper; if (cropper) { const ratio = ratioOptions[value].value; if (ratio.length === 0) { cropper.setAspectRatio(NaN); } else { cropper.setAspectRatio(ratio[0] / ratio[1]); } } }; // Handle crop and upload const handleUpload = async () => { const cropper = cropperRef.current?.cropper; if (!cropper) return; setUploading(true); try { const canvas = cropper.getCroppedCanvas({ maxWidth: 1200, maxHeight: 1200, imageSmoothingEnabled: true, imageSmoothingQuality: 'high', }); canvas.toBlob(async (blob) => { if (!blob) { message.error('裁剪失败'); setUploading(false); return; } const file = new File([blob], `${Date.now()}.jpg`, { type: 'image/jpeg' }); const formData = new FormData(); formData.append('file', file); if (classId) { formData.append('classId', String(classId)); } try { const res = await uploadFile(formData); if (res.code === 0 && res.data?.file?.url) { message.success('上传成功'); setVisible(false); setImgSrc(''); onSuccess?.(res.data.file.url); } else { message.error(res.msg || '上传失败'); } } catch (error: any) { message.error('上传失败: ' + (error.message || '未知错误')); } finally { setUploading(false); } }, 'image/jpeg', 0.9); } catch (error: any) { setUploading(false); message.error('裁剪失败: ' + (error.message || '未知错误')); } }; // Handle crop event for preview const handleCrop = () => { const cropper = cropperRef.current?.cropper; if (cropper) { const canvas = cropper.getCroppedCanvas({ maxWidth: 300, maxHeight: 300, }); setPreviewUrl(canvas.toDataURL()); } }; // Handle modal close const handleClose = () => { setVisible(false); setImgSrc(''); setPreviewUrl(''); setZoom(1); }; return ( <> 取消 , , ]} destroyOnClose >
{/* Left: Cropper Area */}
{imgSrc && ( )}
{/* Toolbar */}