用React+Antd写一个后台小项目

安装React脚手架不多说了,上一个代码:

npx create-react-app my-app

一.使用SCSS预处理器

  1. 装包:yarn add sass -D 需要安装,react中内置了SASS的配置,所以装包就行,生产环境不需要这个包,加-D;

  1. 创建全局样式文件:index.scss

body{
    margin: 0;
}
#root{
    height: 100%;
}

二.配置基础路由

  1. 安装路由:react-router-dom;

  1. pages下创建layout和login两个文件夹

App.js中代码:

import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './pages/Layout'
import Login from './pages/Login'
export default function App() {
  return (
    //配置路由
    <BrowserRouter>
      <div>
        <Routes>
          {/* 创建路由path和对应组件关系 */}
          <Route path='/' element={<Layout/>}></Route>
          <Route path='/login' element={<Login/>}></Route>
          
      </Routes>
    </div>
    </BrowserRouter>

  )
}

三.antd组件库的使用

npm i antd -S

导入antd样式文件

import 'antd/dist/antd.min.css'

四.配置别名路径(不做了,我的报错)

  1. 用@代替根路径src

  1. .安装修改CRA配置的包:yarn add -D @craco/craco;

  1. 项目根目录下创建文件 craco.config.js

//添加自定义对于webpack的配置
const path = require('path')
module.exports = {
    //webpack配置
    webpack: {
        //配置别名
        alias: {
            //约定:使用@表示src文件所在路径
            '@':path.resolve(__dirname,'src')
        }
    }
}

package.json

//将 start/build/test 三个命令改为craco方式
 "scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "craco test",
    "eject": "react-scripts eject"
  },

五.@别名路径提示

在根目录下创建jsconfig.json配置文件

{
    "compilerOptions":{
        "baseUrl":"./",
        "paths":{
            "@/*":["src/*"]
        }
    }
}

六.登录模块

(1)登录表单: pages/Login/index.js:

import { Card } from 'antd'
import logo from '../../assets/images/logo.png'
import './index.scss'//导入样式文件
export default function Login() {
    return (
        <div className="login">
            <Card className="login-container">
                <img className="login-logo" src={logo} alt="" />
                {/* 登录表单 */}
                
          </Card>
        </div>
    )
}

样式文件index.scss

.login {
    width: 100%;
    height: 100%;
    position: absolute;
    left: 0;
    top: 0;
    background: center/cover url('../../assets/images/login.png');
  
    .login-logo {
      width: 200px;
      height: 60px;
      display: block;
      margin: 0 auto 20px;
    }
  
    .login-container {
      width: 440px;
      height: 360px;
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      box-shadow: 0 0 50px rgba(0, 0, 0, 0.1);
    }
  
    .login-checkbox-label {
      color: #1890ff;
    }
  }
  

(2)创建表单结构+添加校验功能

为Form.Item添加rules属性,用来添加表单校验.

import { Card, Form, Input, Checkbox, Button,message } from 'antd'
import logo from '../../assets/images/logo.png'
import './index.scss'//导入样式文件
export default function Login() {
    return (
        <div className="login">
            <Card className="login-container">
                <img className="login-logo" src={logo} alt="" />
                {/* 登录表单 */}
                {/* 子项用到的触发事件 需要在Form中都声明一下才可以 */}
                <Form
                    validateTrigger={['onBlur', 'onChange']}
                    initialValues={
    
    {
                        remember: true,
                        mobile: '13811111111',
                        code: '246810'
                    }}

                >
                    <Form.Item
                        name="mobile"
                        rules={[
                            {
                                required: true,
                                message: '请输入手机号',
                            },
                            {
                                pattern: /^1[3-9]\d{9}$/,
                                message: '请输入正确的手机号',
                                validateTrigger: 'onBlur'//失焦时触发
                            }
                        ]}
                    >
                        <Input size="large" placeholder="请输入手机号" />
                    </Form.Item>
                    <Form.Item
                        name="code"
                        rules={[
                            {
                                required: true,
                                message: '请输入密码',
                            },
                            {
                                len: 6,
                                message: '请输入6位密码',
                                validateTrigger: 'onBlur'
                            }
                        ]}
                    >
                        <Input size="large" placeholder="请输入验证码" />
                    </Form.Item>
                    <Form.Item
                        name="remember"
                        valuePropName="checked"

                    >
                        <Checkbox className="login-checkbox-label">
                            我已阅读并同意「用户协议」和「隐私条款」
            </Checkbox>
                    </Form.Item>

                    <Form.Item>
                        <Button type="primary" htmlType="submit" size="large" block>
                            登录
            </Button>
                    </Form.Item>
                </Form>

            </Card>
        </div>
    )
}

(3)获取登录表单数据

1.为Form组件添加onFinish属性,该事件会在点击登录按钮时触发;

