从0死磕全栈第十一天:使用Vite React ts Antd Design React-Router实现企业级用户管理页面

企业应用开发与运维数据库

人生的真相:

爱情是滤镜,能把普通人磨成男神女神;

生活是卸妆水,一擦全是素颜的烟火气。

从0 死磕全栈系列文章

从0死磕全栈第十天:nest.js集成prisma完成CRUD

上一节,我们已经在后端实现了使用prisma整合数据库的用户crud功能,这次就需要来完成企业里面的正式的用户功能页面了。

在实现这个功能之前,我们需要提前安装好需要的依赖

  
npm install antd @ant-design/icons react-router-dom --force

稍等几分钟,下载好了之后,就可以开始进入正题。

在真正实现一个企业级的页面,我们需要用到成熟的UI框架,给我们提供很多前人已经写好的优秀组件,借助这些组件,可以帮助我们快速搭建页面,这里我们选择的就是Antd Design。

Antd Design简介

Ant Design(简称 antd)是阿里巴巴开源的企业级 React UI 组件库,提供丰富的预设组件和设计规范。它基于 React 技术栈,遵循 Ant Design 设计语言,包含按钮、表单、表格等 50+ 高质量组件,支持主题定制和国际化。antd 以「确定性」、「意义感」、「生长性」和「自然感」为设计价值观,广泛应用于中后台系统开发,具有 TypeScript 支持、响应式布局和优雅的交互体验,能显著提升开发效率。其完善的文档和活跃的社区使其成为最受欢迎的 React UI 库之一。

第一步,项目结构

  
my-react-app/  
├── public/                  # 静态资源  
│   ├── index.html           # 主HTML文件  
│   ├── favicon.ico          # 网站图标  
│   └── robots.txt           # SEO配置  
│  
├── src/  
│   ├── assets/              # 静态资产  
│   │   ├── fonts/           # 字体文件  
│   │   ├── images/          # 图片资源  
│   │   └── styles/          # 全局样式  
│   │  
│   ├── components/          # 通用组件  
│   │   ├── ui/              # 基础UI组件(Button, Input等)  
│   │   ├── layout/          # 布局组件  
│   │   └── shared/          # 业务通用组件  
│   │  
│   ├── features/            # 功能模块(推荐)  
│   │   ├── auth/            # 认证模块  
│   │   │   ├── components/  # 模块专用组件  
│   │   │   ├── hooks/       # 模块hooks  
│   │   │   ├── types/       # 类型定义  
│   │   │   └── index.tsx    # 模块入口  
│   │   │  
│   │   └── dashboard/       # 另一个功能模块  
│   │  
│   ├── hooks/               # 全局自定义hooks  
│   ├── lib/                 # 工具库/第三方配置  
│   ├── pages/               # 页面组件(Next.js风格)  
│   ├── routes/              # 路由配置  
│   ├── stores/              # 状态管理(Zustand/Jotai)  
│   ├── test/                # 测试相关  
│   ├── types/               # 全局类型定义  
│   ├── utils/               # 工具函数  
│   ├── App.tsx              # 根组件  
│   ├── main.tsx             # 应用入口  
│   └── vite-env.d.ts        # 类型声明  
│  
├── .env                     # 环境变量  
├── .eslintrc                # ESLint配置  
├── .prettierrc              # Prettier配置  
├── tsconfig.json            # TypeScript配置  
└── package.json

如上是一个标准的react项目结构,我们也按照这样来组织文件。

第二步,布局

在实现具体页面之前,我们需要首先思考我们的页面的布局是怎样的

页面主要包含左边的菜单栏,点击某个菜单会在右边显示对应的页面

所以我们在src下面新建layout,在小虾米创建一个布局的文件AppLayOut.tsx

  
import React, { useState } from 'react';  
import { Layout, Menu, theme } from 'antd';  
import { UserOutlined } from '@ant-design/icons';  
import { Outlet, useNavigate } from 'react-router-dom';  
import './AppLayout.css';  
const { Header, Content, Sider } = Layout;  
const AppLayout: React.FC = () => {  
  // 导航钩子  
  const navigate = useNavigate();  
  // 菜单状态管理  
  const [collapsed, setCollapsed] = useState(false);  
  // 当前选中的菜单项  
  const [current, setCurrent] = useState('users');  
  // 处理菜单点击  
  const handleMenuClick = (e: { key: string }) => {  
    setCurrent(e.key);  
    navigate(`/${e.key}`);  
  };  
  // 获取Ant Design主题  
  const { token } = theme.useToken();  
  // 侧边栏菜单配置  
  const menuItems = [  
    {  
      key: 'users',  
      icon: <UserOutlined />,  
      label: '用户管理',  
    },  
    // 可以根据需要添加更多菜单项  
  ];  
  return (  
    <Layout style={{ minHeight: '100vh' }}>  
      {/* 顶部导航栏 */}  
      <Header className="header" style={{ background: token.colorBgContainer }}>  
        <div className="logo">企业管理系统</div>  
      </Header>  
      <Layout>  
        {/* 侧边栏 */}  
        <Sider  
          collapsible  
          collapsed={collapsed}  
          onCollapse={(value) => setCollapsed(value)}  
          style={{ background: token.colorBgContainer }}  
        >  
          <Menu  
            mode="inline"  
            selectedKeys={[current]}  
            onClick={handleMenuClick}  
            items={menuItems}  
            style={{ height: '100%', borderRight: 0 }}  
          />  
        </Sider>  
        {/* 主内容区域 */}  
        <Content  
          style={{  
            margin: '0 16px',  
            padding: 24,  
            minHeight: 280,  
            background: token.colorBgContainer,  
          }}  
        >  
          <Outlet />  
        </Content>  
      </Layout>  
    </Layout>  
  );  
};  
export default AppLayout;

