React模态框设计(八)异常修复与优化

228 阅读12分钟

模态框的优化及异常修复

经过一段时间的应用,前面的模态框设计在单体应用上没什么问题,但也暴露出了不少的问题,比如,在多模态框弹出的时候会出现context的数据污染的问题,比如在多模态的情况下注目动画有时会失效,这些都是context数据污染造成的。还有一些小问题等等,这个章节我对这个模态框组件进行了大量而重要的优化,调整了部分数据结构,应用上已经达到比较完美的结果。这一节的知识点比较多,都是经验啊。先看优化后的效果图:

gif_05_01.gif

遮罩的点击事件

之前的设计版本中,我在整个文档中添加了监听事件,以触发弹窗体的动画,这样做有个弊端,就是当有多个弹窗时,点击弹窗外部时所有的可见弹窗都会触发注目动画。这不是我想要的结果,我们要的是点击动作只针对当前活动的弹窗起作用,所以,我现在将_ModelMask.jsx_Draggable.jsx文件进行合并,将mask的点击事件直接写在合并后的代码中,修改_Draggable.jsx的代码如下:

...
export default function Draggable(props) {
    ...
    // 当点击Mask时,弹窗会有一个注目动画效果
    const maskClick = () => {
        setAttentionStyle(anim);
    }

    ...

    return (
        <Box
            css={css`
                ${maskCss};
                ${isVisible && showMaskCss}
              `
            }
            onClick={maskClick}
        >
            <Box
                ref={wrapperRef}
                sx={{
                    // 这里采用了绝对定位设置了top的百分比也可以不用设置定位方式这样top和position都不用设置,默认情况下弹窗会在页面的中间。
                    // position: "absolute",
                    // top: '25%', 
                    transform: `translate(${position.x}px, ${position.y}px)`,
                    cursor: canDrag ? isDragging ? "grabbing" : "grab" : "default",
                    transition: isDragging ? null : `transform 200ms ease-in-out`,
                }}
                onMouseDown={handleMouseDown}
                onMouseMove={onMouseMove}
                onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
            >
                <Box
                    sx={{
                        transform: `${isDragging ? "scale(1.03)" : "scale(1)"}`,
                        transition: `transform 200ms ease-in-out`,
                    }}
                    css={attentionStyle}
                >
                    {
                        children
                    }
                </Box>
            </Box>
        </Box>
    );
}

模态框体的颜色优化

之前对模态框体的背景定义了多种颜色,分别对应 normalerrorwarningsuccessinfo等5种场景,实践证明,这一步是多余的。 对_ModelContainer.jsx修改如下:

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import { useContext, useLayoutEffect, useRef, useState } from 'react';
import { Paper } from '@mui/material';
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;
    `;

/**
 * 弹窗容器,用于包裹弹窗的内容,ModelBody内部使用的组件
 * @param {*} param0 
 * @returns 
 */
const ModelContainer = ({ children, context }) => {
    const modelState = useContext(context);
    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);

    // 获取容器的大小
    useLayoutEffect(() => {
        if (containerRef.current !== null) {
            const rect = containerRef.current.getBoundingClientRect();
            setNormalSize({
                width: rect.width,
                height: rect.height,
            });
        }
    }, []);

    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;
                border: 1px solid ${infoLevel[level].color};
                ${stateMode === 2 ? maxSizeCss : null};
                transition: all 0.3s;
            `}
        >
            {
                children
            }
        </Paper>
    );
};

export default ModelContainer;

关于数据污染的解决方案和思路

