React实现后台管理系统(二)
一、页面tag实现
1、tag显示和兄弟组件传值
①tag显示效果实现
使用antd中的Tag组件实现,在components文件夹下新建tag/index.js以及index.css文件
tag/index.js:
import React, { useState } from 'react'
import { Tag, Space } from 'antd'
import './index.css'
const MyTag = props => {
return (
<Space className='tag-box' size={[0, 8]} wrap>
<Tag>首页</Tag>
<Tag closeIcon onClose={handleClose} color='#55acee'>
Prevent Default
</Tag>
</Space>
)
}
export default MyTag
tag/index.css:
.tag-box {
padding-top: 24px;
padding-left: 24px;
}
在layout中引入tag组件:
... ...
import MyTag from '../components/tag'
... ...
const MyLayout = () => {
... ...
return (
<Layout className='main-container'>
... ...
<MyHeader collapsed={collapsed} Collapsed={Collapsed} setCollapsed={setCollapsed} />
<MyTag></MyTag>
<Content
... ...
>
<Outlet />
</Content>
... ...
</Layout>
)
}
export default MyLayout
②使用redux实现兄弟组件传值
修改src/store/reducers/tab.js文件:
import { createSlice } from '@reduxjs/toolkit'
const tabSlice = createSlice({
name: 'tab',
initialState: {
... ...
tagList: [
{
path: '/',
name: 'home',
label: '首页'
}
]
},
reducers: {
... ...
selectTagList: (state, { payload: val }) => {
if (val.name !== 'home') {
//如果已经存在的tag就不添加
const res = state.tagList.findIndex(item => item.name === val.name)
if (res === -1) {
state.tagList.push(val)
}
}
}
}
})
export const { selectTagList } = tabSlice.actions
export default tabSlice.reducer
修改conponents/aside/index.js文件:
... ...
import { useDispatch } from 'react-redux'
import { selectTagList } from '../../store/reducers/tab'
... ...
const Aside = props => {
... ...
const dispatch = useDispatch()
//添加数据到store
const setTagList = val => {
dispatch(selectTagList(val))
}
... ...
const clickMenu = e => {
let data
menuList.forEach(item => {
if (item.path === e.keyPath[e.keyPath.length - 1]) {
data = item
//如果有二级菜单
if (e.keyPath.length > 1) {
data = item.children.find(child => {
return child.path === e.key
})
}
}
})
setTagList({
path: data.path,
name: data.name,
label: data.label
})
navigate(e.key)
}
... ...
}
export default Aside
修改components/tag/index.js文件:
... ...
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
const MyTag = props => {
const tagList = useSelector(state => state.tab.tagList)
const handleClose = () => {
console.log(123, tagList)
}
... ...
}
export default MyTag
2、tag选中和删除
①当前菜单存储
同样使用redux进行存储,修改src/store/reducers/tab.js文件:
... ...
const tabSlice = createSlice({
name: 'tab',
initialState: {
... ...
currentMenu: {}
},
reducers: {
... ...
selectTagList: (state, { payload: val }) => {
if (val.name !== 'home') {
state.currentMenu = val
... ...
} else if(val.name !== 'home' && state.tagList.length === 1){
state.currentMenu = {}
}
},
setCurrentMenu: (state, { payload: val }) => {
if (val.name === 'home') {
state.currentMenu = {}
} else {
state.currentMenu = val
}
}
}
})
... ...
②tag遍历显示
修改components/tag/index.js文件:
... ...
const MyTag = props => {
const tagList = useSelector(state => state.tab.tagList)
const currentMenu = useSelector(state => state.tab.currentMenu)
const handleCloseTag = () => {
console.log(123, tagList)
}
const handleClickTag = tag => {}
//tag的显示,flag为是否选中标识
const setTag = (flag, item, index) => {
return flag ? (
<Tag closeIcon onClose={() => handleCloseTag(item, index)} color='#55acee' key={item.name}>
{item.label}
</Tag>
) : (
<Tag key={item.name} onClick={() => handleClickTag(item)}>
{item.label}
</Tag>
)
}
return (
<Space className='tag-box' size={[0, 8]} wrap>
{currentMenu.name &&
tagList.map((item, index) => setTag(item.path === currentMenu.path, item, index))}
</Space>
)
}
export default MyTag
③删除tag
修改src/store/reducers/tab.js文件:
import { createSlice } from '@reduxjs/toolkit'
const tabSlice = createSlice({
... ...
reducers: {
... ...
closeTag: (state, { payload: val }) => {
let res = state.tagList.findIndex(item => item.name === val.name)
state.tagList.splice(res, 1)
},
setCurrentMenu: (state, { payload: val }) => {
if (val.name === 'home') {
state.currentMenu = {}
} else {
state.currentMenu = val
}
}
}
})
export const { changeCollapse, selectTagList, closeTag, setCurrentMenu } = tabSlice.actions
export default tabSlice.reducer
修改components/tag/index.js文件:
... ...
import { useNavigate } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { useLocation } from 'react-router-dom'
import { closeTag, setCurrentMenu } from '../../store/reducers/tab'
... ...
const MyTag = props => {
const dispatch = useDispatch()
const navigate = useNavigate()
const tagList = useSelector(state => state.tab.tagList)
const currentMenu = useSelector(state => state.tab.currentMenu)
const location = useLocation()
const handleCloseTag = (tag, index) => {
let length = tagList.length - 1
dispatch(closeTag(tag))
if (index === length) {
//设置当前数据
const curData = tagList[index - 1]
dispatch(setCurrentMenu(curData))
navigate(curData.path)
} else {
//关闭中间tag
if (tagList.length > 1) {
//如果tag至少存在一个数据,则选择后一个
const curData = tagList[index + 1]
dispatch(setCurrentMenu(curData))
navigate(curData.path)
}
}
}
const handleClickTag = tag => {
dispatch(setCurrentMenu(tag))
navigate(tag.path)
}
... ...
④菜单选中效果与tag、路由不一致问题
修改components/aside/index.js文件:
... ...
import { useNavigate, useLocation } from 'react-router-dom'
... ...
const Aside = props => {
... ...
const location = useLocation()
... ...
return (
... ...
<Menu
... ...
selectedKeys={[location.pathname]}
... ...
/>
... ...
)
}
效果:
二、用户管理界面实现
1、用户筛选框显示
使用antd的Button、Form组件实现,修改page/user/index.js文件:
import React, { useEffect, useState } from 'react'
import { Button, Form, Input } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import './index.css'
const User = () => {
const handleModal = () => {}
const onFinish = () => {}
return (
<div className='user-list'>
<div className='filter-box'>
<Button type='primary' onClick={() => handleModal()} icon={<PlusOutlined />}>
新建
</Button>
<Form name='basic' layout='inline' onFinish={onFinish} autoComplete='off'>
<Form.Item name='keyword'>
<Input placeholder='请输入用户名' />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit'>
搜索
</Button>
</Form.Item>
</Form>
</div>
</div>
)
}
export default User
page/user/index.css文件:
.user-list .filter-box {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
2、用户列表table显示
①table数据获取
在src/mock文件夹下新建user.js文件:
import Mock from 'mockjs'
function param2Obj(url) {
const search = url.split('?')[1]
if (!search) {
return {}
}
return JSON.parse(
'{"' +
decodeURIComponent(search).replace(/"/g, '\\"').replace(/&/g, '","').replace(/=/g, '":"') +
'"}'
)
}
let List = []
const count = 200
for (let i = 0; i < count; i++) {
List.push(
Mock.mock({
id: Mock.Random.guid(),
name: Mock.Random.cname(),
addr: Mock.mock('@county(true)'),
'age|18-60': 1,
birth: Mock.Random.date(),
sex: Mock.Random.integer(0, 1)
})
)
}
const userApi = {
/**
* 获取列表
* 参数 name,page,limit;name可以不填,page,limit有默认值。
* @param name
* @param page
* @param limit
* @return {{code:number,count:number,data:*[]}}
*/
getUserList: config => {
const { name, page = 1, limit = 20 } = param2Obj(config.url)
const mockList = List.filter(user => {
if (name && user.name.indexOf(name) === -1 && user.addr.indexOf(name) === -1) return false
return true
})
const pageList = mockList.filter(
(item, index) => index < limit * page && index >= limit * (page - 1)
)
return {
code: 200,
count: mockList.length,
list: pageList
}
}
}
export default userApi
在utils/mock.js文件中新增查询mock接口:
... ...
import userApi from '../mock/user'
//拦截接口
... ...
Mock.mock(/user\/getUser/, userApi.getUserList)
在src/page/user文件夹下新建services.js文件:
import { get, post } from '../../utils/request'
export const getUser = async params => {
return get('/user/getUser', params)
}
在src/page/user/index.js文件中调用接口:
... ...
import { getUser } from './services'
const User = () => {
const [filterParam, setFilterParam] = useState({ //查询条件
name: ''
})
const getTableList = () => {
getUser(filterParam).then(res => {
console.log(res)
})
}
useEffect(() => {
getTableList()
}, [])
const handleModal = () => {}
const onFinish = e => {
setFilterParam({
name: e.keyword
})
}
... ...
export default User
控制台打印:
②table表格数据渲染
修改src/page/user/index.js文件内容:
import React, { useEffect, useState } from 'react'
import { Button, Form, Input, Table, Space, Popconfirm } from 'antd'
... ...
const User = () => {
... ...
const [tableData, setTableData] = useState([])
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name'
},
{
title: '年龄',
dataIndex: 'age',
key: 'age'
},
{
title: '性别',
dataIndex: 'sex',
key: 'sex',
render: val => {
return val ? '女' : '男'
}
},
{
title: '出生日期',
dataIndex: 'birth',
key: 'birth'
},
{
title: '地址',
dataIndex: 'addr',
key: 'addr'
},
{
title: '操作',
key: 'x',
render: (text, record) => {
return (
<Space>
<Button onClick={() => handleModal('edit', text)}>编辑</Button>
<Popconfirm
title='提示'
description='此操作将删除该用户,是否继续?'
onConfirm={() => handleDelete(text)}
onCancel={handleCancel}
okText='确认'
cancelText='取消'
>
<Button type='primary' danger>
删除
</Button>
</Popconfirm>
</Space>
)
}
}
]
const handleCancel = () => {}
const handleDelete = () => {}
... ...
return (
<div className='user-list'>
... ...
<Table columns={columns} dataSource={tableData} rowKey={'id'}></Table>
</div>
)
}
export default User
效果:
3、用户新增编辑弹窗
①弹窗显示
使用antd的Modal组件搭配Form表单实现,修改src/page/user/index.js文件内容:
... ...
import {
Button,
Form,
Input,
Table,
Space,
Popconfirm,
Modal,
InputNumber,
Select,
DatePicker
} from 'antd'
const User = () => {
... ...
const [isModalOpen, setIsModalOpen] = useState(false)
const [method, setMethod] = useState('add')
const [form] = Form.useForm()
... ...
const handleCancel = () => {
setIsModalOpen(false)
}
const handleModal = method => {
setMethod(method)
setIsModalOpen(true)
}
const handleSubmit = () => {}
return (
<div className='user-list'>
... ...
<Table columns={columns} dataSource={tableData} rowKey={'id'}></Table>
<Modal
title={method === 'edit' ? '编辑用户' : '新增用户'}
open={isModalOpen}
onOk={handleSubmit}
onCancel={handleCancel}
okText='确定'
cancelText='取消'
>
<Form
form={form}
name='basic'
labelCol={{
span: 6
}}
wrapperCol={{
span: 18
}}
labelAlign='left'
>
<Form.Item
label='姓名'
name='name'
rules={[
{
required: true,
message: '请输入姓名!'
}
]}
>
<Input placeholder='请输入姓名' />
</Form.Item>
<Form.Item
label='年龄'
name='age'
rules={[
{
required: true,
message: '请输入年龄!'
}
]}
>
<InputNumber placeholder='请输入年龄' />
</Form.Item>
<Form.Item
label='性别'
name='sex'
rules={[
{
required: true,
message: '请选择性别!'
}
]}
>
<Select
placeholder='请选择性别'
options={[
{ value: 0, label: '男' },
{ value: 1, label: '女' }
]}
/>
</Form.Item>
<Form.Item
label='出生日期'
name='birth'
rules={[
{
required: true,
message: '请选择出生日期!'
}
]}
>
<DatePicker placeholder='请选择' format={'YYYY/MM/DD'} />
</Form.Item>
<Form.Item
label='地址'
name='addr'
rules={[
{
required: true,
message: '请填写地址!'
}
]}
>
<Input placeholder='请填写地址' />
</Form.Item>
</Form>
</Modal>
</div>
)
}
export default User
效果:
②弹窗功能实现
添加/编辑时提交的出生日期参数需要做日期转换,需要使用到的是dayjs,所以需要下载dayjs依赖:
npm install dayjs --save
Ⅰ、用户添加功能
在src/mock/user.js文件中新建添加用户mock接口:
... ...
const userApi = {
... ...
/**
* 增加用户
* @param name
* @param addr
* @param age
* @param birth
* @param sex
* @return {{code:number,data:{message:string}}}
*/
createUser: config => {
const { name, addr, age, birth, sex } = JSON.parse(config.body)
List.unshift({
id: Mock.Random.guid(),
name: name,
age: age,
addr: addr,
birth: birth,
sex: sex
})
return {
code: 200,
data: {
message: '添加成功'
}
}
}
export default userApi
在utils/mock.js文件中拦截接口:
... ...
//拦截接口
... ...
Mock.mock(/user\/createUser/, 'post', userApi.createUser)
在src/pages/user/services.js文件新建前端接口:
... ...
export const createUser = async data => {
return post('/user/createUser', data)
}
在src/pages/user/index.js文件调用接口:
... ...
import { getUser, createUser} from './services'
import dayjs from 'dayjs'
const User = () => {
... ...
const handleCancel = () => {
setIsModalOpen(false)
form.resetFields()
}
const handleSubmit = () => {
form
.validateFields()
.then(value => {
value.birth = dayjs(value.birth).format('YYYY-MM-DD')
if (method === 'edit') {
} else {
createUser(value).then(res => {
handleCancel()
getTableList()
message.success(res.data.data.message)
})
}
})
.catch(e => {
console.log(e)
})
}
... ...
}
export default User
效果:
Ⅱ、用户编辑功能
在src/mock/user.js文件中新建编辑用户mock接口:
... ...
const userApi = {
... ...
/**
* 编辑用户
* @param name
* @param addr
* @param age
* @param birth
* @param sex
* @return {{code:number,data:{message:string}}}
*/
updateUser: config => {
const { id, name, addr, age, birth, sex } = JSON.parse(config.body)
const sex_num = parseInt(sex)
List.some(item => {
if (item.id === id) {
item.name = name
item.age = age
item.addr = addr
item.birth = birth
item.sex = sex_num
return true
}
})
return {
code: 200,
data: {
message: '编辑成功'
}
}
}
}
export default userApi
在utils/mock.js文件中拦截接口:
... ...
//拦截接口
... ...
Mock.mock(/user\/updateUser/, 'post', userApi.updateUser)
在src/pages/user/services.js文件新建前端接口:
... ...
export const updateUser = async data => {
return post('/user/updateUser', data)
}
在src/pages/user/index.js文件增加数据回填逻辑并调用接口:
... ...
import { getUser, createUser, updateUser } from './services'
const User = () => {
... ...
const handleModal = (method, data) => {
if (method === 'edit') {
const cloneData = JSON.parse(JSON.stringify(data))
cloneData.birth = dayjs(cloneData.birth)
form.setFieldsValue(cloneData)
}
... ...
}
const handleSubmit = () => {
form
.validateFields()
.then(value => {
value.birth = dayjs(value.birth).format('YYYY-MM-DD')
if (method === 'edit') {
updateUser(value).then(res => {
handleCancel()
getTableList()
message.success(res.data.data.message)
})
} else {
... ...
}
})
.catch(e => {
console.log(e)
})
}
... ...
return (
<div className='user-list'>
... ...
<Modal
... ...
>
<Form
... ...
>
{method === 'edit' && (
<Form.Item name='id' hidden>
<Input />
</Form.Item>
)}
... ...
</Form>
</Modal>
</div>
)
}
export default User
效果:
③删除功能实现
在src/mock/user.js文件中新建删除用户mock接口:
... ...
const userApi = {
... ...
/**
* 删除用户
* @param id
* @return {*}
*/
deleteUser: config => {
const { id } = JSON.parse(config.body)
if (!id) {
return {
code: 400,
message: '删除失败'
}
} else {
List = List.filter(item => item.id !== id)
return {
code: 200,
message: '删除成功'
}
}
}
}
export default userApi
在utils/mock.js文件中拦截接口:
... ...
Mock.mock(/user\/deleteUser/, 'post', userApi.deleteUser)
在src/pages/user/services.js文件新建前端接口:
... ...
export const deleteUser = async data => {
return post('/user/deleteUser', data)
}
在src/pages/user/index.js调用接口:
... ...
import { getUser, createUser, updateUser, deleteUser } from './services'
const User = () => {
... ...
const handleDelete = data => {
const { id } = data
deleteUser({ id }).then(res => {
console.log(res.data)
if (res.data.code === 200) {
getTableList()
message.success(res.data.message)
} else {
message.error(res.data.message)
}
})
}
... ...
}
export default User
效果:
④搜索功能实现
通过监听filterParam来实现搜索,点击搜索时修改filterParam的值,如果filterParam发生改变则调用查询接口,修改src/pages/user/index.js文件:
... ...
const User = () => {
... ...
useEffect(() => {
console.log(123123, filterParam)
getTableList()
}, [filterParam])
... ...
const onFinish = e => {
setFilterParam({
name: e.keyword
})
}
... ...
}
export default User
效果:
二、登录页和用户鉴权实现
1、登录页显示
在pages文件夹下新建login/index.js、login/index.css文件:
增加路由,修改src/router/index.js文件:
... ...
import Login from '../pages/login'
const routes = [
... ...
{
path: 'login',
Component: Login
}
]
... ...
login/index.js文件内容:
import React from 'react'
import { Form, Input, Button } from 'antd'
import './index.css'
const Login = () => {
const handleSubmit = () => {}
return (
<Form className='login-box' onFinish={handleSubmit}>
<div className='title'>系统登录</div>
<Form.Item
label='账号'
name='username'
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input placeholder='请输入账号' />
</Form.Item>
<Form.Item
label='密码'
name='password'
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password placeholder='请输入密码' />
</Form.Item>
<Form.Item className='login-button'>
<Button type='primary' htmlType='submit'>
登录
</Button>
</Form.Item>
</Form>
)
}
export default Login
login/index.css文件内容:
.login-box {
width: 350px;
border: 1px solid #eaeaea;
margin: 180px auto;
padding: 35px 35px 15px 35px;
background-color: #fff;
border-radius: 15px;
box-shadow: 0 0 25px #cac6c6;
box-sizing: border-box;
}
.login-box .title {
text-align: center;
margin-bottom: 40px;
color: #505458;
font-size: 20px;
}
.login-box .login-button {
text-align: center;
}
效果:
2、用户鉴权实现
使用mock模拟登录接口返回token,存入localStorage来判断用户是否登录
①使用mock模拟登录接口
在src/mock文件夹下新建permission.js文件:
import { message } from 'antd'
import Mock from 'mockjs'
const PermissionApi = {
LoginApi: config => {
const { username, password } = JSON.parse(config.body)
//先判断用户是否存在
//判断账号和密码是否对应
if (username === 'admin' && password === 'admin') {
return {
code: 200,
data: {
token: Mock.Random.guid(),
message: '登录成功'
}
}
} else if (username === 'account' && password === 'account') {
return {
code: 200,
data: {
token: Mock.Random.guid(),
message: '登录成功'
}
}
} else {
return {
code: 400,
message: '登录失败'
}
}
}
}
export default PermissionApi
在src/utils/mock.js文件新增以下代码:
... ...
import PermissionApi from '../mock/permission'
... ...
Mock.mock(/permission\/LoginApi/, 'post', PermissionApi.LoginApi)
在src/pages/login文件夹下新建services.js文件:
import { post } from '../../utils/request'
export const LoginApi = async data => {
return post('/permission/LoginApi', data)
}
②界面调用接口
在src/pages/login/index.js文件中增加以下代码:
... ...
import { LoginApi } from './services'
import { useNavigate, Navigate } from 'react-router-dom'
const Login = () => {
const navigator = useNavigate()
//在登陆状态下,需要跳转到home页面
if (localStorage.getItem('token')) {
return <Navigate to='/home' />
}
const handleSubmit = val => {
if (!val.password || !val.username) {
message.warning('请输入用户名和密码')
}
LoginApi(val).then(res => {
const { data } = res
localStorage.setItem('token', data.data.token)
navigator('/home')
})
}
... ...
}
export default Login
③退出登录
在src/components/header/index.js文件中增加登出逻辑:
... ...
import { useNavigate } from 'react-router-dom'
... ...
const MyHeader = props => {
... ...
const navigat = useNavigate()
//登出
const logout = () => {
//清除token
localStorage.removeItem('token')
navigat('/login')
}
}
export default MyHeader
④没有token情况下不让访问除login以外路由
使用高阶组件进行鉴权,在src/router文件下新建routerAuth.js文件:
import { Navigate } from 'react-router-dom'
export const RouterAuth = ({ children }) => {
const token = localStorage.getItem('token')
if (!token) {
return <Navigate to='/login' replace />
}
return children
}
修改src/pages/layout.js文件内容:
... ...
import { RouterAuth } from '../router/routerAuth'
... ...
const MyLayout = () => {
... ...
return (
<RouterAuth>
<Layout className='main-container'>
.... ....
</Layout>
</RouterAuth>
)
}
export default MyLayout
项目到这,整体框架就完成噜,可以根据自身需求修改样式、内容哦。
。:.゚ヽ(。◕‿◕。)ノ゚.:。+゚