picture.image

可以看到实现左边的菜单栏是通过antd的Layout+Sider+Menu这样的层级来构建的。

我们还可以再增加一个角色管理的菜单

只需要再menuItems增加就可以了

  
{  
        key: 'roles',  
        icon: <TeamOutlined />,  
        label: '角色管理',  
      },

picture.image

UserOutlined

含义:用户轮廓图标,通常用于表示与单个用户相关的功能或页面。

视觉特征:一般呈现为一个简化的人形轮廓,有时带有头部和肩部的线条。

使用场景:在项目中,常用于用户管理相关的菜单、按钮或页面标识,如代码中用于<UserOutlined /> 用户管理菜单项,直观传达该菜单与用户数据管理相关。

TeamOutlined

含义:团队/群组轮廓图标,用于表示与团队、角色或用户组相关的功能或页面。

视觉特征:通常呈现为多个人形轮廓的组合(如两个或更多人形并排),强调群体性。

使用场景:在项目中,常用于角色管理、团队配置或用户组管理相关的菜单、按钮或页面标识,如代码中用于<TeamOutlined /> 角色管理菜单项,明确传达该菜单与角色定义和权限分配相关。

第三步,实现UserList组件

  
import React, { useState } from 'react';  
import { useQuery } from 'react-query';  
import { Table, Pagination, Spin, Alert } from 'antd';  
import type { ColumnsType } from 'antd/es/table';  
import { UserOutlined } from '@ant-design/icons';  
import './UserList.css';  
import { fetchUsers } from '../services/userService';  
import type { User, PaginationParams, UserResponse } from '../services/userService';  
const UserList: React.FC = () => {  
  const [pagination, setPagination] = useState<PaginationParams>({  
    pageSize: 10,  
    currentPage: 1,  
  });  
  const { data, isLoading, isError, error } = useQuery<UserResponse>(  
    ['users', pagination],  
    () => fetchUsers(pagination),  
    {  
      refetchOnWindowFocus: false,  
      keepPreviousData: true,  
    }  
  );  
  const handlePaginationChange = (current: number, pageSize: number) => {  
    setPagination({  
      currentPage: current,  
      pageSize,  
    });  
  };  
  const columns: ColumnsType<User> = [  
    {  
      title: 'ID',  
      dataIndex: 'id',  
      key: 'id',  
      width: 80,  
    },  
    {  
      title: '邮箱',  
      dataIndex: 'email',  
      key: 'email',  
      width: 200,  
    },  
    {  
      title: '姓名',  
      dataIndex: 'name',  
      key: 'name',  
      width: 120,  
    },  
  ];  
  if (isLoading) {  
    return (  
      <div className="loading-container">  
        <Spin size="large" />  
        <div className="loading-text">加载用户数据中...</div>  
      </div>  
    );  
  }  
  if (isError) {  
    return (  
      <Alert  
        message="获取用户数据失败"  
        description={error instanceof Error ? error.message : '网络请求异常'}  
        type="error"  
        showIcon  
      />  
    );  
  }  
  if (!data || data.users.length === 0) {  
    return (  
      <Alert  
        message="暂无数据"  
        description="当前没有用户数据"  
        type="info"  
        showIcon  
      />  
    );  
  }  
  return (  
    <div className="user-list-container">  
      <div className="table-header">  
        <h2> 用户管理</h2>  
      </div>  
      <Table  
        columns={columns}  
        dataSource={data.users}  
        rowKey="id"  
        pagination={{  
          current: pagination.currentPage,  
          pageSize: pagination.pageSize,  
          total: data.total,  
          onChange: handlePaginationChange,  
          showSizeChanger: true,  
          pageSizeOptions: ['10', '20', '50'],  
          showTotal: (total) => `共 ${total} 条记录`,  
        }}  
        bordered  
      />  
    </div>  
  );  
};  
export default UserList;

