《CMS后台系统》项目实战 详细分解

写在前头

这个项目是跟着b站up主 单排哥 学习的。

传送门

https://www.bilibili.com/video/BV1ta411b7eo?p=1

具体功能

  • 查看文章
  • 文章编辑
  • 修改资料

image.png

image.png

image.png

一、项目介绍

《CMS后台系统》项目主要是为CMS官网提供文章编辑、上传等功能。其中包括富文本编辑器调用、路由管理、权限管控、图片上传等主体模块。

项目预览路径:http://codesohigh.com/cms-manage/

UI框架使用:Ant Design

二、项目起步

1、创建项目

$ npx create-react-app cms-manage

在一个文件夹目录下点击,输入cmd,进去终端命令框,然后使用上面命名,进行项目创建。

image.png

image.png

显示以下类似的文字就是表面项目已经创建成功。

image.png

2、安装依赖

本项目用到的依赖可以在此预先安装好:

  • antd
  • redux与react-redux
  • react-router-dom
  • axios
  • less与less-loader
$ npm i antd redux react-redux react-router-dom@6 axios less less-loader@6.0.0 --save
 
  • 在vscode中打开终端使用。

image.png

3、Antd引入和测试

Ant Design 传送门

https://ant.design/index-cn

  • 首先将src文件夹下的所有文件删除

image.png

在像图片下面一样新建文件夹及文件。

  • 在bass.css中引入antd。
@import '~antd/dist/antd.css';

image.png

  • 初始化App.jsx
import React from 'react';
import "./assets/base.css"


const App = () => {
    return (
        <div>
            App
        </div>
    );
}

export default App;

image.png

  • index.js
import ReactDOM from 'react-dom'


ReactDOM.render(
        <Router />,
    document.getElementById('root')
)

  • 测试antd

image.png

import React from 'react';
import "./assets/base.css"
import { Button} from 'antd';


const App = () => {
    return (
        <div>
        <Button type="primary">primary</Button>
        </div>
    );
}

export default App;

  • 测试成功

image.png

4、路由配置

  • 安装插件

image.png

  • 创建页面pages

image.png

在每一个jsx文件下使用rfc代码片段快速生成函数组件。(注意:需要安装插件)

import React from 'react'

export default function Edit() {
  return (
    <div>Edit</div>
  )
}
  • 新建router文件夹,书写index.jsx文件

image.png

/*
    App > List + Edit + Means
    Login
    Register
    History模式 BrowserRouter
    Hash模式 HashRouter
*/

import App from '../App'
import List from '../pages/List'
import Edit from '../pages/Edit'
import Means from '../pages/Means'
import Login from '../pages/Login'
import Register from '../pages/Register'
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom'


const BaseRouter = () => (
    <Router>
        <Routes>
            <Route path='/' element={<App />}>
                <Route path='/list' element={<List />}> </Route>
                <Route path='/edit' element={<Edit />}> </Route>
                <Route path='/means' element={<Means />}> </Route>
            </Route>
            <Route path='/login' element={<Login />}> </Route>
            <Route path='/register' element={<Register />}> </Route>
        </Routes>
    </Router>
)

export default BaseRouter
  • 将index.js修改顶级组件

image.png

import ReactDOM from 'react-dom'
import Router from "./router"

ReactDOM.render(
        <Router />,
    document.getElementById('root')
)

  • 更改路径可以检查是否成功

image.png

在App.js组件中使用Outlet

image.png

import React from 'react';
import "./assets/base.css"
import { Button} from 'antd';
import { Outlet } from 'react-router-dom';


const App = () => {
    return (
        <div>
        <Button type="primary">primary</Button>
        <Outlet/>   
        </div>
    );
}

export default App;

  • 检查成功?

image.png

#5.解包配置Less

1、解包并配置Less

  • 在项目根目录下运行解包命令:
# 解包前必须做git提交,否则无法解包(这是为了方便随时做版本回滚)
$ git init
$ git add .
$ git commit -m '解包前'
$ npm run eject

image.png

  • 解包之后,项目根目录下将出现config目录,找到webpack.config.js,搜索 sassModuleRegex 后,在其下方添加:
{
  test: /\.less$/,
    use: getStyleLoaders(
      {
        //暂不配置
      },
      'less-loader'
    ),
},

image.png

修改了配置文件,记得重跑项目哦!

  • 测试Less

删除入口文件index.js下对antd.css的引入,然后在src下创建 assets>base.less:

@import '~antd/dist/antd.css';	// 有个波浪线
@bgcolor: pink;

body{
    background: @bgcolor;
}

image.png

此时看网页是否按钮正常,并且body背景变为粉色。若是,则Less配置成功。

image.png

三、页面布局

登录页布局
  • 基本样式
  1. 修改页面背景颜色
@import '~antd/dist/antd.css';
@bgcolor: #efefef;

body{
    font-family: "微软雅黑";
    font-size: 14px;
    color: #333;
    background: @bgcolor;
}

  1. 删除APP组件中的button按钮。
import React from 'react';
import "./assets/base.less"
import { Outlet } from 'react-router-dom';


const App = () => {
    return (
        <div>
        <Outlet/>   
        </div>
    );
}

export default App;

image.png

  • 进入Login路由组件

form表单-Ant Design 传送门

https://ant.design/components/form-cn/#header

  • 在第一个表单下复制代码
import React from 'react'
import { Form, Input, Button, Checkbox } from 'antd';

export default function Login() {

  const onFinish = (values: any) => {
    console.log('Success:', values);
  };

  const onFinishFailed = (errorInfo: any) => {
    console.log('Failed:', errorInfo);
  };

  return (
    <div>
    <Form
    name="basic"
    labelCol={{ span: 8 }}
    wrapperCol={{ span: 16 }}
    initialValues={{ remember: true }}
    onFinish={onFinish}
    onFinishFailed={onFinishFailed}
    autoComplete="off"
  >
    <Form.Item
      label="Username"
      name="username"
      rules={[{ required: true, message: 'Please input your username!' }]}
    >
      <Input />
    </Form.Item>

    <Form.Item
      label="Password"
      name="password"
      rules={[{ required: true, message: 'Please input your password!' }]}
    >
      <Input.Password />
    </Form.Item>

    <Form.Item name="remember" valuePropName="checked" wrapperCol={{ offset: 8, span: 16 }}>
      <Checkbox>Remember me</Checkbox>
    </Form.Item>

    <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
      <Button type="primary" htmlType="submit">
        Submit
      </Button>
    </Form.Item>
  </Form>
    </div>
  )
}

  • 改变路径检查是否成功。

image.png

添加类名

image.png

  <div className='login'>
        <div className='login_box'>
       
        </div>
    </div>

书写less

.login {
    background: #fff;
    width: 100vw;
    height: 100vh;
    position:relative;
    .login_box{
        width: 500px;
        // 表单位置居中
        position: absolute;
        left:50%;
        top:50%;
        transform: translate(-50%,-50%); 
    }
}

image.png

  • 引入图片

在src-> assets 下添加logo图片

image.png

在login组件添加图片

  1. 引入图片
import logoImg from '../assets/logo.png'
  1. 设置图片标签
  <div className='login_box'>

image.png

  1. 设置图片属性
 img {
            display: block;
            margin: 0 auto 20px;
        }

image.png

  • 改写表单

将复制的代码删除部分不需要的,得到下方代码

        <Form
        name="basic"
        initialValues={{ remember: true }}
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
        autoComplete="off"
        >
        <Form.Item
          name="username"
          rules={[{ required: true, message: 'Please input your username!' }]}
        >
          <Input />
        </Form.Item>
        
        <Form.Item
          name="password"
          rules={[{ required: true, message: 'Please input your password!' }]}
        >
          <Input.Password />
        </Form.Item>
        
        <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
          <Button type="primary" htmlType="submit">
            Submit
          </Button>
        </Form.Item>
        </Form>
  • 修改按钮
   <Form.Item>
        <Button type="primary" htmlType="submit" block  size='large'>
        登录
      </Button>
        </Form.Item>
  • 改写输入框

