antd+react Hook弹窗改进版

之前写过一个react Hook+antd弹窗,虽然功能实现了,但是再使用的时候仍然会有报错,虽然这个报错不影响使用的,但是,作为一个合格的前端切图仔,要再使用中发现问题,改正问题。

问题

  1. 多次调用hook会创建多个相同id的盒子加入页面,并且未初始化就已经有盛放弹窗的盒子
  2. 使用不够简便,一些文件的格式判断以及大小判断没有做
  3. 在热更新时如果弹窗处于打开状态,会报错节点容器已经被创建

问题出现原因及修改办法

针对第一个问题和第三个我呢提主要是因为在调用hook时,就创建了弹窗盛放的容器,这个是不对的,因为虽然调用hook但是并不代表就一定要弹窗,虽然这样说有点牵强,不使用为啥调用弹窗hook。但是主要目的在于我们不应该在调用hook的时候创建容器,而是在调用初始化的时候调用,具体的修改代码,可以直接看完整版的代码
针对于第二个问题,我的解决办法是给定默认大小以及文件格式,如果传递参数就使用传递参数的来判断

完整代码

  • 弹窗hook
import React, { useCallback, useEffect } from "react";
import ReactDOM from "react-dom/client";
import { Button, ConfigProvider, Modal, message } from "antd";
import { useState } from "react";
import { useForm } from "./form";
import { formType } from "../types/hooksTypes/form";
import zhCN from "antd/locale/zh_CN";
import "dayjs/locale/zh-cn";
type PromiseType = {
  resolve?: any;
  reject?: any;
};
/* 
modal类型(分为普通或者表单形式)
由于行内布局传入配置过多暂不支持布局
*/
type modalType = "nomal" | "form";
/* 
按钮类型
txt: 显示的文本内容
type:按钮类型
isDanger:是否危险
*/
export type buttonType = {
  txt?: string;
  type?: "default" | "primary" | "dashed" | "text" | "link";
  isDanger?: boolean;
};
/* 
type: 弹窗类型
title: 弹窗头部显示文本
infoTxt:弹窗为普通类型时,提示文本信息
okBtn:确定按钮配置
cancelBtn:取消按钮配置
formOptions:form表单配置
isEdit:是否显示富文本
isUpload:是否上传图片
sendFn:点击确定,成功后发送数据
successCallback发送数据之后调用的函数
fileRules文件匹配规则
maxSize:文件上传大小限制(单位为m)
*/
type modalPropsType = {
  type?: modalType;
  title?: string;
  infoTxt?: string;
  okBtn?: buttonType;
  cancelBtn?: buttonType;
  formOptions?: formType[];
  isEdit?: boolean; //是否需要显示富文本
  isUpload?: boolean; //是否上传图片
  sendFn?: (data: any) => Promise;
  successCallback?: (values?: any) => void;
  editorName?: string;
  fileRules?: string[];
  maxSize?: number;
};

export const useModal = (props: modalPropsType = {}) => {
  const {
    type = "nomal",
    title = "提示",
    infoTxt = "这是一段提示",
    okBtn = {
      txt: "确定",
      type: "primary",
      isDanger: false,
    },
    cancelBtn = {
      txt: "取消",
      type: "default",
      isDanger: false,
    },
    successCallback = () => {},
    formOptions = [],
    isEdit = false,
    isUpload = false,
    sendFn, //发送数据函数(记得数据处理)
    editorName,
    fileRules,
    maxSize,
  } = props;
  const [show, setShow] = useState(false);
  const [promiseRes, setPromiseRes] = useState();
  const [containerEle, setContainerEle] = useState(null);
  const [messageApi, contextHolder] = message.useMessage();
  // 原本默认值时数组导致输入有问题
  const [defaultValue, setDefaultValue] = useState({});
  const [root, setRoot] = useState(null);
  // 卸载节点
  const unMounted = useCallback(() => {
    if (containerEle) {
      document.body.removeChild(containerEle);
      setContainerEle(null);
      root?.unmount();
    }
  }, [containerEle, root]);
  // 点击确定按钮的回调函数
  const success = useCallback(
    async (values: any) => {
      promiseRes?.resolve(type === "nomal" ? "确定" : values);
      setShow(false);
      unMounted();
      if (sendFn) {
        await sendFn(values);
        // 可进行数据更新
        successCallback && successCallback();
        messageApi.open({
          type: "warning",
          content: "This is a warning message",
        });
      }
    },
    [promiseRes, unMounted, successCallback, type, sendFn, messageApi],
  );
  // 取消
  const cancel = useCallback(() => {
    promiseRes?.reject("取消");
    setShow(false);
    messageApi.open({
      type: "warning",
      content: "已取消",
    });
    unMounted();
  }, [unMounted, promiseRes, messageApi]);
  // 获取form表单结果
  const { MyForm } = useForm({
    cancel,
    success,
    okBtn,
    cancelBtn,
    options: formOptions,
    isEdit,
    isUpload,
    editorName,
    fileRules,
    maxSize,
  });
  // 挂载节点
  useEffect(() => {
    if (!show || !containerEle) {
      return;
    }
    // 根据类型,去判断是简单的弹窗还是form表单
    root.render(
      
        {contextHolder}
        
                {okBtn.txt}
              ,
              ,
            ]
          }
          getContainer={containerEle as HTMLElement}
          >
          {type === "form" && (
            
          )}
          {type === "nomal" && 

{infoTxt}

}
, ); }, [ show, MyForm, root, cancel, containerEle, title, infoTxt, okBtn, cancelBtn, success, type, contextHolder, defaultValue, ]); // 初始化 const init = (defaultValue?: any) => { defaultValue && setDefaultValue(defaultValue); setShow(true); // 创建挂载节点 const div = document.createElement("div"); div.id = "myContainer"; document.body.append(div); setContainerEle(div); setRoot(ReactDOM.createRoot(div as HTMLElement)); return new Promise((resolve, reject) => { setPromiseRes({ resolve, reject }); }); }; return { init, messageApi }; };
  • form表单生成hook
