☰
React后台权限系统:路由守卫+动态菜单+接口拦截一体化方案
2026/10/17 3:37:29 网站建设 项目流程

简介:这是一套基于 React + TypeScript + Vite 构建的轻量级后台管理系统模板,面向前端初学者与中级开发者,适用于快速搭建企业级管理后台原型或学习现代前端工程化实践。资源共76个文件,涵盖30个tsx(核心页面与组件)、16个ts(状态管理、工具函数与类型定义)、9个less(模块化样式)、5个json(配置与依赖清单)及配套HTML、SVG、图片与环境配置文件,整体包体仅544KB,结构精简、开箱即用。已有185人学习下载,适合希望掌握Vite构建流程、TypeScript类型约束实践、React Hooks状态管理及模块化路由设计的学习者。项目目录层次清晰,包含store(Pinia/Zustand风格状态管理雏形)、api(请求封装)、components(可复用UI组件)、router(动态路由配置)、layout(通用布局框架)等典型模块,附带完整README与多环境配置(development/stage/production),便于理解企业级项目组织范式。

1. 这不是又一个“Hello World”后台:它开箱即用跑通权限路由+动态菜单+接口拦截,新手照着 README 跑三步就能进首页,老手拿过来直接改业务模块不碰基建

上周帮某高校实验室搭教学管理后台,导师提了个“朴素需求”:学生提交实验报告后,助教能审阅打分,管理员能导出全量数据,但不同角色看到的菜单、能点的按钮、甚至接口返回字段都得严格隔离。我翻了七八个开源后台模板,要么权限逻辑写死在路由表里改起来像拆炸弹,要么状态管理散落在十几个组件里,加个新角色得全局搜role === 'admin'改十处。直到跑通这个 React + TypeScript + Vite 后台管理系统——它把权限校验压进路由守卫、菜单配置抽成 JSON Schema、API 层统一挂载 token 和错误重试,最关键是:所有权限开关都收在src/access/下一个文件里,改角色能力只要动三行代码。它不炫技,但每个模块都带着生产环境踩过的坑:比如菜单图标懒加载失败时自动 fallback 到文字,接口超时后二次请求前强制清空 loading 状态防 UI 卡死。适合需要快速交付、又不愿被“模板”绑架的中小项目团队。


2. 项目结构与技术选型:Vite 构建链 + TypeScript 类型守门 + Ant Design Pro 视觉基座

这个项目没走“自己造轮子”的路子,而是把 Vite 的冷启动速度、TypeScript 的类型安全、Ant Design Pro 的企业级组件库,焊成了一条高内聚低耦合的流水线。Vite 不是简单替代 Webpack——它让vite dev启动时间压到 300ms 内,HMR 更新粒度精确到单个 React 组件,改个按钮颜色不用等整个页面刷新;TypeScript 也不是加个.d.ts就完事,它把 API 响应体、路由元信息、权限策略全部定义成 interface,连菜单配置项的icon字段都限定了只能是string | ReactNode,避免运行时传错类型导致白屏;Ant Design Pro 则提供了开箱即用的登录页、403/404 页面、ProTable(带搜索/分页/列设置的表格)、ProLayout(支持多级菜单和面包屑的布局),省掉你从零搭 UI 框架的 80% 时间。关键在于,这三者不是简单堆砌,而是深度咬合:Vite 的defineConfig里直接注入 TypeScript 编译选项,Ant Design Pro 的ProTable组件泛型参数直接引用src/services/api.ts里定义的接口类型,改后端返回字段,前端表格列定义会立刻报错提醒你同步更新。

2.1 目录结构:五层职责分明,改业务不碰基建

项目目录按“基础设施 → 通用能力 → 业务视图 → 配置中心 → 入口胶水”分层,每层只做一件事:

