从零实现一个React+Antd5.0后台管理系统-Layout模块头部区域

前言

一般而言,后台管理系统都会有一个统一的布局,其中可以包含头部、侧边栏和内容区域。在本系统中,头部包含左部操作区和用户信息区域,侧边栏展示菜单和系统标题区域,内容区域就是展示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.用弹窗组件暴露的方法控制弹窗显隐,重置密码也与此类似,就不再多写了。最后我们看一下效果图

退出登录

退出登录就是确认退出登录后,清空全局状态及浏览器存储中的tokenrefreshToken。这些我们之前已经写在用户切片的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')
}
相关推荐
大表哥62 小时前
在react中 使用redux
前端·react.js·前端框架
因为奋斗超太帅啦3 小时前
React学习笔记(三)——React 组件通讯
笔记·学习·react.js
西瓜本瓜@5 小时前
React + React Image支持图像的各种转换,如圆形、模糊等效果吗?
前端·react.js·前端框架
黄毛火烧雪下5 小时前
React 的 useEffect 钩子,执行一些异步操作来加载基本信息
前端·chrome·react.js
蓝莓味柯基6 小时前
React——点击事件函数调用问题
前端·javascript·react.js
资深前端之路6 小时前
react jsx
前端·react.js·前端框架
白鹭凡10 小时前
react 甘特图之旅
前端·react.js·甘特图
Passion不晚14 小时前
Vue vs React vs Angular 的对比和选择
vue.js·react.js·前端框架·angular.js
光影少年1 天前
usemeno和usecallback区别及使用场景
react.js
吕彬-前端1 天前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架