常规使用
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 DemoA
interface Mock {
code:number
message:string
}
实现效果
从上图可以看出,如果是正常使用,对话框的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 DemoB
interface Mock {
code:number
message:string
}
实现效果
校验不通过
校验通过
由图可以看出,实现效果其实跟常规使用一样,但是区别就是我们不需要去关注什么时候去控制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);
实现效果
封装后结合小案例(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 DemoD
interface Mock {
code:number
data:any[]
}