React--》从零开始搭建一个文章后台管理系统_react后台管理系统

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;
}
}


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


![](https://img-blog.csdnimg.cn/2272e255983d4b2f8db5ab0d5d4867cd.png)


#### 创建表单结构


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


![](https://img-blog.csdnimg.cn/a2f64225a2404ec3a35176a6d2bb643d.png)


根据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 (



图片
{/* 登录表单 */}
<Form validateTrigger={[‘onBlur’,‘onChange’]} initialValues={{remember: true}}>
<Item name=‘phone’ rules={[
{ required:true,message:‘请输入手机号’ },
{ pattern:/^1[3-9]\d{9}$/, // 设置正则匹配规则
validateTrigger:‘onBlur’, // 设置触发时机失去焦点时触发
message:‘请输入正确的手机号格式’ }
]}>


<Item name=‘password’ rules={[
{ required:true,message:‘请输入密码’ },
{ len:6, // 设置密码长度为6位数
validateTrigger:‘onBlur’,
message:‘请输入6位密码’ }
]}>




我已经阅读并同意 [用户协议] 和 [隐私条款]



登录




)
}

export default Login


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


![](https://img-blog.csdnimg.cn/41db13b0654d4443bc6f02284124db67.png)


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


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


**安装发送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
}


![](https://img-blog.csdnimg.cn/7c28c0644f45437aabef95e504ee9448.png)


**安装集中式状态管理工具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之后,便进行编程式路由导航,进行页面的跳转,如下:


![](https://img-blog.csdnimg.cn/72c199a9e9ea4ee6ad705014c928237e.png)


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


![](https://img-blog.csdnimg.cn/cf630207fc4343c0a6fd333cd9fc415a.gif)


#### 对登录模块的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状态管理工具里面即可,如下:


![](https://img-blog.csdnimg.cn/f94c0ce3bac541fbb1785f4074bc2dfb.png)


![](https://img-blog.csdnimg.cn/1aed553ba9424dc2adbb69379d95132b.gif)


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


![](https://img-blog.csdnimg.cn/6b713921368a469daee2e6148c308788.png)


#### 路由鉴权实现


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



> 
> **实现思路**:自己封装一个路由鉴权的高阶组件,实现未登录拦截,并跳转到登录页面。判断本地是否有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
}
}
export default AuthComponent


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


![](https://img-blog.csdnimg.cn/a4c84854a50e445d915d46be5d773101.png)


![](https://img-blog.csdnimg.cn/50cdb3a6e9914260b4a09778a0918c17.gif)


### 后台页面模块


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


#### 基本页面结构搭建


![](https://img-blog.csdnimg.cn/89b404a7818541db8c8ea69ad84a9746.png)


![](https://img-blog.csdnimg.cn/c3d88ecd85cb4c5689ce6b5fc5ac8ab2.png)


整出代码如下:



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 (



<Menu
theme=“dark”
mode=“inline”
defaultSelectedKeys={[‘1’]}
items={items}
/>


<Header style={{ padding: 0,background: colorBgContainer }}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: ‘trigger’,
onClick: () => setCollapsed(!collapsed),
})}
{/*
/}
username

<Popconfirm
// onConfirm={onConfirm}
title=“是否确认退出?” okText=“退出” cancelText=“取消”>
退出


{/
*/}

<Content
style={{
margin: ‘24px 16px 0px’,
padding: 24,
minHeight: 280,
background: colorBgContainer,
overflowY: ‘auto’
}}
>


<Footer
style={{
textAlign: ‘center’,
}}
>
Ant Design ©2023 Created by Ant UED



);
};
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(数据概览, ‘1’, ),
getItem(内容管理, ‘2’, ),
getItem(发布文章, ‘3’, ),

];

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);
}


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


![](https://img-blog.csdnimg.cn/706177f0485844428f99e3e9263743aa.png)


呈现的结果如下:


![](https://img-blog.csdnimg.cn/73ec84f38e5945a99c96005a930e591a.gif)


#### 菜单高亮显示


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



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


![](https://img-blog.csdnimg.cn/7f44e009e38246419474d87975e9ae72.png)


![](https://img-blog.csdnimg.cn/4dca5a49b9254a8ba80d33711da7525c.png)


![](https://img-blog.csdnimg.cn/a9c657f622664a799cc8c8989982e3d9.png)


![](https://img-blog.csdnimg.cn/99db67f9d56f45e3a5c437c9e42c85ef.gif)


#### 展示个人信息


接下来实现后台页面右上角的用户名信息的展示,这里需要借助状态管理工具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


![](https://img-blog.csdnimg.cn/5c414ed3f5d2449082060253f76197d1.png)


![](https://img-blog.csdnimg.cn/5f9ffcb57e7648ab81aa37937ffa2eba.png)


![](https://img-blog.csdnimg.cn/c7c10d9cc9af4b89902cdbecb13f630b.png)


#### 退出登录实现


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


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


![](https://img-blog.csdnimg.cn/a6fdb61bf36b46598d7bdaf26b141b27.png)


![](https://img-blog.csdnimg.cn/2884bcda5f9a47d5bf84810a61c52fbc.png)


![](https://img-blog.csdnimg.cn/c77dd397f5ff47c4a3cf3068c1d843a6.gif)


#### Token失效处理


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


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


![](https://img-blog.csdnimg.cn/66b353df4cd149898fa7d6899bf8e0df.png)


![](https://img-blog.csdnimg.cn/ed249926cb9946c4b1dea433d816f7cf.gif)


#### 首页Home页面展示


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


![](https://img-blog.csdnimg.cn/34e5b264004a4bdba1769f2d00fd9820.png)


引入之后,在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 (


{/* 准备一个挂载节点 */}


)
}
export default Bar


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



import React from ‘react’
import Bar from ‘…/…/components/Bar’
import ‘./index.scss’

const Home = () => {
return (


{/* 渲染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’ }} />

)
}

export default Home


设置样式如下:



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


最后的界面如下:


![](https://img-blog.csdnimg.cn/ecaea0600ce444238ebee82462b9bf84.png)


#### 内容管理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 (

<Button
type=“primary”
shape=“circle”
icon={}
onClick={() => goPublish(data)}
/>
<Button
type=“primary”
danger
shape=“circle”
icon={}
onClick={() => delArticle(data)}
/>

)
},
fixed: ‘right’
}
]

return (


{/* 筛选区域 */}
<Card
title={
<Breadcrumb separator=“>” items={[
{title:首页},
{title:‘内容管理’}
]} />
}
style={{ marginBottom: 20 }}
>
<Form
onFinish={onFinish}
initialValues={{ status: ‘’ }}>
<Form.Item label=“状态” name=“status”>
<Radio.Group>
全部
草稿
待审核
审核通过
审核失败
</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)


实现的界面如下:


![](https://img-blog.csdnimg.cn/e50a57240d124a6fa6cd23322c0b573a.png)


#### 发布文章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(() => {
const loadDetail = async () => {
const res = await http.get(/mp/articles/${id})
const data = res.data
// 表单数据回填
form.current.setFieldsValue({ …data, type: data.cover.type })
// 回填upload
const formatImgList = data.cover.images.map(url => ({ url }))
setFileList(formatImgList)
// 暂存列表里也存一份
cacheImgList.current = formatImgList
// 图片type
setImgCount(data.cover.type)
}
// 必须是编辑状态 才可以发送请求
if (id) {
loadDetail()
}
}, [id, form])

return (


<Card
title={
<Breadcrumb separator=“>” items={[
{title:首页},
{title: ${id ? '编辑' : '发布'}文章}
]} />
}
>
<Form ref={form} labelCol={{ span: 4 }} wrapperCol={{ span: 16 }} initialValues={{ type: 1, content: ‘’}} onFinish={onFinish}>
<Item label=“标题” name=“title” rules={[{ required: true, message: ‘请输入文章标题’ }]}>
<Input placeholder=“请输入文章标题” style={{ width: 400 }} />

<Item label=“频道” name=“channel_id” rules={[{ required: true, message: ‘请选择文章频道’ }]}>
<Select placeholder=“请选择文章频道” style={{ width: 400 }}>
{ChannelStore.channelList.map(channel => {channel.name})}




<Radio.Group onChange={radioChange}>
单图
三图
无图
</Radio.Group>

{ imgCount > 0 && (
1 } maxCount={ imgCount } >



)
}

      </Item>
      {/* 这里的富文本组件 已经被Item控制 */}
      {/* 它的输入内容 会在onFinished回调中收集起来 */}
      <Item label="内容" name="content" rules={[{ required: true, message: '请输入文章内容' }]}>
        <ReactQuill theme="snow" />
      </Item>
      <Item wrapperCol={{ offset: 4 }}>
        <Space>
          <Button size="large" type="primary" htmlType="submit">
            { id ? '更新' : '发布' }文章
          </Button>
        </Space>
      </Item>
    </Form>
  </Card>
</div>

)
}

export default observer(Publish)


给出样式如下:



.publish {
position: relative;
.ql-container {
height: 400px !important;
}
}

.ant-upload-list {
.ant-upload-list-picture-card-container,
.ant-upload-select {
width: 146px;
height: 146px;
}
}


文章管理与发布文章的联动这里就不在赘述,可以看一下上面的组件代码,画面如下:


![](https://img-blog.csdnimg.cn/62e6fd24982744fb870218e675146b8d.gif)


#### 项目的相关优化


**路由懒加载**:能够对路由进行懒加载实现代码分隔,因为React是单页面应用,webpake打包后文件会很大所以启动的时候打开首页资源过多可能会出现路由懒加载,接下来讲解路由懒加载的使用。具体过程如下:


对router进行路由懒加载,如下:



import { lazy } from “react”;
import { Navigate } from “react-router-dom”;
import Layout from “…/pages/Layout”;
import Login from “…/pages/Login”;
import AuthComponent from “…/components/authComponent”;
const Home = lazy(()=>import(‘…/pages/Home’))
const Article = lazy(()=>import(‘…/pages/Article’))
const Publish = lazy(()=>import(‘…/pages/Publish’))

const routes = [
{ path:‘/login’,element: },
{ path:‘/layout’,element:


,
children:[
{ path:‘/layout/home’,element: },
{ path:‘/layout/article’,element:

},
{ path:‘/layout/publish’,element: }
]
},
{ path:‘/’,element: },
]

export default routes


配置完路由表之后,将要进行路由懒加载的展示区进行组件包裹,并以组件作为回调:


![](https://img-blog.csdnimg.cn/5f90b26f603f478cb44d9687105f1482.png)


给Loading组件添加相关的样式,给页面添加好看的loading组件


![](https://img-blog.csdnimg.cn/6af9eaff424c45dd96544c03f08577e2.png)


![](https://img-blog.csdnimg.cn/34e3230b556e47c59b4c6088e839d865.gif)


**配置全局样式**:在 5.0 版本的 Ant Design 中,提供了一套全新的定制主题方案。不同于 4.x 版本的 less 和 CSS 变量,有了 CSS-in-JS 的加持后,动态主题的能力也得到了加强。具体操作如下:


![](https://img-blog.csdnimg.cn/65027905650944f2b98afa19a2cfe0ec.png)


![](https://img-blog.csdnimg.cn/3254bf00b2b344468dce782111fece8a.png)


![](https://img-blog.csdnimg.cn/d2f5f240fa2d4c60af5c02c1a11a8d8f.png)


### 项目git上线


每次书写项目时,完成一个功能点为了避免后期编写代码产生错误,需要将代码上传到远程仓库上进行一个版本的保存,这样便避免了代码遗失或写错而产生的错误,以 Github 为例,具体操作步骤如下:


**创建远程仓库**:进入github网址,登录自己的账号后点击右上角头像新建仓库,如下


![](https://img-blog.csdnimg.cn/a1f37a8eb6ae42dba6a813dbb8e1c3a3.png)


![](https://img-blog.csdnimg.cn/809272757d824511ae8288e652305fcb.png)


创建完成后,会跳转到如下界面,上面代码详细的告诉你如何将代码提交到远程库:


![](https://img-blog.csdnimg.cn/9f7b69b3f5184b00ae473efeda88d1d5.png)


提交代码需要借用一个 git 工具,如何下载这里不再赘述,自行百度。如果想详细的了解git的使用,可以参考一下我之前的专栏:[git专栏]( ) ,详细操作如下:


**生成本地仓库**:具体步骤如下:



> 
> git init 生成工作区
> 
> 
> git add . 提交到暂存区
> 
> 
> git commit -m "article" 提交到版本区
> 
> 
> 


![](https://img-blog.csdnimg.cn/cd686ebecbad4c359c7c782f01e205bc.png)


**推送到远程仓库**:具体步骤如下:



> 
> git remote add origin git@github.com:ztK63LrD/article-pc.git  关联别名
> 
> 
> git push -u origin "master" 推送分支
> 
> 
> 


![](https://img-blog.csdnimg.cn/dd475e694283411cb44490d9d1ca8cd8.png)


推送完成之后,刷新github页面就会将我们推送的代码呈现在页面上,如下:


![](https://img-blog.csdnimg.cn/9ea04059d71a438a80fb6163031dd031.png)


**创建新的分支**:通常我们在创建完master分支后,并不会直接将所有的代码都整合到该分支上,而是再建立起别的分支,在别的分支上推送代码,当确保代码完整性后再与主分支master进行合并,这样会大大提高代码的正确性和规范性。其具体步骤如下:



> 
> git checkout -b dev 创建并切换到该分支
> 
> 
> git push origin -u "dev" 将该分支推送到远程仓库上
> 
> 
> 


![](https://img-blog.csdnimg.cn/a98f123b2aee42c3a02975beca84ac52.png)


![](https://img-blog.csdnimg.cn/94bc73ca2b21402aa2bf004bedf3bb38.png)


**将dev分支的代码推送到远程仓库**: 因为你使用的不是主分支master,当前日常开发中,解决了一些问题之后,想将其推送到远程仓库做一个保存,需要进行如下操作(和之前操作一致),这里可以直接在编辑器的终端进行操作:


![](https://img-blog.csdnimg.cn/8ee5b322fdcb47c9824f1768cf2f2db2.png)


推送完成之后,远程仓库会有提示,提示你当前分支出现了新的推送:


![](https://img-blog.csdnimg.cn/d2c83068e4e846b4bbdc03a1c4406df8.png)


可以点击当前推送的dev分支,点击如下选项可以查看推送后的代码与之前的代码有何区别:


![](https://img-blog.csdnimg.cn/311fbb79afb24d4495e98393fa35a6d1.png)


![](https://img-blog.csdnimg.cn/f6def3f6556c45e5be3df70a241d094d.png)


**dev分支与主分支master的合并**:当确定dev分支的代码没毛病后,就可以推送到主分支master上面,具体操作如下:


![](https://img-blog.csdnimg.cn/0c03d831830946bf86f52bc5da20b713.png)


进入当前页面后,可以写一些你为啥要修改的描述,也可以不写,直接点击创建即可:


![](https://img-blog.csdnimg.cn/dd5e68a8547544be87b8835952e2a3f1.png)


点击如下按钮进行接收合并的请求:


![](https://img-blog.csdnimg.cn/e9d33e02b37e4170b9b8da105281b1a5.png)


合并之后,两个master就具有了dev分支的内容,当前页面也没有任何的提示消息了:


![](https://img-blog.csdnimg.cn/992e394e54414ad8b4bdaf326ecbbfd7.png)


**修改本地的master分支**:修改了远程库的master分支后,本地的master并没有改变,还是之前的版本,所以我们还需要将远程更新过后的master分支进行一个本地的拉取,如下:



> 
>  git checkout master 切换到master分支
> 
> 
> git pull origin master 将远程的master分支进行一个拉取
> 
> 
> 


![](https://img-blog.csdnimg.cn/915fd12fdb65400298e19c5515735353.png)


### 项目打包


项目完成之后,对项目进行打包然后交给后端服务器进行项目上线即可,下面举个例子:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值