夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Node基础

  • 《MySQL》学习笔记

  • Midway

  • Nest

    • 开篇词
    • 学习理由
    • nest概念扫盲
    • 快速掌握 nestcli
    • 5种http数据传输方式
    • IoC 解决了什么痛点问题?
    • 如何调试 Nest 项目
    • Provider注入对象
    • 全局模块和生命周期
    • AOP 架构有什么好处?
    • 一网打尽 Nest 全部装饰器
    • Nest如何自定义装饰器
    • Metadata和Reflector
    • ExecutionContext切换上下文
    • Module和Provider的循环依赖处理
    • 如何创建动态模块
    • Nest和Express,fastify
    • Nest的Middleware
    • RxJS和Interceptor
    • 内置Pipe和自定义Pipe
    • ValidationPipe验证post请求参数
    • 如何自定义 Exception Filter
    • 图解串一串 Nest 核心概念
    • 接口如何实现多版本共存
    • Express如何使用multer实现文件上传
    • Nest使用multer实现文件上传
    • 图书管理系统
    • 大文件分片上传
    • 最完美的 OSS 上传方案
    • Nest里如何打印日志
    • 为什么Node里要用Winston打印日志
    • Nest 集成日志框架 Winston
    • 通过Desktop学Docker也太简单了
    • 你的第一个 Dockerfile
    • Nest 项目如何编写 Dockerfile
    • 提升 Dockerfile 水平的 5 个技巧
    • Docker 是怎么实现的
    • 为什么 Node 应用要用 PM2 来跑?
    • 快速入门 MySQL
    • SQL 查询语句的所有语法和函数
    • 一对一、join 查询、级联方式
    • 一对多、多对多关系的表设计
    • 子查询和 EXISTS
    • SQL 综合练习
    • MySQL 的事务和隔离级别
    • MySQL 的视图、存储过程和函数
    • Node 操作 MySQL 的两种方式
    • 快速掌握 TypeORM
    • TypeORM 一对一的映射和关联 CRUD
    • TypeORM 一对多的映射和关联 CRUD
    • TypeORM 多对多的映射和关联 CRUD
    • 在 Nest 里集成 TypeORM
    • TypeORM保存任意层级的关系
    • 生产环境为什么用TypeORM的migration迁移功能
    • Nest 项目里如何使用 TypeORM 迁移
    • 如何动态读取不同环境的配置?
    • 快速入门 Redis
    • 在 Nest 里操作 Redis
    • 为什么不用 cache-manager 操作 Redis
    • 两种登录状态保存方式:JWT、Session
    • Nest 里实现 Session 和 JWT
    • MySQL + TypeORM + JWT 实现登录注册
    • 基于 ACL 实现权限控制
    • 基于 RBAC 实现权限控制
    • access_token和refresh_token实现无感登录
    • 单token无限续期实现登录无感刷新
    • 使用 passport 做身份认证
    • passport 实现 GitHub 三方账号登录
    • passport 实现 Google 三方账号登录
    • 为什么要使用 Docker Compose ?
    • Docker 容器通信的最简单方式:桥接网络
    • Docker 支持重启策略,是否还需要 PM2
    • 快速掌握 Nginx 的 2 大核心用法
    • 基于 Nginx 实现灰度系统
    • 基于 Redis 实现分布式 session
    • Redis + 高德地图,实现附近的充电宝
    • 用 Swagger 自动生成 api 文档
    • 如何灵活创建 DTO
    • class- validator 的内置装饰器,如何自定义装饰器
    • 序列化 Entity,你不需要 VO 对象
    • 手写序列化 Entity 的拦截器
    • 使用 compodoc 生成文档
    • Node 如何发邮件?
    • 实现基于邮箱验证码的登录
    • 基于 sharp 实现 gif 压缩工具
    • 大文件如何实现流式下载?
    • Puppeteer 实现爬虫,爬取 BOSS 直聘全部前端岗位
    • 实现扫二维码登录
    • Nest 的 REPL 模式
    • 实现 Excel 导入导出
    • 如何用代码动态生成 PPT
    • 如何拿到服务器 CPU、内存、磁盘状态
    • Nest 如何实现国际化?
    • 会议室预订系统:需求分析和原型图
    • 会议室预订系统:技术方案和数据库设计
    • 会议室预订系统:用户管理模块--用户注册
    • 会议室预订系统:用户管理模块--配置抽离、登录认证鉴权
    • 会议室预订系统:用户管理模块-- interceptor、修改信息接口
    • 会议室预订系统:用户管理模块--用户列表和分页查询
    • 会议室预订系统:用户管理模块-- swagger 接口文档
    • 会议室预订系统:用户管理模块-- 用户端登录注册页面
    • 会议室预订系统:用户管理模块-- 用户端信息修改页面
    • 会议室预订系统:用户管理模块-- 头像上传
    • 会议室预订系统:用户管理模块-- 管理端用户列表页面
      • 总结
    • 会议室预订系统:用户管理模块-- 管理端信息修改页面
    • 会议室预订系统:会议室管理模块-后端开发
    • 会议室预订系统:会议室管理模块-管理端前端开发
    • 会议室预订系统:会议室管理模块-用户端前端开发
    • 会议室预订系统:预定管理模块-后端开发
    • 会议室预订系统:预定管理模块-管理端前端开发
    • 会议室预订系统:预定管理模块-用户端前端开发
    • 会议室预订系统:统计管理模块-后端开发
    • 会议室预订系统:统计管理模块-前端开发
    • 会议室预订系统:后端项目部署到阿里云
    • 会议室预订系统:前端项目部署到阿里云
    • 会议室预定系统:用 migration 初始化表和数据
    • 会议室预定系统:文件上传 OSS
    • 会议室预定系统:Google 账号登录后端开发
    • 会议室预定系统:Google 账号登录前端开发
    • 会议室预定系统:后端代码优化
    • 会议室预定系统:集成日志框架 winston
    • 会议室预定系统:前端代码优化
    • 会议室预定系统:全部功能测试
    • 会议室预定系统:项目总结
    • Nest 如何创建微服务?
    • Nest 的 Monorepo 和 Library
    • 用 Etcd 实现微服务配置中心和注册中心
    • Nest 集成 Etcd 做注册中心、配置中心
    • 用 Nacos 实现微服务配置中心和注册中心
    • 基于 gRPC 实现跨语言的微服务通信
    • 快速入门 ORM 框架 Prisma
    • Prisma 的全部命令
    • Prisma 的全部 schema 语法
    • Primsa Client 单表 CRUD 的全部 api
    • Prisma Client 多表 CRUD 的全部 api
    • 在 Nest 里集成 Prisma
    • 为什么前端监控系统要用 RabbitMQ?
    • 基于 Redis 实现关注关系
    • 基于 Redis 实现各种排行榜(周榜、月榜、年榜)
    • 考试系统:需求分析
    • 考试系统:技术方案和数据库设计
    • 考试系统:微服务、Lib 拆分
    • 考试系统;用户注册
    • 考试系统:用户登录、修改密码
    • 考试系统:考试微服务
    • 考试系统:登录、注册页面
    • 考试系统:修改密码、试卷列表页面
    • 考试系统:新增试卷、回收站
    • 考试系统:试卷编辑器
    • 考试系统:试卷回显、预览、保存
    • 考试系统:答卷微服务
    • 考试系统:答题页面
    • 考试系统:自动判卷
    • 考试系统:分析微服务、排行榜页面
    • 考试系统:整体测试
    • 考试系统:项目总结
    • 用 Node.js 手写 WebSocket 协议
    • Nest 开发 WebSocket 服务
    • 基于 Socket.io 的 room 实现群聊
    • 聊天室:需求分析和原型图
    • 聊天室:技术选型和数据库设计
    • 聊天室:用户注册
    • 聊天室:用户登录
    • 聊天室:修改密码、修改信息
    • 聊天室:好友列表、发送好友申请
    • 聊天室:创建聊天室、加入群聊
    • 聊天室:登录、注册页面开发
    • 聊天室:修改密码、信息页面开发
    • 聊天室:头像上传
    • 聊天室:好友∕群聊列表页面
    • 聊天室:添加好友弹窗、通知页面
    • 聊天室:聊天功能后端开发
    • 聊天室:聊天功能前端开发
    • 聊天室:一对一聊天
    • 聊天室:创建群聊、进入群聊
    • 聊天室:发送表情、图片、文件
    • 聊天室:收藏
    • 聊天室:全部功能测试
    • 聊天室:项目总结
    • MongoDB 快速入门
    • 使用 mongoose 操作 MongoDB 数据库
    • GraphQL 快速入门
    • Nest 开发 GraphQL 服务:实现 CRUD
    • GraphQL + Primsa + React 实现 TodoList
    • 如何调试 Nest 源码?
  • 其他

  • 服务端
  • Nest
