今天我们来用所学的知识来做一个布局菜单的组件, 针对这个组件我之前写过一个教程 React之布局菜单-CSDN博客,那个呢比较基础,这节课算是对那个教程的一个扩展和补充。这个实例讲完,这个系列就算告一段落了。先看效果
这个教程要求对React
知识的了解要求比较全面,如果你是跟着我这个系统文章一路学来的,应该就能跟得上学习进度。本教程内容很多,很详细,会分为几个章节来讲解。
首先要安装MUI
、 React Router
、React Redux
这是必不可少的。我们除了会完成在开头的动图效果示例之外,还有较完整功能添加,所以,会用到一些没有讲过的功能。
暗黑模式
,也就是两种颜色模式。前端是绕不开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,\
');
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
中提供了两个工具,让我们能构获取和设置颜色模式。
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
颜色模式。
这就简单了,我们只要改变顶层包裹组件的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;
这就是我们的颜色系统,根据需要自行定义。
现在我们向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 供我们的组件使用,要不然,设计主题有什么意义呢。
在 STheme目录中创建 文件 useToggleThemeHook.jsx
import { useContext } from 'react';
import { ToggleSThemeContext } from './SThemeContext';
// 获取切换主题的功能函数。
const useToggleTheme = () => {
return useContext(ToggleSThemeContext)
}
export default useToggleTheme;
是不是太完美了。 是相当的完美啊。(未完待续)