260604-个人基本信息模块开发记录.md 14 KB

个人基本信息模块开发记录

日期: 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 的 <a-modal> 默认 destroyOnClose=false,弹窗关闭时内部组件(PersonalInfoForm)仍然挂载在 DOM 中。

  • 先点击"编辑"时,PersonalInfoForm 以 formDisabled=false 的状态挂载
  • 关闭弹窗后组件保持挂载
  • 再点击"详情"时,虽然 disableSubmit 被设为 true,但由于组件实例未重新创建,props 的响应式更新未能正确触发 JFormContainer 的 <fieldset disabled> 状态变更

第一次修复(已废弃):<j-modal> 上添加 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() 方法
  • <PersonalInfoForm> 上绑定 :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 收集 <a-form-item-rest> 包裹第二个 AInputNumber,告知 Form 不收集该字段
date.locale is not a function API 返回的日期字符串 "2000-01-15" 直接赋给 a-date-pickerv-model:value,但 a-date-picker 期望 dayjs 对象 PersonalInfoForm.edit() 方法中将 birthDategraduationDate 字符串转为 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 — 年龄搜索字段用 <a-form-item-rest> 包裹第二个 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<String> tagIds,用于新增/编辑时接收前端提交的标签ID列表
  • 新增 PersonalInfoVO(extends PersonalInfo),含 String tagIds(逗号分隔,表单回显用)和 String tagNames(逗号分隔,详情展示用),用于 queryById 返回
  • PersonalTagController 新增 /listForSelect 接口,返回所有已启用的标签 {id, tagName} 列表
  • PersonalInfoServiceImpl 注入 PersonalTagRelationMapperPersonalTagMapper,提供 saveWithTags/updateWithTags 方法,在保存人员基本信息后同步维护 personal_tag_relation 关联表
  • PersonalInfoController/add/edit 接收 PersonalInfoDTO/queryById 返回 PersonalInfoVO

前端:

  • PersonalInfo.api.ts 新增 queryByIdgetPersonalTagListForSelect API
  • PersonalInfoForm.vue 在"数据来源"前添加"个人标签"多选下拉(a-select mode="multiple"),编辑时自动将后端返回的逗号分隔字符串转为数组回显
  • PersonalInfoDetailModal.vue 新增"个人标签"展示行
  • PersonalInfoList.vuehandleEdit/handleDetail 改为先调用 queryById 获取含标签的完整数据再传给弹窗

Bug修复:

  1. tagIds 空字符串导致选中空选项: queryById 返回 tagIds: "" 时,edit 方法中转换条件用 tmpData.tagIds && ... 会因空字符串为 falsy 跳过转换,Object.assign"" 赋给 formData.tagIdsa-select mode="multiple" 收到非数组后误渲染空白选项。修复:改用 typeof tmpData.tagIds === 'string' 判断,空字符串转为 []

  2. tagIds 数组被误转为字符串导致 JSON 解析失败: submitForm 的循环中,getValueTypetagIds 误判为 string 类型,执行 .join(',') 转为逗号字符串,后端 DTO 接收 List<String> 时报 Cannot deserialize from String value。修复:在循环条件中排除 tagIdsdata !== 'tagIds'),保持数组原样提交。

涉及文件:

文件 操作 原因
jeecg-boot/../personal/vo/PersonalInfoDTO.java 新增 DTO 类,extends PersonalInfo,含 List<String> tagIds,用于 add/edit 接收前端标签数据
jeecg-boot/../personal/vo/PersonalInfoVO.java 新增 VO 类,extends PersonalInfo,含 String tagIdsString tagNames,用于 queryById 返回含标签的完整数据
jeecg-boot/../personal/service/IPersonalInfoService.java 修改 新增 saveWithTagsupdateWithTagsqueryPersonalInfoVOById 方法声明
jeecg-boot/../personal/service/impl/PersonalInfoServiceImpl.java 修改 注入 PersonalTagRelationMapperPersonalTagMapper;实现标签关联保存/更新/查询逻辑;saveTagRelations 改为接收 List<String>
jeecg-boot/../personal/controller/PersonalInfoController.java 修改 add/edit 接收 PersonalInfoDTO;queryById 返回 PersonalInfoVO
jeecg-boot/../tag/controller/PersonalTagController.java 修改 新增 /listForSelect 接口,返回已启用的标签列表供前端下拉选择
jeecgboot-vue3/../personal/PersonalInfo.api.ts 修改 新增 queryByIdgetPersonalTagListForSelect API
jeecgboot-vue3/../personal/PersonalInfoList.vue 修改 handleEdit/handleDetail 改为调用 queryById 获取含标签的完整数据
jeecgboot-vue3/../personal/components/PersonalInfoForm.vue 修改 添加个人标签多选下拉;编辑时字符串→数组回显;提交时排除 tagIds 的数组→字符串转换
jeecgboot-vue3/../personal/components/PersonalInfoDetailModal.vue 修改 新增个人标签展示行