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>
    </>
  );
}

总结

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

相关推荐
AI大模型-小华4 分钟前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子2 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅2 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni3 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学4 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端
先吃饱再说4 小时前
编写一个颜色选择器之后,我理解了 React 工程化的三层架构
react.js·前端框架·前端工程化
先吃饱再说4 小时前
别说你懂 useEffect:从底层机制到生命周期管理,这篇全讲透了
react.js·前端框架
赵庆明老师4 小时前
Vben精讲:21-详解web-antd:tsconfig.json
前端·json·vim
先吃饱再说4 小时前
从“传事件”到“只传值”:React + TypeScript 组件 Props 设计的两次进化
react.js·前端框架·前端工程化