1. 为什么同一个 Admin 后台要折腾五种技术栈后台管理系统这东西做前端的谁没写过几套。CRUD 表格、权限菜单、登录鉴权、图表看板翻来覆去就那些东西。但真正让我下决心用五种技术栈各写一遍同一个 Admin 的是去年接的一个外包咨询项目——客户团队前端用 Vue2 写了一半后端是 NestJS老板又听说 React 生态好想换运维那边还压着一套老 Pear Admin Boot 的 Java 版本要迁移。五拨人五个说法谁也说服不了谁。与其开会扯皮不如直接动手。我花了大概三周时间把同一套 Admin 系统的核心功能——登录、动态路由、权限控制、数据表格、图表看板、文件上传——分别用 React、Vue3、NestJS 全栈、Pear Admin Boot、以及一个极简 Vue3 Admin 模板各实现了一遍。这篇文章不是要评出谁好谁坏那种结论毫无意义。我想聊的是同一件事在不同技术栈里的实现路径差异到底在哪哪些坑是共通的哪些是某个栈独有的。先说清楚这套 Admin 系统到底要什么。核心需求就六条用户登录拿到 token、根据角色动态生成侧边栏菜单、路由级别的权限拦截、一个带分页搜索的订单列表、一个 ECharts 折线图看板、以及文件上传。听起来简单但每一条在不同栈里的实现逻辑差别很大。比如动态路由React 里靠react-router的配置式路由配合状态管理动态注入Vue3 里用router.addRoute()在导航守卫里动态添加NestJS 全栈方案里前端部分其实还是 Vue 或 React但后端要负责把菜单树算好返回。关键词里提到的 VibeCoding 这个词我理解成一种跟着感觉走、快速试错的开发状态。这套多栈实现本身就是 VibeCoding 的产物——不追求一步到位先跑起来再说跑不通就换条路。下面我按技术栈拆开讲每个栈都会给出核心代码、踩过的坑、以及我个人的取舍理由。提示本文所有代码都是可运行的最小实现省略了样式和部分样板代码。完整跑通需要 Node 18 和对应的包管理器。2. React 栈动态路由与状态管理的硬仗2.1 路由配置式 vs 组件式我为什么选配置式React 生态里做 Admin路由方案基本就两条路react-router的配置式v6 的createBrowserRouter和组件式Routes里写Route。我一开始用的是组件式写起来直观但做到动态权限路由的时候直接卡住了——组件式路由是静态声明的你没法在运行时根据后端返回的菜单树动态增删路由。后来换成配置式核心思路是先定义一份完整的路由表包含所有可能的路由然后根据用户权限过滤出可访问的部分再用createBrowserRouter生成实际路由。代码大概长这样// routes.js export const allRoutes [ { path: /dashboard, element: Dashboard /, meta: { roles: [admin, user] } }, { path: /orders, element: Orders /, meta: { roles: [admin] } }, { path: /settings, element: Settings /, meta: { roles: [admin] } }, ]; // 根据权限过滤 export function filterRoutes(routes, userRoles) { return routes.filter(r r.meta.roles.some(role userRoles.includes(role))); }然后在 App 组件里用useMemo根据当前用户角色生成路由。这里有个坑createBrowserRouter必须在组件外调用但用户角色是异步获取的。我的解法是用一个顶层RouterProvider配合useState存路由实例角色变化时重新创建 router。实测下来这样会有一次闪烁但可以接受。2.2 状态管理选型Zustand 还是 Redux ToolkitReact 这边状态管理我试了三个Redux Toolkit、Zustand、Jotai。最后选了 Zustand理由很简单——Admin 系统的状态不复杂无非是用户信息、权限列表、菜单折叠状态、主题色。Redux Toolkit 的样板代码太多一个简单的用户信息要写 slice、reducer、action、selector 四层。Zustand 一个create就搞定import { create } from zustand; export const useUserStore create((set) ({ userInfo: null, roles: [], setUser: (info) set({ userInfo: info, roles: info.roles }), logout: () set({ userInfo: null, roles: [] }), }));Jotai 的原子化思路在 Admin 里反而显得零碎不如 Zustand 的集中式 store 好维护。这里有个经验Admin 系统的状态天然是全局单例性质的用户信息、权限、主题这些在整个应用生命周期里只有一份用集中式 store 比原子化更贴合心智模型。2.3 权限拦截的三种粒度React 里做权限拦截我分了三个粒度。第一层是路由级在路由配置里加loader函数没权限直接redirect。第二层是组件级写一个AuthWrapper roles{[admin]}组件没权限渲染 403 页面。第三层是按钮级用usePermissionhook 返回一个hasPermission(code)函数按钮上判断。function usePermission() { const roles useUserStore(s s.roles); return { hasPermission: (required) roles.some(r required.includes(r)), }; }实测下来路由级拦截最稳因为它在数据加载前就生效。组件级和按钮级容易漏尤其是新来的同事不知道要包一层。我的建议是路由级必须做组件级和按钮级按需做但要在团队规范里写清楚哪些页面必须包。2.4 React 图表与表格的性能陷阱图表用的 ECharts表格用的 Ant Design Table。这两个在数据量大的时候都有坑。ECharts 的问题是每次setOption都会触发重绘如果数据是轮询更新的页面会卡。解法是用useRef存 chart 实例更新时用chart.setOption(option, { notMerge: false })做增量更新。Ant Design Table 的坑在rowKey。如果不指定rowKeyReact 会用索引做 key分页切换时会出现行状态错乱比如选中状态跑到别的行上。一定要用数据的唯一 ID 做rowKey。另外如果表格列超过 20 列建议开virtual虚拟滚动否则滚动会掉帧。3. Vue3 栈组合式 API 下的动态路由与权限3.1 addRoute 的时机与坑Vue3 的动态路由核心就一个 APIrouter.addRoute()。但什么时候调用它是个大学问。我试过三个时机登录成功后、路由守卫里、以及 App 挂载前。登录成功后调用最直观但有个问题——如果用户刷新页面路由就丢了因为addRoute添加的路由不会持久化。所以必须在路由守卫里做。我的做法是在router.beforeEach里判断如果已登录但路由表为空就拉取菜单树并addRoute然后next({ ...to, replace: true })重新导航一次。router.beforeEach(async (to, from, next) { const userStore useUserStore(); if (!userStore.token) { return to.path /login ? next() : next(/login); } if (userStore.routes.length 0) { const menus await fetchMenus(); menus.forEach(menu { router.addRoute(Layout, { path: menu.path, component: () import(../views/${menu.component}.vue), meta: { roles: menu.roles }, }); }); userStore.setRoutes(menus); return next({ ...to, replace: true }); } next(); });这里有个大坑next({ ...to, replace: true })如果不加replace: true会在历史记录里多一条用户点返回会回到空白页。另外addRoute的父路由名必须和实际注册的 Layout 路由名一致否则路由会挂到根上侧边栏高亮会失效。3.2 组合式 API 的状态复用Pinia 的取舍Vue3 的状态管理我用了 Pinia。相比 VuexPinia 的setup写法更贴近组合式 API 的心智。用户 store 大概这样export const useUserStore defineStore(user, () { const token ref(localStorage.getItem(token) || ); const roles ref([]); const routes ref([]); function setToken(t) { token.value t; localStorage.setItem(token, t); } function setRoutes(r) { routes.value r; } return { token, roles, routes, setToken, setRoutes }; });Pinia 的好处是ref和computed直接能用不用像 Vuex 那样区分 state、getters、mutations。但有个细节要注意Pinia 的 store 在路由守卫里使用时必须在守卫函数内部调用useUserStore()不能在模块顶层调用否则会报 getActivePinia was called with no active Pinia。3.3 选项式与组合式混合开发的现实关键词里有个vue 组合式和选项式混合开发这个我实际遇到过。老项目是选项式写的新页面想用组合式两者能共存但有几个注意点。第一this在组合式里不可用所以选项式里的this.$router要换成useRouter()。第二生命周期钩子要对应created对应setup里的直接执行mounted对应onMounted。第三mixins和组合式函数不要混用容易出命名冲突。我的建议是新页面一律用组合式老页面不动但要在setup里通过getCurrentInstance()拿到this来兼容老代码。虽然不优雅但迁移期这是最省事的方案。3.4 Vue 的权限指令与按钮级控制Vue 里做按钮级权限最优雅的是自定义指令app.directive(permission, { mounted(el, binding) { const roles useUserStore().roles; if (!binding.value.some(r roles.includes(r))) { el.parentNode?.removeChild(el); } }, });用的时候直接button v-permission[admin]删除/button。但这里有个坑如果权限是异步获取的指令mounted时 roles 还是空的按钮会被误删。解法是在指令里加updated钩子或者确保权限在路由守卫里已经加载完再渲染页面。我选后者因为更简单。4. NestJS 全栈后端算菜单树前端只管渲染4.1 为什么把菜单树放到后端算NestJS 这套方案里我做了一个和前面两个栈不一样的决定菜单树在后端算好前端只负责渲染。理由是权限逻辑本来就该在后端前端算菜单树等于把权限规则暴露在客户端而且前后端两套权限规则容易不一致。后端用 NestJS 的 Guard 和 Interceptor 做权限。Guard 负责路由级拦截Interceptor 负责给响应统一加包装。菜单树的生成逻辑放在一个MenuService里根据用户的角色从数据库查菜单递归组装成树。Injectable() export class MenuService { async getUserMenus(userId: number) { const roles await this.getUserRoles(userId); const menus await this.menuRepo.find({ where: { roles: In(roles) } }); return this.buildTree(menus); } private buildTree(menus: Menu[]) { const map new Map(menus.map(m [m.id, { ...m, children: [] }])); const roots []; map.forEach(m { if (m.parentId) map.get(m.parentId)?.children.push(m); else roots.push(m); }); return roots; } }4.2 JWT 鉴权与刷新 token 的完整链路NestJS 里 JWT 用nestjs/jwt和passport-jwt。登录成功后签发 access token15 分钟和 refresh token7 天。access token 过期后前端用 refresh token 换新的。这里的关键是 refresh token 要存数据库并且支持撤销。Post(refresh) async refresh(Body(refreshToken) token: string) { const payload this.jwtService.verify(token, { secret: REFRESH_SECRET }); const stored await this.tokenRepo.findOne({ where: { token, revoked: false } }); if (!stored) throw new UnauthorizedException(Token 已失效); return this.authService.issueTokens(payload.sub); }踩过的坑refresh token 的过期时间要比 access token 长但也不能太长7 天是个平衡点。另外前端要在 axios 拦截器里做无感刷新——检测到 401 就调 refresh 接口成功后重放原请求失败才跳登录页。这个逻辑写起来容易出死循环一定要加一个_retry标记。4.3 前后端分离下的跨域与代理NestJS 默认跑在 3000 端口前端跑在 5173跨域是必然的。开发环境用 Vite 的server.proxy代理生产环境用 Nginx 反代。NestJS 这边要开 CORSapp.enableCors({ origin: [http://localhost:5173], credentials: true, });注意credentials: true时origin不能是*必须是具体域名。这个坑我踩过前端带 cookie 请求一直失败查了半天才发现是 CORS 配置问题。4.4 全栈方案里前端部分的选型NestJS 只负责后端前端我还是用了 Vue3。但这里有个取舍既然后端已经算好了菜单树前端就不需要再维护一份路由权限表了直接用后端返回的菜单树生成路由。这样前后端的权限规则只有一份改权限只需要改后端。代价是前端启动时要等菜单接口返回才能渲染路由首屏会慢一点。优化方案是把菜单树缓存在 localStorage下次启动先用缓存渲染同时后台请求最新菜单有变化再更新。实测下来首屏从 800ms 降到 200ms 左右。5. Pear Admin Boot 与极简 Vue3 Admin拿来主义的边界5.1 Pear Admin Boot 的适用场景Pear Admin Boot 是一套基于 Spring Boot 的后台管理框架自带用户、角色、菜单、日志等模块。它的优势是开箱即用适合那种不想从零写权限系统的项目。但它的前端是基于 Layui 的如果你团队是 Vue 或 React 技术栈改起来会很痛苦。我用它实现了一遍订单列表和图表看板。订单列表直接用它自带的表格组件配置一下 URL 和列定义就能跑。图表看板需要自己引入 ECharts然后在页面里初始化。整体感受是如果你的项目是 Java 技术栈且对前端定制要求不高Pear Admin Boot 能省掉至少两周的权限系统开发时间。但如果你要深度定制 UI它的 Layui 体系会成为负担。5.2 极简 Vue3 Admin 模板的裁剪思路关键词里有个vue3 admin 最精简的框架我找了一个只有 500 行代码的极简模板然后按需加功能。极简模板通常只包含登录、Layout、一个示例页面。我的裁剪思路是保留 Layout 和路由框架删掉所有示例页面和多余的依赖然后按项目需求逐个加。这样做的代价是每个功能都要自己写但好处是你对每一行代码都清楚不会出现这个功能是模板自带的但我不知道为什么能用的情况。对于中小型项目我倾向于从极简模板起步而不是从大而全的框架裁剪。5.3 五种实现的横向对比维度ReactVue3NestJS 全栈Pear Admin Boot极简 Vue3动态路由配置式 过滤addRoute后端算树后端算树addRoute状态管理ZustandPiniaPinia自带Pinia权限粒度三层指令 守卫Guard后端守卫首屏速度中快慢等菜单快快定制成本低低中高低适合场景中大型中大型全栈团队Java 团队中小型这张表不是让你选一个而是让你看清楚每个方案的取舍点。比如 NestJS 全栈方案首屏慢但权限最安全Pear Admin Boot 定制成本高但省开发时间。6. 跨栈通用的踩坑清单与实操心得6.1 动态路由的刷新丢失问题不管哪个栈动态路由都有一个共同问题刷新页面后路由丢失。React 里靠重新过滤路由表解决Vue 里靠路由守卫重新 addRouteNestJS 全栈方案里靠重新请求菜单。核心思路都是把权限数据持久化localStorage 或后端刷新后重新生成路由。这里有个细节React 的createBrowserRouter重新创建时如果当前 URL 对应的路由还没生成会直接 404。解法是加一个errorElement或者用loader做重定向。Vue 的addRoute后必须next({ ...to, replace: true })重新导航否则会白屏。6.2 token 存储位置的选择token 存 localStorage 还是 cookie这个争论一直有。我的实践是如果后端支持 httpOnly cookie优先用 cookie因为 XSS 偷不到。如果不支持用 localStorage但要做好 XSS 防护CSP、输入转义。sessionStorage 的问题是关掉标签页就没了用户体验差。refresh token 我建议存 httpOnly cookieaccess token 存内存不持久化。这样即使 XSS 也偷不到 refresh tokenaccess token 过期后自动刷新。代价是刷新页面后 access token 丢失需要用 refresh token 重新换一次。6.3 图表组件的内存泄漏ECharts 和 AntV 这类图表库如果不在组件卸载时dispose会造成内存泄漏。React 里在useEffect的清理函数里调chart.dispose()Vue 里在onUnmounted里调。这个坑在单页应用里不明显但页面切换几十次后浏览器会卡死。另外图表的resize监听也要清理。我见过一个项目每次窗口 resize 都加一个监听从不移除跑一天后 resize 一次触发几百个回调页面直接卡住。6.4 权限变更后的实时生效用户权限变了但已经登录的会话里还是旧权限这个怎么处理。我的方案是后端在权限变更时把该用户的所有 refresh token 标记为失效前端下次刷新 token 时失败强制重新登录。这样虽然用户体验有中断但保证了权限的实时性。如果业务允许延迟可以加一个定时轮询权限版本的接口版本变了就重新拉菜单。6.5 多标签页的状态同步Admin 系统经常开多个标签页一个标签页退出登录其他标签页还显示着旧数据。解法是监听storage事件window.addEventListener(storage, (e) { if (e.key token !e.newValue) { window.location.href /login; } });这个事件只在其他标签页修改 localStorage 时触发当前标签页不触发。所以退出登录时要先清 localStorage再跳转这样其他标签页能收到通知。7. 五种实现跑下来我真正想说的三周时间五套实现最大的收获不是学会了某个 API而是看清了一件事Admin 系统的复杂度不在技术栈而在权限模型和状态同步。不管用 React 还是 Vue动态路由、权限拦截、token 刷新这些核心逻辑的思路是一样的差别只在 API 的写法。如果让我给建议我会说团队用什么栈就用什么栈别为了追新而换。React 和 Vue 在 Admin 场景下的开发效率差距远小于团队熟悉度带来的差距。NestJS 全栈方案适合前后端一个人管的团队Pear Admin Boot 适合 Java 背景且不想折腾前端的团队极简模板适合想完全掌控代码的团队。最后分享一个我踩过的坑别在 Admin 系统里过度设计。我第一版 React 实现里搞了一套插件化的路由系统支持运行时动态加载远程组件结果调试了两天发现根本用不上。Admin 系统的路由和权限在项目生命周期里基本是稳定的用最直接的方式实现就好。那些花哨的架构等真的需要了再加也不迟。
企业数字化 ERP 产品动态
相关推荐
Jev与TypeSafe AI:用类型安全解决LLM API调用中的401与Schema报错 1. 从一个让人抓狂的报错说起:Jev到底解决什么问题如果你最近在折腾大模型相关的开发,大概率见过这个报错:unexpected status 401 unauthorized: incorrect api key provided: sk-svcac****。我第一次看到它的时候,正赶着把一个内… · 2026/11/3 12:14:28
Blender MCP + Three.js 实战:智慧仓储数字孪生从建模到交互 1. 为什么我要用 Blender MCP 来做仓储数字孪生 第一次听到"数字孪生"这个词是在一个物流行业的交流会上,当时有人展示了一个仓库的三维可视化系统,货架、叉车、传送带全都在屏幕上实时跑着,点击任何一个货位就能弹出库存信息。我当… · 2026/11/3 12:14:28
Obsidian构建高效知识管理系统:从Day2笔记到知识图谱 1. 高效笔记系统的核心要素 作为一名从业十年的技术博主,我深知笔记系统对知识管理的重要性。Day2笔记这个概念源于Day One日记应用,但经过实践演化,已经成为一套完整的知识沉淀方法论。它不仅仅是简单的记录,而是将碎片化信息转化… · 2026/11/3 12:14:28
UC网盘免登录直链下载技术解析 1. UC网盘免登录下载方案解析 最近在整理资料时发现不少资源都存放在UC网盘上,但每次下载都需要强制登录账号,对于临时下载的用户很不友好。经过多次测试,我总结出一套稳定的免登录直链获取方案,实测有效且不需要任何特殊工具。 … · 2026/11/3 12:54:33
R语言期末复习指南:从数据类型到假设检验的考点梳理 简介:面向R语言初学者与备考复习人群的试题练习文档,围绕描述性统计与推断性统计的区分、名义数据与计量数据等数据类型判定,以及R软件入门操作展开,帮助读者在刷题中夯实基础概念与语法细节。包内共1个docx文件,约96K… · 2026/11/3 12:54:33
具身智能技术前沿与应用趋势解析 1. 项目背景与核心价值 2026年大湾区具身智能论坛(EI-OAHV 2026)作为华南地区最具前瞻性的智能科技盛会,正进入最后筹备冲刺阶段。这个由学术界与产业界联合发起的国际会议,聚焦具身智能(Embodied Intelligence&#x… · 2026/11/3 12:54:33
2026大湾区具身智能论坛前瞻:技术亮点与产业机遇 1. 项目概述:2026大湾区具身智能论坛(EI-OAHV 2026)前瞻距离截稿只剩48小时,而这场智能科技领域的年度盛会也将在4天后正式拉开帷幕。作为国内首个聚焦"具身智能"(Embodied Intelligence)的垂直领… · 2026/11/3 12:54:33
C语言函数库详解:从报错到精通的实用指南 简介:这份PDF面向C语言初学者与需要随时查阅标准库的开发者,系统梳理了C标准库各头文件的功能定位与常用接口,帮助解决函数记不牢、头文件归属不清、查阅手册零散等问题。资源共1个PDF文件,压缩包约967KB,内容按章节组… · 2026/11/3 12:54:33
SQL Server即时通讯数据查询实战:多表关联、索引优化与窗口函数 1. 从一张“QQ查询”需求单说起:这个项目到底在做什么很多人第一次看到“数据库(SQL Server)QQ查询”这个标题,第一反应是“查QQ号?”,其实真正落到开发场景里,它指的是一套围绕即时通讯类业务数… · 2026/11/3 12:53:33
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46