src/ ├── assets/ # 静态资源:图标、字体、图片(不参与构建,直接拷贝) ├── components/ # 通用业务组件:SearchForm(带重置/搜索按钮的表单)、StatusTag(状态标签) ├── layouts/ # 布局组件:BasicLayout(主框架,含顶部导航+侧边菜单+内容区) ├── models/ # 状态管理:用 Zustand 实现,非 Redux(轻量、TypeScript 友好) │ ├── user.ts # 用户状态:token、角色、个人信息 │ └── menu.ts # 菜单状态:动态生成的菜单树、当前激活菜单 ├── pages/ # 页面组件:按路由组织,如 /user/list 对应 pages/user/List.tsx ├── services/ # 接口服务:Axios 封装,统一处理 token、loading、错误 │ ├── api.ts # 核心请求函数,返回 Promise<T> │ └── endpoints.ts # 所有 API 路径常量,如 USER_LIST = '/api/users' ├── utils/ # 工具函数:request.ts(Axios 实例)、auth.ts(权限校验工具) ├── access/ # 权限控制中心:核心!所有角色能力在此定义 │ └── index.ts # export const canAccess = (role: string, action: string) => boolean ├── routes/ # 路由配置:基于 React Router v6,支持动态路由加载 │ └── index.tsx # 导出路由数组,每项含 path、element、accessKey(对应 access/index.ts) ├── App.tsx # 根组件:包裹 ProLayout + 路由出口 └── main.tsx # 入口:初始化 Zustand store、挂载权限校验中间件

提示:src/access/index.ts是权限系统的“总开关”,它不依赖任何组件或 hooks,纯函数式设计。canAccess('admin', 'user:delete')返回 true/false,所有路由守卫和按钮渲染都调用它,改一处,全系统生效。

2.2 核心依赖版本与选型理由

依赖版本为什么选它替代方案为何被弃用
vite^4.5.0冷启动快、HMR 精准、插件生态成熟(如@vitejs/plugin-react-swc替代 Babel,编译提速 40%)Webpack 5 配置复杂,开发体验拖慢迭代节奏
typescript^5.2.0类型推导强、VS Code 支持好、与 React JSX 完美兼容(React.FC<Props>泛型)JavaScript 在大型后台中类型失控风险高,后期重构成本指数级上升
@ant-design/pro-layout^7.0.0内置菜单权限、面包屑、多语言、暗色模式,且源码开放可定制自研 Layout 需处理响应式折叠、菜单高亮、路由联动等细节,至少多花 3 人日
zustand^4.4.0无 Provider 嵌套、TypeScript 友好、状态更新原子化(set(state => ({...})))Redux Toolkit 虽好但引入createStore/Provider多一层抽象,对中等规模项目略重

2.3 路由与权限的协同机制:从 URL 到菜单的完整链路

权限不是挂在按钮上就完事,而是贯穿“用户访问 URL → 解析路由 → 渲染菜单 → 加载组件 → 控制按钮”的全链路。这套机制靠三个关键文件协同:

  1. src/routes/index.tsx定义路由元信息:
// src/routes/index.tsx import { lazy } from 'react'; import { RouteObject } from 'react-router-dom'; const UserList = lazy(() => import('@/pages/user/List')); const UserEdit = lazy(() => import('@/pages/user/Edit')); export const routes: RouteObject[] = [ { path: '/user', element: <UserList />, // 关键:accessKey 对应 access/index.ts 中的权限标识 handle: { accessKey: 'user:list' }, }, { path: '/user/:id/edit', element: <UserEdit />, handle: { accessKey: 'user:edit' }, }, ];
  1. src/access/index.ts定义权限规则:
// src/access/index.ts // 角色能力映射表:这里集中管理所有角色能做什么 const rolePermissions: Record<string, string[]> = { admin: ['user:list', 'user:edit', 'user:delete', 'report:export'], teacher: ['user:list', 'user:edit'], student: ['user:profile'], }; export const canAccess = (role: string, action: string): boolean => { return rolePermissions[role]?.includes(action) || false; };
  1. src/layouts/BasicLayout.tsx在渲染前校验:
// src/layouts/BasicLayout.tsx import { useNavigate, Outlet, useMatches } from 'react-router-dom'; import { useUserStore } from '@/models/user'; import { canAccess } from '@/access'; const BasicLayout = () => { const navigate = useNavigate(); const { role } = useUserStore(); const matches = useMatches(); // 获取当前匹配的路由栈 // 检查当前路由是否允许访问 const currentRoute = matches.at(-1); const accessKey = currentRoute?.handle?.accessKey as string | undefined; if (accessKey && !canAccess(role, accessKey)) { navigate('/403'); // 无权限跳转 return null; } return ( <ProLayout> <Outlet /> {/* 渲染子路由 */} </ProLayout> ); }; export default BasicLayout;

逻辑说明:当用户访问/user/123/edit时,useMatches()拿到匹配的路由对象,从中提取handle.accessKey(即'user:edit'),再调用canAccess(role, 'user:edit')查角色表。如果返回 false,立即navigate('/403'),且return null阻止后续渲染。整个过程在 Layout 组件内完成,无需每个页面单独写校验逻辑。


3. 快速启动与本地开发:三步跑通,五步验证核心流程

别被“后台管理系统”吓住,这个项目刻意降低了启动门槛。它不依赖 Docker、不强制 Node.js 版本、不搞复杂的环境变量注入,所有配置都收敛在vite.config.ts和.env里。我一般让新人用这五步验证是否真正跑通:能登录 → 能看菜单 → 能进列表页 → 能调接口 → 能改权限。只要这五步走通,说明环境、配置、权限链全都没问题。

3.1 环境准备:Node.js 与包管理器

  • Node.js 版本:必须>= 18.0.0(Vite 4.5+ 要求)。执行node -v检查,低于则升级。
  • 包管理器:推荐pnpm(比 npm/yarn 更快、更省磁盘)。若未安装,执行:
    npm install -g pnpm
  • 为什么不用 npm?pnpm的硬链接机制让node_modules体积减少 70%,pnpm install速度比npm install快 2~3 倍,尤其在 CI/CD 环境下优势明显。

3.2 启动项目:三步命令搞定

# 1. 克隆并进入项目 git clone <your-repo-url> cd your-project-name # 2. 安装依赖(pnpm 会自动创建 pnpm-lock.yaml) pnpm install # 3. 启动开发服务器(默认 http://localhost:5173) pnpm dev

注意:pnpm dev实际执行的是vite,它会自动读取vite.config.ts。如果端口被占用,Vite 会提示Would you like to run the app on another port instead?,输入y即可。

3.3 验证核心流程:五步闭环测试

步骤操作预期结果验证意义
① 登录访问http://localhost:5173/login,输入默认账号admin/admin跳转至/首页,右上角显示 “欢迎,admin”证明登录逻辑、token 存储、用户状态初始化成功
② 菜单点击左侧菜单栏显示 “用户管理”、“系统设置”、“报表分析” 三级菜单证明src/models/menu.ts动态菜单加载、权限过滤(student 角色登录只显示 “个人中心”)
③ 列表页点击 “用户管理 > 用户列表”页面渲染 ProTable,显示模拟数据(姓名、邮箱、状态)证明路由加载、页面组件挂载、基础 UI 渲染无误
④ 接口调用打开浏览器开发者工具 → Network 标签页,刷新列表页看到GET /api/users请求,状态码200,响应体为{"data": [...], "total": 10}证明src/services/api.ts封装的请求函数、Axios 拦截器(自动加 token)、Mock 服务(见下文)全部工作正常
⑤ 权限切换修改src/models/user.ts中role为'student',刷新页面左侧菜单只剩 “个人中心”,点击 “用户管理” 无反应或跳转 403证明canAccess()函数生效,权限校验链路打通

3.4 Mock 接口:前端独立开发不依赖后端

项目内置了vite-plugin-mock,所有 API 请求在开发环境自动转发到 Mock 服务,无需启动后端。Mock 数据定义在mock/目录:

// mock/user.ts import { MockMethod } from 'vite-plugin-mock'; export default [ { url: '/api/users', method: 'get', response: ({ query }) => { // 支持分页参数:?current=1&pageSize=10 const { current = 1, pageSize = 10 } = query; const list = Array.from({ length: 20 }, (_, i) => ({ id: i + 1, name: `用户${i + 1}`, email: `user${i + 1}@example.com`, status: i % 3 === 0 ? 'active' : 'inactive', })); return { code: 0, data: list.slice((Number(current) - 1) * Number(pageSize), Number(current) * Number(pageSize)), total: 20, }; }, }, ] as MockMethod[];

逻辑说明:当页面调用api.getUsers({ current: 1, pageSize: 10 })时,Vite 开发服务器捕获该请求,执行response函数,返回模拟的分页数据。code: 0是约定的成功码,前端services/api.ts里的拦截器会据此 resolve Promise。这样,前端可以完全脱离后端并行开发,等后端接口 Ready 后,只需把vite.config.ts中的enable: true改为false,即可切回真实接口。


4. 常见问题排查:五个血泪经验总结,专治启动失败、菜单不显、接口 401

这个项目在多个团队落地过,以下问题是高频翻车点。它们不是文档没写清楚,而是环境、配置、认知偏差导致的“玄学”问题。我每次帮人 debug,90% 时间花在这五类上。记录下来,省得你重复踩坑。

4.1 现象:pnpm dev启动后页面空白,控制台报错ReferenceError: React is not defined

  • 原因:Vite 4.5+ 默认使用@vitejs/plugin-react-swc(基于 SWC 编译器),它要求React必须在组件文件顶部显式导入,不能依赖全局React变量。而旧版项目可能还留着/** @jsxImportSource react */注释或React.createElement手写调用。
  • 解决:检查所有.tsx文件,确保第一行是import React from 'react';或使用 React 18 的自动导入(import { useState } from 'react';即可,无需单独 import React)。删除所有/** @jsxImportSource react */注释。如果用了自定义 JSX 运行时,需在vite.config.ts中配置:
    // vite.config.ts import react from '@vitejs/plugin-react-swc'; export default defineConfig({ plugins: [ react({ jsxImportSource: 'react', // 显式指定 }), ], });

4.2 现象:登录成功后菜单栏为空,Network 里看不到/api/menus请求

  • 原因:菜单数据由src/models/menu.ts的 Zustand store 管理,其初始化逻辑在src/models/menu.ts的useMenuStorehook 中。如果main.tsx里没有正确初始化 store,或者BasicLayout没包裹在ZustandProvider中,store 就不会触发fetchMenus()。
  • 解决:确认main.tsx是否包含:
    // main.tsx import { Provider } from 'react-redux'; // ❌ 错误!这是 Redux 的 Provider // ✅ 正确:Zustand 不需要 Provider,但 store 初始化必须在根组件执行 import { useMenuStore } from '@/models/menu'; ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode>, ); // App.tsx 中确保首次渲染时触发菜单加载 const App = () => { const { fetchMenus } = useMenuStore(); useEffect(() => { fetchMenus(); // 关键:主动拉取菜单 }, [fetchMenus]); return <BasicLayout />; };
    同时检查src/models/menu.ts的fetchMenus函数是否正确调用api.getMenus(),且该 API 在mock/menu.ts中有对应定义。

4.3 现象:列表页能渲染,但点击“编辑”按钮报错Cannot read properties of undefined (reading 'id')

  • 原因:ProTable的rowKey默认是'id',但如果后端返回的数据字段是_id或userId,而你没在columns配置中指定rowKey,就会导致record.id为 undefined。
  • 解决:在pages/user/List.tsx的ProTable组件中,显式设置rowKey:
    <ProTable rowKey="userId" // 👈 关键:与后端返回字段名一致 columns={[ { dataIndex: 'userId', title: 'ID' }, { dataIndex: 'name', title: '姓名' }, ]} />
    或者,在src/services/endpoints.ts中统一将后端字段映射为前端标准字段(如userId→id),在api.ts的响应拦截器里做转换。