添加icon和提示字体

import { UserOutlined, LockOutlined } from '@ant-design/icons';

<Input size="large" prefix={<UserOutlined className="site-form-item-icon" />} placeholder='请输入用户名'/>
 
<Input.Password size="large" prefix={<LockOutlined className="site-form-item-icon" />} placeholder='请输入密码'/>
          </Form.Item>
  • 添加跳转链接
import {Link} from 'react-router-dom'


<Form.Item>
            <Link to="/register">还没账号?立即注册</Link>
</Form.Item>

  • 最终实现效果图

image.png

注册页布局
  • 将登录的代码复制到注册页面

  • 添加验证密码框

image.png

 <Form.Item
        name="confirm"
        dependencies={['password']}
        hasFeedback
        rules={[
          {
            required: true,
            message: 'Please confirm your password!',
          },
          ({ getFieldValue }) => ({
            validator(_, value) {
              if (!value || getFieldValue('password') === value) {
                return Promise.resolve();
              }
              return Promise.reject(new Error('The two passwords that you entered do not match!'));
            },
          }),
        ]}
      >
        <Input.Password size="large" prefix={<LockOutlined className="site-form-item-icon" />} placeholder='请再次输入密码'/>
      </Form.Item>

将路由跳转修改

  <Form.Item>
           <Link to="/login">已有账号?前往登录</Link>
  </Form.Item>

将按钮修改为“立即注册”

  <Form.Item>
            <Button type="primary" htmlType="submit" block  size='large'>
            立即注册
            </Button>
   </Form.Item>

  • 实现效果

image.png

Request封装

1、接口文档

本项目的接口文档:http://xiaoyaoji.cn/project/1kSQB8SHnDV/share/1mfpzz0vdw0 (opens new window), 密码:zhaowenxian

在src下创建request目录,并在其中创建request.js及api.js。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Tq59j8wZ-1651547144160)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc0c34a8a23e4f3eb153b9e85afe3597~tplv-k3u1fbpfcp-watermark.image?)]

2、封装axios请求

request.js:

import axios from 'axios'

// 配置项
const axiosOption = {
    baseURL: 'http://47.93.114.103:6688/manage',
    timeout: 5000
}

// 创建一个单例
const instance = axios.create(axiosOption);

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});

export default instance;

注意:这里并没有考虑token,后面会添加。

  • 测试baseUrl

http://47.93.114.103:6688/manage

image.png

3、api.js

api.js暂时可定以下格式,后续项目中再修改:

import request from './request'

export const xxApi = () => request.get('/xx')

  • 书写注册接口
// 引入request
import request from './request'

// 注册
export const RegisterApi = (params) => request.post('/register', params)
  • 在Register.jsx中引入RegisterApi
import {RegisterApi} from '../request/api'

image.png

  • 在 onFinish函数中使用RegisterApi,但是需要先解决跨域问题

1、解决跨域

如果你已经进行了 npm run eject ,建议你直接修改 config>webpackDevServer.config.js :

proxy: {
  '/api': {
    target: 'http://47.93.114.103:6688/manage', // 后台服务地址以及端口号
    changeOrigin: true, //是否跨域
    pathRewrite: { '^/api': '/' }
  }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EmfE4WGJ-1651547144163)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98249599bd0349af843f591cb9c276a3~tplv-k3u1fbpfcp-watermark.image?)]

http://47.93.114.103:6688/manage 替换成为api。

将 request.js: 的baseUrl进行修改

image.png

// 配置项
const axiosOption = {
  baseURL: '/api',
  timeout: 5000
}

  • 在Request.js文件的onFinish函数书写请求。
  const onFinish = (values) => {
    RegisterApi({
      username: values.username,
      password: values.password
    }).then(res=>{
      if(res.errCode===0){
        message.success(res.message);
        // 跳到登录页
        setTimeout(()=>navigate('/login'), 1500)
      }else{
        message.error(res.message);
      }
    })
  };

image.png

  • 跳转路由是使用的hook。
import {Link, useNavigate} from 'react-router-dom'


if(res.errCode===0){
        message.success(res.message);
        // 跳到登录页
        setTimeout(()=>navigate('/login'), 1500)
  }
  • 未注册

image.png

  • 已注册

image.png

登录
  • 在api.js 下面添加登录请求
// 登录
export const LoginApi = (params) => request.post('/login', params)

image.png

  • 在Login.js引入

在OnFinish里面调用

import {LoginApi} from '../request/api'

  const onFinish = (values) => {
    console.log('Success:',values);
    LoginApi({
      username:values.username,
      password:values.password
    }).then(res=>{
      console.log(res)
    })
  };

image.png

image.png

  • 使用条件语句判断是否成功
 if(res.errCode === 0) {

      }else {
        message.error(res.message)
      }
  • 存储数据 (不使用对象,方便存取)
 // 存储数据
        localStorage.setItem('avatar', res.data.avatar)
        localStorage.setItem('cms-token', res.data['cms-token'])
        localStorage.setItem('editable', res.data.editable)
        localStorage.setItem('player', res.data.player)
        localStorage.setItem('username', res.data.username)
  • 使用setTimeout()跳转页面
import {Link, useNavigate} from 'react-router-dom'

  const navigate = useNavigate()

// 跳转到根路径
        setTimeout(()=>{
          navigate('/')
        }, 1500)
  • 总代码(登录)
const onFinish = (values) => {
    console.log('Success:',values);
    LoginApi({
      username:values.username,
      password:values.password
    }).then(res=>{
      console.log(res)
      if(res.errCode===0){
        message.success(res.message)
        // 存储数据
        localStorage.setItem('avatar', res.data.avatar)
        localStorage.setItem('cms-token', res.data['cms-token'])
        localStorage.setItem('editable', res.data.editable)
        localStorage.setItem('player', res.data.player)
        localStorage.setItem('username', res.data.username)
        // 跳转到根路径
        setTimeout(()=>{
          navigate('/')
        }, 1500)
      }else{
        message.error(res.message)
      }
    })
  };

image.png

  • 最终效果图

image.png

App布局

布局传送门

https://ant.design/components/layout-cn/

image.png

import { Layout } from 'antd';

const { Header, Footer, Sider, Content } = Layout;

<Layout>
    <Header>Header</Header>
        <Layout>
          <Sider>Sider</Sider>
          <Content>Content</Content>
        </Layout>
    <Footer>Footer</Footer>
</Layout>
  • 略微修改
import React from 'react';
import "./assets/base.less"
import { Outlet } from 'react-router-dom';
import { Layout } from 'antd';

const App = () => {
    
const {Sider, Content } = Layout;

    return (
        <Layout>
        <header>Header</header>
        <Layout>
          <Sider>Sider</Sider>
          <Content>
          <div>
            <Outlet/>   
          </div>
          </Content>
        </Layout>
        <footer>Footer</footer>
      </Layout>
    );
}

export default App;
  • 书写样式 base.less

设置header 和 footer

header {
    height: 70px;
    background-color: pink;
}

footer {
    height: 70px;
    background: #001529;
    color:#fff;
    text-align: center;
    line-height: 70px;
}

实现效果图

image.png

  • 导入logo图片
import logoImg from '../assets/logo.png'

 <img src={logoImg} alt="" className="logo" />

实现效果图

image.png

  • 给logo图片设置间距
