소스 검색

feat: 人员信息的省一体化字典设置

zhangying 3 일 전
부모
커밋
5119474ae2
19개의 변경된 파일96095개의 추가작업 그리고 218개의 파일을 삭제
  1. 334 3
      .docs/260603-前端框架全局开发规范与最佳实践.md
  2. 315 0
      .docs/260604-数据字典类字段显示与导出实现方式.md
  3. 11 0
      .docs/sql/个人信息与简历信息.sql
  4. 8 0
      .docs/sql/字典表.sql
  5. 4358 0
      .docs/sql/数据字典-职业分类(ZYFL).sql
  6. 89420 0
      .docs/sql/数据字典-行政区划(XZQH).sql
  7. 1036 173
      .docs/sql/数据字典数据插入.sql
  8. 43 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/controller/DictionaryController.java
  9. 8 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/entity/Dictionary.java
  10. 8 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/entity/DictionaryItem.java
  11. 57 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/service/IDictionaryItemService.java
  12. 206 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/service/impl/DictionaryItemServiceImpl.java
  13. 5 2
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/controller/PersonalInfoController.java
  14. 14 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/entity/PersonalInfo.java
  15. 15 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/service/impl/PersonalInfoServiceImpl.java
  16. 14 2
      jeecgboot-vue3/src/views/recruitment/personal/PersonalInfo.data.ts
  17. 68 5
      jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue
  18. 6 4
      jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoDetailModal.vue
  19. 169 29
      jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue

+ 334 - 3
.docs/260603-前端框架全局开发规范与最佳实践.md

@@ -2163,9 +2163,340 @@ const [register, { expandAll, collapseAll }] = useTable({ ... });
 
 ---
 
-## 二十二、关键源码索引
+## 二十二、树形下拉选择组件(TreeSelect 表单组件族)
 
