React 新闻发布系统 NewSandBox侧边栏与顶部栏布局
本章内容:1.NewSandbox首页开发 2.sidemenu侧边栏开发 3.Topheader顶部栏开发
效果图展示:
一:NewSandbox首页
(1)NewSandBox的路由地位
首先先介绍一下NewSandBox这个页面 这是整个系统最重要的组件 基本的大多数的组件都会包含在NewSandBox中。
观察路由实例也不难发现 NewSandBox是整个系统的两大组件之一,在NewSandBox之下是一系列子路由。这些子路由共同组成了整个系统。好了就先介绍到这,下面我们来看该组件的代码部分。
(2)开发过程
1、layout布局
首先通过一开始的效果图可以了解到 我们的NewSandBox组件主要是基于antd组件库中的Layout组件来布局的。先来看看效果图
源代码:
import React, { useState } from 'react';
import {MenuFoldOutlined,MenuUnfoldOutlined,UploadOutlined,UserOutlined,VideoCameraOutlined,
} from '@ant-design/icons';
import { Button, Layout, Menu, theme } from 'antd';const { Header, Sider, Content } = Layout;const App: React.FC = () => {const [collapsed, setCollapsed] = useState(false);const {token: { colorBgContainer, borderRadiusLG },} = theme.useToken();return (<Layout><Sider trigger={null} collapsible collapsed={collapsed}><div className="demo-logo-vertical" /><Menutheme="dark"mode="inline"defaultSelectedKeys={['1']}items={[{key: '1',icon: <UserOutlined />,label: 'nav 1',},{key: '2',icon: <VideoCameraOutlined />,label: 'nav 2',},{key: '3',icon: <UploadOutlined />,label: 'nav 3',},]}/></Sider><Layout><Header style={{ padding: 0, background: colorBgContainer }}><Buttontype="text"icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}onClick={() => setCollapsed(!collapsed)}style={{fontSize: '16px',width: 64,height: 64,}}/></Header><Contentstyle={{margin: '24px 16px',padding: 24,minHeight: 280,background: colorBgContainer,borderRadius: borderRadiusLG,}}>Content</Content></Layout></Layout>);
};export default App;
代码中有三个组件 分别是是Sider组件 Header组件 与content组件。其中Sider组件是侧边栏组件,Header组件是顶部栏组件,而content就是内容组件。开发时我们只需要对应开发这三个组件就可以开发出我们管理系统的主要内容。Layout布局也是大部分管理系统惯用的布局之一。
2、NewSandBox组件代码
再来看看我们开发的具体代码:
import { Outlet } from 'react-router-dom';
import SideMenu from '../../components/NewSandBox/SideMenu'
import TopHeader from '../../components/NewSandBox/TopHeader'
import {Layout, theme } from 'antd';
const { Content } = Layout;
import { useState } from 'react';
function NewSandBox(){const {token: { colorBgContainer, borderRadiusLG },} = theme.useToken();//因为这只是简单的状态管理 所以使用简单的组件通信就足矣 没必要用redux这些的管理工具const [collapsed, setCollapsed] = useState(false);return(<Layout><SideMenu collapsed={collapsed} /><Layout><TopHeader collapsed={collapsed} setCollapsed={setCollapsed}/>{/* 这里会渲染子路由 */}<Contentstyle={{margin: '24px 16px',padding: 24,minHeight: 280,background: colorBgContainer,borderRadius: borderRadiusLG,overflow: 'auto',}}><Outlet/></Content></Layout></Layout>)
}export default NewSandBox
代码中的模块引入部分就不过多赘述。NewSandBox的布局主要是通过Layout组件实现的,q其中SideMenu组件就是我们的侧边栏 TopHeader组件就是顶部栏 content组件中的Outlet组件是React-router-dom v6版本的二级路由写法 存放的是NewSandBox的二级路由 也就是子组件。这里的collapsed这个状态是用来控制侧边栏的收起与展开的 。
收起状态:
二:SideMeun组件开发
(1)效果预览
先来看看SIdeMeun组件的效果图:
其中点击不同的导航 ,content会跳转到不同的路由页面中。
源代码:
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { Layout, Menu } from 'antd';
import { useNavigate, useLocation } from 'react-router-dom';
import { UserOutlined, SettingOutlined,UploadOutlined,VideoCameraOutlined,AuditOutlined, FormOutlined, HomeOutlined} from '@ant-design/icons'
const { Sider } = Layout;export default function SideMenu({collapsed}) {const [items, setItems] = useState([]);const navigater = useNavigate();const location = useLocation();const iconMap = {首页: <HomeOutlined />,用户管理: <UserOutlined />,用户列表: <UserOutlined />,权限管理: <SettingOutlined />,新闻管理: <FormOutlined />,审核管理: <AuditOutlined />,发布管理: <UploadOutlined />,}// 根据当前路径设置选中的菜单项const [selectedKeys, setSelectedKeys] = useState([location.pathname]);useEffect(() => {setSelectedKeys([location.pathname])const getItems = async () => {const res = await axios.get('http://localhost:3000/rights?_embed=children');//处理首页部分标签不需要展示问题// console.log(res.data);//处理完的新结果let newRes=[];res.data.forEach(item => {// 分开处理 处理存在children的和不存在的if(item.children.length===0){delete item.childrennewRes.push(item);}else{let newResChildren=[];item.children.forEach(element => {//如果是首页就添加到新的children数组中去if(element.pagepermisson===1){newResChildren.push(element)}});//更新数据 继承原有的item children属性替换成newResChildrennewRes.push({...item,children:newResChildren})}});setItems(newRes);// console.log(newRes);const setIcon=(item,iconMap)=>{// 创建副本以避免直接修改原数组const newItems = JSON.parse(JSON.stringify(item));newItems.forEach(element=>{ for(let key in iconMap){if(key===element.label){element.icon=iconMap[key]}}// 删除可能导致React警告的rightId属性,因为它不是标准DOM属性delete element.rightId;});return newItems;}setItems(setIcon(newRes,iconMap));};getItems();}, [location.pathname]);const openKey='/'+location.pathname.split('/')[1];return (<Sider trigger={null} collapsible collapsed={collapsed}><div className="demo-logo-vertical" style={{lineHeight:'32px',backgroundColor:'rgba(255,255,255,0.2)',textAlign:'center',margin:'5px',borderRadius:'10px',color:'white',fontSize:'18px'}}>全球新闻发布系统</div><Menutheme="dark"mode="inline"selectedKeys={selectedKeys} // 使用 selectedKeys 而不是 defaultSelectedKeysdefaultOpenKeys={[openKey]}items={items}onClick={(item)=>{navigater(item.key);//屏幕滚动到最顶部window.scrollTo(0,0)}}/></Sider>)
}
(2)开发过程
开发SIdeMenu组件就是开发原有的Sideer组件 我们来看看Sider组件是如何开发的
<Siderbreakpoint="lg"collapsedWidth="0"onBreakpoint={(broken) => {console.log(broken);}}onCollapse={(collapsed, type) => {console.log(collapsed, type);}}><div className="demo-logo-vertical" /><Menu theme="dark" mode="inline" defaultSelectedKeys={['4']} items={items} /></Sider>
我们的组件也是基于此开发的
<Sider trigger={null} collapsible collapsed={collapsed}><div className="demo-logo-vertical" style={{lineHeight:'32px',backgroundColor:'rgba(255,255,255,0.2)',textAlign:'center',margin:'5px',borderRadius:'10px',color:'white',fontSize:'18px'}}>全球新闻发布系统</div><Menutheme="dark"mode="inline"selectedKeys={selectedKeys} // 使用 selectedKeys 而不是 defaultSelectedKeysdefaultOpenKeys={[openKey]}items={items}onClick={(item)=>{navigater(item.key);//屏幕滚动到最顶部window.scrollTo(0,0)}}/></Sider>
先看Sider组件中的属性 collapsible表示组件是可折叠的 然后通过coolapsed来控制是否折叠。
在menu组件中 selsectedkeys根据当前路径设置选中的菜单项,当我们刷新页面之后只要网址路由不改变那么默认选中项就不改变 为什么用defaultSelsectedKeys是因为我们在点击不同的子路由的时候 组件是需要受控的 而一般来说antd中defaul属性一般都是不受控的默认属性 ,如果不受控在切换不同的菜单项的时候不会对应跳转到对于二级路由。这里的openkey表示默认展开的,
item属性:就是菜单栏的属性 是一个数组类型的数据 这里的数据一般都是我们从后端请求来的数据
[
{
"id": 1,
"label": "首页",
"key": "/home",
"grade": 1,
"children": []
},
{
"id": 2,
"label": "用户管理",
"key": "/user-manage",
"pagepermisson": 1,
"grade": 1,
"children": [
{
"id": 3,
"label": "添加用户",
"rightId": 2,
"key": "/user-manage/add",
"grade": 2
},
{
"id": 4,
"label": "删除用户",
"rightId": 2,
"key": "/user-manage/delete",
"grade": 2
},
{
"id": 5,
"label": "修改用户",
"rightId": 2,
"key": "/user-manage/update",
"grade": 2
},
{
"id": 6,
"label": "用户列表",
"rightId": 2,
"key": "/user-manage/list",
"pagepermisson": 1,
"grade": 2
}
]
},
{
"id": 7,
"label": "权限管理",
"key": "/right-manage",
"pagepermisson": 1,
"grade": 1,
"children": [
{
"id": 8,
"label": "角色列表",
"rightId": 7,
"key": "/right-manage/role/list",
"pagepermisson": 1,
"grade": 2
},
{
"id": 9,
"label": "权限列表",
"rightId": 7,
"key": "/right-manage/right/list",
"pagepermisson": 1,
"grade": 2
},
{
"id": 10,
"label": "修改角色",
"rightId": 7,
"key": "/right-manage/role/update",
"grade": 2
},
{
"id": 11,
"label": "删除角色",
"rightId": 7,
"key": "/right-manage/role/delete",
"grade": 2
},
{
"id": 12,
"label": "修改权限",
"rightId": 7,
"key": "/right-manage/right/update",
"grade": 2
},
{
"id": 13,
"label": "删除权限",
"rightId": 7,
"key": "/right-manage/right/delete",
"grade": 2
}
]
},
{
"id": 14,
"label": "新闻管理",
"key": "/news-manage",
"pagepermisson": 1,
"grade": 1,
"children": [
{
"id": 15,
"label": "新闻列表",
"rightId": 14,
"key": "/news-manage/list",
"grade": 2
},
{
"id": 16,
"label": "撰写新闻",
"rightId": 14,
"key": "/news-manage/add",
"grade": 2,
"pagepermisson": 1
},
{
"id": 17,
"label": "新闻更新",
"rightId": 14,
"key": "/news-manage/update/:id",
"grade": 2,
"routepermisson": 1
},
{
"id": 18,
"label": "新闻预览",
"rightId": 14,
"key": "/news-manage/preview/:id",
"grade": 2,
"routepermisson": 1
},
{
"id": 19,
"label": "草稿箱",
"rightId": 14,
"key": "/news-manage/draft",
"pagepermisson": 1,
"grade": 2
},
{
"id": 20,
"label": "新闻分类",
"rightId": 14,
"key": "/news-manage/category",
"pagepermisson": 1,
"grade": 2
}
]
},
{
"id": 21,
"label": "审核管理",
"key": "/audit-manage",
"pagepermisson": 1,
"grade": 1,
"children": [
{
"id": 22,
"label": "审核新闻",
"rightId": 21,
"key": "/audit-manage/audit",
"pagepermisson": 1,
"grade": 2
},
{
"id": 23,
"label": "审核列表",
"rightId": 21,
"key": "/audit-manage/list",
"pagepermisson": 1,
"grade": 2
}
]
},
{
"id": 24,
"label": "发布管理",
"key": "/publish-manage",
"pagepermisson": 1,
"grade": 1,
"children": [
{
"id": 25,
"label": "待发布",
"rightId": 24,
"key": "/publish-manage/unpublished",
"pagepermisson": 1,
"grade": 2
},
{
"id": 26,
"label": "已发布",
"rightId": 24,
"key": "/publish-manage/published",
"pagepermisson": 1,
"grade": 2
},
{
"id": 27,
"label": "已下线",
"rightId": 24,
"key": "/publish-manage/sunset",
"pagepermisson": 1,
"grade": 2
}
]
}
]
这里就是我们请求来的json数据 其中id是每个对象都有的 label表示菜单名 也就是我们显示在侧边栏的数据。pagepermisson属性表示我们这子项是否显示在菜单上 如果没有这个属性 或者这个属性不为1 就表示这个对象带表的子项不会出现在侧边栏中。rightid表是从属关系表是这个chilrden从属于哪个父菜单。
下面我们来仔细看看item数据的处理过程:上面展示的数据是我们未经处理的数据 如果直接拿来用会出问题 会有很多不必要的菜单选项出现在侧边栏。
useEffect(() => {setSelectedKeys([location.pathname])const getItems = async () => {const res = await axios.get('http://localhost:3000/rights?_embed=children');//处理首页部分标签不需要展示问题// console.log(res.data);//处理完的新结果let newRes=[];res.data.forEach(item => {// 分开处理 处理存在children的和不存在的if(item.children.length===0){delete item.childrennewRes.push(item);}else{let newResChildren=[];item.children.forEach(element => {//如果是首页就添加到新的children数组中去if(element.pagepermisson===1){newResChildren.push(element)}});//更新数据 继承原有的item children属性替换成newResChildrennewRes.push({...item,children:newResChildren})}});setItems(newRes);// console.log(newRes);const setIcon=(item,iconMap)=>{// 创建副本以避免直接修改原数组const newItems = JSON.parse(JSON.stringify(item));newItems.forEach(element=>{ for(let key in iconMap){if(key===element.label){element.icon=iconMap[key]}}});return newItems;}setItems(setIcon(newRes,iconMap));};getItems();}, [location.pathname]);
将处理函数放在useEffect中作为副作用处理 因为请求是一个异步操作所以还是建议放在useEffect中的。
在代码中,当我们通过异步操作得到res原始数据后观察可以知道,有一部分对象是没有pagepermisson属性的或者是对象的children属性中部分元素没有pagermission属性,所以我们的任务是将这些属性筛选出来 通过foreach遍历 将先将children属性长度为0 的对象找出来 然后删除children属性 因为没有children属性不存在所以没必要进行后续处理 我们直接将这个对象压入栈 也就是存放到数组newRes中。处理完没有children属性的对象之后 我们就开始处理那些不含pagepermisson属性的元素 通过children.foreach方法继续遍历将存在permisson属性的元素压入新的newReschildren数组中 在一切都处理完成之后我们通过继承和替换将处理完的数据存放在newRes中 然后setItem将新数据设置给Item状态。
至于后面设置icon标签的方法就是遍历item数据 将icon映射给相同label的菜单项。
到此我们就完成了SideMenu组件的开发。
三:TopHeader组件开发
(1)效果预览
效果很简单 所以我就简单过一下
(2)开发过程
头组件很简单 ,直接来看源码就行 至于过程就不再过多赘述。可能唯一的要点就是通过状态来控制小箭头的左右方向
import React from 'react'
import {MenuFoldOutlined,MenuUnfoldOutlined,UploadOutlined,UserOutlined,VideoCameraOutlined,
} from '@ant-design/icons';
import { Button, Layout, Menu,theme } from 'antd';
const { Header, Sider, Content } = Layout;
import { DownOutlined } from '@ant-design/icons';
import { Dropdown, Space } from 'antd';
import { Avatar } from 'antd';
export default function TopHeader({collapsed,setCollapsed}) {const {token: { colorBgContainer },} = theme.useToken();const items = [{label: (<p>退出</p>),key: '0',},{label: (<p>超级管理员</p>),key: '1',},];return (<div><Header style={{ padding: 0, background: colorBgContainer }}><Buttontype="text"icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}onClick={() => setCollapsed(!collapsed)}style={{fontSize: '16px',width: 64,height: 64,}}/><div style={{float:'right'}}><span >欢迎admin使用</span><Dropdown menu={{ items }}><a onClick={e => e.preventDefault()}><Space><Avatar size={34} icon={<UserOutlined />} style={{margin:'0 8px'}}/><DownOutlined /></Space></a></Dropdown></div></Header></div>)
}