header {
    height: 70px;
    background-color: #fff;
    padding: 0 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

实现效果图

image.png

  • 底边框可以随着F12的操作框动态移动

App.jsx

<Layout id='app'>
        <header>
            <img src={logoImg} alt="" className="logo" />
        </header>
 <Layout>

base.less

#app {
    height: 100vh;
}

实现效果图

image.png

下拉菜单

  • 将header抽出成为一个Header组件。

在components下面创建Header文件

image.png

Header组件

import React from 'react'
import logoImg from '../assets/logo.png'

export default function Header() {
  return (
    <div>
    <header>
        <img src={logoImg} alt="" className="logo" />
        <div className='right'>右侧</div>
    </header>
    </div>
  )
}

注意:引入图片的路径记得更改,因为是在components文件之外引入图片

  • 在App.js中引入Header组件
import Header from './components/Header'

<Layout id='app'>
    <Header/>
<Layout>

image.png

  • 下拉菜单

传送门(记得使用3.x的版本,不然没有Menu.Item)

https://3x.ant.design/components/dropdown-cn/

image.png

复制代码

import { Menu, Dropdown } from 'antd';

 <Dropdown overlay={menu}>
                <a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
                Hover me 
                </a>
</Dropdown>

const menu = (
        <Menu>
          <Menu.Item>
            <a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">
              1st menu item
            </a>
          </Menu.Item>
          <Menu.Item>
            <a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">
              2nd menu item
            </a>
          </Menu.Item>
          <Menu.Item>
            <a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">
              3rd menu item
            </a>
          </Menu.Item>
        </Menu>
      );

整体展示


import React from 'react'
import logoImg from '../assets/logo.png'
import { Menu, Dropdown } from 'antd';


export default function Header() {

    const menu = (
        <Menu>
          <Menu.Item>
            <a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">
              1st menu item
            </a>
          </Menu.Item>
          <Menu.Item>
            <a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">
              2nd menu item
            </a>
          </Menu.Item>
          <Menu.Item>
            <a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">
              3rd menu item
            </a>
          </Menu.Item>
        </Menu>
      );

  return (
    <div>
    <header>
        <img src={logoImg} alt="" className="logo" />
        <div className='right'>
            <Dropdown overlay={menu}>
                <a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
                Hover me 
                </a>
          </Dropdown>
        </div>
    </header>
    </div>
  )
}

效果展示

image.png

  • 进行修改

添加阴影线

<Menu.Divider />

    const menu = (
        <Menu>
          <Menu.Item>
            修改资料
          </Menu.Item>
           <Menu.Divider />
          <Menu.Item>
            退出登录
          </Menu.Item>

        </Menu>
      );

image.png

  • 修改图标

传送门

https://3x.ant.design/components/icon-cn/

image.png

import { CaretDownOutlined } from '@ant-design/icons';


<CaretDownOutlined />

效果展示

image.png

import React from 'react'
import logoImg from '../assets/logo.png'
import { Menu, Dropdown} from 'antd';
import { CaretDownOutlined } from '@ant-design/icons';

export default function Header() {

    const menu = (
        <Menu>
          <Menu.Item>
            修改资料
          </Menu.Item>
          <Menu.Divider />
          <Menu.Item>
            退出登录
          </Menu.Item>

        </Menu>
      );

  return (
    <div>
    <header>
        <img src={logoImg} alt="" className="logo" />
        <div className='right'>
            <Dropdown overlay={menu}>
                <a className="ant-dropdown-link" onClick={e => e.preventDefault()}>
                Hover me <CaretDownOutlined />
                </a>
          </Dropdown>
        </div>
    </header>
    </div>
  )
}

下拉菜单
引入用户头像
// 引入useState
import React, {useEffect,useState} from 'react'

// 引入默认头像图片
import defaultAvatar from '../assets/defaultAvatar.jpg'

// 使用useState
const [avatar,setAvatar] = useState(defaultAvatar)

// 插入图片
 <img src={avatar} className="avatar" alt='' />
 

image.png

添加默认用户名
// 使用useState
const [username,setUsername] = useState('游客')

<span>{username}</span>

注意:添加key!

const menu = (
        <Menu>
          <Menu.Item key={1}>
            修改资料
          </Menu.Item>
          <Menu.Divider />
          <Menu.Item key ={2}>>
            退出登录
          </Menu.Item>

        </Menu>
      );
  • 实现效果图

image.png

修改样式

在base.less 下书写样式

image.png

    .right {
        height: 40px;
        .avatar {
            width: 40px;
            height: 40px;
            border-radius: 50%;
        }
        span {
            margin-left: 10px;
            margin-right: 10px;
        }
    }
  • 实现效果图

image.png

修改下拉菜单边距
  • 原样

image.png

在base.less中书写对应a标签的属性

        .ant-dropdown-link{
            height: 60px;
            display: block;
            color: #333;
            &:hover{
                color: #1890ff;
            }
        }
  • 实现效果图

image.png

  • 使用useEffect

在Application中会找到请求存储的数据。

image.png

我们将使用这些数据替代默认的用户名和用户头像。

  • 用户名
    // 模拟componentDidMount
  useEffect(()=>{
    let username1 = localStorage.getItem('username')
    if(username1){
      setUsername(username1)
    }
  },[])
  • 实现效果图

image.png

  • 用户头像
    // 模拟componentDidMount
  useEffect(()=>{
    let username1 = localStorage.getItem('username')
    let avatar1 = localStorage.getItem('avatar')
    if(username1) {
      setUsername(username1)
    }
    if(avatar1) {
      setAvatar('http://47.93.114.103:6688/' + avatar1)
    }
  },[])

注意:传入图片时,记得添加路径。

  • 实现效果图

image.png

退出登录

不可以直接使用Link,进行跳转,因为在Application中数据依然存在。退出时必须清除数据。

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

const navigate = useNavigate()
    
// 退出登录
  const logout = () => {
    message.success('退出成功,即将返回登录页')
    localStorage.clear();   // 清除localStorage中的数据
    setTimeout(() => navigate('/login'), 1500)
}

<Link to="/login"  onClick={logout} >退出登录</Link>
  • 实现效果

image.png

侧边栏布局
  • 创建Aside组件

image.png

  • 修改App组件

image.png

const App = () => {

const {Sider, Content } = Layout;

    return (
        <Layout id='app'>
            <Header/>
        <Layout>
        <Aside />
          <Content>
          <div>
            <Outlet/>   
          </div>
          </Content>
        </Layout>
        <footer>Footer</footer>
      </Layout>
    );
}

export default App;
  • 书写Aside组件

Menu

传送门

https://ant.design/components/menu-cn/

Aside组件

import React from 'react'
import { Menu } from 'antd';
import { ReadOutlined, EditOutlined, DatabaseOutlined } from '@ant-design/icons';

export default function Aside() {

  
  const handleClick = e => {
    console.log('click',e)
  };


  return (
    <Menu
        onClick={handleClick}
        style={{ width: 180 }}
        mode="inline"
        theme="dark" // 黑色主题
    >
      <Menu.Item key="3">Option 3</Menu.Item>
      <Menu.Item key="4">Option 4</Menu.Item>
      </Menu>
  )
}

App组件

import Aside from './components/Aside'
import React from 'react';
import "./assets/base.less"
import { Outlet } from 'react-router-dom';
import { Layout } from 'antd';
import Header from './components/Header'
import Aside from './components/Aside'

const App = () => {

const {Content } = Layout;

    return (
        <Layout id='app'>
            <Header/>
        <Layout>
        <Aside />
          <Content>
          <div>
            <Outlet/>   
          </div>
          </Content>
        </Layout>
        <footer>Footer</footer>
      </Layout>
    );
}

export default App;

  • 实现效果图