这个业务组件需要调用后端的接口,为了不让组件有太多代码,我们把调用接口的代码放在另外的文件,这里我们再src下面新建一个services目录,再里面新建一个文件userService.ts

  
// 定义用户类型接口  
export interface User {  
  id: number;  
  name: string;  
  email: string;  
  createTime: string;  
}  
// 定义分页参数接口  
export interface PaginationParams {  
  pageSize: number;  
  currentPage: number;  
}  
// 定义API响应接口  
export interface UserResponse {  
  users: User[];  
  total: number;  
  page: number;  
  pageSize: number;  
  totalPages: number;  
}  
// 获取用户列表数据  
export const fetchUsers = async (  
  params: PaginationParams  
): Promise<UserResponse> => {  
  try {  
    console.log(`【API调用】请求第${params.currentPage}页,每页${params.pageSize}条用户数据`);  
      
    // 构建查询参数  
    const queryParams = new URLSearchParams({  
      page: params.currentPage.toString(),  
      pageSize: params.pageSize.toString(),  
    });  
      
    // 使用fetch API发送请求  
    const response = await fetch(`/api/users/page?${queryParams}`);  
      
    if (!response.ok) {  
      throw new Error(`HTTP error! status: ${response.status}`);  
    }  
      
    const data = await response.json();  
    return data;  
  } catch (error) {  
    console.error('获取用户数据失败:', error);  
    // 错误处理,可以返回默认数据或抛出错误  
    throw error;  
  }  
};  
// 可以导出其他必要的工具或常量  
// export default {...}

第四步:路由

路由我们一般放在src下面的routes文件夹下面,再routes下面创建一个routes.tsx存放项目的路由配置

  
import React from 'react';  
import { createBrowserRouter } from 'react-router-dom';  
import AppLayout from '../components/layout/AppLayout';  
import UserList from '../pages/UserList';  
import RoleList from '../pages/RoleList';  
// 定义路由配置  
const router = createBrowserRouter([  
  {  
    path: '/',  
    element: <AppLayout />,  
    children: [  
      {  
        path: 'users',  
          element: <UserList />,  
        },  
        {          path: 'roles',          element: <RoleList />,        },  
        {  
          path: '/',  
        },  
    ],  
  },  
]);  
export default router;

最后,需要在main.tsx引入routes.tsx

  
import { StrictMode } from 'react'  
import { createRoot } from 'react-dom/client'  
import { QueryClientProvider } from 'react-query'  
import './index.css'  
import { RouterProvider } from 'react-router-dom'  
import router from './routes/routes.tsx'  
import queryClient from './queryClient.ts'  
createRoot(document.getElementById('root')!).render(  
  <StrictMode>  
    <QueryClientProvider client={queryClient}>  
      <RouterProvider router={router} />  
    </QueryClientProvider>  
  </StrictMode>,  
)

目前的页面效果如下

picture.image

虽然看起来样式不好看,但是总算能够把后端返回的数据成功渲染到页面了。

之所以这个表格的内容是偏左的,是因为,我们给的最外层的div宽度太小了,

  
.user-list-container {  
  width: 700px;  
  height: 100%;  
  display: flex;  
}

在UserList.tsx里面引入antd的Space,Typography

  
const { Title, Text } = Typography;

接着给Table配置title

  
 <Table  
        title={() => (  
          <Space align="center" style={{ display: 'flex', alignItems: 'center', width: '120px', }}>  
            <Title level={4} style={{ margin: 0, marginLeft: 8 }}>  
              用户管理  
            </Title>  
          </Space>  
        )}  
        columns={columns}  
        dataSource={data.users}  
        rowKey="id"  
        pagination={{  
          current: pagination.currentPage,  
          pageSize: pagination.pageSize,  
          total: data.total,  
          onChange: handlePaginationChange,  
          showSizeChanger: true,  
          pageSizeOptions: ['10', '20', '50'],  
          showTotal: (total) => `共 ${total} 条记录`,  
        }}  
        bordered  
      />

并且适当的调整columns里面配置的宽度

  
const columns: ColumnsType<User> = [  
    {  
      title: 'ID',  
      dataIndex: 'id',  
      key: 'id',  
      width: 80,  
    },  
    {  
      title: '邮箱',  
      dataIndex: 'email',  
      key: 'email',  
      width: 250,  
    },  
    {  
      title: '姓名',  
      dataIndex: 'name',  
      key: 'name',  
      width: 250,  
    },  
  ];

picture.image

此时的页面样式就比之前好看一些了,调整样式是一个细致活,需要慢慢调。

好吧,恭喜自己已经基本完成了分页查询用户信息的页面!!!

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
字节跳动 NoSQL 的实践与探索
随着 NoSQL 的蓬勃发展越来越多的数据存储在了 NoSQL 系统中,并且 NoSQL 和 RDBMS 的界限越来越模糊,各种不同的专用 NoSQL 系统不停涌现,各具特色,形态不一。本次主要分享字节跳动内部和火山引擎 NoSQL 的实践,希望能够给大家一定的启发。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论