由于是采用的模块化设计的思路,所以就绕不开模块与模块之前的数据通信的问题,由其是父组件与子孙组件的数据通信问题,这种情况下Context应当是首先方案, 我也是这么做的,但是之前我只考虑到单模态的场景,而没有考虑到多模态的场景,由其是嵌套弹窗的问题。比如,在一个弹窗中的按钮点击事件中又弹出一个弹窗,这种情况下如何处理Context的数据, 如果在新弹窗弹出后没有关闭父弹窗,那么就不会出现数据污染的问题,因为每一层级的Context的数据都有一个弹窗进行消费,所以就正常,但是如果在弹出子弹窗(新弹窗)时关闭了父弹窗,那么这时的父弹窗的Context就会影响子弹窗,就形成了子弹窗的部分形态继承了父弹窗的状态,这就造成了数据混淆,即数据污染,如何解决呢, 我现行的方法是为每一个弹窗生成一个新的Context, 由于之前的Context的类型是固定的,而不是唯一的,这是造成数据污染的主要原因。然后用这个新的Context 对弹窗进行包裹:

/**
 * 为每一个弹窗生成一个唯一的上下文,避免数据污染问题
 * @returns 
 */
function createDynamicContext() {
    return createContext();
}

那么我们就要对弹窗管理器的数据结构做一个调整,

// 创建model
const model = {
    id: modelId,
    context: newContext,
    body: (
        <ModelBody
            id={modelId}
            onClose={() => closeModal(modelId)}
            context={newContext}
            {...props}
        >
            {content}
        </ModelBody>
    )
};

// 添加model
setModelStack((modelStack) => [...modelStack, model]);

我们增加一个 id,一个新的context, 这样,移除弹窗的时候可以通过 id 来进行处理。_ModelProvider.jsx的整个代码如下:

import React, { useContext, createContext, useState, useCallback } from "react";
import ModelStack from "./_ModelStack";
import ModelBody from "./_ModelBody";
import { randomString } from "../STools/Tools";

// 用于管理ModelProvider的状态,即modelStack和popModel
export const ModelProviderContext = createContext(null);

// 使用ModelProvider的状态
export const useModelProviderState = () => useContext(ModelProviderContext);

/**
 * 为每一个弹窗生成一个唯一的上下文,避免数据污染问题
 * @returns 
 */
function createDynamicContext() {
    return createContext();
}

/**
 * 弹窗提供者,只要在需要弹窗的地方使用usePopModel即可
 * @param {*} param0 
 * @returns 
 */
function ModelProvider({ children }) {
    const [modelStack, setModelStack] = useState([]);

    // 关闭所有弹窗
    const closeAll = () => {
        setModelStack([]);
    }

    // 关闭指定弹窗
    const closeModal = useCallback(
        (modelId) => {
            setModelStack((prevModals) => prevModals.filter((modal) => modal.id !== modelId));
        },
        [modelStack]
    );

    // 弹出一个弹窗
    const popModel = useCallback((configure) => {
        const config = {
            sizeMode: "sm", //弹窗的大小
            level: "default", // 弹窗的类型(主要是颜色类型),选项有:default, error, warning, success, info
            title: "提示", //标题
            content: null, //内容
            enableDragging: true, // 是否允许拖拽
            enableController: false, //是否显示控制按钮
            attentionIndex: -1, //高亮按钮索引,-1表示没有高亮按钮
            actions: [ //操作按钮
                {
                    lable: "确定", //按钮标题
                    onClick: (setLoading, setDisable, onClose) => { onClose(); } //按钮回调
                },
            ],//功能按钮
            ...configure
        }

        const { content, ...props } = config;

        const newContext = createDynamicContext(null);

        const modelId = randomString(32); // 生成随机key, 用于唯一标识model

        // 创建model
        const model = {
            id: modelId,
            context: newContext,
            body: (
                <ModelBody
                    id={modelId}
                    onClose={() => closeModal(modelId)}
                    context={newContext}
                    {...props}
                >
                    {content}
                </ModelBody>
            )
        };

        // 添加model
        setModelStack((modelStack) => [...modelStack, model]);

    }, [])

    // ModelProvider提供的上下文, 包含modelStack用于管理Model, popModel用来弹出model
    return (
        <ModelProviderContext.Provider value={{ modelStack, popModel, closeAll }}>
            {
                children
            }
            <ModelStack modelStack={modelStack} />
        </ModelProviderContext.Provider>
    )
}

export default ModelProvider;

由于数据结构发生了变化,那么对相应的组件也要进行修改:_ModelStack.jsx:

import React from 'react';

/**
 * ModelProvider中的modelStack的管理组件,用于展示modelStack中的所有model
 * @returns 
 */
function ModelStack({ modelStack }) {
    return (
        <React.Fragment>
            {
                modelStack.map((modelObj, index) => {
                    return (
                        <React.Fragment key={index}>
                            { modelObj.body }
                        </React.Fragment>
                    )
                })
            }
        </React.Fragment>
    )
}

export default ModelStack;

_ModelBody.jsx中我们要把context修改为我们生成的context, 并将这个 context 传递给每一个子组件,修改如下:

import React, { useState } from 'react';
import ModelHeader from './_ModelHeader';
import ModelContent from './_ModelContent';
import ModelActions from './_ModelActions';
import ModelContainer from './_ModelContianer';
import Draggable from './_Draggable';
import { useSTheme } from '../STheme/SThemeProvider';

/**
 * 弹窗的主体组件
 * @param {*} param0 
 * @returns 
 */
function ModelBody({
    id, //弹窗的id
    context, //弹窗的上下文
    sizeMode = "sm", //弹窗的大小
    level = "default", // 弹窗的类型(主要是颜色类型),选项有:default, error, warning, success, info
    title = "提示", //标题
    onClose, //关闭弹窗的回调
    enableDragging = true,
    enableController = true, //是否显示控制按钮
    children, //弹窗内容
    attentionIndex = -1, //高亮按钮索引,-1表示没有高亮按钮
    actions //功能按钮
}) {
    const stheme = useSTheme();
    const [stateMode, setStateMode] = useState(1); // 弹窗的状态,0: 最小化, 1: 正常, 2: 最大化

    return (
        <context.Provider value={{
            stateMode, // 弹窗的状态0: 最小化, 1: 正常, 2: 最大化
            setStateMode, // 设置弹窗的状态
            sizeMode, // 弹窗最大宽度
            onClose, // 关闭弹窗的回调
            isDark: stheme.isDark, //是否是暗黑模式
            level, // 弹窗的类型(主要是颜色类型),选项有normal, error, warning, success, info
        }}>

            <Draggable
                enableDragging={enableDragging && stateMode !== 2}
                enableHandler={true}
                stateMode={stateMode}
            >
                <ModelContainer context={context}>
                    <ModelHeader
                        context={context}
                        // 拖拽的handler, 用于拖拽的区域,一定要加上这个class否则无法拖拽, 这个类名在Draggable组件中定义的可以自定义className=".model-handler" 
                        title={title}
                        level={level}
                        onClose={onClose}
                        enableController={enableController}
                    />

                    <ModelContent>
                        {
                            children
                        }
                    </ModelContent>

                    {
                        actions && actions.length > 0 &&
                        <ModelActions
                            context={context}
                            actions={actions}
                            attentionIndex={attentionIndex}
                            onClose={onClose}
                        />
                    }
                </ModelContainer>
            </Draggable>
        </context.Provider>
    );
};

export default ModelBody;

这样大体上就大功告成了。最后就是针对应用,我设计了一些 hooks,以方便使用,如下所示(useAlert.jsx):

import { useModelProviderState } from "./ModelProvider";

/**
 * 弹窗
 * @returns 
 */
export const useAlert = () => {
  const { popModel } = useModelProviderState();

  const alertInfo = (message, title = "提示") => {
    popModel({
      title,
      level: "info",
      content: message,
      enableController: false,
      enableDragging: true,
    });
  };

  const alertWarning = (message, title = "提示") => {
    popModel({
      title,
      level: "warning",
      content: message,
      enableController: false,
      enableDragging: true,
    });
  };

  const alertError = (message, title = "提示") => {
    popModel({
      title,
      level: "error",
      content: message,
      enableController: false,
      enableDragging: true,
    });
  };

  const alertSuccess = (message, title = "提示") => {
    popModel({
      title,
      level: "success",
      content: message,
      enableController: false,
      enableDragging: true,
    });
  };

  const alertNormal = (message, title = "提示") => {
    popModel({
      title,
      level: "default",
      content: message,
      enableController: false,
      enableDragging: true,
    });
  }

  const alert = (message, title = "提示", level = "default") => {
    popModel({
      title,
      level,
      content: message,
      enableController: false,
      enableDragging: true,
    });
  }

  return {
    alertInfo,
    alertWarning,
    alertError,
    alertSuccess,
    alertNormal,
    alert,
  };
}