4.4 现象:修改src/access/index.ts的权限规则后,页面没变化,仍能访问受限路由

  • 原因:Vite 的 HMR(热模块替换)默认不监听src/access/目录下的文件变更,因为它是纯逻辑函数,没有导出 React 组件。修改后需手动刷新页面。
  • 解决:两种方式任选其一:
    1. 手动刷新:改完access/index.ts后,按Ctrl+R(Windows)或Cmd+R(Mac)强制刷新。
    2. 配置 HMR 监听:在vite.config.ts中添加:
      export default defineConfig({ server: { hmr: { overlay: true, // 强制监听 access 目录 watchOptions: { include: ['src/access/**'], }, }, }, });

4.5 现象:登录后接口请求头没带Authorization,返回 401

  • 原因:src/services/api.ts中的 Axios 请求拦截器,依赖src/models/user.ts的token状态。如果userStore初始化失败(如localStorage.getItem('token')为空),或拦截器注册顺序错误(在 store 初始化前就创建了 axios 实例),token 就不会被注入。
  • 解决:检查src/services/api.ts的创建时机:
    // ✅ 正确:在 store 初始化后,再创建 axios 实例 import { getUserToken } from '@/models/user'; const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, }); // 请求拦截器必须在 store 初始化后注册 request.interceptors.request.use( (config) => { const token = getUserToken(); // 从 store 或 localStorage 读取 if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), );
    同时确认main.tsx中useUserStore的初始化逻辑是否在request创建之前执行。

5. 核心模块代码逐行解析:从登录到菜单渲染的完整数据流

现在我们把镜头拉近,聚焦在“用户登录成功后,如何驱动左侧菜单动态渲染”这一高频场景。这不是简单的setState,而是一条横跨API 层 → 状态层 → 路由层 → 视图层的数据流。我会逐行拆解src/pages/login/LoginForm.tsx(登录表单)、src/services/api.ts(请求封装)、src/models/menu.ts(菜单状态)、src/layouts/BasicLayout.tsx(菜单渲染)四个关键文件,告诉你数据如何从后端接口,最终变成你看到的菜单项。

5.1 登录表单:src/pages/login/LoginForm.tsx

// src/pages/login/LoginForm.tsx import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useUserStore } from '@/models/user'; import { login } from '@/services/api'; const LoginForm = () => { const [loading, setLoading] = useState(false); const navigate = useNavigate(); const { setUser } = useUserStore(); // 👈 Zustand 的 action,用于更新用户状态 const handleSubmit = async (values: { username: string; password: string }) => { setLoading(true); try { // 1. 调用 API 层的 login 函数 const res = await login(values); // 2. 登录成功,将 token 和用户信息存入 Zustand store setUser({ token: res.data.token, role: res.data.role, name: res.data.name, }); // 3. 跳转到首页 navigate('/'); } catch (error) { console.error('Login failed:', error); } finally { setLoading(false); } }; return ( <Form onFinish={handleSubmit}> <Form.Item name="username" rules={[{ required: true }]}> <Input placeholder="用户名" /> </Form.Item> <Form.Item name="password" rules={[{ required: true }]}> <Input.Password placeholder="密码" /> </Form.Item> <Button type="primary" htmlType="submit" loading={loading}> 登录 </Button> </Form> ); }; export default LoginForm;

参数说明:

  • useUserStore():Zustand hook,返回{ setUser }action。setUser是一个函数,接收{ token, role, name }对象,内部调用set()更新 store。
  • login(values):来自src/services/api.ts的封装函数,返回Promise<LoginResponse>,其中LoginResponse是 TypeScript 接口,定义了data.token和data.role的类型。
  • navigate('/'):React Router 的导航函数,触发路由跳转,进而触发BasicLayout渲染。

5.2 API 封装:src/services/api.ts

// src/services/api.ts import axios from 'axios'; import { LoginParams, LoginResponse } from '@/services/types'; // 👈 类型定义文件 // 创建 axios 实例 const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000, }); // 请求拦截器:添加 token request.interceptors.request.use( (config) => { // 从 localStorage 读取 token(兜底,以防 store 未初始化) const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), ); // 响应拦截器:统一处理错误 request.interceptors.response.use( (response) => { // 后端约定:code === 0 表示成功 if (response.data.code === 0) { return response.data; // 返回 data 字段,而非整个 response } else { throw new Error(response.data.message || '请求失败'); } }, (error) => { if (error.response?.status === 401) { // token 失效,清空本地状态并跳转登录页 localStorage.removeItem('token'); window.location.href = '/login'; } return Promise.reject(error); }, ); // 导出具体的 API 函数 export const login = (params: LoginParams) => { return request.post<LoginResponse>('/login', params); }; // 其他 API... export const getMenus = () => { return request.get<MenuResponse[]>('/menus'); // 👈 菜单接口,供 menu store 调用 };

逻辑说明:login函数返回一个Promise<LoginResponse>,其中LoginResponse在src/services/types.ts中定义:

// src/services/types.ts export interface LoginParams { username: string; password: string; } export interface LoginResponse { data: { token: string; role: 'admin' | 'teacher' | 'student'; name: string; }; }

这样,await login(values)的返回值类型就是LoginResponse,TS 编译器能确保res.data.token一定存在且是字符串类型,避免运行时undefined错误。

5.3 菜单状态管理:src/models/menu.ts

// src/models/menu.ts import { create } from 'zustand'; import { getMenus } from '@/services/api'; import { MenuDataItem } from '@/types/menu'; // 👈 菜单数据类型定义 interface MenuState { menuList: MenuDataItem[]; loading: boolean; fetchMenus: () => Promise<void>; } export const useMenuStore = create<MenuState>((set) => ({ menuList: [], loading: false, // 👇 关键:fetchMenus 是一个异步 action fetchMenus: async () => { set({ loading: true }); try { // 调用 API 层的 getMenus 函数 const res = await getMenus(); // 更新 store 状态 set({ menuList: res.data, loading: false }); } catch (error) { console.error('Fetch menus failed:', error); set({ loading: false }); } }, }));

MenuDataItem类型定义(src/types/menu.ts):

export interface MenuDataItem { key: string; // 路由 path,如 '/user/list' name: string; // 菜单项名称,如 '用户列表' icon?: string; // 图标名,如 'UserOutlined' children?: MenuDataItem[]; // 子菜单 }

注意:fetchMenus是一个async函数,但它在 Zustand 中被声明为() => Promise<void>,这意味着调用方(如App.tsx)必须用await或.then()处理。这就是为什么在App.tsx中要用useEffect(() => { fetchMenus(); }, []),而不是直接fetchMenus()。

5.4 菜单渲染:src/layouts/BasicLayout.tsx

// src/layouts/BasicLayout.tsx import { useMenuStore } from '@/models/menu'; import { ProLayout, PageContainer } from '@ant-design/pro-layout'; import { Outlet } from 'react-router-dom'; const BasicLayout = () => { const { menuList, loading } = useMenuStore(); // 👈 订阅菜单状态 return ( <ProLayout // 👇 关键:menuDataRender 返回菜单数据,ProLayout 会自动渲染 menuDataRender={() => menuList} // 当菜单数据为空时显示 loading menuItemRender={(item, dom) => ( <div onClick={() => item.path && window.location.assign(item.path)}> {dom} </div> )} // 侧边菜单宽度 siderWidth={256} // 顶部标题 title="后台管理系统" // logo logo={<div>LOGO</div>} // 当前路径高亮 location={{ pathname: window.location.pathname, }} > {/* 内容区 */} <PageContainer> <Outlet /> </PageContainer> </ProLayout> ); }; export default BasicLayout;

逻辑说明:ProLayout的menuDataRender属性是一个函数,它返回一个MenuDataItem[]数组。useMenuStore()订阅了menuList状态,一旦fetchMenus()更新了menuList,menuDataRender就会重新执行,ProLayout自动重新渲染菜单。menuItemRender则控制每个菜单项的点击行为,这里简单地用window.location.assign跳转,你也可以换成useNavigate。


6. 二次开发实战:如何新增一个“设备管理”模块并接入权限系统

现在你已经跑通了项目,下一步往往是加业务功能。以“设备管理”为例,它需要:新增路由 → 新建页面 → 定义接口 → 配置菜单 → 接入权限。整个过程不需要动基建代码,所有改动都在src/下完成。我习惯用“四步法”:先搭骨架 → 再填血肉 → 接入权限 → 最后联调。下面带你走一遍,每一步都给出可复制的代码和避坑点。

6.1 第一步:搭骨架——新增路由与页面结构

在src/routes/index.tsx中添加新路由:

// src/routes/index.tsx import { lazy } from 'react'; import { RouteObject } from 'react-router-dom'; // 👇 新增:设备管理路由 const DeviceList = lazy(() => import('@/pages/device/List')); const DeviceDetail = lazy(() => import('@/pages/device/Detail')); export const routes: RouteObject[] = [ // ... 其他路由 { path: '/device', element: <DeviceList />, handle: { accessKey: 'device:list' }, // 👈 权限标识,先占位 }, { path: '/device/:id', element: <DeviceDetail />, handle: { accessKey: 'device:detail' }, }, ];

然后创建页面文件夹和基础组件:

mkdir -p src/pages/device
// src/pages/device/List.tsx import { ProTable } from '@ant-design/pro-components'; const DeviceList = () => { return ( <ProTable rowKey="id" columns={[ { dataIndex: 'id', title: '设备ID' }, { dataIndex: 'name', title: '设备名称' }, { dataIndex: 'status', title: '状态' }, ]} request={async () => { // 模拟请求,后续替换成真实 API return { data: [], total: 0 }; }} /> ); }; export default DeviceList;

避坑:ProTable的request属性必须是async函数,返回{ data: any[], total: number }。如果直接写request={() => []}会报错,因为 ProTable 期望 Promise。

6.2 第二步:填血肉——定义接口与服务

在src/services/endpoints.ts中添加设备相关路径:

// src/services/endpoints.ts export const DEVICE_LIST = '/api/devices'; export const DEVICE_DETAIL = '/api/devices/:id';

在src/services/api.ts中添加接口函数:

// src/services/api.ts export const getDevices = (params: { current: number; pageSize: number }) => { return request.get<{ data: DeviceItem[]; total: number }>(DEVICE_LIST, { params }); }; export const getDeviceDetail = (id: string) => { return request.get<{ data: DeviceItem }>(DEVICE_DETAIL.replace(':id', id)); }; // 设备类型定义 export interface DeviceItem { id: string; name: string; status: 'online' | 'offline' | 'maintenance'; lastOnline: string; }

更新src/pages/device/List.tsx的request:

// src/pages/device/List.tsx import { getDevices } from '@/services/api'; const DeviceList = () => { return ( <ProTable rowKey="id" columns={[ { dataIndex: 'id', title: '设备ID' }, { dataIndex: 'name', title: '设备名称' }, { dataIndex: 'status', title: '状态' }, ]} request={async (params) => { const res = await getDevices(params); return { data: res.data, success: true, total: res.total, }; }} /> ); };

6.3 第三步:接入权限——配置菜单与权限规则

在src/access/index.ts中添加新权限:

// src/access/index.ts const rolePermissions: Record<string, string[]> = { admin: [ 'user:list', 'user:edit', 'device:list', // 👈 新增 'device:detail', // 👈 新增 ], teacher: ['user:list', 'device:list'], // 👈 教师也能看设备列表 student: ['user:profile'], };

在src/models/menu.ts的fetchMenus中,确保返回的菜单数据包含设备项。如果你的后端/api/menus接口已支持,这步可跳过;否则,需在 Mock 中补充:

// mock/menu.ts export default [ { url: '/api/menus', method: 'get', response: () => <p> <a href="https://download.csdn.net/download/sinat_36612213/89618925" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询