# 数据字典类字段显示与导出实现方式 ## 1. 后端接口请求 ### 1.1 批量字典查询接口 项目采用"前端预加载字典 + 本地映射"方案,后端提供批量查询接口,前端一次性获取页面所需的所有字典数据。 **接口位置**: `jeecg-boot/.../dictionary/controller/DictionaryController.java` ```java @Operation(summary = "批量根据字典编码获取字典项") @PostMapping(value = "/getDictBatch") public Result>>> getDictBatch(@RequestBody List dictionaryCodes) { return Result.OK(dictionaryItemService.getDictItemsBatch(dictionaryCodes)); } ``` - Controller 只负责参数接收和结果返回,业务逻辑在 Service 层实现 - Service 层通过 QueryWrapper 查询 `DICTIONARY_ITEM` 表,按 `OrderNo` 排序,结果封装为 `{label, value}` 格式 **请求示例**: ``` POST /system/dictionary/getDictBatch Content-Type: application/json ["Gender", "MaritalStatus"] ``` **返回格式**(`Result` 包装后的 `result` 字段内容): ```json { "Gender": [ { "label": "男性", "value": 1 }, { "label": "女性", "value": 2 } ], "MaritalStatus": [ { "label": "未婚", "value": 1 }, { "label": "已婚", "value": 2 } ] } ``` ### 1.2 业务接口设计原则 业务接口(如人员列表查询)只返回字典 value(数字编码),不做 JOIN 查 label。例如人员信息接口返回 `{ gender: 1, maritalStatus: 2 }`,label 的转换由前端统一完成。这样业务接口与字典表解耦,字典变更不影响业务接口。 > **Excel 导出场景**:导出时采用"后端内存映射"方案(详见第 5 章),导出前批量查询字典,在内存中将字典值替换为中文标签,再由 > AutoPoi 写入 Excel。 --- ## 2. 前端数据缓存 ### 2.1 useDict 工具 **位置**: `jeecgboot-vue3/src/hooks/dictionary/useDict.ts` `useDict` 是一个 Vue 3 composable,负责从后端批量接口加载字典数据,并维护全局响应式缓存。 #### 核心设计 - **模块级单例缓存**:使用 `reactive()` 创建模块级 `dictCache` 对象,多个组件实例共享同一份缓存,只发起一次网络请求 - **防重复请求**:当已有请求在途中时,后续调用方等待同一请求完成,完成后再次检查是否仍缺失所需字典,缺则继续发起新请求 - **响应式更新**:缓存数据加载完成后,所有依赖 `dictCache` 的 `computed` 和模板自动刷新 - **类型统一**:后端 `DictionaryItem.value` 为 `Integer`,`PersonalInfo` 中字典字段为 `String`,缓存时通过 `String(item.value)` 统一转为字符串,确保 `` 的 `v-model` 能正确匹配选项 #### 导出方法 | 方法 | 用途 | |----------------------------|------------------------------------| | `getDictText(code, value)` | 根据字典编码和 value 获取对应 label,用于表格列显示 | | `getDictOptions(code)` | 获取 `{label, value}[]` 选项列表,用于下拉框绑定 | | `fetchDicts()` | 手动触发字典加载(在 `onMounted` 中自动调用) | `getDictText` 内部使用 `String(i.value) === String(value)` 做类型不敏感比较,兼容后端返回的数字和字符串类型混用场景。 #### 使用方式 ```typescript import { useDict } from '/@/hooks/dictionary/useDict'; const { getDictText, getDictOptions } = useDict([ 'Gender', 'MaritalStatus', 'JobSeekerCategory' ]); ``` --- ## 3. 页面 input 组件实现 ### 3.1 列表页搜索框 **位置**: `jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue` 预加载字典后,用 `` 绑定 `getDictOptions()` 返回的选项列表,替代原来的 `` 或 ``。 ```vue ``` ### 3.2 表单页编辑字段 **位置**: `jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue` 将字典相关的字段从 `` 改为 `` 下拉选择。表单页需要加载所有涉及的字典,包括列表页可能没用到但表单编辑需要的字典。 ```vue ``` --- ## 4. 数据表格显示 ### 4.1 使用 bodyCell 插槽翻译字典值 **位置**: `jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue` 表格列通过 `column.dataIndex` 配置为字典字段的 value 字段名,渲染时由 `bodyCell` 插槽拦截,调用 `getDictText()` 将 value 翻译为 label 显示。 ```vue ``` ### 4.2 处理逻辑 1. 表格从业务接口获取原始数据,字典字段只包含 value(如 `{ gender: 1 }`) 2. JeecgBoot 的 `BasicTable` 渲染时,`bodyCell` 插槽的 `text` 参数即为原始 value 3. 通过 `column.dataIndex` 识别哪个列是字典字段 4. 调用对应的 `getDictText('字典编码', text)` 从缓存中查找匹配项 5. 匹配成功显示 label,匹配失败回退显示原始 value --- ## 5. Excel 导出字典映射(后端内存映射方案) ### 5.1 方案说明 Excel 导出由后端直接生成文件,不经过前端映射。采用"后端内存映射"方案: - 导出前通过 `IDictionaryItemService.getDictItemsBatch()` 批量查询导出涉及的所有字典 - 构建 `Map<字典编码, Map>` 快速查找结构 - 遍历导出数据,将实体中字典字段的 value 值替换为对应的中文 label - 替换完成后交由 AutoPoi 框架写入 Excel ### 5.2 职责划分 字典翻译的业务逻辑不在 Controller 中实现,而是提取到 Service 层,遵循分层架构原则: | 层级 | 职责 | 文件 | |----------------|------------------------------------|--------------------------------| | **Controller** | 参数接收、查询条件组装、选中数据过滤、ModelAndView 构建 | `PersonalInfoController.java` | | **Service** | 批量查询字典、构建 value→label 映射、遍历替换实体字段值 | `PersonalInfoServiceImpl.java` | ### 5.3 Controller 层代码 **位置**: `jeecg-boot/.../personal/controller/PersonalInfoController.java`(`exportXls` 方法) Controller 只负责请求处理和视图构建,字典翻译逻辑委托给 Service: ```java @RequiresPermissions("personal:personal_info:exportXls") @RequestMapping(value = "/exportXls") public ModelAndView exportXls(HttpServletRequest request, PersonalInfo personalInfo) { // Step.1 组装查询条件 QueryWrapper queryWrapper = QueryGenerator.initQueryWrapper(personalInfo, request.getParameterMap()); LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); // 过滤选中数据 String selections = request.getParameter("selections"); if (oConvertUtils.isNotEmpty(selections)) { List selectionList = Arrays.asList(selections.split(",")); queryWrapper.in("id", selectionList); } // Step.2 获取导出数据并进行字典值→标签内存映射(业务逻辑委托给 Service) List exportList = personalInfoService.translateDictFields( service.list(queryWrapper), EXPORT_DICT_FIELD_MAP); // Step.3 AutoPoi 导出Excel(此时字典字段已替换为中文标签) ModelAndView mv = new ModelAndView(new JeecgEntityExcelView()); String title = "个人基本信息表"; mv.addObject(NormalExcelConstants.FILE_NAME, title); mv.addObject(NormalExcelConstants.CLASS, PersonalInfo.class); ExportParams exportParams = new ExportParams(title + "报表", "导出人:" + sysUser.getRealname(), title, ExcelType.XSSF); mv.addObject(NormalExcelConstants.PARAMS, exportParams); mv.addObject(NormalExcelConstants.DATA_LIST, exportList); String exportFields = "fullName,gender,birthDate,education,householdLocation,currentResidence,jobSeekerCategory,contactPhone,jobSearchStatus,dataSource"; mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields); return mv; } ``` ### 5.4 Service 层实现 **位置**: `jeecg-boot/.../personal/service/IPersonalInfoService.java`(接口声明) ```java /** * 导出时对字典字段进行值→标签的内存映射翻译 * * @param list 待导出的数据列表 * @param fieldDictMap 字段名→字典编码 的映射关系 * @return 字典字段已替换为中文标签的数据列表 */ List translateDictFields(List list, Map fieldDictMap); ``` **位置**: `jeecg-boot/.../personal/service/impl/PersonalInfoServiceImpl.java`(实现) ```java @Service public class PersonalInfoServiceImpl extends ServiceImpl implements IPersonalInfoService { @Autowired private IDictionaryItemService dictionaryItemService; @Override public List translateDictFields(List list, Map fieldDictMap) { // 批量查询导出涉及的所有字典数据 List dictCodes = new ArrayList<>(fieldDictMap.values()); Map>> dictData = dictionaryItemService.getDictItemsBatch(dictCodes); // 构建 value→label 快速查找映射: Map<字典编码, Map> Map> valueLabelMap = new HashMap<>(); for (Map.Entry>> entry : dictData.entrySet()) { Map valueToLabel = new HashMap<>(); for (Map item : entry.getValue()) { valueToLabel.put(String.valueOf(item.get("value")), String.valueOf(item.get("label"))); } valueLabelMap.put(entry.getKey(), valueToLabel); } // 遍历导出数据,将字典字段的值替换为对应的中文标签 for (PersonalInfo info : list) { for (Map.Entry fieldEntry : fieldDictMap.entrySet()) { String fieldName = fieldEntry.getKey(); String dictCode = fieldEntry.getValue(); Map mapping = valueLabelMap.get(dictCode); if (mapping == null) continue; switch (fieldName) { case "gender": if (info.getGender() != null) info.setGender(mapping.getOrDefault(info.getGender(), info.getGender())); break; case "jobSeekerCategory": if (info.getJobSeekerCategory() != null) info.setJobSeekerCategory(mapping.getOrDefault(info.getJobSeekerCategory(), info.getJobSeekerCategory())); break; case "jobSearchStatus": if (info.getJobSearchStatus() != null) info.setJobSearchStatus(mapping.getOrDefault(info.getJobSearchStatus(), info.getJobSearchStatus())); break; case "dataSource": if (info.getDataSource() != null) info.setDataSource(mapping.getOrDefault(info.getDataSource(), info.getDataSource())); break; } } } return list; } } ``` ### 5.5 字段映射配置 **位置**: `PersonalInfoController.java`(类级别常量) ```java /** * 导出字段与字典编码的映射关系:entity字段名 → 字典编码 * 用于导出时自动将字典值翻译为中文标签 */ private static final Map EXPORT_DICT_FIELD_MAP = new LinkedHashMap<>(); static { EXPORT_DICT_FIELD_MAP.put("gender", "Gender"); EXPORT_DICT_FIELD_MAP.put("jobSeekerCategory", "JobSeekerCategory"); EXPORT_DICT_FIELD_MAP.put("jobSearchStatus", "JobSeekerStatus"); EXPORT_DICT_FIELD_MAP.put("dataSource", "DataSource"); } ``` 如需为其他字典字段添加导出翻译,只需在此映射中追加条目,并在 Service 的 `switch` 中添加对应 case 即可。 ### 5.6 数据流 ``` 业务库数据: { gender: "1", jobSeekerCategory: "2", jobSearchStatus: "1", dataSource: "1" } │ ▼ Service.translateDictFields() │ ├── ① getDictItemsBatch(["Gender", "JobSeekerCategory", "JobSeekerStatus", "DataSource"]) │ ├── ② 构建 value→label 映射 │ { Gender → {"1"→"男性", "2"→"女性"}, ... } │ ├── ③ 遍历替换实体字段值 │ { gender: "男性", jobSeekerCategory: "失业人员", jobSearchStatus: "待业中", dataSource: "系统录入" } │ └── ④ 返回替换后的列表给 Controller │ ▼ Controller → AutoPoi → Excel Excel 中直接显示中文标签 ``` ### 5.7 与前端映射方案对比 | 环节 | 前端映射方案 | 后端内存映射方案 | |--------|--------------------------------|----------------------------| | 适用场景 | 页面展示、查询表单、编辑表单 | Excel 导出 | | 字典查询时机 | 页面挂载时一次性预加载 | 导出时实时查询 | | 缓存方式 | `reactive()` 模块级全局缓存 | 临时 Map,导出完成后丢弃 | | 替换方式 | `bodyCell` 插槽中 `getDictText()` | Service 层 setter 直接修改实体字段值 | | 逻辑归属 | 前端 `useDict` composable | 后端 Service 层 | | 字典变更影响 | 页面刷新后生效 | 每次导出实时查询,立即生效 |