# 个人基本信息模块开发记录 **日期:** 2026-06-04 ## 模块概述 个人基本信息模块(personal)是招聘管理系统中的核心基础模块,负责管理求职者的个人信息和简历信息。该模块采用 JeecgBoot 的前后端分离架构,前端基于 Vue3 + Ant Design Vue,后端基于 Spring Boot + MyBatis-Plus。 ### 业务关系 - **PersonalInfo(个人信息)**:主表,记录求职者的基本资料(姓名、证件、联系方式等) - **ResumeInfo(简历信息)**:关联表,与个人信息关联,记录简历概要 - **简历子表(8个)**:教育经历、工作经历、培训经历、技能、职称、获奖情况、留学经历、求职意向,与简历形成一对多关系 --- ## 模块文件结构 ### 前端(jeecgboot-vue3/src/views/recruitment/personal/) | 文件 | 用途 | |------------------------------------|--------------------------------| | `PersonalInfoList.vue` | 个人信息列表页面,提供查询、新增、编辑、删除、导入导出等功能 | | `PersonalInfo.api.ts` | 个人信息 API 封装 | | `PersonalInfo.data.ts` | 列表页表格列配置与搜索字段定义 | | `ResumeInfo.api.ts` | 简历信息 API 封装 | | `ResumeInfo.data.ts` | 简历表格列配置与搜索字段定义 | | `components/PersonalInfoForm.vue` | 个人信息新增/编辑/详情表单组件 | | `components/PersonalInfoModal.vue` | 个人信息弹窗组件,包裹 PersonalInfoForm | | `components/ResumeInfoForm.vue` | 简历信息表单组件 | | `components/ResumeInfoModal.vue` | 简历信息弹窗组件,包裹 ResumeInfoForm | | `components/ResumeListModal.vue` | 简历列表弹窗组件,查看个人信息关联的简历 | ### 后端(jeecg-boot-module/jeecg-module-zjrs/.../org/jeecg/modules/zjrs/personal/) | 层次 | 文件 | |--------------|------------------------------------------------------------------------| | Controller | `PersonalInfoController.java`, `ResumeInfoController.java` | | Entity | `PersonalInfo.java`, `ResumeInfo.java`, 及 8 个子表实体 | | Service | `IPersonalInfoService.java`, `IResumeInfoService.java` 及 8 个子表 Service | | Service Impl | 对应 10 个 Service 的实现类 | | Mapper | 11 个 Mapper 接口 | | Mapper XML | 10 个 MyBatis XML 映射文件 | | VO/DTO | `ResumeInfoPage.java`, `ResumeInfoDTO.java` | --- ## 开发记录 ### 功能开发 | 日期 | 内容 | 涉及文件 | 说明 | |------------|--------------|-----------------------|------------------------| | 2026-06-04 | 个人基本信息模块初始构建 | 前端 10 个文件 + 后端 30+ 文件 | 完成个人信息与简历管理的基础 CRUD 功能 | ### Bug 修复 #### 2026-06-04:详情查看时表单未禁用修复(先编辑后详情场景) **问题描述:** 在个人信息列表页,先点击"编辑"后关闭弹窗,再点击"详情"时,表单字段和图片上传组件未处于禁用/只读状态。 **复现条件:** 页面刷新后先点编辑再点详情才会出现;页面刷新后直接点详情则正常禁用。 **原因分析:** Ant Design Vue 的 `` 默认 `destroyOnClose=false`,弹窗关闭时内部组件(PersonalInfoForm)仍然挂载在 DOM 中。 - 先点击"编辑"时,PersonalInfoForm 以 `formDisabled=false` 的状态挂载 - 关闭弹窗后组件保持挂载 - 再点击"详情"时,虽然 `disableSubmit` 被设为 `true`,但由于组件实例未重新创建,props 的响应式更新未能正确触发 JFormContainer 的 `
` 状态变更 **第一次修复(已废弃):** 在 `` 上添加 `destroyOnClose` 属性,每次关闭弹窗时销毁 PersonalInfoForm 组件。 **问题:** `destroyOnClose` 导致组件销毁后 `registerForm.value` 为 null,打开弹窗后 Vue 重新渲染组件时 ref 还未赋值, `handleOk()` 调用 `registerForm.value.submitForm()` 报错 `Cannot read properties of null (reading 'submitForm')`。 **第二次修复(已废弃):** 使用 `v-if="visible"` 手动控制组件创建/销毁。 **问题:** 导致多个控制台错误(日期转换报错、异步组件加载报错等)。 **最终修复方案:** 使用 `:key` 方式强制组件重建。 - 在 PersonalInfoModal 中引入 `formKey` ref 和 `nextFormKey()` 方法 - 在 `` 上绑定 `:key="formKey"` - 每次打开弹窗(add/edit)时先调用 `nextFormKey()` 递增 key,触发 Vue 重建组件实例 - 由于组件始终挂载在 DOM 中(非 `v-if` 控制),ref 在重建完成后自然可用 **涉及文件:** - `jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoModal.vue` — 改用 `:key` 替代 `v-if` 控制组件重建 #### 2026-06-04:打开编辑弹窗多个控制台错误修复 **问题描述:** 打开编辑弹窗时控制台出现 5 类错误: 1. `FormItem can only collect one field item` — 年龄搜索字段两个 `AInputNumber` 在同一 `a-form-item` 中 2. `date.locale is not a function` — 日期字符串未转为 dayjs 对象 3. `Cannot read properties of null (reading 'type')` — 日期错误的级联报错 4. `Cannot read properties of null (reading 'submitForm')` — registerForm.value 为空 5. `Cannot read properties of undefined (reading '__asyncLoader')` — 组件异步加载异常 **原因分析与修复方案:** | 错误 | 原因 | 修复 | |---------------------------------|--------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------| | FormItem 警告 | 年龄范围搜索字段中两个 `AInputNumber` 被同一个 `a-form-item` 收集 | 用 `` 包裹第二个 `AInputNumber`,告知 Form 不收集该字段 | | `date.locale is not a function` | API 返回的日期字符串 `"2000-01-15"` 直接赋给 `a-date-picker` 的 `v-model:value`,但 `a-date-picker` 期望 dayjs 对象 | 在 `PersonalInfoForm.edit()` 方法中将 `birthDate` 和 `graduationDate` 字符串转为 `dayjs()` 对象 | | 级联错误 | 日期转换错误导致 `a-date-picker` 内部操作失败 | 修复日期转换后自动解决 | | `submitForm` 报错 | `v-if="visible"` 导致组件未挂载时 ref 为 null | 改用 `:key` 方式重建组件(同上修复1) | | `__asyncLoader` 报错 | 组件重建时序问题导致异步组件加载异常 | 改用 `:key` 重建后解决 | **涉及文件:** - `jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoForm.vue` — edit 方法增加日期字符串转 dayjs 对象逻辑;引入 dayjs 依赖 - `jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoModal.vue` — 改用 `:key` 替代 `v-if` - `jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue` — 年龄搜索字段用 `` 包裹第二个 `AInputNumber` ### 2026-06-05:详情查看改用独立的 a-descriptions 详情弹窗 **问题描述:** 原详情查看功能复用 `PersonalInfoModal` + `PersonalInfoForm`,打开的是禁用状态的编辑表单,布局与编辑界面相同但字段不可编辑,用户体验不佳。 **解决方案:** 新建独立的 `PersonalInfoDetailModal` 组件,使用 `a-descriptions` 描述列表以只读方式展示数据。 **实现要点:** - 使用 `a-descriptions :column="3" bordered` 呈现三列带边框的字段列表,布局与编辑表单完全一致(10 行字段) - 使用 `a-image` 正方形组件展示头像(150x150px),与描述列表形成左右布局(左侧 span=2,右侧 span=22) - 字典字段(证件类型、性别、婚姻状况、政治面貌、工作经验、户口性质、求职人员类别、求职状态、数据来源、学历)通过 `getDictText` 自动转为中文显示 - 弹窗默认为全屏模式(`:fullscreen="true"`),底部仅保留「关闭」按钮 - 修复 `a-image` 预览按钮铺满父容器的问题:外层包裹 `display: inline-block` 的 div 约束宽度 **涉及文件:** - `jeecgboot-vue3/src/views/recruitment/personal/components/PersonalInfoDetailModal.vue` — 新增详情弹窗组件 - `jeecgboot-vue3/src/views/recruitment/personal/PersonalInfoList.vue` — 导入新组件,修改 `handleDetail` 方法调用新弹窗 ### 2026-06-05:人员编辑表单增加个人标签字段 **需求描述:** 在人员编辑表单(PersonalInfoForm)中增加"个人标签"字段,支持多选,数据来源于个人标签库(PersonalTag)。新增/编辑人员时同步保存标签关联关系,详情弹窗展示标签名称。 **实现方案:** 后端: - 新增 `PersonalInfoDTO`(extends PersonalInfo),含 `List tagIds`,用于新增/编辑时接收前端提交的标签ID列表 - 新增 `PersonalInfoVO`(extends PersonalInfo),含 `String tagIds`(逗号分隔,表单回显用)和 `String tagNames`(逗号分隔,详情展示用),用于 `queryById` 返回 - `PersonalTagController` 新增 `/listForSelect` 接口,返回所有已启用的标签 `{id, tagName}` 列表 - `PersonalInfoServiceImpl` 注入 `PersonalTagRelationMapper` 和 `PersonalTagMapper`,提供 `saveWithTags`/`updateWithTags` 方法,在保存人员基本信息后同步维护 `personal_tag_relation` 关联表 - `PersonalInfoController` 的 `/add` 和 `/edit` 接收 `PersonalInfoDTO`,`/queryById` 返回 `PersonalInfoVO` 前端: - `PersonalInfo.api.ts` 新增 `queryById` 和 `getPersonalTagListForSelect` API - `PersonalInfoForm.vue` 在"数据来源"前添加"个人标签"多选下拉(`a-select mode="multiple"`),编辑时自动将后端返回的逗号分隔字符串转为数组回显 - `PersonalInfoDetailModal.vue` 新增"个人标签"展示行 - `PersonalInfoList.vue` 的 `handleEdit`/`handleDetail` 改为先调用 `queryById` 获取含标签的完整数据再传给弹窗 **Bug修复:** 1. **tagIds 空字符串导致选中空选项:** `queryById` 返回 `tagIds: ""` 时,`edit` 方法中转换条件用 `tmpData.tagIds && ...` 会因空字符串为 falsy 跳过转换,`Object.assign` 把 `""` 赋给 `formData.tagIds`,`a-select mode="multiple"` 收到非数组后误渲染空白选项。修复:改用 `typeof tmpData.tagIds === 'string'` 判断,空字符串转为 `[]`。 2. **tagIds 数组被误转为字符串导致 JSON 解析失败:** `submitForm` 的循环中,`getValueType` 将 `tagIds` 误判为 `string` 类型,执行 `.join(',')` 转为逗号字符串,后端 DTO 接收 `List` 时报 `Cannot deserialize from String value`。修复:在循环条件中排除 `tagIds`(`data !== 'tagIds'`),保持数组原样提交。 **涉及文件:** | 文件 | 操作 | 原因 | |------|------|------| | `jeecg-boot/../personal/vo/PersonalInfoDTO.java` | **新增** | DTO 类,extends PersonalInfo,含 `List tagIds`,用于 add/edit 接收前端标签数据 | | `jeecg-boot/../personal/vo/PersonalInfoVO.java` | **新增** | VO 类,extends PersonalInfo,含 `String tagIds` 和 `String tagNames`,用于 queryById 返回含标签的完整数据 | | `jeecg-boot/../personal/service/IPersonalInfoService.java` | **修改** | 新增 `saveWithTags`、`updateWithTags`、`queryPersonalInfoVOById` 方法声明 | | `jeecg-boot/../personal/service/impl/PersonalInfoServiceImpl.java` | **修改** | 注入 `PersonalTagRelationMapper` 和 `PersonalTagMapper`;实现标签关联保存/更新/查询逻辑;`saveTagRelations` 改为接收 `List` | | `jeecg-boot/../personal/controller/PersonalInfoController.java` | **修改** | add/edit 接收 `PersonalInfoDTO`;queryById 返回 `PersonalInfoVO` | | `jeecg-boot/../tag/controller/PersonalTagController.java` | **修改** | 新增 `/listForSelect` 接口,返回已启用的标签列表供前端下拉选择 | | `jeecgboot-vue3/../personal/PersonalInfo.api.ts` | **修改** | 新增 `queryById` 和 `getPersonalTagListForSelect` API | | `jeecgboot-vue3/../personal/PersonalInfoList.vue` | **修改** | handleEdit/handleDetail 改为调用 queryById 获取含标签的完整数据 | | `jeecgboot-vue3/../personal/components/PersonalInfoForm.vue` | **修改** | 添加个人标签多选下拉;编辑时字符串→数组回显;提交时排除 tagIds 的数组→字符串转换 | | `jeecgboot-vue3/../personal/components/PersonalInfoDetailModal.vue` | **修改** | 新增个人标签展示行 |