import { Button, InputNumber } from 'antd';
import { useEffect, useRef, useState } from 'react';
import Cropper from 'react-cropper';
import { UNVMessageBox, UNVModal } from 'UNV-DESIGN';
import styles from './index.less';
interface types {
// 图片URL
src?: string;
// 图片文件对象(src、file二选一,同时传会使用file)
file?: File;
// 弹窗显示
setVisible: (visible: boolean) => void;
// 确认的回调
onOK: (newFile: File) => void;
// 默认尺寸
defaultSize?: { width: number, height: number };
}
const CropperModal = (props: types) => {
const { src, setVisible, file, onOK, defaultSize } = props;
// 原图片url
const [originalSrc, setOriginalSrc] = useState<any>(src || '');
// 原图片名
const [fileName, setFileName] = useState<string>('');
// 原图片长宽
const [originalSize, setOriginalSize] = useState<{ width: number, height: number }>({ width: 0, height: 0 });
// 预览url
const [viewSrc, setViewSrc] = useState<string>('');
const cropperRef = useRef<any>(null);
useEffect(() => {
if (file) {
setFileName(file.name);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (event) => {
setOriginalSrc(event.target?.result);
};
}
}, []);
useEffect(() => {
if (originalSrc) {
const originalImg = new Image;
originalImg.src = originalSrc;
if (defaultSize && (originalImg.width < defaultSize.width || originalImg.height < defaultSize.height)) {
UNVMessageBox.error(
intl.formatMessage({ id: 'The image is too small. The image size must be greater than xx * xx' }, { size: `${defaultSize.width}*${defaultSize.height}` })
);
setVisible(false);
}
setOriginalSize({ width: originalImg.width, height: originalImg.height });
if (defaultSize) {
const originalImg = new Image;
originalImg.src = originalSrc;
const originalScaleX = originalImg.width / 500;
const originalScaleY = originalImg.height / 400;
const realScale = Math.max(originalScaleX, originalScaleY);
if (realScale) {
const imageElement = cropperRef?.current;
const cropper = imageElement?.cropper;
setTimeout(() => {
cropper.cropBoxData.minWidth = defaultSize.width / realScale;
cropper.cropBoxData.minHeight = defaultSize.height / realScale;
// 模拟一次调整
cropper.setData({ width: 0.8 * originalImg.width });
_crop();
}, 10);
}
}
}
}, [originalSrc]);
/**
* @description 确定生成裁剪图片
*/
const _handleSaveImg = () => {
const uint8 = _getUint8Arr(viewSrc);
const newFile = new File([uint8.u8arr], fileName, { type: uint8.mime });
onOK(newFile);
};
/**
* 二进制容器
* @param {String} dataurl 图片URL
*/
const _getUint8Arr = (dataurl: string) => {
// 截取base64的数据内容
const arr: any = dataurl.split(',');
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
// 获取解码后的二进制数据的长度,用于后面创建二进制数据容器
let n = bstr.length;
// 创建一个Uint8Array类型的数组以存放二进制数据
const u8arr = new Uint8Array(n);
// 将二进制数据存入Uint8Array类型的数组中
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return { u8arr, mime };
};
/**
* @description 刷新预览图像
*/
const _crop = () => {
const imageElement = cropperRef?.current;
const cropper = imageElement?.cropper;
const newURL = cropper.getCroppedCanvas({ ...defaultSize, imageSmoothingQuality: 'high' }).toDataURL('image/png');
setViewSrc(newURL);
};
return (
<UNVModal
title={intl.formatMessage({ id: 'Crop Image' })}
visible
onCancel={() => setVisible(false)}
width={800}
className={styles.cropperModal_model_theModal}
footer={[
<Button key="ok" type="primary" onClick={() => _handleSaveImg()}>
{intl.formatMessage({ id: 'OK' })}
</Button>,
<Button key="cancel" onClick={() => setVisible(false)}>{intl.formatMessage({ id: 'Cancel' })}</Button>
]}
destroyOnClose
canDragm
>
<div className={styles.cropperModal_model_content}>
<Cropper
src={originalSrc}
ref={cropperRef}
viewMode={1} // 定义cropper的视图模式
zoomable={false} // 是否允许放大图像
movable={false} // 是否允许移动图像
guides={false} // 显示在裁剪框上方的虚线
background={false} // 是否显示背景的马赛克
rotatable={false} // 是否旋转
className={styles.cropperModal_model_cropperArea}
// autoCropArea={1} // 默认值0.8(图片的80%)。--0-1之间的数值,定义自动剪裁区域的大小
aspectRatio={defaultSize ? defaultSize.width / defaultSize.height : NaN} // 固定为1:1 可以自己设置比例, 默认情况为自由比例
// cropBoxResizable={false} // 默认true ,是否允许拖动 改变裁剪框大小
// cropBoxMovable // 是否可以拖拽裁剪框 默认true
dragMode="move" // 拖动模式, 默认crop当鼠标 点击一处时根据这个点重新生成一个 裁剪框,move可以拖动图片,none:图片不能拖动
cropend={_crop}
center
/>
<div className={styles.cropperModal_model_InfoArea}>
<img src={viewSrc || originalSrc} />
{defaultSize ? `${defaultSize.width}*${defaultSize.height}` : ''}
{
!defaultSize &&
<div className={styles.cropperModal_model_sizeArea}>
<div>
{intl.formatMessage({ id: 'Width' })}:
<InputNumber
value={viewSrc ? cropperRef?.current?.cropper.getData().width : originalSize.width}
onChange={(value) => cropperRef?.current?.cropper.setData({ width: value })}
min={0}
max={originalSize.width}
/>
</div>
<div>
{intl.formatMessage({ id: 'Height' })}:
<InputNumber
value={viewSrc ? cropperRef?.current?.cropper.getData().height : originalSize.height}
onChange={(value) => cropperRef?.current?.cropper.setData({ height: value })}
min={0}
max={originalSize.height}
/>
</div>
</div>
}
</div>
</div>
</UNVModal>
);
};
export default CropperModal;
组件引用
{showCropper &&
<CropperModal
setVisible={setShowCropper}
file={picFile}
defaultSize={{ width: 230, height: 150 }}
onOK={goUpPic}
/>
}
图片上传校验
/**
* 图片上传校验
* @param param0 上传的图片文件
* @returns
*/
const _onChange = ({ file, fileList }: any) => {
const isImg =
file.type === 'image/jpeg' ||
file.type === 'image/png' ||
file.type === 'image/jpg';
if (!isImg) {
UNVMessageBox.warn(intl.formatMessage({ id: 'Only JPG, PNG, and JPEG images are allowed.' }));
return false;
} else {
setPicFile(file);
setShowCropper(true);
}
};
裁切后的图片上传
/**
* 上传图片
* @param file 图片文件
*/
const goUpPic = (file: File) => {
const picFormData = new FormData();
picFormData.append('file', file);
dispatch({
type: 'banner/uploadPic',
payload: picFormData,
callback: (data: any) => {
setCoverImgs([...coverImgs, data]);
setShowCropper(false);
}
});
};