2.创建onFinish函数,通过函数参数values拿到表单值,

  1. Form组件添加initialValues属性,来初始化表单值.

     function onFinish(values) {
        //values:放置的是所有表单项中用户输入的内容
        console.log(values);
    }
   <Form
                    validateTrigger={['onBlur', 'onChange']}
                    initialValues={
    
    {
                        remember: true,
                        mobile: '13811111111',
                        code: '246810'
                    }}
                    onFinish={onFinish}
                    onFinishFailed={onFinishFailed} //失败时的提示,不常用

                >
                  </Form>

(4)登录axios统一封装处理

npm i axios -S

1.创建utils/http.js文件;

2.创建axios实例,配置baseURL,请求拦截器,响应拦截器;

在utils/http.js中,统一导出http.

// 封装axios
// 实例化  请求拦截器 响应拦截器

import axios from 'axios'
const http = axios.create({
    baseURL: 'http://geek.itheima.net/v1_0',
    timeout: 5000
})
// 添加请求拦截器
http.interceptors.request.use((config) => {
    return config
}, (error) => {
    return Promise.reject(error)
})
// 添加响应拦截器
http.interceptors.response.use((response) => {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response
}, (error) => {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    console.dir(error)

    return Promise.reject(error)
})
export { http }

(5)配置登录Mobx

Mobx类似redux和vuex.

装包:yarn add mobx mobx-react-lite,管理用户登录的store

store/login.Store.js中:(接口应该是有错了,报400错误)

// login 模块
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.js中,管理所有的模块

// 把所有的模块做统一处理
// 导出一个统一的方法 useStore
import React from "react"
import LoginStore from './login.Store'
class RootStore{
    constructor() {
        this.loginStore = new LoginStore()
    }
}


// 实例化根
// 导出useStore context
const rootStore = new RootStore()
const context = React.createContext(rootStore)
const useStore = () => React.useContext(context)

export { useStore }

(6)token持久化

1.封装工具函数

创建utils/token.js

// 封装ls存取token

const key = 'pc-key'

const setToken = (token) => {
  return window.localStorage.setItem(key, token)
}

const getToken = () => {
  return window.localStorage.getItem(key)
}

const removeToken = () => {
  return window.localStorage.removeItem(key)
}

export {
  setToken,
  getToken,
  removeToken
}

在utils/index.js统一封装的工具函数中:

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




export {
  http,
  setToken,
  getToken,
  removeToken
}

在store/login.Store.js中新增下列代码:

import { setToken,getToken} from '../utils'

  token =getToken() || ''//取的时候优先从本地拿token
  setToken(this.token)//往本地存一份

(7)请求拦截器注入token

在utils/http.js中新增下列代码

import {getToken} from './token'//新增
// 添加请求拦截器
http.interceptors.request.use((config) => {
    const token = getToken()//新增
    if (token) {                    //新增
        config.headers.Authorization = `Bearer ${token}`
    }
    return config
}, (error) => {
    return Promise.reject(error)
})

(8)路由鉴权实现

思路:自己封装AuthRoute路由鉴权高阶组件,实现未登陆拦截,并返回到登录页面.

判断本地是否有token,有的话,就返回子组件,否则就重定向到登录Login.

components/AuthComponent.js中:

// 1. 判断token是否存在
// 2. 如果存在 直接正常渲染
// 3. 如果不存在 重定向到登录路由

// 高阶组件:把一个组件当成另外一个组件的参数传入
// 然后通过一定的判断 返回新的组件
import { getToken } from '../utils'
import { Navigate } from 'react-router-dom'
function AuthComponent({ children }) {
    const isToken = getToken()
    if (isToken) {
        return <>{children}</>
    } else {
        return <Navigate to='/login' replace></Navigate>
    }
}
// <AuthComponent> <Layout/> </AuthComponent>
// 登录:<><Layout/></>--登录就渲染对应的组件
// 非登录:<Navigate to="/login" replace />

export {
    AuthComponent
}

在App.js中:(接口有误,我自己项目就不添加这步了)

import { AuthComponent } from './components/AuthComponent'
  <Routes>

                    {/* 创建路由path和对应组件关系 */}
                    {/* Layout需要鉴权处理的 */}
                    {/* 这里的Layout不一定不能写死 要根据是否登录进行判断 */}
                    <Route path='/' element={
                        <AuthComponent>//包裹一下
                            <Layout />
                        </AuthComponent>

                    }></Route>
                        {/* 这个不需要,登录页嘛 */}
                    <Route path='/login' element={<Login />}></Route>

                </Routes>

看下登录页面的效果静态图:

五.layout模块

看下要实现的效果图:

(1)解决layout首页显示高度的问题

App.scss中:

.App{
    height: 100vh;
}

Layout/index.js结构:

import { Layout, Menu, Popconfirm } from 'antd'
import { Outlet, Link } from 'react-router-dom'

import {
  HomeOutlined,
  DiffOutlined,
  EditOutlined,
  LogoutOutlined
} from '@ant-design/icons'
import './index.scss'

const { Header, Sider } = Layout