神说要有光
2025-03-10
目录

会议室预订系统:用户管理模块-- 管理端用户列表页面

前面我们写了用户端的一些页面,这节继续来写管理端的。

涉及到这些页面:

这节我们来写前两个。

先新建个 react 项目:

npx create-react-app --template=typescript meeting_room_booking_system_frontend_admin
1

进入项目目录,把开发服务跑起来:

npm run start
1

浏览器访问 http://localhost:3000 可以看到这个界面:

就说明 react 项目成功跑起来了。

然后我们添加 router:

npm install --save react-router-dom
1

在 index.tsx 加上路由的配置:

import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider, createBrowserRouter, Link, Outlet } from 'react-router-dom';

function Index() {
  return <div>index<Outlet></Outlet></div>
}
function ErrorPage() {
  return <div>Error Page</div>
}

function UserManage() {
  return <div>user manage</div>
}

function Login() {
  return <div>login</div>
}

const routes = [
  {
    path: "/",
    element: <Index></Index>,
    errorElement: <ErrorPage />,
    children: [
      {
        path: 'user_manage',
        element: <UserManage/>
      }
    ]
  },
  {
    path: "login",
    element: <Login />,
  }
];
const router = createBrowserRouter(routes);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(<RouterProvider router={router}/>);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

配置了 4 个路由:

