
简介本资源是一套基于JavaFX实现的权限管理系统桌面端UI面向Java桌面应用开发者及后台管理框架学习者解决传统Web权限系统在离线场景、轻量级部署或跨平台桌面集成中的界面开发需求。项目复刻RuoYi-Vue的简洁布局与交互逻辑涵盖用户管理、角色配置、菜单权限绑定、动态视图切换等核心模块适合作为Spring BootJavaFX混合架构的前端参考实现。压缩包含310个文件194个Java业务逻辑类、36个FXML界面定义、22个SCSS样式文件、18个字体资源及SVG/PNG图标总大小38.7MB结构清晰模块化程度高便于快速理解JavaFX组件组织与权限驱动的UI渲染机制。目前已有156人学习下载提供完整可运行的UI工程骨架包含登录注册、仪表盘、监控视图、用户信息管理等典型页面以及MenuDialogView、UserInfoView等关键视图组件助开发者快速掌握JavaFX在企业级权限系统中的落地实践。1. 为什么要把 RuoYi-Vue 的长相搬进 JavaFX1.1 一个很常见的需求先说说这个项目是怎么来的。公司内部有一套老的桌面运维工具用 Java Swing 写的界面还停留在十年前的水平。业务那边提了无数次能不能做得跟网页版后台一样漂亮因为他们的主力系统是基于 RuoYi-Vue 搭建的操作习惯、视觉认知都已经固化了。桌面工具如果还是老面孔培训成本、接受度、日常使用效率都会受影响。市面上做桌面端界面的方案其实不少JavaFX、Avalonia、Qt、Electron、Tauri。选 JavaFX 的理由也很实际——研发团队全是 Java 出身后端逻辑复用现成的 Spring Boot 服务JavaFX 是唯一不引入 Node.js 或者 .NET 依赖的路线。而且 JavaFX 在 JDK 8 之后虽然被移出了默认 JDK但依然作为独立模块维护Java 17 时代的 OpenJFX 其实一直在更新稳定性是够用的。这个项目的目标很明确不做业务功能只做一套权限管理系统的用户界面。说得再直白一点就是把 RuoYi-Vue 那种经典的 AdminLTE 风格后台管理界面用 JavaFX 重新画一遍。用户管理、角色管理、菜单管理、部门管理这几个核心页面要做到像——不是像素级还原而是让用惯了 RuoYi 的人一眼就觉得不陌生。1.2 从整系统到只做 UI的边界我接手的时候最大的诱惑是顺手把后端也接了吧。但实际上权限管理的后端逻辑极其繁琐数据权限隔离、菜单路由表、角色分配、操作日志……这些如果用 JavaFX 客户端直接连数据库等于重写一个 RuoYi 的服务端工作量翻三倍不止。所以这个项目的边界很清晰UI 层独立成前端工程通过 HTTP 调用已有的 RuoYi 后端接口。界面上每个按钮点击之后发请求、收响应、刷视图仅此而已。这套思路的好处是后续如果界面要换皮或者要对接其他后端系统完全不用动业务逻辑。我把 UI 框架、样式主题、权限控件封装成了独立模块和具体页面解耦后面每加一个新页面基本就是堆组件。如果你也想做类似的事我建议第一步先理清楚这个边界你是要复刻一个 RuoYi 客户端还是用 RuoYi 风格做一套自己的管理界面。前者要兼容后端的接口契约后者你完全可以自己定义数据模型。我这个项目更偏向后者的思路但因为要对接现有后端所以数据字段还是尽量对齐了 RuoYi 的返回结构。2. RuoYi-Vue 设计语言拆解照着画之前先看明白2.1 布局骨架的数值记忆RuoYi-Vue 的布局大家都见过左边 220px 左右的折叠菜单顶部 50px 左右的导航栏中间是内容区右下角偶尔有一个返回顶部的小按钮。这套布局其实是从若依早期版本继承下来的时间长了已经成了国产后台管理系统的一种标准脸。移植到 JavaFX 之前我先把这套布局的数值拆出来区域尺寸JavaFX 对应容器左侧菜单栏宽 220px折叠后 64pxVBox 或自定义 Sidebar顶部导航栏高 50pxHBox内容区自适应剩余空间内边距约 15pxStackPane 或 TabPane标签页栏高 40px可有可无TabPane 的 tab header底部状态栏高 24px可选HBox不要小看这些数字。用户对界面像不像的第一感知往往不是颜色有问题而是间距不对。你一眼觉得别扭又说不出哪里怪多半就是 padding、margin 和 web 端不一致。JavaFX 里布局用的是 BorderPane上中下左右五个区域天生适合做这种后台框架。顶部放 HBox左中放 VBox中央放 ContentPane完美对应 RuoYi 的框架结构。需要注意的一点是JavaFX 的布局嵌套层级如果太多会影响性能。菜单如果用 TreeView展开收起时的闪烁和阴影很烦人所以我在核心项目里干脆用 VBox 自定义折叠动画来做的菜单效果比 TreeView 稳定得多。2.2 色彩与间距的移植方案RuoYi-Vue 的默认主题色是 Element UI 的蓝色#409EFF背景色接近#F0F2F5卡片是纯白#FFFFFF。文字分级主文字#303133次要文字#606266提示文字#909399边框#DCDFE6。JavaFX 用 CSS 设置颜色的方式完全一样。需要注意的差异点是JavaFX 的 CSS 虽然叫 CSS但不是 Web CSS它不支持box-shadow、flex、grid圆角属性也略有不同。我一开始直接拿 Web 端的 CSS 变量思路来写结果发现 JavaFX 的样式变量支持有限后来老老实实写了一个公共样式表定义成-fx-ruoyi-primary: #409EFF;这种自定义颜色变量在根节点引用。间距方面RuoYi 的组件普遍使用小圆角4px卡片内边距20px组件间距10px左右。JavaFX 里按钮、输入框的-fx-background-radius默认会带一点圆角但如果你设置了-fx-background-color必须同步设置-fx-background-radius否则会很突兀。2.3 组件状态悬浮、禁用、聚焦Web 端的 Element UI 组件每一个交互状态都有明确的视觉反馈按钮 hover 变深、focus 有光圈、disabled 变灰。JavaFX 同样支持伪类选择器但写法有差异。例如 JavaFX 里判断按钮 hover 态CSS 要写成.button:hover { -fx-background-color: #66b1ff; } .button:disabled { -fx-opacity: 0.5; }重点来了JavaFX 的disabled伪类写法是:disabled不要写成:disabled那是 Web 写法。此外JavaFX 有一个特殊机制——控件如果设置了setDisable(true)整个节点的鼠标事件默认会被拦截这时候你用 hover 变色是无效的因为没有 hover 事件。所以权限控制的禁用按钮我后来统一用灰色样式 setMouseTransparent(true)视觉效果比原生的disabled更可控。3. JavaFX 里复刻 RuoYi 布局的完整方案3.1 项目结构与 MVCI 约定JavaFX 项目如果全堆在一个类里过两周你自己都找不到入口。参考了社区里比较常见的 MVCI 分层模式Model-View-Controller-Interactor我按功能拆包每个页面一个目录com.example.ruoyi.ui ├── App.java // Application 启动类 ├── styles │ ├── base.css // 全局基础样式 │ ├── theme-dark.css // 暗色主题后续扩展 │ └── fonts.css // 字体与图标 ├── common │ ├── components // Avatar、IconText、Toast 等通用组件 │ ├── permission // 按钮权限拦截辅助类 │ └── http // HttpClient 封装 ├── module │ ├── login // 登录页 │ ├── dashboard // 首页仪表盘 │ ├── user // 用户管理 │ ├── role // 角色管理 │ └── menu // 菜单管理 └── shell ├── MainFrame.java // 主框架 ├── Sidebar.java // 侧边菜单 ├── TopBar.java // 顶部导航 └── ContentArea.java // 内容区这个结构参考了 MVCI 的约定但没走那么死。每个 module 里面View 负责建控件Controller 负责监听事件Interactor 负责调接口数据模型用普通 POJO。这样拆的好处是UI 和逻辑完全分离即便是新人接手看到目录结构就大概知道去哪儿改页面。启动类这边特别提醒一句JavaFX 的Application.launch不能直接在 main 方法里调用必须包装一层。另外要在start里先设置UserAgentStylesheet否则默认样式会和你的自定义样式打架Override public void start(Stage primaryStage) { Application.setUserAgentStylesheet(getClass().getResource(/styles/base.css).toExternalForm()); // 构建 MainFrame MainFrame frame new MainFrame(); Scene scene new Scene(frame.getRoot(), 1440, 900); primaryStage.setScene(scene); primaryStage.setTitle(若依风格权限管理系统); primaryStage.show(); }3.2 主框架的搭建细节主框架我用 BorderPane 分五块top、left、center、bottom。left 这块是侧边菜单默认 220px 宽点击折叠按钮切换到 64px。折叠动画如果用Timeline改 preferredWidth在高分辨率屏幕下会有明显的锯齿感后来我直接改成了无动画切换观感反而更干净。内容区我用的是StackPane通过一个统一的ContentArea.switchView(String menuKey)方法来切换页面。为什么不直接加 TabPane因为 RuoYi 的标签页本质上是对应浏览器 tab 记忆的一种形态但这个项目的权限管理页面本来就不多做成 Tab 反而多一套缓存逻辑。如果你未来要扩展成几十个功能页可以考虑用 TabPane 方案但记得给每个 tab 设置setClosable(false)来保证主页面不被误关。侧边菜单我根据 RuoYi 的菜单层级做了两级结构一级菜单点击展开二级菜单二级菜单才是真正的页面入口。这个数据结构在 JavaFX 里最合适的控件其实是TreeView但 TreeView 的展开箭头、缩进、选中背景很难完全改造所以我用了Accordion嵌套TitledPane每个一级菜单对应一个 TitledPane里面放 VBox 的按钮列表。效果更接近 RuoYi而且代码好控制。TitledPane pane new TitledPane(menuName, new VBox(menuItems)); pane.setAnimated(false); pane.setCollapsible(true); accordion.getPanes().add(pane);setAnimated(false)很关键。如果你不关动画菜单展开收起的延迟会让用户觉得卡虽然技术上那不是卡只是动画帧太多导致的感觉。3.3 主题样式的组织方式JavaFX 样式表加载顺序是有讲究的。把base.css当成所有组件的基础皮肤把fonts.css单独拆出来因为字体如果封装成本地资源路径加载失败会导致启动时静默报错非常难排查。我使用的颜色、字号、圆角变量都在base.css根节点定义.root { -fx-ruoyi-primary: #409EFF; -fx-ruoyi-bg: #F0F2F5; -fx-ruoyi-card-bg: #FFFFFF; -fx-ruoyi-text-main: #303133; -fx-ruoyi-text-secondary: #606266; -fx-ruoyi-border: #DCDFE6; -fx-ruoyi-radius: 4px; }然后在具体控件里引用这些变量.table-view { -fx-background-color: -fx-ruoyi-card-bg; -fx-border-color: -fx-ruoyi-border; -fx-border-radius: -fx-ruoyi-radius; -fx-background-radius: -fx-ruoyi-radius; }JavaFX 对这种变量的支持没有 bug但有个小坑如果变量定义在.root上而某些控件不是 .root 的子节点链比如 Popup、ContextMenu 里的节点它们可能拿不到变量。遇到这种情况要么给 Popup 也设置一个getStylesheets()要么直接在控件的样式里写死颜色值。我的方案是给所有弹窗统一调用一个工具方法先把容器的样式表加进去再显示。4. 权限管理核心页面 UI 落地细节4.1 用户管理表格 状态开关 操作列用户管理是权限系统里最典型的 CRUD 页面。RuoYi 的用户表格大概长这样头像、用户名、昵称、手机号、状态开关、创建时间、操作修改、删除、重置密码。我这里的实现核心难点在 TableView 的自定义单元格。默认的TableView列是显示字符串的但状态列需要 Switch操作列需要多个按钮。这两个都必须写setCellFactory。状态列我用了一个javafx.scene.control.Switch并给它绑定了自定义事件。statusColumn.setCellFactory(col - new TableCellSysUser, Integer() { Override protected void updateItem(Integer status, boolean empty) { super.updateItem(status, empty); if (empty || status null) { setGraphic(null); } else { Switch statusSwitch new Switch(); statusSwitch.setSelected(status 1); statusSwitch.setOnAction(e - { SysUser user getTableView().getItems().get(getIndex()); userService.changeStatus(user.getId(), statusSwitch.isSelected()); }); setGraphic(statusSwitch); } } });这里有一个很容易踩的坑cellFactory 里不能直接用getItem()来拿当前行的数据因为单元格复用时getItem()可能会拿到旧值。最稳妥的是用getTableView().getItems().get(getIndex())。另外Switch作为图形节点每次updateItem都要新建不要复用同一个实例。操作列我把修改删除重置密码三个按钮放进一个HBox间距设置为 5px。删除按钮我特意用了setVisible(false)而非setDisable(true)来控制权限因为如果按钮不可见行内布局会更整齐。后面按钮级权限那块我会详细讲这个逻辑。4.2 角色管理卡片式列表和权限勾选弹窗角色管理没有用表格而是用了卡片流左侧一个ListView显示角色中间是角色基本信息区域下方是菜单权限树。如果你想做得更像 RuoYi也可以把卡片的每个角色变成一个圆角矩形VBox里面放角色名、角色编码、备注以及一个分配权限的按钮。我最终采用了ListView 右侧ScrollPane的方案因为角色数量很少而且需要展示每个角色的菜单权限明细。点击左侧角色的时候右侧加载对应的权限树一个CheckBoxTreeItem组成的TreeView父节点是顶级菜单子节点是按钮权限。权限树这块JavaFX 的CheckBoxTreeItem有一个很合适的机制独立选择模式independent。默认情况下父节点选中会联动子节点但权限系统的菜单树是父菜单不一定要勾选所有子按钮的所以需要把CheckBoxTreeItem.setIndependent(true)打开。否则用户勾选一个菜单时下面所有按钮权限都会被带上很容易配置出拥有删除权限但看不到删除按钮的奇怪状态。CheckBoxTreeItemString item new CheckBoxTreeItem(menuName); item.setIndependent(true);这个independent属性只对CheckBoxTreeItem有效如果你用的是普通TreeItem 自定义 cell就需要自己维护选中状态工作量会大很多。4.3 菜单管理树表 表单组件组合菜单管理页面是 RuoYi 里结构最复杂的一页因为菜单有父子层级、类型目录/菜单/按钮、路由地址、权限标识等多种字段。JavaFX 的TableView不支持树形展开我一开始纠结了很久最后选了TreeTableView来做。TreeTableView的每一行是一个TreeItem列可以正常绑定属性。实现起来比 TableView 复杂不少但换来的是和 RuoYi 完全一致的展开/折叠菜单树体验。这里需要注意TreeTableView的列宽不会自动适应内容需要手动设置setPrefWidth或者用列宽百分比TreeTableColumnSysMenu, String nameColumn new TreeTableColumn(菜单名称); nameColumn.setPrefWidth(220); nameColumn.setCellValueFactory(param - param.getValue().getValue().nameProperty());表单部分RuoYi 的菜单编辑是一个弹窗 Dialog字段很多。JavaFX 的DialogPane默认布局比较简陋我建议直接用Stage 自定义VBox来做弹窗样式完全可控。弹窗里注意回车键提交、Esc 关闭这两个常见交互JavaFX 的Button如果你是放在DialogPane里的可以设置setDefaultButton(true)来响应回车。如果用的是普通 Stage就要给 Scene 加KeyEvent过滤器。5. 菜单渲染与按钮级权限的联动逻辑5.1 动态菜单登录后拉取菜单树权限管理系统的 UI 不能是死菜单。RuoYi 的接口返回结构大致是一个菜单里面包含children数组每个菜单有menuTypeM 目录 / C 菜单 / F 按钮、menuName、perms权限标识、icon、visible等字段。前端拿到数据后我要做的是过滤掉visible 0的菜单然后渲染到侧边栏。这个逻辑我用了一个简单的递归方法public void buildMenu(ListSysMenu menus, VBox parent) { for (SysMenu menu : menus) { if (0.equals(menu.getVisible())) { continue; } // 有子菜单 - 创建 TitledPane // 无子菜单 - 创建 MenuButton } }图标这块我没有用图片资源而是用字体图标。JavaFX 里要显示图标可以用FontAwesomeFX但那个库对最新 Java 版本的支持一般。我后来换了一种更轻的方案用Text控件 Material Icons 字体。字体文件放在资源目录启动时加载Font.loadFont(App.class.getResourceAsStream(/fonts/MaterialIcons-Regular.ttf), 16);然后显示图标的方法就很简单了——把图标的 Unicode 字符放到 Text 控件里Text icon new Text(\uE853); // 对应 Material Icons 中的 refresh 图标 icon.setFont(Font.font(Material Icons, 16));用字体图标的好处是随字体可以随意改变颜色和大小不占图片资源窗口缩放也不会模糊。5.2 按钮级权限控制的收口方案RuoYi 的按钮权限方案是在页面按钮上绑定v-hasPermi[system:user:remove]之类的指令。JavaFX 没有指令我封装了一个权限工具类public class Perm { public static boolean has(String code) { return LoginUser.getInstance().getPerms().contains(code); } public static void check(Button button, String code) { boolean ok has(code); button.setManaged(ok); button.setVisible(ok); } }在页面初始化的时候对每个按钮调用一次Perm.check(deleteBtn, system:user:remove)就把 RuoYi 那套按钮权限逻辑平移到 JavaFX 了。这里有个细节按钮的setManaged(false)和setVisible(false)要一起写。managed控制的是该节点是否参与父容器的布局如果只设visible不设managed按钮虽然看不见但它在 HBox 里依然占位置操作列会出现一个奇怪的间隔。我踩过这个坑调试了半天才发现是managed没设。登录用户的权限集合怎么拿我在LoginUser单例里存了一份 List登录成功后从后端接口/getInfo拉取权限标识更新。切换账号后要清空整个主框架重新加载否则菜单和按钮权限会残留上一次登录的数据。这里建议做一个LoginContext的登出清理方法里面把侧边栏的所有 TitledPane 销毁强制走一次菜单重新构建。6. 这六个坑我踩得最疼6.1 分页组件得自己造RuoYi 的表格分页组件在页面底部上一页、页码、下一页、总记录数、每页条数。JavaFX 官方没有提供这个组件我一开始想找第三方库后来还是自己写了一个PaginationBar一个HBox左边是共 x 条记录右边是上一页 页码按钮 下一页 每页条数下拉框。核心逻辑就是维护一个 currentPage 和 totalPage点击页码时触发一个Runnable回调。public class PaginationBar extends HBox { private int currentPage 1; private int totalPage 1; private Runnable onPageChanged; public void setTotalCount(int total, int pageSize) { totalPage Math.max(1, (total pageSize - 1) / pageSize); render(); } }页码按钮数量多了会挤我给了一个简化策略只显示当前页前后 2 页以及第一页和最后一页中间用三个点省略。这个简化策略如果直接写循环生成按钮代码会有点绕。建议先把页码数组构造好private ListInteger buildPages() { ListInteger pages new ArrayList(); for (int i 1; i totalPage; i) { if (i 1 || i totalPage || Math.abs(i - currentPage) 2) { pages.add(i); } } return pages; }然后渲染的时候如果相邻两个页码差值大于 1则插入...文本。6.2 中文渲染和字体缺失JavaFX 在 Windows 上默认字体是微软雅黑看起来还行。但部署到某些精简版 Windows 或者 Linux 服务器上默认字体可能不包含完整中文界面全是方块字。解决办法是打包一份开源中文字体比如思源黑体的子集到资源目录在启动时显式加载Font.loadFont(App.class.getResourceAsStream(/fonts/SourceHanSansCN-Regular.otf), 14);然后全局设置字体.root { -fx-font-family: Source Han Sans CN; }字体文件如果过大启动加载会有点慢。特别是 OTF 字体动辄 10MB 以上在低配机器上会出现启动白屏。后来我换了 TTF 版本的思源黑体体积小一些加载明显更快。6.3 高分屏下界面糊成一团现在很多笔记本都是 2K、4K 分辨率Windows 的显示缩放默认是 150% 或者 200%。JavaFX 默认行为和高 DPI 的适配有时会抽风字体、控件尺寸、图片全都不对劲要么虚要么小。解决办法是启动时检测屏幕 DPI然后动态给 Scene 设置一个缩放系数。JavaFX 有个com.sun.javafx.scene.control.skin.Utils之类的内部类但不建议直接调。更稳妥的方案是在拼接 CSS 时把所有尺寸乘上一个比例。我实际采用的方式是在main方法里读取 Windows 的SystemDPI环境变量拿到缩放比例double scale GraphicsEnvironment.getLocalGraphicsEnvironment() .getDefaultScreenDevice().getDefaultConfiguration() .getDefaultTransform().getScaleX();然后在根节点上设置一个基准字号.root { -fx-font-size: 14px; /* 100% 缩放 */ -fx-scale-x: 1.25; -fx-scale-y: 1.25; }但我更推荐的做法是把所有尺寸都用百分比或变量表驱动在启动时根据scale生成一个theme-scale.css文件里面重写一套-fx-font-size和-fx-pref-width之类的值。这样代码不会到处都是硬编码尺寸。6.4 Toast 提示和确认框要自己封装RuoYi 的操作提示习惯是右上角弹出绿色操作成功、红色操作失败。JavaFX 的Alert类和这个视觉风格相差太远。我封装了一个Toast组件一个无边框的 Stage透明背景里面的 Label 设置圆角背景和文字颜色固定在窗口右上角2 秒后自动淡出。原理很简单public static void show(String message, Stage owner, boolean success) { Stage toastStage new Stage(); toastStage.initOwner(owner); toastStage.initStyle(StageStyle.TRANSPARENT); Label label new Label(message); label.setStyle(-fx-background-color: rgba(0,0,0,0.75); -fx-text-fill: white; -fx-padding: 10px 20px; -fx-background-radius: 6px;); Scene scene new Scene(new StackPane(label)); scene.setFill(null); toastStage.setScene(scene); // 计算位置owner 右上角 double x owner.getX() owner.getWidth() - 300; double y owner.getY() 60; toastStage.setX(x); toastStage.setY(y); toastStage.show(); // 2 秒后淡出关闭 PauseTransition delay new PauseTransition(Duration.seconds(2)); delay.setOnFinished(e - { FadeTransition fade new FadeTransition(Duration.millis(300), scene.getRoot()); fade.setToValue(0); fade.setOnFinished(ev - toastStage.close()); fade.play(); }); delay.play(); }确认框删除前提示确认删除我用的是Alert(AlertType.CONFIRMATION)配上DialogPane样式覆盖让它的按钮布局看起来和 Element UI 的 MessageBox 类似。这个方案偏简单但对内网工具来说已经够用了。6.5 JavaFX CSS 和 Web CSS 的差异点写这个项目的人如果之前是写 Web 前端的最容易踩的就是把 Web CSS 写法直接套到 JavaFX 上。整理几个常见的差异场景Web CSSJavaFX CSS背景渐变background: linear-gradient(...)-fx-background-color: linear-gradient(...)圆角border-radius: 4px-fx-background-radius: 4px; -fx-border-radius: 4px阴影box-shadow: 0 2px 8px rgba(0,0,0,.1)JavaFX 里用 effect: dropshadow(...)字体font-family-fx-font-family内边距padding-fx-paddingJavaFX 里给节点加阴影我推荐用 CSS 的方式而不是在代码里 new DropShadow。CSS 写法是.card { -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.1), 10, 0, 0, 2); }这个效果和 Web 的 box-shadow 很接近。但要注意一个节点只能有一个 Effect如果你同时想加阴影和模糊两个 Effect 会互相覆盖需要把多个 Effect 用Shadow组合起来。6.6 性能细节TableView 假死和滚动掉帧用户管理这个页面如果一次性加载几千条用户数据JavaFX 的 TableView 会明显变慢滚动时掉帧、内存占用飙升。数据库分页可以缓解一部分但有时候你就是要加载全部数据做本地搜索。实测下来TableView 慢的主要原因不是渲染而是cellFactory里的节点创建成本。如果你在updateItem里 new 了很多控件比如上面的 Switch每次滚动都会重新创建肯定会卡。优化方向有两个用Label代替复杂节点数据变化时只更新 Label 的文字。状态列用一个共享的 Switch 池按行号复用而不是每次 new。我实际做的时候把状态列改成了Label点击后再弹一个确认框虽然少了一个即时开关的交互手感但性能提升非常明显。表格 500 行数据内滚动手感很流畅。7. 再往前走一步从复刻到自定义桌面后台7.1 深度定制主题暗色模式RuoYi 本身有暗色菜单的变体但不能算完整的暗色主题。JavaFX 做暗色主题很方便——只要全局切换一套 CSS 就行。我在项目里预留了theme-dark.css里面覆盖了所有基础控件的颜色.root { -fx-ruoyi-bg: #1D1E1F; -fx-ruoyi-card-bg: #2A2B2D; -fx-ruoyi-text-main: #E5EAF0; -fx-ruoyi-border: #3D3D3F; }切换主题只需要一行代码scene.getStylesheets().set(0, getClass().getResource(/styles/theme-dark.css).toExternalForm());需要注意的是如果是运行时切换之前创建的控件如果已经有内联样式切换不会生效。所以最好把颜色全部放到 CSS 类不要在代码里setStyle硬写。7.2 更大的想象空间做完这套 UI 之后我有一个很强烈的感受JavaFX 并不是老掉牙的技术它只是缺少像 RuoYi 那样一套成熟的中后台设计语言。如果大家有兴趣其实可以把这个项目继续扩展成对接 WebSocket 做在线用户统计和通知中心集成ControlsFX的 Notification 和任务进度条把权限树组件进一步封装形成一套JavaFX 版 Element UI用ConcurrentHashMapObjectProperty做缓存层让页面之间切换数据不丢失。我在实际使用中发现凡是 RuoYi 这种后台管理系统常见的页面JavaFX 都能做出来只是很多能力需要自己封装不像 Web 生态有现成的库可以直接用。这也意味着一旦你把这套基础组件积累下来了后续业务接入的速度会越来越快。如果你正准备做类似的事我的建议是别一上来就想要不要换 Electron。先想清楚你的用户到底需要什么——如果他们要的是像 Web 后台一样好看、好用的桌面工具JavaFX 完全可以胜任。而且它没有 Electron 那么重的内存占用也没有跨平台字体渲染的糟心事只要 CS 架构下你能接受桌面端部署JavaFX 这套方案是很值得投入的。本文还有配套的精品资源点击获取