今天我们来用所学的知识来做一个布局菜单的组件, 针对这个组件我之前写过一个教程 React之布局菜单-CSDN博客,那个呢比较基础,这节课算是对那个教程的一个扩展和补充。这个实例讲完,这个系列就算告一段落了。先看效果
这个教程要求对React
知识的了解要求比较全面,如果你是跟着我这个系统文章一路学来的,应该就能跟得上学习进度。本教程内容很多,很详细,会分为几个章节来讲解。
安装
首先要安装MUI
、 React Router
、React Redux
这是必不可少的。我们除了会完成在开头的动图效果示例之外,还有较完整功能添加,所以,会用到一些没有讲过的功能。
设计前的考虑
- 为了考虑到高度自定义这个特性,我们把菜单的配置以 json数组的形式进行配置。你可以把菜单配置放在服务器上,基于权限的考虑你甚至可以在后台根据用户权限的不同返回不同的菜单配置,实现角色的功能 。
- 我们尽可能的将与业务代码无关的东西封装在组件内部,这样调用起来代码就很简洁。
- 根据业务逻辑尽可能的细小化、模块化。
- 所有UI元素都要适配
暗黑模式
,也就是两种颜色模式。
Bootstrap
前端是绕不开Css的,但是对于一个完整的项目来说,写Css就很繁琐,我的主张是,能偷懒就偷懒,不能偷懒想办法偷懒。这不,对于布局中的Grid
和 Flex
方面,Bootstrap
就提供了相当完美的功能了,我认为这方面它比MUI强许多,既然如此,何不做个拿来主义者
呢,何苦自己为难自己呢。书回正传,回到我们的项目,在源目录(src) 下新建一个本章的实例目录:SMenu , 并在这个目录下新建目录 SCSS, 我们把网上下载的Bootstrap5.3的Css文件放到这个目录里。另外,我也提供了两个其它的两个css文件,目录结构如下所示:
关于Bootstrap的样式,请大家自行学习,此处不做详解。
以下是 components.css 的内容
.fade-enter {
opacity: 0;
transform: translateX(-100%);
}
.fade-enter-active {
opacity: 1;
transform: translateX(0%);
}
.fade-exit {
opacity: 1;
transform: translateX(0%);
}
.fade-exit-active {
opacity: 0;
transform: translateX(100%);
}
.fade-enter-active,
.fade-exit-active {
transition: opacity 500ms, transform 500ms;
}
.my-node-enter {
opacity: 0;
}
.my-node-enter-active {
opacity: 1;
transition: opacity 200ms;
}
.my-node-exit {
opacity: 1;
}
.my-node-exit-active {
opacity: 0;
transition: opacity 200ms;
}
/*
*弹窗动画
*/
.speedx-alert-enter {
opacity: 0;
transform: scale(0.9);
}
.speedx-alert-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.speedx-alert-exit {
opacity: 1;
}
.speedx-alert-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}
下面是public.css
的内容
html {
background-color:#f2f2f2;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 18px;
line-height: 1.667;
color: #222;
text-align: justify;
word-wrap: break-word;
word-break: break-word;
-moz-hyphens: auto;
hyphens: auto;
}
input,
textarea {
font-family: 'Roboto', sans-serif;
line-height: 1.4;
background: #eee;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
color: #353535d9;
overflow-wrap: break-word;
}
:not(pre) > code {
background-color: rgb(214, 214, 214);
border-radius: 3px;
padding: 1px 3px;
}
img {
max-width: 100%;
max-height: 20em;
}
.page-container {
position: relative;
display: flex;
flex-direction: column;
background-color:white;
min-height: 100vh;
}
.layout-content{
margin: 10px, 0px;
padding: 0px;
min-height: 100%;
flex: 1;
text-align:justify;
}
.content-wrap {
padding-bottom: 2.5rem; /* Footer height */
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 2.5rem; /* Footer height */
padding: 20px 0;
/* box-shadow: 3px 0 5px #c9c9c9; */
}
blockquote {
border-left: 2px solid rgb(1, 154, 192);
margin-left: 0;
margin-right: 0;
padding-left: 10px;
color: rgb(150, 150, 150);
font-style: italic;
}
blockquote[dir='rtl'] {
border-left: none;
padding-left: 0;
padding-right: 10px;
border-right: 2px solid #ddd;
}
input {
box-sizing: border-box;
font-size: 0.85em;
width: 100%;
padding: 0.5em;
border: 2px solid #ddd;
background: #fafafa;
}
input:focus {
outline: 0;
border-color: blue;
}
iframe {
width: 100%;
border: 1px solid #eee;
}
[data-slate-editor] > * + * {
margin-top: 1em;
}
#root{
display: flex;
min-height: 100vh;
flex-direction: column;
background-color:#f2f2f2;
}
.alignCenterVH{
position: relative;
top: 50%;
transform: translateY(-50%);
text-align: center;
}
.mainBoxPosition{
flex: 1;
display: flex;
justify-content: center;
align-items: top;
}
.titleInput {
display: block;
width: 100%;
font-weight: bold;
min-height: 50px;
font-size: 22px;
border:none;
border-bottom: 1px;
border-color: rgb(190, 190, 190);
outline:none;
}
.selectElement {
display: block;
max-width: 100%;
max-height: 20em;
}
.imgsubstring {
display: block;
color:rgb(116, 116, 116);
font-weight: 500;
font-size: medium;
padding: 5px;
text-align: center;
}
.mayi-select {
width: 400px; height: 200px;line-height: 200px;text-align: center;margin:auto;
border: 1px solid #ccc;
background: linear-gradient(#efefef,#ccc) padding-box,
linear-gradient(135deg, rgba(0, 0, 0, 1) 25%, transparent 25%, transparent 50%, rgba(1, 1, 1, 1) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-size:100% 100%, 8px 8px;
animation: bg 1s linear infinite;
}
.mayi-select:hover{
cursor: pointer;
border: 1px dashed transparent;
}
@keyframes bg {
0% {
background-position: 0 0;
}
100% {
background-position: 8px 0;
}
}
.alignCenter {
display: table-cell;
/*垂直居中 */
vertical-align: middle;
/*水平居中*/
text-align: center;
/* text-align: center;
background-color: #fff;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%); */
}
.site-layout-background{
background-color: white;
}
.check-background {
width: 100px;
height: 100px;
background-image: url('data:image/svg+xml,\
<svg xmlns="http://www.w3.org/2000/svg"\
width="100" height="100" fill-opacity=".25">\
<rect x="50" width="50" height="50"></rect>\
<rect y="50" width="50" height="50"></rect>\
</svg>');
background-size:50px 50px;
}
设计一个样式组件,在App中引入一下就可以了,就可以保证我们的所有组件就能够应用到我们的样式。在STheme文件夹中创建 AdapterCss.jsx
import CssBaseline from '@mui/material/CssBaseline';
import '../SCSS/public.css';
import '../SCSS/components.css';
import '../SCSS/bootstrap5.3.0/bootstrap-utilities.min.css';
import '../SCSS/bootstrap5.3.0/bootstrap-grid.min.css';
// import '../SCSS/bootstrap5.3.0/bootstrap.min.css';
export default function AdapterCss() {
return <CssBaseline />
}
我们只要在根组件中引入一次这个样式适配器就好了。
颜色模式
因为我们要为App
适配暗模式,所以在设计之初就要考虑好这个问题。首先,MUI所有的组件就已经适配了两种颜色模式,Bootstrap
也是一样。还有一个就是我们自己封装的组件也要适配到暗模式中,就这要求我们自己设计的组件元素应用的颜色模式要么来处MUI, 要么采用Bootstrap
,要么我们自己提供一个双模式的颜色体系。也就是说这三种不同框架之间的颜色体系是共存的。下面我们分别来说一说:
MUI的Theme模式
MUI
中提供了两个工具,让我们能构获取和设置颜色模式。
ThemeProvider
这个很好理解,就是一个颜色模式厂,就是一个Context
;createTheme
创建一个颜色模式。我们这里只是用它来改变MUI
的颜色模式;
下面我们用示例说明用法:
import { createTheme } from '@mui/material/styles';
function createMuiTheme(mode) {
const themeMode = mode === "light" ? "light" : "dark";
return createTheme({
palette: {
mode: themeMode,
},
});
}
上面的函数根据我们传入的模式关键字来创建相应的MUI
颜色模式。
Bootstrap的颜色模式
这就简单了,我们只要改变顶层包裹组件的data-bs-theme
属性值就可以切换颜色模式。
<div data-bs-theme="light"> 这是 light 模式 <div>
<div data-bs-theme="dark"> 这是 dark 模式 <div>
很简单吧。
自定义颜色模式
自定义颜色模式就有点技术含量了。也是最繁琐的一环。首先我们要定义的每种颜色要有两个模式下的颜色值。这就要一个标准,由于我没有采用 TS 设计模式,所以就要用其它的办法来约束定义的行为,比如一个函数就是一个很好的办法。
我们在STheme目录中创建一个工具函数库,把所有的我们自定义的工具函数放到其中统一导出就好了。
// SThemeUtils.jsx
import SThemeCodors from "./SColors";
// 生成基本颜色,lightColor为浅色,darkColor为深色
export function sColor(lightColor, darkColor) {
return {
light: lightColor,
dark: darkColor,
}
}
/**
* 生成主题模型
* @param {} mode
* @returns
*/
export default function createSTheme(mode = "light") {
const themeMode = mode === "light" ? "light" : "dark";
const sTheme = {mode: themeMode};
Object.keys(SThemeCodors).forEach(key => {
sTheme[key] = SThemeCodors[key][themeMode];
}
);
return sTheme;
}
/**
* 生成MUI系统主题
* @param {*} mode
* @returns
*/
export function createMuiTheme(mode) {
const themeMode = mode === "light" ? "light" : "dark";
return createTheme({
palette: {
mode: themeMode,
},
});
}
- 我们通过
sColor
函数生成一个颜色对象,这样行为就统一了。每个颜色对象中都有一个light
色 和一个dark
色。所以我们设计之初就要把每种不同模式下的颜色配置好。这关系到我们整体的App
风格。你看,我们设计一个App其实没那么简单对不对,对不同技术技能都要些要求的。 createSTheme根据自定义颜色模式生成基于自定义颜色的
theme`
现在就是定义颜色了,在相同的目录下,创建颜色库文件
// sColors.jsx
import { sColor } from "./SThemeUtils";
/**
* 定义主题颜色模型
*/
const SThemeColors = {
bgColor: sColor("#edf3f2", "#1D1D1D"), //背景色
/**
* 菜单色配置
*/
badge: sColor("red", "red"), //小红点色
menuBgcolor: sColor("#EEEEEE", "#0D2745"),//菜单栏的背景色
hoverMenuBgcolor: sColor("#FFEACC", "#091C32"), //菜单栏背景色Hover
iconColorNormal: sColor("#1c2322", "#EEEEEE"), //图标色
iconColorSquare: sColor("#363c3b", "#CCCCCC"), //无图标时的替代色
menuNomalColor: sColor("#333333", "#07172A"), //菜单栏正常字体色
activeMenuBgcolor: sColor("#FFEACC", "#1C54AD"), //活动菜单背景色
activeBorderColor: sColor("#007AFF", "#1C54AD"), //活动菜单边框色
menuSpliderColor: sColor("#DDDDDD", "#143C6A"), // 菜单栏分隔色
menuSubitemColor: sColor("#545a59", "#B8B8B8"), //子菜单字体色
hoverSubitemColor: sColor("#9fa2a1", "#3C628B"), //hover时的子菜单字体色
hoverMenuSubitemBgcolor: sColor("#FFEACC", "#123862"), //子菜单的hover背景色
activeMenuSubitemBgcolor: sColor("#FFBF66", "#0E2C4D"),//活动子菜单的背景色
activeQuickMenuBgcolor: sColor("#FFBF66", "#2266B5"),//活动快捷菜单的子菜单
}
export default SThemeColors;
这就是我们的颜色系统,根据需要自行定义。
创建 ThemeProvider
现在我们向App提供三种 provider
, 还要提供 切换 模式的方法,最好的办法当然就是 Context
了,我们来设计这几个Provider
: 创建 SThemeContext.jsx
文件:
// SThemeContext.jsx
import { createContext } from 'react';
/**
* 创建自定义主题上下文
*/
export const SThemeContext = createContext(null);
export function CusThemeProvider({ theme, children }) {
return (
<SThemeContext.Provider value={theme}>
{
children
}
</SThemeContext.Provider>
)
}
/**
* 创建切换主题上下文
*/
export const ToggleSThemeContext = createContext(null);
export function ToggleSThemeProvider({ handler, children }) {
return (
<ToggleSThemeContext.Provider value={handler}>
{
children
}
</ToggleSThemeContext.Provider>
)
}
/**
* 创建Bootstrap主题上下文
* @param {*} param0
* @returns
*/
export function BootstrapThemeProvider({ mode, children }) {
return (
<div data-bs-theme={mode}>
{
children
}
</div>
)
}
文件里已经备注的很清楚了,就是创建两个上下文就OK了。
现在三种颜色的框架都有了。接下来我们就是要把这三个模式合并成一个Provider
就完美了。我们来创建这个文件。在STheme
目录下创建 SThemeProvider.jsx
文件
// SThemeProvider.jsx
import { useState } from 'react';
import { ThemeProvider } from '@mui/material/styles';
import AdapterCss from './AdapterCss';
import createSTheme, { createMuiTheme } from './SThemeUtils';
import { BootstrapThemeProvider, CusThemeProvider, ToggleSThemeProvider} from './SThemeContext';
/**
* 项目的皮肤供应器
* @param {} param0
* @returns
*/
function SThemeProvider({ children }) {
const [theme, changeTheme] = useState({ custom: createSTheme("light"), muiTheme: createMuiTheme("light")});
const toggleThemHandler = () => {
const muiThemeMode = theme.muiTheme.palette.mode === "light" ? "dark" : "light";
changeTheme({
custom: createSTheme(muiThemeMode),
muiTheme: createMuiTheme(muiThemeMode),
})
}
return (
<ThemeProvider theme={theme.muiTheme}>
<CusThemeProvider theme={theme.custom}>
<BootstrapThemeProvider mode={theme.custom.mode}>
<ToggleSThemeProvider handler={toggleThemHandler}>
<AdapterCss />
{
children
}
</ToggleSThemeProvider>
</BootstrapThemeProvider>
</CusThemeProvider>
</ThemeProvider>
)
}
export default SThemeProvider;
现在层次很清晰了吧。是不是清爽了许我,这样,我们在根组件中用 SThemeProvider
包裹就好了。是不是很优雅。我们只需要在项目入口文件 main.jsx 中这样写就行了。
import React from 'react'
import ReactDOM from 'react-dom/client'
import SThemeProvider from './SMenu/STheme/SThemeProvider.jsx';
import App from './SMenu/App.jsx';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<SThemeProvider>
<App />
</SThemeProvider>
</React.StrictMode>,
)
编写主题切换Hook
这是主题最后一个环节,我们要提供一个 Hook 供我们的组件使用,要不然,设计主题有什么意义呢。
在 STheme目录中创建 文件 useToggleThemeHook.jsx
import { useContext } from 'react';
import { ToggleSThemeContext } from './SThemeContext';
// 获取切换主题的功能函数。
const useToggleTheme = () => {
return useContext(ToggleSThemeContext)
}
export default useToggleTheme;
是不是太完美了。 是相当的完美啊。(未完待续)