const GeekLayout = () => {
 
  return (
    <Layout>
      <Header className="header">
        <div className="logo" />
        <div className="user-info">
          <span className="user-name">用户名</span>
          <span className="user-logout">
            <Popconfirm
             
              title="是否确认退出?" okText="退出" cancelText="取消">
             < LogoutOutlined/> 退出
            </Popconfirm>
          </span>
        </div>
      </Header>
      <Layout>
        <Sider width={200} className="site-layout-background">
          {/* 高亮原理:defaultSelectedKeys === item key */}
          {/* 获取当前激活的path路径? */}
          {/* 
             defaultSelectedKeys: 初始化渲染的时候生效一次
             selectedKeys: 每次有值更新时都会重新渲染视图
          */}
          <Menu
            mode="inline"
            theme="dark"
           
           
            style={
    
    { height: '100%', borderRight: 0 }}
          >
            <Menu.Item icon={<HomeOutlined />} key="/">
              <Link to='/'>数据概览</Link>
            </Menu.Item>
            <Menu.Item icon={<DiffOutlined />} key="/article">
              <Link to="/article">内容管理</Link>
            </Menu.Item>
            <Menu.Item icon={<EditOutlined />} key="/publish">
              <Link to='/publish'> 发布文章</Link>
            </Menu.Item>
          </Menu>
        </Sider>
        <Layout className="layout-content" style={
    
    { padding: 20 }}>
          {/* 二级路由出口 */}
          <Outlet />
        </Layout>
      </Layout>
    </Layout>
  )
}

export default GeekLayout

样式index.scss

.ant-layout {
    height: 100%;
  }
  
  .header {
    padding: 0;
  }
  
  .logo {
    width: 200px;
    height: 60px;
    background: url('../../assets//images/logo.png') no-repeat center / 160px auto;
  }
  
  .layout-content {
    overflow-y: auto;
  }
  
  .user-info {
    position: absolute;
    right: 0;
    top: 0;
    padding-right: 20px;
    color: #fff;
  
    .user-name {
      margin-right: 20px;
    }
  
    .user-logout {
      display: inline-block;
      cursor: pointer;
    }
  }
  .ant-layout-header {
    padding: 0 !important;
  }
  

(2)二级路由跳转

新建Home,Article和Publish三个文件

在App.js中引入,并注册二级路由,默认展示Home页:

import Article from './pages/Article'
import Home from './pages/Home'
import Publish from './pages/Publish'

 <Route path='/' element={<Layout />}>
             <Route index element={<Home/>}></Route>
               <Route path='article' element={<Article/>}></Route>
                 <Route path='publish' element={<Publish/>}></Route>
   </Route>

此处注意,必须要在Layout/index.js中,Link可以实现跳转到响应的页面

import { Outlet} from 'react-router-dom'//这是出口,不写的话页面什么也没有

(3)路由跳转配置

Layout/index.js中

import { Link } from 'react-router-dom'

            <Menu.Item icon={<HomeOutlined />} key="/">
              <Link to='/'>数据概览</Link>
            </Menu.Item>
            <Menu.Item icon={<DiffOutlined />} key="/article">
              <Link to="/article">内容管理</Link>
            </Menu.Item>
            <Menu.Item icon={<EditOutlined />} key="/publish">
              <Link to='/publish'> 发布文章</Link>
            </Menu.Item>
          </Menu>

(4)菜单反向高亮

Layout/index.js中

import { useLocation } from 'react-router-dom'

 const {pathname} = useLocation()//解构一下
  {/* 高亮原理:defaultSelectedKeys === item key */}
          {/* 获取当前激活的path路径? */}
          {/* 
             defaultSelectedKeys: 初始化渲染的时候生效一次
             selectedKeys: 每次有值更新时都会重新渲染视图
      */}
      
        <Menu
            mode="inline"
            theme="dark"
           defaultSelectedKeys={[pathname]} //这里是
           
            style={
    
    { height: '100%', borderRight: 0 }}
          >
            <Menu.Item icon={<HomeOutlined />} key="/">//每个key也是
              <Link to='/'>数据概览</Link>
            </Menu.Item>
            <Menu.Item icon={<DiffOutlined />} key="/article">//每个key也是
              <Link to="/article">内容管理</Link>
            </Menu.Item>
            <Menu.Item icon={<EditOutlined />} key="/publish">//每个key也是
              <Link to='/publish'> 发布文章</Link>
            </Menu.Item>
          </Menu>

(5)展示用户信息

store/user.Store.js中:

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/index.js中:

import UserStore from './user.Store'
Layout/index.js中:
import {observer} from 'mobx-react-lite' //引入中间件
import {useStore} from '../../store'
import { useEffect } from 'react'
+  const {userStore}=useStore()
+ useEffect(() => {
    userStore.getUserInfo()
  },[userStore])//填入userStore仅仅是让他不提醒没有使用
  
    <span className="user-name">用户名</span>//正常这里的用户名是{
    
    {userStore.userInfo.name}}
    +export default observer(GeekLayout) //observer解决用户名一刷新就丢失的问题

(6)退出登录模块

store/login.Store.js中:

import { http ,setToken,getToken,+removeToken} from '../utils'
+ loginOut = () => {
        this.token = ''
        removeToken()
    }
   

Layout/index.js中:

import { Outlet, Link, useLocation, +useNavigate } from 'react-router-dom'

 //点击退出的回调
  const navigate = useNavigate()
  const onConfirm = () => {
    //退出登录 清空token 返回登录页
    loginStore.loginOut()
    navigate('/login')
  }
  
    <Popconfirm
             + onConfirm={onConfirm}
              title="是否确认退出?" okText="退出" cancelText="取消">
              < LogoutOutlined /> 退出
            </Popconfirm>

(7)处理Token失效

思路:在响应拦截器中处理token失效

**难点:怎么在组件之外实现退出登录返回首页:

来一个链接,详细讲解的:

在组件之外实现退出登录返回首页

//示例代码
import { createBrowserHistory } from 'history';
import { +unstable_HistoryRouter as HistoryRouter } from 'react-router-dom';

let history = createBrowserHistory();

function App() {
  return (
    <HistoryRouter history={history}>
      // The rest of your app
    </HistoryRouter>
  );
}

history.push("/foo");

安装包:yarn add history (这一部分我就在这展示,要不然我就登录不上去了)

常规的js文件中,那些useXXX文件都是失效的,所以得从history 中引入一个方法,需要在路由中配置

在util中新建history.js

import { createBrowserHistory} from 'history'
const history = createBrowserHistory()

export {history}

在util/http.js中:

 import { history } from './history'
 if (error.response.status === 401) {
        // 跳回到登录 reactRouter默认状态下 并不支持在组件之外完成路由跳转
        // 需要自己来实现
        console.log('login')
        history.push('/login')
    }

在App.js中:

+import { +unstable_HistoryRouter as HistoryRouter , Routes, Route } from 'react-router-dom'
+ <HistoryRouter history={history}>
 
   </HistoryRouter>

六.首页echart实现

yarn add echarts

echarts官网

components中新建Bar.js:

// 封装图表bar组件
import { useEffect, useRef } from 'react'
import * as echarts from 'echarts';
export default function 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>
    )
}

Home/index.js中:

// 思路:
// 1. 看官方文档 把echarts加入项目  
// 如何在react获取dom -> useRef
// 在什么地方获取dom节点 -> useEffect
// 2. 不抽离定制化的参数 先把最小化的demo跑起来
// 3. 按照需求,哪些参数需要自定义 抽象出来
import Bar from '../../components/Bar'

export default function Home() {

    return (
        <div>
            {/* 渲染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: '300px', height: '200px' }} />
        </div>
    )
}

七.内容管理

(1)筛选区结构

import { Link } from 'react-router-dom'
import { Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd'
import 'moment/locale/zh-cn'
import locale from 'antd/es/date-picker/locale/zh_CN'
import './index.scss'

const { Option } = Select
const { RangePicker } = DatePicker
const Article = () => {
    const onFinish = (values) => {
        console.log(values);
    }
    return (
        <div>
            {/* 筛选区域 */}
            <Card
                title={
                    <Breadcrumb separator=">">
                        <Breadcrumb.Item>
                            <Link to="/home">首页</Link>
                        </Breadcrumb.Item>
                        <Breadcrumb.Item>内容管理</Breadcrumb.Item>
                    </Breadcrumb>
                }
                style={
    
    { marginBottom: 20 }}
            >
                <Form
                    onFinish={onFinish}
                    initialValues={
    
    { status: null }}>
                    <Form.Item label="状态" name="status">
                        <Radio.Group>
                            <Radio value={null}>全部</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 }}
                        >

                        </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={`根据筛选条件共查询到  条结果:`}>

            </Card>
        </div>
    )
}
export default Article

(2)表格区域结构

import { +Table,+Tag, +Space,Card, Breadcrumb, Form, Button, Radio, DatePicker, Select } from 'antd'
import { EditOutlined, DeleteOutlined } from '@ant-design/icons'
import img404 from '../../assets/images/error.png'

 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',
          render: data => <Tag color='green'>审核通过</Tag>
        },
        {
          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 />}
                  />
                <Button
                  type="primary"
                  danger
                  shape="circle"
                  icon={<DeleteOutlined />}
                 
                />
              </Space>
            )
          },
          fixed: 'right'
        }
      ]
    const data = [
        {
            id: '8218',
            comment_count: 0,
            cover: {
                images:['http:geek.itheima.net/resources/images/15.jpg']
            },
            like_count: 0,
            pubdate: '2019-03-11 09:00:00',
            read_count: 2,
            status: 2,
            title:'理想化处置方案'
        }
    ]
    
     {/* 文章列表区域 */}
            <Card title={`根据筛选条件共查询到  条结果:`}>
                <Table
                    rowKey="id"
                    columns={columns}
                    dataSource={data}
                />
            </Card>

(3)频道列表渲染

Article/index.js中:

import { http } from '../../utils/http'
 //频道列表管理
    const [channelList, setChannelList] = useState([])
    const loadChannelList = async () => {
        const res = await http.get('/channels')
        console.log('1',res);
        setChannelList(res.data.data.channels)
    }
    useEffect(() => {
        loadChannelList()
    }, [])
    
      <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>

(4)获取文章列表

Article/index.js中:

//文章列表管理  统一管理数据 将来修改给setArticleData传对象
    const [list, setList] = useState({
        list: [],//文章列表
        count: 0//文章数量
    })

    //文章参数管理
    const [params, setParams] = useState({
        page: 1,
        per_page: 10
    })

    //如果异步请求函数需要依赖一些数据的变化而重新执行
    //推荐把它写到内部
    //统一不抽离函数到外面,只要涉及到异步请求的函数,都放到useEffect内部
    //本质区别:写到外面每次组件更新都会重新进行函数初始化,是一次性能浪费
    //而写到useEffect中,只会在依赖项发送变化时,函数才会重新进行初始化
    useEffect(() => {
        const loadList = async () => {
            const res = await http.get('/mp/articles', { params })
            console.log('2', res);
        }
        loadList()
    }, [params])

(5)渲染文章列表

Article/index.js中:接口走不通,在这写一下过程把,代码我是直接铺设自己的数据了

+    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])
    
    + <Card title={`根据筛选条件共查询到 ${articleData.count} 条结果:`}>
    + dataSource={articleData.list}

(6)筛选功能实现

思路:1.为表单添加onFinish属性,监听表单提交;

2.根据接口字段格式要求,格式化参数的格式;

  1. 修改params,触发接口重新发送.

 <Form
   + onFinish={onFinish}
   </Form>
   
     const onFinish = (values) => {
        console.log(values);
        const { channel_id, date, status } = values
        //数据处理
        const _params = {}
        if (status !== -1) {//-1代表全部的意思
            _params.status=status
        }
        if (channel_id) {
            _params.channel_id=channel_id
        }
        if (date) {//begin_pubdate后端要求
            _params.begin_pubdate=data[0].format('YYYY-MM-DD') 
            _params.end_pubdate=data[1].format('YYYY-MM-DD') 
        }
        //修改params数据,引起接口的重新发送 对象的合并是一个整体覆盖 改了对象的整体应用
        setParams({//这样的话就不是整体替换了
            ...params,
            ..._params
        })
    }

(7)分页器功能

 //改变分页
 +   const pageChange = (page) => {
        setParams({
            ...params,
            page
        })
    }
<Table
          rowKey="id"
          columns={columns}
          dataSource={articleData.list}
          +pagination={
            {
              pageSize: params.per_page,
              total: articleData.count,
              onChange: pageChange,
              current: params.page
            }
          }
          bordered
        />

(8)文章删除功能

  // 删除文章
    const delArticle = async (data) => {
        await http.delete(`/mp/articles/${data.id}`)
        // 刷新一下列表
        setParams({
            ...params,
            page: 1
        })
    }
<Button
     type="primary"
      danger
      shape="circle"
      icon={<DeleteOutlined />}
      +onClick={() => delArticle(data)}
            />

(9)跳转到编辑页

import { Link, +useNavigate } from 'react-router-dom'
   //跳转到编辑页
    const navigate = useNavigate()//必须写这个地方,写到内部就报错了
    const goPublish = (data) => {
        navigate(`/publish?id=${data.id}`)
    }
  render: data => {
                return (
                    <Space size="middle">
                        <Button
                            type="primary"
                            shape="circle"
                            icon={<EditOutlined />}
                         +   onClick={() => goPublish(data)}
                        />
                      
                    </Space>
                )
            },

八.发布文章结构搭建

(1)基本结构搭建

Publish/index.js中:

import {
    Card,
    Breadcrumb,
    Form,
    Button,
    Radio,
    Input,
    Upload,
    Space,
    Select,
    message
} from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'

import './index.scss'



const { Option } = Select
const Publish = () => {

    return (
        <div className="publish">
            <Card
                title={
                    <Breadcrumb separator=">">
                        <Breadcrumb.Item>
                            <Link to="/home">首页</Link>
                        </Breadcrumb.Item>
                        <Breadcrumb.Item>文章</Breadcrumb.Item>
                    </Breadcrumb>
                }
            >
                <Form
                    labelCol={
    
    { span: 4 }}
                    wrapperCol={
    
    { span: 16 }}
                    initialValues={
    
    { type: 1, content: '' }}


                >
                    <Form.Item
                        label="标题"
                        name="title"
                        rules={[{ required: true, message: '请输入文章标题' }]}
                    >
                        <Input placeholder="请输入文章标题" style={
    
    { width: 400 }} />
                    </Form.Item>
                    <Form.Item
                        label="频道"
                        name="channel_id"
                        rules={[{ required: true, message: '请选择文章频道' }]}
                    >
                        <Select placeholder="请选择文章频道" style={
    
    { width: 400 }}>

                            <Option key='' value=''>1</Option>


                        </Select>
                    </Form.Item>

                    <Form.Item label="封面">
                        <Form.Item name="type">
                            <Radio.Group >
                                <Radio value={1}>单图</Radio>
                                <Radio value={3}>三图</Radio>
                                <Radio value={0}>无图</Radio>
                            </Radio.Group>
                        </Form.Item>
                      <Upload
                            name="image"
                            listType="picture-card"
                            className="avatar-uploader"
                            showUploadList
                            action="http://geek.itheima.net/v1_0/upload"




                        >
                            <div style={
    
    { marginTop: 8 }}>
                                <PlusOutlined />
                            </div>
                        </Upload>

                    </Form.Item>
                    {/* 这里的富文本组件 已经被Form.Item控制 */}
                    {/* 它的输入内容 会在onFinished回调中收集起来 */}
                    <Form.Item
                        label="内容"
                        name="content"
                        rules={[{ required: true, message: '请输入文章内容' }]}
                    >
                      
                    </Form.Item>

                    <Form.Item wrapperCol={
    
    { offset: 4 }}>
                        <Space>
                            <Button size="large" type="primary" htmlType="submit">
                                文章
                </Button>
                        </Space>
                    </Form.Item>
                </Form>
            </Card>
        </div>
    )
}

