每次带移动端项目,只要一听到“用若依改个App页面出来”,我心里就知道又要跟 RuoYiApp 这版源码打交道了。这套基于 Spring Boot 后端、UniApp 前端的移动端脚手架,确实把“后台管理”和“移动办公”两件事串到了一起,但很多第一次接手的人,最容易卡住的地方反而不是后端接口,而是那一堆页面到底怎么组织、怎么改、怎么调通。
这篇文章我就专门聚焦若依 RuoYiApp 版的前端页面部分,从整体结构拆解、核心页面实现,到实际开发过程中的踩坑记录,一次性把“页面”这件事说透。适合刚接手 RuoYiApp 二次开发、或者想在移动端复用若依权限体系的同学参考,我会尽量避开那些官方文档已经写烂的东西,多讲实际操作中才用得上细节。
1. 页面整体设计与技术栈拆解
1.1 RuoYiApp 版的前端选型逻辑
RuoYiApp 版的前端没有沿用 Vue2 + Element UI 那套桌面端方案,而是选了 UniApp 做跨端框架。这个选择说白了就是省成本:同一套 Vue 代码,编译成 App、H5、微信小程序都能跑。后端接口还是走若依统一的/prod-api前缀转发,权限用的还是 Sa-Token 那套登录凭证逻辑。
页面目录结构里有几个核心目录需要先记住,分别是pages(业务页面)、uni_modules(组件与扩展)、static(静态资源)、common(公共方法和配置文件)。如果你们项目是拿到源码二次开发,最先要动的就是pages.json这个文件,所有页面路由、tabBar 配置、导航栏样式都在这一个文件里控制。很多人第一次进来会习惯性去pages目录下找路由配置文件,找了半天没有——因为 UniApp 的路由是声明式配置,不是像 Vue Router 那样独立一个文件。
从页面类型上看,RuoYiApp 版的页面大致可以分为四类:登录注册页、主框架页(tabBar 页面)、业务列表页、表单新增/编辑页。这四类页面在结构组织上差异很大,踩坑点也各不相同。
1.2 为什么说页面结构决定了二次开发成本
我见过太多团队接手若依移动端后,第一周都在跟页面结构较劲。原因很直接:RuoYiApp 的页面不像纯业务系统那样扁平,它把登录态、权限指令、用户信息、字典数据全部跟页面生命周期绑在了一起。你改动一个列表页,不只是在改 UI,同时还在改它对接口返回结构的假设、对权限指令的依赖、对缓存的读取方式。
举一个具体的例子:pages/index/index这个首页工作台页面,默认展示了一张欢迎 banner、待办数量的角标、功能宫格导航。这个页面里看似静态的宫格导航,实际上是通过uni.getStorageSync读取本地用户信息、再结合后端下发的菜单权限渲染出来的。如果你直接把宫格导航改成写死的数据,短期内看起来没问题,一旦账号权限变化,导航项不会自动增减,就会出现“没权限的入口也能点进去”的尴尬局面。
所以建议拿到代码的第一件事,不是急着改界面,而是先画一张“页面依赖图”,把每个页面对接口的依赖、对缓存 key 的依赖、对权限指令的依赖全部理清楚。这一步做完,后面改起来才能有的放矢。
2. 核心页面详解与二次开发要点
2.1 登录/注册页:搞定会话和应用启动逻辑
登录页是 RuoYiApp 版里逻辑最集中的页面之一,代码位置在pages/login/login.vue。这页面表面上只有一个手机号输入框和验证码按钮,实际上负责的事情包括:
- 手机号格式校验与密码登录切换;
- 调用
/auth/login接口获取 token; - 将 token 写入本地缓存并同步更新全局用户状态;
- 根据返回的用户信息拉取菜单权限和按钮权限;
- 登录成功后跳转到主页或者来源页面。
这个页面的坑主要集中在“登录跳转”逻辑。源码里通常会判断uni.getStorageSync('redirectUrl')这个值,如果存在就跳转到这个地址,否则跳转到首页。实际业务中经常出现“登录成功但没跳对页面”的问题,十有八九是这个 redirectUrl 存在脏数据。我建议在登录成功回调里,拿到跳转地址后立刻uni.removeStorageSync('redirectUrl'),避免下一次登录被旧地址干扰。
验证码方面,RuoYiApp 版有图形验证码和短信验证码两种方式。图形验证码在代码里走的是/captchaImage接口,返回 base64 图片和 uuid,本地用<image>标签渲染,点击时重新请求。短信验证码一般要接入第三方短信服务商,代码里用的是若依后端预留的SmsController接口。如果你们的后端没有真正接入短信服务商,测试环境强烈建议直接在后端把短信通道关掉,改成万能验证码,否则前端这边反复点“获取验证码”只会白白把短信配额烧完。
2.2 首页工作台:动态菜单与待办数据的组装
首页pages/index/index这块我必须多说几句,因为它是整个 App 的“门面”,也是绝大多数定制需求的重灾区。
RuoYiApp 版的首页同时包含了几块数据:
| 模块 | 数据来源 | 说明 |
|---|---|---|
| 顶部用户信息 | 本地缓存中的用户对象 | 展示头像、昵称、部门,可配置跳转个人中心 |
| 待办角标数字 | /index/todo接口 | 返回待办数量,用于角标渲染 |
| 宫格导航菜单 | 登录时下发的菜单数据 | 由后端权限动态生成,非写死的 UI |
| 底部轮播 banner | /index/banner接口 | 可选模块,与后端约定按位置返回图片 |
开发中常遇到两个问题。第一个问题是宫格导航数量太多,一行五个图标加上文字显得拥挤。这个不是改 CSS 就能完美解决的,需要在拿到菜单数据后用splice或分页逻辑控制显示数量,默认建议不超过两行十个入口。第二个问题是待办数量接口尚未开发,前端拿不到数据,角标位置一直空白。这种情况下不要在前端写死数字,更不要 mock 一个假接口,正确做法是让后端先按接口文档返回空数组或 0,前端做空数据兜底渲染。
还有一个细节容易被忽略:首页的轮播图高度、宫格图标大小、间距这类样式参数,在源码里经常出现rpx和px混用的情况。RuoYiApp 的 UI 设计基准是 750rpx 宽设计稿,拿到的设计稿如果是 375px 宽度,记得乘以 2 再写成 rpx,否则在高清屏上会出现图标明显偏小的问题。
2.3 业务列表页:列表数据加载与权限控制实践
RuoYiApp 版的业务列表页,源码里最具代表性的是pages/work/index.vue和相关模块页面。这类页面采用了标准的“搜索条件 + 列表 + 分页”结构,用的是自定义的useListPage组合式函数或者onLoad生命周期里手动调接口。核心逻辑可以概括成四个动作:
initData():初始化查询条件;getList():请求当前页数据并赋值给列表变量;onReachBottom():滚动到底部时页码加一继续请求;handleSearch():重置页码并重新请求。
这里最大的踩坑点是下拉刷新与搜索条件未重置的问题。在启用 UniApp 的enablePullDownRefresh后,下拉刷新通常只做了reload列表数据,没有把搜索条件重置为初始状态。这就导致用户搜索过“状态=1”之后,下拉刷新看到的仍然只有状态为 1 的数据,页面看起来像卡住了。正确逻辑应该在下拉刷新回调里,先重置搜索表单再重新请求。
权限指令方面,若依移动端在directive目录下封装了v-permission类似的自定义指令,用于按钮级权限控制。但与 Vue2 的 web 端不同,UniApp 中有些环境对自定义指令支持有限,必须确认当前页面运行环境。实测在 App 端自定义指令是可用的,H5 端一般也没问题,但在某些小程序平台的老基础库版本下有兼容性问题。一个更稳妥的做法是在按钮渲染处用v-if配合checkPermission方法显式判断,虽然代码量多了一些,但可预测性更好。
2.4 表单新增/编辑页:那个让新手反复踩坑的树形部门
RuoYiApp 的业务表单页其实并不复杂,无非是 uni-forms 表单校验加提交接口。真正让一大批人卡住的,是用户管理、部门管理这类表单里的“上级部门”树形选择器。
若依后端返回的部门数据是标准的树结构,但移动端不能直接用 Element UI 的el-tree,RuoYiApp 版的方案是封装了一个tree-select组件,在弹层里递归渲染树节点。实际开发中经常遇到三个问题:
第一,树节点全部展开太深,一屏显示不完,滚动卡顿。需要后端配合将默认展开层级控制在一到两级,前端也可以根据node.level做剪枝处理。第二,选中父节点后自动选中所有子节点,保存后父子权限难以区分。若依门店的树选择器通常实现的是单选,如果你需要支持“选择父级并不一定选子级”的权限模型,得改组件逻辑,默认情况下建议直接做成单选模式,最省事。第三,部门数据量太大时,一次性加载全部树节点导致页面首次渲染明显白屏。实际项目中我们曾遇到一万多部门节点,最终方案是改为懒加载模式,选中展开某一层时再动态请求该层子节点,页面交互流畅了很多。
3. 实操过程:从零跑通 RuoYiApp 页面开发
3.1 环境准备与本地运行
在动手改页面之前,先把运行环境跑通。需要准备的都是常见工具:
- HBuilderX(UniApp 官方 IDE,版本建议 3.8 以上);
- Node.js(建议 16 及以上,部分依赖要求 Vue3+vite 工程);
- Android 模拟器或真机(开发 App 端需要);
- 后端若依项目已启动,确保
/prod-api地址能正常访问。
如果你们只做前端开发,后端用现成的测试环境也行,但需要在前端项目里找到config.js或env配置文件,把VITE_APP_BASE_URL之类的参数指向后端地址。注意一个最容易忽略的问题:手机 App 端不能像 H5 一样直接访问localhost,必须把地址改成局域网 IP 或线上域名,否则请求会全部失败。这个坑我见过太多次,代码明明没问题,页面上就是一直转圈加载。
运行方式上,HBuilderX 直接打开项目,运行到浏览器或运行到手机 App 模拟器即可。首次运行会初始化一堆依赖和编译缓存,耐心等待,不要关掉终端窗口。如果运行到小程序,还需要在manifest.json里配置对应的小程序 AppID。
3.2 新增一个页面的完整操作流程
我们以一个常见的“通知公告列表页”为例,演示在 RuoYiApp 版里新增页面的完整流程。
第一步,在pages目录下新建notice/notice.vue文件。如果目录不存在就先创建目录,注意 UniApp 的目录名约定用英文小写加下划线,不要用中文或驼峰命名,避免在部分真机环境下出现路径解析异常。
第二步,在pages.json的pages数组里注册新页面:
{ "path": "pages/notice/notice", "style": { "navigationBarTitleText": "通知公告", "enablePullDownRefresh": true } }这里建议navigationBarTitleText用中文,可以直接显示在导航栏。同时,如果是 tabBar 页面,需要在tabBar.list里也注册,否则会编译报错。
第三步,编写页面代码。列表页的基本结构可以复制其他列表页的写法,核心部分是模板里的列表渲染和脚本里的数据请求:
<template> <view class="page-container"> <view class="list-wrap"> <view class="list-item" v-for="item in list" :key="item.id" @click="goDetail(item.id)"> <text class="item-title">{{ item.title }}</text> <text class="item-time">{{ item.createTime }}</text> </view> </view> <view class="loading-state" v-if="loading"> <text>加载中...</text> </view> <view class="nomore-state" v-if="finished"> <text>没有更多了</text> </view> </view> </template> <script setup> import { ref } from 'vue' import { getNoticeList } from '@/api/system/notice' const list = ref([]) const pageNum = ref(1) const pageSize = ref(10) const loading = ref(false) const finished = ref(false) const getList = async () => { if (loading.value || finished.value) return loading.value = true try { const res = await getNoticeList({ pageNum: pageNum.value, pageSize: pageSize.value }) list.value = list.value.concat(res.rows || []) pageNum.value += 1 if (list.value.length >= res.total) { finished.value = true } } finally { loading.value = false } } const onLoad = () => getList() const onReachBottom = () => getList() const goDetail = (id) => { uni.navigateTo({ url: `/pages/notice/detail?id=${id}` }) } </script>第四步,在api目录下新增或复用对应的接口请求函数,例如在common/api中定义getNoticeList,封装好request工具。
整个过程走完后,刷新页面能看到列表数据,就说明新增页面的链路已经通了。之后再把空数据兜底、上拉加载状态优化一下,一个合格的列表页就完成了。
3.3 连接后端接口的身份鉴权配置
页面调通接口离不开登录态,很多新人在这个环节翻车。RuoYiApp 版的前端请求统一走request.js里封装好的request方法,它会在每次请求前从uni.getStorageSync('token')取出 token,并追加到请求头。如果 token 失效,后端返回 401,前端会跳转登录页并清除本地缓存。
实际操作中有一个高频问题:登录状态明明还在,但请求依然返回 401。这种情况大概率是后端的 token 配置与前端存储的 key 不一致。若依后端的 token 默认放在请求头的Authorization字段,且通常带Bearer前缀,而 RuoYiApp 前端源码里可能只存了裸 token。检查一遍请求拦截器,若有前缀没带上,后端的 Sa-Token 校验会直接判无效,返回未认证结果。
3.4 tabBar 自定义图标的兼容性排查
tabBar 是 App 页面里很核心的导航元素。RuoYiApp 默认的 tabBar 有首页、工作台、消息、我的这四个入口。修改 tabBar 是业务需求里最常见的,但不少开发者在真机上调试时出现一个诡异问题:自己在pages.json里配置的 tabBar 图标在 H5 端正常显示,跑到 App 端却变成了空白或默认图标。
原因在于 App 端的 tabBar 只能使用本地图片资源,不支持网络图片,也不能直接使用@/static/icon.png这类别名路径,部分库版本还不支持 iconfont 字体图标。正确做法是把图标放在static目录下的tabbar子目录里,然后在pages.json的iconPath和selectedIconPath中填写相对路径,例如:
"tabBar": { "color": "#7A7E83", "selectedColor": "#2E5BFF", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tabbar/home.png", "selectedIconPath": "static/tabbar/home_active.png" } ] }如果图标仍然显示异常,还有一个更隐蔽的坑:PNG 图片资源没有放在正确目录。很多新手习惯把图片放到assets目录下,这在 Vue 项目中是惯例,但 UniApp 的 tabBar 图标比较特殊性,它要求必须放在static目录里。放到非 static 目录下编译时可能不报错,但运行时会找不到资源,表现为图标空白。
4. 常见问题与页面开发避坑指南
4.1 页面白屏与组件加载异常
白屏问题在 RuoYiApp 页面开发里有两个高发原因,一是路由配置路径错误,二是异步组件加载异常。
先说路由配置。如果你在pages.json里配置的路径与实际的.vue文件路径不一致,编译可能不报错,但运行时页面白屏,终端大概率输出一条报错信息。有个偷懒的排查方法:在pages.json中找到该页面配置,复制path值,然后看它跟pages目录下实际文件路径是否完全一致。注意不要忽略大小写,因为 Mac 下开发环境可能不敏感,但 Android 真机的文件系统是大小写敏感的,经常出现本地开发正常、打包后白屏的问题。
异步组件加载异常方面,UniApp 在小程序端对动态import()的支持有限。如果你在页面里用const xxx = () => import('@/components/xxx')这种写法,在 H5 端可能没问题,但小程序端会出现找不到组件的报错。RuoYiApp 源码中大部分组件走的是 easycom 规范,即组件放到uni_modules或components目录下并满足命名规则后,不需要手动 import 就能使用。当你发现页面白屏时,优先检查是否在页面中手动 import 了 easycom 可以处理的组件,导致重复注册。
4.2 导航栏与状态栏适配问题
RuoYiApp 在真机上最常见的样式问题就是异形屏刘海导致的页面顶部遮挡。虽然页面默认使用navigationBarTitleText会自动适配系统导航栏,但在自定义导航栏的页面里,需要手动处理状态栏高度。我强烈建议不要自己写一堆适配代码,直接用官方封装的uni.getSystemInfoSync()获取statusBarHeight,再配合safeAreaInsets设置导航栏占位高度。
一个参考实现片段:
const systemInfo = uni.getSystemInfoSync() const statusBarHeight = systemInfo.statusBarHeight || 44 // 建议在页面根节点 style 中绑定 padding-top还有一个常见问题是页面底部被 Home 指示条遮挡。若你的页面中有底部操作按钮,一定要计算safeAreaInsets.bottom,否则在 iPhone 全面屏机型上按钮会被系统手势条压住。
4.3 列表页上拉加载失效的处理
上拉加载是列表页最基础的功能,但 RuoYiApp 在 H5 开发模式下经常遇到onReachBottom不触发的问题。这通常是页面滚动的容器不是页面本身,而是内嵌了一个scroll-view。UniApp 的onReachBottom触发条件是页面滚动条滚动到底部,如果你把列表放在一个高度固定的scroll-view里,页面本身是不滚动的,那么onReachBottom永远不触发。
排查方法很简单:看pages.json里是否开启了enablePullDownRefresh,以及页面有没有设置固定高度容器。如果用了scroll-view,就换用scroll-view的@scrolltolower事件来加载数据。另一个方案是把列表从scroll-view中拆出来,直接放在页面根节点,让页面本身滚动。
4.4 真机调试时接口跨域与代理配置
H5 端调试接口时,跨域是绕不开的问题。若依后端默认配置了跨域允许,但不是所有环境都生效。如果你在浏览器里能访问后端接口,App 端却请求失败,先确认不是跨域导致,而是网络权限和地址配置的问题。App 端不存在浏览器跨域概念,但存在网络安全策略,某些框架版本默认只允许 HTTPS,如果你后端的地址是 HTTP,需要在manifest.json中配置网络权限。
这个配置路径在 HBuilderX 里比较隐蔽,通常在 manifest 的可视化配置界面中选择“App 常用其它设置”,勾选“不校验合法域名”等选项。具体的配置项名称因版本而异,需要按实际编辑器中的文案选择。
4.5 页面缓存与用户状态不同步
RuoYiApp 的用户状态通过pinia或vuex管理,常见是使用本地缓存做持久化。如果你发现修改用户头像或用户名后,页面没有实时刷新,十有八九是状态没有同步到本地缓存。
解决方案是在修改用户信息的接口回调里,同步调用更新缓存的方法。不要只修改store里的内存变量,一定要同步写入uni.setStorageSync('userInfo', ...)才能保证下次冷启动时新数据生效。同理,退出登录状态时清理缓存时,要记得把可能残留的redirectUrl、旧 token、菜单数据等也一并清理干净。
4.6 常见问题速查表
| 问题现象 | 可能原因 | 推荐处理方案 |
|---|---|---|
| 登录成功但跳转回登录页 | 401 拦截跳转或 token key 不匹配 | 检查请求拦截器 header 格式与后端规定是否一致 |
| 列表页下拉刷新无新数据 | 搜索条件未重置 | 下拉回调里先重置查询字段再 getList |
| tabBar 图标空白 | 图片路径不在 static 目录 | 把图标移到 static 目录下并填写相对路径 |
| 页面顶部被刘海遮挡 | 未适配状态栏高度 | 获取 statusBarHeight 并做 padding-top |
| onReachBottom 不触发 | 内嵌 scroll-view 导致页面无滚动 | 改用 scroll-view 的滚动触底事件 |
| 页面白屏无操作报错 | 路由路径大小写不一致 | 比对 pages.json 与实际文件路径 |
| 首次加载慢 | 一次性加载大树形数据 | 改为懒加载并控制默认展开层级 |
| 自定义指令在部分端失效 | 指令兼容性问题 | 改用 v-if + checkPermission 方法 |
5. 关于页面后续扩展与维护的个人体会
做完一轮 RuoYiApp 页面开发之后,我的感受是:这套框架的前端页面本身并不复杂,真正的复杂度全藏在“跨端兼容”和“权限联动”这两件事里。页面能不能稳定运行,往往不是因为 Vue 代码写得漂不漂亮,而是你有没有把导航栏适配、请求头格式、资源路径规范、分页加载策略这些看起来琐碎的细节都照顾到位。
一个想给大家的建议是,在工程初始化阶段一定要花半天时间把pages.json和manifest.json完全吃透。这两个文件,一个决定页面怎么组织,一个决定应用在真机上怎么呈现,几乎覆盖了 RuoYiApp 页面端一半以上的坑。
另外,二次开发中如果遇到页面样式诡异、真机与浏览器表现不一致,建议第一时间去查是不是 rpx 和 px 混用、还有 Flex 布局在部分安卓 webview 的兼容问题。若依本身的样式体系已经做过大量适配,但业务页面上新加的样式还是要通过真机验证后再交付。
最后再分享一个小技巧:如果你要批量修改页面内统一的导航栏配色、标题字号或按钮风格,不要一个页面一个页面去改,直接找到全局公共样式文件(通常是common/uni.css或App.vue里的全局样式),在那里统一覆盖变量。RuoYiApp 的页面样式体系里有很多公共类,比如.btn-primary、.cell-item这类通用样式,善用它们能少写大量重复代码,也让整体 UI 风格保持统一。这个技巧我在多个项目里用下来,维护成本明显降低,页面风格也更一致。