RuoYi-Vue项目实战:手把手教你改造字典模块,实现省市区级联选择(附完整代码)
RuoYi-Vue字典模块深度改造构建高性能级联选择器实战指南在企业管理系统的开发中省市区三级联动选择器几乎是每个项目都会遇到的标配功能。传统做法往往是为这类固定数据单独建表管理但当系统需要管理多种树形结构数据如组织架构、分类体系时这种方案会导致数据库表膨胀且维护成本剧增。本文将带你深度改造RuoYi-Vue的字典模块使其原生支持树形数据结构并以省市区级联选择器为例演示完整实现方案。1. 架构设计与核心改造思路树形字典的核心在于数据关系表达和高效查询。RuoYi默认的字典模块采用扁平化存储要实现级联选择需要解决三个关键问题数据关系存储在sys_dict_data表中增加parent_id字段构建父子关系树形数据查询改造后端接口返回嵌套结构的字典数据前端组件适配调整字典组件使其支持树形数据渲染数据库改造方案对比方案优点缺点适用场景增加parent_id字段改造成本低兼容现有功能递归查询性能一般层级固定且深度有限的数据闭包表(Closure Table)查询效率高支持任意深度存储空间大实现复杂需要频繁查询子树或路径的场景嵌套集(Nested Set)查询子树效率高写入性能差维护复杂读多写少的数据考虑到省市区数据通常不超过4级且变更频率低我们选择最简单的parent_id方案。在sys_dict_data表中新增字段ALTER TABLE sys_dict_data ADD COLUMN parent_id bigint(20) DEFAULT 0 COMMENT 父级字典ID;提示字段默认值设为0表示根节点与RuoYi的菜单模块设计保持一致便于复用现有工具方法2. 后端改造关键步骤2.1 实体类与Mapper调整在SysDictData实体类中增加字段映射// 新增父节点ID字段 private Long parentId; Excel(name 父级ID) public Long getParentId() { return parentId; } public void setParentId(Long parentId) { this.parentId parentId; }Mapper.xml中需要确保查询结果包含该字段resultMap typeSysDictData idSysDictDataResult !-- 原有映射保持不变 -- result propertyparentId columnparent_id/ /resultMap2.2 控制器接口改造新增不分页查询接口用于获取完整树形结构PreAuthorize(ss.hasPermi(system:dict:list)) GetMapping(/dataTree) public AjaxResult dataTree(SysDictData dictData) { ListSysDictData list dictDataService.selectDictDataList(dictData); return success(buildDictTree(list)); } private ListSysDictData buildDictTree(ListSysDictData list) { // 使用Ruoyi内置工具方法构建树形结构 return TreeUtils.build(list, 0L, dictCode, parentId, children); }修改字典保存逻辑增加父子关系校验PostMapping public AjaxResult add(Validated RequestBody SysDictData dict) { // 防止选择自己作为父节点 if (dict.getParentId() ! null dict.getParentId().equals(dict.getDictCode())) { return error(不能选择自己作为上级节点); } // 原有保存逻辑... }3. 前端深度适配方案3.1 字典管理界面树形改造创建dataTree.vue替代原字典管理页面核心改动点template el-table :datadataList row-keydictCode :tree-props{children: children} default-expand-all !-- 列定义保持不变 -- /el-table el-dialog el-form-item label上级字典 propparentId treeselect v-modelform.parentId :optionsdictOptions :normalizernormalizer placeholder选择上级字典/ /el-form-item /el-dialog /template script import Treeselect from riophae/vue-treeselect; export default { methods: { normalizer(node) { return { id: node.dictCode, label: node.dictLabel, children: node.children }; } } }; /script3.2 字典组件核心改造修改src/components/DictData/index.vue中的字典加载逻辑function install() { Vue.use(DataDict, { metas: { *: { request(dictMeta) { return new Promise((resolve) { getDicts(dictMeta.type).then(res { const treeData handleTree(res.data, dictCode, parentId); resolve(treeData); }); }); } } } }); }新增DictConverter递归处理逻辑function dictConverter(dict, dictMeta) { const children dict[dictMeta.childrenField] || []; return new DictData( dict[dictMeta.labelField], dict[dictMeta.valueField], dict, children.map(child dictConverter(child, dictMeta)) ); }4. 级联选择器实战应用4.1 省市区数据准备建议按以下格式准备字典数据- 字典类型sys_area - 数据项 - 北京市 (dictCode1, parentId0) - 东城区 (dictCode11, parentId1) - 西城区 (dictCode12, parentId1) - 上海市 (dictCode2, parentId0) - 黄浦区 (dictCode21, parentId2)4.2 组件调用示例在表单页面中使用级联选择器template el-cascader v-modelform.area :propsareaProps :optionsareaOptions/ /template script export default { data() { return { areaProps: { value: value, label: label, children: children, checkStrictly: true // 可选择任意级别 }, areaOptions: [] }; }, created() { this.$dict.load(sys_area).then(data { this.areaOptions data; }); } }; /script4.3 性能优化技巧懒加载配置areaProps: { lazy: true, async lazyLoad(node, resolve) { const { level } node; const parentId level 0 ? 0 : node.value; const { data } await listAreas(parentId); resolve(data); } }缓存策略// 在store中缓存已加载的字典数据 const dictCache new Map(); function loadDict(type) { if (dictCache.has(type)) { return Promise.resolve(dictCache.get(type)); } return getDictTree(type).then(data { dictCache.set(type, data); return data; }); }5. 进阶扩展方案5.1 多级字典联动实现实现省市区三级联动后可进一步扩展为行业分类等复杂场景el-cascader v-modelform.industry :props{ value: dictCode, label: dictLabel, children: children } :optionsindustryOptions/5.2 与代码生成器集成修改代码生成模板自动识别树形字典并生成级联选择器// 在GenUtil.java中增加字典类型判断 if (dictType ! null isTreeDict(dictType)) { vueTemplate vueTemplate.replace( el-select, el-cascader :props{value:\dictValue\,label:\dictLabel\,children:\children\} ); }5.3 类型安全增强为字典数据定义TypeScript类型interface DictTree { value: string; label: string; children?: DictTree[]; raw?: Recordstring, any; } declare module vue/types/vue { interface Vue { $dict: { load(type: string): PromiseDictTree[]; }; } }6. 常见问题解决方案Q1. 如何解决大数据量下的性能问题A. 可采用以下优化策略后端添加Cacheable注解缓存树形数据前端实现虚拟滚动使用el-tree-v2按需加载子节点配置lazy:trueQ2. 现有项目如何平滑迁移分阶段迁移方案先保持原有字典表不变新增sys_dict_tree专用表通过定时任务同步关键字典数据逐步改造前端页面最后移除旧表Q3. 如何实现字典项的拖动排序在dataTree.vue中集成vuedraggabledraggable v-modeldataList groupdict endonDragEnd !-- 原有表格内容 -- /draggable script import draggable from vuedraggable; export default { methods: { onDragEnd() { updateSort(this.dataList); } } }; /scriptQ4. 如何保证父子关系的一致性添加数据库约束ALTER TABLE sys_dict_data ADD CONSTRAINT fk_parent_id FOREIGN KEY (parent_id) REFERENCES sys_dict_data(dict_code) ON DELETE CASCADE;7. 工程化建议API文档生成ApiOperation(获取树形字典数据) GetMapping(/tree) public ResultListSysDictData getDictTree(RequestParam String dictType) { // ... }单元测试覆盖Test public void testBuildDictTree() { ListSysDictData list Arrays.asList( new SysDictData(1L, 北京, 110000, 0L), new SysDictData(2L, 东城, 110101, 1L) ); ListSysDictData tree dictService.buildTree(list); assertEquals(1, tree.size()); assertEquals(1, tree.get(0).getChildren().size()); }前端组件封装!-- DictCascader.vue -- template el-cascader v-bind$attrs :optionstreeData v-on$listeners/ /template script export default { props: [dictType], data() { return { treeData: [] }; }, created() { this.loadDict(); }, methods: { async loadDict() { this.treeData await this.$dict.load(this.dictType); } } }; /script改造后的字典模块不仅支持省市区级联选择更能作为通用树形数据管理方案轻松应对组织架构、产品分类、权限树等各类场景。实际项目中可根据具体需求进一步扩展字段验证、操作日志、数据权限等企业级功能。