antd+react Hook弹窗改进版

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

问题

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

问题出现原因及修改办法

针对第一个问题和第三个我呢提主要是因为在调用hook时,就创建了弹窗盛放的容器,这个是不对的,因为虽然调用hook但是并不代表就一定要弹窗,虽然这样说有点牵强,不使用为啥调用弹窗hook。但是主要目的在于我们不应该在调用hook的时候创建容器,而是在调用初始化的时候调用,具体的修改代码,可以直接看完整版的代码

针对于第二个问题,我的解决办法是给定默认大小以及文件格式,如果传递参数就使用传递参数的来判断

完整代码

  • 弹窗hook
tsx 复制代码
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<any>;
  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<boolean>(false);
  const [promiseRes, setPromiseRes] = useState<PromiseType>();
  const [containerEle, setContainerEle] = useState<HTMLElement | null>(null);
  const [messageApi, contextHolder] = message.useMessage();
  // 原本默认值时数组导致输入有问题
  const [defaultValue, setDefaultValue] = useState<any>({});
  const [root, setRoot] = useState<any>(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(
      <ConfigProvider locale={zhCN}>
        {contextHolder}
        <Modal
          onCancel={cancel}
          open={show}
          onOk={success}
          destroyOnClose={true}
          title={title}
          wrapClassName="modal-wrap"
          cancelButtonProps={{ shape: "round" }}
          okButtonProps={{ shape: "round" }}
          width={900}
          footer={
            type === "form"
            ? null
            : [
              <Button
                key="success"
                type={okBtn.type}
                onClick={success}
                danger={okBtn.isDanger}
                >
                {okBtn.txt}
              </Button>,
              <Button
                key="cancel"
                onClick={cancel}
                danger={cancelBtn.isDanger}
                type={cancelBtn.type}
                >
                {cancelBtn.txt}
              </Button>,
            ]
          }
          getContainer={containerEle as HTMLElement}
          >
          {type === "form" && (
            <MyForm defaultValue={defaultValue || {}}></MyForm>
          )}
          {type === "nomal" && <p>{infoTxt}</p>}
        </Modal>
      </ConfigProvider>,
    );
  }, [
    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
tsx 复制代码
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<FormInstance>(null);
    const [html, setHtml] = useState<string>("");
    const [txt, setTxt] = useState<string>("");
    const [fileList, setFileList] = useState<any>([]);
    // 初始化
    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 (
      <Form
        ref={formRef}
        labelCol={{ span: 3 }}
        wrapperCol={{ span: 20 }}
        initialValues={{ remember: true }}
        autoComplete="off"
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
        >
        {options.map((item: formType, index: number) => {
          let attr = {};
          if (item.isMultiple) {
            attr = {
              mode: "multiple",
            };
          }
          return item.Custom ? (
            // 存放自定义组件
            <Form.Item name={item.name} label={item.label}>
              <item.Custom></item.Custom>
            </Form.Item>
          ) : item.type === "switch" ? (
            <Form.Item
              key={`${index}-${item.name}`}
              label={item.label}
              name={item.name}
              rules={item.rules}
              valuePropName="checked"
              >
              {/* 开关 */}
              {item.type === "switch" ? (
                <Switch
                  checkedChildren={item.openTxt}
                  unCheckedChildren={item.closeTxt}
                  />
              ) : null}
            </Form.Item>
          ) : (
            <Form.Item
              key={`${index}-${item.name}`}
              label={item.label}
              name={item.name}
              rules={item.rules}
              >
              {/* 普通输入框 */}
              {item.type === "input" ? (
                <Input placeholder={item.placeholder}></Input>
              ) : null}
              {/* 时间 */}
              {item.type === "timeDefault" ? (
                <TimePicker format={item.format}></TimePicker>
              ) : null}
              {/* 日期范围 */}
              {item.type === "timeRange" ? (
                <RangePicker format={item.format} showTime />
              ) : null}
              {/* 多选框 */}
              {item.type === "select" ? (
                <Select
                  {...attr}
                  style={{ width: 300 }}
                  placeholder={item.placeholder}
                  >
                  {item.data?.map((data: any) => {
                    return (
                      <Select.Option
                        value={data[item.dataValue!]}
                        key={data.id}
                        >
                        {data[item.dataName!]}
                      </Select.Option>
                    );
                  })}
                </Select>
              ) : null}
              {/* 富文本 */}
              {item.type === "editor" ? (
                <MyEditor handelChange={htmlOnChange}></MyEditor>
              ) : null}
              {/* 文本框 */}
              {item.type === "textArea" ? (
                <Input.TextArea
                  showCount={item.isShowTxtCount}
                  placeholder={item.placeholder}
                  maxLength={item.limit}
                  ></Input.TextArea>
              ) : null}
              {/* 文件 */}
              {item.type === "file" ? (
                <MyUpload
                  fileList={defaultValue?.file}
                  onChangeFn={fileChange}
                  limit={item.limit ? item.limit : 1}
                  ></MyUpload>
              ) : null}
              {/* 单选框(主要是性别) */}
              {item.type === "radio" ? (
                <Radio.Group>
                  {item.data?.map((data: any) => {
                  return (
                    <Radio value={data[item.dataValue!]} key={data.id}>
                      {data[item.dataName!]}
                    </Radio>
                  );
                })}
                </Radio.Group>
              ) : null}
              {/* 数字框 */}
              {item.type === "inputNumber" ? (
                <InputNumber
                  min={item.minNumber}
                  max={item.maxNumber}
                  defaultValue={item.minNumber}
                  step={item.step}
                  />
              ) : null}
            </Form.Item>
          );
        })}
        <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
          <Space wrap>
            <Button type={okBtn.type} danger={okBtn.isDanger} htmlType="submit">
              {okBtn.txt}
            </Button>
            <Button danger htmlType="button" onClick={onReset}>
              重置
            </Button>
            <Button
              onClick={cancel}
              type={cancelBtn.type}
              danger={cancelBtn.isDanger}
              >
              {cancelBtn.txt}
            </Button>
          </Space>
        </Form.Item>
      </Form>
    );
  };

  return {
    MyForm,
  };
};

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

