写在前头
这个项目是跟着b站up主 单排哥 学习的。
传送门
https://www.bilibili.com/video/BV1ta411b7eo?p=1
具体功能
- 查看文章
- 文章编辑
- 修改资料
一、项目介绍
《CMS后台系统》项目主要是为CMS官网提供文章编辑、上传等功能。其中包括富文本编辑器调用、路由管理、权限管控、图片上传等主体模块。
项目预览路径:http://codesohigh.com/cms-manage/
UI框架使用:Ant Design
二、项目起步
1、创建项目
$ npx create-react-app cms-manage
在一个文件夹目录下点击,输入cmd,进去终端命令框,然后使用上面命名,进行项目创建。
显示以下类似的文字就是表面项目已经创建成功。
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中打开终端使用。
3、Antd引入和测试
Ant Design 传送门
https://ant.design/index-cn
- 首先将src文件夹下的所有文件删除
在像图片下面一样新建文件夹及文件。
- 在bass.css中引入antd。
@import '~antd/dist/antd.css';
- 初始化App.jsx
import React from 'react';
import "./assets/base.css"
const App = () => {
return (
<div>
App
</div>
);
}
export default App;
- index.js
import ReactDOM from 'react-dom'
ReactDOM.render(
<Router />,
document.getElementById('root')
)
- 测试antd
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;
- 测试成功
4、路由配置
- 安装插件
- 创建页面pages
在每一个jsx文件下使用rfc代码片段快速生成函数组件。(注意:需要安装插件)
import React from 'react'
export default function Edit() {
return (
<div>Edit</div>
)
}
- 新建router文件夹,书写index.jsx文件
/*
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修改顶级组件
import ReactDOM from 'react-dom'
import Router from "./router"
ReactDOM.render(
<Router />,
document.getElementById('root')
)
- 更改路径可以检查是否成功
在App.js组件中使用Outlet
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;
- 检查成功?
#5.解包配置Less
1、解包并配置Less
- 在项目根目录下运行解包命令:
# 解包前必须做git提交,否则无法解包(这是为了方便随时做版本回滚)
$ git init
$ git add .
$ git commit -m '解包前'
$ npm run eject
- 解包之后,项目根目录下将出现config目录,找到webpack.config.js,搜索
sassModuleRegex
后,在其下方添加:
{
test: /\.less$/,
use: getStyleLoaders(
{
//暂不配置
},
'less-loader'
),
},
修改了配置文件,记得重跑项目哦!
- 测试Less
删除入口文件index.js下对antd.css的引入,然后在src下创建 assets>base.less:
@import '~antd/dist/antd.css'; // 有个波浪线
@bgcolor: pink;
body{
background: @bgcolor;
}
此时看网页是否按钮正常,并且body背景变为粉色。若是,则Less配置成功。
三、页面布局
登录页布局
- 基本样式
- 修改页面背景颜色
@import '~antd/dist/antd.css';
@bgcolor: #efefef;
body{
font-family: "微软雅黑";
font-size: 14px;
color: #333;
background: @bgcolor;
}
- 删除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;
- 进入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>
)
}
- 改变路径检查是否成功。
添加类名
<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%);
}
}
- 引入图片
在src-> assets 下添加logo图片
在login组件添加图片
- 引入图片
import logoImg from '../assets/logo.png'
- 设置图片标签
<div className='login_box'>
- 设置图片属性
img {
display: block;
margin: 0 auto 20px;
}
- 改写表单
将复制的代码删除部分不需要的,得到下方代码
<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>
- 最终实现效果图
注册页布局
-
将登录的代码复制到注册页面
-
添加验证密码框
<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>
- 实现效果
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
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'
- 在 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进行修改
// 配置项
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);
}
})
};
- 跳转路由是使用的hook。
import {Link, useNavigate} from 'react-router-dom'
if(res.errCode===0){
message.success(res.message);
// 跳到登录页
setTimeout(()=>navigate('/login'), 1500)
}
- 未注册
- 已注册
登录
- 在api.js 下面添加登录请求
// 登录
export const LoginApi = (params) => request.post('/login', params)
- 在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)
})
};
- 使用条件语句判断是否成功
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)
}
})
};
- 最终效果图
App布局
布局传送门
https://ant.design/components/layout-cn/
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;
}
实现效果图
- 导入logo图片
import logoImg from '../assets/logo.png'
<img src={logoImg} alt="" className="logo" />
实现效果图
- 给logo图片设置间距
header {
height: 70px;
background-color: #fff;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
实现效果图
- 底边框可以随着F12的操作框动态移动
App.jsx
<Layout id='app'>
<header>
<img src={logoImg} alt="" className="logo" />
</header>
<Layout>
base.less
#app {
height: 100vh;
}
实现效果图
下拉菜单
- 将header抽出成为一个Header组件。
在components下面创建Header文件
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>
- 下拉菜单
传送门(记得使用3.x的版本,不然没有Menu.Item)
https://3x.ant.design/components/dropdown-cn/
复制代码
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>
)
}
效果展示
- 进行修改
添加阴影线
<Menu.Divider />
const menu = (
<Menu>
<Menu.Item>
修改资料
</Menu.Item>
<Menu.Divider />
<Menu.Item>
退出登录
</Menu.Item>
</Menu>
);
- 修改图标
传送门
https://3x.ant.design/components/icon-cn/
import { CaretDownOutlined } from '@ant-design/icons';
<CaretDownOutlined />
效果展示
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='' />
添加默认用户名
// 使用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>
);
- 实现效果图
修改样式
在base.less 下书写样式
.right {
height: 40px;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
span {
margin-left: 10px;
margin-right: 10px;
}
}
- 实现效果图
修改下拉菜单边距
- 原样
在base.less中书写对应a标签的属性
.ant-dropdown-link{
height: 60px;
display: block;
color: #333;
&:hover{
color: #1890ff;
}
}
- 实现效果图
- 使用useEffect
在Application中会找到请求存储的数据。
我们将使用这些数据替代默认的用户名和用户头像。
- 用户名
// 模拟componentDidMount
useEffect(()=>{
let username1 = localStorage.getItem('username')
if(username1){
setUsername(username1)
}
},[])
- 实现效果图
- 用户头像
// 模拟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)
}
},[])
注意:传入图片时,记得添加路径。
- 实现效果图
退出登录
不可以直接使用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>
- 实现效果
侧边栏布局
- 创建Aside组件
- 修改App组件
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;
- 实现效果图
- 为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;
}
}
}
-实现效果
添加图标
<Menu.Item key="1"><ReadOutlined /> 查看文章列表</Menu.Item>
<Menu.Item key="2"><EditOutlined /> 文章编辑</Menu.Item>
<Menu.Item key="3"><DatabaseOutlined /> 修改资料</Menu.Item>
在文字和图标紧凑在一起的时候,我们需要可以敲进一个空格来使得排版更加美观。
- 实现效果图
点击实现路由跳转
- 修改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']}
- 实现效果
遇到的bug
刷新之后路径并未改变,菜单栏改变了。
- 片段代码
引入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)
};
- 实现效果图(自己刷新试试咯)
面包屑
传送门
https://ant.design/components/breadcrumb-cn/#header
- 创建Bread组件
初始化书写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个路由组件
- 配置路由(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>
- 实现效果
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文件
.list_table{
width: 100%;
background: #fff;
height: 100%;
}
- 引入less样式到ListTable
import './less/ListTable.less'
// 不要忘记添加类名
<div className='list_table'>ListTable</div>
- 实现效果图
表格结构搭建
传送门
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>
- 隐藏表头
- 在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>
),
},
];
- 显示效果
- 渲染标题副标题
在第一列下面修改
{
dataIndex: 'name',
key: 'name',
render: text => (
<>
<h4>标题</h4>
<p>简直是大家</p>
</>
),
},
- 渲染时间
在第二列添加渲染
{
dataIndex: 'address',
key: 'address',
render: text => (
<p>
2022-03-03 20:33:06
</p>
)
},
- 改变第一列的宽度
使用width属性
- 修改副标题颜色
{
dataIndex: 'name',
key: 'name',
width:'60%',
render: text => (
<>
<h4>标题</h4>
<p style={{color:'#999'}}>简直是大家</p>
</>
),
},
在p标签中添加style属性
<p style={{color:'#999'}}>简直是大家</p>
- 实现效果
- 修改标题实现跳转
引入Link
import {Link} from 'react-router-dom'
修改标签
<Link to="/" className='table_title'>标题</Link>
// 标题样式
.table_title{
color: #333;
&:hover {
color: #1890ff;
}
}
- 使用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)
}
})
}, []);
- 工作台
列表渲染
-
使用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',
- 实现效果
更换更好的渲染标题的方法
创建一个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>
- 实现效果图
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>
)
}
- 操作台显示
使用点击事件获取id值(text.key就是我们需要的id)
<Button type='primary' onClick={()=>console.log(text.key)}>编辑</Button>
<Button type='danger' onClick={()=>console.log(text.key)}>删除</Button>
- 实现效果图(点击按钮输出id到控制台)
封装请求文章
将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)
}
点击换页标签时,操作台会输出以下内容
- 在Table标签中pagination
使用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);
}, []);
实现效果
换页按钮变为了一页,原因没有设置总条数。
找到分页的total属性
注意:这里视频突然发现Table有total属性。这里再度尝试输出res.data,看是否返回数据有total。
console.log(res.data)
请求数据之后更改pagination
// 更改pagination
let { num, count, total } = res.data;
setPagination({ current: num, pageSize: count, total })
点击分页按钮后会返回一串数据
在分页函数中调用getArticleList封装请求函数(更新点击就调用getArticleList)
// 分页的函数
const pageChange = (arg) => getArticleList(arg.current, arg.pageSize);
滚动样式
想要实现的效果
实际存在的问题,内容超出
- 给面包屑设置高度
<Breadcrumb style={{height: '30px',background:'red', lineHeight: '30px'}}>
- 修改整体页面高度
在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 © 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;
}
}
}
注意记得把那个面包屑背景颜色去掉!!!
- 最终实现效果
列表组件引入
- 借用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
- 最后代码
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>
)
}
实现效果
使用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>
- 实现效果
- 将标题挤开部分
style={{ padding: '20px' }}
- 设置分页
引入分页
import { List,Skeleton,Pagination } from 'antd';
使用API
<Pagination onChange={onChange} total={50} />
书写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="文章编辑"
- 实现效果
- 使用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>
- 富文本编辑器
改为外界声明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}>
修改标题
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 ;
});
};
- 实现效果
onOk添加onText、cancelText
okText="提交" cancelText="取消"
发送文章请求
- 添加token
let token = localStorage.getItem('cms-token')
if(token){
config.headers = {
'cms-token': token
}
}
- 书写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 ;
});
-发送请求
// 请求
ArticleAddApi({title,subTitle,content}).then(res => {
console.log(res)
})
实现效果
编辑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>
- 实现效果
- 实现只有路径带有id值是才会有返回箭头
引入useParams
import { useParams } from 'react-router-dom'
const params = useParams()
书写onBack
onBack={ params.id ? () => window.history.back() : null}
-实现效果
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) // 重新设置编辑器内容
}
})
}
- 实现效果(点击编辑之后会跳转到编辑页面,并且显示出文本内容)
- 设置title、subTitle
const [title, setTitle] = useState('')
const [subTitle, setSubTitle] = useState('')
setTitle(res.data.title)
setSubTitle(res.data.subTitle)
- 为表单添加初始值
在Form 标签中使用该属性
initialValues={{title:title,subTitle:subTitle}}
- 实现效果
修改更新文章
书写更新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)
})
}
- 实现效果
- 使用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
点击编辑一篇文章之后,再次点击菜单栏,文章编辑。页面的路径更改但是文本框并没有清除。
解决方案:监听路由的变化
引入location
import { useParams, useNavigate, useLocation } from 'react-router-dom'
const location = useLocation()
- 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)
- 为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;
}
- 实现效果
- 引入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);
}
})
}, []);
- 实现效果
修改用户资料
- 书写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,并且修改
// 点击了上传图片
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)
}
);
}
};
- 函数调用修改
引入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';
- 实现效果
Upload组件添加请求头
headers={{"cms-token": localStorage.getItem('cms-token')}}
- 存储图片名称
这里需要打印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 © 2022 Author 你单排吧</footer>
</Layout>
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)
}
);
}
};
添加强制刷新
window.location.reload()
使用react-redux
安装react-redux
yarn add react-redux