访问 /login 的时候,渲染 Login 组件。

访问 / 的时候,渲染 Index 组件。

访问 /user_manage 的时候,渲染 / 和 user_manage 的二级路由,也就是 Index + UserManage 组件。

以及出错的时候,渲染 ErrorPage 组件。

测试下:

都没问题。

把 src 目录下其余文件去掉:

然后创建 4 个组件:

src/pages/Login/Login.tsx

export function Login() {
    return <div>login</div>   
}
1
2
3

src/pages/Index/Index.tsx

import { Outlet } from "react-router-dom";

export function Index() {
    return <div>Index<Outlet></Outlet></div>   
}
1
2
3
4
5

src/pages/UserManage/UserManage.tsx

export function UserManage() {
    return <div>UserManage</div>
}
1
2
3

src/pages/ErrorPage/ErrorPage.tsx

export function ErrorPage() {
    return <div>Error Page</div>
}
1
2
3

改下 index.tsx 配置对应的路由:

import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider, createBrowserRouter, Link, Outlet } from 'react-router-dom';
import { Index } from './pages/Index/Index';
import { ErrorPage } from './pages/ErrorPage/ErrorPage';
import { UserManage } from './pages/UserManage/UserManage';
import { Login } from './pages/Login/Login';

const routes = [
  {
    path: "/",
    element: <Index></Index>,
    errorElement: <ErrorPage />,
    children: [
      {
        path: 'user_manage',
        element: <UserManage/>
      }
    ]
  },
  {
    path: "login",
    element: <Login />,
  }
];
const router = createBrowserRouter(routes);

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(<RouterProvider router={router}/>);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

测试下:

都没啥问题。

然后来写 Login 页面:

引入 Ant Design 组件库:

npm install antd --save
1

在 Login 组件引入 DatePicker 组件:

import { DatePicker } from "antd";

export function Login() {
    return <div><DatePicker/></div>   
}
1
2
3
4
5

没啥问题,说明 antd 引入成功了。

然后我们把登录页面写一下:

import { Button, Checkbox, Form, Input } from 'antd';
import './login.css';
import { useCallback } from 'react';