image.png

  • 为Aside设置类名
    <Menu
        onClick={handleClick}
        style={{ width: 180 }}
        mode="inline"
        className='aside'
        theme="dark" // 黑色主题
    >
      <Menu.Item key="3">Option 3</Menu.Item>
      <Menu.Item key="4">Option 4</Menu.Item>
      </Menu>
  • App组件内设置属性名
import React from 'react';
import "./assets/base.less"
import { Outlet } from 'react-router-dom';
import { Layout } from 'antd';
import Header from './components/Header'
import Aside from './components/Aside'

function App() {
    return (
        <Layout id='app'>
            <Header/>
        <div className='container'>
         <Aside />       
        <div className='container_box'>
            <div>
              <Outlet/>   
            </div>
        </div>
      </div>  
        <footer>Footer</footer>
      </Layout>
    );
}

export default App;
  • base.less 书写属性
.container{
    display: flex;
    // justify-content: space-between;
    .aside{
        height: calc(100vh - 140px);
    }
    .container_box{
        flex: 1;
        box-sizing: border-box;
        padding: 20px;
        display: flex;
        flex-direction: column;
        .container_content{
            height: calc(100vh - 210px);
            overflow: hidden;
        }
    }
}

-实现效果

image.png

添加图标
      <Menu.Item key="1"><ReadOutlined /> 查看文章列表</Menu.Item>
      <Menu.Item key="2"><EditOutlined /> 文章编辑</Menu.Item>
      <Menu.Item key="3"><DatabaseOutlined /> 修改资料</Menu.Item>

在文字和图标紧凑在一起的时候,我们需要可以敲进一个空格来使得排版更加美观。

  • 实现效果图

image.png

点击实现路由跳转
  • 修改key值
      <Menu.Item key="list"><ReadOutlined /> 查看文章列表</Menu.Item>
      <Menu.Item key="edit"><EditOutlined /> 文章编辑</Menu.Item>
      <Menu.Item key="means"><DatabaseOutlined /> 修改资料</Menu.Item>
  • 使用hook跳转
import {useNavigate} from 'react-router-dom'

  const navigate = useNavigate()
// 修改handleClick

  const handleClick = e => {
    navigate('/'+e.key)
  };
  // 默认路由
  defaultSelectedKeys={['list']}

image.png

  • 实现效果

image.png

遇到的bug

刷新之后路径并未改变,菜单栏改变了。

image.png

  • 片段代码

引入useLocation

import React, { useEffect, useState } from 'react'
import {useNavigate, useLocation} from 'react-router-dom'

const location = useLocation()
const [defaultKey, setDefaultKey] = useState('')

// 一旦渲染立刻获取动态的路由路径,不在使用默认的
  useEffect(() => {
      let path = location.pathname;
      let key = path.split('/')[1];
      setDefaultKey(key)
  }, []);
  
  
  // 及时更新路由路径
    const handleClick = e => {
    navigate('/'+e.key)
    setDefaultKey(e.key)
  };
  • 实现效果图(自己刷新试试咯)

image.png

image.png

面包屑

传送门

https://ant.design/components/breadcrumb-cn/#header

  • 创建Bread组件

image.png

初始化书写Bread组件

import React from 'react';
import { Breadcrumb } from 'antd';
import { HomeOutlined } from '@ant-design/icons';

const Bread = () => {
    return (
        <Breadcrumb>
        <Breadcrumb.Item href="">
          <HomeOutlined />
        </Breadcrumb.Item>
        <Breadcrumb.Item>Application</Breadcrumb.Item>
      </Breadcrumb>
    );
}

export default Bread;

在App组件中引入一个Bread组件

import Bread from './components/Bread'


<div className='container_box'>
    <Bread/>
      <Outlet/>   
</div>
  • 根据路径更新面包屑

更新面包屑名字(useEffect取过来,useState再赋值更新上)

// 引入hook
import React, {useState,useEffect} from 'react';

// 设置变量
const [breadName, setBreadName] = useState('')

// 获取路径
const {pathname} = useLocation()

// 不是在组件mounted时去获取路径,而是路径一旦变化,就要获取对应的路径名称,并且修改breadName
// 监听路由的路径(/list /edit /means)
useEffect(() => {
    switch (pathname) {
        case "/list":
            setBreadName('查看文章列表');
            break;
        case "/edit":
            setBreadName('文章编辑');
            break;
        case "/means":
            setBreadName('修改资料');
            break;
        default:
            break;
    }
}, [pathname])
内容补充(表单,表格)
  • 创建2个路由组件

image.png

image.png

  • 配置路由(index.jsx)
import ListTable from '../pages/ListTable'
import ListList from '../pages/ListList'


const BaseRouter = () => (
    <Router>
        <Routes>
            <Route path='/' element={<App />}>
                <Route path='/listtable' element={<ListTable />}></Route>
                <Route path='/listlist' element={<ListList />}></Route>
                <Route path='/edit' element={<Edit />}></Route>
                <Route path='/means' element={<Means />}> </Route>
            </Route>
            <Route path='/login' element={<Login />}> </Route>
            <Route path='/register' element={<Register />}> </Route>
        </Routes>
    </Router>
)
  • 修改面包屑
    useEffect(() => {
        switch (pathname) {
            case "/listlist":
                setBreadName('查看文章列表List');
                break;
            case "/listtable":
                setBreadName('查看文章列表Table');
                break;
            case "/edit":
                setBreadName('文章编辑');
                break;
            case "/means":
                setBreadName('修改资料');
                break;
            default:
                setBreadName(pathname.includes('edit') ? '文章编辑' : "");
                break;
        }
    }, [pathname])
  • 修改侧边栏
            <Menu.Item key="listlist"><ReadOutlined /> 查看文章列表List</Menu.Item>
            <Menu.Item key="listtable"><ReadOutlined /> 查看文章列表Table</Menu.Item>
  • 实现效果

image.png

ListTable 书写样式
  • 在APP组件中设置布局属性
<div className='container_box'>
    <Bread/>
      <Outlet/>   
</div>

base.less

.container .container_box {
  flex: 1;
  box-sizing: border-box;
  padding: 20px;
  display: flex;
  flex-direction: column;
}
  • 创建less文件

image.png

.list_table{
    width: 100%;
    background: #fff;
    height: 100%;
 
}
  • 引入less样式到ListTable
import './less/ListTable.less'

// 不要忘记添加类名
<div className='list_table'>ListTable</div>

  • 实现效果图

image.png

表格结构搭建

传送门

https://ant.design/components/table-cn/#components-table-demo-basic

  • 删除标签、删除data中的tags、删除年龄一列

完整代码

import React from 'react'
import './less/ListTable.less'
import { Table, Tag, Space } from 'antd';
export default function ListTable() {

  // 真正从后端拿的数据要替换这个data
const data = [
  {
    key: '1',
    name: 'John Brown',
    address: 'New York No. 1 Lake Park',
  },
  {
    key: '2',
    name: 'Jim Green',
    address: 'London No. 1 Lake Park',
  },
  {
    key: '3',
    name: 'Joe Black',
    address: 'Sidney No. 1 Lake Park',
  },
];

// 每一列
const columns = [
  {
    title: 'Name',
    dataIndex: 'name',
    key: 'name',
    render: text => <a>{text}</a>,
  },
  {
    title: 'Age',
    dataIndex: 'age',
    key: 'age',
  },
  {
    title: 'Address',
    dataIndex: 'address',
    key: 'address',
  },
  {
    title: 'Action',
    key: 'action',
    render: (text, record) => (
      <Space size="middle">
        <a>Invite {record.name}</a>
        <a>Delete</a>
      </Space>
    ),
  },
];

  return (
    <div className='list_table'>
    {/* columns列 dataSource数据 */}
       <Table columns={columns} dataSource={data} />
    </div>
  )
}
  • 引入button(在Action下面)
// 引入button

