之前写过一个react Hook+antd弹窗,虽然功能实现了,但是再使用的时候仍然会有报错,虽然这个报错不影响使用的,但是,作为一个合格的前端切图仔,要再使用中发现问题,改正问题。
问题
- 多次调用hook会创建多个相同id的盒子加入页面,并且未初始化就已经有盛放弹窗的盒子
- 使用不够简便,一些文件的格式判断以及大小判断没有做
- 在热更新时如果弹窗处于打开状态,会报错节点容器已经被创建
问题出现原因及修改办法
针对第一个问题和第三个我呢提主要是因为在调用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<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}<ModalonCancel={cancel}open={show}onOk={success}destroyOnClose={true}title={title}wrapClassName="modal-wrap"cancelButtonProps={{ shape: "round" }}okButtonProps={{ shape: "round" }}width={900}footer={type === "form"? null: [<Buttonkey="success"type={okBtn.type}onClick={success}danger={okBtn.isDanger}>{okBtn.txt}</Button>,<Buttonkey="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
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 (<Formref={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.Itemkey={`${index}-${item.name}`}label={item.label}name={item.name}rules={item.rules}valuePropName="checked">{/* 开关 */}{item.type === "switch" ? (<SwitchcheckedChildren={item.openTxt}unCheckedChildren={item.closeTxt}/>) : null}</Form.Item>) : (<Form.Itemkey={`${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.Optionvalue={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.TextAreashowCount={item.isShowTxtCount}placeholder={item.placeholder}maxLength={item.limit}></Input.TextArea>) : null}{/* 文件 */}{item.type === "file" ? (<MyUploadfileList={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" ? (<InputNumbermin={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><ButtononClick={cancel}type={cancelBtn.type}danger={cancelBtn.isDanger}>{cancelBtn.txt}</Button></Space></Form.Item></Form>);};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<any>(null); // 存储 editor 实例const [html, setHtml] = useState<string>("");const toolbarConfig = {};const editorConfig = {placeholder: "请输入内容...",autoFocus: false,//插入图片MENU_CONF: {uploadImage: {// 单个文件的最大体积限制,默认为 2MmaxFileSize: 4 * 1024 * 1024, // 4M// 最多可上传几个文件,默认为 100maxNumberOfFiles: 10,// 超时时间,默认为 10 秒timeout: 5 * 1000, // 5 秒// 用户自定义上传图片async customUpload(file: any, insertFn: any) {const formdata = new FormData();formdata.append("file", file);},},},};// 及时销毁 editoruseEffect(() => {return () => {if (editor == null) return;editor.destroy();setEditor(null);};}, [editor]);return (<><div style={{ border: "1px solid #ccc", zIndex: 100 }}><Toolbareditor={editor}defaultConfig={toolbarConfig}mode="default"style={{ borderBottom: "1px solid #ccc" }}/><EditordefaultConfig={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></>);
};
文件类型
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 (<><UploadlistType="picture-card"fileList={uploadFileList}onPreview={handlePreview}onChange={handleChange}beforeUpload={() => false}>{uploadFileList.length >= limit ? null : (<div><PlusOutlined /><div style={{ marginTop: 8 }}>上传</div></div>)}</Upload><Modalopen={previewOpen}title={previewTitle}footer={null}onCancel={handleCancel}><img alt="example" style={{ width: "100%" }} src={previewImage} /></Modal></>);
}
总结
以上就是完整的代码以及解决的一些问题,随后遇到什么问题再修改吧