interface LoginUser {
    username: string;
    password: string;
}

const layout1 = {
    labelCol: { span: 4 },
    wrapperCol: { span: 20 }
}

export function Login() {

    const onFinish = useCallback((values: LoginUser) => {
        console.log(values);
    }, []);

    return <div id="login-container">
        <h1>会议室预订系统</h1>
        <Form
            {...layout1}
            onFinish={onFinish}
            colon={false}
            autoComplete="off"
        >
            <Form.Item
                label="用户名"
                name="username"
                rules={[{ required: true, message: '请输入用户名!' }]}
            >
                <Input />
            </Form.Item>

            <Form.Item
                label="密码"
                name="password"
                rules={[{ required: true, message: '请输入密码!' }]}
            >
                <Input.Password />
            </Form.Item>

            <Form.Item label=" ">
                <Button className='btn' type="primary" htmlType="submit">
                    登录
                </Button>
            </Form.Item>
        </Form>
    </div>   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

这里和用户端差不多.

login.css 如下:

#login-container {
    width: 400px;
    margin: 100px auto 0 auto;
    text-align: center;
}
#login-container .links {
    display: flex;
    justify-content: space-between;
}
#login-container .btn {
    width: 100%;
}
1
2
3
4
5
6
7
8
9
10
11
12

访问 /login,可以看到现在的登录页面:

然后看一下接口文档 http://localhost:3005/api-doc

传入用户名、密码、返回用户信息和 token。

在 postman 里测试下登录接口:

然后在点击登录按钮之后,用 axios 调用它:

安装 axios:

npm install axios
1

在前端项目创建个 src/interfaces/interfaces.ts

import axios from "axios";

const axiosInstance = axios.create({
    baseURL: 'http://localhost:3005/',
    timeout: 3000
});

export async function login(username: string, password: string) {
    return await axiosInstance.post('/user/admin/login', {
        username, password
    });
}
1
2
3
4
5
6
7
8
9
10
11
12

在这里集中管理接口。

然后 onFinish 里调用:

const navigate = useNavigate();

const onFinish = useCallback(async (values: LoginUser) => {
    const res = await login(values.username, values.password);

    const { code, message: msg, data} = res.data;
    if(res.status === 201 || res.status === 200) {
        message.success('登录成功');

        localStorage.setItem('access_token', data.accessToken);
        localStorage.setItem('refresh_token', data.refreshToken);
        localStorage.setItem('user_info', JSON.stringify(data.userInfo));

        setTimeout(() => {
            navigate('/');
        }, 1000);
    } else {
        message.error(data || '系统繁忙,请稍后再试');
    }
}, []);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这里和用户端一摸一样。

登录下:

提示 400 错误没处理。

因为接口返回 400 的时候,axios 会抛异常:

我们加一个响应的 interceptor,返回 error.response 而不是 Promise.reject(error.response)

axiosInstance.interceptors.response.use(
    (response) => {
        return response;
    },
    async (error) => {
        return error.response;
    }
);
1
2
3
4
5
6
7
8

再测试下:

当用户不存在时:

当密码错误时:

登录成功时:

都没啥问题。

这样,管理员登录的前后端功能就都完成了。

然后是用户管理的页面:

修改下 Index.tsx

import { UserOutlined } from "@ant-design/icons";
import { Outlet } from "react-router-dom";
import './index.css';

export function Index() {
    return <div id="index-container">
        <div className="header">
            <h1>会议室预定系统-后台管理</h1>
            <UserOutlined className="icon"/>
        </div>
        <div className="body">
            <Outlet></Outlet>
        </div>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里用到了 antd 的 icon 组件,需要安装用到的包:

npm install @ant-design/icons --save
1

css 如下:

#index-container{
    height: 100vh;
    display: flex;
    flex-direction: column;
}
#index-container .header{
    height: 80px;
    border-bottom: 1px solid #aaa;
    line-height: 80px;
    display: flex;
    justify-content: space-between;
    padding: 0 20px;
}
#index-container h1{
    margin: 0;
}

#index-container .icon {
    font-size: 40px;
    margin-top: 20px;
}
#index-container .body{
    flex: 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

