一、背景
RuoYi(若依) 大概是国内最流行的开源后台管理框架之一:RBAC 权限体系完善、代码简洁、文档与社区成熟,RuoYi-Vue 几乎成了「后台管理系统」的默认起手式。
但官方前端多年来停留在上一代的技术审美里:观感略显陈旧、主题定制能力弱(换个主色要改一堆 Less 变量)、布局单一、没有国际化。很多团队的真实状态是------后端用着若依,前端另起炉灶重写一遍,权限、字典、代码生成这些能力全部要重新对接。
于是就有了 ruoyi-vue-nys:一套适配若依后端 API 的现代化中后台前端。
后端零改动,前端整套替换。
/getRouters、登录鉴权、字典、代码生成等接口全部原样对接;原有的业务页面、指令与全局方法直接可用,50 余个页面零改动迁移。
二、效果
亮色主题 + 主题配置抽屉(主题模式 / 布局模式 / 主题色 / 页面功能一屏可调):

暗色模式 + 英文界面(i18n 一键切换):

用户管理页(亮 / 暗),组织机构树、搜索表单、分页、行内操作一应俱全:


ECharts 监控图表也能自动跟随主题:

登录页(暗色):

三、技术栈
| 分类 | 选型 |
|---|---|
| 框架 | Vue 3.5 + Vite(rolldown-vite)+ TypeScript 5.9 |
| UI | Element Plus 2.14 + UnoCSS |
| 状态 / 路由 | Pinia + vue-router |
| 国际化 | vue-i18n(中 / 英) |
| 图表 | ECharts 6 |
| 工程 | pnpm workspace + ESLint 9 + vue-tsc |
项目通过 pnpm workspace 内置了 5 个 @sa/* 包:color(色板生成)、hooks、materials、utils、uno-preset,主题、布局、路由约定等基础架构参考了同样开源的 soybean-admin 体系。
四、核心「零改动接管」
把若依的页面直接拷过来,代码是这样的:
html
<el-table :data="userList" v-loading="loading">
<el-table-column label="部门" prop="deptName" />
</el-table>
js
export default {
methods: {
getList() {
listUser(this.queryParams).then(res => {
this.userList = res.rows;
this.total = res.total;
});
},
handleExport() {
this.download('system/user/export', { ...this.queryParams }, `user_${new Date().getTime()}.xlsx`);
}
}
};
res.rows、this.download、this.$modal、v-hasPermi......这些全是若依的「方言」。要零改动迁移,就必须把这些方言一模一样地在新技术栈上重新实现出来。
4.1 全局能力注入(250+ 调用点)
若依页面大量依赖全局方法。新框架把它们全部挂回去(src/plugins/ruoyi.ts):
ts
export function setupRuoYiPlugins(app: App) {
// RuoYi 全局方法
app.config.globalProperties.useDict = useDict;
app.config.globalProperties.download = download;
app.config.globalProperties.parseTime = parseTime;
app.config.globalProperties.resetForm = resetForm;
app.config.globalProperties.handleTree = handleTree;
app.config.globalProperties.addDateRange = addDateRange;
// ...
// RuoYi 插件对象
app.config.globalProperties.$modal = modal;
app.config.globalProperties.$tab = ruoyiTab;
app.config.globalProperties.$cache = cache;
// 新框架按需引入 Element Plus,没有全量注册,这里补齐若依用到的部分
app.config.globalProperties.$alert = ElMessageBox.alert;
app.config.globalProperties.$confirm = ElMessageBox.confirm;
app.config.globalProperties.$prompt = ElMessageBox.prompt;
app.config.globalProperties.$msgbox = ElMessageBox;
app.config.globalProperties.$notify = ElNotification;
// 权限指令:v-hasPermi / v-hasRole / v-copyText
directive(app);
}
其中 $modal、$tab、$cache 这类「若依专属插件对象」也有讲究:比如 $tab.closePage() 要同时兼容新框架的多标签页与 KeepAlive 语义,$modal.msgSuccess() 内部则转译到 Element Plus 的 ElMessage。
4.2 图标按需注册
若依页面用字符串引用图标(icon="Search"),这依赖图标被全局注册。最简单的做法是:
js
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
// 294 个图标 ≈ 300 KB,全部进首屏
全量注册的代价是把 294 个图标约 300 KB 全部打进首屏 chunk,而项目实际只用到几十个。这里的做法是写一个生成脚本扫描源码,得出真实使用集合,只注册命中的图标(实测 55 个):
js
// scripts/gen-ep-icons.mjs → src/plugins/ep-icons.ts
const hit =
src.includes(`icon="${name}"`) ||
src.includes(`<${name} `) ||
src.includes(`:icon="'${name}'"`) /* ... */;
if (hit) used.push(name); // 只保留实际用到的图标
还有一个隐藏坑:若依「表单构建」页用字符串 tag('el-input'、'el-select')动态渲染组件,如果为此把整套 Element Plus 全局注册一遍,这些组件会跟着入口文件一起进首屏。最终方案是让表单构建的画布自行映射这些动态组件,入口只保留按需引入。
五、请求层:保持若依的调用契约
后端返回的是若依特有的「信封」结构:
json
{ "code": 200, "msg": "操作成功", "rows": [], "total": 100 }
请求层(src/utils/request.js)在移植时保留了这个契约,所以业务代码里的 res.rows / res.total 完全不用改:
js
// 响应拦截:保持 { code, msg, data | rows, total } 原样返回
service.interceptors.response.use(res => {
const code = res.data.code || 200;
if (code === 401) { /* 提示并重置登录态 */ }
else if (code === 500) { ElMessage({ message: msg, type: 'error' }); }
else if (code !== 200) { ElNotification.error({ title: msg }); }
return res.data;
});
在保持契约的同时,顺手修掉了两个历史遗留问题:
- Token 从 Cookie 改为 localStorage,与 SPA 的登录态管理(Pinia store + 路由守卫)统一;
- 防重复提交的数据缓存做了脱敏 。若依会把上次请求体写入 sessionStorage 用于比对,直接存的话密码等敏感字段会以明文落盘;这里在写入前把
password / passwd / pwd类字段替换为掩码,只用于「判断数据是否相同」。
六、动态路由:后端菜单直接驱动
若依的 /getRouters 返回 RouterVo 树:component 字段是字符串(Layout / ParentView / InnerLink / 视图路径),meta.noCache 控制缓存,一级菜单还会包一层 meta 为空的 Layout 壳。转换层(src/router/dynamic/ruoyi.ts)负责把它翻译成前端路由:
ts
function transformComponent(component?: string | null) {
if (!component) return undefined;
if (component === 'Layout') return 'layout.base';
if (component === 'ParentView') return 'view.ParentView';
if (component === 'InnerLink') return 'view.InnerLink';
return `view.${component}`;
}
const meta = {
title: route.meta?.title ?? '',
hideInMenu: Boolean(route.hidden),
// RuoYi 的 noCache 与前端 keepAlive 语义相反,注意取反
keepAlive: !route.meta?.noCache
};
几个细节值得一提:
- 拍平 isMenuFrame 壳 :后端对一级菜单会包一层
meta: null的Layout,转换时识别并拍平,避免菜单树多出一级; - 外链 :
meta.link或http(s)://开头的路由注册为安全路径,由路由守卫window.open打开; - 目录重定向 :父级目录自动
redirect到第一个可见子路由,直接访问不会白屏; - 图标:后端菜单的 icon 字符串自动映射到项目图标体系(含 iconify 图标集)。
菜单管理里改一下菜单,刷新页面即时生效,无需重新构建。
七、图标离线化
@iconify/vue 默认在运行期 向 https://api.iconify.design 拉取图标数据。这在内网 / 无外网环境会直接导致图标缺失------实测单次请求还要等约 7 秒。
解决方案是在构建前执行内联脚本,把项目实际用到的 27 个图标数据生成到 src/plugins/iconify.ts,随包发布、离线可用:
bash
pnpm dev # 内部先执行 node scripts/gen-iconify-offline.mjs
pnpm build:prod # 同上,生成后再构建
同时保留在线兜底:配置 VITE_ICONIFY_URL 指向自建 iconify 服务后,未内联的图标会自动回退到该地址拉取。新增图标只需在脚本清单里加一行、重新执行生成脚本即可。
八、主题与布局
- 暗黑模式:跟随系统 / 手动切换,图表、表格、弹窗全量适配;
- 主题定制 :基于
@sa/color的色板生成(主色 → 50~950 色阶 → CSS 变量),支持色弱模式、深色侧栏; - 多布局:垂直、垂直混合、水平、顶部混合,侧栏宽度 / 头部高度 / 页签风格均可调;
- 国际化:中英双语,菜单标题、路由标题同样走 i18n;
- 多标签页 + KeepAlive :与若依原有语义对齐(
noCache→ 不缓存),刷新、关闭、右键菜单完整。
主题配置在开发模式下不落缓存,改默认值立即生效;生产构建以 BUILD_TIME 作为覆盖标记,新版本发布后自动应用新的默认主题,不会被老用户的本地缓存「锁死」。
九、快速开始
bash
# 1. 前端
git clone https://github.com/niyongsheng/ruoyi-vue-nys.git
cd ruoyi-vue-nys
pnpm install
pnpm dev # 默认 9527 端口,/dev-api 代理到 http://localhost:8080
bash
pnpm typecheck # 类型检查
pnpm lint # 代码检查
pnpm build:prod # 生产构建(/prod-api)→ dist/
pnpm build:stage # 预发布构建(/stage-api)→ dist/
后端直接使用任意 RuoYi-Vue 3.9.x 版本(推荐 RuoYi-Vue-fast 单模块版),前端代理指过去即可,无需改动后端任何代码。
十、小结
ruoyi-vue-nys 不是又一个「重新造轮子」的后台模板,而是一次接管式迁移:承认若依后端生态的价值,把前端从上一代技术栈整体搬到 Vue 3.5 + Vite + TS + UnoCSS 上,同时用兼容层保证业务页面零改动。
- 新项目:直接拿它当若依的前端起手式,业务代码可以照抄 RuoYi-Vue3 的页面;
- 存量项目:把
src/api、src/views拷过来,改掉少量 import 即可完成前端升级。
如果这个项目对你有帮助,欢迎 Star、提 Issue,或者直接提 PR。
GitHub:github.com/niyongsheng...