富文本类型

tsx 复制代码
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<any>(null); // 存储 editor 实例
  const [html, setHtml] = useState<string>("");

  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 (
    <>
      <div style={{ border: "1px solid #ccc", zIndex: 100 }}>
        <Toolbar
          editor={editor}
          defaultConfig={toolbarConfig}
          mode="default"
          style={{ borderBottom: "1px solid #ccc" }}
          />
        <Editor
          defaultConfig={editorConfig}
          value={html}
          onCreated={setEditor}
          onChange={(editor) => {
            setHtml(editor.getHtml().replace(/(^\s*)|(\s*$)/g, ""));
            handelChange(
              editor.getHtml().replace(/(^\s*)|(\s*$)/g, ""),
              editor.getText()
            );
          }}
          mode="default"
          style={{ height: "300px" }}
          />
      </div>
    </>
  );
};

文件类型

tsx 复制代码
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<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 [previewOpen, setPreviewOpen] = useState(false);
  const [previewImage, setPreviewImage] = useState("");
  const [previewTitle, setPreviewTitle] = useState("");
  const [uploadFileList, setUploadFileList] = useState<any>([]);

  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 (
    <>
      <Upload
        listType="picture-card"
        fileList={uploadFileList}
        onPreview={handlePreview}
        onChange={handleChange}
        beforeUpload={() => false}
        >
        {uploadFileList.length >= limit ? null : (
          <div>
            <PlusOutlined />
            <div style={{ marginTop: 8 }}>上传</div>
          </div>
        )}
      </Upload>
      <Modal
        open={previewOpen}
        title={previewTitle}
        footer={null}
        onCancel={handleCancel}
        >
        <img alt="example" style={{ width: "100%" }} src={previewImage} />
      </Modal>
    </>
  );
}

总结

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

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js