从零实现一个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')
}
相关推荐
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
林太白7 小时前
❤React-React 组件通讯
前端·javascript·react.js
豆华8 小时前
React 中 为什么多个 JSX 标签需要被一个父元素包裹?
前端·react.js·前端框架
前端熊猫8 小时前
React第一个项目
前端·javascript·react.js
练习两年半的工程师8 小时前
使用React和Vite构建一个AirBnb Experiences克隆网站
前端·react.js·前端框架
林太白8 小时前
❤React-JSX语法认识和使用
前端·react.js·前端框架
女生也可以敲代码8 小时前
react中如何在一张图片上加一个灰色蒙层,并添加事件?
前端·react.js·前端框架
布兰妮甜9 小时前
前端框架大比拼:React.js, Vue.js 及 Angular 的优势与适用场景探讨
前端·vue.js·react.js·前端框架·angular.js
老码沉思录9 小时前
React Native 全栈开发实战班 - 核心组件与导航
javascript·react native·react.js