export default Publish

样式index.scss:

.publish {
    position: relative;
    .ql-container {//富文本的;类名
      height: 400px !important;
    }
  }
  
  .ant-upload-list {
    .ant-upload-list-picture-card-container,
    .ant-upload-select {
      width: 146px;
      height: 146px;
    }
  }
  

(2)富文本编辑器实现

1.安装富文本编辑器yarn add react-quill;

2.导入富文本编辑器以及样式文件,并渲染;

  1. 通过Form组件的initialValues为富文本设置初始值,否则会报错.

富文本编辑器

import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

 <Form.Item
                        label="内容"
                        name="content"
                        rules={[{ required: true, message: '请输入文章内容' }]}
                    >
                      +  <ReactQuill theme="snow" />
                    </Form.Item>

(3)重构频道获取

1.频道列表专门封装

store中新建channel.Store.js

import { makeAutoObservable } from 'mobx'
import {http} from '../utils'
class ChannelStore{
    channelList = []
    constructor() {
        makeAutoObservable(this)
    }
      // article publish 哪里调用这个函数呢?
    loadChannelList = async() => {
        const res = await http.get('/channels')
        this.channelList=res.data.data.channels
    }
}
export default ChannelStore
  1. 改造内容管理Article中的频道列表

 //频道列表管理
  -  const [channelList, setChannelList] = useState([])
  -  const loadChannelList = async () => {
     -   const res = await http.get('/channels')
     -   console.log('1',res);
     -   setChannelList(res.data.data.channels)
    }
  -  useEffect(() => {
  -      loadChannelList()
  -  }, [])
  //替换成
  +import {observer} from 'mobx-react-lite'
  + const {channelStore}=useStore()
   {
   + channelStore.channelList.map(channel => <Option key={channel.id} value={channel.id}>{channel.name}</Option>)
    }
    +export default observer(Article)
  1. 在发布文章Publish.js中使用:(和上面一样的操作)

 +import {observer} from 'mobx-react-lite'
  + const {channelStore}=useStore()
   {
   + channelStore.channelList.map(channel => <Option key={channel.id} value={channel.id}>{channel.name}</Option>)
    }
   +export default observer(Publish)

(4)基础上传实现

   +import { useState } from 'react';
     + const { channelStore } = useStore()
    //存放上传图片的列表(在最后一次阶段才能拿到图片的响应路径,控制台输出第三次的时候)
    //也就是图片是分阶段上传的 这里区别Vue 成功失败都在这里显示
    +const [fileList, setFileList] = useState([])
    +const onUploadChange = ({fileList}) => {
        console.log(fileList);
        setFileList(fileList)
    }
   <Upload
                            name="image"
                            listType="picture-card"
                            className="avatar-uploader"
                            showUploadList
                            action="http://geek.itheima.net/v1_0/upload"
                           + fileList={fileList}
                           + onChange={onUploadChange}
                           
                        >
                            <div style={
    
    { marginTop: 8 }}>
                                <PlusOutlined />
                            </div>
                        </Upload>

(5)控制封面数量

   //切换图片
    const [imgCount, setImgCount] = useState()
    const radioChange = (e) => {
        console.log(e.target.value);
        setImgCount(e.target.value)
    }
    <Form.Item name="type">
     <Radio.Group  +onChange={radioChange}>
          <Radio value={1}>单图</Radio>
           <Radio value={3}>三图</Radio>
           <Radio value={0}>无图</Radio>
           </Radio.Group>
               {//判断是否是无图,无图就不展示上传了
                            imgCount > 0 && (
                                <Upload
                                    name="image"
                                    listType="picture-card"
                                    className="avatar-uploader"
                                    showUploadList
                                    action="http://geek.itheima.net/v1_0/upload"
                                    fileList={fileList}
                                    onChange={onUploadChange}

                                >
                                    <div style={
    
    { marginTop: 8 }}>
                                        <PlusOutlined />
                                    </div>
                                </Upload>
                            )
                        }