/**
 * confirm弹窗
 * @param {弹窗内容} message
 * @param {确定按钮回调} onOk
 * @param {取消按钮回调} onCancel
 * onOk: (setLoading, setTitle, setDisable, onClose) => {}
 * onCancel: (setLoading, setTitle, setDisable, onClose) => {}
 */
export const useConfirm = () => {
  const { popModel } = useModelProviderState();
  return (
    title,
    message,
    actions = [
      {
        lable: "取消",
        onClick: (setLoading, setDisable, onClose) => { onClose(); }
      },

      {
        lable: "确定",
        onClick: (setLoading, setDisable, onClose) => { onClose(); }
      },
    ],
    attentionIndex = -1,
  ) => {
    popModel({
      title,
      level: "default",
      content: message,
      enableController: false,
      enableDragging: true,
      attentionIndex,
      actions
    });
  };
}

/**
 * 自定义弹窗
 * @returns 
 */
export const useModel = () => {
  const { popModel } = useModelProviderState();
  return (config = {}) => {
    const defaultConfig = {
      title: "提示",
      content : null,
      level:"default",
      actions:[
        {
          lable: "确定",
          onClick: (setLoading, setDisable, onClose) => { onClose(); }
        },
      ],
      attentionIndex: -1,
      enableDragging: true,
      enableController: true,
      ...config
    };
    popModel(defaultConfig);
  }
}

上面给出了几乎所有场景的应用方式。应该足够用了。

我们来测试一下,测试组件如下所示,就和开头的动图一样,相当的完美(ModelTest.jsx)。

import React, { useState } from 'react';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Masonry from '@mui/lab/Masonry';
import TextField from '@mui/material/TextField';
import ToggleThemeButton from '../../framework-kakaer/STheme/_TaggleThemeButton';
import Typography from '@mui/material/Typography';
import { useAlert, useConfirm, useModel } from '../../framework-kakaer/SModel/useAlert';
import { useModelProviderState } from '../../framework-kakaer/SModel/ModelProvider';

