前言
一般而言,后台管理系统都会有一个统一的布局,其中可以包含头部、侧边栏和内容区域。在本系统中,头部包含左部操作区和用户信息区域,侧边栏展示菜单和系统标题区域,内容区域就是展示Layout路由的子路由页面,放置标签点击菜单后能展示对应子路由页面。
Layout模块
所需要的头部、侧边栏、内容区域结构大致如下图所示
我们先来实现外部框架。
框架实现
这里Ant Design有现成的Layout组件,我们挑选一个布局直接用它提供的做下改造。
src/Layout/index.jsx
javascript
import React, { useState } from 'react'
import { MenuFoldOutlined, MenuUnfoldOutlined, DashboardFilled } from '@ant-design/icons'
import { Layout, Menu, Button, theme, Switch } from 'antd'
import './Layout.scss'
const { Header, Sider, Content } = Layout
const LayoutApp = () => {
const [collapsed, setCollapsed] = useState(false)
const {
token: { colorBgContainer }
} = theme.useToken()
// 侧边栏主题模式
const [themeVari, setThemeVari] = useState('dark')
// 切换侧边栏主题颜色
const changeTheme = (value) => {
setThemeVari(value ? 'light' : 'dark')
}
return (
<Layout className="layout">
<Sider trigger={null} collapsible collapsed={collapsed} theme={themeVari}>
<div className="layout-logo-vertical" style={{ color: themeVari === 'dark' ? '#fff' : '#000' }}>
<span className="layout-logo">
{' '}
<DashboardFilled />
</span>
{!collapsed && <span>react-antd5-admin</span>}
</div>
<Switch
className="sider-switch"
checkedChildren="☀"
unCheckedChildren="🌙"
onChange={changeTheme}
style={{ transform: collapsed ? 'translateX(15px)' : 'translateX(75px)' }}
/>
<Menu theme={themeVari} mode="inline" defaultSelectedKeys={[]} items={[]} />
</Sider>
<Layout>
<Header
style={{
padding: 0,
background: colorBgContainer
}}>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
width: 64,
height: 64
}}
/>
</Header>
<Content
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer
}}>
Content
</Content>
</Layout>
</Layout>
)
}
export default LayoutApp
整体外观如下图:
完成了框架,接下来我们就来实现各个功能模块
头部区域
左部的操作按钮区现在就是伸缩侧边栏的按钮,右侧用户信息区域主要就是一个用户头像,悬浮出现下拉框展示修改密码、退出登录的功能按钮。
用户头像展示
用户的头像我们直接在全局状态中用户切片的userinfo
字段中去取,若有的话展示,无就展示一张默认图片。
ini
// 用户头像
const avatar = useSelector(state=>state.user.userinfo.avatar)
...
<Header
style={{padding: 0,background: colorBgContainer,
display: 'flex',justifyContent: 'space-between'
}}>
...
<div className="header-right">
<Space>
<img
src={avatar || require('@/assets/images/avatar/default_avatar.jpg')}
className="user-icon"
alt="avatar"
/>
<DownOutlined />
</Space>
</div>
</Header>
下拉菜单
下拉菜单直接用Antd
有一个Dropdown
组件包裹上面的用户头像
Dropdown
menu:菜单配置项,其中items为菜单项数组
items:包含菜单项item的配置数组
key
:菜单项的唯一标志label
:菜单项标题placement: 菜单弹出位置:
bottom
bottomLeft
bottomRight
top
topLeft
topRight
我们先按照格式设置一个菜单数组
arduino
/** 下拉菜单 */
// 下拉菜单项数组
const dropdownMenuItems = [
{
key: '1',
label: (
<div onClick={() => console.log('个人中心')}>
<UserOutlined /> 个人中心
</div>
)
},
{
key: '2',
label: (
<Popconfirm
onConfirm={() => console.log('重置密码')}
title="是否确认重置密码?"
okText="重置"
cancelText="取消">
<UndoOutlined /> 重置密码
</Popconfirm>
)
},
{
key: '3',
label: (
<Popconfirm onConfirm={() => console.log('退出登录')} title="是否确认退出?" okText="退出" cancelText="取消">
<LogoutOutlined /> 退出登录
</Popconfirm>
)
}]
然后模板结构添加DropDown
xml
<div className="header-right">
<Dropdown menu={{ items: dropdownMenuItems }} placement="bottomRight">
<Space>
...
</Space>
</Dropdown>
</div>
完成后展示效果如下所示
接下来我们来完成各个功能,个人中心和重置密码都是弹窗进行表单操作,因为弹窗是通用的组件,我们就先封装一个自定义弹窗。
自定义弹窗封装
弹窗的封装我们只封装最基本的弹窗组件,然后只向外暴露一个弹窗显隐的方法供外部组件使用。
src/components/CustomModal/index.jsx
javascript
import React, { useState, useImperativeHandle, forwardRef } from 'react'
import { Modal } from 'antd'
// forwardRef : 传递弹窗组件的ref
const CustomModal = forwardRef(({ title, children }, ref) => {
const [isModalOpen, setIsModalOpen] = useState(false)
// 取消事件
const handleCancel = () => {
setIsModalOpen(false)
}
// useImperativeHandle:自定义父组件ref.current接收到的方法
useImperativeHandle(
ref,
() => ({
toggleShowStatus: (status) => {
/** 改变状态 */
setIsModalOpen(status)
}
}),
[]
)
return (
<Modal title={title} open={isModalOpen} footer={null} onCancel={handleCancel}>
{children}
</Modal>
)
})
export default CustomModal
useImperativeHandle(ref, createHandle, dependencies?)
作用:在组件顶层通过调用
useImperativeHandle
来自定义 ref 暴露出来的句柄(避免父组件直接访问到子组件中的DOM节点):
ref
:该ref
是你从渲染函数中获得的第二个参数。createHandle
:该函数无需参数,它返回你想要暴露的 ref 的句柄。该句柄可以包含任何类型。通常,你会返回一个包含你想暴露的方法的对象。- 可选的
dependencies
:句柄中的依赖项,变化则句柄重新执行
个人中心的实现
弹窗已经封装完毕,我们现在再额外创建一个组件去构建内部表单。个人信息中需要修改的头像、用户名、昵称、邮箱作为表单项。这里用户名、昵称、邮箱等文本字段与登录表单类似,在此不再多做赘述。我们便专注于头像的修改。头像的修改用到的是upload
上传组件。但我们首先得获取到用户的信息回显。
1.回显信息
src/Layout/components/UserCenterForm.jsx
javascript
// 导入api
import userApi from '@/api/user'
...
// 获取当前登录用户的id
const user_id = useSelector((state) => state.user.userinfo.user_id)
// 表单组件实例
const [form] = Form.useForm()
// upload组件回显图片
const [imageUrl, setImageUrl] = useState()
useEffect(() => {
// 获取当前登录用户信息回显
const fetchUserInfo = async () => {
const {
data: { name, nickname, email, avatar }
} = await userApi.center.get(user_id)
form.setFieldsValue({
name,
nickname,
email
})
if (avatar) setImageUrl(process.env.React_APP_IMG_API + '/' + avatar)
}
fetchUserInfo()
}, [form, user_id])
2.设置upload组件
做法一样,直接去官网找upload组件用户头像的案例粘过来
javascript
const getBase64 = (img, callback) => {
const reader = new FileReader()
reader.addEventListener('load', () => callback(reader.result))
reader.readAsDataURL(img)
}
const 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 < 2
if (!isLt2M) {
message.error('Image must smaller than 2MB!')
}
return isJpgOrPng && isLt2M
}
const UserCenterForm = (props) => {
// 获取token
const token = useSelector((state) => state.user.token)
const [loading, setLoading] = useState(false)
/** 图片上传参数及方法 */
const uploadUrl =
process.env.NODE_ENV === 'development' ? '/api/user/myInfo/updateAvatar' : '/user/myInfo/updateAvatar'
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, (url) => {
setLoading(false)
setImageUrl(url)
})
}
}
const uploadButton = (
<div>
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div
style={{
marginTop: 8
}}>
Upload
</div>
</div>
)
return (
<>
<div style={{ marginBottom: 10 }}>头像</div>
<Upload
name="avatar"
listType="picture-circle"
className="avatar-uploader"
showUploadList={false}
action={uploadUrl}
headers={{ Authorization: token }}
beforeUpload={beforeUpload}
onChange={handleChange}
style={{ textAlign: 'center' }}>
{imageUrl ? (
<img
src={imageUrl}
alt="avatar"
style={{
width: '100%'
}}
/>
) : (
uploadButton
)}
</Upload>
{/* 其余表单项 */}
<Form ...>
</Form>
}
3.放置该组件到弹窗里
首先我们要引入useRef hook
来接收子组件传递的弹窗显隐的方法,然后设置一个方法操控。其次要传递给内部表单组件以便其能关闭弹窗。
src/Layout/index.jsx
javascript
...
import {useRef} from 'React'
import UserCenterForm from './components/UserCenterForm'
...
/** 个人中心 */
const userCenterRef = useRef()
const toggleCenterStatus = (status) => {
userCenterRef.current.toggleShowStatus(status)
}
...
<CustomModal title="个人中心" ref={userCenterRef}>
<UserCenterForm toggleCenterStatus={toggleCenterStatus} />
</CustomModal>
个人中心大概就是这样,关键步骤就是1.封装弹窗组件2.编写内部表单组件3.用弹窗组件暴露的方法控制弹窗显隐,重置密码也与此类似,就不再多写了。最后我们看一下效果图
退出登录
退出登录就是确认退出登录后,清空全局状态及浏览器存储中的token
和refreshToken
。这些我们之前已经写在用户切片的reducers配置项中,我们直接分发调用即可。然后我们还得加个确认操作,防止用户误触。
src/Layout/index.jsx
arduino
...
/** 下拉菜单 */
// 下拉菜单项数组
const dropdownMenuItems = [
...
{
key: '3',
label: (
<Popconfirm onConfirm={() => handleLogout()} title="是否确认退出?" okText="退出" cancelText="取消">
<LogoutOutlined /> 退出登录
</Popconfirm>
)
}
]
再编写对应handleLogout
方法
javascript
import { logout } from '@/store/reducers/userSlice'
...
// 退出登录
const handleLogout = () => {
dispatch(logout())
navigate('/login')
}