(6)控制最大上传数量

1.修改Upload组件的maxCount(最大数量)属性控制最大上传数量;

  1. 控制multiple(支持多图选择属性)控制是否支持选择多张图片.

 <Upload
                                    name="image"
                                    listType="picture-card"
                                    className="avatar-uploader"
                                    showUploadList
                                    action="http://geek.itheima.net/v1_0/upload"
                                    fileList={fileList}
                                    onChange={onUploadChange}
                                   + multiple={imgCount > 1}
                                   + maxCount={imgCount}  最大数量
                                >
                                    <div style={
    
    { marginTop: 8 }}>
                                        <PlusOutlined />
                                    </div>
                                </Upload>

(7)发布文章实现

接口有误,正常的话点击发布后会在文章管理Table中多一条数据.

  //发布文章
    const onFinish = async(values) => {
        console.log(values);
         // 数据的二次处理 重点是处理cover字段
        const { channel_id, content, title, type } = values
        const params = {
            channel_id, content, title, type,
            cover: {
                type: type,
                images:fileList.map(item=>item.response.data.url)
            }
        }
        console.log(params);
        await http.post('/mp/articles?draft=false', params)
    }
    
       <Form
                    labelCol={
    
    { span: 4 }}
                    wrapperCol={
    
    { span: 16 }}
                    initialValues={
    
    { type: 1, content: '1234' }}
                   + onFinish={onFinish}

            >

(8)暂存图片列表实现

思路:在上传完毕之后通过ref存储所有图片,需要几张就显示几张,其实是把ref当仓库,需要几张拿几张.

import { useState, +useRef } from 'react';

const Publish = () => {
      // 使用useRef声明一个暂存仓库
    +const cacheImgList = useRef()
    const [fileList, setFileList] = useState([])
    const onUploadChange = ({ fileList }) => {
        console.log(fileList);
        setFileList(fileList)
        //同时把图片列表存入仓库一份
       + cacheImgList.current = fileList
    }
    
      //切换图片
    const [imgCount, setImgCount] = useState()
    const radioChange = (e) => {
       + const rawValue = e.target.value
        setImgCount(rawValue)
        //从仓库取对应的图片数量,交给渲染图片列表的fileList
        //通过调用setFileList
        if (cacheImgList.current.length === 0) {
            return false
        }

      +  if (rawValue === 1) {
            const img = cacheImgList.current ? cacheImgList.current[0] : []
            setFileList([img])
     +   } else if (rawValue === 3) {
            setFileList(cacheImgList.current)
        }
    }
}

(9)编辑文章-适配文案

思路:1.通过路由参数拿到文章id;

2.根据文章id是否存在判断是否是编辑状态;

  1. 如果是编辑状态,展示编辑时的文案信息.

import { Link, +useSearchParams } from 'react-router-dom'
 //编辑功能
    const [params] = useSearchParams()
    const id = params.get('id')
    console.log('文章id是', id);
    
      <Card
                title={
                    <Breadcrumb separator=">">
                        <Breadcrumb.Item>
                            <Link to="/home">首页</Link>
                        </Breadcrumb.Item>
                       + <Breadcrumb.Item>{id ? '编辑' : '发布'}文章</Breadcrumb.Item>
                    </Breadcrumb>
                }
            >
               <Button size="large" type="primary" htmlType="submit">
                                {id ? '编辑' : '发布'}  文章
                </Button>
          </Card>

(10)回显基础数据

因为接口错误,目前实现的效果是点击内容管理的编辑会跑到发布文章页,除了上传图片,剩下的内容都能回显出来.

   //编辑功能
    const [params] = useSearchParams()
    const id = params.get('id')
    console.log('文章id是', id);
    // 数据回填  id调用接口  1.表单回填 2.暂存列表 3.Upload组件fileList
   + const form = useRef(null)
    useEffect(() => {
        const loadDetail = async () => {
            const res = await http.get(`/mp/articles/${id}`)
            //表单数据回填,实例方法
       +     form.current.setFieldsValue(res.data)
            console.log(res);
        }
        if (id) {//id存在的话,才发起编辑
            loadDetail()
            console.log('form', form.current);
        }
        
    }, [id])
    
       <Form
                    labelCol={
    
    { span: 4 }}
                    wrapperCol={
    
    { span: 16 }}
                    initialValues={
    
    { type: 1, content: '1234' }}
                    onFinish={onFinish}
                 +   form={form}
                >

(11)回显upload上传图片

   //编辑功能
    const [params] = useSearchParams()
    const id = params.get('id')
    console.log('文章id是', id);
    // 数据回填  id调用接口  1.表单回填 2.暂存列表 3.Upload组件fileList
    const form = useRef(null)
    useEffect(() => {
        const loadDetail = async () => {
            const res = await http.get(`/mp/articles/${id}`)
            //表单数据回填,实例方法
            const data = res.data
          +  form.current.setFieldsValue({ ...data, type: data.cover.type })
            //调用setFileList方法回填upload
            +const formatImgList=data.cover.images.map(url =>({url}))
           + setFileList(formatImgList)
            //暂存列表里也存一份(暂存列表和fileList回显列表数据保持一致就能正常显示)
           + cacheImgList.current = formatImgList
        }
        if (id) {//id存在的话,才发起编辑
            loadDetail()
            console.log('form', form.current);
        }

    }, [id])
    

