React 模态框的设计(四)状态管理

news2024/11/24 7:30:49

最近忙的不可开交,每天恨不得把时间掰开使用,挣不到钱还没时间,有时候我在想我怎么混得这个样子。题外话不多说,从这节课开始,我把这个模态框的教程写完整。请看效果:
在这里插入图片描述

这个模态框功能相对比较完整,应该能满足大部分的使用场景了,相信看完这个系统的文章后,你应该就能开发出一个自己的模态框了。

其实看似简单,其实比较复杂的,react 有其天生的缺陷,如何绕开它的缺点,我们就要多思考,把React文档看透才行,多练习,多实践,多做笔记,这样我们才能少走弯路。

我的文章都有关联性,学习这个之前你最好先看看我之前的Rect的相关的文章,这样才能更好的理解本系列的内容。对于一个复杂的功能组件我们要把它细分成多个小部分,然后通过状态整合在一起,这样设计和管理起来都比较好。

模态框分为几个部分:遮罩、弹窗主体、状态、事件,其事主体又可分为多小组件:标题栏、控制区、内容区、功能区。所以你看,能把这么多的组件及功能整合在一起也是一件不简单事哦。我们先从第一个遮罩开始:

遮罩

遮罩就是背景那个阴暗的部分。它是一个div,黑色的带透明度的全屏组件。还有一点说明,我所有的样式都是用@emotion/react 书写的。

_ModelMask.jsx

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import { useState, useEffect } from 'react';
import Box from '@mui/material/Box';

const maskCss = css`
        position: fixed;
        background-color: rgba(0,0,0,0.4);
        border-radius: 5px;
        top: 0px;
        left: 0px;
        width: 100%;
        height: 100%;
        overflow: hidden;
        z-index: 999;
        opacity: 0;
        transition: opacity 200ms ease-in-out;
        display: flex;
        justify-content: center;
        align-items: center;
    `;

const showMaskCss = css`
    opacity: 1;
`;

/**
 * 弹窗遮罩
 * @returns 
 */
function ModelMask({ children }) {
    const [isVisible, setIsVisible] = useState(false); // 开启渐显动画

    // 弹窗的动画监听
    useEffect(function () {
        setIsVisible(true);
    }, []);

    return (
        <Box
            css={css`
                ${maskCss};
                ${isVisible && showMaskCss}
              `
            }
        >
            {
                children
            }
        </Box>
    );
};

export default ModelMask;


很简单, useEffect的作用是在组件挂载后开启渐显动画。就是透明度从 0 变化到 1。css部分大家应该都能理解。

组件外点击监听

看前面的效果图中,当点击遮罩时弹窗是有个动作的,我们要么把弹窗关闭,要么让弹窗有个动态效果提示用户注意,也就是注目效果。总之,这个事件要能分别点击点是遮罩还是弹窗主体。如何我们把事件写在ModelMask里,那么就意味着这个事件要层层传递,直到要使用它的子组件中。 如果不想通过层层传递的方式 ,我们可使用的方法也有很多,比如redux的方式、Context的方式等等。各种方法都有利弊。我比较倾向于hook的方式,咱就是图个方便。

_useOutsideClick.jsx

import { useEffect } from "react";

/**
 * element.addEventListener(event, function () { }, false);
 * addEventListener()基本上有三个参数,
 * 「事件名称 string」,指定事件的名称。如「click」、「mousedown」、「touchstart」等。
 * 「事件的处理程序」(事件触发时执行的function)
 * 「Boolean」值,由这个Boolean决定事件是以「捕获」还是「冒泡」机制执行,若不指定则预设为「冒泡」。
 * 那么事件是先捕获再冒泡的
 * 捕获(true):从启动事件的元素节点开始,逐层往下传递
 * 冒泡(false):逐层向上依序被触发
 */

/**
 * 点击组件外部事件,用于弹窗关闭
 * @param { 要排除的组件节点 } ref 
 * @param { 组件外点击事件 } onOutsideClick  
 */
export const useOutsideClick = (ref, onOutsideClick) => {
  useEffect(() => {
    function handleClickOutside(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        onOutsideClick && onOutsideClick();
      }
    }

    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [ref, onOutsideClick]);
};

我们向document添加了一个mousedown的事件监听,判断鼠标当下是不是ref对象就可以了,这样就能监听到组件外是否点击了。记住,我们向document添加的监听事件一定要在组件卸载时移除。

当然,同样的事情我们也以用组件包裹的方式实现。

_OutsideClick.jsx

import React, { useEffect, useRef } from 'react';
import Box from '@mui/material/Box';