import { Table, Button, Space } from 'antd';

//书写button编辑、删除
      <Button type='primary' >编辑</Button>
      <Button type='danger'>删除</Button>

image.png

  • 隐藏表头

image.png

  • 在Table标签是添加showHeader属性
 <Table columns={columns} showHeader = {false} dataSource={data} />
  • 在columns中删除标题
// 每一列
const columns = [
  {
    dataIndex: 'name',
    key: 'name',
    render: text => <a>{text}</a>,
  },
  {
    dataIndex: 'age',
    key: 'age',
  },
  {
    dataIndex: 'address',
    key: 'address',
  },
  {
    key: 'action',
    render: (text, record) => (
      <Space size="middle">
      <Button type='primary' >编辑</Button>
      <Button type='danger'>删除</Button>
      </Space>
    ),
  },
];
  • 显示效果

image.png

  • 渲染标题副标题

在第一列下面修改

image.png

  {
    dataIndex: 'name',
    key: 'name',
    render: text => (
      <>
        <h4>标题</h4>
        <p>简直是大家</p>
      </>
    ),
  },

image.png

  • 渲染时间

在第二列添加渲染

  {
    dataIndex: 'address',
    key: 'address',
    render: text => (
      <p>
        2022-03-03 20:33:06
      </p>
      )
  },

image.png

  • 改变第一列的宽度

使用width属性

image.png

  • 修改副标题颜色
 {
    dataIndex: 'name',
    key: 'name',
    width:'60%',
    render: text => (
      <>
        <h4>标题</h4>
        <p style={{color:'#999'}}>简直是大家</p>
      </>
    ),
  },

在p标签中添加style属性

    <p style={{color:'#999'}}>简直是大家</p>
  • 实现效果

image.png

  • 修改标题实现跳转

引入Link

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

修改标签

 <Link to="/" className='table_title'>标题</Link>
// 标题样式
    .table_title{
        color: #333;
        &:hover {
          color: #1890ff;
        }
      }

image.png

  • 使用useState更新data数据

将原先的data数组传入arr变量中

// 引入useState
import React,{useState} from 'react'

// 初始化
  const [arr,setArr] = useState([
    {
      key: '1',
      name: 'John Brown',
      address: 'New York No. 1 Lake Park',
    }
  ])
  
// 更改Table标签的属性、dataSource的属性值

<Table columns={columns} showHeader = {false} dataSource={arr} />
axios请求格式

get请求必须书写params

axios.get({
    params: {
        num:1
    }
})

axios.post({
    num:1
})
  • 书写获取文章的api

api.js文件下

// 获取文章列表
export const ArticleListApi = (params) => request.get('/article', {params})

在ListTable.jsx中引入api

import { ArticleListApi } from '../request/api';
  • 使用useEffect来请求文章列表
// 引入useEffect
import React,{useState, useEffect} from 'react'

  // 请求文章列表
  useEffect(() => {
    ArticleListApi().then(res=>{
     console.log(res.data)
   })
  }, []);

数据处理

  • 解决副标题无法渲染,数组无key值

生成一个新数组,然后map遍历赋值一个新key值

  // 请求文章列表
  useEffect(() => {
    ArticleListApi().then(res=>{
      if(res.errCode === 0) {
        let newArr = JSON.parse(JSON.stringify(res.data.arr))
            /* 
                1. 要给每个数组项加key,让key=id
                2. 需要有一套标签结构,赋予一个属性
            */
        newArr.map(item=> {
         item.key = item.id;
         item.mytitle = `
          <>
              <Link to="/" className='table_title'>标题</Link>
              <p style={{color:'#999'}}>简直是大家</p>
          </>
         `;
        })
        console.log(newArr)
      }
   })
  }, []);
  • 工作台

image.png

列表渲染

  • 使用setArr传入newArr

  • 规范渲染时间date

在第二列将dataIndex、key修改为date

  {
    dataIndex: 'date',
    key: 'date',
    render: text => (
      <p>
        {text}
      </p>
      )
  },
  • 安装moment

yarn add moment

  • 引入moment
import moment from 'moment'
  • 整改date
   item.date = moment(item.date).format("YYYY-MM-DD hh:mm:ss")
  • 渲染文章标题
 item.mytitle = `
  <div>
      <Link to="/" className='table_title'>${item.title}</Link>
      <p style={{color:'#999'}}>${item.subTitle}</p>
  </div>
 `;
 
  render: text => <div dangerouslySetInnerHTML={{__html:text}}></div>

记得修改

dataIndex: 'mytitle',
key: 'mytitle',
  • 实现效果

image.png

更换更好的渲染标题的方法

创建一个myArr数组保存对象obj。每次遍历newArr的时候就创建一个obj。通过props属性将获得的标题传递给MyTitle组件。

将mytitle的值改写为MyTitle组件 记得去掉$ 和修改props。

function MyTitle(props) {
  return (
    <div>
        <Link to="/" className='table_title'>{props.title}</Link>
        <p style={{color:'#999'}}>{props.subTitle}</p>
    </div>
  )
}

创建对象obj

   // 声明一个空数组
   let myarr = []
   newArr.map(item => {
    let obj = {
        key: item.id,
        date: moment(item.date).format("YYYY-MM-DD hh:mm:ss"),
        mytitle: <MyTitle id={item.id} title={item.title} subTitle={item.subTitle} />
    }
    myarr.push(obj)
})
setArr(myarr)

// 注意在对应的列中更改渲染
    render: text =>  <div>{text}</div>
  • 实现效果图

image.png

a标签跳转

将Link标签修改为a标签

添加href

跳转新窗口 target=“_blank”

<a to="/" className='table_title' href={"http://codesohigh.com:8765/article/" + props.id} target="_blank">{props.title}</a>

id的获取

先输出text,看打印内容是什么

代码显示

    render: (text, record) => {
      console.log(text)
      return (
        <Space size="middle">
        <Button type='primary' >编辑</Button>
        <Button type='danger'>删除</Button>
        </Space>
      )
    }
  • 操作台显示

image.png

使用点击事件获取id值(text.key就是我们需要的id)

        <Button type='primary' onClick={()=>console.log(text.key)}>编辑</Button>
        <Button type='danger' onClick={()=>console.log(text.key)}>删除</Button>
  • 实现效果图(点击按钮输出id到控制台)

image.png

封装请求文章

将useEffect内部的代码全部剪切到新定义的getArticleList的函数内部。

  // 提取请求的代码
  const getArticleList = ()=> {
      ArticleListApi().then(res=>{
        if(res.errCode === 0) {
          let newArr = JSON.parse(JSON.stringify(res.data.arr))
              /* 
                  1. 要给每个数组项加key,让key=id
                  2. 需要有一套标签结构,赋予一个属性
              */
            // 声明一个空数组
            let myarr = []
            newArr.map(item => {
              let obj = {
                  key: item.id,
                  date: moment(item.date).format("YYYY-MM-DD hh:mm:ss"),
                  mytitle: <MyTitle id={item.id} title={item.title} subTitle={item.subTitle} />
              }
              myarr.push(obj)
          })
          setArr(myarr)
        }
    })
  }


  // 请求文章列表
  useEffect(() => {
    getArticleList();
  }, []);

分页函数

  • 分页传送门

https://ant.design/components/pagination-cn/#API

在Table标签中添加onChange事件

<Table
columns={columns} 
showHeader = {false}
 dataSource={arr}
 onChange={pageChange}
 />

书写分页函数

// 分页的函数
const pageChange = (pagination) => {
  console.log(pagination)
}

点击换页标签时,操作台会输出以下内容

image.png

  • 在Table标签中pagination

image.png

