260604-数据字典类字段显示与导出实现方式.md 28 KB

数据字典类字段显示与导出实现方式

1. 后端接口请求

1.1 批量字典查询接口

项目采用"前端预加载字典 + 本地映射"方案,后端提供批量查询接口,前端一次性获取页面所需的所有字典数据。

接口位置: 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));
}
  • Controller 只负责参数接收和结果返回,业务逻辑在 Service 层实现
  • Service 层通过 QueryWrapper 查询 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 }
  ]
}

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 对象,多个组件实例共享同一份缓存,只发起一次网络请求
  • 防重复请求:当已有请求在途中时,后续调用方等待同一请求完成,完成后再次检查是否仍缺失所需字典,缺则继续发起新请求
  • 响应式更新:缓存数据加载完成后,所有依赖 dictCachecomputed 和模板自动刷新
  • 类型统一:后端 DictionaryItem.valueIntegerPersonalInfo 中字典字段为 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'
]);

3. 页面 input 组件实现

3.1 列表页搜索框

位置: 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>

3.2 表单页编辑字段

位置: 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>

4. 数据表格显示

4.1 使用 bodyCell 插槽翻译字典值

位置: 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>

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>> 快速查找结构
  • 遍历导出数据,将实体中字典字段的 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.javaexportXls 方法)

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;
}

5.4 Service 层实现

位置: 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;
    }
}

5.5 字段映射配置

位置: 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 即可。

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 层
字典变更影响 页面刷新后生效 每次导出实时查询,立即生效

6. 树形字典(以行政区划 XZQH 为例)

6.1 方案概述

对于大规模层级数据(如行政区划 44K+ 节点、职业分类 2K+ 节点),不能使用第 1~5 章的扁平字典方案一次性全量加载。 本方案采用"懒加载树 + 双字段存储"策略:

项目 说明
存储方案 实体存两个字段:叶子代码(已有列,如 householdLocation)+ 完整路径(新增列,如 householdAreaName
后端加载 懒加载接口 /getDictTreeChildren,按需加载当前展开层级的子节点
前端编辑 <a-tree-select> + loadData 懒加载 + labelInValue 回显完整路径
前端回显 编辑时直接用已存储的完整路径文本(householdAreaName),无需调 API
表格/详情/导出 直接显示 householdAreaName 字段,无需字典翻译

6.2 数据模型设计

数据库表中已有叶子代码列,新增完整路径文本列:

位置: .docs/sql/个人信息与简历信息.sql

ALTER TABLE PERSONAL_INFO ADD HOUSEHOLD_AREA_NAME VARCHAR(500);
COMMENT ON COLUMN PERSONAL_INFO.HOUSEHOLD_AREA_NAME IS '户口所在地完整路径(如:广东省湛江市赤坎区)';
ALTER TABLE PERSONAL_INFO ADD RESIDENCE_AREA_NAME VARCHAR(500);
COMMENT ON COLUMN PERSONAL_INFO.RESIDENCE_AREA_NAME IS '现居住地完整路径(如:广东省湛江市赤坎区)';

Entity 新增字段(位置:PersonalInfo.java):

/** 户口所在地完整路径 */
@Excel(name = "户口所在地完整路径", width = 25)
private java.lang.String householdAreaName;

/** 现居住地完整路径 */
@Excel(name = "现居住地完整路径", width = 25)
private java.lang.String residenceAreaName;

6.3 后端接口

所有树形字典接口位于 DictionaryController.java,路径前缀 /system/dictionary

6.3.1 懒加载子节点(核心接口)

GET /system/dictionary/getDictTreeChildren?dictionaryCode=XZQH&parentCode=
  • parentCode 为空时返回根节点(省级)
  • 返回格式 [{key, title, leaf}]leaf=true 表示叶子节点(无展开箭头)
  • 通过一次批量 SELECT DISTINCT ParentItemID 查询判断每个子节点是否有后代,避免 N+1

Service 实现位置: DictionaryItemServiceImpl.javagetDictTreeChildren()

6.3.2 获取完整路径

GET /system/dictionary/getItemFullPath?dictionaryCode=XZQH&code=440802
→ "广东省湛江市赤坎区"

沿 parentItemId 链从叶子节点向上回溯,将各层级名称拼接。

Service 实现位置: DictionaryItemServiceImpl.javagetItemFullPath()

6.3.3 根据 code 回显名称(value→label)

GET /system/dictionary/loadDictItem/XZQH?key=440100,440200
→ ["广州市", "韶关市"]

支持逗号分隔批量查询,按 key 的原始顺序返回。

6.3.4 模糊搜索(用于搜索展开)

GET /system/dictionary/searchDictItem?dictionaryCode=XZQH&keyword=湛江&limit=20
→ [{code, name, fullPath, ancestorCodes}, ...]

Name LIKE '%keyword%' 模糊匹配,附带完整路径和祖先节点 code 链。

6.4 前端编辑表单实现

位置: jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue

6.4.1 模板

使用 <a-tree-select> 替代 <a-input>

<a-tree-select
  v-model:value="householdValueObj"
  v-model:tree-expanded-keys="householdExpandedKeys"
  :tree-data="householdTreeData"
  :load-data="(node) => loadTreeChildren(node, 'householdTreeData')"
  placeholder="请选择户口所在地"
  allow-clear
  label-in-value
  @change="(val) => handleAreaChange(val, 'household')"
/>

关键属性:

  • label-in-value: v-model 绑定 {value, label} 对象,支持自定义显示文本
  • load-data: 展开节点时触发懒加载
  • tree-expanded-keys: 受控展开,编辑回显时可自动展开祖先层级
  • 不使用 show-search(内置客户端搜索无法覆盖懒加载未加载的节点)

6.4.2 根节点加载

async function loadAreaRootNodes() {
  const res = await defHttp.get({
    url: '/system/dictionary/getDictTreeChildren',
    params: { dictionaryCode: 'XZQH', parentCode: '' },
  }, { isTransformResponse: false });
  if (res.success && res.result) {
    return res.result.map((item) => ({
      key: item.key, title: item.title, value: item.key, isLeaf: item.leaf,
    }));
  }
  return [];
}

// onMounted 中调用
const rootNodes = await loadAreaRootNodes();
householdTreeData.value = rootNodes;
residenceTreeData.value = rootNodes;

6.4.3 懒加载子节点

async function loadTreeChildren(treeNode, treeDataRefName) {
  // 避免重复加载
  if (treeNode.dataRef.children) return Promise.resolve();

  const res = await defHttp.get({
    url: '/system/dictionary/getDictTreeChildren',
    params: { dictionaryCode: 'XZQH', parentCode: treeNode.value },
  }, { isTransformResponse: false });

  if (res.success && res.result) {
    const children = res.result.map((item) => ({
      key: item.key, title: item.title, value: item.key, isLeaf: item.leaf,
    }));
    // 挂子节点到 a-tree-select 的 dataRef 上
    treeNode.dataRef.children = children;
    // 强制触发 Vue 响应式更新
    if (treeDataRefName === 'householdTreeData') {
      householdTreeData.value = [...householdTreeData.value];
    } else {
      residenceTreeData.value = [...residenceTreeData.value];
    }
  }
  return Promise.resolve();
}

注意defHttp.get 需传入 { isTransformResponse: false },否则框架会自动解包只返回 data.result,导致 res.success 无法判断。

6.4.4 选择变更处理

async function handleAreaChange(val, prefix) {
  if (val && val.value) {
    // 存叶子代码到已有列
    formData[`${prefix}Location`] = val.value;
    // 调 API 获取完整路径,存到新增的 areaName 列
    const res = await defHttp.get({
      url: '/system/dictionary/getItemFullPath',
      params: { dictionaryCode: 'XZQH', code: val.value },
    }, { isTransformResponse: false });
    const fullPath = res.success ? res.result : val.label;
    formData[`${prefix}AreaName`] = fullPath;
    // 更新下拉框显示为完整路径
    if (prefix === 'household') {
      householdValueObj.value = { value: val.value, label: fullPath };
    } else {
      residenceValueObj.value = { value: val.value, label: fullPath };
    }
  } else {
    // 清空
    formData[`${prefix}Location`] = '';
    formData[`${prefix}AreaName`] = '';
    // ...
  }
}

6.4.5 编辑回显

编辑时直接从数据记录中取已存储的路径文本,无需调 API

function edit(record) {
  nextTick(() => {
    // ...赋值 formData
    householdValueObj.value = tmpData.householdLocation
      ? { value: tmpData.householdLocation, label: tmpData.householdAreaName || tmpData.householdLocation }
      : undefined;
    residenceValueObj.value = tmpData.currentResidence
      ? { value: tmpData.currentResidence, label: tmpData.residenceAreaName || tmpData.currentResidence }
      : undefined;
  });
}

6.5 表格列显示

位置: jeecgboot-vue3/src/views/recruitment/personal/PersonalInfo.data.ts

树形字典字段的 dataIndex 直接指向 areaName 列,无需 bodyCell 插槽字典翻译

{
  title: '户口所在地',
  align: 'center',
  dataIndex: 'householdAreaName',  // ← 直接用路径文本列,不翻译
  width: 200,
},
{
  title: '现居住地',
  align: 'center',
  dataIndex: 'residenceAreaName',
  width: 200,
},

6.6 详情弹窗显示

位置: jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoDetailModal.vue

<a-descriptions-item label="户口所在地" :span="1">
  {{ detailData.householdAreaName || detailData.householdLocation || '-' }}
</a-descriptions-item>
<a-descriptions-item label="现居住地" :span="1">
  {{ detailData.residenceAreaName || detailData.currentResidence || '-' }}
</a-descriptions-item>

优先显示路径文本,无数据时回退显示叶子代码。

6.7 Excel 导出

位置: PersonalInfoController.javaexportXls()

树形字典字段已在 PersonalInfo Entity 上有独立的 householdAreaName/residenceAreaName 列,且标注了 @Excel 注解。 导出字段列表中直接包含这两个列名即可,无需走 translateDictFields 字典映射

String exportFields = "...",
    + "householdAreaName,residenceAreaName,"
    + "jobSeekerCategory,contactPhone,jobSearchStatus,dataSource";

EXPORT_DICT_FIELD_MAP 中无需添加树形字典的映射项。

6.8 数据流总结

【新增/编辑】
  用户展开树 → getDictTreeChildren 懒加载子节点
  用户选中叶子节点 → handleAreaChange
    ├─ formData.householdLocation = "440802" (存 code)
    ├─ getItemFullPath API → "广东省湛江市赤坎区"
    ├─ formData.householdAreaName = "广东省湛江市赤坎区" (存路径)
    └─ householdValueObj.label = "广东省湛江市赤坎区" (下拉框显示)
      保存 → 后端 code + 路径文本同时入库

【编辑回显】
  编辑表单 → 直接用 householdAreaName 回填 valueObj
  → 下拉框显示完整路径,零 API 调用

【表格/详情/导出】
  → 直接使用 householdAreaName 字段
  → 无需字典翻译

6.9 与普通字典方案对比

环节 普通字典(如 Gender) 树形字典(如 XZQH)
数据量 小(几条~几十条) 大(K 级~万级)
加载方式 前端预加载全部 懒加载,按需加载子节点
编辑组件 <a-select> + useDict <a-tree-select> + 自定义懒加载
存储方式 只存 code(如 1 存 code(440802)+ 路径文本(广东省湛江市赤坎区
表格显示 getDictText() 翻译 code→label 直接显示路径文本列
详情显示 getDictText() 翻译 直接显示路径文本列
Excel 导出 translateDictFields 内存映射 直接导出路径文本列(@Excel 注解)
后端接口 /getDictBatch 批量查询 /getDictTreeChildren 懒加载 + /getItemFullPath 路径解析

6.10 关键源码文件索引

层级 文件 说明
后端 Controller jeecg-boot/.../dictionary/controller/DictionaryController.java 6 个接口:getDictTreeChildren/loadDictItem/getItemFullPath/searchDictItem/getDict/getDictBatch
后端 Service jeecg-boot/.../dictionary/service/IDictionaryItemService.java 接口声明
后端 Service Impl jeecg-boot/.../dictionary/service/impl/DictionaryItemServiceImpl.java 懒加载、路径回溯、批量检查后代等核心逻辑
后端 Entity jeecg-boot/.../personal/entity/PersonalInfo.java householdAreaName/residenceAreaName 字段
后端 Controller jeecg-boot/.../personal/controller/PersonalInfoController.java exportFields 包含 areaName 列
前端编辑表单 jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue <a-tree-select> + 懒加载 + 路径回显
前端表格列配置 jeecgboot-vue3/src/views/recruitment/personal/PersonalInfo.data.ts dataIndex: 'householdAreaName'
前端详情弹窗 jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoDetailModal.vue 直接显示 areaName 字段
SQL .docs/sql/个人信息与简历信息.sql ALTER TABLE 新增 areaName 列
SQL .docs/sql/数据字典-行政区划(XZQH).sql XZQH 字典数据插入脚本(44K+ 条)
SQL .docs/sql/数据字典-职业分类(ZYFL).sql ZYFL 字典数据插入脚本(2K+ 条)