/**
 * 检测点击元素是否在ref元素外部,如果在外部则执行onOutsideClick
 * @param { 当点击外部时要执行的事件 } onOutsideClick  
 * @returns 
 */
export default function OutsideClickCheck({ children, onOutsideClick }) {
    const ref = useRef();
    useEffect(() => {
        const listener = (event) => {
            if (ref.current && !ref.current.contains(event.target)) {
                onOutsideClick && onOutsideClick();
            }
        };

        document.addEventListener("click", listener, true);

        return () => {
            document.removeEventListener("click", listener, true);
        };
    }, [ref, onOutsideClick]);

    return (
        <Box ref={ref}>
            {
                children
            }
        </Box>
    );
}   

同样的原理我们换了一种方式来实现。可根据不同场景来选择使用。

使用弹窗

至于这个弹窗如何呈现现,我在前面的章节中已经说明了,也同样有多种方法,Provider的方式、root.Render的方式、createPortal的方式等等等等。这里我采用ReactRender的方式。我再来设计一个Hook,用于弹窗的渲染。我认为这种方式最简单最粗暴,这一波我必须逼格满满。

_useModel.jsx

import React, { useContext } from 'react';
import ReactDOM from 'react-dom/client';
import { useSTheme } from '../STheme/useToggleThemeHook';

export const ModelContext = React.createContext(null);
export const useModelState = () => useContext(ModelContext);

/**
 * 
 * @param {弹窗标题} title 
 * @param {弹窗的类型, 可选, } level
 * @param {是否显示控制按钮组} enableController
 * @returns 
 */
export default function useModel(configure) {
    const theme = useSTheme(); //获取主题

    const config = {
        sizeMode:"sm", //弹窗的大小
        level: "default", // 弹窗的类型(主要是颜色类型),选项有:normal, error, warning, success, info
        title: "提示", //标题
        enableDragging: false, // 是否允许拖拽
        enableController: false, //是否显示控制按钮
        content: "暂无弹窗内容", //弹窗内容
        actions : [ //操作按钮
            {
                title: "确定", //按钮标题
                attention: false, //是否为操作按钮
                onClick: (setLoading, setTitle, setDisable, onClose) => { onClose(); } //按钮回调
            },
        ],
        ...configure
    } 

    return (Component) => {
        const { children, ...others } = config;
        // const Component = component || null;
        // 创建一个div容器,作为弹窗的根节点
        const modelContainer = document.createElement("div");

        // 将div容器添加到body中
        document.body.appendChild(modelContainer);

        // 创建一个根节点
        const modelRoot = ReactDOM.createRoot(modelContainer);

        // 卸载事件
        const unmountEvent = () => {
            modelContainer.remove();
            modelRoot.unmount();
        }

        const setContent = (content) => {
            config.content = content;
        };

        modelRoot.render(
            <Component
                {...others}
                onClose={unmountEvent}
                setContent={setContent}
                isDark={ theme.mode === "dark" ? true : false } // 是否是暗黑模式
            />
        );
    }
}

关于useSTheme主题的设计请参考我布局菜单系列的文章,那里讲得很通透了。配置参数已经很明了了,我都作了说明。使用的时候这样使用就行了:

const alert = useModel({...});
...
 
// 然后在事件里直接调用
alert(Model);

那么这个Model 就是我们的弹窗主体了。设计如下:

_Model.jsx

/** @jsxImportSource @emotion/react */
import { css, jsx, keyframes } from '@emotion/react'
import React, { useState, useRef, useEffect, useCallback } from 'react';

import { ModelContext } from './_useModel';
import SThemeProvider from '../STheme/SThemeProvider';
import { useSTheme } from '../STheme/useToggleThemeHook';