-### 22.1 核心 Hooks
+### 22.1 概述
+
+本项目在表单(编辑/查询)中提供了 **5 个树形下拉选择组件**,全部基于 Ant Design Vue 的 `<a-tree-select>` 封装,已在 [componentMap.ts](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/componentMap.ts) 中注册,可通过 schema 的 `component` 字段直接使用。
+
+| 组件名 | 注册名 | 适用场景 | 数据来源 |
+|--------|--------|----------|----------|
+| `TreeSelect` | `TreeSelect` | 通用树选择(需手动传 treeData) | 静态数据或自行加载 |
+| `JTreeSelect` | `JTreeSelect` | **最常用**,数据库表异步树 | 通过 `dict` 指定表名加载 |
+| `ApiTreeSelect` | `ApiTreeSelect` | API 接口驱动的树选择 | 传入 API 函数加载 |
+| `JTreeDict` | `JTreeDict` | 分类字典树选择 | `/sys/category/` 系列 API |
+| `JCategorySelect` | `JCategorySelect` | 分类下拉树选择(旧版) | `/sys/category/` 系列 API |
+
+### 22.2 TreeSelect — Ant Design Vue 原生组件
+
+直接使用 `a-tree-select`,未做二次封装,是最基础的树下拉组件。
+
+**注册位置**:[componentMap.ts#L25](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/componentMap.ts#L25)
+
+```typescript
+componentMap.set('TreeSelect', createAsyncComponent(() => import('ant-design-vue/es/tree-select')));
+```
+
+**Schema 用法**:
+
+```typescript
+{
+  field: 'parentId',
+  label: '上级节点',
+  component: 'TreeSelect',
+  componentProps: {
+    treeData: [                          // 手动传入树形数据
+      {
+        title: '根节点',
+        value: '1',
+        children: [
+          { title: '子节点1', value: '1-1' },
+          { title: '子节点2', value: '1-2' },
+        ],
+      },
+    ],
+    placeholder: '请选择上级节点',
+    treeDefaultExpandAll: true,          // 默认展开所有节点
+    allowClear: true,
+    showSearch: true,                    // 支持搜索
+    treeNodeFilterProp: 'title',         // 按 title 过滤
+  },
+}
+```
+
+**适用场景**:选项固定或前端已知树数据,需要完全控制 `treeData`。
+
+### 22.3 JTreeSelect — 异步数据库树下拉(最常用)
+
+**文件**:[JTreeSelect.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JTreeSelect.vue)
+
+**注册位置**:[componentMap.ts#L80](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/componentMap.ts#L80)
+
+通过配置 `dict` 表名即可自动从数据库加载树形数据,支持异步懒加载子节点、单选/多选、搜索过滤、复选框勾选等。
+
+#### Props 详解
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `dict` | `string` | `'id'` | **核心属性**,格式:`表名,显示字段,存储字段`,如 `sys_depart,depart_name,id` |
+| `pidField` | `string` | `'pid'` | 父节点字段名 |
+| `pidValue` | `string` | `''` | 根节点的父 ID 值(空表示查询父节点为空的根记录) |
+| `hasChildField` | `string` | `''` | 判断是否有子节点的字段名(如 `has_child`) |
+| `converIsLeafVal` | `number` | `1` | 与 `hasChildField` 配合,值为该值时表示叶子节点 |
+| `condition` | `string` | `''` | 额外查询条件,JSON 字符串格式 |
+| `multiple` | `boolean` | `false` | 是否多选 |
+| `treeCheckAble` | `boolean` | `false` | 是否显示复选框(父子联动勾选) |
+| `url` | `string` | `''` | 自定义数据加载 URL(覆盖默认的 `/sys/dict/loadTreeData`) |
+| `params` | `object` | `{}` | 配合 `url` 使用的请求参数 |
+| `hiddenNodeKey` | `string` | `''` | 隐藏指定节点(编辑时防止选自己为父节点) |
+| `loadTriggleChange` | `boolean` | `false` | 单选模式下,初始加载后是否触发 change 事件 |
+| `reload` | `number` | `1` | 改变此值强制重新加载树数据 |
+
+#### 数据加载机制
+
+1. **根节点加载**:根据 `dict` 解析表名、文本字段、存储字段,调用 `/sys/dict/loadTreeData` 接口传入 `pidField`、`pidValue` 等参数获取根节点
+2. **子节点懒加载**:展开节点时,调用同一接口传入当前节点的 key 作为 `pid` 获取子节点
+3. **回显翻译**:根据 `value` 调用 `/sys/dict/loadDictItem/{dict}` 接口翻译为标签文本
+4. **自定义 URL**:配置 `url` 后,所有数据加载走自定义接口,不依赖默认字典接口
+
+#### Schema 使用示例
+
+**基础用法 — 单选**:
+
+```typescript
+{
+  field: 'deptId',
+  label: '所属部门',
+  component: 'JTreeSelect',
+  componentProps: {
+    dict: 'sys_depart,depart_name,id',   // 表名,显示字段,存储字段
+    pidField: 'parent_id',
+    pidValue: '',                         // 空=查根节点
+    placeholder: '请选择部门',
+  },
+}
+```
+
+**多选 + 复选框**:
+
+```typescript
+{
+  field: 'roles',
+  label: '角色',
+  component: 'JTreeSelect',
+  componentProps: {
+    dict: 'sys_role,role_name,id',
+    multiple: true,
+    treeCheckAble: true,                  // 显示复选框,父子联动
+    placeholder: '请选择角色',
+  },
+}
+```
+
+**自定义 URL 加载**:
+
+```typescript
+{
+  field: 'categoryId',
+  label: '分类',
+  component: 'JTreeSelect',
+  componentProps: {
+    url: '/biz/category/treeData',        // 自定义数据接口
+    params: { type: 'product' },          // 额外请求参数
+    placeholder: '请选择分类',
+  },
+}
+```
+
+**编辑时隐藏自身节点**(防止选自己为父节点):
+
+```typescript
+{
+  field: 'parentId',
+  label: '上级节点',
+  component: 'JTreeSelect',
+  componentProps: {
+    dict: 'sys_category,name,id',
+    hiddenNodeKey: recordId,              // 隐藏当前编辑记录的节点
+  },
+}
+```
+
+#### 搜索表单中使用
+
+```typescript
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'deptId',
+    label: '部门',
+    component: 'JTreeSelect',
+    componentProps: {
+      dict: 'sys_depart,depart_name,id',
+      pidField: 'parent_id',
+      placeholder: '请选择部门',
+    },
+    colProps: { span: 6 },
+  },
+];
+```
+
+### 22.4 ApiTreeSelect — API 函数驱动的树下拉
+
+**文件**:[ApiTreeSelect.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/components/ApiTreeSelect.vue)
+
+**注册位置**:[componentMap.ts#L26](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/componentMap.ts#L26)
+
+传入一个返回树数据的 API 函数,组件自动调用获取数据并渲染为下拉树。适合接口返回格式与默认字典接口不一致的场景。
+
+#### Props 详解
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `api` | `Function` | — | **核心属性**,返回树数据的异步函数,签名为 `(arg?: Recordable) => Promise<Recordable>` |
+| `params` | `Object` | — | 传给 `api` 函数的参数,变化时自动重新加载(deep watch) |
+| `immediate` | `boolean` | `true` | 是否在 mounted 时立即加载 |
+| `resultField` | `string` | `''` | 如果 API 返回的是 `{ data: treeData }` 格式,用此字段指定取数据的路径 |
+
+#### Schema 使用示例
+
+**基础用法**:
+
+```typescript
+import { defHttp } from '/@/utils/http/axios';
+
+{
+  field: 'areaId',
+  label: '所属区域',
+  component: 'ApiTreeSelect',
+  componentProps: {
+    api: (params) => defHttp.get({ url: '/biz/area/tree', params }),
+    params: { type: 'district' },
+    placeholder: '请选择区域',
+    treeDefaultExpandAll: true,
+  },
+}
+```
+
+**带 resultField 的用法**:
+
+```typescript
+{
+  field: 'orgId',
+  label: '组织机构',
+  component: 'ApiTreeSelect',
+  componentProps: {
+    api: (params) => defHttp.get({ url: '/sys/org/tree', params }),
+    resultField: 'data.items',            // 返回 { data: { items: [...] } } 时指定路径
+    placeholder: '请选择组织机构',
+  },
+}
+```
+
+**特点**:
+- 支持 `params` 变化时自动重新加载
+- 加载中时 `suffixIcon` 显示旋转 Loading 图标
+- 可透传 `a-tree-select` 的所有原生属性(通过 `useAttrs`)
+
+### 22.5 JTreeDict — 分类字典树下拉
+
+**文件**:[JTreeDict.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JTreeDict.vue)
+
+**注册位置**:[componentMap.ts#L75](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/componentMap.ts#L75)
+
+专门用于**分类字典数据**的树形下拉,数据从 `/sys/category/` 系列接口加载。
+
+#### Props 详解
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `value` | `string` | `''` | v-model 绑定值 |
+| `field` | `string` | `'id'` | 存储字段,`'id'` 存主键或 `'code'` 存编码 |
+| `parentCode` | `string` | `''` | 父节点编码,用于指定加载哪个分类树 |
+| `async` | `boolean` | `false` | 是否启用异步加载子节点(懒加载) |
+
+#### Schema 使用示例
+
+```typescript
+{
+  field: 'categoryId',
+  label: '所属分类',
+  component: 'JTreeDict',
+  componentProps: {
+    parentCode: 'product_type',           // 分类编码
+    field: 'id',                          // 存储 id(默认),也可用 'code' 存储编码
+    async: true,                          // 子节点懒加载
+    placeholder: '请选择分类',
+  },
+}
+```
+
+### 22.6 JCategorySelect — 分类下拉树(旧版)
+
+**文件**:[JCategorySelect.vue](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/jeecg/components/JCategorySelect.vue)
+
+**注册位置**:[componentMap.ts#L68](file:///d:/Code/Project/湛江人社/code/zjrs-jeecgBoot/jeecgboot-vue3/src/components/Form/src/componentMap.ts#L68)
+
+另一个分类树下拉选择组件,与 `JTreeDict` 功能类似但接口调用方式不同。通过 `pcode` 指定分类编码,使用 `loadTreeData` / `loadDictItem` API 加载数据。
+
+#### Props 详解
+
+| 属性 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `value` | `string/array` | — | v-model 绑定值 |
+| `placeholder` | `string` | `'请选择'` | 占位文本 |
+| `disabled` | `boolean` | `false` | 是否禁用 |
+| `condition` | `string` | `''` | 额外查询条件(JSON 字符串) |
+| `multiple` | `boolean/string` | `false` | 是否多选 |
+| `pid` | `string` | `''` | 指定根节点的父 ID |
+| `pcode` | `string` | `''` | 分类编码(常用),默认为 `'0'` |
+| `back` | `string` | `''` | 回传字段名,选中时将 label 回传到 `change` 事件的第二个参数 |
+| `loadTriggleChange` | `boolean` | `false` | 初始加载后是否触发 change |
+
+#### Schema 使用示例
+
+```typescript
+{
+  field: 'category',
+  label: '分类',
+  component: 'JCategorySelect',
+  componentProps: {
+    pcode: 'product_category',           // 分类编码
+    multiple: false,
+    placeholder: '请选择分类',
+  },
+}
+```
+
+### 22.7 组件选型指南
+
+```
+需要树形下拉选择?
+├── 数据来自数据库表(表名已知)
+│   └── 使用 JTreeSelect + dict 属性                 ← 最常用
+├── 数据来自自定义 API(接口格式不标准)
+│   └── 使用 ApiTreeSelect + api 函数
+├── 数据来自分类字典(/sys/category/)
+│   ├── 新版分类字典 → 使用 JTreeDict
+│   └── 旧版分类字典 → 使用 JCategorySelect
+└── 数据来自前端静态变量
+    └── 使用 TreeSelect(原生 ant-design-vue)
+```
+
+### 22.8 通用注意事项
+
+1. **值绑定格式**:所有树选择组件内部使用 `labelInValue` 模式(`{ value, label }` 对象),向外部 `emit` 时会自动转换为纯字符串/数组,表单通过 `v-model` 绑定时无需关心内部格式
+2. **下拉容器**:默认强制 `getPopupContainer: (node) => node?.parentNode`,使下拉菜单挂载到父节点而非 body,避免滚动时位置偏移
+3. **回显翻译**:编辑回填时,组件会根据 value 自动请求接口获取对应的 label 文本展示
+4. **搜索支持**:所有组件基于 `a-tree-select`,天然支持输入搜索过滤节点
+5. **懒加载**:`JTreeSelect`、`JTreeDict`、`JCategorySelect` 均支持展开节点时异步加载子节点,避免一次性加载大量数据
+
+### 22.9 关键源码文件
+
+| 文件 | 职责 |
+|------|------|
+| `components/Form/src/jeecg/components/JTreeSelect.vue` | 核心异步树下拉组件(dict 模式) |
+| `components/Form/src/components/ApiTreeSelect.vue` | API 驱动树下拉组件 |
+| `components/Form/src/jeecg/components/JTreeDict.vue` | 分类字典树下拉 |
+| `components/Form/src/jeecg/components/JCategorySelect.vue` | 分类下拉树(旧版) |
+| `components/Form/src/componentMap.ts` | 组件注册映射表 |
+| `components/Form/src/types/index.ts` | 表单组件类型定义 |
+| `components/jeecg/JVxeTable/src/components/cells/JVxeTreeSelectCell.vue` | JVxeTable 表格内树选择单元格 |
+
+---
+
+## 二十三、关键源码索引
+
+### 23.1 核心 Hooks
 
 | 组件            | 文件                                               |
 |---------------|--------------------------------------------------|
@@ -2210,7 +2541,7 @@ const [register, { expandAll, collapseAll }] = useTable({ ... });
 
 ---
 
-## 二十、涉及的文件清单
+## 二十、涉及的文件清单
 
 | 文件                                         | 操作 | 原因                  |
 |--------------------------------------------|----|---------------------|

+ 315 - 0
.docs/260604-数据字典类字段显示与导出实现方式.md

@@ -385,3 +385,318 @@ static {
 | 替换方式   | `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`
+
+```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`):
+
+```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.java` → `getDictTreeChildren()`
+
+#### 6.3.2 获取完整路径
+
+```
+GET /system/dictionary/getItemFullPath?dictionaryCode=XZQH&code=440802
+→ "广东省湛江市赤坎区"
+```
+
+沿 `parentItemId` 链从叶子节点向上回溯,将各层级名称拼接。
+
+**Service 实现位置**: `DictionaryItemServiceImpl.java` → `getItemFullPath()`
+
+#### 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>`:
+
+```vue
+<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 根节点加载
+
+```typescript
+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 懒加载子节点
+
+```typescript
+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 选择变更处理
+
+```typescript
+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**:
+
+```typescript
+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` 插槽字典翻译**:
+
+```typescript
+{
+  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`
+
+```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.java` → `exportXls()`
+
+树形字典字段已在 `PersonalInfo` Entity 上有独立的 `householdAreaName`/`residenceAreaName` 列,且标注了 `@Excel` 注解。
+导出字段列表中直接包含这两个列名即可,**无需走 `translateDictFields` 字典映射**:
+
+```java
+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+ 条) |

+ 11 - 0
.docs/sql/个人信息与简历信息.sql

@@ -44,6 +44,17 @@ CREATE TABLE PERSONAL_INFO
 COMMENT
 ON TABLE PERSONAL_INFO IS '个人基本信息表';
 
+-- ----------------------------
+-- ALTER: 为户口所在地和现居住地新增完整路径字段
+-- 用于 JTreeSelect 组件选择行政区域时存储完整路径文本
+-- HOUSEHOLD_LOCATION / CURRENT_RESIDENCE 列存叶子代码(如 '440802')
+-- *_AREA_NAME 列存完整路径(如 '广东省/湛江市/赤坎区')
+-- ----------------------------
+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 '现居住地完整路径(如:广东省/湛江市/赤坎区)';
+
 -- ============================================================
 -- 表2:简历信息表(主表)
 -- 说明:存储求职者的简历基本信息,含个人优势、证书及求职意向信息

+ 8 - 0
.docs/sql/字典表.sql

@@ -14,6 +14,7 @@ CREATE TABLE DICTIONARY
     Name           VARCHAR(500),
     OrderNo        INT         NOT NULL,
     RecordStatus   INT         NOT NULL,
+    DictType       INT         DEFAULT 0,
     PRIMARY KEY (DictionaryCode)
 );
 
@@ -31,6 +32,13 @@ CREATE TABLE DICTIONARY_ITEM
     OrderNo          INT         NOT NULL,
     RecordStatus     INT,
     IsEditable       TINYINT,
+    ParentItemID     VARCHAR(50),
     PRIMARY KEY (DictionaryItemID)
 );
 
+-- ----------------------------
+-- 树形字典支持 - 修改已有表结构(如需升级已有数据库)
+-- ----------------------------
+ALTER TABLE DICTIONARY ADD DictType INT DEFAULT 0;
+ALTER TABLE DICTIONARY_ITEM ADD ParentItemID VARCHAR(50);
+

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 4358 - 0
.docs/sql/数据字典-职业分类(ZYFL).sql


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 89420 - 0
.docs/sql/数据字典-行政区划(XZQH).sql


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1036 - 173
.docs/sql/数据字典数据插入.sql


+ 43 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/controller/DictionaryController.java

@@ -31,4 +31,47 @@ public class DictionaryController {
     public Result<Map<String, List<Map<String, Object>>>> getDictBatch(@RequestBody List<String> dictionaryCodes) {
         return Result.OK(dictionaryItemService.getDictItemsBatch(dictionaryCodes));
     }
+
+    @Operation(summary = "根据字典编码获取树形字典项")
+    @GetMapping(value = "/getDictTree")
+    public Result<List<Map<String, Object>>> getDictTree(@RequestParam(name = "dictionaryCode", required = true) String dictionaryCode) {
+        return Result.OK(dictionaryItemService.getDictTree(dictionaryCode));
+    }
+
+    @Operation(summary = "懒加载树形字典的子节点(按需加载,一次只加载当前层级)")
+    @GetMapping(value = "/getDictTreeChildren")
+    public Result<List<Map<String, Object>>> getDictTreeChildren(
+            @RequestParam(name = "dictionaryCode", required = true) String dictionaryCode,
+            @RequestParam(name = "parentCode", required = false) String parentCode) {
+        return Result.OK(dictionaryItemService.getDictTreeChildren(dictionaryCode, parentCode));
+    }
+
+    @Operation(summary = "根据字典编码和code值查询字典项名称(value→label回显翻译)")
+    @GetMapping(value = "/loadDictItem/{dictionaryCode}")
+    public Result<List<String>> loadDictItem(
+            @PathVariable(name = "dictionaryCode", required = true) String dictionaryCode,
+            @RequestParam(name = "key", required = true) String key) {
+        return Result.OK(dictionaryItemService.loadDictItem(dictionaryCode, key));
+    }
+
+    @Operation(summary = "获取树形字典项的完整路径(从根到当前节点的全路径名称)")
+    @GetMapping(value = "/getItemFullPath")
+    public Result<String> getItemFullPath(
+            @RequestParam(name = "dictionaryCode", required = true) String dictionaryCode,
+            @RequestParam(name = "code", required = true) String code) {
+        String path = dictionaryItemService.getItemFullPath(dictionaryCode, code);
+        if (path == null) {
+            return Result.error("未找到对应字典项");
+        }
+        return Result.OK(path);
+    }
+
+    @Operation(summary = "模糊搜索树形字典项(按名称关键字,返回含完整路径的匹配结果)")
+    @GetMapping(value = "/searchDictItem")
+    public Result<List<Map<String, Object>>> searchDictItem(
+            @RequestParam(name = "dictionaryCode", required = true) String dictionaryCode,
+            @RequestParam(name = "keyword", required = true) String keyword,
+            @RequestParam(name = "limit", defaultValue = "20") int limit) {
+        return Result.OK(dictionaryItemService.searchDictItem(dictionaryCode, keyword, limit));
+    }
 }

+ 8 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/entity/Dictionary.java

@@ -54,4 +54,12 @@ public class Dictionary implements Serializable {
     @Schema(description = "记录状态")
     @TableField("RecordStatus")
     private Integer recordStatus;
+
+    /**
+     * 字典类型:0-普通列表,1-树形字典
+     */
+    @Excel(name = "字典类型", width = 15)
+    @Schema(description = "字典类型:0-普通列表,1-树形字典")
+    @TableField("DictType")
+    private Integer dictType;
 }

+ 8 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/entity/DictionaryItem.java

@@ -87,4 +87,12 @@ public class DictionaryItem implements Serializable {
     @Schema(description = "是否可编辑")
     @TableField("IsEditable")
     private Integer isEditable;
+
+    /**
+     * 父级字典项ID,树形字典使用,根节点为 null
+     */
+    @Excel(name = "父级字典项ID", width = 15)
+    @Schema(description = "父级字典项ID,树形字典使用")
+    @TableField("ParentItemID")
+    private String parentItemId;
 }

+ 57 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/service/IDictionaryItemService.java

@@ -17,4 +17,61 @@ public interface IDictionaryItemService extends IService<DictionaryItem> {
      * 批量获取字典项,返回 Map,key 为字典编码,value 为 label/value 键值对列表
      */
     Map<String, List<Map<String, Object>>> getDictItemsBatch(List<String> dictionaryCodes);
+
+    /**
+     * 根据字典编码获取树形字典项,返回带 children 的树形结构
+     */
+    List<Map<String, Object>> getDictTree(String dictionaryCode);
+
+    /**
+     * 懒加载树形字典的子节点(按需加载,一次性只加载当前展开层级的子节点)
+     * <p>
+     * 前端 JTreeSelect 组件展开节点时调用此接口,每次只返回当前父节点下的直接子节点,
+     * 避免一次性加载整棵树的全部数据(如 XZQH 行政区划树 44K+ 节点)。
+     * 每个子节点会提前检查是否还有下级,以便前端正确显示展开箭头。
+     *
+     * @param dictionaryCode 字典编码(如 XZQH)
+     * @param parentCode     父节点 code 值,null 或空串表示查询根节点
+     * @return 子节点列表,格式: [{key, title, leaf}, ...]
+     *         key=节点code值, title=节点名称, leaf=是否叶子节点(无子节点)
+     */
+    List<Map<String, Object>> getDictTreeChildren(String dictionaryCode, String parentCode);
+
+    /**
+     * 根据字典编码和 code 值查询对应的字典项名称列表(value→label 回显翻译)
+     * <p>
+     * 用于编辑表单回显时,将存储的 code 值翻译为展示用的中文名称。
+     * 支持传入多个 code 值(逗号分隔),按传入顺序返回对应的名称列表。
+     *
+     * @param dictionaryCode 字典编码
+     * @param keys           逗号分隔的 code 值(如 "440100,440200")
+     * @return 名称列表,顺序与 keys 中 code 的顺序一致
+     */
+    List<String> loadDictItem(String dictionaryCode, String keys);
+
+    /**
+     * 获取树形字典项的完整路径(从根到当前节点的全路径名称)
+     * <p>
+     * 沿 parentItemId 链从叶子节点向上回溯至根节点,将所有层级的名称用 "/" 拼接。
+     * 用于详情页、表格列和 Excel 导出时显示完整层级信息。
+     *
+     * @param dictionaryCode 字典编码(如 XZQH)
+     * @param code           叶子节点的 code 值(如 "440802")
+     * @return 完整路径(如 "广东省/湛江市/赤坎区"),节点不存在返回 null
+     */
+    String getItemFullPath(String dictionaryCode, String code);
+
+    /**
+     * 模糊搜索树形字典项(按名称关键字搜索)
+     * <p>
+     * 用于前端 tree-select 的搜索功能,解决懒加载模式下未加载节点无法被客户端搜索的问题。
+     * 匹配的项会附带其完整路径,前端可以展示搜索结果的层级位置。
+     *
+     * @param dictionaryCode 字典编码(如 XZQH)
+     * @param keyword        搜索关键字
+     * @param limit          返回结果数量上限
+     * @return 匹配项列表,每项包含: {code, name, fullPath}
+     *         code=节点code值, name=节点名称, fullPath=从根到当前节点的完整路径
+     */
+    List<Map<String, Object>> searchDictItem(String dictionaryCode, String keyword, int limit);
 }

+ 206 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/service/impl/DictionaryItemServiceImpl.java

@@ -2,6 +2,7 @@ package org.jeecg.modules.zjrs.dictionary.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.jeecg.common.util.oConvertUtils;
 import org.jeecg.modules.zjrs.dictionary.entity.DictionaryItem;
 import org.jeecg.modules.zjrs.dictionary.mapper.DictionaryItemMapper;
 import org.jeecg.modules.zjrs.dictionary.service.IDictionaryItemService;
@@ -56,4 +57,209 @@ public class DictionaryItemServiceImpl extends ServiceImpl<DictionaryItemMapper,
         }
         return result;
     }
+
+    @Override
+    public List<Map<String, Object>> getDictTree(String dictionaryCode) {
+        // 查询该字典编码下的所有项,按排序号升序
+        QueryWrapper<DictionaryItem> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("DictionaryCode", dictionaryCode);
+        queryWrapper.orderByAsc("OrderNo");
+        List<DictionaryItem> allItems = list(queryWrapper);
+
+        // 按 parentItemId 分组,便于构建树
+        Map<String, List<DictionaryItem>> childrenMap = allItems.stream()
+                .filter(item -> item.getParentItemId() != null)
+                .collect(Collectors.groupingBy(DictionaryItem::getParentItemId));
+
+        // 遍历根节点(parentItemId 为 null 的项),递归构建子树
+        return allItems.stream()
+                .filter(item -> item.getParentItemId() == null)
+                .map(item -> buildTreeNode(item, childrenMap))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 递归构建树形节点
+     */
+    private Map<String, Object> buildTreeNode(DictionaryItem item, Map<String, List<DictionaryItem>> childrenMap) {
+        Map<String, Object> node = new LinkedHashMap<>();
+        node.put("label", item.getName());
+        node.put("value", item.getValue());
+        // 递归构建子节点
+        List<DictionaryItem> children = childrenMap.get(item.getDictionaryItemId());
+        if (children != null && !children.isEmpty()) {
+            node.put("children", children.stream()
+                    .map(child -> buildTreeNode(child, childrenMap))
+                    .collect(Collectors.toList()));
+        } else {
+            node.put("children", Collections.emptyList());
+        }
+        return node;
+    }
+
+    @Override
+    public List<Map<String, Object>> getDictTreeChildren(String dictionaryCode, String parentCode) {
+        // Step 1: 如果传了 parentCode,先通过 code 找到父节点的 dictionaryItemId
+        //         在根节点展开时 parentCode 为空,直接查询 ParentItemID IS NULL 的项
+        String parentItemId = null;
+        if (oConvertUtils.isNotEmpty(parentCode)) {
+            QueryWrapper<DictionaryItem> parentQuery = new QueryWrapper<>();
+            parentQuery.eq("DictionaryCode", dictionaryCode);
+            parentQuery.eq("Code", parentCode);
+            parentQuery.last("LIMIT 1");
+            DictionaryItem parent = getOne(parentQuery);
+            if (parent == null) {
+                return Collections.emptyList();
+            }
+            parentItemId = parent.getDictionaryItemId();
+        }
+
+        // Step 2: 查询直接子节点
+        QueryWrapper<DictionaryItem> childQuery = new QueryWrapper<>();
+        childQuery.eq("DictionaryCode", dictionaryCode);
+        if (parentItemId != null) {
+            childQuery.eq("ParentItemID", parentItemId);
+        } else {
+            childQuery.isNull("ParentItemID");
+        }
+        childQuery.orderByAsc("OrderNo");
+        List<DictionaryItem> children = list(childQuery);
+
+        if (children.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // Step 3: 批量检查每个子节点是否还有下级的后代节点
+        //         一次性查出所有子节点的字典项ID,然后查这些ID是否被其他记录的ParentItemID引用
+        //         用 count 查询比逐个 count 更高效
+        Set<String> childIds = children.stream()
+                .map(DictionaryItem::getDictionaryItemId)
+                .collect(Collectors.toSet());
+
+        Set<String> idsWithChildren = Collections.emptySet();
+        if (!childIds.isEmpty()) {
+            QueryWrapper<DictionaryItem> hasChildQuery = new QueryWrapper<>();
+            hasChildQuery.in("ParentItemID", childIds);
+            hasChildQuery.select("DISTINCT ParentItemID");
+            List<DictionaryItem> itemsWithChildren = list(hasChildQuery);
+            idsWithChildren = itemsWithChildren.stream()
+                    .map(DictionaryItem::getParentItemId)
+                    .collect(Collectors.toSet());
+        }
+
+        // Step 4: 组装响应结果
+        Set<String> finalIdsWithChildren = idsWithChildren;
+        return children.stream().map(child -> {
+            Map<String, Object> node = new LinkedHashMap<>();
+            // key 和 title 分别对应该节点的 code 和 name,与 JTreeSelect 的格式兼容
+            node.put("key", child.getCode());
+            node.put("title", child.getName());
+            // 检查此子节点是否有后代,leaf=true 表示无子节点(不可再展开)
+            node.put("leaf", !finalIdsWithChildren.contains(child.getDictionaryItemId()));
+            return node;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<String> loadDictItem(String dictionaryCode, String keys) {
+        if (oConvertUtils.isEmpty(keys)) {
+            return Collections.emptyList();
+        }
+
+        // 按逗号分割 key 值,保留原始顺序用于结果对齐
+        String[] keyArray = keys.split(",");
+
+        // 批量查询匹配的记录
+        QueryWrapper<DictionaryItem> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("DictionaryCode", dictionaryCode);
+        queryWrapper.in("Code", Arrays.asList(keyArray));
+        queryWrapper.orderByAsc("OrderNo");
+        List<DictionaryItem> items = list(queryWrapper);
+
+        // 构建 code → name 的快速查找映射
+        Map<String, String> codeNameMap = items.stream()
+                .collect(Collectors.toMap(DictionaryItem::getCode, DictionaryItem::getName));
+
+        // 按 keys 的原始顺序返回名称列表,匹配不到的保留原值返回
+        return Arrays.stream(keyArray)
+                .map(key -> codeNameMap.getOrDefault(key.trim(), key.trim()))
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public String getItemFullPath(String dictionaryCode, String code) {
+        if (oConvertUtils.isEmpty(dictionaryCode) || oConvertUtils.isEmpty(code)) {
+            return null;
+        }
+
+        // Step 1: 找到当前节点
+        QueryWrapper<DictionaryItem> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("DictionaryCode", dictionaryCode);
+        queryWrapper.eq("Code", code);
+        queryWrapper.last("LIMIT 1");
+        DictionaryItem current = getOne(queryWrapper);
+        if (current == null) {
+            return null;
+        }
+
+        // Step 2: 沿 parentItemId 链向上回溯,收集名称
+        LinkedList<String> nameChain = new LinkedList<>();
+        nameChain.addFirst(current.getName());
+
+        String nextParentId = current.getParentItemId();
+        while (oConvertUtils.isNotEmpty(nextParentId)) {
+            DictionaryItem parent = getById(nextParentId);
+            if (parent == null) {
+                break;
+            }
+            nameChain.addFirst(parent.getName());
+            nextParentId = parent.getParentItemId();
+        }
+
+        // Step 3: 无分隔符拼接(如"广东省湛江市赤坎区")
+        return String.join("", nameChain);
+    }
+
+    @Override
+    public List<Map<String, Object>> searchDictItem(String dictionaryCode, String keyword, int limit) {
+        if (oConvertUtils.isEmpty(dictionaryCode) || oConvertUtils.isEmpty(keyword)) {
+            return Collections.emptyList();
+        }
+
+        // 按名称模糊搜索,返回前 limit 条
+        QueryWrapper<DictionaryItem> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("DictionaryCode", dictionaryCode);
+        queryWrapper.like("Name", keyword.trim());
+        queryWrapper.orderByAsc("OrderNo");
+        queryWrapper.last("LIMIT " + limit);
+        List<DictionaryItem> items = list(queryWrapper);
+
+        // 对每个匹配项,附带其完整路径和祖先节点 code 链
+        return items.stream().map(item -> {
+            Map<String, Object> result = new LinkedHashMap<>();
+            result.put("code", item.getCode());
+            result.put("name", item.getName());
+            result.put("fullPath", getItemFullPath(dictionaryCode, item.getCode()));
+            result.put("ancestorCodes", getAncestorCodes(item));
+            return result;
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 从叶子节点向上回溯,收集从根到当前节点的所有 code(不含自身)
+     * 例如:叶子 code=440802 → ["44", "4408"],前端用于加载祖先节点到 treeData
+     */
+    private List<String> getAncestorCodes(DictionaryItem item) {
+        LinkedList<String> codes = new LinkedList<>();
+        String nextParentId = item.getParentItemId();
+        while (oConvertUtils.isNotEmpty(nextParentId)) {
+            DictionaryItem parent = getById(nextParentId);
+            if (parent == null) {
+                break;
+            }
+            codes.addFirst(parent.getCode());
+            nextParentId = parent.getParentItemId();
+        }
+        return codes;
+    }
 }

+ 5 - 2
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/controller/PersonalInfoController.java

@@ -56,6 +56,9 @@ public class PersonalInfoController extends JeecgController<PersonalInfo, IPerso
 
     static {
         EXPORT_DICT_FIELD_MAP.put("gender", "Gender");
+        EXPORT_DICT_FIELD_MAP.put("nation", "Ethnicity");
+        EXPORT_DICT_FIELD_MAP.put("nationality", "Nationality");
+        EXPORT_DICT_FIELD_MAP.put("education", "Education");
         EXPORT_DICT_FIELD_MAP.put("jobSeekerCategory", "JobSeekerCategory");
         EXPORT_DICT_FIELD_MAP.put("jobSearchStatus", "JobSeekerStatus");
         EXPORT_DICT_FIELD_MAP.put("dataSource", "DataSource");
@@ -254,8 +257,8 @@ public class PersonalInfoController extends JeecgController<PersonalInfo, IPerso
         mv.addObject(NormalExcelConstants.PARAMS, exportParams);
         mv.addObject(NormalExcelConstants.DATA_LIST, exportList);
 
-        // 指定导出字段:姓名、性别、出生日期(年龄)、学历、户口所在地、现居住地、求职人员类别、联系电话、求职状态、数据来源
-        String exportFields = "fullName,gender,birthDate,education,householdLocation,currentResidence,jobSeekerCategory,contactPhone,jobSearchStatus,dataSource";
+        // 指定导出字段:姓名、性别、民族、国籍、出生日期(年龄)、学历、户口所在地、现居住地、户口所在地完整路径、现居住地完整路径、求职人员类别、联系电话、求职状态、数据来源
+        String exportFields = "fullName,gender,nation,nationality,birthDate,education,householdLocation,currentResidence,householdAreaName,residenceAreaName,jobSeekerCategory,contactPhone,jobSearchStatus,dataSource";
         mv.addObject(NormalExcelConstants.EXPORT_FIELDS, exportFields);
         return mv;
     }

+ 14 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/entity/PersonalInfo.java

@@ -145,6 +145,20 @@ public class PersonalInfo implements Serializable {
     @Excel(name = "现居住地址", width = 15)
     @Schema(description = "现居住地址")
     private java.lang.String currentAddress;
+    /**
+     * 户口所在地完整路径(如:广东省/湛江市/赤坎区)
+     * 叶子代码仍使用 householdLocation 字段存储
+     */
+    @Excel(name = "户口所在地完整路径", width = 25)
+    @Schema(description = "户口所在地完整路径(如:广东省/湛江市/赤坎区)")
+    private java.lang.String householdAreaName;
+    /**
+     * 现居住地完整路径(如:广东省/湛江市/赤坎区)
+     * 叶子代码仍使用 currentResidence 字段存储
+     */
+    @Excel(name = "现居住地完整路径", width = 25)
+    @Schema(description = "现居住地完整路径(如:广东省/湛江市/赤坎区)")
+    private java.lang.String residenceAreaName;
     /**
      * 求职人员类别
      */

+ 15 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personal/service/impl/PersonalInfoServiceImpl.java

@@ -72,6 +72,21 @@ public class PersonalInfoServiceImpl extends ServiceImpl<PersonalInfoMapper, Per
                             info.setGender(mapping.getOrDefault(info.getGender(), info.getGender()));
                         }
                         break;
+                    case "nation":
+                        if (info.getNation() != null) {
+                            info.setNation(mapping.getOrDefault(info.getNation(), info.getNation()));
+                        }
+                        break;
+                    case "nationality":
+                        if (info.getNationality() != null) {
+                            info.setNationality(mapping.getOrDefault(info.getNationality(), info.getNationality()));
+                        }
+                        break;
+                    case "education":
+                        if (info.getEducation() != null) {
+                            info.setEducation(mapping.getOrDefault(info.getEducation(), info.getEducation()));
+                        }
+                        break;
                     case "jobSeekerCategory":
                         if (info.getJobSeekerCategory() != null) {
                             info.setJobSeekerCategory(mapping.getOrDefault(info.getJobSeekerCategory(), info.getJobSeekerCategory()));

+ 14 - 2
jeecgboot-vue3/src/views/recruitment/personal/PersonalInfo.data.ts

@@ -44,16 +44,28 @@ export const columns: BasicColumn[] = [
     dataIndex: 'education',
     width: 120,
   },
+  {
+    title: '民族',
+    align: 'center',
+    dataIndex: 'nation',
+    width: 80,
+  },
+  {
+    title: '国籍',
+    align: 'center',
+    dataIndex: 'nationality',
+    width: 100,
+  },
   {
     title: '户口所在地',
     align: 'center',
-    dataIndex: 'householdLocation',
+    dataIndex: 'householdAreaName',
     width: 200,
   },
   {
     title: '现居住地',
     align: 'center',
-    dataIndex: 'currentResidence',
+    dataIndex: 'residenceAreaName',
     width: 200,
   },
   {

+ 68 - 5
jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue

@@ -16,17 +16,33 @@
           </a-col>
           <a-col :lg="8">
             <a-form-item label="学历" name="education">
-              <j-dict-select-tag v-model:value="queryParam.education" dictCode="education" placeholder="请选择学历" />
+              <a-select v-model:value="queryParam.education" :options="educationOptions" allow-clear placeholder="请选择学历" />
             </a-form-item>
           </a-col>
           <a-col :lg="8">
             <a-form-item label="户口所在地" name="householdLocation">
-              <a-input v-model:value="queryParam.householdLocation" placeholder="请输入户口所在地"></a-input>
+              <a-tree-select
+                v-model:value="queryParam.householdLocation"
+                :tree-data="searchTreeData"
+                :load-data="(node) => loadSearchTreeChildren(node)"
+                placeholder="请选择户口所在地"
+                allow-clear
+                show-search
+                tree-node-filter-prop="title"
+              />
             </a-form-item>
           </a-col>
           <a-col :lg="8">
             <a-form-item label="现居住地点" name="currentResidence">
-              <a-input v-model:value="queryParam.currentResidence" placeholder="请输入现居住地点"></a-input>
+              <a-tree-select
+                v-model:value="queryParam.currentResidence"
+                :tree-data="searchTreeData"
+                :load-data="(node) => loadSearchTreeChildren(node)"
+                placeholder="请选择现居住地点"
+                allow-clear
+                show-search
+                tree-node-filter-prop="title"
+              />
             </a-form-item>
           </a-col>
           <a-col :lg="8">
@@ -111,7 +127,13 @@
           {{ getDictText('DataSource', text) }}
         </template>
         <template v-else-if="column.dataIndex === 'education'">
-          {{ getDictText('education', text) }}
+          {{ getDictText('Education', text) }}
+        </template>
+        <template v-else-if="column.dataIndex === 'nation'">
+          {{ getDictText('Ethnicity', text) }}
+        </template>
+        <template v-else-if="column.dataIndex === 'nationality'">
+          {{ getDictText('Nationality', text) }}
         </template>
         <template v-else-if="column.dataIndex === 'tagNames'">
           {{ text || '-' }}
@@ -136,8 +158,8 @@
   import PersonalInfoModal from './components/PersonalInfoModal.vue';
   import PersonalInfoDetailModal from './components/PersonalInfoDetailModal.vue';
   import ResumeListModal from './components/ResumeListModal.vue';
-  import JDictSelectTag from '@/components/Form/src/jeecg/components/JDictSelectTag.vue';
   import { useDict } from '/@/hooks/dictionary/useDict';
+  import { defHttp } from '/@/utils/http/axios';
 
   const formRef = ref();
   const queryParam = reactive<any>({});
@@ -157,6 +179,9 @@
     'JobSeekerCategory',
     'JobSeekerStatus',
     'DataSource',
+    'Ethnicity',
+    'Nationality',
+    'Education',
   ]);
 
   // 将预加载的字典转为 a-select 的 options
@@ -164,6 +189,44 @@
   const jobSeekerCategoryOptions = computed(() => getDictOptions('JobSeekerCategory'));
   const jobSearchStatusOptions = computed(() => getDictOptions('JobSeekerStatus'));
   const dataSourceOptions = computed(() => getDictOptions('DataSource'));
+  const ethnicityOptions = computed(() => getDictOptions('Ethnicity'));
+  const nationalityOptions = computed(() => getDictOptions('Nationality'));
+  const educationOptions = computed(() => getDictOptions('Education'));
+
+  // ---------- 树形下拉:行政区划搜索 ----------
+  const searchTreeData = ref<any[]>([]);
+
+  /**
+   * 加载行政区划树根节点(省级)
+   */
+  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 [];
+  }
+
+  /**
+   * 懒加载树子节点
+   */
+  async function loadSearchTreeChildren(treeNode) {
+    if (treeNode.dataRef.children) return;
+    const res = await defHttp.get({
+      url: '/system/dictionary/getDictTreeChildren',
+      params: { dictionaryCode: 'XZQH', parentCode: treeNode.value },
+    }, { isTransformResponse: false });
+    if (res.success && res.result) {
+      treeNode.dataRef.children = res.result.map((item) => ({ key: item.key, title: item.title, value: item.key, isLeaf: item.leaf }));
+      searchTreeData.value = [...searchTreeData.value];
+    }
+  }
+
+  // 加载根节点
+  loadAreaRootNodes().then((nodes) => { searchTreeData.value = nodes; });
 
   //注册table数据
   const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({

+ 6 - 4
jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoDetailModal.vue

@@ -35,12 +35,12 @@
               {{ detailData.birthDate }}
             </a-descriptions-item>
             <a-descriptions-item label="民族" :span="1">
-              {{ detailData.nation }}
+              {{ getDictText('Ethnicity', detailData.nation) }}
             </a-descriptions-item>
 
             <!-- 第三行 -->
             <a-descriptions-item label="国籍" :span="1">
-              {{ detailData.nationality }}
+              {{ getDictText('Nationality', detailData.nationality) }}
             </a-descriptions-item>
             <a-descriptions-item label="婚姻状况" :span="1">
               {{ getDictText('MaritalStatus', detailData.maritalStatus) }}
@@ -73,10 +73,10 @@
 
             <!-- 第六行 -->
             <a-descriptions-item label="户口所在地" :span="1">
-              {{ detailData.householdLocation }}
+              {{ detailData.householdAreaName || detailData.householdLocation || '-' }}
             </a-descriptions-item>
             <a-descriptions-item label="现居住地" :span="1">
-              {{ detailData.currentResidence }}
+              {{ detailData.residenceAreaName || detailData.currentResidence || '-' }}
             </a-descriptions-item>
             <a-descriptions-item label="现居住地址" :span="1">
               {{ detailData.currentAddress }}
@@ -155,6 +155,8 @@
     'JobSeekerStatus',
     'DataSource',
     'Education',
+    'Ethnicity',
+    'Nationality',
   ]);
 
   /**

+ 169 - 29
jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue

@@ -40,13 +40,13 @@
                 </a-col>
                 <a-col :span="8">
                   <a-form-item id="PersonalInfoForm-nation" label="民族" name="nation" v-bind="validateInfos.nation">
-                    <a-input v-model:value="formData.nation" allow-clear placeholder="请输入民族"></a-input>
+                    <a-select v-model:value="formData.nation" :options="ethnicityOptions" allow-clear placeholder="请选择民族" />
                   </a-form-item>
                 </a-col>
                 <!-- 第三行 -->
                 <a-col :span="8">
                   <a-form-item id="PersonalInfoForm-nationality" label="国籍" name="nationality" v-bind="validateInfos.nationality">
-                    <a-input v-model:value="formData.nationality" allow-clear placeholder="请输入国籍"></a-input>
+                    <a-select v-model:value="formData.nationality" :options="nationalityOptions" allow-clear placeholder="请选择国籍" />
                   </a-form-item>
                 </a-col>
                 <a-col :span="8">
@@ -56,7 +56,7 @@
                 </a-col>
                 <a-col :span="8">
                   <a-form-item id="PersonalInfoForm-education" label="学历" name="education" v-bind="validateInfos.education">
-                    <a-input v-model:value="formData.education" allow-clear placeholder="请输入学历"></a-input>
+                    <a-select v-model:value="formData.education" :options="educationOptions" allow-clear placeholder="请选择学历" />
                   </a-form-item>
                 </a-col>
                 <!-- 第四行 -->
@@ -91,7 +91,6 @@
                     <a-select v-model:value="formData.householdType" :options="householdTypeOptions" allow-clear placeholder="请选择户口性质" />
                   </a-form-item>
                 </a-col>
-                <!-- 第六行 -->
                 <a-col :span="8">
                   <a-form-item
                     id="PersonalInfoForm-householdLocation"
@@ -99,7 +98,16 @@
                     name="householdLocation"
                     v-bind="validateInfos.householdLocation"
                   >
-                    <a-input v-model:value="formData.householdLocation" allow-clear placeholder="请输入户口所在地"></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')"
+                    />
                   </a-form-item>
                 </a-col>
                 <a-col :span="8">
@@ -109,7 +117,16 @@
                     name="currentResidence"
                     v-bind="validateInfos.currentResidence"
                   >
-                    <a-input v-model:value="formData.currentResidence" allow-clear placeholder="请输入现居住地"></a-input>
+                    <a-tree-select
+                      v-model:value="residenceValueObj"
+                      v-model:tree-expanded-keys="residenceExpandedKeys"
+                      :tree-data="residenceTreeData"
+                      :load-data="(node) => loadTreeChildren(node, 'residenceTreeData')"
+                      placeholder="请选择现居住地"
+                      allow-clear
+                      label-in-value
+                      @change="(val) => handleAreaChange(val, 'residence')"
+                    />
                   </a-form-item>
                 </a-col>
                 <a-col :span="8">
@@ -161,7 +178,7 @@
                     name="isOverseasTalent"
                     v-bind="validateInfos.isOverseasTalent"
                   >
-                    <a-input v-model:value="formData.isOverseasTalent" allow-clear placeholder="请输入是否留学人才"></a-input>
+                    <a-select v-model:value="formData.isOverseasTalent" :options="yesNoOptions" allow-clear placeholder="请选择是否留学人才" />
                   </a-form-item>
                 </a-col>
                 <!-- 第九行 -->
@@ -182,7 +199,7 @@
                     name="acceptRecommend"
                     v-bind="validateInfos.acceptRecommend"
                   >
-                    <a-input v-model:value="formData.acceptRecommend" allow-clear placeholder="请输入是否接受公共就业服务机构推荐职位"></a-input>
+                    <a-select v-model:value="formData.acceptRecommend" :options="yesNoOptions" allow-clear placeholder="请选择是否接受推荐职位" />
                   </a-form-item>
                 </a-col>
                 <!-- 第十行 -->
@@ -214,6 +231,7 @@
   import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue';
   import JImageUpload from '@/components/Form/src/jeecg/components/JImageUpload.vue';
   import { useDict } from '/@/hooks/dictionary/useDict';
+  import { defHttp } from '/@/utils/http/axios';
   import dayjs from 'dayjs';
 
   const props = defineProps({
@@ -226,33 +244,35 @@
   const emit = defineEmits(['register', 'ok']);
   const formData = reactive<Record<string, any>>({
     id: '',
-    idType: '',
+    idType: undefined,
     idNumber: '',
     fullName: '',
-    gender: '',
+    gender: undefined,
     birthDate: '',
-    nation: '',
-    nationality: '',
-    maritalStatus: '',
-    education: '',
+    nation: undefined,
+    nationality: undefined,
+    maritalStatus: undefined,
+    education: undefined,
     graduationDate: '',
     graduateSchool: '',
     major: '',
-    politicalStatus: '',
-    workExperience: '',
-    householdType: '',
+    politicalStatus: undefined,
+    workExperience: undefined,
+    householdType: undefined,
     householdLocation: '',
+    householdAreaName: '',
     currentResidence: '',
+    residenceAreaName: '',
     currentAddress: '',
-    jobSeekerCategory: '',
+    jobSeekerCategory: undefined,
     contactPhone: '',
     email: '',
     qqNumber: '',
     wechatId: '',
-    isOverseasTalent: '',
+    isOverseasTalent: undefined,
     skillLevel: '',
-    jobSearchStatus: '',
-    acceptRecommend: '',
+    jobSearchStatus: undefined,
+    acceptRecommend: undefined,
     avatarPath: '',
     tagIds: [],
     dataSource: '2',
@@ -273,6 +293,9 @@
     'JobSeekerCategory',
     'JobSeekerStatus',
     'DataSource',
+    'Ethnicity',
+    'Nationality',
+    'Education',
   ]);
 
   // 将预加载的字典转为 a-select 的 options
@@ -285,11 +308,117 @@
   const jobSeekerCategoryOptions = computed(() => getDictOptions('JobSeekerCategory'));
   const jobSearchStatusOptions = computed(() => getDictOptions('JobSeekerStatus'));
   const dataSourceOptions = computed(() => getDictOptions('DataSource'));
+  const ethnicityOptions = computed(() => getDictOptions('Ethnicity'));
+  const nationalityOptions = computed(() => getDictOptions('Nationality'));
+  const educationOptions = computed(() => getDictOptions('Education'));
+  const yesNoOptions = ref([
+    { label: '是', value: '1' },
+    { label: '否', value: '0' },
+  ]);
+
+  // ---------- 树形下拉:行政区划懒加载+服务端搜索展开 ----------
+  // 户口所在地树数据
+  const householdTreeData = ref<any[]>([]);
+  // 户口所在地 labelInValue 对象(用于编辑回显)
+  const householdValueObj = ref<any>(undefined);
+  // 户口所在地展开节点 code 列表(搜索时自动展开匹配项所在分支)
+  const householdExpandedKeys = ref<string[]>([]);
+
+  // 现居住地树数据
+  const residenceTreeData = ref<any[]>([]);
+  // 现居住地 labelInValue 对象(用于编辑回显)
+  const residenceValueObj = ref<any>(undefined);
+  // 现居住地展开节点 code 列表
+  const residenceExpandedKeys = ref<string[]>([]);
+
+  /**
+   * 加载行政区划树根节点(省级)
+   */
+  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 [];
+  }
+
+  /**
+   * 懒加载行政区划树的子节点(按需加载,一次只加载展开节点的下一级)
+   * @param treeNode a-tree-select 展开的节点对象
+   * @param treeDataRefName 对应 treeData 的 ref 名称(householdTreeData / residenceTreeData)
+   */
+  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,
+      }));
+      // 将子节点挂到 dataRef 上
+      treeNode.dataRef.children = children;
+      // 触发响应式更新
+      if (treeDataRefName === 'householdTreeData') {
+        householdTreeData.value = [...householdTreeData.value];
+      } else {
+        residenceTreeData.value = [...residenceTreeData.value];
+      }
+    }
+    return Promise.resolve();
+  }
 
+  /**
+   * 行政区划选择变更处理
+   * - 同步更新 formData 中的 code 字段(householdLocation / currentResidence)
+   * - 调后端 API 获取完整路径,填入 areaName 字段(householdAreaName / residenceAreaName)
+   * - 更新 valueObj.label 为完整路径,使下拉框直接显示完整路径文本
+   */
+  async function handleAreaChange(val, prefix) {
+    if (val && val.value) {
+      formData[`${prefix}Location`] = val.value;
+      // 获取完整路径,同时用于 areaName 字段存储和下拉框 label 显示
+      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`] = '';
+      if (prefix === 'household') {
+        householdValueObj.value = undefined;
+      } else {
+        residenceValueObj.value = undefined;
+      }
+    }
+  }
   // 个人标签选项列表(从后端加载)
   const personalTagOptions = ref<any[]>([]);
 
-  // 组件挂载时加载个人标签列表
+  // 组件挂载时加载个人标签列表和行政区划树
   onMounted(async () => {
     try {
       const res = await getPersonalTagListForSelect();
@@ -300,6 +429,10 @@
     } catch {
       personalTagOptions.value = [];
     }
+    // 加载行政区划树根节点(省级),两个下拉共用相同根节点数据
+    const rootNodes = await loadAreaRootNodes();
+    householdTreeData.value = rootNodes;
+    residenceTreeData.value = rootNodes;
   });
 
   //表单验证
@@ -309,19 +442,19 @@
     fullName: [{ required: true, message: '请输入姓名' }],
     gender: [{ required: true, message: '请选择性别' }],
     birthDate: [{ required: true, message: '请选择出生日期' }],
-    nation: [{ required: true, message: '请输入民族' }],
-    nationality: [{ required: true, message: '请输入国籍' }],
-    education: [{ required: true, message: '请输入学历' }],
+    nation: [{ required: true, message: '请选择民族' }],
+    nationality: [{ required: true, message: '请选择国籍' }],
+    education: [{ required: true, message: '请选择学历' }],
     graduationDate: [{ required: true, message: '请选择毕业日期' }],
     graduateSchool: [{ required: true, message: '请输入毕业院校' }],
     workExperience: [{ required: true, message: '请选择工作经验' }],
-    householdLocation: [{ required: true, message: '请输入户口所在地' }],
-    currentResidence: [{ required: true, message: '请输入现居住地' }],
+    householdLocation: [{ required: true, message: '请选择户口所在地' }],
+    currentResidence: [{ required: true, message: '请选择现居住地' }],
     jobSeekerCategory: [{ required: true, message: '请选择求职人员类别' }],
     contactPhone: [{ required: true, message: '请输入联系电话' }],
-    isOverseasTalent: [{ required: true, message: '请输入是否留学人才' }],
+    isOverseasTalent: [{ required: true, message: '请选择是否留学人才' }],
     jobSearchStatus: [{ required: true, message: '请选择求职状态' }],
-    acceptRecommend: [{ required: true, message: '请输入是否接受公共就业服务机构推荐职位' }],
+    acceptRecommend: [{ required: true, message: '请选择是否接受推荐职位' }],
   });
   const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false });
   //日期个性化选择
@@ -374,6 +507,13 @@
       }
       //赋值
       Object.assign(formData, tmpData);
+      // 回填行政区划树形下拉的 label 显示(直接用已存储的完整路径文本,无需调 API)
+      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;
     });
   }