测试下:

没啥问题。

不知道同学们有没有发现,其实这个页面应该是三级路由:

因为左边这部分也是要多个页面共用的。

我们改一下路由配置:

const routes = [
  {
    path: "/",
    element: <Index></Index>,
    errorElement: <ErrorPage />,
    children: [
      {
        path: "/",
        element: <Menu></Menu>,
        children: [
          {
            path: 'user_manage',
            element: <UserManage/>
          }
        ]
      }
    ]
  },
  {
    path: "login",
    element: <Login />,
  }
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

添加 src/pages/Menu/Menu.tsx

import { Outlet } from "react-router-dom";

export function Menu() {
    return <div>
        menu <Outlet></Outlet>
    </div>
}
1
2
3
4
5
6
7

渲染出来是这样的:

我们来写一下 Menu 组件:

import { Outlet } from "react-router-dom";
import { Menu as AntdMenu, MenuProps } from 'antd';
import './menu.css';

const items: MenuProps['items'] = [
    {
        key: '1',
        label: "会议室管理"
    },
    {
        key: '2',
        label: "预定管理"
    },
    {
        key: '3',
        label: "用户管理"
    },
    {
        key: '4',
        label: "统计"
    }
];

export function Menu() {
    return <div id="menu-container">
        <div className="menu-area">
            <AntdMenu
                defaultSelectedKeys={['3']}
                items={items}
            />
        </div>
        <div className="content-area">
            <Outlet></Outlet>
        </div>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

menu.css 如下:

#menu-container {
    display: flex;
    flex-direction: row;
}
#menu-container .menu-area {
    width: 200px;
}
1
2
3
4
5
6
7

渲染出来是这样的:

然后来写 UserManage 组件:

可以分为 2 部分,上面的搜索表单、下面的结果表格。

我们来写一下:

import { Button, Form, Input, Table } from "antd";
import { useCallback } from "react";
import './UserManage.css';

interface SearchUser {
    username: string;
    nickName: string;
    email: string;
}

export function UserManage() {

    const searchUser = useCallback(async (values: SearchUser) => {
        console.log(values);
    }, []);

    return <div id="userManage-container">
        <div className="userManage-form">
            <Form
                onFinish={searchUser}
                name="search"
                layout='inline'
                colon={false}
            >
                <Form.Item label="用户名" name="username">
                    <Input />
                </Form.Item>

                <Form.Item label="昵称" name="nickName">
                    <Input />
                </Form.Item>

                <Form.Item label="邮箱" name="email" rules={[
                    { type: "email", message: '请输入合法邮箱地址!'}
                ]}>
                    <Input/>
                </Form.Item>

                <Form.Item label=" ">
                    <Button type="primary" htmlType="submit">
                        搜索用户
                    </Button>
                </Form.Item>
            </Form>
        </div>
        <div className="userManage-table">
                
        </div>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

UserManage.css

#userManage-container {
    padding: 20px;
}
1
2
3

先把 form 部分写完。

测试下:

然后再写 table 部分:

import { Button, Form, Input, Table } from "antd";
import { useCallback } from "react";
import './UserManage.css';
import { ColumnsType } from "antd/es/table";

interface SearchUser {
    username: string;
    nickName: string;
    email: string;
}

interface UserSearchResult {
    username: string;
    nickName: string;
    email: string;
    headPic: string;
    createTime: Date;
}
const columns: ColumnsType<UserSearchResult> = [
    {
        title: '用户名',
        dataIndex: 'username'
    },
    {
        title: '头像',
        dataIndex: 'headPic'
    },
    {
        title: '昵称',
        dataIndex: 'nickName'
    },
    {
        title: '邮箱',
        dataIndex: 'email'
    },
    {
        title: '注册时间',
        dataIndex: 'createTime'
    }   
];

const data = [
    {
        key: '1',
        username: 'xx',
        headPic: 'xxx.png',
        nickName: 'xxx',
        email: 'xx@xx.com',
        createTime: new Date()
    },
    {
        key: '12',
        username: 'yy',
        headPic: 'yy.png',
        nickName: 'yyy',
        email: 'yy@yy.com',
        createTime: new Date()
    }
]