function Model(props) {
    const {
        sizeMode = "sm", //弹窗的大小
        level = "default", // 弹窗的类型(主要是颜色类型),选项有:normal, error, warning, success, info
        title = "提示", //标题
        isDark,
        onClose,  //关闭弹窗后的回调
        enableDragging = true,
        enableController = true, //是否显示控制按钮
        content = "暂无弹窗内容", //弹窗内容
        actions = [ //操作按钮
            {
                title: "确定", //按钮标题
                attention: false, //是否为操作按钮
                onClick: (setLoading, setTitle, setDisable, onClose) => { onClose(); } //按钮回调
            },
        ],//功能按钮
    } = props;

    const [stateMode, setStateMode] = useState(1); // 弹窗的状态,0: 最小化, 1: 正常, 2: 最大化
    const theme = useSTheme(); //获取主题
    console.log(`theme => ${theme}`);

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

export default Model;


我们在这个组件里用了一个主题provider,这是因为我们弹窗是通过Render的方式显示的,是一个独立的React根组件,所以这个主题必须从Model内部手动管理。我们把还把Model相关的一些功能放到别一个Provider中了,这样子组件就可以直接使用相关的功能而不需要通过Props来传递了。

配置

我这里还写了一个整个组件的配置文件:

_ModelConfigure.jsx

import InfoIcon from '@mui/icons-material/Info';
import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined';
import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined';

import pink from '@mui/material/colors/pink';
import orange from '@mui/material/colors/orange';
import green from '@mui/material/colors/green';
import blue from '@mui/material/colors/blue';
import grey from '@mui/material/colors/grey';

//弹窗的大小选项
export const widthType = {
    sm: 576,
    md: 768,
    lg: 992,
    xl: 1200,
    xxl: 1400,
};

//弹窗的图标大小
export const iconSize = 25;

//弹窗的图标类型、颜色
export const infoLevel = {
    info: { color: blue[50], divider: blue[70], level: "info", Icon: InfoOutlinedIcon, iColor: blue[500]},
    error: { color: pink[50], divider: pink[70], level: "error", Icon: ErrorOutlineOutlinedIcon, iColor: pink[500]},
    warning: { color: orange[50], divider: orange[70], level: "warning", Icon: WarningAmberOutlinedIcon, iColor: orange[500]},
    success: { color: green[50], divider: green[70],  level: "success", Icon: TaskAltOutlinedIcon, iColor: green[500]},
    default: { color: grey[50], divider: grey[70], level: "primary", Icon: InfoIcon , iColor: grey[500]}
}

export const minHeight = 45; //弹窗的最小高度
export const minWidth = 300; //弹窗的最小宽度 

接下来就是弹窗的主体设计了。下回分解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1489221.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

桂院校园导航 静态项目 二次开发教程 2.0

Gitee代码仓库&#xff1a;桂院校园导航小程序 GitHub代码仓库&#xff1a;GLU-Campus-Guide 静态项目 2.0版本 升级日志 序号 板块 详情 1 首页 重做了首页&#xff0c;界面更加高效和美观 2 校园页 新增了 “校园指南” 功能&#xff0c;可以搜索和浏览校园生活指南…

聊聊国内「类Sora模型」发展现状,和 Sora 的差距到底有多大?

2024 年 2 月 16 日。 就在谷歌发布他新一代的多模态大模型 Gemini 1.5 Pro 的同一天&#xff0c;OpenAI 带着新一代的文生视频模型 Sora 再次抓住了全世界人们的眼球。 “颠覆”、“炸裂”、“变天”、“疯狂”&#xff0c;类似的形容词一夜之间簇拥在 Sora 周围&#xff0c;…

浅谈一个CTF中xss小案例

一、案例代码 二、解释 X-XSS-Protection: 0&#xff1a;关闭XSS防护 之后get传参&#xff0c;替换过滤为空&#xff0c;通过过滤保护输出到img src里面 三、正常去做无法通过 因为这道题出的不严谨所以反引号也是可以绕过的 正常考察我们的点不在这里&#xff0c;正常考察…

LSA头部结构简述

LSA&#xff08;Link State Advertisement&#xff09;是一种用于路由协议头部结构&#xff0c;用于在网络中传递路由信息。 LSA头部结构包含以下几个字段&#xff1a; 1、LSA类型&#xff08;LSA Type&#xff09;&#xff1a;指示LSA的类型&#xff0c;不同类型的LSA用于传递…

怎么压缩成mp4视频?

在数字化时代&#xff0c;视频已经成为我们日常生活中不可或缺的一部分。然而&#xff0c;有时候我们可能会遇到视频文件太大的问题&#xff0c;不便于传输、存储或分享。那么&#xff0c;如何将视频压缩成MP4格式&#xff0c;以减小文件大小呢&#xff1f;本文将为您介绍几种简…

某大型制造企业数字化转型规划方案(附下载)

目录 一、项目背景和目标 二、业务现状 1. 总体应用现状 2. 各模块业务问题 2.1 设计 2.2 仿真 2.3 制造 2.4 服务 2.5 管理 三、业务需求及预期效果 1. 总体业务需求 2. 各模块业务需求 2.1 设计 2.2 仿真 2.3 制造 2.4 服务 2.5 管理 四、…

四年一段旅途,一个起点,一个机会

不得不感慨一下&#xff0c;现在的年轻人、大学生实在是太厉害了 最近加入了一个社群&#xff0c;是一名大三学生创建的&#xff0c;他短短一年间&#xff0c;就创建了一个数千人的社群&#xff0c;还运营的几十个副业社群&#xff0c;一年的时间变现100W&#xff0c;这些成绩…

嵌入式学习第二十四天!(进程间通信:消息队列、共享内存、信号灯)

进程间的通信&#xff1a; 消息队列、共享内存、信号灯&#xff1a; 1. IPC对象&#xff1a;内存文件 1. ipcs&#xff1a; 查看系统中的消息队列&#xff0c;共享内存、信号灯的信息 2. ipcrm&#xff1a; 删除消息队列、共享内存、信号灯 ipcrm -Q/-M/-S key ipcrm -q/-m/-s…

Jmeter系列(4) 线程属性详解

线程属性 线程组是配置压测策略的一个重要环节线程组决定了测试执行的请求数量 线程数 在这里线程数相当于一个虚拟用户每个线程数大约占内存1M特别注意⚠️ 单台机器最大线程数不要超过1000&#xff0c;不然可能会造成内存溢出 Ramp-Up时间 所有线程在多长时间内全部启动…

S7-1200PLC脉冲轴位置控制功能块优化(完整SCL源代码)

博途PLC 位置控制功能块常用应用&#xff0c;可以参考下面文章链接&#xff1a; 1、博途PLC脉冲轴绝对定位往复运动控制 https://rxxw-control.blog.csdn.net/article/details/135768878https://rxxw-control.blog.csdn.net/article/details/1357688782、脉冲轴位置控制功能块…

自测-5 Shuffling Machine(python版本)

文章预览&#xff1a; 题目翻译算法python代码oj反馈结果 题目 翻译 shuffle是用于随机化一副扑克牌的过程。由于标准的洗牌技术被认为是薄弱的&#xff0c;并且为了避免员工通过不适当的洗牌与赌徒合作的“内部工作”&#xff0c;许多赌场使用了自动洗牌机。你的任务是模拟一…

【LeetCode:225. 用队列实现栈 + 栈 | 队列】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

HTML5:七天学会基础动画网页5

CSS3渐变 (可以给背景颜色设置一个渐变的效果) 线性渐变:Linear Gradients(从直线上向远处见面) 语法: background:linear-gradient(direction&#xff0c;color-stop1&#xff0c;color-stop2…)&#xff1b; direction:方向 to left, to right, 90deg 径向渐变:Radial …

HM_2019在面积不变情况下编辑网格

首先&#xff0c;应该保存其形状&#xff0c;计算他的面积。让面积不变作为一个约束&#xff0c;然后进行网格的形变。

HTML5:七天学会基础动画网页7

CSS3高级特效 2D转换方法 移动:translate() 旋转:rotate() 缩放:scale() 倾斜:skew() 属性:transform 作用:对元素进行移动,旋转,缩放,倾斜。 2D移动 设定元素从当前位置移动到给定位置(x,y) 方法 说明 translate(x,y) 2D转换 沿X轴和Y轴移…

什么是张量?如何理解张量?

一、张量概念 张量&#xff08;tensor&#xff09;是一个多维数组&#xff0c;它是向量&#xff08;一维数组&#xff09;和矩阵&#xff08;二维数组&#xff09;的推广。在数学和物理学中&#xff0c;张量是一种广泛应用的概念&#xff0c;用来描述物理量在空间中的分布和变…

VMware之VSAN

VMware VSAN特点 聚合了虚拟化管理程序的极其简单的存储软件 1、完全式&#xff1a;全都是固态硬盘 2、混合式存储解决方案&#xff1a; →磁盘&#xff08;硬盘&#xff09; →基于闪存的磁盘&#xff08;固态硬盘&#xff09; 3、横向扩展体系&#xff1a; 增加主机数量 4、…

uniapp实现进度条组件

首先&#xff0c;在uniapp项目中创建一个自定义组件&#xff0c;可以命名为Progress.vue。在Progress.vue中&#xff0c;编写如下代码&#xff1a; <template><view class"progress"><view class"progress-bar" :style"{width: progr…

水牛社软件是真的吗?

软件是真的&#xff0c;不过毕竟是为了赚钱或者获取资源而买的&#xff0c;所以大部分只关心能赚多少钱吧 说实话&#xff0c;我用了2年了&#xff0c;一些独立的项目还有群&#xff0c;有一月挣几千上万的&#xff0c;有一月赚几百的 软件是一个集合体&#xff0c;不是像很多…

Html基础标签以及属性和用法

HTML基础 超文本标记语言&#xff08;英语&#xff1a;HyperText Markup Language&#xff0c;简称&#xff1a;HTML&#xff09;是一种用于创建网页的标准标记语言。 您可以使用 HTML 来建立自己的 WEB 站点&#xff0c;HTML 运行在浏览器上&#xff0c;由浏览器来解析。 HTM…