首页IT科技react新建项目(React–》从零开始搭建一个文章后台管理系统)

react新建项目(React–》从零开始搭建一个文章后台管理系统)

时间2025-05-06 04:16:01分类IT科技浏览3574
导读:目录...

目录

项目准备

项目搭建

scss预处理器的使用

配置基础路由

组件库antd的使用

开发者工具的安装

登录模块

基本样式搭建

创建表单结构

获取表单数据并进行相关登录操作

对登录模块的token进行相关处理

路由鉴权实现

后台页面模块

基本页面结构搭建

菜单高亮显示

展示个人信息

退出登录实现

Token失效处理

首页Home页面展示

内容管理Article页面展示

发布文章Publish页面展示

项目的相关优化

项目git上线

项目打包

项目准备

本篇文章讲解的是一个简单的文章后台管理系统            ,系统的功能很简单                ,如下:

登录          、退出;首页;内容(文章)管理:文章列表                  、发布文章      、修改文章          。

看完本篇文章你将了解学习到的知识如下:

React官方脚手架:create-react-app

react-hooks

状态管理:mobx

UI组件库:antd v5

ajax请求库:axios

路由:react-router-dom 以及 history

富文本编辑器:react-quill

CSS预编译器:sass

项目搭建

本系统是基于react官方脚手架搭建      ,具体的详细搭建         ,参考文章:React脚手架的搭建与使用                   。

使用如下命令生成项目:

npx create-react-app article-pc

将生成的文件拖到vscode编辑器                ,删除一些不必要的文件        ,然后终端执行 npm start 即可      ,如下:

scss预处理器的使用

SASS是一种预编译的CSS                 ,作用类似于Less      。由于React中内置了处理SASS的配置          ,所有在CRA创建的项目中可以直接使用SASS来写样式   ,实现如下:

安装解析sass的第三方包:

npm install sass

创建全局样式文件并引入:

配置基础路由

前端路由是能够实现页面跳转的导航按钮                  ,在前端领域中             ,路由是必不可少要掌握的技能之一,详情了解的话可以参加我之前的文章:ReactRouter5讲解                ,ReactRouter6讲解        。其实现步骤如下:

安装路由:

npm install react-router-dom

实现过程如下:

在src目录下创建router文件夹并创建index.jsx文件                ,里面存放着配置路由的路由表   ,如下:

打开入口文件进行如下操作:

打开App根组件            ,将路由内容进行呈现                ,如下:

组件库antd的使用

antd是一个经常被react使用的一个组件库      ,大大提高了前端程序员的编码效率         ,详细了解的可以参考一下我之前的文章:UI组件库ant-design的介绍与使用                 ,使用步骤如下:

安装 antd 组件库:

npm install antd

安装完成之后        ,访问 antd官网       ,随机点击一个功能                 ,例如一个按钮的功能          ,如下:

可见如下的效果   ,引入的没毛病

开发者工具的安装

如果你是第一次接触React开发或者说还没有安装react的开发者工具建议还是安装一下                  ,如果不能打开谷歌网上商店的uu             ,推荐国内安装插件的一个网站:极简插件                  。如下:

下载解压,将文件拖到你的浏览器安装插件的界面即可               ,如下以谷歌浏览器为例:

登录模块

接下来开始实现登录页功能的实现                ,具体步骤分为以下几个方面:

基本样式搭建

登录页面需要一个简单的背景图片   ,有需要的还可以自行添加一个logo图片            ,如下:

