React实现后台管理系统(二)

今天也要攒钱 2024-09-03 10:33:01 阅读 74

React实现后台管理系统(二)

一、页面tag实现1、tag显示和兄弟组件传值①tag显示效果实现②使用redux实现兄弟组件传值

2、tag选中和删除①当前菜单存储②tag遍历显示③删除tag④菜单选中效果与tag、路由不一致问题

二、用户管理界面实现1、用户筛选框显示2、用户列表table显示①table数据获取

②table表格数据渲染3、用户新增编辑弹窗①弹窗显示②弹窗功能实现Ⅰ、用户添加功能Ⅱ、用户编辑功能

③删除功能实现④搜索功能实现

二、登录页和用户鉴权实现1、登录页显示2、用户鉴权实现①使用mock模拟登录接口②界面调用接口③退出登录④没有token情况下不让访问除login以外路由

一、页面tag实现

1、tag显示和兄弟组件传值

①tag显示效果实现

使用antd中的Tag组件实现,在components文件夹下新建tag/index.js以及index.css文件

tag/index.js:

<code>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>code>

<Tag>首页</Tag>

<Tag closeIcon onClose={ -- -->handleClose} color='#55acee'>code>

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'>code>

... ...

<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}>code>

{ -- -->item.label}

</Tag>

) : (

<Tag key={ item.name} onClick={ () => handleClickTag(item)}>

{ item.label}

</Tag>

)

}

return (

<Space className='tag-box' size={ -- -->[0, 8]} wrap>code>

{ -- -->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文件:

<code>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'>code>

<div className='filter-box'>code>

<Button type='primary' onClick={ -- -->() => handleModal()} icon={ <PlusOutlined />}>code>

新建

</Button>

<Form name='basic' layout='inline' onFinish={ -- -->onFinish} autoComplete='off'>code>

<Form.Item name='keyword'>code>

<Input placeholder='请输入用户名' />code>

</Form.Item>

<Form.Item>

<Button type='primary' htmlType='submit'>code>

搜索

</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文件内容:

<code>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='提示'code>

description='此操作将删除该用户,是否继续?'code>

onConfirm={ -- -->() => handleDelete(text)}

onCancel={ handleCancel}

okText='确认'code>

cancelText='取消'code>

>

<Button type='primary' danger>code>

删除

</Button>

</Popconfirm>

</Space>

)

}

}

]

const handleCancel = () => { -- -->}

const handleDelete = () => { }

... ...

return (

<div className='user-list'>code>

... ...

<Table columns={ -- -->columns} dataSource={ tableData} rowKey={ 'id'}></Table>

</div>

)

}

export default User

效果:

在这里插入图片描述

3、用户新增编辑弹窗

①弹窗显示

使用antd的Modal组件搭配Form表单实现,修改src/page/user/index.js文件内容:

<code>... ...

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'>code>

... ...

<Table columns={ -- -->columns} dataSource={ tableData} rowKey={ 'id'}></Table>

<Modal

title={ method === 'edit' ? '编辑用户' : '新增用户'}

open={ isModalOpen}

onOk={ handleSubmit}

onCancel={ handleCancel}

okText='确定'code>

cancelText='取消'code>

>

<Form

form={ -- -->form}

name='basic'code>

labelCol={ -- -->{

span: 6

}}

wrapperCol={ {

span: 18

}}

labelAlign='left'code>

>

<Form.Item

label='姓名'code>

name='name'code>

rules={ -- -->[

{

required: true,

message: '请输入姓名!'

}

]}

>

<Input placeholder='请输入姓名' />code>

</Form.Item>

<Form.Item

label='年龄'code>

name='age'code>

rules={ -- -->[

{

required: true,

message: '请输入年龄!'

}

]}

>

<InputNumber placeholder='请输入年龄' />code>

</Form.Item>

<Form.Item

label='性别'code>

name='sex'code>

rules={ -- -->[

{

required: true,

message: '请选择性别!'

}

]}

>

<Select

placeholder='请选择性别'code>

options={ -- -->[

{ value: 0, label: '男' },

{ value: 1, label: '女' }

]}

/>

</Form.Item>

<Form.Item

label='出生日期'code>

name='birth'code>

rules={ -- -->[

{

required: true,

message: '请选择出生日期!'

}

]}

>

<DatePicker placeholder='请选择' format={ -- -->'YYYY/MM/DD'} />code>

</Form.Item>

<Form.Item

label='地址'code>

name='addr'code>

rules={ -- -->[

{

required: true,

message: '请填写地址!'

}

]}

>

<Input placeholder='请填写地址' />code>

</Form.Item>

</Form>

</Modal>

</div>

)

}

export default User

效果:

在这里插入图片描述

②弹窗功能实现

添加/编辑时提交的出生日期参数需要做日期转换,需要使用到的是dayjs,所以需要下载dayjs依赖:

<code>npm install dayjs --save

在这里插入图片描述

Ⅰ、用户添加功能

在src/mock/user.js文件中新建添加用户mock接口:

<code> ... ...

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接口:

<code> ... ...

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'>code>

... ...

<Modal

... ...

>

<Form

... ...

>

{ -- -->method === 'edit' && (

<Form.Item name='id' hidden>code>

<Input />

</Form.Item>

)}

... ...

</Form>

</Modal>

</div>

)

}

export default User

效果:

请添加图片描述

③删除功能实现

在src/mock/user.js文件中新建删除用户mock接口:

<code> ... ...

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文件:

<code>... ...

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文件:

<code>... ...

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}>code>

<div className='title'>系统登录</div>code>

<Form.Item

label='账号'code>

name='username'code>

rules={ -- -->[{ required: true, message: 'Please input your username!' }]}

>

<Input placeholder='请输入账号' />code>

</Form.Item>

<Form.Item

label='密码'code>

name='password'code>

rules={ -- -->[{ required: true, message: 'Please input your password!' }]}

>

<Input.Password placeholder='请输入密码' />code>

</Form.Item>

<Form.Item className='login-button'>code>

<Button type='primary' htmlType='submit'>code>

登录

</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文件:

<code>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' />code>

}

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 />code>

}

return children

}

修改src/pages/layout.js文件内容:

... ...

import { -- --> RouterAuth } from '../router/routerAuth'

... ...

const MyLayout = () => {

... ...

return (

<RouterAuth>

<Layout className='main-container'>code>

.... ....

</Layout>

</RouterAuth>

)

}

export default MyLayout

项目到这,整体框架就完成噜,可以根据自身需求修改样式、内容哦。

。:.゚ヽ(。◕‿◕。)ノ゚.:。+゚



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。