React之Antd-Modal-Ts二次封装

1,815 阅读5分钟

常规使用

import { FC, useCallback, useState } from 'react'
import {Button,Modal,Spin} from 'antd'
import TestComponent from '@/components/TestComponent'
const DemoA:FC = ()=>{
  const [visible,setVisible] = useState<boolean>(false)
  const [content,setContent] = useState<string>('')
  const [spinning,setSpinning] = useState<boolean>(false)
  const [confirmLoading,setConfirmLoading] = useState<boolean>(false)
​
  // 网络请求
  const mock1 = useCallback(()=>{
    return new Promise<Mock>((resolve)=>{
      setTimeout(()=>{
        resolve({
          code:0,
          message:'React好难啊,救救我~🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆'
        })
      },2000)
    })
  },[])
  const mock2 = useCallback(()=>{
    return new Promise<Mock>((resolve)=>{
      setTimeout(() => {
        resolve({
          code:0,
          message:'摆烂🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆'
        })
      }, 2000);
    })
  },[])
  // 打开对话框
  const showModal = useCallback(async () => {
    setContent('')
    setSpinning(true)
    setVisible(true)
    const res = await mock1()
    if(res.code === 0){
      setContent(res.message)
      setSpinning(false)
    }
  },[content])
  const handleOk = useCallback(async() => {
    setConfirmLoading(true)
    const res = await mock2()
    if(res.code === 0) {
      setConfirmLoading(false)
      setVisible(false)
      console.log(res.message);  
    }
  },[])
  const handleCancel = useCallback(() => {
    setVisible(false)
  },[])
​
  return (
    <div>
      <Modal
        destroyOnClose
        title='弹窗标题'
        visible={visible}
        onOk={handleOk}
        onCancel={handleCancel}
        confirmLoading={confirmLoading}
        bodyStyle={{
          height:'300px',
          display:'flex',
          alignItems:'center',
          justifyContent:'center',
          fontSize:'18px'
        }}
        okButtonProps = {{
          style:{
            display: content ? "" : "none"
          }
        }}
      >
        <Spin spinning={spinning}>
          <TestComponent message={content}></TestComponent>
        </Spin>
      </Modal>
      <Button
        onClick={showModal}
      >显示弹窗A</Button>
    </div>
  )
}
​
export default DemoAinterface Mock {
  code:number
  message:string
}
​

实现效果

常规使用效果.gif

从上图可以看出,如果是正常使用,对话框的loading,显示隐藏我们需要自己去控制,而我们使用这个组件,其实更希望去注重对话框的逻辑,内容展示等,所以我们就可以对它进行二次封装,让一些简单而又必要操作让它自己控制

初步封装(DemoB)

Modal.tsx

