《CMS后台系统》项目实战 详细分解
写在前头
这个项目是跟着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 + MeansLoginRegisterHistory模式 BrowserRouterHash模式 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><Formname="basic"labelCol={{ span: 8 }}wrapperCol={{ span: 16 }}initialValues={{ remember: true }}onFinish={onFinish}onFinishFailed={onFinishFailed}autoComplete="off"><Form.Itemlabel="Username"name="username"rules={[{ required: true, message: 'Please input your username!' }]}><Input /></Form.Item><Form.Itemlabel="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;}
- 改写表单
将复制的代码删除部分不需要的,得到下方代码
<Formname="basic"initialValues={{ remember: true }}onFinish={onFinish}onFinishFailed={onFinishFailed}autoComplete="off"><Form.Itemname="username"rules={[{ required: true, message: 'Please input your username!' }]}><Input /></Form.Item><Form.Itemname="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.Itemname="confirm"dependencies={['password']}hasFeedbackrules={[{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中会找到请求存储的数据。
我们将使用这些数据替代默认的用户名和用户头像。
- 用户名
// 模拟componentDidMountuseEffect(()=>{let username1 = localStorage.getItem('username')if(username1){setUsername(username1)}},[])
- 实现效果图
- 用户头像
// 模拟componentDidMountuseEffect(()=>{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 (<MenuonClick={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设置类名
<MenuonClick={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()
// 修改handleClickconst 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下面)
// 引入buttonimport { 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=id2. 需要有一套标签结构,赋予一个属性*/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=id2. 需要有一套标签结构,赋予一个属性*/// 声明一个空数组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})
<Tablecolumns={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=id2. 需要有一套标签结构,赋予一个属性*/// 声明一个空数组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 ? (<divstyle={{textAlign: 'center',marginTop: 12,height: 32,lineHeight: '32px',}}><Button onClick={this.onLoadMore}>loading more</Button></div>) : null;<div className='list_table'><ListclassName="demo-loadmore-list"loading={initLoading}itemLayout="horizontal"loadMore={loadMore}dataSource={list}renderItem={item => (<List.Itemactions={[<a key="list-loadmore-edit">edit</a>, <a key="list-loadmore-more">more</a>]}><Skeleton avatar title={false} loading={item.loading} active><List.Item.Metaavatar={<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'><ListclassName="demo-loadmore-list"itemLayout="horizontal"dataSource={list}renderItem={item => (<List.Itemactions={[<a key="list-loadmore-edit">edit</a>, <a key="list-loadmore-more">more</a>]}><Skeleton title={true} loading={item.loading} active><List.Item.Metatitle={<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.Metatitle={<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>
按钮
// 引入buttonimport { 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';
复制页头去掉描述
<PageHeaderghost={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下添加表单代码<Formname="basic"labelCol={{ span: 3 }}wrapperCol={{ span: 21 }}autoComplete="off"
><Form.Itemlabel="标题"name="title"rules={[{ required: true, message: '请填写标题!' }]}><Input /></Form.Item><Form.Itemlabel="副标题"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} = valuesconsole.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监听路由
// 一般加个空数组就是为了模仿componentDidMounteduseEffect(()=>{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'><Formstyle={{width: '400px'}}name="basic"initialValues={{remember: true,}}autoComplete="off"><Form.Itemlabel="修改用户名"name="username"rules={[{required: true,message: 'Please input your username!',},]}><Input placeholder='请输入新用户名' /></Form.Item><Form.Itemlabel="修 改 密 码"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
《CMS后台系统》项目实战 详细分解相关推荐
- 全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis)
全程配图超清晰的Springboot权限控制后台管理项目实战第二期(Springboot+shiro+mybatis+redis) 众所周知,作为一个后端新手学习者,通过项目来学习,增长项目经验,是一 ...
- 个人博客系统--项目实战
个人博客系统–项目实战 先附上设计文档和项目源码. 个人博客设计文档 Github 这项目大概做了十多天,其基本功能都实现了,因为没有美工的关系,界面可能有点丑,请谅解. 后台采用SSH技术,版本为: ...
- 儒猿秒杀季!互联网大厂的企业级风控系统项目实战
疯狂秒杀季:499元秒杀 原价 1899元 的 <互联网大厂的企业级风控系统项目实战> 今天 上午11点,仅 20 套,先到先得! === 课程介绍 === | SparkStreamin ...
- 电影购票系统项目实战
电影购票系统项目实战 电影购票系统简介.项目功能演示. 日志框架搭建.系统角色分析 首页.登录.商家界面.用户界面实现 商家功能-展示详情.影片上架.退出 商家功能-影片下架.影片修改 用户功能-展示 ...
- 【Spark】基于Spark的大型电商网站交互式行为分析系统项目实战
1.项目背景 (1)Spark在美团的实践 美团是数据驱动的互联网服务,用户每天在美团上的点击.浏览.下单支付行为都会产生海量的日志,这些日志数据将被汇总处理.分析.挖掘与学习,为美团的各种推荐.搜索 ...
- Java 游戏账号出租系统项目(超详细注解)
Java 游戏账号出租系统项目 一级目录为包.二级目录为类 注解详细 debug调试更清晰 AccountRentApp(main) package accountrentsystem; import ...
- java cms视频_【视频+源码】JAVA CMS系统项目实战
01 CMS系统功能需求简介02 如何采用用例分析方法来理解需求03 后台管理系统用例04 实现验证码的初步思路05 生成验证码06 判断验证码是否正确07 返回登录页面时,把刚刚输入的用户名和密码回 ...
- java cms视频_领航致远JAVA CMS系统项目实战 视频+源码精品实战项目
[课程内容] 01 CMS系统功能需求简介 02 如何采用用例分析方法来理解需求 03 后台管理系统用例 04 实现验证码的初步思路 05 生成验证码 06 判断验证码是否正确 07 返回登录页面时, ...
- 【笔记】Vue Element+Node.js开发企业通用管理后台系统——项目需求分析
文章目录 一.项目技术架构 1.三个应用 2.项目目标 二.技术难点分析 1.登录 2.电子书上传 3.电子书解析 4.电子书增删改 5.epub 电子书 三.Nginx 服务器搭建 1.安装 ngi ...
- 软件测试项目实战案例分解,跟着我一步一步操作【人力资源管理系统】
目录 一.引言 二.项目概述 三.平台.角色和权限 四.人资管理员 五.最后 一.引言 1.编写目的 本文档将列举实现人力资源管理系统(以下简称HR系统)所需要的全部功能,并对每个功能给出简单的描述. ...
最新文章
- zb如何导出自己画的_zbrush纹理贴图(zbrush怎么导出映射贴图)
- xrdp安装包linux,linux xrdp0.6 安装
- 小而美的个人博客——前端——about
- android不能使用udp获取数据解决
- DevOps(过程、方法与系统的统称)是什么
- Joint Face Detection and Alignment using Multi-task Cascaded Convolutional Networks -译文
- 汇编语言第三章检测题
- chrome vue 未响应_vue之骨架屏踩坑之路
- Sklearn专题实战——针对Category特征进行分类
- Windowserver2012部署always on
- GitHub使用教程详解(上)——官网操作指南[翻译]
- 如何关掉 pyg解密小组声明窗口 (飘云阁番茄插件)
- 积极响应号召,ModStart支持用户主动注销账号功能
- Java基础之类加载器
- java和ssm开发的医院体检预约系统有论文
- 函数模板和类模板的使用
- 【信息安全】信息安全三要素CIA
- 40000字 Matplotlib 实操干货,真的全!
- 微型计算机ALE,微型计算机技术课后习题一二三章答案
- 如何证明地球是圆的呢
热门文章
- MySQL 一起重新认识下count(*) ,count(1),count(id)
- C语言日期计算器vs2022
- 在阿里云上设置CDN
- 【UIAutomator2】实现微信自动加好友功能
- 《卓有成效的管理者(The Effective Executive)》读后感
- 中国电信 CTExcel - 亲测境外首选电话卡(推荐码:SHQL 或 KJNC)
- 高德地图大头针功能_关于高德地图遇到的自定义大头针的坑
- 【在线教育直播】直播很卡怎么办?
- led大屏按实际尺寸设计画面_led显示屏尺寸大小的计算方式
- 路由器安全——破解wifi密码,同时中间人攻击