const longContent = `碧玉妆成一树高”,写柳树给人的总体印象。柳树的形象美在于它那曼长披拂的枝条,一年一度,它长出了嫩绿的新叶,丝丝下垂,在春风吹拂中,有着一种迷人的意态。这里的“碧玉”既可指真实的玉,又暗含“碧玉小家女”(《碧玉歌》)中“碧玉”之意,指小户人家出身的年轻秀美的女子。古典诗词常借用柳树的形象美来形容美人苗条的身段、婀娜的腰肢,但此诗别出新意,翻转过来,将柳树化身为美人。用“碧玉”来比柳实际上有两层意思:一是“碧玉”和柳的颜色有关,“碧”和下句的“绿”是互相生发、互为补充的;二是“碧玉”这个人在人们头脑中留下的是年轻的印象,在古代文学作品里,“碧玉”几乎成了年轻貌美的女子的泛称。用“碧玉”来比柳,人们就会想象到这美人还未到丰容盛鬋的年华,这柳也还是早春稚柳,没有到密叶藏鸦的时候,同时和下文的“细叶”“二月春风”又是有联系的。
“万条垂下绿丝绦”,具体描写那茂密并轻柔下垂的柳枝,它是柳树最具代表性的部分。有了上句的铺垫,这千条万缕的垂丝,也随之变成了美人的裙带。上句的“高”字,衬托出美人婷婷袅袅的风姿;下句的“垂”字,暗示出纤腰在风中款摆。诗中没有“杨柳”和“腰肢”字样,然而这早春的垂柳以及柳树化身的美人,却给写活了。《南史》说刘悛之为益州刺史,献蜀柳数株,“条甚长,状若丝缕”。齐武帝把这些杨柳种植在太昌云和殿前,玩赏不置,说它“风流可爱”。这里把柳条说成“绿丝绦”,可能是暗用这个关于杨柳的典故。但这里的化用,几乎看不出一点痕迹。
“不知细叶谁裁出,二月春风似剪刀。”这两句进一步细描细绘,刻画柳树的嫩叶。每一片树叶都造型别致,纹理细腻,仿佛都是精心裁剪而出。诗人由于惊叹不禁发问:这满树的细叶到底出自哪位高明的裁缝之手?接着找到了答案:原来是大自然的杰作,她手持二月春风这把大剪刀裁出了满树春色。绿叶好比美人衣裙上的花纹和图案,至此,那位美人便形神毕现地跃然纸上了。“二月春风似剪刀”这一新巧的比喻,把视之无形又不可捉摸的春风形象化地描绘出来。春风和剪刀,本来全不相干,它们的相同处只存在于诗人的想象之中。因此,“二月春风似剪刀”既新奇,又能唤起人们丰富的联想。
这首诗立意高远,比喻巧妙,先从大处着眼,然后分部描述,越写越细,把柳树的形神栩栩如生地表现了出来。题目是咏柳,但又不仅仅是咏柳,更是咏春,歌咏自然造化。全诗由“碧玉妆成”引出了“绿丝绦”,“绿丝绦”引出了“谁裁出”,最后,那视之无形的不可捉摸的“春风”,也被用“似剪刀”形象化地描绘了出来。这“剪刀”裁制出嫩绿鲜红的花花草草,给大地换上了新妆,它正是自然活力的象征,是春给予人们美的启示。从“碧玉妆成”到“剪刀”,可以看出诗人一系列艺术构思的过程。诗歌里出现的一连串的形象,是一环紧扣一环的。`;

const normalContent = "唐玄宗天宝三载(744),贺知章奉诏告老回乡,百官送行。他坐船经南京、杭州,顺萧绍官河到达萧山县城,越州官员到驿站相迎,然后再坐船去南门外潘水河边的旧宅。此时正是二月早春,柳芽初发,春意盎然,微风拂面。贺知章如脱笼之鸟回到家乡,心情自然格外高兴,即景写下了这首诗。";

const size = 100;
function Item(props) {
    return (
        <Box>
            <Paper
                sx={{
                    height: 100,
                    width: 100,
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    border: 1,
                    borderColor: 'primary.main',
                }}
            >
                {props.children}
            </Paper>
        </Box>
    );
}