使用useState设置分页

  //分页
  const [pagination,setPagination] = useState({current:1,pageSize:1,total:0})
       <Table
        columns={columns} 
        showHeader = {false}
         dataSource={arr}
         onChange={pageChange}
         pagination={pagination}
         />
  • 请求十条数据

为请求函数设置参数,传入current,pageSize

设置形参

  // 提取请求的代码
  const getArticleList = (current,pageSize)=> {
      ArticleListApi({
        num:current,
        count:pageSize
      }).then(res=>{
        if(res.errCode === 0) {
          let newArr = JSON.parse(JSON.stringify(res.data.arr))
              /* 
                  1. 要给每个数组项加key,让key=id
                  2. 需要有一套标签结构,赋予一个属性
              */
            // 声明一个空数组
            let myarr = []
            newArr.map(item => {
              let obj = {
                  key: item.id,
                  date: moment(item.date).format("YYYY-MM-DD hh:mm:ss"),
                  mytitle: <MyTitle id={item.id} title={item.title} subTitle={item.subTitle} />
              }
              myarr.push(obj)
          })
          setArr(myarr)
        }
    })
  }

调用getArticleList() 传入实参

  // 请求文章列表
  useEffect(() => {
    getArticleList(pagination.current,pagination.pageSize);
  }, []);

实现效果

image.png

换页按钮变为了一页,原因没有设置总条数。

找到分页的total属性

image.png

注意:这里视频突然发现Table有total属性。这里再度尝试输出res.data,看是否返回数据有total。

image.png

        console.log(res.data)

image.png

请求数据之后更改pagination

 // 更改pagination
let { num, count, total } = res.data;
setPagination({ current: num, pageSize: count, total })

点击分页按钮后会返回一串数据

image.png

在分页函数中调用getArticleList封装请求函数(更新点击就调用getArticleList)

// 分页的函数
const pageChange = (arg) => getArticleList(arg.current, arg.pageSize);

滚动样式

想要实现的效果

image.png

实际存在的问题,内容超出

image.png

  • 给面包屑设置高度
<Breadcrumb style={{height: '30px',background:'red', lineHeight: '30px'}}>

image.png

  • 修改整体页面高度

在base.less中

.container_content{
    height: calc(100vh - 210px);
    overflow: hidden;
}

App组件

function App() {
    return (
        <Layout id='app'>
            <Header/>
        <div className='container'>
         <Aside />       
        <div className='container_box'>
            <Bread/>
            <div className="container_content">
            <Outlet />
          </div>
        </div>
      </div>  
       <footer>Respect | Copyright &copy; 2022 Author 你单排吧</footer>
      </Layout>
    );
}

在ListTable.less中添加滚动

.list_table{
    width: 100%;
    background: #fff;
    height: 100%;
    overflow-y: scroll;
    &::-webkit-scrollbar {
        /*滚动条整体样式*/
        width: 10px;
        height: 100%;
        background: #fff;
        border-radius: 10px;
    }
    
    &::-webkit-scrollbar-track {
        /*滚动条里面轨道*/
        box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
        border-radius: 10px;
        background: #EDEDED;
    }
    
    &::-webkit-scrollbar-thumb {
        /*滚动条里面小方块*/
        border-radius: 10px;
        box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
        background: #535353;
    }
    .table_title{
        color: #333;
        &:hover{
            color: #1890ff;
        }
    }
}

注意记得把那个面包屑背景颜色去掉!!!

  • 最终实现效果

image.png

列表组件引入

  • 借用ListTable的css样式

直接在div盒子上添加对应属性

    <div className='list_table'>ListList</div>

列表传送门

https://ant.design/components/list-cn/

  • 复制对应代码
import { List, Avatar, Button, Skeleton } from 'antd';

const count = 3; const fakeDataUrl = `https://randomuser.me/api/?results=${count}&inc=name,gender,email,nat,picture&noinfo`;

state = { initLoading: true, loading: false, data: [], list: [], };


  const { initLoading, loading, list } = this.state;
  const loadMore =
    !initLoading && !loading ? (
      <div
        style={{
          textAlign: 'center',
          marginTop: 12,
          height: 32,
          lineHeight: '32px',
        }}
      >
        <Button onClick={this.onLoadMore}>loading more</Button>
      </div>
    ) : null;



    <div className='list_table'>
        <List
        className="demo-loadmore-list"
        loading={initLoading}
        itemLayout="horizontal"
        loadMore={loadMore}
        dataSource={list}
        renderItem={item => (
          <List.Item
            actions={[<a key="list-loadmore-edit">edit</a>, <a key="list-loadmore-more">more</a>]}
          >
            <Skeleton avatar title={false} loading={item.loading} active>
              <List.Item.Meta
                avatar={<Avatar src={item.picture.large} />}
                title={<a href="https://ant.design">{item.name.last}</a>}
                description="Ant Design, a design language for background applications, is refined by Ant UED Team"
              />
              <div>content</div>
            </Skeleton>
          </List.Item>
        )}
      />
    </div>

修改代码

  • 使用useState
// 引入useState
import React,{useState} from 'react'

将List转换就行其余删掉

const [list, setList] = useState([])

删除 fakeDataUrl 、count

删除List中的initLoading、loadMore、 avatar

image.png

image.png

  • 最后代码
import React,{useState} from 'react'
import { List,Skeleton } from 'antd';


export default function ListList() {

  const [list, setList] = useState([])


  return (
    <div className='list_table'>
        <List
        className="demo-loadmore-list"
        itemLayout="horizontal"
        dataSource={list}
        renderItem={item => (
          <List.Item
            actions={[<a key="list-loadmore-edit">edit</a>, <a key="list-loadmore-more">more</a>]}
          >
            <Skeleton  title={true} loading={item.loading} active>
              <List.Item.Meta
                title={<a href="!#">标题</a>}
                description="副标题"
              />
              <div>日期</div>
            </Skeleton>
          </List.Item>
        )}
      />
    </div>
  )
}

实现效果

image.png

使用useEffect发送请求

// 引入useEffect
import React,{useState,useEffect} from 'react'

// 引入api
import { ArticleListApi } from '../request/api';

  // 请求列表数据
  useEffect(()=>{
      ArticleListApi().then(res=>{
        console.log(res.data.arr)
        if(res.errCode === 0) {
          let {arr,total,num,count} = res.data;
          setList(arr)
        }
      })
  },[])
  • 渲染数据
 <Skeleton  loading={false}>
              <List.Item.Meta
                title={<a href="!#">{item.title}</a>}
                description={item.subTitle}
              />
              <div>{item.date}</div>
            </Skeleton>
  • 实现效果

image.png

  • 将标题挤开部分
style={{ padding: '20px' }}

image.png

  • 设置分页

引入分页

import { List,Skeleton,Pagination } from 'antd';

使用API

<Pagination onChange={onChange} total={50} />

image.png

书写onChange事件

    // 分页
    const onChange = (pages) => {
      console.log(pages)
    }

使用useState设置total、current、pageSize

  const [total, setTotal] = useState(0)
  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)

在Pagination中可以直接使用total、current、pageSize

<Pagination onChange={onChange} total={50} total={total} current={current} pageSize={pageSize} />

请求更新total、current、pageSize

  // 请求列表数据
  useEffect(()=>{
      ArticleListApi({
        num: current,
        count: pageSize
      }).then(res=>{
        console.log(res.data.arr)
        if(res.errCode === 0) {
          let {arr,total,num,count} = res.data;
          setList(arr);
          setTotal(total);
          setCurrent(num);
          setPageSize(count)
        }
      })
  },[])
  • 请求封装
  // 请求封装
  const getList = (num) => {
    ArticleListApi({
      num: num,
      count: pageSize
    }).then(res=>{
      console.log(res.data.arr)
      if(res.errCode === 0) {
        let {arr,total,num,count} = res.data;
        setList(arr);
        setTotal(total);
        setCurrent(num);
        setPageSize(count)
      }
    })
  }
  • 书写点击事件
  // 分页
  const onChange = (pages) => {
    getList(pages);
  }

