如何读懂一个react后台项目
之前做了一个,因此有一定的基础。
QUESTION:在阅读antd组件源码时,发现还有很多看不懂,非常吃力。一方面是因为自己没有tsx组件设计的经验(其实没有ts的使用经验,也只是能看懂);也有一部分是不会快速上手一个项目
1. 概览
1.1 node_modules
该项目存在nodejs环境,这是npm包管理文件夹
1.2 public
静态资源文件夹,在里面看到了index.html
React App
1.3 src
里面有index.js,看来是页面的主要内容文件了
1.4 package.json
package.json 文件是项目的清单。 它可以做很多完全互不相关的事情。 例如,它是用于工具的配置中心。 它也是 npm 和 yarn 存储所有已安装软件包的名称和版本的地方。
{ // https://www.jianshu.com/p/b525e009cc4e
"name": "newssystem", //项目名
"version": "0.1.0",//版本号
"private": true,//私有项目,禁止意外发布私有存储库的方法
"dependencies": {//依赖包,在开发和线上环境均需要使用
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^13.5.0",
"antd": "^4.18.9",//antd组件库
"axios": "^0.26.0",//网络请求
"dayjs": "^1.10.8",// 日期事件处理
"draft-js": "^0.11.7",//富文本编辑
"draftjs-to-html": "^0.9.1",// 富文本转html
"echarts": "^5.3.1",//图表
"html-to-draftjs": "^1.5.0",// HTML转富文本
"http-proxy-middleware": "^2.0.3",// 反向代理中间件
"lodash-es": "^4.17.21",//js方法库
"moment": "^2.29.1",// 日期事件处理,已用dayjs替换掉了(性能优化https://www.cnblogs.com/shixiu/p/16002113.html)
"nprogress": "^0.2.0",//进度条
"react": "^17.0.2",//react核心库
"react-dom": "^17.0.2",//react核心库,处理虚拟DOM渲染等功能
"react-draft-wysiwyg": "^1.14.7",// react富文本编辑器,基于 ReactJS 和 DraftJS
"react-redux": "^7.2.6",//状态管理
"react-router-dom": "^6.2.2",//路由
"react-scripts": "5.0.0",//react项目配置https://newsn.net/say/react-scripts-action.html
"react-tsparticles": "^1.41.6",// 粒子效果
"redux": "^4.1.2",//状态管理
"redux-persist": "^6.0.0",// 状态持久化
"sass": "^1.49.9",// css拓展
"web-vitals": "^2.1.4"// 性能检测工具https://juejin.cn/post/6930903996127248392
},
"scripts": {//配置命令,执行npm run xxx即可运行scripts文件下对应的js文件
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {//eslint规则
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {//浏览器兼容范围,也可以配置在.browserslistrc文件,会被Autoprefixer Babel postcss-preset-env等使用
"production": [
">0.2%",//兼容市场份额在0.2%以上的浏览器
"not dead", //在维护中
"not op_mini all"//忽略OperaMini浏览器
],
"development": [//开发环境只需兼容以下三种浏览器的最新版本
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {//只在开发环境存在的依赖
"terser-brunch": "^4.1.0"// Brunch 生产构建
}
}
1.5 package-lock.json
- 该文件旨在跟踪被安装的每个软件包的确切版本,以便产品可以以相同的方式被 100% 复制(即使软件包的维护者更新了软件包)。
- package-lock.json 会固化当前安装的每个软件包的版本,当运行 npm install时,npm 会使用这些确切的版本。
- 当运行 npm update 时,package-lock.json 文件中的依赖的版本会被更新。
2. JS
2.1 index.js
index.js是项目的入口文件
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
// 导入APP组件
import App from './App';
// 导入工具包
import './util/http'
// react拓展禁用
if (window.location.port && typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function () {}
}
// ReactDOM.render(template,targetDOM)将app组件渲染到root根节点中
ReactDOM.render(
,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
2.2 util/http.js
index.js引入了我们就先看下。在这里配置了axios网络请求的内容;使用redux完成当进行网络请求时显示加载的圈。
// Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js
import axios from 'axios'
// 导入redux-store
import {store} from '../redux/store'
// 全局axios默认值
axios.defaults.baseURL="http://localhost:5000"
// axios.defaults.headers
// 请求拦截器、响应拦截器
// axios.interceptors.request.use
// axios.interceptors.response.use
axios.interceptors.request.use(function (config) {
// Do something before request is sent
// 显示loading。更新 state 的唯一方法是调用 store.dispatch() 并传入一个 action 对象。
store.dispatch({
type:"change_loading",
payload:true
})
return config;
}, function (error) {
// Do something with request error
// Promise.reject()方法返回一个带有拒绝原因(error)的Promise对象。
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
store.dispatch({
type:"change_loading",
payload:false
})
//隐藏loading
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
store.dispatch({
type:"change_loading",
payload:false
})
//隐藏loading
return Promise.reject(error);
});
2.3 redux
对redux不熟悉的可以参考
2.3.1 store.js
状态容器。
// 创建Store,combineReducers:合并Reducer
import {createStore,combineReducers} from 'redux'
// 导入两个Reducer
import {CollapsedReducer} from './reducers/CollapsedReducer'
import {LoadingReducer} from './reducers/LoadingReducer'
// 状态持久化:https://github.com/rt2zz/redux-persist
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
// 持久化配置,LoadingReducer列入黑名单(不需要持久化)
const persistConfig = {
key: 'hangyi',
storage,
blacklist: ['LoadingReducer']
}
// 合并
const reducer = combineReducers({
CollapsedReducer,
LoadingReducer
})
// 持久化Reducer
const persistedReducer = persistReducer(persistConfig, reducer)
// 创建store
const store = createStore(persistedReducer);
// 持久化store
const persistor = persistStore(store)
export {
store, // 正常store
persistor // 持久化后的store
}
/*
store.dispatch()
store.subsribe()
*/
2.3.2 reducers/CollapsedReducer.js
侧边折叠状态控制。
// 当前state为 isCollapsed:false
export const CollapsedReducer = (prevState={
isCollapsed:false
},action)=>{
// 解构出type
let {type} =action
// 检查reducer是否关心传入的action
switch(type){
case "change_collapsed": // 如果关心
let newstate = {...prevState} // 复制state
newstate.isCollapsed = !newstate.isCollapsed // 改变state:取反
return newstate
default: // 不关心则不变
return prevState
}
}
2.3.3 reduces/LoadingReducer.js
网络请求状态控制。
export const LoadingReducer = (prevState={
isLoading:false
},action)=>{
// 解构出type,以及附加信息payload
let {type,payload} =action
switch(type){
case "change_loading":
let newstate = {...prevState}
newstate.isLoading = payload
return newstate
default:
return prevState
}
}
结合http.js和LoadingReducer.js,我们知道它实现了一个功能:每次发送axios请求前,将Loading状态改为true;拿到响应后结束Loading。这个状态可以用来设置数据加载时的一些用户体验,我们往后看。
2.4 App.js
这里应该就是App的核心内容架构了
import './App.css'
// 引入路由配置
import IndexRouter from "./router/IndexRouter";
// 引入store配置
import { Provider } from "react-redux";
import {store} from "./redux/store";
function App(){
// Provider包裹
return
}
export default App
2.5 router
我们来看下路由配置
import React from 'react'
// 路由相关
import {
HashRouter as Router, // HashRouter只是一个容器,并没有DOM结构,它渲染的就是它的子组件,并向下层传递locationhttps://www.cnblogs.com/lyt0207/p/12734944.html
Routes, // 路由容器:只显示匹配到的第一个路由(以前版本的switch)
Route, // 路由规则
Navigate // 导航&重定向
} from "react-router-dom"
// 四个组件
import Login from '../views/login/Login'
import NewsSandBox from '../views/sandbox/NewsSandBox'
import News from '../views/news/News'
import Detail from '../views/news/Detail'
export default function IndexRouter() {
return (
} />
}/>
}/>
: } />
{/*
{localStorage.getItem("token")? } />: } />}
*/}
)
}
到这里我们可以结合axios网络请求的配置看下页面了
登陆页面 http://localhost:3000/#/login
游客新闻页面 http://localhost:3000/#/news
游客新闻详情 http://localhost:3000/#/detail/3
对其他任意页面,如果localStorage.getItem("token")
有值会导航到组件,如果没有则会导航到login组件
去看看吧
2.6 views/sandbox/NewsSandBox.js
主页面框架。
import React, { useEffect } from 'react' // useEffect副作用函数,一般用来设置请求数据、事件处理、订阅等
// 导入了侧边栏和顶栏的组件
import SideMenu from '../../components/sandbox/SideMenu'
import TopHeader from '../../components/sandbox/TopHeader'
import NProgress from 'nprogress' // nprogress是个进度条插件
import 'nprogress/nprogress.css'
//css
import './NewsSandBox.css'
//antd
import { Layout } from 'antd'
// 导入新闻路由组件
import NewsRouter from '../../components/sandbox/NewsRouter'
// 解构出Content组件
const { Content } = Layout;
export default function NewsSandBox() {
// 进度条加载,'组件挂载完成之后 或 组件数据更新完成之后 执行'进度条取消:https://developer.aliyun.com/article/792403
NProgress.start()
useEffect(()=>{
NProgress.done()
})
return (
className="site-layout-background"
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
}}
>
)
}
2.7 components/sandbox/SideMenu
先看下侧边栏吧
import React, { useState, useEffect } from 'react';
import '../../index.css'
import { Layout, Menu } from 'antd';
import {
UserOutlined
} from '@ant-design/icons';
// useNavigate导航方法。useLocation获取路径
import { useNavigate, useLocation } from 'react-router-dom'
import axios from 'axios';
import {connect} from 'react-redux'
const { Sider } = Layout;
const { SubMenu } = Menu;
// // 模拟数组结构
// const menuList = [
// {
// key: "/home",
// title: "首页",
// icon:
// },
// {
// key: "/user-manage",
// title: "用户管理",
// icon: ,
// children: [
// {
// key: "/user-manage/list",
// title: "用户列表",
// icon:
// }
// ]
// },
// {
// key: "/right-manage",
// title: "权限管理",
// icon: ,
// children: [
// {
// key: "/right-manage/role/list",
// title: "角色列表",
// icon:
// },
// {
// key: "/right-manage/right/list",
// title: "角色列表",
// icon:
// }
// ]
// }
// ]
// 定义了一个对象,映射侧边栏路径与图标
const iconList = {
"/home": ,
"/user-manage": ,
"/user-manage/list": ,
"/right-manage": ,
"/right-manage/role/list": ,
"/right-manage/right/list":
//.......
}
// 既然用到了connect,就要传props了,不知道connect的看下[Redux入门](https://www.cnblogs.com/shixiu/p/16011266.html)
function SideMenu(props) {
// 要动态渲染侧边栏,就会有状态
const [menu, setMenu] = useState([])
// 初始化侧边栏内容列表,包含了父子关系(树形)
useEffect(() => {
axios.get("/rights?_embed=children").then(res => {
// console.log(res.data)
setMenu(res.data)
})
}, []);
// 解构当前用户的页面权限,JSON.parse解析JSON格式返回对象
const {role: {rights}} = JSON.parse(localStorage.getItem("token"));
// 检查登录用户页面权限方法:
const checkPagePermission = (item) => {
return item.pagepermisson && rights.includes(item.key)
};
// 导航方法
const navigate = useNavigate();
// 截取当前URL路径
const location = useLocation();
const selectedkeys = location.pathname; // 截取出整个路径
const openkeys = ["/" + location.pathname.split("/")[1]]; // 截取出一级路径
// 侧边栏内容列表渲染方法:传入形参menuList
const renderMenu = (menuList) => {
// map遍历
return menuList.map(item => {
// 检查每一项是否有下级列表(使用可选链语法)&& 页面权限
if (item.children?.length > 0 && checkPagePermission(item)) { // 如果有子菜单
return // 渲染父菜单
{renderMenu(item.children)} // 递归调用渲染子菜单
}
return checkPagePermission(item) &&
navigate(item.key)
}>{item.title} // 没有子菜单的直接检查权限并渲染
})
}
return (
新闻发布后台管理系统
)
}
// 利用connect连接store,获取props.isCollapsed
const mapStateToProps = ({CollapsedReducer: {isCollapsed}})=>({isCollapsed})
export default connect(mapStateToProps)(SideMenu)
2.8 components/sandbox/TopHeader.js
顶栏。
import React from 'react'
import { Layout,Menu, Dropdown,Avatar } from 'antd';
import {
MenuUnfoldOutlined,
MenuFoldOutlined,
UserOutlined
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'
import {connect} from 'react-redux'
const { Header } = Layout;
function TopHeader(props) {
// console.log(props)
const navigate = useNavigate();
// 因为使用了store,所以不需要在组件内定义这个状态了
// const [collapsed, setCollapsed] = useState(false)
const changeCollapsed = () => {
// 使用props.changeCollapsed()触发action
// setCollapsed(!collapsed)
props.changeCollapsed()
}
// 解构出用户名和权限
const {username,role: {roleName}} = JSON.parse(localStorage.getItem("token"));
// 退出登录的方法
const leaveMethod = () => {
localStorage.removeItem("token");
navigate("/login")
}
// 头像菜单栏
const menu = (
);
return (
{/* {React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: this.toggle,
})} */}
{props.isCollapsed ? : }
欢迎{username}回来
} />
)
}
/*
connect(
// mapStateToProps
// mapDispatchToProps
)(被包装的组件)
*/
// 从store获取isCollapsed状态
const mapStateToProps = ({CollapsedReducer: {isCollapsed}})=>{
return {
isCollapsed
}
}
// dispatch 更新store的状态
const mapDispatchToProps = {
changeCollapsed(){
return {
type:"change_collapsed"
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(TopHeader)
至此我们可以看出项目中redux的一个使用了。利用CollapsedReducer完成了兄弟组件之间的通信:顶部栏图标影响侧边栏的折叠状态;并通过黑名单持久化保证了刷新页面也能维持折叠状态。
2.9 components/sandbox/NewsRouter.js
侧边栏看完,看看页面主体吧。看起来又封装了一个路由。
import React, { useEffect, useState,Suspense,lazy } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
import axios from 'axios'
import {connect} from 'react-redux'
import { Spin } from 'antd'
// 路由懒加载lazy()能实现切换到该页面时才加载相关js,这样会进行代码分割,webpack打包时会分开打
import Home from '../../views/sandbox/home/Home'
const NoPermission = lazy(()=> import('../../views/sandbox/nopermission/NoPermission'))
const RightList = lazy(()=> import('../../views/sandbox/right-manage/RightList'))
const RoleList = lazy(()=> import('../../views/sandbox/right-manage/RoleList'))
const UserList = lazy(()=> import('../../views/sandbox/user-manage/UserList'))
const NewsAdd = lazy(()=> import('../../views/sandbox/news-manage/NewsAdd'))
const NewsDraft = lazy(()=> import('../../views/sandbox/news-manage/NewsDraft'))
const NewsCategory = lazy(()=> import('../../views/sandbox/news-manage/NewsCategory'))
const Audit = lazy(()=> import('../../views/sandbox/audit-manage/Audit'))
const AuditList = lazy(()=> import('../../views/sandbox/audit-manage/AuditList'))
const Unpublished = lazy(()=> import('../../views/sandbox/publish-manage/Unpublished'))
const Published = lazy(()=> import('../../views/sandbox/publish-manage/Published'))
const Sunset = lazy(()=> import('../../views/sandbox/publish-manage/Sunset'))
const NewsPreview = lazy(()=> import('../../views/sandbox/news-manage/NewsPreview'))
const NewsUpdate = lazy(()=> import('../../views/sandbox/news-manage/NewsUpdate'))
// 路由与组件映射
const LocalRouterMap = {
"/home": ,
"/user-manage/list": ,
"/right-manage/role/list": ,
"/right-manage/right/list": ,
"/news-manage/add": ,
"/news-manage/draft": ,
"/news-manage/preview/:id": ,
"/news-manage/update/:id": ,
"/news-manage/category": ,
"/audit-manage/audit": ,
"/audit-manage/list": ,
"/publish-manage/unpublished": ,
"/publish-manage/published": ,
"/publish-manage/sunset":
}
function NewsRouter(props) {
const [BackRouteList, setBackRouteList] = useState([])
// 初始化所有路由列表
useEffect(() => {
Promise.all([ // 使用Promise等待一级菜单,二级菜单数据返回
axios.get("/rights"),
axios.get("/children"),
]).then(res => {
// console.log(res)
setBackRouteList([...res[0].data, ...res[1].data]) // 设置返回路由清单为所有的页面(一级+二级)
// console.log([...res[0].data,...res[1].data])
})
}, [])
const {role:{rights}} = JSON.parse(localStorage.getItem("token"))
// 路由自身页面权限:映射列表存在+ (页面配置权限(判断该一级页面是否允许配置)||路由配置权限(判断该二级页面是否是侧边路由))
const checkRoute = (item)=>{
return LocalRouterMap[item.key] && (item.pagepermisson || item.routepermisson)
}
// 用户权限检查:解构出来的页面列表
const checkUserPermission = (item)=>{
return rights.includes(item.key)
}
return (
{BackRouteList.map(item => {
if(checkRoute(item) && checkUserPermission(item)){
return Loading...
至此总体的路由、状态管理等就已经看完了。接下来就到具体的页面和组件了。这部分就基本上只看业务逻辑就行了。
3. views
3.1 login/Login.js
登录模块
- 使用Particles实现登陆页面粒子效果
- Form表单实现用户数据收集
- 表单上定义onFinish方法,收集数据后像后端请求验证,如果返回的数据长度为0,说明用户名&密码不正确,使用antd message完成消息弹出;否则登陆成功,localStorage的token存储数据,跳转至首页。
import React from 'react'
import './Login.css'
import { Form, Button, Input ,message } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import Particles from "react-tsparticles";
import axios from 'axios'
import { useNavigate } from 'react-router-dom'
export default function Login(props) {
const navigate = useNavigate();
const onFinish = (values) => {
axios.get(`/users?username=${values.username}&password=${values.password}&roleState=true&_expand=role`).then(res=>{
if(res.data.length===0){
message.error("用户名或密码不匹配")
}else{
localStorage.setItem("token",JSON.stringify(res.data[0]))
navigate("/")
}
})
}
return (
全球新闻发布管理系统
} placeholder="Username" />
}
type="password"
placeholder="Password"
autoComplete="on"
/>
)
}
3.2 sandbox
3.2.1 home/Home.js
首页
首页画了三个卡片和一个柱形图。
- 使用ref.current获取dom节点,使echarts图表正确内置
- 使用lodash的groupBy方法处理数据
- 使用JSON-SERVER内置方法获取前六的数据
- 使用Echarts.resize()来时图表响应window.onresize的变化
- 新闻标题点击进入预览页面,这与新闻管理时的新闻预览需求合并,提取出了NewsPreview组件
- 图片使用了webp格式优化性能
- 在card组件使用action属性,在里面写了一个设置图标触发饼图的Drawer容器渲染,并在容器内部渲染饼图。在这里使用settimeout函数封装了先显示Drawer容器,再渲染饼图。原理是settimeout作为异步函数,内部的任务会进入任务队列执行,而任务队列先进先出,所以能先渲染容器。react的状态更新是异步的,因此不能保证先后。
import React, { useEffect, useState, useRef } from 'react'
import { Card, Col, Row, List, Avatar, Drawer } from 'antd';
import { EditOutlined, EllipsisOutlined, SettingOutlined } from '@ant-design/icons';
import axios from 'axios'
// 按需引入lodash
import {groupBy} from 'lodash-es'
// 按需引入echarts
// import * as Echarts from 'echarts'
import * as Echarts from 'echarts/core';
import {
BarChart,
PieChart
} from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DatasetComponent
} from 'echarts/components';
import {
CanvasRenderer
} from 'echarts/renderers';
Echarts.use(
[
TitleComponent,
TooltipComponent,
GridComponent,
BarChart,
PieChart,
LegendComponent,
DatasetComponent,
CanvasRenderer
]
);
const { Meta } = Card;
export default function Home() {
// const ajax = () => {
// // 取数
// // axios.get("http://localhost:8000/posts/1").then(res => console.log(res.data))
// // 增数
// // axios.post("http://localhost:8000/posts",{
// // title:"title3",
// // author:"threeMan"
// // })
// // 修改
// // axios.put("http://localhost:8000/posts/1",{
// // title:"title1.1"
// // })
// // 更新
// // axios.patch("http://localhost:8000/posts/1",{
// // title:"title1.2"
// // })
// // 删除
// // axios.delete("http://localhost:8000/posts/2")
// // _embed 级联关系
// // axios.get("http://localhost:8000/posts?_embed=comments").then(res => console.log(res.data))
// // _expand 父级关系
// axios.get("http://localhost:8000/comments?_expand=post").then(res => console.log(res.data))
// }
const [viewList, setviewList] = useState([])
const [starList, setstarList] = useState([])
const [allList, setallList] = useState([])
const [visible, setvisible] = useState(false)
const [pieChart, setpieChart] = useState(null)
const barRef = useRef()
const pieRef = useRef()
useEffect(() => {
axios.get("/news?publishState=2&_expand=category&_sort=view&_order=desc&_limit=6").then(res => {
// console.log(res.data)
setviewList(res.data)
})
}, [])
useEffect(() => {
axios.get("/news?publishState=2&_expand=category&_sort=star&_order=desc&_limit=6").then(res => {
// console.log(res.data)
setstarList(res.data)
})
}, [])
useEffect(() => {
axios.get("/news?publishState=2&_expand=category").then(res => {
// console.log(res.data)
// console.log()
// 柱形图数据
renderBarView(groupBy(res.data, item => item.category.title))
// 饼图数据(需要更多处理
setallList(res.data)
})
// 组件销毁时清除图标响应
return ()=>{
window.onresize = null
}
}, [])
const renderBarView = (obj) => {
// console.log(obj)
var myChart = Echarts.init(barRef.current);
// 指定图表的配置项和数据
var option = {
title: {
text: '新闻分类图示'
},
tooltip: {},
legend: {
data: ['数量']
},
xAxis: {
data: Object.keys(obj),
axisLabel:{
rotate:"45",
interval:0
}
},
yAxis: {
minInterval: 1
},
series: [{
name: '数量',
type: 'bar',
data: Object.values(obj).map(item => item.length)
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
// 图表响应
window.onresize= ()=>{
// console.log("resize")
myChart.resize()
}
}
// 饼图渲染
const renderPieView = (obj) => {
//数据处理工作
let currentList =allList.filter(item=>item.author===username)
let groupObj = groupBy(currentList,item=>item.category.title)
let list = []
for(let i in groupObj){
list.push({
name:i,
value:groupObj[i].length
})
}
let myChart;
if(!pieChart){
myChart = Echarts.init(pieRef.current);
setpieChart(myChart)
}else{
myChart = pieChart
}
let option = {
title: {
text: '当前用户新闻分类图示',
// subtext: '纯属虚构',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '发布数量',
type: 'pie',
radius: '50%',
data: list,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
option && myChart.setOption(option);
}
const { username, region, role: { roleName } } = JSON.parse(localStorage.getItem("token"))
return (
{item.title}
}
/>
{item.title}
}
/>
}
actions={[
{
setTimeout(() => {
setvisible(true)
// init初始化
renderPieView()
}, 0)
}} />,
,
,
]}
>
}
title={username}
description={
{region ? region : "全球"}
{roleName}
}
/>
{
setvisible(false)
}}
visible={visible}
>
)
}
3.2.2 right-manage
权限管理
3.2.2.1 RoleList.js
角色管理:渲染了一个表格
- antd column中,如果设置了dataIndex,则render(x,y)中形参x为dataindex\y为行数据(y可以省略);如果没有dataindex,则render(y)
- 删除方法中,我们使用dataSource.filter返回id不等于删除项id的数据并用来更新状态值,随后推送后端删除
- 使用antd组件modal弹出对话框,在其中包裹树形组件Tree
- 点击操作按钮时,触发三个操作:显示对话框;初始化该角色权限数据;初始化该角色ID
- 点击权限树中的某个选项时,根据checkedKeys.checked属性值更新该角色权限数据
- 点击OK时,触发:隐藏对话框;使用{...item, rights:currentRights}写法更新该角色数据;再使用patch修补后端数据
import { Button, Table, Modal, Tree} from 'antd';
import axios from 'axios';
import React, { useEffect, useState } from 'react'
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
const { confirm } = Modal;
export default function RoleList() {
const [dataSource,setdataSource] = useState([]);
const [isModalVisible,setisModalVisible] = useState(false);
const [rightList,setRightList] = useState([]);
const [currentRights,setcurrentRights] = useState([]);
const [currentId,setcurrentId] = useState(0);
useEffect(() => {
axios.get("/roles").then(res => {
setdataSource(res.data)
})
},[]);
useEffect(() => {
axios.get("/rights?_embed=children").then(res => {
setRightList(res.data)
})
},[])
const columns = [
{
title: 'ID',
dataIndex: 'id',
render: (id) => {
return {id}
}
},
{
title: '角色名称',
dataIndex: 'roleName'
},
{
title: "操作",
render: (item) => {
return
} onClick={() => confirmDelete(item)} />
} onClick={() => {
setisModalVisible(true);
setcurrentRights(item.rights);
setcurrentId(item.id);
}} />
}
}
];
const confirmDelete = (item) => {
confirm({
title: '你确定要删除?',
icon: ,
// content: 'Some descriptions',
onOk() {
deleteMethod(item);
},
onCancel() {
// console.log('Cancel');
},
});
};
const deleteMethod = (item) => {
setdataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/roles/${item.id}`)
};
const handleOk = () => {
setisModalVisible(false);
setdataSource(dataSource.map(item => {
if (item.id===currentId) {
return {
...item,
rights:currentRights
}
}
return item
}));
axios.patch(`/roles/${currentId}`,{
rights:currentRights
});
};
const handleCancel = () => {
setisModalVisible(false);
};
const onCheck = (checkedKeys) => {
setcurrentRights(checkedKeys.checked);
}
return (
item.id} />
)
}
3.2.2.2 RightList.js
权限列表
- antd Pagination 分页
- 删除页面方法:先判断是否为1级页面;否则根据rightId字段找到children进行删除
- 使用了antd tag标签
- 使用了antd Popover气泡标签
import { Button, Popover, Table, Tag, Modal, Switch } from 'antd'
import axios from 'axios';
import React, { useEffect, useState } from 'react'
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
const { confirm } = Modal;
export default function RightList() {
const [dataSource, setdataSource] = useState([]);
useEffect(() => {
axios.get("/rights?_embed=children").then(res => {
const list = res.data;
list.forEach(element => {
if (element.children.length === 0) {
element.children = ""
}
});
setdataSource(list)
})
}, []);
const confirmDelete = (item) => {
confirm({
title: '你确定要删除?',
icon: ,
// content: 'Some descriptions',
onOk() {
deleteMethod(item);
},
onCancel() {
// console.log('Cancel');
},
});
};
const deleteMethod = (item) => {
if (item.grade === 1) {
setdataSource(dataSource.filter(data => data.id !== item.id));
axios.delete(`/rights/${item.id}`)
} else {
const list = dataSource.filter(data => data.id === item.rightId);
list[0].children = list[0].children.filter(data => data.id !== item.id);
setdataSource([...dataSource]);
axios.delete(`/children/${item.id}`)
}
};
const switchMethod = (item) => {
item.pagepermisson = item.pagepermisson===1?0:1;
setdataSource([...dataSource]);
if (item.grade === 1) {
axios.patch(`/rights/${item.id}`,{pagepermisson:item.pagepermisson})
} else {
axios.patch(`/children/${item.id}`,{pagepermisson:item.pagepermisson})
}
};
const columns = [
{
title: "ID",
dataIndex: "id",
render: (id) => {
return {id}
}
},
{
title: "权限名称",
dataIndex: "title"
},
{
title: "权限路径",
dataIndex: "key",
render: (key) => {
return {key}
}
},
{
title: "操作",
render: (item) => {
return
} onClick={() => confirmDelete(item)} />
switchMethod(item)} />
} trigger={item.pagepermisson===undefined?'':"click"}>
} disabled={item.pagepermisson===undefined} />
}
}
]
return (
)
}
3.2.3 user-manage
用户管理
3.2.3.1 user-manage/UserList.js
用户列表。用户列表展示一个增加用户按钮,一个用户表格。
- 使用Modal对话框弹出增加用户、编辑用户操作。
- 增加用户、编辑用户共用UserForm组件,使用父组件定义ref加子组件forwardref获取子组件DOM节点和值
- 再columns使用filters、onFilter完成列数据筛选
- 增加用户:
addForm.current.validateFields().then(value => {}).catch(err =>{})
进行表单校验,有数据进行后端提交等操作, addForm.current.resetFields()
进行清空表单
import { Button, Table, Modal, Switch } from 'antd'
import axios from 'axios';
import React, { useEffect, useState, useRef } from 'react'
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
import UserForm from '../../../components/user-manage/UserForm';
const { confirm } = Modal;
export default function UserList() {
const [dataSource, setdataSource] = useState([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [roleList, setroleList] = useState([]);
const [regionList, setregionList] = useState([]);
const addForm = useRef("");
const [isUpdateVisible,setisUpdateVisible] = useState(false);
const updateForm = useRef("");
const [isUpdateDisabled,setisUpdateDisabled] = useState(false);
const [current,setcurrent] =useState(null);
const {roleId,region,username} = JSON.parse(localStorage.getItem("token"));
// 初始化用户权限列表
useEffect(() => {
const roleObj = {
"1":"superadmin",
"2":"admin",
"3":"editor"
}
axios.get("/users?_expand=role").then(res => {
const list = res.data;
setdataSource(roleObj[roleId]==="superadmin"?list:[
// 超级管理员不限制,区域管理员:自己+自己区域编辑,区域编辑:看不到用户列表
...list.filter(item=>item.username===username),
...list.filter(item=>item.region===region&& roleObj[item.roleId]==="editor")
])
})
}, [roleId,region,username]);
// 初始化区域列表
useEffect(() => {
axios.get("/regions").then(res => {
const list = res.data;
setregionList(list)
})
}, []);
// 初始化角色列表
useEffect(() => {
axios.get("/roles").then(res => {
const list = res.data;
setroleList(list)
})
}, []);
// 删除确认,删除方法
const confirmDelete = (item) => {
confirm({
title: '你确定要删除?',
icon: ,
// content: 'Some descriptions',
onOk() {
deleteMethod(item);
},
onCancel() {
// console.log('Cancel');
},
});
};
const deleteMethod = (item) => {
setdataSource(dataSource.filter(data => data.id !== item.id))
axios.delete(`/users/${item.id}`)
};
// 开关方法
const switchMethod = (item) => {
item.roleState = !item.roleState;
setdataSource([...dataSource]);
axios.patch(`/users/${item.id}`, {
roleState: item.roleState
})
};
// 增加用户对话框
const showModal = () => {
setIsModalVisible(true);
};
const handleOk = () => {
addForm.current.validateFields().then(value => {
setIsModalVisible(false);
// 重置下增加表单
addForm.current.resetFields();
// 先post生成id
axios.post("/users", {
...value,
roleState: true,
default: false
}).then(res => {
setdataSource([...dataSource, {
...res.data,
// 提交数据中没有角色名称,是关联得来的
role: roleList.filter(item => item.id === value.roleId)[0]
}])
})
//
}).catch(err => {
console.log(err)
})
};
const handleCancel = () => {
setIsModalVisible(false);
};
// 更新用户对话框
const showUpdate = (item) => {
setTimeout(()=> {setisUpdateVisible(true);
if(item.roleId===1) {
setisUpdateDisabled(true)
} else {
setisUpdateDisabled(false)
};
updateForm.current.setFieldsValue(item)},0)
setcurrent(item);
};
const updateOk = () => {
updateForm.current.validateFields().then(value => {
setisUpdateVisible(false);
setdataSource(dataSource.map(item => {
if(item.id===current.id) {
return {
...item,
...value,
role:roleList.filter(data => data.id === value.roleId)[0]
}
}
return item
}))
setisUpdateDisabled(!isUpdateDisabled);
axios.patch(`/users/${current.id}`,value)
})
};
const updateCancel = () => {
setisUpdateVisible(false);
setisUpdateDisabled(!isUpdateDisabled);
};
const columns = [
{
title: "区域",
dataIndex: "region",
filters:[
...regionList.map(item => ({
text:item.title,
value:item.value
})),
{
text:"全球",
value:"全球"
}
],
onFilter: (value, item) => {
if(value==="全球") {
return item.region === ""
}
return item.region.includes(value)
},
render: (region) => {
return {region === "" ? "全球" : region}
}
},
{
title: "角色名称",
dataIndex: "role",
render: (role) => {
return role?.roleName
}
},
{
title: "用户名称",
dataIndex: "username"
},
{
title: "用户状态",
dataIndex: "roleState",
render: (roleState, item) => {
return switchMethod(item)} />
}
},
{
title: "操作",
render: (item) => {
return
} onClick={() => confirmDelete(item)} disabled={item.default} />
} disabled={item.default} onClick={() => showUpdate(item)} />
}
}
]
return (
item.id}
/>
)
}
3.2.3.2 components/user-manage/UserForm.js
用户表单
- 使用forwardRef保证父组件能拿到和控制子组件(本组件)的节点和值
- 有props,父组件需要向该组件传递isUpdateDisabled、isUpdate、regionList、roleList四个属性
- 该组件返回一个表单:用户名、密码、区域、角色
- 区域的验证规则:
- 用isDisabled状态接受props.isUpdateDisabled;
- isDisabled为真时,没有规则校验;为假时触发校验;
- isDisabled为真时,禁用select选择。
结合父组件看:
- isUpdateDisabled默认值为false;
- 弹出更新框时,校验当前修改的用户角色,如果是超级管理员,则设为true;否则设为假。
- 当关闭更新框时,对该值进行取反。
从用户逻辑的角度,当修改某个用户时:
【父组件点击弹出更新框时】如果修改用户初始为超级管理员,设置isUpdateDisabled为真,避免违反子组件校验规则;否则设置isUpdateDisabled为假,子组件正常校验。设置完之后使用updateForm.current.setFieldsValue(item)
向子组件填写数据。
【进入到子组件表单】当修改某用户为超级管理员时,设置isDisabled为真,避免违反区域校验规则;并使用ref.current.setFieldsValue({ region: ""})
设置区域为空。当修改某用户为其他时,设置isDisabled为假。
【当点击提交或者取消】对isUpdateDisabled进行取反,让子组件useEffect()函数检测到状态变化,清除掉上一次的内容。否则会出现BUG:当点击修改某个非超级,传入isUpdateDisabled false;修改其为超级,此时isDisabled为true;点击取消,再次点击修一个非超级,传入isUpdateDisabled false,useEffect()没有检测到变化,不更改isDisabled,这样出现了非超级的区域被禁用的情况。
- 因为使用了Modal,所以提交数据的时候不知道更新的哪条数据,因此需要一个current状态来比对推送后端。
const [isDisabled, setisDisabled] = useState(false)
useEffect(()=> {
setisDisabled(props.isUpdateDisabled)
},[props.isUpdateDisabled])
...
rules={isDisabled ? [] : [{ required: true, message: 'Please input your username!' }]}
...
完整代码
import React, { forwardRef, useEffect, useState } from 'react'
import { Form, Input, Select } from 'antd'
const { Option } = Select;
const UserForm = forwardRef((props, ref) => {
const [isDisabled, setisDisabled] = useState(false)
useEffect(()=> {
setisDisabled(props.isUpdateDisabled)
},[props.isUpdateDisabled])
const {roleId,region} = JSON.parse(localStorage.getItem("token"))
const roleObj = {
"1":"superadmin",
"2":"admin",
"3":"editor"
}
const checkRegionDisabled = (item)=>{
if(props.isUpdate){
if(roleObj[roleId]==="superadmin"){
return false
}else{
return true
}
}else{
if(roleObj[roleId]==="superadmin"){
return false
}else{
return item.value!==region
}
}
}
const checkRoleDisabled = (item)=>{
if(props.isUpdate){
if(roleObj[roleId]==="superadmin"){
return false
}else{
return true
}
}else{
if(roleObj[roleId]==="superadmin"){
return false
}else{
return roleObj[item.id]!=="editor"
}
}
}
return (
)
})
export default UserForm
3.2.4 news-manage
新闻管理,主要页面:新闻分类、新增新闻、新闻草稿箱,以及新闻预览、新闻更新
3.2.4.1 NewsCategory.js
新闻分类
新闻分类主要难点在可编辑单元格,参考https://ant.design/components/table-cn/#components-table-demo-edit-cell 的实现
import React, { useState, useEffect,useRef,useContext } from 'react'
import { Button, Table, Modal,Form,Input } from 'antd'
import axios from 'axios'
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
const { confirm } = Modal
export default function NewsCategory() {
const [dataSource, setdataSource] = useState([])
// 初始化新闻分类数据
useEffect(() => {
axios.get("/categories").then(res => {
setdataSource(res.data)
})
}, [])
// 修改数据保存,推送后端
const handleSave = (record)=>{
// console.log(record)
setdataSource(dataSource.map(item=>{
if(item.id===record.id){
return {
id:item.id,
title:record.title,
value:record.title
}
}
return item
}))
axios.patch(`/categories/${record.id}`,{
title:record.title,
value:record.title
})
}
// 表单列
const columns = [
{
title: 'ID',
dataIndex: 'id',
render: (id) => {
return {id}
}
},
// 栏目列需可修改
{
title: '栏目名称',
dataIndex: 'title',
onCell: (record) => ({
record,
editable: true,
dataIndex: 'title',
title: '栏目名称',
handleSave: handleSave,
}),
},
{
title: "操作",
render: (item) => {
return
} onClick={() => confirmMethod(item)} />
}
}
];
// 确认删除
const confirmMethod = (item) => {
confirm({
title: '你确定要删除?',
icon: ,
// content: 'Some descriptions',
onOk() {
// console.log('OK');
deleteMethod(item)
},
onCancel() {
// console.log('Cancel');
},
});
}
// 数据删除
const deleteMethod = (item) => {
// 当前页面同步状态 + 后端同步
setdataSource(dataSource.filter(data => data.id !== item.id))
axios.delete(`/categories/${item.id}`)
}
// 参考https://ant.design/components/table-cn/#components-table-demo-edit-cell
// 使用Context来实现跨层级的组件数据传递
const EditableContext = React.createContext(null);
const EditableRow = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
);
};
const EditableCell = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef(null);
const form = useContext(EditableContext);
useEffect(() => {
if (editing) {
inputRef.current.focus();
}
}, [editing]);
const toggleEdit = () => {
setEditing(!editing);
form.setFieldsValue({
[dataIndex]: record[dataIndex],
});
};
const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({ ...record, ...values });
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
) : (
{children}
);
}
return {childNode} ;
};
return (
item.id}
components={{
body: {
row: EditableRow,
cell: EditableCell,
}
}}
/>
)
}
3.2.4.2 NewsAdd.js
3.2.4.3 NewsUpdate.js
3.2.4.4 NewsDraft,js
3.2.4.5 NewsPreview.js
3.2.5 audit-manage
3.2.5.1 Audit.js
3.2.5.2 AuditList.js
3.2.6 publish-manage
3.2.6.1 Published.js
3.2.6.2 Unpublished.js
3.2.6.3 Sunset.js
4. 总结
看组件的js时,先看他返回了什么(就是实际渲染了什么);再去看状态
参考:
theme-color
meta
overflow
Public
package.json
package.json逐行解释
react-scripts
web-vitals性能检测工具
package-lock.json
NProgress
antd layout
antd 可编辑单元格
相关