function PopModelTest() {
    const {
        alertInfo,
        alertWarning,
        alertError,
        alertSuccess,
    } = useAlert();

    const alertConfirm = useConfirm();
    const alertModel = useModel();
    const { closeAll } = useModelProviderState();

    let userName = '张三';
    let age = 20;

    const InputCompoment = (
        <Stack spacing={2} >
            <TextField
                id="standard-name"
                label="用户名"
                variant="standard"
                defaultValue='张三'
                onChange={(event) => {
                    userName = event.target.value;
                }}
            />
            <TextField
                id="standard-age"
                label="年龄"
                variant="standard"
                defaultValue={20}
                onChange={(event) => {
                    age = event.target.value;
                }}
            />
        </Stack>
    )

    const switchTheme = (
        <Stack>
            <Typography variant="subtitle2" gutterBottom>
                可以在弹窗内部切换主题,这是全局的。点击下面的按钮切换主题
            </Typography>

            <Box><ToggleThemeButton /></Box>
        </Stack>
    )

    return (
        <Box
            sx={{
                width: '100%',
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'center',
                justifyContent: 'center',
                height: '100%',
                bgcolor: 'background.default',
            }}
        >
            <Box>
                <Masonry columns={4} spacing={2}>
                    <Item>
                        <Button onClick={() => alertInfo('这是提示信息,请注意', "提示")}>Info</Button>
                    </Item>

                    <Item>
                        <Button onClick={() => alertError('系统在操作过程中出现了异常!', "错误")}>Error</Button>
                    </Item>

                    <Item>
                        <Button onClick={() => alertWarning('您开在进行很危险的操作!!!', "警告")}>warining</Button>
                    </Item>

                    <Item>
                        <Button onClick={() => alertSuccess('操作成功,恭喜!', "成功")}>success</Button>
                    </Item>

                    <Item>
                        <Button onClick={
                            () => alertConfirm(
                                '提示',
                                normalContent,
                                [
                                    {
                                        lable: '取消',
                                        onClick: (setLoading, setDisable, onClose) => { onClose(); }
                                    },

                                    {
                                        lable: '完成',
                                        onClick: (setLoading, setDisable, onClose) => {
                                            onClose();
                                            alertSuccess("操作成功", '成功');
                                        }
                                    }
                                ]
                            )}
                        >询问弹窗</Button>
                    </Item>

                    <Item>
                        <Button onClick={() => alertModel({
                            title: "提示",
                            content: longContent,
                            level: "success",
                            enableController: true,
                            enableDragging: true,
                            actions: [
                                {
                                    lable: "这个按钮可以关闭",
                                    onClick: (setLoading, setDisable, onClose) => {
                                        alertError("我是开玩笑的,其实我也无法关闭", '关闭失败');
                                    }
                                },
                                {
                                    lable: "这个按钮不可以关闭",
                                    onClick: (setLoading, setDisable, onClose) => { setDisable(true) }
                                }
                            ]
                        })}>受控弹窗</Button>
                    </Item>

                    <Item>
                        <Button onClick={() => alertModel({
                            title: "提示",
                            content: InputCompoment,
                            level: "success",
                            enableController: false,
                            enableDragging: true,
                            actions: [
                                {
                                    lable: "取消",
                                    onClick: (setLoading, setDisable, onClose) => { onClose(); }
                                },
                                {
                                    lable: "确定",
                                    onClick: (setLoading, setDisable, onClose) => {
                                        onClose();
                                        alertSuccess(
                                            <Stack>
                                                <Typography variant="subtitle2" gutterBottom>
                                                    你输入的信息如下:
                                                </Typography>
                                                <Typography variant="subtitle2" gutterBottom>
                                                    用户名:{userName}, 年龄:{age}
                                                </Typography>
                                            </Stack>, '成功');
                                    }
                                }
                            ]
                        })}>自定义内容组件的弹窗</Button>
                    </Item>

                    <Item>
                        <Button onClick={() => alertSuccess(switchTheme, "成功")}>切换主题</Button>
                    </Item>

                    <Item>
                        <Button onClick={() => alertModel({
                            title: "提示",
                            content: '这个示例可以异步请求数据后后执行操作',
                            level: "success",
                            enableController: false,
                            enableDragging: true,
                            actions: [
                                {
                                    lable: "取消",
                                    onClick: (setLoading, setDisable, onClose) => { onClose(); }
                                },
                                {
                                    lable: "提交",
                                    onClick: (setLoading, setDisable, onClose) => {
                                        setLoading(true)
                                        setTimeout(function () {
                                            onClose();
                                            alertModel({
                                                title: "提示",
                                                content: (
                                                    <Stack>
                                                        <Typography variant="subtitle2" gutterBottom>
                                                            你输入的信息如下:
                                                        </Typography>
                                                        <Typography variant="subtitle2" gutterBottom>
                                                            用户名:{userName}, 年龄:{age}
                                                        </Typography>
                                                    </Stack>
                                                ),
                                                level: "success",
                                                enableController: false,
                                                enableDragging: true,
                                                actions: [
                                                    {
                                                        lable: "关闭",
                                                        onClick: (setLoading, setDisable, onClose) => {
                                                            closeAll();
                                                        }
                                                    }
                                                ]
                                            })
                                            
                                        }, 3000)
                                    }
                                }
                            ]
                        })}>异步弹窗</Button>
                    </Item>

                </Masonry>
            </Box>
        </Box>
    )
}

export default PopModelTest;

OK, 至此,关于Model就完美收官了。