将分页按钮书写到右边

style={{float: 'right',marginTop: '20px'}}

日期域按钮

日期

// 引入moment
import moment from 'moment'

//规范时间
<div>{moment(item.date).format("YYYY-MM-DD hh:mm:ss")}</div>

按钮

// 引入button

import { ArticleListApi } from '../request/api';

  actions={[
    <Button type='primary' onClick={()=>console.log(item.id)}>编辑</Button>, 
    <Button type='danger' onClick={()=>console.log(item.id)}>删除</Button>
  ]}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6saeGWrN-1651547144225)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f34bc508b8d44b6ea2a8e60cc1f20944~tplv-k3u1fbpfcp-watermark.image?)]

文章编辑页面

PageHeader页头

传送门

https://ant.design/components/page-header-cn/#header

引入

import { PageHeader, Button } from 'antd';

复制页头去掉描述

    <PageHeader
        ghost={false}
        onBack={() => window.history.back()}
        title="Title"
        subTitle="This is a subtitle"
        extra={[
          <Button key="3">Operation</Button>,
          <Button key="2">Operation</Button>,
          <Button key="1" type="primary">
            Primary
          </Button>,
        ]}
      >
  </PageHeader>

修改按钮

extra={
  <Button key="1" type="primary">
    提交文章
  </Button>}

设置时间和标题

引入moment

import moment from 'moment'

修改时间

subTitle={"当前日期:" + moment(new Date()).format("YYYY-MM-DD")}

修改标题

        title="文章编辑"
  • 实现效果

image.png

  • 使用wangEditor

安装依赖

npm i wangeditor  --save

引入对象E

import E from 'wangeditor'

创建对象实例放入div盒子

使用useEffect

import React,{useEffect} from 'react'

// 模拟componentDidMount
useEffect(()=>{
  const editor = new E('#div1');
  editor.create()
},[])

<div id="div1"></div>

image.png

  • 富文本编辑器

改为外界声明editor

let editor = null;

书写editor的onChange函数

  editor.config.onchange = (newHtml) => {
    SVGTextContentElement(newHtml)
  }

销毁editor

  return () => {
      editor.destroy()
  }

注意使用useState创建content。

import React, { useEffect, useState } from 'react'

 const [content, setContent] = useState('')

为文本编辑框设置边距

 style={{ padding: '0 20px 20px', background: '#fff' }}

设置对话框

对话框传输门

https://ant.design/components/modal-cn/#header

引入Modal

import { Modal, Button } from 'antd';

设置button

  <Button key="1" type="primary" onClick={showModal}>
    提交文章
  </Button>}

添加方法和模块


  const showModal = () => {
    setIsModalVisible(true);
  };

  const handleOk = () => {
    setIsModalVisible(false);
  };

  const handleCancel = () => {
    setIsModalVisible(false);
  };

<Modal title="Basic Modal" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>

处理层级问题

在Modal设置zIndex={99999}

<Modal zIndex={99999} title="Basic Modal" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>

修改标题

image.png

 title="填写文章标题"

简化函数代码

onClick={() =>  setIsModalVisible(true)}


onCancel={() =>  setIsModalVisible(false)}

Form表单传送门

https://ant.design/components/form-cn/

引入Form、Input

import {Form, Input, PageHeader, Button ,Modal} from 'antd';

//在Model下添加表单代码
  <Form
  name="basic"
  labelCol={{ span: 3 }}
  wrapperCol={{ span: 21 }}
  autoComplete="off"
>
  <Form.Item
    label="标题"
    name="title"
    rules={[{ required: true, message: '请填写标题!' }]}
  >
    <Input />
  </Form.Item>

  <Form.Item
    label="副标题"
    name="subTitle"
  >
    <Input />
  </Form.Item>
</Form>

对话框获取表单的值

Form表单弹出层传送门

https://ant.design/components/form-cn/#components-form-demo-form-in-modal

 const [form] = Form.useForm();

Form表单上添加属性

          form={form}

对话框点击了提交

  // 对话框点击了提交
  const handleOk = () => {
    // setIsModalVisible(false); // 关闭对话框
    form
    .validateFields()
    .then(values => {
      form.resetFields();
      onCreate(values);
    })
    .catch(info => {
      console.log('Validate Failed:', info);
    });
  };

删除onCreate(values)、修改ok函数

  // 对话框点击了提交
  const handleOk = () => {
    // setIsModalVisible(false); // 关闭对话框
    form
    .validateFields()
    .then(values => {
      form.resetFields();
    })
    .catch(() => {
      return ;
    });
  };

  • 实现效果

image.png

onOk添加onText、cancelText

okText="提交" cancelText="取消"

image.png

发送文章请求

  • 添加token
  let token = localStorage.getItem('cms-token')
  if(token){
    config.headers = {
      'cms-token': token
    }
  }

image.png

  • 书写api
// 添加文章
export const ArticleAddApi = (params) => request.post('/article/add', params)

引入api

import {ArticleAddApi } from '../request/api'

验证content是否取到

    // setIsModalVisible(false); // 关闭对话框
    form
    .validateFields()
    .then(values => {
      // form.resetFields(); // reset重置
      console.log('Received values of form: ', values);
      let {title,subTitle} = values
      console.log(content)
      
    })
    .catch(() => {
      return ;
    });

image.png

image.png

-发送请求

  // 请求
  ArticleAddApi({title,subTitle,content}).then(res => {
    console.log(res)
  })

实现效果

image.png

编辑id控制 实现页面跳转

在ListList中

引入useNavigate

import {  useNavigate, } from 'react-router-dom'

const navigate = useNavigate()

在onClick中实现路由跳转

  actions={[
    <Button type='primary' onClick={()=>navigate('/edit/'+item.id)}>编辑</Button>, 
    <Button type='danger' onClick={()=>console.log(item.id)}>删除</Button>
  ]}
  • 修改路由文件index

是添加!不是修改

<Route path='/edit' element={<Edit />}></Route>
<Route path='/edit/:id' element={<Edit />}></Route>
  • 实现效果

image.png

  • 实现只有路径带有id值是才会有返回箭头

引入useParams

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

const params = useParams()

书写onBack

 onBack={ params.id ? () => window.history.back() : null}

-实现效果

image.png

wangeditor 内容渲染

  • 书写查看文章api
// 查看文章
export const ArticleSearchApi = (params) => request.get(`/article/${params.id}`)

引入查看文章api

import {ArticleAddApi,ArticleSearchApi } from '../request/api'

根据地址栏id做请求

// 根据地址栏id做请求
if(params.id) {
  ArticleSearchApi({
    id:params.id
  }).then(res=>{
    if(res.errCode === 0) {
      let {title,subTitle} = res.data;
      editor.txt.html(res.data.content) // 重新设置编辑器内容
    }
  })
}
  • 实现效果(点击编辑之后会跳转到编辑页面,并且显示出文本内容)

image.png

  • 设置title、subTitle
const [title, setTitle] = useState('')
const [subTitle, setSubTitle] = useState('')
  
  
setTitle(res.data.title)
setSubTitle(res.data.subTitle)
  • 为表单添加初始值

在Form 标签中使用该属性

      initialValues={{title:title,subTitle:subTitle}}
  • 实现效果

image.png

修改更新文章

书写更新api

// 重新编辑文章
export const ArticleUpdateApi = (params) => request.put('/article/update', params)

调用api

  // 地址栏有id代表现在想要更新一篇文章
  if(params.id) {
    ArticleUpdateApi({title,subTitle,content}).then(res => {
      console.log(res)
    })
  }else {
      // 添加文章的请求
    ArticleAddApi({title,subTitle,content}).then(res => {
      console.log(res)
    })
  }
  • 实现效果