// src/components/Modal/Modal.tsx   
import React, {
  forwardRef,
  ForwardRefRenderFunction,
  useCallback,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import { Modal as AntdModal, Spin } from 'antd';
import type {ModalRef,ModalProps} from './type'
​
const Modal: ForwardRefRenderFunction<ModalRef, ModalProps> = (
  props,
  ref
) => {
  const { children, onOk, onCancel, ...reset } = props;
  const [spinning, setSpinning] = useState<boolean>(false);
  const [visible, setVisible] = useState<boolean>(false);
  const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
  const isStop = useRef<boolean>(false);
​
  const stopClose = useCallback(() => {
    isStop.current = true;
  }, []);
​
  const handleOnOK = useCallback(
    async (event: React.MouseEvent<HTMLElement>) => {
      setConfirmLoading(true);
      onOk && (await onOk({ ...event, stopClose }));
      setConfirmLoading(false);
      if (isStop.current) isStop.current = false;
      else setVisible(false);
    },
    [onOk, stopClose]
  );
​
  const handleOnCancel = useCallback(() => {
    onCancel && onCancel();
    setVisible(false);
  }, [onCancel]);
​
  useImperativeHandle(ref, () => ({
    async showModal(options = {}) {
      const { afterShowModal } = options;
      setVisible(true);
      if (afterShowModal) {
        setSpinning(true);
        await afterShowModal();
        setSpinning(false);
      }
    },
    closeModal() {
      setVisible(false);
    },
  }));
​
  return (
    <AntdModal
      {...reset}
      visible={visible}
      confirmLoading={confirmLoading}
      onOk={handleOnOK}
      onCancel={handleOnCancel}
    >
      <Spin spinning={spinning}>{children}</Spin>
    </AntdModal>
  );
};
​
export default forwardRef(Modal);

type.ts

// src/components/Modal/type.ts 
import {ModalProps as AntdModalProps} from 'antd';
​
interface Options {
  afterShowModal?(): void | Promise<void>;
}
​
export interface ModalRef {
  showModal(options?: Options): Promise<void>;
  closeModal(): void;
}
​
export interface ModalProps
  extends Omit<
    AntdModalProps,
    'onOk' | 'onCancel'
  > {
  onOk?:OnOkType
  onCancel?(): void | Promise<void>;
}
​
export type OnOkType = (event:React.MouseEvent<HTMLElement> & { stopClose: () => void }) => void | Promise<void>

初步封装使用

DemoB.tsx

import React, { useCallback, useRef, useState } from 'react'
import { Button } from 'antd'
import Modal from '../components/Modal/Modal'
import TestComponent from '@/components/TestComponent'const DemoB = () => {
  const [content, setContent] = useState<string>('')
  const modalRef = useRef<React.ElementRef<typeof Modal>>(null)
  // 模拟网络请求
  const mock1 = () =>{
    return new Promise<Mock>((resolve)=>{
      setTimeout(() => {
        resolve({
          code:0,
          message:'React好难啊,救救我~🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆'
        })
      }, 3000);
    })
  }
  // 模拟网络请求
  const mock2 = () =>{
    return new Promise<Mock>((resolve)=>{
      setTimeout(() => {
        resolve({
          code:0,
          message:'摆烂🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆'
        })
      }, 3000);
    })
  }
  // 打开对话框
  const handleShowModal = useCallback(() => {
    setContent('')
    modalRef.current?.showModal({
      afterShowModal() {
        return new Promise(async(resolve) => {
          const res = await mock1()
          if(res.code === 0) {
            setContent(res.message)
            resolve()
          }
        })
      }
    })
  }, [])
  // 确定
  const handleOnOk = (event: React.MouseEvent<HTMLElement> & { stopClose: () => void }) => {
    return new Promise<void>(async(resolve) => {
      // 1.是否需要做校验
      // event?.stopClose()
      // resolve()
      // console.log('校验不通过');
      // 2.校验通过
      const res = await mock2()
      if(res.code === 0) {
        console.log(res.message);      
        resolve()
      }
    })
  }
  // 取消
  const handleOnCancle = () => {
    console.log('🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆');
  }
  return (
    <div>
      <Modal
        destroyOnClose
        title='弹窗标题'
        ref={modalRef}
        onOk={handleOnOk}
        onCancel={handleOnCancle}
        okButtonProps = {{
          style:{
            display: content ? "" : "none"
          }
        }}
        bodyStyle={{
          height:'300px',
          display:'flex',
          alignItems:'center',
          justifyContent:'center',
          fontSize:'18px'
        }}
      >
        <TestComponent message={content}></TestComponent>
      </Modal>
      <Button onClick={handleShowModal}>显示弹窗B</Button>
    </div>
  )
}
​
export default DemoBinterface Mock {
  code:number
  message:string
}

实现效果

校验不通过

校验不通过.gif

校验通过

校验通过.gif

由图可以看出,实现效果其实跟常规使用一样,但是区别就是我们不需要去关注什么时候去控制loading变量,什么时候去控制对话框关闭变量了,而且,如果我们需要给这个对话框加额外的功能比如说拖拽等,我们就可以将这些功能全部写在封装好的组件里面,通过传相应标识符来决定该功能用不用,而不是在每次需要用该功能就去写一次

加对话框拖拽效果(DemoC)

要实现拖拽效果之前,首先要搞清楚思路,如何实现

原生实现可先参考俺的另一篇文章:如何用原生写法实现一个拖拽效果

React的实现拖拽方法其实也是类似,核心在与onmousedown事件、onmouseup事件、onmousemove事件

所以需要在封装好的modal.tsx里面再写一个拖拽方法并运用它

实现原理跟原生的一模一样,这里通过传一个参数 isDrag 来决定对话框是否有拖拽效果

// 拖拽方法
  const onMouseDown = (e: React.MouseEvent<HTMLElement>) => { 
    e.preventDefault()
    const content = document.getElementsByClassName("ant-modal-content")[0] as HTMLDivElement;
    const contentHeight = content.getBoundingClientRect().height
    const contentWidth = content.getBoundingClientRect().width
    // 记录初始拖动鼠标位置
    const startX = e.clientX
    const startY = e.clientY
    
    const { styleLeft, styleTop } = styleLT
    // console.log('222',styleLT);
    
    // 添加鼠标移动事件
    document.onmousemove = (e) => {      
      let cx = e.clientX - startX + styleLeft
      let cy = e.clientY - startY + styleTop
      if (cx < 0) {
        cx = 0;
      }
      if (cy < 0) {
        cy = 0;
      }
      if (cx + contentWidth > window.innerWidth){
        cx = window.innerWidth - contentWidth
      }
      if(cy + contentHeight > window.innerHeight){
        cy = window.innerHeight - contentHeight
      }
      setStyleLT({
        styleLeft: cx,
        styleTop: cy
      })
      
    }
    // 鼠标松开去除移动事件
    document.onmouseup = (e) => {
      document.onmousemove = null
    }
  }

完整代码

import React, {
  forwardRef,
  ForwardRefRenderFunction,
  useCallback,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import { Modal as AntdModal, Spin } from 'antd';
import type { ModalRef, ModalProps } from './type'
import './Modal.css'
​
const Modal: ForwardRefRenderFunction<ModalRef, ModalProps> = (
  props,
  ref
) => {
  const { children,isDrag, onOk, onCancel,title, ...reset } = props;
  const [spinning, setSpinning] = useState(false);
  const [visible, setVisible] = useState(false);
  const [confirmLoading, setConfirmLoading] = useState(false);
  const isStop = useRef(false);
  
  const [styleLT, setStyleLT] = useState({
    styleLeft: window.innerWidth/2 - 400,
    styleTop: window.innerHeight/2 - 250
  })
  // console.log('111',styleLT);
  
  
  const style = {
    left: styleLT.styleLeft,
    top: styleLT.styleTop
  }
  // 阻止弹窗关闭
  const stopClose = useCallback(() => {
    isStop.current = true;
  }, []);
  // 确定触发
  const handleOnOK = useCallback(
    async (event: React.MouseEvent<HTMLElement>) => {
      setConfirmLoading(true);
      onOk && (await onOk({ ...event, stopClose }));
      setConfirmLoading(false);
      if (isStop.current) isStop.current = false;
      else setVisible(false);
    },
    [onOk, stopClose]
  );
  // 取消触发
  const handleOnCancel = useCallback(() => {
    onCancel && onCancel();
    setVisible(false);
  }, [onCancel]);
  // 弹窗关闭后触发
  const afterClose = useCallback(() => {
    if(isDrag){
      setStyleLT({
        styleLeft: window.innerWidth/2 - 400,
        styleTop: window.innerHeight/2 - 250
      })
    }
  },[]) 
  // 拖拽方法
  const onMouseDown = (e: React.MouseEvent<HTMLElement>) => { 
    e.preventDefault()
    const content = document.getElementsByClassName("ant-modal-content")[0] as HTMLDivElement;
    const contentHeight = content.getBoundingClientRect().height
    const contentWidth = content.getBoundingClientRect().width
    // 记录初始拖动鼠标位置
    const startX = e.clientX
    const startY = e.clientY
    
    const { styleLeft, styleTop } = styleLT
    // console.log('222',styleLT);
    
    // 添加鼠标移动事件
    document.onmousemove = (e) => {      
      let cx = e.clientX - startX + styleLeft
      let cy = e.clientY - startY + styleTop
      if (cx < 0) {
        cx = 0;
      }
      if (cy < 0) {
        cy = 0;
      }
      if (cx + contentWidth > window.innerWidth){
        cx = window.innerWidth - contentWidth
      }
      if(cy + contentHeight > window.innerHeight){
        cy = window.innerHeight - contentHeight
      }
      setStyleLT({
        styleLeft: cx,
        styleTop: cy
      })
      
    }
    // 鼠标松开去除移动事件
    document.onmouseup = (e) => {
      document.onmousemove = null
    }
  }
  // 向外暴露方法
  useImperativeHandle(ref, () => ({
    // 打开弹窗
    async showModal(options = {}) {
      const { afterShowModal } = options;
      setVisible(true);
      if (afterShowModal) {
        setSpinning(true);
        await afterShowModal();
        setSpinning(false);
      }
    },
    // 关闭弹窗
    closeModal() {
      setVisible(false);
    },
  }));
​
​
  return (
    <AntdModal
      {...reset}
      visible={visible}
      confirmLoading={confirmLoading}
      onOk={handleOnOK}
      onCancel={handleOnCancel}
      style={style}
      afterClose={afterClose}
      // title={title}
      title= {
        <div
          className= {isDrag ? 'dragBoxBar' : ''}
          onMouseDown={ isDrag ? onMouseDown : ()=> {}}
        >
          {title}
        </div>
      }
    >
      <Spin spinning={spinning}>{children}</Spin>
    </AntdModal>
  );
};
​
export default forwardRef(Modal);

实现效果

拖拽效果 00_00_00-00_00_30.gif

封装后结合小案例(DemoD)

import React, { useCallback, useRef, useState } from 'react'
import moment from 'moment'
import Modal from '../components/SuperModal/Modal'
import { Button } from 'antd'
import TodoList from '../components/TodoList'const mock1 = () =>{
  return new Promise<Mock>((resolve)=>{
    setTimeout(() => {
      resolve({
        code:0,
        data:[{
          id:Math.random().toString().slice(2),
          title:'React好难啊,救救我~🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆',
          createTime:moment().valueOf()
        }]
      })
    }, 2000);
  })
}
​
const DemoD = () => {
  const [content,setContent] = useState<any[]>([])
  const modalRef = useRef<React.ElementRef<typeof Modal>>(null)
​
  const handleShowModal = useCallback(()=>{
    setContent([])
    modalRef.current?.showModal({
      afterShowModal(){
        return new Promise(async(resolve) => {
          const res = await mock1()
          if(res.code === 0) {
            setContent([...content,...res.data])
            resolve()
          }
        })
      }
    })
  },[])
​
  const handleOnOk = useCallback((event:React.MouseEvent<HTMLElement> & { stopClose: () => void }) => {
    return new Promise<void>((resolve) => {
      console.log('确定了~🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆');
      // event?.stopClose()
      // resolve
      setTimeout(()=>{
        resolve()
      },2000)
    })
  },[])
​
  const handleCancle = useCallback(() => {
    console.log('取消了~🦆🦆');
  },[])
​
  return (
    <>
      <Modal
        isDrag
        title='弹窗标题'
        // title= {
        //   <div
        //     className= {'dragBoxBar'}
        //     onMouseDown={ modalRef.current?.onMouseDown}
        //   >
        //     弹窗标题
        //   </div>
        // }
        ref={modalRef}
        onOk={ handleOnOk }
        onCancel={ handleCancle }
      >
        {
          <TodoList data={content}></TodoList>
        }
      </Modal>
      <Button onClick={handleShowModal}>弹窗D</Button>
    </>
  )
}
​
export default DemoDinterface Mock {
  code:number
  data:any[]
}

实现效果

最终效果 00_00_00-00_00_30.gif