使用React全家桶实现一个新闻发布管理系统
本项目1.0完成于2022年3月8日,请注意时效~
// 暂不可用!项目部署预览地址:点击查看
// 暂不可用!项目Github地址(包含所有源码、数据):点击查看如有疑问请私信博客,期待你的star!
项目Gitee地址(包含所有源码、数据):点击查看如有疑问请私信博客,期待你的star!
React教程参考:千锋2022版React全家桶教程
一、项目目标
1.1 个人期望
经过了下列文档&刷题,期望实战中提高。
MDN:web/html/css/js文档
React官方文档
FCC:响应式网页设计(html5等)/js算法与数据结构(ES6、面向对象编程、函数式编程、算法等)/前端开发库(react/redux/sass等)刷题
技术目标:使用React全家桶
React组件开发
React Hooks
React Router
Recat Redux
Antd组件库
1.2 产品选择
可借鉴的有:网易云音乐PC项目、后台管理系统项目。结合使用帆软数据可视化产品的经验,选择做后台管理项目。业务交互选择新闻后台管理。
1.3 项目描述
实现一个新闻发布管理平台,业务目标:
用户登录
游客访问:浏览新闻
用户管理:新增用户、修改用户、删除用户、禁用用户
权限管理:角色管理、页面访问权限控制、侧边栏权限控制
新闻业务:撰写新闻、草稿箱、新闻审核、新闻发布及下线等
1.4 适合人群
对前端有兴趣
对HTML/CSS/JS/REACT有一定了解
希望有一定的的那个项目经验
1.5 推荐用时
60h~100h
二、技术选型
create-react-app:脚手架
React Hooks:函数式编程,用过的都说真的爽
React Router V6:路由控制访问,V6升级了许多东西
Recat Redux:状态管理,组件通信
Antd组件库:你为什么要使用react?
axios:实现网络请求
JSON Server:生成数据接口
react-tsparticles:登陆页面粒子美化
draft-js:富文本编辑
draftjs-to-html:富文本转换html
html-to-draftjs:html转换富文本
Echarts:数据可视化(柱状图、饼图)
Sass: CSS辅助工具,实现变量、嵌套、导入
http-proxy-middleware:开发环境反向代理跨域(前期使用练手,JSON Server不需要~);引入后需要重启服务器
CSS Modules: CSS模块化,选取class.moduleTest或id选择器,将CSS module文件引入style变量,设置className={style.moduleTest}
三、项目模块文档
3.1 登录
实现用户登录功能:用户进入登陆页面,输入必填项账号及密码,点击登录校验账号密码,登录成功后保存状态,跳转至home页面;若登陆失败弹出“用户名或密码不匹配”。
实现效果:
(首页使用了粒子效果太大了,展示不出来)
3.2 首页
首页展示四个模块:用户最常浏览、用户点赞最多、用户信息、新闻分类。用户最常浏览模块展示浏览量最多的6个新闻标题;用户点赞最多模块展示点赞量最多的6个新闻标题;用户信息展示用户头像&名称&角色&地区,并设有按钮弹出展示该用户已发布新闻分类的饼图;新闻分类使用柱状图展示所有用户的新闻分类数量。其中,新闻标题可点击预览新闻内容。
实现效果:
![image](https://img2022.cnblogs.com/blog/2506612/202203/2506612-20220309224108055-104166641.gif)
3.3 用户管理
用户管理页面展示用户信息列表及用户操作:包括新增用户、区域筛选、用户状态开关、删除用户、编辑用户等。用户信息列表展示区域、角色、名称、状态、操作(删除、编辑)。超级管理员可以添加、删除、编辑所有用户;区域管理员尽可以新增、删除、编辑本用户及本区域下的区域编辑用户;区域编辑没有本页面权限。
![image](https://img2022.cnblogs.com/blog/2506612/202203/2506612-20220309224108134-1351491412.gif)
3.4 权限管理
权限管理包含两个页面:角色列表;权限列表。
角色列表展示角色ID、角色名称、角色操作(删除角色、编辑角色权限)。
权限列表展示页面ID、权限名称、权限路径、操作(删除路径、路径配置状态)。
实现效果:
![image](https://img2022.cnblogs.com/blog/2506612/202203/2506612-20220309224236817-1170936573.gif)
3.5 新闻管理
新闻管理包含三个页面:撰写新闻、草稿箱、新闻分类。
撰写新闻包括:新闻标题、新闻分类(下拉选择)、新闻内容、新闻提交。其中新闻提交要包括保存草稿箱及提交审核两个操作。
点击保存草稿箱,跳转至草稿箱页面,并在右下侧通知用户相关消息,草稿箱页面显示新闻ID、新闻标题(新闻标题可点击预览新闻内容)、作者、分类、操作(删除、修改、提交审核)。
点击提交审核,将跳转至审核管理-审核列表,并在右下侧通知用户相关消息。
新闻分类页面展示分类ID、分类名称(可修改)、操作(删除)。
实现效果:
![image](https://img2022.cnblogs.com/blog/2506612/202203/2506612-20220309224242599-1152084992.gif)
3.6 审核管理
审核管理包括两个页面:审核新闻、审核列表。
审核新闻页面展示待审核的新闻项,内容有:新闻标题、作者、分类、操作(通过、驳回)。点击通过或驳回在右下侧通知用户相关消息。
审核列表展示本用户在审核阶段的新闻,内容有:新闻标题、作者、分类、审核状态、操作。若审核状态为未通过、操作为更新;若审核状态为已通过、操作为发布。点击更新可编辑新闻内容(类似撰写新闻的页面);点击发布则跳转至已发布页面,并在右下侧通知用户相关消息。
实现效果:
![image](https://img2022.cnblogs.com/blog/2506612/202203/2506612-20220309224247546-372003870.gif)
3.7 发布管理
发布管理包括三个页面:待发布、已发布、已下线。
待发布页面展示本用户审核通过仍未发布的新闻,内容有新闻标题、作者、分类、操作(发布)。
已发布页面展示本用户已发布的新闻,内容有新闻标题、作者、分类、操作(下线)。
已下线页面展示本用户已下线的新闻,内容有新闻标题、作者、分类、操作(删除)。
实现效果:
![image](https://img2022.cnblogs.com/blog/2506612/202203/2506612-20220309224252177-1973604510.gif)
四、项目规范
文件夹、文件名称统一小写
JS组件采用大驼峰命名,比那辆采用小驼峰命名
使用hooks编写
丰富注释
rudux:每个模块有自己独立的reducer,通过combineReducer进行合并
五、技术文档
接口:使用JSON Server部署本地数据接口(http://localhost:5000)
功能
接口地址
调用案例
用户
/users
获取用户及其角色权限/users?_expand=role
角色权限
/roles
子菜单
/children
父菜单
/rights
取父子菜单/rights?_embed=children
新闻分类
/categories
区域
/regions
新闻
/news
获取对应新闻内容、分类及作者权限/news/${id}?_expand=category&_expand=role// 审核状态、发布状态映射(数组id即为状态码)const auditList = ["未审核", '审核中', '已通过', '未通过']const publishList = ["未发布", '待发布', '已上线', '已下线']
路由架构
// V6实例
import React from 'react'
import {
HashRouter as Router,
Routes,
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")?:}
*/}
)
}
简单数据处理:使用lodash进行简单数据处理
renderBarView(_.groupBy(res.data, item => item.category.title))
顶栏控制侧边栏伸缩
// 顶栏组件
const mapStateToProps = ({CollapsedReducer: {isCollapsed}})=>{
return {
isCollapsed
}
}
const mapDispatchToProps = {
changeCollapsed(){
return {
type:"change_collapsed"
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(TopHeader)
// 侧边栏组件
// 侧边栏伸缩,使用connect
const mapStateToProps = ({CollapsedReducer: {isCollapsed}})=>({isCollapsed})
export default connect(mapStateToProps)(SideMenu)
实现效果:
![image](https://img2022.cnblogs.com/blog/2506612/202203/2506612-20220309224306389-1617259752.gif)
数据加载Loading,状态持久化
// Redux store设置,使用黑名单避免&实现持久化
import {createStore,combineReducers} from 'redux'
import {CollapsedReducer} from './reducers/CollapsedReducer'
import {LoadingReducer} from './reducers/LoadingReducer'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
const persistConfig = {
key: 'hangyi',
storage,
blacklist: ['LoadingReducer']
}
const reducer = combineReducers({
CollapsedReducer,
LoadingReducer
})
const persistedReducer = persistReducer(persistConfig, reducer)
const store = createStore(persistedReducer);
const persistor = persistStore(store)
export {
store,
persistor
}
// Reducer设置
export const CollapsedReducer = (prevState={
isCollapsed:false
},action)=>{
let {type} =action
switch(type){
case "change_collapsed":
let newstate = {...prevState}
newstate.isCollapsed = !newstate.isCollapsed
return newstate
default:
return prevState
}
}
JSON Server方法
//取数据 get
// axios.get("http://localhost:8000/posts/2").then(res=>{
// console.log(res.data)
// })
// 增 post
// axios.post("http://localhost:8000/posts",{
// title:"33333",
// author:"xiaoming"
// })
// 更新 put
// axios.put("http://localhost:8000/posts/1",{
// title:"1111-修改"
// })
// 更新 patch
// axios.patch("http://localhost:8000/posts/1",{
// title:"1111-修改-11111"
// })
// 删除 delete
// axios.delete("http://localhost:8000/posts/1")
// _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 {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]];
// 侧边栏内容列表
const renderMenu = (menuList) => {
return menuList.map(item => {
// 检查每一项是否有下级列表(使用可选链语法)&& 页面权限
if (item.children?.length > 0 && checkPagePermission(item)) {
return
{renderMenu(item.children)}
}
return checkPagePermission(item) &&
navigate(item.key)
}>{item.title}
})
}
多级用户管理:在添加用户、编辑用户时,超级管理员可以随意添加、区域管理员可以添加编辑本人及本区域下的区域编辑。
// 代码节选
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]);
// 控制区域、角色的新增&编辑权限
// 父组件传递 regionList={regionList} roleList={roleList}等参数
// 子组件props.regionList
{props.regionList.map(item => {
return {item.title}
})}
可编辑单元格
// 参考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};
};
···
item.id}
components={{
body: {
row: EditableRow,
cell: EditableCell,
}
}}
/>
六、疑难巧点
6.1 react routerV6新版本
Routes代替Switch
element代替component:
// V6
element={}
// history
component={Login}
Navigate干掉了Redirect:
// V6
// history
localStorage.getItem("token")?:}/>
useNavigate, useLocation等代替withRouter,props.history.push等的方法
// V6
const location = useLocation();
const selectedkeys = location.pathname;
const openkeys = ["/" + location.pathname.split("/")[1]];
// history
const selectKeys = [props.location.pathname]
const openKeys = ["/"+props.location.pathname.split("/")[1]]
// 导航方法
// V6
const navigate = useNavigate();
navigate(item.key)
// history
props.history.push(item.key)
6.2 axios拦截
可以实现:
简化每次axios请求的代码量
实现加载数据时有提示
import axios from 'axios'
import {store} from '../redux/store'
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
store.dispatch({
type:"change_loading",
payload:true
})
return config;
}, function (error) {
// Do something with request error
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);
});
七、待补充
性能优化:useMemo、useCallback和memo等
redux hooks
八、附录
8.1 命令表
// 创建react-app
npx create-react-app my-app
// 进入该目录
cd my-app
// 启动工程
npm start
// npm安装相关依赖(例如antd)
npm i --save antd
// JSON Server启动(使用db.json文件,本地5000端口)
json-server --watch db.json --port 5000
本文来自博客园,作者:沧浪浊兮,转载请注明原文链接:https://www.cnblogs.com/shixiu/p/15983351.html
|