import React from react import { Card } from antd import logo from ../../assets/logo.jpg import ./index.scss const Login = () => { return ( <div className=login> <Card className=login-container> <img src={logo} alt="图片" className=login-logo /> {/* 登录表单 */} </Card> </div> ) } export default Login .login{ width: 100%; height: 100%; position: absolute; left: 0; top: 0; background: center/cover url(../../assets/login.jpg); .login-container{ width: 600px; height: 400px; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); box-shadow: 0 0 50px rgb(0 0 0 /10%); } .login-logo{ width: 200px; height: 100px; display: flex; margin: 0 auto 20px; } .login-checkbox-label{ color: #1890ff; } }

图片可以自行百度寻找满意的图片                ,如下是个人简单的实现效果:

创建表单结构

表单的登录解构可以参考antd的Form表单对登录框的书写方式      ,如下:

根据antd给出的登录框的书写样式         ,结合自身需求                ,给出如下代码:

import React from react import { Card,Form,Input,Checkbox,Button } from antd import logo from ../../assets/logo.jpg import ./index.scss // Form.Item的简写形式 const Item = Form.Item const Login = () => { return ( <div className=login> <Card className=login-container> <img src={logo} alt="图片" className=login-logo /> {/* 登录表单 */} <Form validateTrigger={[onBlur,onChange]} initialValues={{remember: true}}> <Item name=phone rules={[ { required:true,message:请输入手机号 }, { pattern:/^1[3-9]\d{9}$/, // 设置正则匹配规则 validateTrigger:onBlur, // 设置触发时机失去焦点时触发 message:请输入正确的手机号格式 } ]}> <Input size=large placeholder=请输入手机号 /> </Item> <Item name=password rules={[ { required:true,message:请输入密码 }, { len:6, // 设置密码长度为6位数 validateTrigger:onBlur, message:请输入6位密码 } ]}> <Input size=large placeholder=请输入密码 /> </Item> <Item name="remember" valuePropName=checked > <Checkbox className=login-checkbox-label> 我已经阅读并同意 [用户协议] 和 [隐私条款] </Checkbox> </Item> <Item> <Button type=primary htmlType=submit size=large block>登录</Button> </Item> </Form> </Card> </div> ) } export default Login

具体的校验规则可在文章末尾给出的源码地址        ,自行下载探索:

获取表单数据并进行相关登录操作

在开始之前先安装好项目要准备的第三方库      ,如下:

安装发送ajax请求的第三方库

npm install axios

安装好发送ajax请求的第三方库之后                 ,在src目录下新建utils文件夹          ,里面存放着项目的所有工具函数   ,当然发送ajax的工具函数也会放置在里面                  ,命名为 http.jsx             ,如下:

// 封装axios import axios from "axios"; const http = axios.create({ baseURL:"http://geek.itheima.net/v1_0", timeout:5000 // 超时时间定下5秒钟 }) // 添加请求拦截器 http.interceptors.request.use((config)=>{ return config },(error)=>{ return Promise.reject(error) }) // 添加响应拦截器 http.interceptors.response.use((response)=>{ // 2xx 范围内的状态码都会触发该函数 return response.data },(error)=>{ // 超出 2xx 范围内的状态码都会触发该函数 return Promise.reject(error) }) export default http

定义好发送ajax请求的工具函数后,在当前utils文件夹下新建一个index.jsx文件               ,用来封装整合所有的工具函数                ,以后所有书写的工具函数都会存放到这   ,便于调用:

// 先把所有的工具函数导出的模块在这里导入            ,整合在一起再统一导出 import http from ./http.jsx export { http }

安装集中式状态管理工具Mobx

npm install mobx mobx-react-lite

安装好mobx状态管理工具之后                ,在src目录下新建一个store文件夹用来处理所有要使用的状态      ,如下将要使用的登录的token存放到状态中         ,并命名为 login.jsx文件                ,如下:

// login module import { makeAutoObservable } from mobx import { http } from ../utils class LoginStore { token = constructor(){ // 响应式 makeAutoObservable(this) } getToken = async({mobile,code}) => { // 调用登录接口 const res = await http.post(http://geek.itheima.net/v1_0/authorizations,{mobile,code}) // 存入token this.token = res.data.token } } export default LoginStore

定义好状态文件后        ,还需要在store文件夹下新建一个index.jsx文件      ,用来管理所有的要操作状态的函数和方法                 ,如下:

// 把所有模块进行一个统一的处理          ,导出一个统一的方法 useStore import React from "react"; import LoginStore from "./login"; class RootStore { constructor(){ this.LoginStore = new LoginStore() } } // 实例化根 导出useStore context const rootStore = new RootStore() const context = React.createContext(rootStore) const useStore = () => React.useContext(context) export default useStore

登录的接口和状态都配置完成之后   ,便开始需要在登录页面调用要获取接口的getToken函数                  ,得到自己的token之后             ,便进行编程式路由导航,进行页面的跳转               ,如下:

因为调用的接口是固定死的                ,密码必须是这个   ,用户名可以随便输入:

对登录模块的token进行相关处理

token对于登录模块而言至关重要            ,它保证着你登录后能够坚持登录后数据的时间                ,以及不同的token登录会获取不同的数据的一个身份凭证      ,所有在设计登录模块的时候         ,通常会进行如下操作:

token持久化:因为设置token持久化的也是一个工具函数                ,所以还是需要封装在utils文件夹下面并设置相关操作token的函数        ,如下:

// 定义操作token的函数 const key = pc-key // 存token const setToken = (token) =>{ return window.localStorage.setItem(key,token) } // 取token const getToken = ()=>{ return window.localStorage.getItem(key) } // 删token const removeToken = ()=>{ return window.localStorage.removeItem(key) } export { setToken, getToken, removeToken }

 将封装好的token工具函数也存放到当前文件夹下的index.jsx文件夹下:

// 先把所有的工具函数导出的模块在这里导入      ,整合在一起再统一导出 import http from ./http.jsx import{ setToken,getToken,removeToken} from ./token.jsx export { http, setToken, getToken, removeToken }

接下来需要将设置好的操作token的函数导入到store状态管理工具里面即可                 ,如下:

请求拦截器注入token: 在每次接口正式发送之前进行拦截          ,将获取到的token进行装入   ,凡是调用了自己设计的接口请求                  ,就会自动拥有token             ,不需要每次发送请求时都去请求一遍token接口函数,起到了一处配置多处生效的效果         。如下:

路由鉴权实现

先解释一下什么是路由鉴权               ,假设你知道登录后台主页的访问路径                ,在没有登录的情况下   ,你能直接访问后台主页的路径吗?答案是肯定的 (在没有设置路由鉴权的情况下)             ,所以后台设置路由鉴权极为重要     。具体过程如下:

实现思路:自己封装一个路由鉴权的高阶组件                ,实现未登录拦截      ,并跳转到登录页面                。判断本地是否有token         ,如果有就返回登录之后的子组件                ,否则就重定向到登录的Login组件            。

在component文件夹下新建authComponent文件        ,用来对登录页面进行鉴权      ,如果本地没有token值就强制跳转到登录页面                 ,如下:

// 判断token是否存在          ,如果存在正常渲染   ,如果不存在重定向到登录路由 import { getToken } from "../utils"; import { Navigate } from "react-router-dom"; const AuthComponent = ({children}) =>{ const isToken = getToken() if(isToken){ return <>{children}</> }else{ return <Navigate to=/login replace /> } } export default AuthComponent

设置好函数之后路由表对其进行判断                  ,如下:

后台页面模块

接下来进行后台页面的搭建             ,这里也可以借助antd的Layout布局和Menu导航菜单   。

基本页面结构搭建

整出代码如下:

import React, { useState } from react; import { Outlet } from react-router-dom import { Layout, Menu, theme,Popconfirm } from antd; import { MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined } from @ant-design/icons; import items from ../../config/index.jsx import ./index.scss const { Header, Sider, Content,Footer } = Layout; const App = () => { const [collapsed, setCollapsed] = useState(false); const { token: { colorBgContainer }, } = theme.useToken(); return ( <Layout> <Sider trigger={null} collapsible collapsed={collapsed} className=sider> <div className="logo" /> <Menu theme="dark" mode="inline" defaultSelectedKeys={[1]} items={items} /> </Sider> <Layout className="site-layout"> <Header style={{ padding: 0,background: colorBgContainer }}> {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: trigger, onClick: () => setCollapsed(!collapsed), })} {/* <div className="user-info"> */} <span className="user-name">username</span> <span className="user-logout"> <Popconfirm // onConfirm={onConfirm} title="是否确认退出?" okText="退出" cancelText="取消"> <LogoutOutlined /> 退出 </Popconfirm> </span> {/* </div> */} </Header> <Content style={{ margin: 24px 16px 0px, padding: 24, minHeight: 280, background: colorBgContainer, overflowY: auto }} > <Outlet /> </Content> <Footer style={{ textAlign: center, }} > Ant Design ©2023 Created by Ant UED </Footer> </Layout> </Layout> ); }; export default App;

将items文件单独抽离出来如下代码:

import { Link } from react-router-dom import { HomeOutlined,DiffOutlined,EditOutlined } from @ant-design/icons; function getItem(label, key, icon, children, type) { return { key, icon, children, label, type, }; } const items = [ getItem(<Link to=/layout/home>数据概览</Link>, 1, <HomeOutlined />), getItem(<Link to=/layout/article>内容管理</Link>, 2, <DiffOutlined />), getItem(<Link to=/layout/publish>发布文章</Link>, 3, <EditOutlined />), ]; export default items

给出的代码样式为:

.ant-layout { height: 100%; } .ant-layout-sider{ flex: 0 0 235px !important; max-width: 300px !important; } .sider { padding: 0; } .logo { width: 225px; height: 60px; background: url(../../assets/logo.jpg) no-repeat center / 160px auto; margin: 10px auto 10px; } .ant-layout-header svg{ font-size: 15px; margin-left: 15px; } .user-name { position: absolute; right: 5%; margin-right: 10px; margin-left: 22px; } .user-logout { position: absolute; right: 2%; display: inline-block; cursor: pointer; } #components-layout-demo-custom-trigger .trigger { padding: 0 24px; font-size: 18px; line-height: 64px; cursor: pointer; transition: color 0.3s; } #components-layout-demo-custom-trigger .trigger:hover { color: #1890ff; } #components-layout-demo-custom-trigger .logo { height: 32px; margin: 16px; background: rgba(255, 255, 255, 0.3); }

在单独为每个导航菜单创建一个单独的组件进行页面的切换显示,如下:

呈现的结果如下:

菜单高亮显示

配置完路由后               ,需要对菜单进行相应的高亮显示                ,在其刷新之后还是处于我们选中的菜单路由   ,点击浏览器的回退按钮会回退到上一个点击的菜单路由            ,如下:

具体思路:使用 useLocation 拿到当前的访问路径                ,根据路径修改key值      ,来选中当前的key来实现菜单路由的高亮         ,如下:

展示个人信息

接下来实现后台页面右上角的用户名信息的展示                ,这里需要借助状态管理工具mobx        ,如下:

// 获取当前的用户名称即手机号 import { makeAutoObservable } from "mobx"; import { http } from "../utils"; class UserStore { userInfo = {} constructor(){ makeAutoObservable(this) } getUserInfo = async() => { // 调用接口数据 const res = await http.get(/user/profile) this.userInfo = res.data } } export default UserStore

将定义好的管理用户名的状态存放到根store里面      ,如下:

// 把所有模块进行一个统一的处理                 ,导出一个统一的方法 useStore import React from "react"; import LoginStore from "./login"; import UserStore from ./username class RootStore { constructor(){ this.LoginStore = new LoginStore() this.UserStore = new UserStore() } } // 实例化根 导出useStore context const rootStore = new RootStore() const context = React.createContext(rootStore) const useStore = () => React.useContext(context) export default useStore

退出登录实现

退出登录需要对token进行删除          ,具体操作如下:

在处理登录的mobx状态文件中   ,新增一个退出的功能:

Token失效处理

在日常开发中我们不能让token一直保持活性                  ,需要给其一定的寿命             ,超过时间token就失活,需要重新登录               ,这样会保证用户一定的安全性                ,而当token发送错误时应该如何操作   ,具体操作如下:

当token发生错误时            ,调用 window 去跳转到登录页面即可                。

首页Home页面展示

首页Home页面采用 echarts 图表封装进行数据显示                ,这里需要借助 exharts官网       ,进行相关操作如下         ,需要先安装 echarts 第三方插件包                ,并使用其第一个案例:

引入之后        ,在components组件中单独创建应该设置图表的组件      ,如下:

// 封装图表bar组件 import * as echarts from echarts import { useEffect, useRef } from react const Bar = ({ title, xData, yData, style }) =>{ const domRef = useRef() const chartInit = () => { // 基于准备好的dom                 ,初始化echarts实例 const myChart = echarts.init(domRef.current) // 绘制图表 myChart.setOption({ title: { text: title }, tooltip: {}, xAxis: { data: xData }, yAxis: {}, series: [ { name: 销量, type: bar, data: yData } ] }) } useEffect(()=>{ chartInit() }) return ( <div> {/* 准备一个挂载节点 */} <div ref={domRef} style={style}></div> </div> ) } export default Bar

在Home组件中导入设置图表的组件Bar          ,如下:

import React from react import Bar from ../../components/Bar import ./index.scss const Home = () => { return ( <div className=home> {/* 渲染Bar组件 */} <Bar title=主流框架满意度 xData={[react, vue, angular]} yData={[30, 40, 50]} style={{ width: 500px, height: 400px }} /> <Bar title=主流框架使用度 xData={[react, vue, angular]} yData={[60, 70, 80]} style={{ width: 500px, height: 400px }} /> </div> ) } export default Home

设置样式如下:

.home { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }

最后的界面如下:

内容管理Article页面展示

内容的article组件需要借助 antd中 Card       、Form                 、DatePicker等功能块   ,给出代码如下:

import { useEffect, useState } from react import { Link, useNavigate } from react-router-dom import { observer } from mobx-react-lite import { Table, Space, Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from antd import { EditOutlined, DeleteOutlined } from @ant-design/icons import locale from antd/es/date-picker/locale/zh_CN import { http } from ../../utils import img404 from ../../assets/error.jpg const { Option } = Select const { RangePicker } = DatePicker const Article = () => { // 路由导航 const navigate = useNavigate() // 频道列表管理 const [channelList,setChannelList] = useState([]) // 文章列表管理 const [articleData,setArticleData] = useState({ list:[], // 文章列表 count:0 // 文章数量 }) // 文章参数管理 const [params,setParams] = useState({ page:1, per_page:10 }) // 获取频道管理的数据 const loadChannelList = async () =>{ const res = await http.get(/channels) setChannelList(res.data.channels) } useEffect(()=>{ loadChannelList() },[]) useEffect(()=>{ // 获取文章列表数据 const loadList = async () =>{ const res = await http.get(/mp/articles,{ params }) const { results,total_count } = res.data setArticleData({ list:results, count:total_count }) } loadList() },[params]) const onFinish = (values) =>{ console.log(values); // 获取表单数据 const { channel_id,date,status } = values // 数据处理 const _params = {} if(status !== -1){ _params.status = status } if(channel_id){ _params.channel_id = channel_id } if(date){ _params.begin_pubdate = date[0].format(YYYY-MM-DD) _params.end_pubdate = date[1].format(YYYY-MM-DD) } // 修改params数据 引起接口的重新发送 对象的合并是一个整体覆盖 改了对象的整体引用 setParams({ ...params, ..._params }) } // 翻页实现 const pageChange = (page) => { setParams({ ...params, page }) } // 删除文章 const delArticle = async (data) => { await http.delete(`/mp/articles/${data.id}`) // 刷新一下列表 setParams({ ...params, page: 1 }) } // 编辑文章 const goPublish = (data) => { navigate(`/layout/publish?id=${data.id}`) } const columns = [ { title: 封面, dataIndex: cover, width: 120, render: cover => { return <img src={cover.images[0] || img404} width={80} height={60} alt="" /> } }, { title: 标题, dataIndex: title, width: 220 }, { title: 状态, dataIndex: status, }, { title: 发布时间, dataIndex: pubdate }, { title: 阅读数, dataIndex: read_count }, { title: 评论数, dataIndex: comment_count }, { title: 点赞数, dataIndex: like_count }, { title: 操作, render: data => { return ( <Space size="middle"> <Button type="primary" shape="circle" icon={<EditOutlined />} onClick={() => goPublish(data)} /> <Button type="primary" danger shape="circle" icon={<DeleteOutlined />} onClick={() => delArticle(data)} /> </Space> ) }, fixed: right } ] return ( <div> {/* 筛选区域 */} <Card title={ <Breadcrumb separator=">" items={[ {title:<Link to="/layout/home">首页</Link>}, {title:内容管理} ]} /> } style={{ marginBottom: 20 }} > <Form onFinish={onFinish} initialValues={{ status: }}> <Form.Item label="状态" name="status"> <Radio.Group> <Radio value=>全部</Radio> <Radio value={0}>草稿</Radio> <Radio value={1}>待审核</Radio> <Radio value={2}>审核通过</Radio> <Radio value={3}>审核失败</Radio> </Radio.Group> </Form.Item> <Form.Item label="频道" name="channel_id"> <Select placeholder="请选择文章频道" style={{ width: 120 }} > {channelList.map(channel => <Option key={channel.id} value={channel.id}>{channel.name}</Option>)} </Select> </Form.Item> <Form.Item label="日期" name="date"> {/* 传入locale属性 控制中文显示*/} <RangePicker locale={locale}></RangePicker> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" style={{ marginLeft: 80 }}> 筛选 </Button> </Form.Item> </Form> </Card> {/* 文章列表区域 */} <Card title={`根据筛选条件共查询到 ${articleData.count} 条结果:`}> <Table rowKey="id" columns={columns} dataSource={articleData.list} pagination={ { pageSize: params.per_page, total: articleData.count, onChange: pageChange, current: params.page } } /> </Card> </div> ) } export default observer(Article)

实现的界面如下:

发布文章Publish页面展示

发布文章这个界面需要使用富文本编辑器                  ,这里需要借助第三方插件库             ,详细的使用教程可以参考一下github上的介绍,网址为:react-quill                ,其安装命令如下:

npm install react-quill

给出如下代码:

import { useEffect, useRef, useState } from react import { Link, useNavigate, useSearchParams } from react-router-dom import { observer } from mobx-react-lite import { Card,Breadcrumb,Form,Button,Radio,Input,Upload,Space,Select,message} from antd import { PlusOutlined } from @ant-design/icons import ./index.scss import ReactQuill from react-quill import react-quill/dist/quill.snow.css import useStore from ../../store import http from ../../utils/http const { Option } = Select const Item = Form.Item const Publish = () => { const navigate = useNavigate() // 获取频道数据 const { ChannelStore } = useStore() // 存放上传图片的列表 const [fileList,setFileList] = useState([]) // 保存的图片数量 const [imgCount,setImgCount] = useState(1) // 声明图片的暂存仓库 const cacheImgList = useRef() // 获取表单数据 const onFinish = async(values) =>{ // 数据的二次处理 重点是处理cover字段 const { channel_id, content, title, type } = values // 判断type fileList 是匹配的才能正常提交 const params = { channel_id, content, title, type, cover: { type: type, images: fileList.map(item => item.url) } } if (id) { await http.put(`/mp/articles/${id}?draft=false`, params) } else { await http.post(/mp/articles?draft=false, params) } // 跳转列表 提示用户 navigate(/layout/article) message.success(`${id ? 更新成功 : 发布成功}`) } const onUploadChange = ({fileList}) =>{ // 这里关键位置:需要做数据格式化 const formatList = fileList.map(file => { // 上传完毕 做数据处理 if (file.response) { return { url: file.response.data.url } } // 否则在上传中时                ,不做处理 return file }) // 存放data数据 setFileList(formatList) // 同时把图片列表存入仓库一份 cacheImgList.current = formatList } // 切换图片 const radioChange = (e) =>{ const rawValue = e.target.value setImgCount(rawValue) console.log(cacheImgList.current); // 从仓库里面获取对应的图片数量 交给用来渲染图片的fileList if(cacheImgList.current === undefined || 0){ return false } if( rawValue === 1 ){ const img = cacheImgList.current ? cacheImgList.current[0] : [] setFileList([img]) }else if ( rawValue === 3 ){ setFileList(cacheImgList.current) } } // 编辑功能 文案适配 路由参数id 判断条件 const [params] = useSearchParams() const id = params.get(id) // 数据回填 id调用接口 1.表单回填 2.暂存列表 3.Upload组件fileList const [form] = Form.useForm() useEffect(() => { %2

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
windows更新后开始菜单打不开(部分Win11升级者反映新系统出现旧的Win10开始菜单)