组件实例:
## ModalInstance.tsx
import React, { useEffect, ReactNode } from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import './Modal.scss';
function Modal (props: ModalProps):React.ReactPortal | null {
const {
visible,
className,
closeClassName,
children,
width = 280,
cancelText = '取消',
okText = '确认',
showFooter = false,
cancelable = true,
maskClosable = false,
position = 'center',
onOk = () => {},
onCancel = () => {},
afterClose = () => {},
close = onCancel,
} = props;
const handleModalClick = (ev) => {
ev.stopPropagation();
};
useEffect(() => {
if (!visible) {
afterClose();
}
}, [visible]);
if (!visible) {
return null;
}
return ReactDOM.createPortal(
<div className="CommonModal" onClick={handleModalClick}>
<div className="CommonModal-mask" onClick={maskClosable ? close : () => {}} />
<div
className={classNames('CommonModal-modal', `is-${position}`, className)}
style={{
width: typeof width === 'number' ? `${width}px` : width,
}}
>
<i className={classNames('CommonModal-close', closeClassName)} onClick={close} />
<div>{children}</div>
{!!showFooter && (
<div className="CommonModal-footer">
{cancelable && (
<button className="CommonModal-btn CommonModal-btn--cancel" type="button" onClick={onCancel}>
{cancelText}
</button>
)}
<button className="CommonModal-btn CommonModal-btn--comfirm" type="button" onClick={onOk}>
{okText}
</button>
</div>
)}
</div>
</div>,
document.body,
);
}
export default Modal;
Modal.tsx
import { createRoot } from 'react-dom/client';
import ModalInstance, { ModalProps } from './ModalInstance';
type ConfigUpdate = ModalProps | ((prevConfig: ModalProps) => ModalProps);
export interfave ModalInfoInstance {
destroy: () => void;
update: (configUpdate: ConfigUpdate) => viod;
}
type ModalFunc = (props: ModalProps) => ModalInfoInstance;
type ModalStaticFunctions = Record<NonNullable<ModalProps['type']>, ModalFunc>;
type ModalType = typeof ModalInstance & ModalStaticFunctions
const Modal = ModalInstance as ModalType;
const ModalInfo = (props = {} as ModalProps) => <Modal {...props}>{props.content}</Modal>;
Modal.info = (props: ModalProps) => {
const div = document.createElement('div');
document.body.appendChild(div);
const root = createRoot(div)
let config = {} as ModalProps;
const render = (renderProps: ModalProps) => {
root.render(<ModalInfo {...renderProps} />);
};
const destory = () => {
root.unmount();
if (div.parentNode) {
div.parentNode.removeChild(div);
}
}
const update = (newConfig: ConfigUpdate) => {
config = {
...config,
...newConfig,
};
render(config);
};
const close = () => {
config = {
...config,
visible: false,
afterClose: destroy,
};
render(config);
};
config = {
...props,
onOk: props.onOk || close,
onCancel: props.onCancel || close,
close: props.close || props.onCancel || close,
visible: true,
};
render (config);
return {
destroy: close,
update,
}
};
export default Modal;
Modal.scss
@keyframes normalModalAnimate {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes bottomModalAnimate {
0% {
opacity: .5;
transform: translateY(100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.CommonModal {
.text-elipics {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2999;
}
&-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
z-index: 2999;
&:not(.is-bottom) {
animation: normalModalAnimate .3s;
}
&.is-bottom {
top: auto;
left: 0;
bottom: 0;
transform: none;
animation: bottomModalAnimate .5s;
}
}
&-close {
position: absolute;
cursor: pointer;
}
&-footer {
text-align: center;
}
&-btn {
display: inline-block;
width: 102px;
height: 36px;
margin: 0 4px;
font-size: 13px;
color: #FFF7C5;
font-weight: 600;
text-align: center;
background-repeat: no-repeat;
background-position: center;
background-size: 100% auto;
vertical-align: top;
cursor: pointer;
&--comfirm {
background-image: url(src/assets/modal-btn-confirm.png);
}
&--cancel {
background-image: url(src/assets/modal-btn-cancel.png);
}
}
}