使用vite+react+ts+Ant Design开发后台管理项目(四)

时间:2024-09-29 19:09:19

  前言


本文将引导开发者从零基础开始,运用vite、react、react-router、react-redux、Ant Design、less、tailwindcss、axios等前沿技术栈,构建一个高效、响应式的后台管理系统。通过详细的步骤和实践指导,文章旨在为开发者揭示如何利用这些技术工具,从项目构思到最终实现的全过程,提供清晰的开发思路和实用的技术应用技巧。

 项目gitee地址:lbking666666/enqi-admin

 本系列文章:

  • 使用vite+react+ts+Ant Design开发后台管理项目(一)
  • 使用vite+react+ts+Ant Design开发后台管理项目(二)
  • 使用vite+react+ts+Ant Design开发后台管理项目(三)
  • 使用vite+react+ts+Ant Design开发后台管理项目(四)

本章节添加菜单对应的路由页面、菜单的数据使用mock模拟接口,菜单数据的请求

菜单页面

路由页面

在src文件夹下新增pages文件夹,新增三个文件夹home\setting\shop

1.新增首页文件,在home文件夹下新增index.tsx

const Home = ()=>{
    return (
        <div>
        <h1>Home</h1>
    </div>
    )
}
export default Home;

2.新增用户管理和角色管理页面,在setting文件夹下新增role.tsx和user.tsx

//role.tsx
const Role = () => {
  return (<div>Role</div>);
};
export default Role;
//user.tsx
const user = ()=>{
    return(<div>User</div>)
}
export default user;

 3. 新增商品分类、订单管理、商品管理页面,在shop文件夹下新增category.tsx、order.tsx和product.tsx

//category
const Category = ()=>{
    return(
        <div>
            <h1>Category</h1>
        </div>
    )
}
export default Category;
//order.tsx
const Order = ()=>{
    return (
        <div>Order</div>
    )
}
export default Order;
//product.tsx
const Product = ()=>{
    return(
        <div>
            Product
        </div>
    )   
}
export default Product;

路由配置

把新建的路由页面引入到路由文件中并添加一个重定向访问/时候重定向到/home,使用react-router中的Navigte组件

import { createBrowserRouter, Navigate } from "react-router-dom";

import AppLayout from "@/layout/index";
import Home from "@/pages/home";
import Role from "@/pages/setting/role";
import User from "@/pages/setting/user";
import Category from "@/pages/shop/category";
import Product from "@/pages/shop/product";
import Order from "@/pages/shop/order";

const routers = createBrowserRouter([
  {
    path: "/",
    element: <AppLayout />,
    children: [
      {
        path: "/",
        element: <Navigate to="/home" />,
      },
      {
        path: "/home",
        element: <Home />,
      },
      {
        path: "/setting/role",
        element: <Role />,
      },
      {
        path: "/setting/user",
        element: <User />,
      },
      {
        path: "/shop/category",
        element: <Category />,
      },
      {
        path: "/shop/product",
        element: <Product />,
      },
      {
        path: "/shop/order",
        element: <Order />,
      },
    ],
  },
]);

export default routers;

路由呈现

路由配置完成怎么能在游览器中输入不同的路由地址显示当前路由对应的内容呢,这里使用react-router中的Outlet组件。

修改layout文件夹下的main.tsx文件

import { Layout, theme } from "antd";
import {Outlet}  from 'react-router-dom'

const { Content } = Layout;
const AppMain = () => {
  const {
    token: { colorBgContainer, borderRadiusLG },
  } = theme.useToken();
  return (
    <Content
      style={{
        margin: "24px 16px",
        padding: 24,
        minHeight: 280,
        background: colorBgContainer,
        borderRadius: borderRadiusLG,
      }}
    >
      <Outlet />
    </Content>
  );
};
export default AppMain;

菜单数据

这里使用mock来模拟接口数据

引入mock

npm i mockjs @types/mockjs vite-plugin-mock -D

配置mock

vite-plugin-mock提供本地和生产模拟服务。

vite 的数据模拟插件,是基于 vite.js 开发的。 并同时支持本地环境和生产环境。 Connect 服务中间件在本地使用,mockjs 在生产环境中使用。

修改vite.config.ts文件

import * as path from 'path';
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    viteMockServe({ 
      mockPath: './src/mock', // mock文件夹路径默认是 src/mock
      enable: true, // 默认是 false,可以根据环境变量开启
    }),
  ],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, './src') // 路径别名
    }
  }
})

菜单项属性

在src文件夹下新增types文件夹,在types文件夹下新增menu.d.ts 

// 菜单项属性
export interface MenuItemProps {
    id?: string;
    key:string;
    icon?:string;
    label:string;
    children?:MenuItemProps[];
  }

菜单mock数据

在src文件夹下新增mock文件夹

这里的文件夹名称需要和vite配置中viteMockServe的地址一致

 新增menu.ts

