参考 antd
的官网图片裁剪案例就基本能实现效果了,但是还需要进一步封装成自己想要的样子。
https://ant.design/components/upload-cn/
antd-img-crop 中文文档:
https://github.com/nanxiaobei/antd-img-crop/blob/main/README.zh-CN.md
props结构
需要封装的组件,可以携带一个图片地址字符串的数组,并且改变这个数组就可以更改;当然携带 antd
的文件数组也是需要支持的。根据需求我自己定义的 props
如下。
type PropsType = {
/** 图片地址数组 */
urls?: string[];
/** 图片文件对象数组 */
fileList?: UploadFile[];
/** 裁切区域宽高比,width / height */
aspect?: number;
/** 是否弹出删除确认框 默认:true */
isRemovePop?: boolean;
/** 上传图片的最大数量 默认: 5 */
maxLength?: number;
/** 是否展示文件列表 */
showUploadList?: boolean;
children?: ReactNode;
beforeUpload?: (file: File) => Promise<File>;
onChange: (urls: string[], fileList?: UploadFile[]) => void;
};
使用方式
- url数组方式
const [ulrs, setUrls] = useState<string[]>([]);
return (
<UploadCropperImg
urls={urls}
onChange={setUrls}
aspect={760 / 1140} // 这个就是长宽比
/>
)
- file文件数组形式
const [fileList, setFileList] = useState<UploadFile[]>([]);
useEffect(() => {
getData()
}, [])
// 从接口获取数据
const getData = async () => {
const data = await getDataAPI()
const {imgList} = data
const newArr = imgList?.map((url) => {
return {url: url, uid: url, name: url,}
})
setFileList(newArr)
}
function beforeUpload(file: File) {
return new Promise<File>((resolve, reject) => {
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片最大不超过 2MB!');
return reject(false);
}
return resolve(file);
});
return (
<UploadCropperImg
aspect={1920/1080}
fileList={fileList}
beforeUpload={beforeUpload}
onChange={(urls, fileList) => {
setFileList(fileList ?? []);
}}
>
<div>上传可以裁剪的图片</div>
</UploadCropperImg>
)
完整代码
import { useState, useEffect, ReactNode } from 'react';
import { Upload, message, Modal } from 'antd';
import ImgCrop from 'antd-img-crop';
// 注意:这两个样式需要导入,不然样式可能会有问题
// 弹出层的样式
import 'antd/es/modal/style';
// 这是滑动条的样式
import 'antd/es/slider/style';
import './index.less';
import type {
UploadChangeParam,
RcFile,
UploadFile,
UploadProps,
} from 'antd/lib/upload/interface';
import { BASE_URL, CDN_URL } from '@/configs/base';
// 这里是后端报错返回的类型
import { ResponseError } from '@/service/type';
// 这里是后端接口返回的数据类型
import { UploadResponse } from '@/service/api-oss';
// import { getBase64 } from '@/utils/handleFile';
import { PlusOutlined } from '@ant-design/icons';
const { confirm } = Modal;
type PropsType = {
/** 图片地址数组 */
urls?: string[];
/** 图片文件对象数组 */
fileList?: UploadFile[];
/** 裁切区域宽高比,width / height */
aspect?: number;
/** 是否弹出删除确认框 默认:true */
isRemovePop?: boolean;
/** 上传图片的最大数量 默认: 5 */
maxLength?: number;
/** 是否展示文件列表 */
showUploadList?: boolean;
children?: ReactNode;
beforeUpload?: (file: File) => Promise<File>;
onChange: (urls: string[], fileList?: UploadFile[]) => void;
};
/** https://github.com/nanxiaobei/antd-img-crop/blob/main/README.zh-CN.md */
/** 可裁剪的上传图片组件 */
export default ({
urls = [],
fileList: imgList,
aspect,
isRemovePop = true,
maxLength = 5,
showUploadList = true,
children,
beforeUpload,
onChange,
}: PropsType) => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewImage, setPreviewImage] = useState('');
/** 初始化url */
useEffect(() => {
if (!fileList.length && urls?.length) {
const arr: UploadFile[] =
urls?.map((url) => {
const reg = new RegExp(CDN_URL);
const name = url.replace(reg, '');
return { uid: name, name, url };
}) ?? [];
setFileList(arr);
}
}, [urls]);
useEffect(() => {
if (imgList) {
setFileList(imgList);
}
}, [imgList]);
const _onChange: UploadProps['onChange'] = (
res: UploadChangeParam<UploadFile<UploadResponse[]>>,
) => {
// console.log('_onChange: ', res);
setFileList(res.fileList);
if (res.file.status === 'uploading') {
return;
} else if (res.file.status === 'done') {
// 上传完成
if (!res?.file?.response?.[0]?.url) return;
const resUrl = res?.file?.response[0]?.url;
onChange?.([...urls, resUrl], res.fileList);
} else if (res.file.status === 'error') {
message.error({
content: (res?.file?.response as unknown as ResponseError)?.msg,
});
}
};
/** 预览图片 */
const onPreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as RcFile);
}
setPreviewImage(file.url || (file.preview as string));
setPreviewVisible(true);
};
/** 删除图片 */
const onRemove = (res: UploadFile) => {
if (isRemovePop) {
return new Promise<boolean>((resolve) =>
confirm({
title: '您确定删除图片吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
const index = fileList.findIndex((item) => item.uid === res.uid);
if (index !== -1) {
urls.splice(index, 1);
onChange([...urls]);
}
resolve(true);
},
onCancel: () => resolve(false),
}),
);
} else {
const index = fileList.findIndex((item) => item.uid === res.uid);
if (index !== -1) {
urls.splice(index, 1);
onChange([...urls]);
}
return true;
}
};
const imgCropProps = {
// 裁切区域宽高比,width / height
aspect: aspect,
// 启用图片旋转
rotate: true,
// 显示裁切区域网格(九宫格)
grid: true,
};
return (
<div className="com-uploadCropperImg">
<ImgCrop {...imgCropProps}>
<Upload
name="files"
action={BASE_URL + 'api/master/oss/fromFiles?fileType=img'}
headers={{
'x-master-session-token':
localStorage.getItem('x-master-session-token') ||
sessionStorage.getItem('x-master-session-token') ||
'',
}} // 以上三个属性根据自己的api需求更改就好了
showUploadList={showUploadList}
listType="picture-card"
fileList={fileList}
beforeUpload={beforeUpload}
onChange={_onChange}
onPreview={onPreview}
onRemove={onRemove}
>
{fileList.length < maxLength && (
<>
{children ?? (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</div>
)}
</>
)}
</Upload>
</ImgCrop>
<Modal
visible={previewVisible}
title="预览图片"
footer={null}
onCancel={() => setPreviewVisible(false)}
>
<div className={`uploadCropperImg-modal`}>
<img src={previewImage} className={`uploadCropperImg-modalImg`} />
</div>
</Modal>
</div>
);
};
/** 获取antd的base64 */
export const getBase64 = (file: RcFile): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});