弹窗的主体设计没什么特别,就是把细分化后的各个功能封装在一个个的小组件内,然后再整合。这样逻辑就分开了,不乱。
弹窗容器
这个容器是弹窗主体的根组件(不含遮罩),要能根据主题的变化能做出相应的改变。还要记录渲染后的主体的大小参数,原为有最大化和最小化的操作,这些操作是依据当前主体的大小而计算得来的。
_ModelContianer.jsx
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import { useRef, useState } from 'react';
import { Paper } from '@mui/material';
import { useModelState } from './_useModel';
import { infoLevel } from './_ModelConfigure';
// 计算不同状态下的高度
const calHeight = (sizeMode, normalHeight) => {
switch (sizeMode) {
case 0:
return '45px';
case 1:
return normalHeight > 0 ? normalHeight + 'px' : 'auto';
case 2:
return '100vh';
default:
return 'auto';
}
}
// 最大化时的固定样式
const maxSizeCss = css`
width: 100vw;
height: 100vh;
top: 0;
left: 0;
`;
/**
* 弹窗容器
* @param {*} param0
* @returns
*/
const ModelContainer = ({ children }) => {
const modelState = useModelState();
const {
stateMode, // 弹窗的状态,0: 最小化, 1: 正常, 2: 最大化
level, // 弹窗的类型(主要是颜色类型),选项有:default, error, warning, success, info
isDark, //是否是暗黑模式
} = modelState;
const [nomalSize, setNormalSize] = useState({ width: 0, height: 0 });
const containerRef = useRef(null);
return (
<Paper
ref={containerRef}
css={css`
border: 1px solid #A0A0A0;
border-radius: 5px;
width: ${ stateMode === 2 ? '100vw' : stateMode === 0 ? '300px' : '576px' };
height: ${ calHeight(stateMode, nomalSize.height) };
overflow: hidden;
max-width: 100%;
max-height: 100vh;
display: flex;
flex-direction: column;
background-color: ${isDark ? '#333' : infoLevel[level] ? infoLevel[level].color : "white" };
${stateMode === 2 ? maxSizeCss : null};
transition: all 0.3s;
`}
onPointerEnter={() => {
if (nomalSize.width === 0) {
const rect = containerRef.current.getBoundingClientRect();
setNormalSize({
width: rect.width,
height: rect.height,
});
}
}}
>
{
children
}
</Paper>
);
};
export default ModelContainer;
控制按钮
就是最大化、最小化、关闭按钮,最大化按钮和最小化按钮的功能是互斥的,也就是说最大化时不能最小化,最小化时不能最大化。
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
import Stack from '@mui/material/Stack';
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
import Fullscreen from '@mui/icons-material/Fullscreen';
import MinimizeIcon from '@mui/icons-material/Minimize';
import { useModelState } from './_useModel';
/**
* 弹窗的控制按钮, 最小化,最大化,关闭
* @param { 弹窗大小状态, 0: 最小化; 1: 正常; 2: 最大化} sizeMode
* @param {设置弹窗大小状态} setSizeMode
* @returns
*/
function ModelController() {
const modelstate = useModelState();
const {
stateMode, // 弹窗的状态,0: 最小化, 1: 正常, 2: 最大化
setStateMode, // 设置弹窗的状态
onClose, //关闭弹窗
} = modelstate;
// 最大化事件
const maxmizeEvent = (e) => {
setStateMode(stateMode === 2 ? 1 : 2);
};
// 最小化事件
const minimizeEvent = (e) => {
setStateMode(stateMode === 0 ? 1 : 0);
};
// 关闭事件
const onCloseHandler = (e) => {
e.preventDefault();
e.stopPropagation();
onClose && onClose();
}
return (
<Stack direction="row" spacing={1}>
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={minimizeEvent}
disabled={stateMode === 2}
>
{
<MinimizeIcon fontSize="inherit" />
}
</IconButton>
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={maxmizeEvent}
disabled={stateMode === 0}
>
{
stateMode === 2 ? <FullscreenExitIcon fontSize="inherit" /> : <Fullscreen fontSize="inherit" />
}
</IconButton>
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={onCloseHandler}
>
<CloseIcon fontSize="inherit" />
</IconButton>
</Stack>
);
};
export default ModelController;
标题栏
标题栏有标题图标、标题、控制按钮组。
_ModelHeader.jsx
import React, { useRef } from 'react';
import Stack from '@mui/material/Stack';
import AlertTitle from '@mui/material/AlertTitle';
import ModelController from './_ModelController';
import { useModelState } from './_useModel';
import { iconSize, infoLevel } from './_ModelConfigure';
// 计算标题长度, 最小化时最多显示8个字符, 正常时最多显示20个字符
const calTitle = (sizeMode, title) => {
if (sizeMode === 0) {
return title.length > 8 ? title.substring(0, 8) + "..." : title;
}
return title.length > 20 ? title.substring(0, 20) + " ..." : title;
}
/**
* 弹窗的头部标题栏
* @param {*} props
* @returns
*/
function ModelHeader(props) {
const containerRef = useRef();
const {
level = "default", // 弹窗的类型(主要是颜色类型),选项有:normal, error, warning, success, info
title = "提示", //标题
enableController = true,
...others
} = props;
const { Icon, iColor } = infoLevel[level] || infoLevel["default"]; //弹窗的颜色和图标
const modelstate = useModelState();
const {
stateMode, // 弹窗的状态,0: 最小化, 1: 正常, 2: 最大化
} = modelstate;
return (
<Stack
ref={containerRef}
direction="row"
justifyContent="space-between"
justifyItems={"center"}
spacing={2}
sx={{
p: 1,
borderBottom: stateMode === 0 ? "none" : 1,
borderColor: "divider",
height: 45,
}}
{...others}
>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="center"
spacing={1}
sx={{ userSelect: "none" }}
>
<Icon sx={{ fontSize: iconSize, color: iColor }} />
<AlertTitle
sx={{
userSelect: "none",
textOverflow: "ellipsis",
overflow: "hidden",
wordBreak: "break-all",
whiteSpace: "nowrap",
}}
>
{
calTitle(stateMode, title)
}
</AlertTitle>
</Stack>
{
enableController && <ModelController />
}
</Stack>
);
};
export default ModelHeader;
内容区
这个没什么好说的,就是弹窗的内容呈现部分
_ModelContent.jsx
import React from 'react';
import "./_ModelContent.css";
import Box from '@mui/material/Box';
function ModelContent({ children}) {
return (
<Box
className="noscrollbar"
sx={{
flex: 1,
p: 2,
pt: 1,
pb: 1,
width: "100%",
overflowY: "auto",
userSelect: "none",
}}
>
{
children
}
</Box>
)
}
export default ModelContent;
功能按钮
这个功能按钮带有Loding状态,是MUILab中自带的。
_StateButton.jsx
import React, { useState } from 'react';
import LoadingButton from '@mui/lab/LoadingButton';
/**
* 带Loading状态的按钮
* @param {按钮标题} title
* @param {按钮点击事件, onClick的类型为: (setLoading, setTitle, setDisable, onClose)=> {}} onClick
* @param {模态框的关闭事件} onClose
* @param {是否为警告按钮} attention
* @returns
*/
export default function StateButton({ title, onClick, onClose, attention = false }) {
const [loading, setLoading] = useState(false);
const [disabled, setDisable] = useState(false);
const [actionTitle, setTitle] = useState(title);
const onClickEvent = () => {
onClick(setLoading, setTitle, setDisable, onClose);
}
return (
<LoadingButton
variant={attention ? "contained" : "outlined" }
color={attention ? "error" : "primary"}
disabled={disabled}
loading={loading}
onClick={ onClickEvent }
size="small"
>
{actionTitle }
</LoadingButton>
)
}
功能区
就是根据配置的数据生成多少个功能按钮,如下所示:
_ModelActions.jsx
import React from 'react';
import DialogActions from '@mui/material/DialogActions';
import StateButton from './_StateButton';
function ModelActions({ actions, onClose }) {
return (
<DialogActions sx={{ mr: 1 }}>
{
actions.map((item, index) => {
return (
<StateButton
key={index}
title={item.title}
onClick={item.onClick}
onClose={onClose}
attention={item.attention}
/>
)
})
}
</DialogActions>
)
}
export default ModelActions;
到这里所有主体的子组件都设计 完成了。当然我的文章并没有结束,重头戏还在后面。再回分解。