export function UserManage() {

    const searchUser = useCallback(async (values: SearchUser) => {
        console.log(values);
    }, []);

    return <div id="userManage-container">
        <div className="userManage-form">
            <Form
                onFinish={searchUser}
                name="search"
                layout='inline'
                colon={false}
            >
                <Form.Item label="用户名" name="username">
                    <Input />
                </Form.Item>

                <Form.Item label="昵称" name="nickName">
                    <Input />
                </Form.Item>

                <Form.Item label="邮箱" name="email" rules={[
                    { type: "email", message: '请输入合法邮箱地址!'}
                ]}>
                    <Input/>
                </Form.Item>

                <Form.Item label=" ">
                    <Button type="primary" htmlType="submit">
                        搜索用户
                    </Button>
                </Form.Item>
            </Form>
        </div>
        <div className="userManage-table">
            <Table columns={columns} dataSource={data} pagination={ {
                pageSize: 10
            }}/>
        </div>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

渲染出来是这样的:

然后我们调用下搜索接口。

看下接口文档:

在 postman 里调用下:

这个接口是需要登录的。

我们先登录一下:

带上 access_token 再访问:

返回了 8 条数据。

然后我们在页面里调用下:

先把之前写的 axios 的 interceptors 自动添加 authorization 的 header,自动 refresh token 的逻辑拿过来:

axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
        config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
})

axiosInstance.interceptors.response.use(
    (response) => {
        return response;
    },
    async (error) => {
        let { data, config } = error.response;

        if (data.code === 401 && !config.url.includes('/user/admin/refresh')) {
            
            const res = await refreshToken();

            if(res.status === 200 || res.status === 201) {
                return axiosInstance(config);
            } else {
                message.error(res.data);

                setTimeout(() => {
                    window.location.href = '/login';
                }, 1500);
            }
            
        } else {
            return error.response;
        }
    }
)

