项目采用"前端预加载字典 + 本地映射"方案,后端提供批量查询接口,前端一次性获取页面所需的所有字典数据。
接口位置: jeecg-boot/.../dictionary/controller/DictionaryController.java
@Operation(summary = "批量根据字典编码获取字典项")
@PostMapping(value = "/getDictBatch")
public Result<Map<String, List<Map<String, Object>>>> getDictBatch(@RequestBody List<String> dictionaryCodes) {
return Result.OK(dictionaryItemService.getDictItemsBatch(dictionaryCodes));
}
DICTIONARY_ITEM 表,按 OrderNo 排序,结果封装为 {label, value} 格式请求示例:
POST /system/dictionary/getDictBatch
Content-Type: application/json
["Gender", "MaritalStatus"]
返回格式(Result 包装后的 result 字段内容):
{
"Gender": [
{ "label": "男性", "value": 1 },
{ "label": "女性", "value": 2 }
],
"MaritalStatus": [
{ "label": "未婚", "value": 1 },
{ "label": "已婚", "value": 2 }
]
}
业务接口(如人员列表查询)只返回字典 value(数字编码),不做 JOIN 查 label。例如人员信息接口返回
{ gender: 1, maritalStatus: 2 },label 的转换由前端统一完成。这样业务接口与字典表解耦,字典变更不影响业务接口。
Excel 导出场景:导出时采用"后端内存映射"方案(详见第 5 章),导出前批量查询字典,在内存中将字典值替换为中文标签,再由 AutoPoi 写入 Excel。
位置: jeecgboot-vue3/src/hooks/dictionary/useDict.ts
useDict 是一个 Vue 3 composable,负责从后端批量接口加载字典数据,并维护全局响应式缓存。
reactive() 创建模块级 dictCache 对象,多个组件实例共享同一份缓存,只发起一次网络请求dictCache 的 computed 和模板自动刷新DictionaryItem.value 为 Integer,PersonalInfo 中字典字段为 String,缓存时通过
String(item.value) 统一转为字符串,确保 <a-select> 的 v-model 能正确匹配选项| 方法 | 用途 |
|---|---|
getDictText(code, value) |
根据字典编码和 value 获取对应 label,用于表格列显示 |
getDictOptions(code) |
获取 {label, value}[] 选项列表,用于下拉框绑定 |
fetchDicts() |
手动触发字典加载(在 onMounted 中自动调用) |
getDictText 内部使用 String(i.value) === String(value) 做类型不敏感比较,兼容后端返回的数字和字符串类型混用场景。
import { useDict } from '/@/hooks/dictionary/useDict';
const { getDictText, getDictOptions } = useDict([
'Gender', 'MaritalStatus', 'JobSeekerCategory'
]);
位置: jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue
预加载字典后,用 <a-select> 绑定 getDictOptions() 返回的选项列表,替代原来的 <j-dict-select-tag> 或 <a-input>。
<script setup lang="ts">
const { getDictOptions } = useDict(['Gender', 'JobSeekerCategory', 'JobSeekerStatus', 'DataSource']);
const genderOptions = computed(() => getDictOptions('Gender'));
const jobSeekerCategoryOptions = computed(() => getDictOptions('JobSeekerCategory'));
const jobSearchStatusOptions = computed(() => getDictOptions('JobSeekerStatus'));
const dataSourceOptions = computed(() => getDictOptions('DataSource'));
</script>
<template>
<a-select v-model:value="queryParam.gender" placeholder="请选择性别"
allow-clear :options="genderOptions" />
<a-select v-model:value="queryParam.jobSeekerCategory" placeholder="请选择求职人员类别"
allow-clear :options="jobSeekerCategoryOptions" />
<a-select v-model:value="queryParam.jobSearchStatus" placeholder="请选择求职状态"
allow-clear :options="jobSearchStatusOptions" />
<a-select v-model:value="queryParam.dataSource" placeholder="请选择数据来源"
allow-clear :options="dataSourceOptions" />
</template>
位置: jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue
将字典相关的字段从 <a-input> 改为 <a-select> 下拉选择。表单页需要加载所有涉及的字典,包括列表页可能没用到但表单编辑需要的字典。
<script setup lang="ts">
const { getDictOptions } = useDict([
'IDType', 'Gender', 'MaritalStatus', 'PoliticalStatus',
'WorkExperience', 'HouseholdType',
'JobSeekerCategory', 'JobSeekerStatus', 'DataSource'
]);
const idTypeOptions = computed(() => getDictOptions('IDType'));
const genderOptions = computed(() => getDictOptions('Gender'));
const maritalStatusOptions = computed(() => getDictOptions('MaritalStatus'));
const politicalStatusOptions = computed(() => getDictOptions('PoliticalStatus'));
const workExperienceOptions = computed(() => getDictOptions('WorkExperience'));
const householdTypeOptions = computed(() => getDictOptions('HouseholdType'));
const jobSeekerCategoryOptions = computed(() => getDictOptions('JobSeekerCategory'));
const jobSearchStatusOptions = computed(() => getDictOptions('JobSeekerStatus'));
const dataSourceOptions = computed(() => getDictOptions('DataSource'));
</script>
<template>
<a-form-model-item label="性别" prop="gender">
<a-select v-model:value="formData.gender" placeholder="请选择性别"
allow-clear :options="genderOptions" />
</a-form-model-item>
<a-form-model-item label="婚姻状况" prop="maritalStatus">
<a-select v-model:value="formData.maritalStatus" placeholder="请选择婚姻状况"
allow-clear :options="maritalStatusOptions" />
</a-form-model-item>
<!-- 其他字典字段类似 -->
</template>
位置: jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue
表格列通过 column.dataIndex 配置为字典字段的 value 字段名,渲染时由 bodyCell 插槽拦截,调用 getDictText() 将 value
翻译为 label 显示。
<BasicTable @register="registerTable">
<template v-slot:bodyCell="{ column, text }">
<template v-if="column.dataIndex === 'gender'">
{{ getDictText('Gender', text) }}
</template>
<template v-else-if="column.dataIndex === 'jobSeekerCategory'">
{{ getDictText('JobSeekerCategory', text) }}
</template>
<template v-else-if="column.dataIndex === 'jobSearchStatus'">
{{ getDictText('JobSeekerStatus', text) }}
</template>
<template v-else-if="column.dataIndex === 'dataSource'">
{{ getDictText('DataSource', text) }}
</template>
<!-- 非字典字段不做处理 -->
</template>
</BasicTable>
{ gender: 1 })BasicTable 渲染时,bodyCell 插槽的 text 参数即为原始 valuecolumn.dataIndex 识别哪个列是字典字段getDictText('字典编码', text) 从缓存中查找匹配项Excel 导出由后端直接生成文件,不经过前端映射。采用"后端内存映射"方案:
IDictionaryItemService.getDictItemsBatch() 批量查询导出涉及的所有字典Map<字典编码, Map<value, label>> 快速查找结构字典翻译的业务逻辑不在 Controller 中实现,而是提取到 Service 层,遵循分层架构原则:
| 层级 | 职责 | 文件 |
|---|---|---|
| Controller | 参数接收、查询条件组装、选中数据过滤、ModelAndView 构建 | PersonalInfoController.java |
| Service | 批量查询字典、构建 value→label 映射、遍历替换实体字段值 | PersonalInfoServiceImpl.java |
位置: jeecg-boot/.../personal/controller/PersonalInfoController.java(exportXls 方法)
Controller 只负责请求处理和视图构建,字典翻译逻辑委托给 Service:
@RequiresPermissions("personal:personal_info:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, PersonalInfo personalInfo) {
// Step.1 组装查询条件
QueryWrapper<PersonalInfo> queryWrapper = QueryGenerator.initQueryWrapper(personalInfo, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
// 过滤选中数据
String selections = request.getParameter("selections");
if (oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id", selectionList);
}
// Step.2 获取导出数据并进行字典值→标签内存映射(业务逻辑委托给 Service)
List<PersonalInfo> 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;
}
位置: jeecg-boot/.../personal/service/IPersonalInfoService.java(接口声明)
/**
* 导出时对字典字段进行值→标签的内存映射翻译
*
* @param list 待导出的数据列表
* @param fieldDictMap 字段名→字典编码 的映射关系
* @return 字典字段已替换为中文标签的数据列表
*/
List<PersonalInfo> translateDictFields(List<PersonalInfo> list, Map<String, String> fieldDictMap);
位置: jeecg-boot/.../personal/service/impl/PersonalInfoServiceImpl.java(实现)
@Service
public class PersonalInfoServiceImpl extends ServiceImpl<PersonalInfoMapper, PersonalInfo>
implements IPersonalInfoService {
@Autowired
private IDictionaryItemService dictionaryItemService;
@Override
public List<PersonalInfo> translateDictFields(List<PersonalInfo> list, Map<String, String> fieldDictMap) {
// 批量查询导出涉及的所有字典数据
List<String> dictCodes = new ArrayList<>(fieldDictMap.values());
Map<String, List<Map<String, Object>>> dictData = dictionaryItemService.getDictItemsBatch(dictCodes);
// 构建 value→label 快速查找映射: Map<字典编码, Map<value, label>>
Map<String, Map<String, String>> valueLabelMap = new HashMap<>();
for (Map.Entry<String, List<Map<String, Object>>> entry : dictData.entrySet()) {
Map<String, String> valueToLabel = new HashMap<>();
for (Map<String, Object> 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<String, String> fieldEntry : fieldDictMap.entrySet()) {
String fieldName = fieldEntry.getKey();
String dictCode = fieldEntry.getValue();
Map<String, String> 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;
}
}
位置: PersonalInfoController.java(类级别常量)
/**
* 导出字段与字典编码的映射关系:entity字段名 → 字典编码
* 用于导出时自动将字典值翻译为中文标签
*/
private static final Map<String, String> 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 即可。
业务库数据: { 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 中直接显示中文标签
| 环节 | 前端映射方案 | 后端内存映射方案 |
|---|---|---|
| 适用场景 | 页面展示、查询表单、编辑表单 | Excel 导出 |
| 字典查询时机 | 页面挂载时一次性预加载 | 导出时实时查询 |
| 缓存方式 | reactive() 模块级全局缓存 |
临时 Map,导出完成后丢弃 |
| 替换方式 | bodyCell 插槽中 getDictText() |
Service 层 setter 直接修改实体字段值 |
| 逻辑归属 | 前端 useDict composable |
后端 Service 层 |
| 字典变更影响 | 页面刷新后生效 | 每次导出实时查询,立即生效 |