antd Form结合自定义upload组件使用

502 阅读4分钟

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;