async function refreshToken() {
    const res = await axiosInstance.get('/user/admin/refresh', {
        params: {
          refresh_token: localStorage.getItem('refresh_token')
        }
    });
    localStorage.setItem('access_token', res.data.access_token);
    localStorage.setItem('refresh_token', res.data.refresh_token);
    return res;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

然后添加一个接口:

export async function userSearch(username: string, nickName: string, email: string, pageNo: number, pageSize: number) {
    return await axiosInstance.get('/user/list', {
        params: {
            username,
            nickName,
            email,
            pageNo,
            pageSize
        }
    });
}
1
2
3
4
5
6
7
8
9
10
11

在页面调用下:

const [pageNo, setPageNo] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [userResult, setUserResult] = useState<UserSearchResult[]>();

const searchUser = useCallback(async (values: SearchUser) => {
    const res = await userSearch(values.username,values.nickName, values.email, pageNo, pageSize);

    const { data } = res.data;
    if(res.status === 201 || res.status === 200) {
        setUserResult(data.users.map((item: UserSearchResult) => {
            return {
                key: item.username,
                ...item
            }
        }))
    } else {
        message.error(data || '系统繁忙,请稍后再试');
    }
}, []);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

使用 useState 创建 pageNo、pageSize、userResult 这三个状态。

请求接口,成功后把数据设置到 userResult。

然后修改下 table 的 dataSource:

测试下:

先登录。

然后访问 http://localhost:3000/user_manage

搜索接口对接成功。

然后再对接下分页:

<Table columns={columns} dataSource={userResult} pagination={ {
    current: pageNo,
    pageSize: pageSize,
    onChange: changePage
}}/>
1
2
3
4
5

设置 pageNo 和 pageSize,并监听 onChange 事件

useEffect(() => {
    searchUser({
        username: '',
        email: '',
        nickName: ''
    });
}, [pageNo, pageSize]);

const changePage = useCallback(function(pageNo: number, pageSize: number) {
    setPageNo(pageNo);
    setPageSize(pageSize);
}, []);
1
2
3
4
5
6
7
8
9
10
11
12

分页设置改变的时候,设置 pageNo 和 pageSize。

并且 useEffect 监听这两个 state,在变化的时候,重新发送请求。

这样,刚进入页面的时候,就会触发一次渲染,并且在分页设置改变时也会触发:

然后修改下 headPic,改为图片:

const columns: ColumnsType<UserSearchResult> = [
    {
        title: '用户名',
        dataIndex: 'username'
    },
    {
        title: '头像',
        dataIndex: 'headPic',
        render: value => {
            return value ? <Image
                    width={50}
                    src={`http://localhost:3005/${value}`}
            /> : '';
        }
    },
    {
        title: '昵称',
        dataIndex: 'nickName'
    },
    {
        title: '邮箱',
        dataIndex: 'email'
    },
    {
        title: '注册时间',
        dataIndex: 'createTime'
    }   
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

这里用的是 antd 的 Image 组件,有预览的功能:

原型图还有个冻结功能:

看下接口文档:

很简单,就是个 get 接口。

我们在表格里加一列:

{
    title: '操作',
    render: (_, record) => (
        <a href="#" onClick={() => {freezeUser(record.id)}}>冻结</a>
    )
}
1
2
3
4
5
6

这里用到了 id,我们在类型里加一下:

然后在 interfaces.tsx 添加这个接口:

export async function freeze(id: number) {
    return await axiosInstance.get('/user/freeze', {
        params: {
            id
        }
    });
}
1
2
3
4
5
6
7

在组件里创建 freezeUser 方法:

async function freezeUser(id: number) {
    const res = await freeze(id);

    const { data } = res.data;
    if(res.status === 201 || res.status === 200) {
        message.success('冻结成功');
    } else {
        message.error(data || '系统繁忙,请稍后再试');
    }
}
1
2
3
4
5
6
7
8
9
10

测试下:

2023-09-12 11.31.21.gif

然后我们还要把冻结状态显示出来:

这部分数据是返回了的:

需要添加一列:

{
    title: '状态',
    dataIndex: 'isFrozen',
    render: (_, record) => (
        record.isFrozen ? <Badge status="success">已冻结</Badge> : ''
    )
},
1
2
3
4
5
6
7

在类型部分也要添加下:

测试下:

冻结之后,刷新页面,会显示已冻结。

这里我们在冻结之后自动刷新下。

这需要把逻辑移到组件内:

把 columns 移到组件内,用 useMemo 包裹,这样只会创建一次:

freeezeUser 也是:

const freezeUser = useCallback(async (id: number) => {
    const res = await freeze(id);

    const { data } = res.data;
    if(res.status === 201 || res.status === 200) {
        message.success('冻结成功');
    } else {
        message.error(data || '系统繁忙,请稍后再试');
    }
}, []);
1
2
3
4
5
6
7
8
9
10

添加一个 num 的 state,冻结之后设置一个随机值:

把它添加到 useEffect 的依赖里,这样就能触发重新搜索:

测试下:

但其实现在这个重新搜索有问题:

我搜索之后再冻结,然后刷新就丢失了搜索条件了。

这里需要搜索的时候带上当前的条件:

用 useForm 拿到 form 的 api:

然后在搜索的时候拿到最新的表单值:

useEffect(() => {
    searchUser({
        username: form.getFieldValue('username'),
        email: form.getFieldValue('email'),
        nickName: form.getFieldValue('nickName')
    });
}, [pageNo, pageSize, num]);
1
2
3
4
5
6
7

这样就可以了:

这样,用户管理页面就写完了。

全部代码如下:

import { Badge, Button, Form, Image, Input, Table, message } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import './UserManage.css';
import { ColumnsType } from "antd/es/table";
import { freeze, userSearch } from "../../interfaces/interfaces";
import { useForm } from "antd/es/form/Form";

interface SearchUser {
    username: string;
    nickName: string;
    email: string;
}

interface UserSearchResult {
    id: number,
    username: string;
    nickName: string;
    email: string;
    headPic: string;
    createTime: Date;
    isFrozen: boolean;
}


export function UserManage() {
    const [pageNo, setPageNo] = useState<number>(1);
    const [pageSize, setPageSize] = useState<number>(10);
    const [userResult, setUserResult] = useState<UserSearchResult[]>();
    const [num, setNum] = useState(0);

    const columns: ColumnsType<UserSearchResult> = useMemo(() => [
        {
            title: '用户名',
            dataIndex: 'username'
        },
        {
            title: '头像',
            dataIndex: 'headPic',
            render: value => {
                return value ? <Image
                        width={50}
                        src={`http://localhost:3005/${value}`}
                /> : '';
            }
        },
        {
            title: '昵称',
            dataIndex: 'nickName'
        },
        {
            title: '邮箱',
            dataIndex: 'email'
        },
        {
            title: '注册时间',
            dataIndex: 'createTime'
        },
        {
            title: '状态',
            dataIndex: 'isFrozen',
            render: (_, record) => (
                record.isFrozen ? <Badge status="success">已冻结</Badge> : ''
            )
        },
        {
            title: '操作',
            render: (_, record) => (
                <a href="#" onClick={() => {freezeUser(record.id)}}>冻结</a>
            )
        }
    ], []);
    
    const freezeUser = useCallback(async (id: number) => {
        const res = await freeze(id);
    
        const { data } = res.data;
        if(res.status === 201 || res.status === 200) {
            message.success('冻结成功');
            setNum(Math.random())
        } else {
            message.error(data || '系统繁忙,请稍后再试');
        }
    }, []);

    const searchUser = useCallback(async (values: SearchUser) => {
        const res = await userSearch(values.username,values.nickName, values.email, pageNo, pageSize);

        const { data } = res.data;
        if(res.status === 201 || res.status === 200) {
            setUserResult(data.users.map((item: UserSearchResult) => {
                return {
                    key: item.username,
                    ...item
                }
            }))
        } else {
            message.error(data || '系统繁忙,请稍后再试');
        }
    }, []);

    const [form ]  = useForm();

    useEffect(() => {
        searchUser({
            username: form.getFieldValue('username'),
            email: form.getFieldValue('email'),
            nickName: form.getFieldValue('nickName')
        });
    }, [pageNo, pageSize, num]);

    const changePage = useCallback(function(pageNo: number, pageSize: number) {
        setPageNo(pageNo);
        setPageSize(pageSize);
    }, []);


    return <div id="userManage-container">
        <div className="userManage-form">
            <Form
                form={form}
                onFinish={searchUser}
                name="search"
                layout='inline'
                colon={false}
            >
                <Form.Item label="用户名" name="username">
                    <Input />
                </Form.Item>

                <Form.Item label="昵称" name="nickName">
                    <Input />
                </Form.Item>

                <Form.Item label="邮箱" name="email" rules={[
                    { type: "email", message: '请输入合法邮箱地址!'}
                ]}>
                    <Input/>
                </Form.Item>

                <Form.Item label=" ">
                    <Button type="primary" htmlType="submit">
                        搜索用户
                    </Button>
                </Form.Item>
            </Form>
        </div>
        <div className="userManage-table">
            <Table columns={columns} dataSource={userResult} pagination={ {
                current: pageNo,
                pageSize: pageSize,
                onChange: changePage
            }}/>
        </div>
    </div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155

案例代码上传了小册仓库 (opens new window)。

# 总结

这节我们实现了管理端的登录和用户管理页面。

和用户端的一样,都是通过 axios interceptor 自动添加 header 和自动 refresh token。

这里涉及到三级路由,第一级展示上面的 header,第二级展示左侧的 menu,第三级才是具体的页面。

使用 table 组件来渲染列表,通过 useEffect 在 pageNo、pageSize 改变的时候自动重发请求。

这样,这两个页面的前后端代码都完成了。

编辑 (opens new window)
上次更新: 2025/10/27 10:53:52
会议室预订系统:用户管理模块-- 头像上传
会议室预订系统:用户管理模块-- 管理端信息修改页面

← 会议室预订系统:用户管理模块-- 头像上传 会议室预订系统:用户管理模块-- 管理端信息修改页面→

最近更新
01
H5调用微信jssdk
09-28
02
VueVirtualScroller
09-19
03
如何调试 Nest 项目
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式