0. 阿里云oss文件上传+React+antd-upload
Q1 upload组件中使用useEffect初始化问题
form页面使用了Modal包裹form和upload组件,upload组件中使用useEffect初始化。
但是只在Modal第一次打开才正确执行,后续Modal开关都没法执行。
后续换了从父组件传递的数个参数,upload组件useEffect监听props.xxx,还是不行。
后来打印props发现有个value属性,是父组件Form.Item的值,改为监听value,果然生效了。生效后,自定义回显数据。
或者在Modal上添加destroyOnClose={true},每次关闭modal都销毁子组件达到重置upload组件的目的。
<Modal
title={"添加小说"}
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
width={"80%"}
destroyOnClose={true}
useEffect(() => {
/////
}, [props.value]);
Q2 antd的upload组件beforeUpload还有个比较坑的地方,
beforeUpload校验不通过return false的话,还是会上传文件。 只有
return Upload.LIST_IGNORE 才会阻止文件上传。
const beforeUpload = (file) => {
// return false;
/**
* type 用于区分上传的文件类型
*/
if (props.type === "cover_key") {
if (file.type !== "image/png" && file.type !== "image/jpeg") {
message.error("只能上传png或者jpg格式的图片");
return Upload.LIST_IGNORE;
}
}
if (props.type === "novel_key") {
// 限制txt格式
if (file.type !== "text/plain") {
message.error("只能上传txt格式的文件");
return Upload.LIST_IGNORE;
}
}
return true;
};
1. form页面
import StoreUploadFile from "@/components/StoreUploadFile";
<Modal
title="Basic Modal"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
>
<Form
// preserve={false}
form={createNovelForm}
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
autoComplete="off"
>
<Form.Item<INovelCreate>
label="小说名称"
name="title"
rules={[{ required: true, message: "请输入小说名称!" }]}
>
<Input />
</Form.Item>
<Form.Item<INovelCreate>
label="小说作者"
name="author"
rules={[{ required: true, message: "请输入小说作者!" }]}
>
<Input />
</Form.Item>
<Form.Item<INovelCreate>
label="小说封面"
name="coverUrl"
rules={[{ required: true, message: "请输入小说封面路径!" }]}
>
<StoreUploadFile
maxCount={1}
key={"cover_key"}
type={"cover_key"}
OSS_DIR={"novel"}
tips={'小说封面,限制上传一张图片'}
showRemoveIcon={true}
onUploadSuccess={(value) => {
createNovelForm.setFieldsValue({
coverUrl: value,
ossCoverUrl: "",
});
}}
/>
</Form.Item>
<Form.Item name="categories" label="小说分类">
<Select
mode="tags"
fieldNames={{ label: "name", value: "id" }}
style={{ width: 180 }}
options={categoryList}
/>
</Form.Item>
<Form.Item name="tags" label="小说标签">
<Select
mode="tags"
fieldNames={{ label: "name", value: "id" }}
style={{ width: 180 }}
options={tagList}
/>
</Form.Item>
<Form.Item<INovelCreate>
label="文件路径"
name="ossPath"
rules={[{ required: true, message: "请输入文件路径!" }]}
>
<StoreUploadFile
key={"novel_key"}
type={"novel_key"}
OSS_DIR={"novel"}
showRemoveIcon={false}
onUploadSuccess={onUploadSuccess}
tips={'小说文件,限制上传一个txt文件'}
/>
</Form.Item>
<Form.Item name="ossUrl" style={{ display: "none" }}>
<Input />
</Form.Item>
<Form.Item name="ossCoverUrl" style={{ display: "none" }}>
<Input />
</Form.Item>
<Form.Item name="id" style={{ display: "none" }}>
<Input />
</Form.Item>
</Form>
</Modal>
2. upload组件
// StoreUploadFile
import React, { useEffect, useState } from "react";
import { Upload, Modal, Form, Divider } from "antd";
import { InfoCircleOutlined, PlusOutlined } from "@ant-design/icons";
import type { UploadFile } from "antd/es/upload/interface";
import { message } from "@/utils/antdGlobal";
import type { UploadProps, FormInstance } from "antd";
import type { RcFile } from "antd/es/upload";
import { useOSS } from "@/store";
interface UploadFileProps extends UploadProps {
onProgress?: (progress: any, file: any) => void;
}
interface StoreUploadFileProps {
onUploadSuccess?: (url: string) => void;
form?: FormInstance;
formData?: any;
showRemoveIcon?: boolean;
OSS_DIR?: string;
value?: any;
type?: string;
maxCount?: number;
tips?: string;
}
const StoreUploadFile = (props: StoreUploadFileProps) => {
// 上一个组件传来的修改资源URL的函数,可用于展示远程的资源
// const changeSrc = props.changeSrc;
const [show, changeShow] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState("");
const [previewTitle, setPreviewTitle] = useState("");
const [previewUrl, setPreviewUrl] = useState("");
const oss = useOSS();
const form = Form.useFormInstance();
const { type } = props;
useEffect(() => {
setTimeout(() => {
if (!type) return;
const resetValueTypes = ["cover_key", "novel_key"];
if (resetValueTypes.includes(type)) {
if (form) {
const { ossPath, ossUrl, coverUrl, ossCoverUrl } =
form.getFieldsValue();
/**
* 造file文件,回显用
*/
switch (type) {
case "novel_key":
if (ossPath && ossUrl) {
const file = {
name: ossPath,
percent: 100,
status: "done",
url: ossUrl,
response: {
name: ossPath,
},
} as UploadFile;
setFileList([file]);
}
break;
case "cover_key":
if (coverUrl && ossCoverUrl) {
const file = {
name: coverUrl,
percent: 100,
status: "done",
url: ossCoverUrl,
// type: "image/png",
response: {
name: coverUrl,
},
} as UploadFile;
setFileList([file]);
}
break;
}
}
}
}, 1);
}, [props.value]);
useEffect(() => {
oss.initOSS().then(() => {
//
});
}, []);
const [fileList, setFileList] = useState<UploadFile[]>([]);
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);
});
const handleCancel = () => setPreviewOpen(false);
const handlePreview = async (file: UploadFile) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj as RcFile);
}
setPreviewImage(file.url || (file.preview as string));
setPreviewOpen(true);
setPreviewTitle(
file.name || file.url!.substring(file.url!.lastIndexOf("/") + 1),
);
};
const uploadPath = (path, file) => {
return `${path}/${file.name.split(".")[0]}-${file.uid}.${
file.type.split("/")[1]
}`;
};
const OssUpload = async (option) => {
const { client, STSData } = await oss.initOSS();
const { file, onSuccess, onProgress, onError } = option;
const folder = props.OSS_DIR ?? STSData!.OSS_DIR;
const url = uploadPath(folder, file);
let data = null;
try {
data = await client.multipartUpload(url, file, {
progress: function (p) {
console.log("获取进度条的值==>", (p * 100).toFixed(2));
onProgress({ percent: (p * 100).toFixed(2) }, file);
},
callback: {
url: "http://xxxxxx:13000/api/v1/upload/callback",
body: `filename=${file.name}&url=${url}`,
contentType: "application/x-www-form-urlencoded",
},
});
props.onUploadSuccess && props.onUploadSuccess(url);
onSuccess(data, file);
} catch (e) {
// message.error(e.message);
onError(e);
}
};
const OSS_DOWNLOAD = async (url): Promise<void> => {
const { client } = await oss.initOSS();
try {
const result = await client.signatureUrl(url, {
expires: 3600, // 有效时间 3600 秒
method: "GET",
});
console.log("OSS_DOWNLOAD result:", result);
window.open(result);
// setPreviewUrl(result);
// return result;
} catch (error) {
console.error("OSS_DOWNLOAD error:", error);
throw error;
}
};
const OSS_DELETE = async (url): Promise<void> => {
const { client } = await oss.initOSS();
try {
const result = await client.delete(url);
console.log("OSS_DELETE result:", result);
if (result.res.status === 204) {
message.success("删除成功");
}
// return result;
} catch (error) {
console.error("OSS_DELETE error:", error);
throw error;
}
};
const beforeUpload = (file) => {
// return false;
/**
* type 用于区分上传的文件类型
*/
if (props.type === "cover_key") {
if (file.type !== "image/png" && file.type !== "image/jpeg") {
message.error("只能上传png或者jpg格式的图片");
return Upload.LIST_IGNORE;
}
}
if (props.type === "novel_key") {
// 限制txt格式
if (file.type !== "text/plain") {
message.error("只能上传txt格式的文件");
return Upload.LIST_IGNORE;
}
}
return true;
};
const uploadProps: UploadFileProps = {
maxCount: props.maxCount ?? undefined,
showUploadList: {
showRemoveIcon: props.showRemoveIcon ?? true,
showPreviewIcon: true,
showDownloadIcon: true,
},
customRequest: OssUpload,
beforeUpload: beforeUpload,
fileList: fileList,
listType: "picture-card",
onPreview: handlePreview,
onProgress({ percent }, file) {
const index = fileList.findIndex((item) => item.uid === file.uid);
fileList[index].percent = percent;
setFileList([...fileList]);
// console.log("onProgress", `${percent}%`, file);
},
onChange(info: any) {
console.log("onChange😊===》", info);
if (info.file.status !== "uploading") {
console.log(info.file, info.fileList);
}
if (info.file.status === "done") {
// console.log(`${info.file.name} 文件上传成功`);
message.success(`${info.file.name} 文件上传成功`);
} else if (info.file.status === "error") {
info.fileList = info.fileList.filter(
(item) => item.uid !== info.file.uid,
);
message.error(`${info.file.name} 文件上传失败`);
}
setFileList([...info.fileList]);
},
onRemove(file) {
OSS_DELETE(file.response.name);
},
onDownload(file) {
OSS_DOWNLOAD(file.response.name);
},
};
const uploadButton = (
<div>
<PlusOutlined />
<div className="ant-upload-text">上传</div>
</div>
);
return (
<div>
<Upload {...uploadProps}>{uploadButton}</Upload>
{props.tips ? (
<Divider className={"before:bg-lime-400 after:bg-lime-400"}>
<span className={"text-[14px] text-gray-500"}>
<InfoCircleOutlined /> {props.tips}
</span>
</Divider>
) : null}
<Modal
open={previewOpen}
title={previewTitle}
footer={null}
onCancel={handleCancel}
>
<img alt="example" style={{ width: "100%" }} src={previewImage} />
</Modal>
</div>
);
};
export default StoreUploadFile;