• 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}
            { shape: "round" }}
              okButtonProps={{ shape: "round" }}
              width={900}
              footer={
                type === "form"
                ? null
                : [
                  ,
                  ,
                ]
              }
              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 }; };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 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 (
          
    { 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 ? ( // 存放自定义组件 ) : 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} ); })} { offset: 8, span: 16 }}>
    ); }; return { MyForm, }; };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273

    针对于上边的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 (
        <>
          
    { border: "1px solid #ccc", zIndex: 100 }}> { borderBottom: "1px solid #ccc" }} /> { setHtml(editor.getHtml().replace(/(^\s*)|(\s*$)/g, "")); handelChange( editor.getHtml().replace(/(^\s*)|(\s*$)/g, ""), editor.getText() ); }} mode="default" style={{ height: "300px" }} />
    ); };
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    文件类型

    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 : (
              
    { marginTop: 8 }}>上传
    )}
    example{ width: "100%" }} src={previewImage} /> ); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    总结

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

  • 相关阅读:
    进程和多线程
    学习Nano编辑器:入门指南、安装步骤、基本操作和高级功能
    2023.9.23(对这一年过去几个月的总结)
    变量的定义和使用
    Linux下安装mysql8.0
    软件工程导论概述-----MP微软编程和MSF
    媒体管理软件Jellyseerr
    (学习日记)2022.7.21
    1.springboot 集成elasticsearch组件
    【数据结构】树与二叉树(三):二叉树的定义、特点、性质及相关证明
  • 原文地址:https://blog.csdn.net/YX0711/article/details/133187981