(12)点击修改保存提交

    const onUploadChange = ({ fileList }) => {
        console.log(fileList);
        setFileList(fileList)
        //同时把图片列表存入仓库一份
        const formatList = fileList.map(file => {
            //上传完毕,作数据处理
          +  if (file.response) {
                return {
                   url:file.response.data.url
               } 
            }
            //否则正在上传,不做处理
           + return file
        })
        cacheImgList.current = fileList
    }
    
      //发布文章
    const navigator=useNavigate()
    const onFinish = async (values) => {
        console.log(values);
        // 数据的二次处理 重点是处理cover字段
        const { channel_id, content, title, type } = values
        const params = {
            channel_id, content, title, type,
            cover: {
                type: type,
             +   images: fileList.map(item => item.url)
            }
        }
        if (id) {
            await http.post(`/mp/articles/${id}?draft=false`, params)
        } else {
            await http.post('/mp/articles?draft=false', params)
        }
        navigator('/article')
        message.success(`${id?'编辑成功':'发布成功'}`)

    }

九.项目打包

(1)打包

yarn build

(2)打包体积分析

  1. 安装分析打包体积的包

yarn add source-map-explorer

2.在package.json中的scripts标签中,添加分析打包体积的命令;

3.对项目打包:yarn build,执行过的话这步省略;

4.运行分析命令:yarn analyze;

  1. 通过浏览器打开的命令,分析图表中的包体积.

"scripts":{
    "report":"source-map-explorer 'build/static/js/*.js'"
}

(3)CDN配置

说明:使用craco 来修改webpack配置,从而实现CDN优化.

新建craco.config.js

// 添加自定义对于webpack的配置

const path = require('path')
const { whenProd, getPlugin, pluginByName } = require('@craco/craco')

module.exports = {
  // webpack 配置
  webpack: {
    // 配置别名
    alias: {
      // 约定:使用 @ 表示 src 文件所在路径
      '@': path.resolve(__dirname, 'src')
    },
    // 配置webpack
    // 配置CDN
    configure: (webpackConfig) => {
      // webpackConfig自动注入的webpack配置对象
      // 可以在这个函数中对它进行详细的自定义配置
      // 只要最后return出去就行
      let cdn = {
        js: [],
        css: []
      }
      // 只有生产环境才配置
      whenProd(() => {
        // key:需要不参与打包的具体的包
        // value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
        // 通过import 导入的 react / react-dom
        webpackConfig.externals = {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
        // 配置现成的cdn 资源数组 现在是公共为了测试
        // 实际开发的时候 用公司自己花钱买的cdn服务器
        cdn = {
          js: [
            'https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js',
            'https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js',
          ],
          css: []
        }
      })

      // 都是为了将来配置 htmlWebpackPlugin插件 将来在public/index.html注入
      // cdn资源数组时 准备好的一些现成的资源
      const { isFound, match } = getPlugin(
        webpackConfig,
        pluginByName('HtmlWebpackPlugin')
      )

      if (isFound) {
        // 找到了HtmlWebpackPlugin的插件
        match.userOptions.cdn = cdn
      }

      return webpackConfig
    }
  }
}
public/index.html中:
  <body>
  <!-- 加载第三发包的 CDN 链接 
  
    这个代码写到head里有可能 也有可能写到body之上 取决于依赖dom完成生成
  -->
  <% htmlWebpackPlugin.options.cdn.js.forEach(cdnURL=> { %>
    <script src="<%= cdnURL %>"></script>
    <% }) %>

再次执行yarn build

(4)路由懒加载

1.在App.js组件中,导入Suspense组件(异步加载);

2.在路由Router内部,使用Suspense组件包裹组件内容;

3.为Suspense组件提供fallback属性,指定loading占位内容;

  1. 导入lazy函数,并修改为懒加载方式导入路由组件.

App.js:

import { lazy, Suspense } from 'react'
// 按需导入组件
const Login = lazy(() => import('./pages/Login'))
const Layout = lazy(() => import('./pages/Layout'))
const Home = lazy(() => import('./pages/Home'))
const Article = lazy(() => import('./pages/Article'))
const Publish = lazy(() => import('./pages/Publish'))

 <Suspense
                    fallback={
                        <div
                            style={
    
    {
                                textAlign: 'center',
                                marginTop: 200
                            }}
                        >
                            loading...
                        </div>
                    }
                >
                 <Routes>
                   ...
                   </Routes>
      </Suspense>

觉得还不错的话,动动动您的小手点个赞呗!!

猜你喜欢

转载自blog.csdn.net/huihui_999/article/details/129473654
今日推荐