import Mock from "mockjs";
import { MenuItemProps } from "@/types/menu.d";
// 修正icon的类型问题,因为JSX元素不能作为JSON对象的一部分,这里已经改为字符串
const items:MenuItemProps[] = [
  {
    id: Mock.mock("@id"),
    key: "home",
    icon: "home",
    label: "首页",
  },
  {
    id: Mock.mock("@id"),
    key: "setting",
    icon: "setting",
    label: "系统管理",
    children: [
      {
        key: "user",
        label: "用户管理",
      },
      {
        key: "role",
        label: "角色管理",
      },
    ],
  },
  {
    id: Mock.mock("@id"),
    key: "shop",
    icon: "shop",
    label: "商城管理",
    children: [
      {
        key: "category",
        label: "商品分类",
      },
      {
        key: "product",
        label: "商品管理",
      },
      {
        key: "order",
        label: "订单管理",
      }
    ],
  }
];

export default [
  // 用户登录
  {
    url: "/api/menu",
    method: "GET",
    response: () => {
      return {
        code: 200,
        success: true,
        message: "请求成功。",
        data: items,
      };
    },
  },
];

菜单请求

引入axios

npm install axios

封装axios

在src文件夹下新增api文件夹,在api文件夹下新增request.ts文件

//request.ts
import axios, { AxiosRequestConfig } from "axios";
//接口返回数据
export interface ApiRes<T> {
  success: boolean;
  code: number;
  data?: T;
  message: string;
}
const instance = axios.create({
  baseURL: "/api",
  timeout: 5000,
});
// 添加请求拦截器
instance.interceptors.request.use(
  function (config) {
    // 请求成功做点什么
    return config;
  },
  function (error) {
    // 对请求错误做点什么
    return Promise.reject(error);
  }
);
// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    // 对响应成功做点什么
    return response.data;
  },
  function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);

export default async <T>(config: AxiosRequestConfig) => {
  const response: ApiRes<T> = await instance(config);
  return response;
};

菜单请求

在api文件夹下新增menu.ts

//menu.ts
import request from './request'

// 获取当前用户信息
export const getMenu = () => {
  return request({
    method: 'GET',
    url: '/menu'
  })
}

菜单完善

菜单组件

在layout文件夹下新增menu.tsx

//layout/menu.tsx
import React from "react";
import { HomeOutlined, SettingOutlined, ShopOutlined } from "@ant-design/icons";
import { Menu } from "antd";
import { MenuItemProps } from "@/types/menu";
import { useNavigate } from "react-router-dom";

// 图标映射
const Icons = {
  home: HomeOutlined,
  setting: SettingOutlined,
  shop: ShopOutlined,
};
// 获取图标组件
const IconByName: React.FC<{ iconName: string }> = ({ iconName }) => {
  // 获取图标组件
  const IconComponent = Icons[iconName as keyof typeof Icons];
  // 返回图标组件
  return IconComponent ? <IconComponent /> : null;
};

// 侧边栏
const AppMenu: React.FC<{ menu: MenuItemProps[] }> = ({ menu }) => {
  const navigate = useNavigate();

  const handleMenu = ({ keyPath }: { keyPath: string[] }) => {
    const routerKey: string = keyPath.reverse().join("/");
    navigate( routerKey );
  };
  const menuData = menu.map((item: MenuItemProps) => {
    return {
      key: item.key,
      label: item.label,
      icon: item.icon ? <IconByName iconName={item.icon} /> : undefined,
      children: item.children?.map((child) => ({
        key: child.key,
        label: child.label,
      })),
    };
  });
  return (
    <Menu onClick={handleMenu} theme="dark" mode="inline" items={menuData} />
  );
};

export default AppMenu;

接口请求

修改layout文件夹下的sider.tsx

import React, { useEffect, useState } from "react";
import { Layout } from "antd";
import { getMenu } from "../api/menu";
import AppMenu from "./menu";
import { MenuItemProps } from "@/types/menu";
const { Sider } = Layout;

// 侧边栏
const AppSider: React.FC<{ collapsed: boolean }> = ({ collapsed }) => {
  // 菜单数据
  const [menu, setMenu] = useState([] as MenuItemProps[]);
  // 获取菜单数据
  useEffect(() => {
    // 获取菜单数据
    const getData = async () => {
      const res = await getMenu();
      const menuData = res?.data as MenuItemProps[];
      // 设置菜单数据
      setMenu([...menuData]);
    };
    getData();
  }, []);
  // 返回侧边栏
  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="demo-logo-vertical" />
      <AppMenu menu={menu} />
    </Sider>
  );
};

export default AppSider;

 效果预览

在network中可以看到接口请求成功,菜单渲染出来,点击后可以正常跳转页面

一些说明

1.菜单中点击后路由跳转使用了react-router中的useNavigate

2.请求接口使用useEffect

3.查看接口请求发现请求了两次这里是因为使用了严格模式StrictMode

严格模式启用了以下仅在开发环境下有效的行为:

  • 组件将 重新渲染一次,以查找由于非纯渲染而引起的错误。
  • 组件将 重新运行 Effect 一次,以查找由于缺少 Effect 清理而引起的错误。
  • 组件将被 检查是否使用了已弃用的 API

后续 

本篇文章为项目使用axios和mock模拟接口,代码已经同步到了gitee仓库,下一篇丰富具体的页面