image.png

  • 使用message提示修改成功并且跳转页面
  // 地址栏有id代表现在想要更新一篇文章
  if(params.id) {
    ArticleUpdateApi({title,subTitle,content}).then(res => {
      if(res.errCode === 0) {
        message.success(res.message);
        //跳转到list页面
        navigate('/listlist')
      }else{
        message.error(res.message)
      }
      setIsModalVisible(false) // 关闭对话框
    })
  }else {
      // 添加文章的请求
    ArticleAddApi({title,subTitle,content}).then(res => {
      console.log(res)
    })
  }

解决bug

点击编辑一篇文章之后,再次点击菜单栏,文章编辑。页面的路径更改但是文本框并没有清除。

image.png

解决方案:监听路由的变化

引入location

import { useParams, useNavigate, useLocation } from 'react-router-dom'

const location = useLocation()
  

image.png

  • Aside监听路由
    // 一般加个空数组就是为了模仿componentDidMounted
    useEffect(()=>{
      let path = location.pathname;
      let key = path.split('/')[1];
      setDefaultKey(key)
  }, [location.pathname])
  • 封装函数(使用message提示修改成功并且跳转页面)

删除文章

书写删除api

// 删除文章
export const ArticleDelApi = (params) => request.post('/article/remove', params)

在ListList中引入api

import { ArticleListApi ,ArticleDelApi} from '../request/api';

在button中修改点击事件

<Button type='danger' onClick={()=>delFn(item.id)}>删除</Button>

书写删除函数delFn

重新刷页面,要么重新请求这个列表的数据 window.reload 调用getList(1) 增加变量的检测

  // 删除
  const delFn = (id) => {
    ArticleDelApi({id}).then(res=>{
      if(res.errCode===0){
        message.success(res.message)
        // 重新刷页面,要么重新请求这个列表的数据   window.reload   调用getList(1)  增加变量的检测
        setUpdate(update+1)
      }else{
        message.success(res.message)
      }
    })
  }

监听刷新页面

const [update, setUpdate] = useState(1)

image.png

  • 为Table添加编辑、删除功能

注意引入的内容和函数(太冗杂不写了,傲娇~)

用户资料表单布局

书写类名,设置样式

import "./less/Means.less"

import React from 'react'

export default function Means() {
  return (
    <div className='means'>Means</div>
  )
}


.means{
    background: #fff;
    height: 100%;
    padding: 20px;
    box-sizing: border-box;
}
  • 实现效果

image.png

  • 引入form表单(设置宽度、修改按钮,修改宽高)
import React from 'react'
import { Form, Input,Button} from 'antd';
import "./less/Means.less"

export default function Means() {
  return (
    <div className='means'>
    <Form
    style={{width: '400px'}}
    name="basic"
    initialValues={{
      remember: true,
    }}

    autoComplete="off"
  >
    <Form.Item
      label="修改用户名"
      name="username"
      rules={[
        {
          required: true,
          message: 'Please input your username!',
        },
      ]}
    >
    <Input placeholder='请输入新用户名' />
    </Form.Item>

    <Form.Item
      label="修 改 密 码"
      name="password"
    >
    <Input.Password placeholder='请输入新密码' />
    </Form.Item>

    <Form.Item>
    <Button type="primary" htmlType="submit" style={{float: 'right'}}>提交</Button>
  </Form.Item>

  </Form>
    </div>
  )
}

获取用户请求

  • 书写api
// 获取用户资料
export const GetUserDataApi = () => request.get('/info')

import {GetUserDataApi} from '../request/api'

使用useEffect、更新请求用户,设置初始值。

  const [username1,setUsername1] = useState("");
  const [password1,setPassword1] = useState("")

  useEffect(() => {
      GetUserDataApi().then(res => {
        console.log(res)
        if(res.errCode === 0) {
          message.success(res.message);
          setUsername1(res.data.username);
          setPassword1(res.data.Password);
        }
      })
  }, []);
  • 实现效果

image.png

修改用户资料

  • 书写api
// 修改用户资料
export const ChangeUserDataApi = (params) => request.put('/info', params)

  • 引入
import {GetUserDataApi, ChangeUserDataApi} from '../request/api'
  • 发送请求
  // 表单提交的事件
  const onFinish = (values) => {
    // 如果表单的username有值,并且不等于初始化时拿到的username,同时密码非空
    if(values.username && values.username!==sessionStorage.getItem('username') && values.password.trim() !== ""){
      // 做表单的提交...
      ChangeUserDataApi({
        username: values.username,
        password: values.password
      }).then(res=>{
        console.log(res)
        // 当你修改成功的时候,不要忘了重新登录
      })
    }
  }

当你修改成功的时候,不要忘了重新登录!

Upload引入

添加标题

      <p>点击下方修改头像:</p>

Upload上传

传送门

https://ant.design/components/upload-cn/#header

upload组件中直接书写请求体

书写action接口

action="/api/upload"

上传前 beforeUpload

// 限制图片大小只能是200KB
function beforeUpload(file) {
  const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
  if (!isJpgOrPng) {
    message.error('You can only upload JPG/PNG file!');
  }
  const isLt2M = file.size / 1024 / 1024 / 1024  < 200;
  if (!isLt2M) {
    message.error('请上传小于200KB的图!');
  }
  return isJpgOrPng && isLt2M;
}
  • 使用useState改写loading、imageUrl
import React, { useEffect, useState } from 'react'

  const [loading, setLoading] = useState(false)
  const [imageUrl, setImageUrl] = useState("")
  • 添加handleChange,并且修改

image.png

  // 点击了上传图片
 const  handleChange = info => {
    if (info.file.status === 'uploading') {
       setLoading(true);
      return;
    }
    if (info.file.status === 'done') {
      // Get this url from response in real world.
      getBase64(info.file.originFileObj, imageUrl => {
        setLoading(false)
        setImageUrl(imageUrl)       
      }
      );
    }
  };
  • 函数调用修改

image.png

引入base64函数

// 将图片路径改位base64
function getBase64(img, callback) {
  const reader = new FileReader();
  reader.addEventListener('load', () => callback(reader.result));
  reader.readAsDataURL(img);
}

注意引入

import { Form, Input,Button, message,Upload} from 'antd';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
  • 实现效果

image.png

Upload组件添加请求头

headers={{"cms-token": localStorage.getItem('cms-token')}}

image.png

  • 存储图片名称

这里需要打印info.file找到上传图片的名称。

// 存储图片名称
localStorage.setItem('avatar', info.file.response.data.filePath)

更新Header组件

使用useState设置mykey

    <Layout id='app'>
        <Header key={mykey} />
    <div className='container'>
     <Aside />       
    <div className='container_box'>
        <Bread/>
        <div className="container_content">
        <Outlet  setMyKey={setMyKey} />
      </div>
    </div>
  </div>  
   <footer>Respect | Copyright &copy; 2022 Author 你单排吧</footer>
  </Layout>

image.png

Means组件

注意记得函数接收产生props

  // 点击了上传图片
 const  handleChange = info => {
    if (info.file.status === 'uploading') {
       setLoading(true);
      return;
    }
    if (info.file.status === 'done') {
      // Get this url from response in real world.
      getBase64(info.file.originFileObj, imageUrl => {
        setLoading(false)
        setImageUrl(imageUrl)       
        // 存储图片名称
        localStorage.setItem('avatar', info.file.response.data.filePath)
        // 触发Header组件更新
        props.setMyKey(props.myKey+1)
      }
      );
    }
  };

image.png

添加强制刷新

window.location.reload()

使用react-redux

安装react-redux

yarn add react-redux
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值