import {
  Button,
  Form,
  FormInstance,
  Input,
  Space,
  DatePicker,
  Select,
  Switch,
  Radio,
  InputNumber,
  TimePicker,
} from "antd";
import React, { useEffect, useState } from "react";
import { useCallback } from "react";
import { buttonType } from "./modal";
import { formType } from "../types/hooksTypes/form";
import { MyEditor } from "../components/utils/MyEditor";
import { MyUpload } from "../components/utils/MyUpload";

const { RangePicker } = DatePicker;
/*
  传递配置对象()
  1. 成功回调
  2.失败回调
  3.配置对象(自动生成form表单)
  4.类型是否使用自定义控件
  */
type formProp = {
  success: (values: any) => void;
  cancel: () => void;
  okBtn: buttonType;
  cancelBtn: buttonType;
  options?: formType[]; //普通组件配置对象
  isEdit?: boolean; //是否需要显示富文本
  isUpload?: boolean; //是否上传图片
  editorName?: string;
  fileRules?: string[];
  maxSize?: number;
};

type MyformProp = {
  defaultValue: any;
};
// 使用富文本字段是comment,上传文件是file
export const useForm = (formProp: formProp) => {
  const {
    success,
    cancel,
    okBtn,
    cancelBtn,
    options = [],
    isEdit,
    isUpload,
    editorName,
    fileRules = ["image/png", "image/jpg", "image/jpeg", "image/webp"],
    maxSize = 5,
  } = formProp;
  const MyForm = ({ defaultValue = {} }: MyformProp) => {
    const formRef = React.useRef(null);
    const [html, setHtml] = useState("");
    const [txt, setTxt] = useState("");
    const [fileList, setFileList] = useState([]);
    // 初始化
    useEffect(() => {
      formRef.current?.setFieldsValue(defaultValue);
    }, [defaultValue]);
    const onFinish = useCallback(
      (values: any) => {
        if (isEdit) {
          if (txt.replace(/(^\s*)|(\s*$)/g, "") === "") {
            formRef.current?.setFields([
              { name: editorName!, errors: ["请输入内容"] },
            ]);
            return;
          }
          values[editorName!] = html;
        }
        if (isUpload) {
          if (fileList.length === 0) {
            formRef.current?.setFields([
              { name: "file", errors: ["请上传图片"] },
            ]);
            return;
          }
          const notTrueFile = fileList.filter((item: any) => {
            return !fileRules.includes(item.type);
          });
          if (notTrueFile.length > 0) {
            formRef.current?.setFields([
              { name: "file", errors: ["请上传指定格式文件"] },
            ]);
            return;
          }
          // 判断文件大小
          const notTrueSizeFile = fileList.filter((item: any) => {
            return item.size > maxSize * 1024 * 1024;
          });
          if (notTrueSizeFile.length > 0) {
            formRef.current?.setFields([
              { name: "file", errors: ["文件过大"] },
            ]);
            return;
          }
          values.file = fileList;
        }
        success(values);
      },
      [html, fileList, txt],
    );
    const fileChange = useCallback((fileList: any) => {
      if (fileList.length >= 0) {
        formRef.current?.setFields([{ name: "file", errors: [""] }]);
      }
      setFileList(fileList);
    }, []);
    const onFinishFailed = useCallback((values: any) => {}, []);
    const onReset = useCallback(() => {
      formRef.current?.resetFields();
    }, []);
    const htmlOnChange = useCallback((values: string, txt: string) => {
      if (txt.replace(/(^\s*)|(\s*$)/g, "") !== "") {
        formRef.current?.setFields([{ name: editorName!, errors: [""] }]);
      }
      setTxt(txt);
      setHtml(values);
    }, []);
    return (
      
{options.map((item: formType, index: number) => { let attr = {}; if (item.isMultiple) { attr = { mode: "multiple", }; } return item.Custom ? ( // 存放自定义组件 ) : item.type === "switch" ? ( {/* 开关 */} {item.type === "switch" ? ( ) : null} ) : ( {/* 普通输入框 */} {item.type === "input" ? ( ) : null} {/* 时间 */} {item.type === "timeDefault" ? ( ) : null} {/* 日期范围 */} {item.type === "timeRange" ? ( ) : null} {/* 多选框 */} {item.type === "select" ? ( ) : null} {/* 富文本 */} {item.type === "editor" ? ( ) : null} {/* 文本框 */} {item.type === "textArea" ? ( ) : null} {/* 文件 */} {item.type === "file" ? ( ) : null} {/* 单选框(主要是性别) */} {item.type === "radio" ? ( {item.data?.map((data: any) => { return ( {data[item.dataName!]} ); })} ) : null} {/* 数字框 */} {item.type === "inputNumber" ? ( ) : null} ); })}
); }; return { MyForm, }; };

针对于上边的form表单类型,我还自定义了两种自己封装的类型,一个是富文本类型,一种是文件类型
富文本类型

import React, { useState, useEffect } from "react";
import "@wangeditor/editor/dist/css/style.css";
import { Editor, Toolbar } from "@wangeditor/editor-for-react";

type editorType = {
  handelChange: (value: any, txt: any) => void;
};

export const MyEditor = ({ handelChange }: editorType) => {
  const [editor, setEditor] = useState(null); // 存储 editor 实例
  const [html, setHtml] = useState("");

  const toolbarConfig = {};
  const editorConfig = {
    placeholder: "请输入内容...",
    autoFocus: false,
    //插入图片
    MENU_CONF: {
      uploadImage: {
        // 单个文件的最大体积限制,默认为 2M
        maxFileSize: 4 * 1024 * 1024, // 4M
        // 最多可上传几个文件,默认为 100
        maxNumberOfFiles: 10,
        // 超时时间,默认为 10 秒
        timeout: 5 * 1000, // 5 秒
        // 用户自定义上传图片
        async customUpload(file: any, insertFn: any) {
          const formdata = new FormData();
          formdata.append("file", file);
        },
      },
    },
  };

  // 及时销毁 editor
  useEffect(() => {
    return () => {
      if (editor == null) return;
      editor.destroy();
      setEditor(null);
    };
  }, [editor]);

  return (
    <>
      
{ setHtml(editor.getHtml().replace(/(^\s*)|(\s*$)/g, "")); handelChange( editor.getHtml().replace(/(^\s*)|(\s*$)/g, ""), editor.getText() ); }} mode="default" style={{ height: "300px" }} />
); };

文件类型

import React, { useEffect, useState } from "react";
import { PlusOutlined } from "@ant-design/icons";
import { Modal, Upload } from "antd";
import type { RcFile, UploadProps } from "antd/es/upload";
import type { UploadFile } from "antd/es/upload/interface";

// 传递改变函数,限制图片个数,是否裁剪
export function MyUpload({
  onChangeFn,
  fileList,
  limit,
}: {
  onChangeFn: (file: any) => void;
  fileList: any;
  limit: number;
}) {
  const getBase64 = (file: RcFile): Promise =>
    new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.readAsDataURL(file);
      reader.onload = () => resolve(reader.result as string);
      reader.onerror = (error) => reject(error);
    });

  const [previewOpen, setPreviewOpen] = useState(false);
  const [previewImage, setPreviewImage] = useState("");
  const [previewTitle, setPreviewTitle] = useState("");
  const [uploadFileList, setUploadFileList] = useState([]);

  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),
    );
  };
  useEffect(() => {
    if (fileList) {
      setUploadFileList(fileList);
      onChangeFn(fileList);
    }
  }, [fileList, onChangeFn]);

  const handleChange: UploadProps["onChange"] = ({ fileList: newFileList }) => {
    setUploadFileList(newFileList);
    onChangeFn(newFileList);
  };
  return (
    <>
       false}
        >
        {uploadFileList.length >= limit ? null : (
          
上传
)}
example ); }

总结

以上就是完整的代码以及解决的一些问题,随后遇到什么问题再修改吧

你可能感兴趣的:(react.js,前端,前端框架)