Quellcode durchsuchen

1.完善“信息智能匹配推送”-“重点关注人员信息”

kk vor 5 Tagen
Ursprung
Commit
cf0ce3bed4
17 geänderte Dateien mit 1063 neuen und 247 gelöschten Zeilen
  1. 427 82
      .docs/重点关注人员管理-P0任务实现记录.md
  2. 58 2
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/controller/FocusPersonnelController.java
  3. 1 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/entity/FocusPersonnel.java
  4. 4 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/entity/FocusPersonnelDetailVo.java
  5. 5 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/entity/FocusPersonnelPageVo.java
  6. 34 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/mapper/FocusPersonnelMapper.java
  7. 52 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/mapper/xml/FocusPersonnelMapper.xml
  8. 19 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/service/IFocusPersonnelService.java
  9. 32 1
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/service/impl/FocusPersonnelServiceImpl.java
  10. 25 0
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_3__alter_view_focus_personnel_list.sql
  11. 39 0
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_4__fix_minor_tag_dict.sql
  12. 81 0
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_5__fix_tag_dict_codes.sql
  13. 26 0
      jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnel.api.ts
  14. 11 5
      jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnel.data.ts
  15. 136 113
      jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnelList.vue
  16. 110 41
      jeecgboot-vue3/src/views/focuspersonnel/components/FocusPersonnelDetail.vue
  17. 3 3
      jeecgboot-vue3/src/views/focuspersonnel/components/FocusPersonnelModal.vue

+ 427 - 82
.docs/重点关注人员管理-P0任务实现记录.md

@@ -16,125 +16,470 @@
 
 | 序号 | 文件路径 | 说明 |
 |:----:|----------|------|
-| 1 | `jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_1__init_focus_personnel_dict.sql` | 字典数据初始化SQL脚本 |
-| 2 | `jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_2__menu_insert_FocusPersonnel_buttons.sql` | 补充5个按钮权限SQL |
-| 3 | `jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/entity/FocusPersonnelDetailVo.java` | 详情VO(含个人信息30个字段) |
-| 4 | `jeecgboot-vue3/src/views/focuspersonnel/components/FocusPersonnelDetail.vue` | 前端详情展示组件(分5个分组展示30个字段) |
+| 1 | `jeecg-boot/.../flyway/sql/mysql/V20260604_1__init_focus_personnel_dict.sql` | 字典数据初始化SQL脚本 |
+| 2 | `jeecg-boot/.../flyway/sql/mysql/V20260604_2__menu_insert_FocusPersonnel_buttons.sql` | 补充5个按钮权限SQL |
+| 3 | `jeecg-boot/.../flyway/sql/mysql/V20260604_3__alter_view_focus_personnel_list.sql` | 更新视图,添加birthDate和age计算字段(达梦兼容) |
+| 4 | `jeecg-boot/.../flyway/sql/mysql/V20260604_4__fix_minor_tag_dict.sql` | 修复旧数据与字典不匹配的映射更新SQL |
+| 5 | `jeecg-boot/.../focuspersonnel/entity/FocusPersonnelDetailVo.java` | 详情VO(含个人信息30个字段) |
+| 6 | `jeecgboot-vue3/.../focuspersonnel/components/FocusPersonnelDetail.vue` | 前端详情展示组件(Tab页签+概要头部) |
 
 ### 2.2 修改文件
 
 | 序号 | 文件路径 | 修改内容 |
 |:----:|----------|----------|
-| 1 | `FocusPersonnel.java` | minorTag字段添加@Dict注解(focus_minor_tag字典) |
-| 2 | `FocusPersonnelMapper.java` | 新增queryDetailById方法 |
-| 3 | `FocusPersonnelMapper.xml` | 新增detail查询SQL(LEFT JOIN personal_info);列表查询补充5个查询条件 |
-| 4 | `IFocusPersonnelService.java` | 新增queryDetailById接口方法 |
-| 5 | `FocusPersonnelServiceImpl.java` | 实现queryDetailById方法 |
-| 6 | `FocusPersonnelController.java` | 新增queryDetailById端点;补充5个查询参数 |
-| 7 | `FocusPersonnel.api.ts` | 新增queryDetailById API |
-| 8 | `FocusPersonnelList.vue` | **重新设计**:去除高级搜索;查询条件改为10个(按需求文档);新增"刷新生成"按钮;操作栏扩展为详情/服务跟进/岗位推送/消息推送/自定义标签/删除 |
-| 9 | `FocusPersonnelModal.vue` | 新增detail方法,支持详情/编辑双模式切换 |
-| 10 | `FocusPersonnelForm.vue` | minorTag改为字典选择器(focus_minor_tag) |
-| 11 | `FocusPersonnel.data.ts` | minorTag列添加dictCode配置;清空superQuerySchema(暂不使用高级查询) |
+| 1 | `FocusPersonnel.java` | minorTag字段添加@Dict注解 |
+| 2 | `FocusPersonnelPageVo.java` | 添加@Dict注解和@Excel注解 |
+| 3 | `FocusPersonnelMapper.java` | 新增queryDetailById方法 |
+| 4 | `FocusPersonnelMapper.xml` | 新增detail查询SQL;列表补充5个查询条件;增加age范围过滤 |
+| 5 | `IFocusPersonnelService.java` | 新增queryDetailById接口 |
+| 6 | `FocusPersonnelServiceImpl.java` | 实现queryDetailById |
+| 7 | `FocusPersonnelController.java` | 新增queryDetailById;补充查询参数;增加ageBegin/ageEnd |
+| 8 | `FocusPersonnel.api.ts` | 新增queryDetailById API |
+| 9 | `FocusPersonnelList.vue` | **多次迭代**:最终改为查询条件10个、顶部按钮仅刷新生成+导出、操作栏显示3个按钮(查看+岗位推送+服务跟进)、年龄范围查询、重置按钮手动清空年龄范围 |
+| 10 | `FocusPersonnelModal.vue` | 新增detail方法,支持详情/编辑双模式 |
+| 11 | `FocusPersonnelForm.vue` | minorTag改为字典选择器 |
+| 12 | `FocusPersonnel.data.ts` | minorTag配置dictCode;清空superQuerySchema |
 
 ---
 
-## 三、数据库初始化详细说明
+## 三、数据库初始化
 
 ### 3.1 字典数据
 
 | 字典编码 | 字典名称 | 字典项数 | 说明 |
 |----------|----------|:--------:|------|
-| `focus_major_tag` | 人员大类标签 | 2 | 就业困难人员、脱贫人员(见习人员已独立为见习人员管理模块) |
-| `focus_minor_tag` | 人员小类标签 | 16 | 含就业困难人员13类、脱贫人员3类(不含见习人员小类) |
+| `focus_major_tag` | 人员大类标签 | 2 | 就业困难人员、脱贫人员 |
+| `focus_minor_tag` | 人员小类标签 | 16 | 就业困难13类 + 脱贫3类 |
 
-### 3.2 补充的按钮权限
+### 3.2 按钮权限
 
-| 权限ID | 权限标识 | 名称 | 状态 |
-|--------|----------|------|------|
-| 178060400000050 | `focus_personnel:refresh` | 刷新生成 | 占位(待开发) |
-| 178060400000051 | `focus_personnel:messagePush` | 消息推送 | 占位(待开发) |
-| 178060400000052 | `focus_personnel:jobPush` | 岗位推送 | 占位(待开发) |
-| 178060400000053 | `focus_personnel:serviceFollow` | 服务跟进 | 占位(待开发) |
-| 178060400000054 | `focus_personnel:customTag` | 自定义标签 | 占位(待开发) |
+| 权限标识 | 名称 | 状态 |
+|----------|------|:----:|
+| focus_personnel:refresh | 刷新生成 | 占位 |
+| focus_personnel:messagePush | 消息推送 | 占位 |
+| focus_personnel:jobPush | 岗位推送 | 占位 |
+| focus_personnel:serviceFollow | 服务跟进 | 占位 |
+| focus_personnel:customTag | 自定义标签 | 占位 |
+
+### 3.3 视图更新
+
+在 V20260604_3 中更新 `v_focus_personnel_list`,添加了:
+- `birthDate` 字段(来自personal_info)
+- `age` 计算字段(达梦兼容使用 `FLOOR(MONTHS_BETWEEN(CURRENT_DATE, pi.birth_date) / 12)`)
 
 ---
 
-## 四、后端详情接口
+## 四、列表页最终状态
 
-### GET /focusPersonnel/queryDetailById?id={id}
+### 查询条件(10个)
+姓名、性别、学历、户口所在地、现居住地、年龄(范围)、大类标签、小类标签、自定义标签、就业状态
 
-**请求参数:** id(重点关注人员主键)
+### 顶部按钮
+刷新生成、导出
 
-**返回数据:** FocusPersonnelDetailVo(30个字段)
+### 操作栏
+- 主按钮:查看
+- 下拉菜单:岗位推送、服务跟进
 
-**业务逻辑:**
-- 以focus_personnel为主表
-- LEFT JOIN personal_info 通过personal_id关联
-- 同时计算年龄(TIMESTAMPDIFF)
+---
 
-### 查询条件补充
+## 五、注意事项
 
-列表查询新增5个查询条件:
-- 户口所在地(householdLocation,模糊匹配)
-- 现居住地(currentResidence,模糊匹配)
-- 人员小类标签(minorTag,精确匹配)
-- 自定义标签(customTag,模糊匹配)
+1. **SQL脚本执行顺序**:V20260604_1 → V20260604_2 → V20260604_3(必须按顺序)
+2. **字典初始化**:需要执行V20260604_1字典才会生效,否则minorTag无法翻译
+3. **视图更新**:需要执行V20260604_3后才能使用年龄范围搜索
+4. **年龄搜索**:使用视图中的age计算字段(`FLOOR(MONTHS_BETWEEN(CURRENT_DATE, pi.birth_date) / 12)`),达梦兼容
+5. **按钮权限**:V20260604_2未执行时,因前端已去掉v-auth限制,按钮始终可见
+6. **岗位推送/服务跟进**:当前为占位提示,需后续开发
+7. **达梦数据库兼容**:所有SQL无反引号,使用 FROM DUAL
 
 ---
 
-## 五、前端详情组件
+## 六、第二次迭代修改(自定义标签+推送消息按钮+详情页标签编辑)
+
+### 修改日期:2026-06-04
+
+### 6.1 修改说明
+根据用户需求:
+1. 添加"自定义标签"和"推送消息"按钮,与"导出"按钮同行并放在右侧
+2. 自定义标签搜索条件从文本输入改为下拉框
+3. 此页面自定义标签只在此页面使用(从 `focus_personnel` 表中提取去重)
+4. 点击"查看"进入详情页时,标签信息Tab支持添加/编辑/删除自定义标签
+
+### 6.2 修改文件清单
 
-详情页面分5个分组展示:
+#### 后端修改
 
-| 分组 | 包含字段 | 字段数 |
-|------|----------|:------:|
-| 基本信息 | 姓名、性别、证件类型、证件号码、出生日期、年龄、民族、国籍、婚姻状况、政治面貌 | 10 |
-| 教育背景 | 学历、毕业日期、毕业院校、专业 | 4 |
-| 联系信息 | 联系电话、邮箱、QQ号码、微信号 | 4 |
-| 户籍与居住 | 户口性质、户口所在地、现居住地、现居住地址 | 4 |
-| 求职信息 | 求职人员类别、求职状态、工作经验、职业技能等级、是否留学人才、是否接受推荐职位 | 6 |
-| 标签信息 | 人员大类标签、人员小类标签、自定义标签 | 3 |
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnelMapper.xml` | 新增 `listCustomTags` SQL,查询所有非空 `custom_tag` 原始数据 |
+| 2 | `FocusPersonnelMapper.java` | 新增 `listCustomTags()` 方法定义,导入 `List` |
+| 3 | `IFocusPersonnelService.java` | 新增 `listCustomTags()` 接口方法 |
+| 4 | `FocusPersonnelServiceImpl.java` | 实现 `listCustomTags()`:拆分逗号分隔值、去重、排序 |
+| 5 | `FocusPersonnelController.java` | 新增 `GET /listCustomTags` 端点 |
+
+#### 前端修改
+
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnel.api.ts` | 新增 `listCustomTags` API 枚举和导出函数 |
+| 2 | `FocusPersonnelList.vue` | **自定义标签搜索改为 `<a-select>` 下拉框**(从后端动态加载标签选项);**头部按钮区改为 flex 左右布局**:左侧"刷新生成",右侧"自定义标签"+"推送消息"+"导出";新增"自定义标签管理弹窗"(查看现有标签+添加新标签到选项列表);页面加载时调用 `listCustomTags` 初始化下拉选项 |
+| 3 | `FocusPersonnelDetail.vue` | **标签信息Tab自定义标签改为可编辑**:非编辑态显示标签+"编辑"按钮;编辑态显示可关闭的标签列表+输入添加框+保存/取消按钮;保存后调用 `/focusPersonnel/edit` 更新 `custom_tag` 字段;导入 `PlusOutlined` 图标、`saveOrUpdate` API、`useMessage` |
+
+### 6.3 功能说明
+
+#### 1. 自定义标签搜索下拉框
+- 页面加载时调用 `GET /focusPersonnel/listCustomTags` 获取所有标签
+- 后端从 `focus_personnel` 表提取 `custom_tag` 字段,拆分逗号分隔值、去重、排序
+- 下拉框单选,支持模糊搜索匹配选项
+
+#### 2. 顶部按钮布局
+```
+[刷新生成]                          [自定义标签] [推送消息] [导出]
+```
+- 使用 `a-row justify="space-between"` 实现左右分布
+- "自定义标签"按钮打开管理弹窗,可查看所有标签和添加新标签
+- "推送消息"按钮暂为占位提示
+- 按钮均有 `v-auth` 权限控制
+
+#### 3. 详情页自定义标签编辑
+- 标签信息Tab中自定义标签行添加"编辑"按钮
+- 点击"编辑"进入编辑模式:标签可关闭删除、输入框回车或点击加号添加新标签
+- 点击"保存"调用 `/focusPersonnel/edit` 接口更新数据
+- 保存后自动刷新本地展示数据,退出编辑模式
+
+### 6.4 接口说明
+
+| 接口 | 方法 | 说明 |
+|------|:----:|------|
+| `/focusPersonnel/listCustomTags` | GET | 获取所有自定义标签(去重排序后的列表) |
+| `/focusPersonnel/edit` | PUT/POST | 保存自定义标签(通过 `id` + `customTag` 字段) |
+
+### 6.5 注意事项
+1. **自定义标签选项来源**:从 `focus_personnel.custom_tag` 字段提取(已有数据的逗号分隔值)。新添加的标签若只在管理弹窗中添加而未分配给任何人,页面刷新后会消失(因为没有持久化到任何人的记录中)
+2. **权限按钮**:`focus_personnel:customTag` 和 `focus_personnel:messagePush` 已在 V20260604_2 SQL 中定义,需要执行该脚本才能生效
+3. **自定义标签搜索**:下拉框选择某个标签后,会以 `customTag = '选中值'` 的精确匹配方式查询(而非之前的 LIKE 模糊匹配)。如果希望模糊搜索,需要在后端 XML 中修改为 LIKE 查询
+4. **需要重启后端**:Java 代码修改后需要重新编译部署
+5. **详情页保存**:使用原有的 `/focusPersonnel/edit` 接口,只更新 `id` 和 `customTag` 字段,不影响其他字段
 
 ---
 
-## 六、前端搜索条件(10个,按需求文档)
-
-| 序号 | 字段 | 控件类型 | 查询方式 |
-|:----:|------|----------|----------|
-| 1 | 姓名 | a-input | 模糊 |
-| 2 | 性别 | a-select | 精确 |
-| 3 | 学历 | a-select | 精确 |
-| 4 | 户口所在地 | a-input | 模糊 |
-| 5 | 现居住地 | a-input | 模糊 |
-| 6 | 年龄 | a-input-number | 精确 |
-| 7 | 人员大类标签 | j-dict-select-tag | 精确 |
-| 8 | 人员小类标签 | j-dict-select-tag | 精确 |
-| 9 | 自定义标签 | a-input | 模糊 |
-| 10 | 就业状态 | a-select | 精确 |
+## 七、第三次迭代修改(字典code重构+自定义标签字典+按钮布局修复)
+
+### 修改日期:2026-06-04
+
+### 7.1 修改说明
+根据用户反馈:
+1. **标签写死问题**:`focus_personnel` 表中 `major_tag`/`minor_tag` 存储的是文本值(如"就业困难人员"),不是字典code,应该关联字典使用数字code
+2. **自定义标签字典**:自定义标签也需要关联字典 `focus_custom_tag`
+3. **按钮布局不显示**:新增的"自定义标签"和"推送消息"按钮因 `v-auth` 权限未配置未显示(V20260604_2 SQL 未执行)
+
+### 7.2 修改文件清单
+
+#### 新增文件
+
+| 序号 | 文件路径 | 说明 |
+|:----:|----------|------|
+| 1 | `flyway/sql/mysql/V20260604_5__fix_tag_dict_codes.sql` | 字典code重构 + 自定义标签字典 + 数据迁移 |
+
+#### 后端修改
+
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnel.java` | `customTag` 字段添加 `@Dict(dicCode = "focus_custom_tag")` 注解 |
+| 2 | `FocusPersonnelPageVo.java` | `customTag` 字段添加 `@Dict(dicCode = "focus_custom_tag")` 注解 |
+| 3 | `FocusPersonnelDetailVo.java` | 导入 `@Dict` 并给 `customTag` 字段添加 `@Dict(dicCode = "focus_custom_tag")` 注解 |
+
+#### 前端修改
+
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnel.data.ts` | 自定义标签列添加 `dictCode: 'focus_custom_tag'` |
+| 2 | `FocusPersonnelList.vue` | 暂时移除"自定义标签"和"推送消息"按钮的 `v-auth` 指令(等 V20260604_2 SQL 执行后再加回) |
+
+### 7.3 字典code映射表
+
+#### focus_major_tag(大类标签)
+
+| item_value(code) | item_text(显示) |
+|:------------------:|-------------------|
+| 1 | 就业困难人员 |
+| 2 | 脱贫人员 |
+
+#### focus_minor_tag(小类标签)
+
+| item_value(code) | item_text(显示) | 类别 |
+|:------------------:|-------------------|:----:|
+| 1 | 大龄失业人员 | 就业困难 |
+| 2 | 残疾人员 | 就业困难 |
+| 3 | 享受最低生活保障待遇人员 | 就业困难 |
+| 4 | 城镇零就业家庭人员 | 就业困难 |
+| 5 | 农村零转移就业原建档立卡贫困家庭人员 | 就业困难 |
+| 6 | 失地农民 | 就业困难 |
+| 7 | 连续失业1年以上人员 | 就业困难 |
+| 8 | 戒毒康复人员 | 就业困难 |
+| 9 | 刑满释放人员 | 就业困难 |
+| 10 | 精神障碍康复人员 | 就业困难 |
+| 11 | 失业6个月以上退役军人 | 就业困难 |
+| 12 | 需赡养患重病直系亲属的人员 | 就业困难 |
+| 13 | 省人民政府规定的其他人员 | 就业困难 |
+| 14 | 脱贫不稳定户 | 脱贫人员 |
+| 15 | 边缘易致贫户 | 脱贫人员 |
+| 16 | 突发严重困难户 | 脱贫人员 |
+
+#### focus_custom_tag(自定义标签)
+
+| item_value | item_text | 说明 |
+|:----------:|:---------:|:----:|
+| 标签文本 | 标签文本 | `item_value = item_text`(标签本身就是值) |
+
+### 7.4 SQL执行说明
+
+**已存在的SQL脚本(按顺序执行):**
+
+| 序号 | SQL脚本 | 说明 | 状态 |
+|:----:|---------|------|:----:|
+| 1 | `V20260604_1` | 初始化字典(focus_major_tag、focus_minor_tag) | 已执行 |
+| 2 | `V20260604_2` | 补充按钮权限 | **未执行**(导致按钮不显示) |
+| 3 | `V20260604_3` | 重建视图(含age计算字段) | 已执行 |
+| 4 | `V20260604_4` | 修复旧数据字典映射 | 已执行 |
+| 5 | `V20260604_5` | **字典code重构 + 自定义标签字典 + 数据迁移** | **需要执行** |
+
+> **重要**:V20260604_5 必须在 V20260604_1~V20260604_4 之后执行。它依赖于前序脚本创建的字典ID和数据。
+
+### 7.5 按钮问题说明
+- "自定义标签"和"推送消息"按钮目前移除了 `v-auth` 指令,所有用户均可看到
+- 如需权限控制,需执行 V20260604_2 SQL 并在按钮上恢复 `v-auth` 指令
+- "刷新生成"和"导出"按钮保留原有的 `v-auth` 权限控制
+
+### 7.6 注意事项
+1. **数据迁移风险**:V20260604_5 会修改 `focus_personnel` 表中的 `major_tag` 和 `minor_tag` 数据(文本→code),建议执行前备份
+2. **视图自动适配**:`v_focus_personnel_list` 是视图,数据迁移后自动返回code值,无需重建
+3. **自定义标签字典**:`focus_custom_tag` 的 `item_value = item_text`,`@Dict` 注解翻译后和原值一致
+4. **需要重启后端**:Java 代码修改后需要重新编译部署
+5. **SQL执行顺序**:必须先执行 V20260604_1 ~ V20260604_4,再执行 V20260604_5
 
 ---
 
-## 七、前端操作栏(按需求文档)
+## 八、第四次迭代修改(自定义标签字典化管理+序号列+标签显示修复+删除服务跟进Tab)
+
+### 修改日期:2026-06-04
+
+### 8.1 修改说明
+根据用户反馈:
+1. **自定义标签管理和分配分离**:管理员在"自定义标签"按钮中管理字典项(增删),用户在详情页只能从字典中选择已有标签(多选下拉),不允许直接输入新标签
+2. **列表加序号列**
+3. **详情页标签显示名称而非code**:`majorTag`/`minorTag` 显示字典翻译后的文本
+4. **详情页去掉"服务跟进"Tab**:列表已有服务跟进功能,无需重复
+5. **列表页标签显示名称**:通过 `dictCode` 客户端翻译
+
+### 8.2 修改文件清单
+
+#### 后端修改
+
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnelMapper.xml` | 重构 `listCustomTags` 从 `sys_dict_item` 表查询;新增 `addCustomTagItem`/`deleteCustomTagItem`/`checkCustomTagItemExists` SQL |
+| 2 | `FocusPersonnelMapper.java` | 新增 `addCustomTagItem`/`deleteCustomTagItem`/`checkCustomTagItemExists` 方法 |
+| 3 | `IFocusPersonnelService.java` | 新增 `addCustomTagItem`/`deleteCustomTagItem` 接口方法 |
+| 4 | `FocusPersonnelServiceImpl.java` | 实现新接口方法(UUID生成器、检查重复、增删字典项);`listCustomTags` 改为直接查字典 |
+| 5 | `FocusPersonnelController.java` | 新增 `POST /addCustomTagItem` 和 `DELETE /deleteCustomTagItem` 端点 |
+| 6 | `FocusPersonnelDetailVo.java` | `majorTag` 和 `minorTag` 添加 `@Dict(dicCode = "focus_major_tag")` 和 `@Dict(dicCode = "focus_minor_tag")` |
+| 7 | `FocusPersonnelPageVo.java` | `majorTag` 添加 `@Dict(dicCode = "focus_major_tag")` |
+
+#### 前端修改
 
-| 操作 | 权限标识 | 实现状态 |
-|------|----------|:--------:|
-| 编辑 | focus_personnel:edit | ✅ 已实现 |
-| 详情 | 无 | ✅ 已实现 |
-| 服务跟进 | focus_personnel:serviceFollow | ⚠️ 占位 |
-| 岗位推送 | focus_personnel:jobPush | ⚠️ 占位 |
-| 消息推送 | focus_personnel:messagePush | ⚠️ 占位 |
-| 自定义标签 | focus_personnel:customTag | ⚠️ 占位 |
-| 删除 | focus_personnel:delete | ✅ 已实现 |
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnel.api.ts` | 新增 `addCustomTagItem`/`deleteCustomTagItem` API 枚举和导出函数 |
+| 2 | `FocusPersonnel.data.ts` | 添加"序号"列(`customRender: ({index}) => index + 1`) |
+| 3 | `FocusPersonnelList.vue` | 自定义标签管理弹窗改为调用后端 API 增删字典项;添加删除确认弹窗;打开管理弹窗时自动刷新标签列表 |
+| 4 | `FocusPersonnelDetail.vue` | **去掉"服务跟进"Tab**(Tab5及相关代码全部删除);**自定义标签编辑改为 `<a-select mode="multiple">` 多选下拉**(从 `listCustomTags` 加载选项);**标签显示改为使用 `getDictText` 函数**(优先使用 `majorTag_dictText` 等后端翻译字段);移除 `PlusOutlined` 导入 |
+
+### 8.3 自定义标签字典化管理流程
+
+```
+管理员操作(列表页):
+  点击"自定义标签"按钮 → 弹窗展示 focus_custom_tag 字典所有项
+  → 输入名称点击"添加" → 调用 POST /addCustomTagItem → 持久化到 sys_dict_item
+  → 点击"删除" → 调用 DELETE /deleteCustomTagItem → 从字典删除
+
+用户操作(详情页):
+  点击"编辑" → 显示 <a-select mode="multiple"> 多选下拉
+  → 只能从已有字典项中选择 → 不能输入新标签
+  → 点击"保存" → 调用 POST /focusPersonnel/edit 更新 custom_tag 字段
+```
+
+### 8.4 新增接口说明
+
+| 接口 | 方法 | 说明 |
+|------|:----:|------|
+| `/focusPersonnel/addCustomTagItem` | POST | 添加自定义标签字典项(参数:itemText) |
+| `/focusPersonnel/deleteCustomTagItem` | DELETE | 删除自定义标签字典项(参数:itemValue) |
+
+### 8.5 标签显示修复说明
+- **后端**:`FocusPersonnelDetailVo` 和 `FocusPersonnelPageVo` 的 `majorTag`/`minorTag` 都加了 `@Dict` 注解
+- **DictAspect** 自动为返回的 JSON 添加 `majorTag_dictText`/`minorTag_dictText`/`customTag_dictText` 等翻译字段
+- **前端详情页**:`getDictText` 函数优先使用 `_dictText` 后缀字段,兜底返回原始值
+- **前端列表页**:通过列配置的 `dictCode: 'focus_major_tag'` 等,由 BasicTable 客户端自动翻译
+
+### 8.6 注意事项
+1. **需要执行 V20260604_5 SQL**:字典code重构后,数据存储的是数字code,需要后端 `@Dict` 或前端 `dictCode` 翻译
+2. **需要重启后端**:Java 代码修改后需要重新编译部署
+3. **详情页自定义标签编辑**:改为下拉多选后,用户只能从管理员预定义的标签中选择,不再支持自由输入
 
 ---
 
-## 八、注意事项
+## 九、BUG修复(API参数错误+标签显示数字)
+
+### 修改日期:2026-06-04
+
+### 9.1 BUG1:addCustomTagItem 参数错误
+**错误现象**:
+```
+Required request parameter 'itemText' for method parameter type String is not present
+```
+
+**原因**:前端 `defHttp.post({ url, params })` 中 `params` 在 POST 请求中被当作请求体(body)发送,而不是 URL 查询参数,而后端使用 `@RequestParam` 只能从查询参数或表单中读取。
+
+**修复**:
+- 后端 `FocusPersonnelController.java`:`@PostMapping` 改为 `@GetMapping`
+- 前端不变(`params` 在 GET 请求中会自动作为 URL 查询参数)
+
+### 9.2 BUG2:列表和详情标签显示数字而非文字
+**错误现象**:字典code重构(V20260604_5)后,数据存储为数字code(如 `1`),但前端未正确翻译。
+
+**原因**:
+1. 详情页 `getDictText` 函数依赖后端 `_dictText` 字段(DictAspect可能未拦截自定义方法)
+2. 列表页 BasicTable 的 `dictCode` 翻译依赖字典缓存,如果用户登录后字典才创建/修改,缓存未更新
+
+**修复**:
+- **详情页**:改用 `initDictOptions` 从 `/sys/dict/getDictItems/{dictCode}` 加载字典数据,构建 `Map<item_value, item_text>` 查询映射,`getDictText` 函数直接查映射
+- **列表页**:`onMounted` 中预加载三个字典(`focus_major_tag`、`focus_minor_tag`、`focus_custom_tag`),确保 BasicTable 翻译生效
 
-1. **SQL兼容性**:本项目使用达梦数据库,不支持反引号 `` ` ``,SQL中所有表名、字段名均未使用反引号
-2. **字典兼容**:字典插入使用了 `SELECT ... WHERE NOT EXISTS` 条件判断,避免重复插入
-3. **原有queryById接口保留不变**,新增queryDetailById接口用于详情页面
-4. 前端详情组件通过 `watch record` 自动加载数据,无需手动调用
-5. **不显示高级搜索**,按用户要求去除
-6. **5个新增按钮均为占位**,需后续开发(刷新生成、消息推送、岗位推送、服务跟进、自定义标签)
-7. 数据库不删除原有数据:V20260604_1 和 V20260604_2 脚本都用了 WHERE NOT EXISTS,重复执行不会报错
+### 9.3 修改文件清单
+
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnelController.java` | `addCustomTagItem` 方法改为 `@GetMapping` |
+| 2 | `FocusPersonnelDetail.vue` | 导入 `initDictOptions`;`loadDictData` 加载大类小类字典构建查询映射;`getDictText` 函数改为查映射 |
+| 3 | `FocusPersonnelList.vue` | 导入 `initDictOptions`;`onMounted` 中预加载三个字典 |
+
+### 9.4 补充修复(第一次修复无效的原因)
+
+| 问题 | 错误原因 | 修复内容 |
+|:----|----------|----------|
+| API错误 `Request method 'POST' is not supported` | 后端改为 `@GetMapping` 但前端仍用 `defHttp.post` | 前端 API `addCustomTagItem` 改为 `defHttp.get` |
+| 标签仍显示数字 | `initDictOptions` 返回的字典项字段是 `text`(不是 `label`)。JeecgBoot 的 `DictModel` 只有 `{value, text}` 字段 | 将 `item.label` 改为 `item.text \|\| item.title \|\| item.label`(兼容多种字段名) |
+
+### 9.5 注意事项
+1. **字典缓存问题**:`initDictOptions` 首次调用会从服务器拉取并缓存。如果字典在用户登录后被修改,需要刷新页面或重新登录清除缓存
+2. **需要重启后端**:Java 代码修改后需重新编译部署
+3. **前端需刷新**:前端 `api.ts` 和 `Detail.vue` 修改后需重启前端 dev server 或硬刷新(Ctrl+F5)
+
+---
+
+## 十、列表和详情标签显示数字修复(_dictText + 绕过缓存)
+
+### 修改日期:2026-06-04
+
+### 10.1 问题现象
+列表页和详情页的人员大类标签、人员小类标签、自定义标签都显示数字(如 `1`、`2`),而不是对应的字典文本(如"就业困难人员"、"脱贫人员")。
+
+### 10.2 根因分析
+
+**列表页**:JeecgBoot-vue3 的 BasicTable 组件不识别 `columns` 配置中的 `dictCode` 属性。字典翻译完全依赖后端 `DictAspect` 为返回结果添加的 `{fieldName}_dictText` 字段(如 `majorTag_dictText`)。但 `data.ts` 中列定义的 `dataIndex` 配置的是原始字段名(如 `majorTag`),导致 BasicTable 显示的是原始数字值,而不是 `majorTag_dictText` 的翻译文本。
+
+参考 JeecgBoot 其他模块(如 `manage.data.ts`),字典列的 `dataIndex` 直接使用 `{field}_dictText` 格式:
+```ts
+// manage.data.ts 示例
+{ title: '发送状态', dataIndex: 'esSendStatus_dictText' }
+```
+
+**详情页**:`loadDictData` 使用 `initDictOptions` 加载字典数据。`initDictOptions` 内部优先检查前端缓存(`userStore.getAllDictItems` 或 `authCache`),如果用户在字典被修改(V20260604_5 SQL)之前已登录,缓存中就是旧的字典数据(`value='就业困难人员'` 而非 `value='1'`),导致翻译查找失败。
+
+### 10.3 修复内容
+
+#### 列表页修复
+
+**文件**:`FocusPersonnel.data.ts`
+
+将 `majorTag`、`minorTag`、`customTag` 列的 `dataIndex` 从原始字段名改为 `{field}_dictText`,移除无用的 `dictCode` 属性:
+
+```ts
+// 修改前
+{ title: '人员大类标签', dataIndex: 'majorTag', dictCode: 'focus_major_tag' }
+{ title: '人员小类标签', dataIndex: 'minorTag', dictCode: 'focus_minor_tag' }
+{ title: '自定义标签',   dataIndex: 'customTag', dictCode: 'focus_custom_tag' }
+
+// 修改后
+{ title: '人员大类标签', dataIndex: 'majorTag_dictText' }
+{ title: '人员小类标签', dataIndex: 'minorTag_dictText' }
+{ title: '自定义标签',   dataIndex: 'customTag_dictText' }
+```
+
+#### 详情页修复
+
+**文件**:`FocusPersonnelDetail.vue`
+
+1. 导入 `ajaxGetDictItems`(从 `/@/utils/dict/index`)
+2. 将 `loadDictData` 中的 `initDictOptions` 改为 `ajaxGetDictItems`,绕过前端缓存直接调用后端 API
+
+```ts
+// 修改前
+const majorDict = await initDictOptions('focus_major_tag');
+const minorDict = await initDictOptions('focus_minor_tag');
+
+// 修改后
+const majorDict = await ajaxGetDictItems('focus_major_tag');
+const minorDict = await ajaxGetDictItems('focus_minor_tag');
+```
+
+### 10.4 修改文件清单
+
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 1 | `FocusPersonnel.data.ts` | `majorTag`/`minorTag`/`customTag` 列的 `dataIndex` 改为 `majorTag_dictText`/`minorTag_dictText`/`customTag_dictText`,移除 `dictCode` |
+| 2 | `FocusPersonnelDetail.vue` | 导入 `ajaxGetDictItems`;`loadDictData` 中使用 `ajaxGetDictItems` 替代 `initDictOptions` 加载大类小类字典 |
+| 3 | `FocusPersonnel.api.ts` | `deleteCustomTagItem` 增加 `{ joinParamsToUrl: true }` 选项,修复 DELETE 请求参数未传递到 URL 查询参数的问题 |
+
+### 10.5 补充修复:deleteCustomTagItem 参数错误
+
+**错误现象**:
+```
+Required request parameter 'itemValue' for method parameter type String is not present
+```
+
+**原因**:`defHttp.delete` 默认将 `params` 放在请求体(body)中发送,而后端 `@RequestParam` 只能从 URL 查询参数中读取。`deleteOne` 已有 `{ joinParamsToUrl: true }` 正确处理,但 `deleteCustomTagItem` 未添加此选项。
+
+**修复**:在 `FocusPersonnel.api.ts` 的 `deleteCustomTagItem` 函数中增加 `{ joinParamsToUrl: true }` 选项。
+
+### 10.6 补充修复:删除自定义标签时检查是否被用户绑定
+
+**需求变更**:删除自定义标签时,如果有用户绑定了该标签,应禁止删除并给出提示,而不是自动清理。
+
+**原因**:后端 `deleteCustomTagItem` 需要先检查 `focus_personnel` 表中是否有用户使用了该标签。
+
+**修复**(4个文件修改):
+
+1. **`FocusPersonnelMapper.java`** — 移除 `removeCustomTagFromPersonnel`,新增 `countPersonnelByCustomTag` 方法
+2. **`FocusPersonnelMapper.xml`** — 移除清理 UPDATE SQL,新增 `countPersonnelByCustomTag` SELECT SQL,使用 4 种 LIKE 条件精确匹配逗号分隔值中的标签(完全匹配、开头、结尾、中间)
+3. **`FocusPersonnelServiceImpl.java`** — `deleteCustomTagItem` 先检查绑定用户数,大于 0 则返回 false
+4. **`FocusPersonnelController.java`** — 处理 Service 返回结果,若不可删除则返回 `Result.error("该标签已被用户绑定,无法删除")`
+5. **`FocusPersonnelList.vue`** — 错误提示改用 `e.message` 显示后端返回的具体错误信息
+
+**修改文件清单更新**:
+
+| 序号 | 文件路径 | 修改内容 |
+|:----:|----------|----------|
+| 3 | `FocusPersonnel.api.ts` | `deleteCustomTagItem` 增加 `{ joinParamsToUrl: true }` |
+| 4 | `FocusPersonnelMapper.java` | 移除 `removeCustomTagFromPersonnel`,新增 `countPersonnelByCustomTag` |
+| 5 | `FocusPersonnelMapper.xml` | 移除清理 UPDATE,新增绑定检查 SELECT |
+| 6 | `FocusPersonnelServiceImpl.java` | `deleteCustomTagItem` 先检查绑定再删字典项 |
+| 7 | `FocusPersonnelController.java` | 返回 `Result.error` 提示已被绑定 |
+| 8 | `FocusPersonnelList.vue` | 错误提示显示后端消息 |
+
+### 10.7 注意事项
+1. **需重启前端 dev server** 或硬刷新(Ctrl+F5)使前端修改生效
+2. **需重启后端**:Java 代码修改后需重新编译部署
+3. **列表页依赖后端 DictAspect**:已在第九章为 `FocusPersonnelPageVo` 添加 `@Dict` 注解

+ 58 - 2
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/controller/FocusPersonnelController.java

@@ -1,6 +1,5 @@
 package org.jeecg.modules.zjrs.focuspersonnel.controller;
 
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import io.swagger.v3.oas.annotations.Operation;
@@ -12,7 +11,6 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
 import org.jeecg.common.api.vo.Result;
 import org.jeecg.common.aspect.annotation.AutoLog;
 import org.jeecg.common.system.base.controller.JeecgController;
-import org.jeecg.common.system.query.QueryGenerator;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnel;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelDetailVo;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelPageVo;
@@ -23,6 +21,7 @@ import org.springframework.web.servlet.ModelAndView;
 
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -41,6 +40,8 @@ public class FocusPersonnelController extends JeecgController<FocusPersonnel, IF
 
     /**
      * 分页列表查询(关联个人信息表,返回个人信息字段)
+     * 支持的查询参数:fullName/education/householdLocation/currentResidence/majorTag/minorTag/customTag/jobSearchStatus
+     * 年龄范围参数:birthDate_begin/birthDate_end(前端将年龄转为出生日期范围)
      */
     @Operation(summary = "就业一湛通-重点关注人员管理-分页列表查询")
     @GetMapping(value = "/list")
@@ -53,14 +54,27 @@ public class FocusPersonnelController extends JeecgController<FocusPersonnel, IF
         String fullName = req.getParameter("fullName");
         String gender = req.getParameter("gender");
         String education = req.getParameter("education");
+        String householdLocation = req.getParameter("householdLocation");
+        String currentResidence = req.getParameter("currentResidence");
         String majorTag = req.getParameter("majorTag");
+        String minorTag = req.getParameter("minorTag");
+        String customTag = req.getParameter("customTag");
         String jobSearchStatus = req.getParameter("jobSearchStatus");
 
         if (fullName != null && !fullName.isEmpty()) params.put("fullName", fullName);
         if (gender != null && !gender.isEmpty()) params.put("gender", gender);
         if (education != null && !education.isEmpty()) params.put("education", education);
+        if (householdLocation != null && !householdLocation.isEmpty()) params.put("householdLocation", householdLocation);
+        if (currentResidence != null && !currentResidence.isEmpty()) params.put("currentResidence", currentResidence);
         if (majorTag != null && !majorTag.isEmpty()) params.put("majorTag", majorTag);
+        if (minorTag != null && !minorTag.isEmpty()) params.put("minorTag", minorTag);
+        if (customTag != null && !customTag.isEmpty()) params.put("customTag", customTag);
         if (jobSearchStatus != null && !jobSearchStatus.isEmpty()) params.put("jobSearchStatus", jobSearchStatus);
+        // 年龄范围参数(视图已有age计算字段)
+        String ageBegin = req.getParameter("ageBegin");
+        String ageEnd = req.getParameter("ageEnd");
+        if (ageBegin != null && !ageBegin.isEmpty()) params.put("ageBegin", ageBegin);
+        if (ageEnd != null && !ageEnd.isEmpty()) params.put("ageEnd", ageEnd);
 
         Page<FocusPersonnelPageVo> page = new Page<>(pageNo, pageSize);
         IPage<FocusPersonnelPageVo> pageList = focusPersonnelService.queryPageList(page, params);
@@ -137,4 +151,46 @@ public class FocusPersonnelController extends JeecgController<FocusPersonnel, IF
     public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
         return super.importExcel(request, response, FocusPersonnel.class);
     }
+
+    /**
+     * 获取所有自定义标签列表(从 focus_custom_tag 字典查询)
+     * 用于前端自定义标签搜索下拉框的数据源
+     */
+    @Operation(summary = "就业一湛通-重点关注人员管理-获取所有自定义标签列表")
+    @GetMapping(value = "/listCustomTags")
+    public Result<List<String>> listCustomTags() {
+        return Result.OK(focusPersonnelService.listCustomTags());
+    }
+
+    /**
+     * 添加自定义标签字典项
+     * @param itemText 标签名称
+     */
+    @Operation(summary = "就业一湛通-重点关注人员管理-添加自定义标签")
+    @GetMapping(value = "/addCustomTagItem")
+    public Result<String> addCustomTagItem(@RequestParam(name = "itemText") String itemText) {
+        if (itemText == null || itemText.trim().isEmpty()) {
+            return Result.error("标签名称不能为空");
+        }
+        boolean success = focusPersonnelService.addCustomTagItem(itemText.trim());
+        if (success) {
+            return Result.OK("添加成功");
+        } else {
+            return Result.error("该标签已存在");
+        }
+    }
+
+    /**
+     * 删除自定义标签字典项
+     * @param itemValue 标签值
+     */
+    @Operation(summary = "就业一湛通-重点关注人员管理-删除自定义标签")
+    @DeleteMapping(value = "/deleteCustomTagItem")
+    public Result<String> deleteCustomTagItem(@RequestParam(name = "itemValue") String itemValue) {
+        boolean deleted = focusPersonnelService.deleteCustomTagItem(itemValue);
+        if (!deleted) {
+            return Result.error("该标签已被用户绑定,无法删除");
+        }
+        return Result.OK("删除成功");
+    }
 }

+ 1 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/entity/FocusPersonnel.java

@@ -49,6 +49,7 @@ public class FocusPersonnel implements Serializable {
     private java.lang.String minorTag;
 
     /**自定义标签*/
+    @Dict(dicCode = "focus_custom_tag")
     @Excel(name = "自定义标签", width = 15)
     @Schema(description = "自定义标签")
     private java.lang.String customTag;

+ 4 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/entity/FocusPersonnelDetailVo.java

@@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
+import org.jeecg.common.aspect.annotation.Dict;
 import org.springframework.format.annotation.DateTimeFormat;
 
 import java.io.Serializable;
@@ -31,12 +32,15 @@ public class FocusPersonnelDetailVo implements Serializable {
     private java.lang.String personalId;
 
     @Schema(description = "人员大类标签")
+    @Dict(dicCode = "focus_major_tag")
     private java.lang.String majorTag;
 
     @Schema(description = "人员小类标签")
+    @Dict(dicCode = "focus_minor_tag")
     private java.lang.String minorTag;
 
     @Schema(description = "自定义标签")
+    @Dict(dicCode = "focus_custom_tag")
     private java.lang.String customTag;
 
     // ========== 个人信息表字段 ==========

+ 5 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/entity/FocusPersonnelPageVo.java

@@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
+import org.jeecg.common.aspect.annotation.Dict;
+import org.jeecgframework.poi.excel.annotation.Excel;
 
 import java.io.Serializable;
 
@@ -28,14 +30,17 @@ public class FocusPersonnelPageVo implements Serializable {
     private java.lang.String personalId;
 
     /**人员大类标签*/
+    @Dict(dicCode = "focus_major_tag")
     @Schema(description = "人员大类标签")
     private java.lang.String majorTag;
 
     /**人员小类标签*/
+    @Dict(dicCode = "focus_minor_tag")
     @Schema(description = "人员小类标签")
     private java.lang.String minorTag;
 
     /**自定义标签*/
+    @Dict(dicCode = "focus_custom_tag")
     @Schema(description = "自定义标签")
     private java.lang.String customTag;
 

+ 34 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/mapper/FocusPersonnelMapper.java

@@ -7,6 +7,7 @@ import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnel;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelDetailVo;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelPageVo;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -31,4 +32,37 @@ public interface FocusPersonnelMapper extends BaseMapper<FocusPersonnel> {
      * @return 包含个人信息全部字段的详情数据
      */
     FocusPersonnelDetailVo queryDetailById(@Param("id") String id);
+
+    /**
+     * 获取所有自定义标签(从focus_custom_tag字典查询)
+     * @return 去重排序后的自定义标签列表
+     */
+    List<String> listCustomTags();
+
+    /**
+     * 添加自定义标签字典项
+     * @param id 字典项ID
+     * @param itemText 标签名称(同时作为item_value和item_text)
+     */
+    void addCustomTagItem(@Param("id") String id, @Param("itemText") String itemText);
+
+    /**
+     * 删除自定义标签字典项
+     * @param itemValue 标签值
+     */
+    void deleteCustomTagItem(@Param("itemValue") String itemValue);
+
+    /**
+     * 检查自定义标签字典项是否已存在
+     * @param itemValue 标签值
+     * @return 存在数量
+     */
+    int checkCustomTagItemExists(@Param("itemValue") String itemValue);
+
+    /**
+     * 统计使用了指定自定义标签的重点关注人员数量(用于删除前检查是否被绑定)
+     * @param itemValue 标签值
+     * @return 使用该标签的用户数
+     */
+    int countPersonnelByCustomTag(@Param("itemValue") String itemValue);
 }

+ 52 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/mapper/xml/FocusPersonnelMapper.xml

@@ -33,6 +33,12 @@
             <if test="params.jobSearchStatus != null and params.jobSearchStatus != ''">
                 AND job_search_status = #{params.jobSearchStatus}
             </if>
+            <if test="params.ageBegin != null and params.ageBegin != ''">
+                <![CDATA[ AND age >= #{params.ageBegin} ]]>
+            </if>
+            <if test="params.ageEnd != null and params.ageEnd != ''">
+                <![CDATA[ AND age <= #{params.ageEnd} ]]>
+            </if>
         </where>
         ORDER BY id DESC
     </select>
@@ -78,4 +84,50 @@
         WHERE fp.id = #{id}
     </select>
 
+    <!-- 获取所有自定义标签(从focus_custom_tag字典查询) -->
+    <select id="listCustomTags" resultType="string">
+        SELECT item_value FROM sys_dict_item
+        WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_custom_tag' AND del_flag = 0)
+          AND status = 1
+        ORDER BY sort_order ASC, item_value ASC
+    </select>
+
+    <!-- 添加自定义标签字典项 -->
+    <insert id="addCustomTagItem">
+        INSERT INTO sys_dict_item (id, dict_id, item_text, item_value, status, create_by, create_time)
+        VALUES (
+            #{id},
+            (SELECT id FROM sys_dict WHERE dict_code = 'focus_custom_tag' AND del_flag = 0),
+            #{itemText},
+            #{itemText},
+            1,
+            'admin',
+            CURRENT_TIMESTAMP
+        )
+    </insert>
+
+    <!-- 删除自定义标签字典项(按item_text删除) -->
+    <delete id="deleteCustomTagItem">
+        DELETE FROM sys_dict_item
+        WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_custom_tag' AND del_flag = 0)
+          AND item_value = #{itemValue}
+    </delete>
+
+    <!-- 统计使用了指定自定义标签的重点关注人员数量(删除前检查是否被绑定) -->
+    <select id="countPersonnelByCustomTag" resultType="int">
+        SELECT COUNT(*) FROM focus_personnel
+        WHERE custom_tag IS NOT NULL
+          AND (custom_tag = #{itemValue}
+               OR custom_tag LIKE #{itemValue} || ',%'
+               OR custom_tag LIKE '%,' || #{itemValue}
+               OR custom_tag LIKE '%,' || #{itemValue} || ',%')
+    </select>
+
+    <!-- 检查自定义标签字典项是否已存在 -->
+    <select id="checkCustomTagItemExists" resultType="int">
+        SELECT COUNT(*) FROM sys_dict_item
+        WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_custom_tag' AND del_flag = 0)
+          AND item_value = #{itemValue}
+    </select>
+
 </mapper>

+ 19 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/service/IFocusPersonnelService.java

@@ -6,6 +6,7 @@ import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnel;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelDetailVo;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelPageVo;
 
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -27,4 +28,22 @@ public interface IFocusPersonnelService extends IService<FocusPersonnel> {
      * @return 包含个人信息全部字段的详情VO
      */
     FocusPersonnelDetailVo queryDetailById(String id);
+
+    /**
+     * 获取所有自定义标签列表(从 focus_custom_tag 字典查询)
+     * @return 字典中的自定义标签列表
+     */
+    List<String> listCustomTags();
+
+    /**
+     * 添加自定义标签字典项
+     * @param itemText 标签名称
+     */
+    boolean addCustomTagItem(String itemText);
+
+    /**
+     * 删除自定义标签字典项
+     * @param itemValue 标签值
+     */
+    boolean deleteCustomTagItem(String itemValue);
 }

+ 32 - 1
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/focuspersonnel/service/impl/FocusPersonnelServiceImpl.java

@@ -2,6 +2,7 @@ package org.jeecg.modules.zjrs.focuspersonnel.service.impl;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.jeecg.common.util.UUIDGenerator;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnel;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelDetailVo;
 import org.jeecg.modules.zjrs.focuspersonnel.entity.FocusPersonnelPageVo;
@@ -9,7 +10,8 @@ import org.jeecg.modules.zjrs.focuspersonnel.mapper.FocusPersonnelMapper;
 import org.jeecg.modules.zjrs.focuspersonnel.service.IFocusPersonnelService;
 import org.springframework.stereotype.Service;
 
-import java.util.Map;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * @Description: 就业一湛通-重点关注人员管理
@@ -29,4 +31,33 @@ public class FocusPersonnelServiceImpl extends ServiceImpl<FocusPersonnelMapper,
     public FocusPersonnelDetailVo queryDetailById(String id) {
         return baseMapper.queryDetailById(id);
     }
+
+    @Override
+    public List<String> listCustomTags() {
+        // 从 focus_custom_tag 字典查询已启用的标签项
+        return baseMapper.listCustomTags();
+    }
+
+    @Override
+    public boolean addCustomTagItem(String itemText) {
+        // 检查是否已存在
+        if (baseMapper.checkCustomTagItemExists(itemText) > 0) {
+            return false;
+        }
+        // 生成UUID作为字典项ID
+        String id = UUIDGenerator.generate();
+        baseMapper.addCustomTagItem(id, itemText);
+        return true;
+    }
+
+    @Override
+    public boolean deleteCustomTagItem(String itemValue) {
+        // 检查该标签是否有用户绑定,有则禁止删除
+        int count = baseMapper.countPersonnelByCustomTag(itemValue);
+        if (count > 0) {
+            return false;
+        }
+        baseMapper.deleteCustomTagItem(itemValue);
+        return true;
+    }
 }

+ 25 - 0
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_3__alter_view_focus_personnel_list.sql

@@ -0,0 +1,25 @@
+-- ============================================================
+-- 更新 v_focus_personnel_list 视图
+-- 1. 添加 birth_date 字段支持年龄范围搜索
+-- 2. 列名统一用下划线格式(与XML WHERE条件对齐)
+-- 达梦数据库兼容:无反引号,使用 FLOOR+MONTHS_BETWEEN 计算年龄
+-- ============================================================
+CREATE OR REPLACE VIEW v_focus_personnel_list AS
+SELECT
+  fp.id,
+  fp.personal_id,
+  fp.major_tag,
+  fp.minor_tag,
+  fp.custom_tag,
+  pi.full_name,
+  pi.gender,
+  FLOOR(MONTHS_BETWEEN(CURRENT_DATE, pi.birth_date) / 12) AS age,
+  pi.education,
+  pi.household_location,
+  pi.current_residence,
+  pi.job_seeker_category,
+  pi.contact_phone,
+  pi.job_search_status,
+  pi.birth_date
+FROM focus_personnel fp
+LEFT JOIN personal_info pi ON fp.personal_id = pi.id;

+ 39 - 0
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_4__fix_minor_tag_dict.sql

@@ -0,0 +1,39 @@
+-- ============================================================
+-- 修复重点关注人员小类标签数据 - 旧值映射到字典标准值
+-- 现有数据值与字典focus_minor_tag的标准值不一致,
+-- 通过CASE WHEN进行批量映射更新
+-- ============================================================
+
+-- 1. 先查看将受影响的记录数
+SELECT minor_tag AS 旧值, COUNT(*) AS 记录数
+FROM focus_personnel
+WHERE minor_tag IS NOT NULL
+GROUP BY minor_tag
+ORDER BY minor_tag;
+
+-- 2. 执行映射更新
+-- 映射规则:
+--   4050人员 → 大龄失业人员(4050指女性40+/男性50+失业人员)
+--   女性失业人员 → 大龄失业人员(旧分类归入大龄失业人员)
+--   脱贫监测户 → 脱贫不稳定户
+--   脱贫边缘户 → 边缘易致贫户
+--   长期失业人员 → 连续失业1年以上人员
+--   零就业家庭 → 城镇零就业家庭人员
+UPDATE focus_personnel
+SET minor_tag = CASE
+  WHEN minor_tag = '4050人员' THEN '大龄失业人员'
+  WHEN minor_tag = '女性失业人员' THEN '大龄失业人员'
+  WHEN minor_tag = '脱贫监测户' THEN '脱贫不稳定户'
+  WHEN minor_tag = '脱贫边缘户' THEN '边缘易致贫户'
+  WHEN minor_tag = '长期失业人员' THEN '连续失业1年以上人员'
+  WHEN minor_tag = '零就业家庭' THEN '城镇零就业家庭人员'
+  ELSE minor_tag
+END
+WHERE minor_tag IN ('4050人员', '女性失业人员', '脱贫监测户', '脱贫边缘户', '长期失业人员', '零就业家庭');
+
+-- 3. 验证更新结果
+SELECT minor_tag AS 更新后值, COUNT(*) AS 记录数
+FROM focus_personnel
+WHERE minor_tag IS NOT NULL
+GROUP BY minor_tag
+ORDER BY minor_tag;

+ 81 - 0
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V20260604_5__fix_tag_dict_codes.sql

@@ -0,0 +1,81 @@
+-- ============================================================
+-- 重点关注人员管理 - 字典code重构 + 自定义标签字典
+-- 说明:
+--   1. focus_major_tag:item_value 改为数字code(1=就业困难人员, 2=脱贫人员)
+--   2. focus_minor_tag:item_value 改为数字code(1~16)
+--   3. 创建 focus_custom_tag 字典(自定义标签)
+--   4. 迁移 focus_personnel 表中现有数据从文本值→code值
+-- 达梦数据库兼容(不加反引号,使用 FROM DUAL)
+-- ============================================================
+
+-- ============================================================
+-- 1. 重构 focus_major_tag 字典项:item_value 改为数字code
+-- ============================================================
+UPDATE sys_dict_item
+SET item_value = '1'
+WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_major_tag' AND del_flag = 0)
+  AND item_text = '就业困难人员';
+
+UPDATE sys_dict_item
+SET item_value = '2'
+WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_major_tag' AND del_flag = 0)
+  AND item_text = '脱贫人员';
+
+-- 迁移 focus_personnel 表数据:文本值 → code
+UPDATE focus_personnel SET major_tag = '1' WHERE major_tag = '就业困难人员';
+UPDATE focus_personnel SET major_tag = '2' WHERE major_tag = '脱贫人员';
+
+-- ============================================================
+-- 2. 重构 focus_minor_tag 字典项:item_value 改为数字code(1~16)
+-- ============================================================
+-- 就业困难人员小类(1~13)
+UPDATE sys_dict_item SET item_value = '1'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '大龄失业人员';
+UPDATE sys_dict_item SET item_value = '2'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '残疾人员';
+UPDATE sys_dict_item SET item_value = '3'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '享受最低生活保障待遇人员';
+UPDATE sys_dict_item SET item_value = '4'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '城镇零就业家庭人员';
+UPDATE sys_dict_item SET item_value = '5'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '农村零转移就业原建档立卡贫困家庭人员';
+UPDATE sys_dict_item SET item_value = '6'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '失地农民';
+UPDATE sys_dict_item SET item_value = '7'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '连续失业1年以上人员';
+UPDATE sys_dict_item SET item_value = '8'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '戒毒康复人员';
+UPDATE sys_dict_item SET item_value = '9'  WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '刑满释放人员';
+UPDATE sys_dict_item SET item_value = '10' WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '精神障碍康复人员';
+UPDATE sys_dict_item SET item_value = '11' WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '失业6个月以上退役军人';
+UPDATE sys_dict_item SET item_value = '12' WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '需赡养患重病直系亲属的人员';
+UPDATE sys_dict_item SET item_value = '13' WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '省人民政府规定的其他人员';
+-- 脱贫人员小类(14~16)
+UPDATE sys_dict_item SET item_value = '14' WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '脱贫不稳定户';
+UPDATE sys_dict_item SET item_value = '15' WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '边缘易致贫户';
+UPDATE sys_dict_item SET item_value = '16' WHERE dict_id = (SELECT id FROM sys_dict WHERE dict_code = 'focus_minor_tag' AND del_flag = 0) AND item_text = '突发严重困难户';
+
+-- 迁移 focus_personnel 表数据:文本值 → code
+UPDATE focus_personnel SET minor_tag = '1'  WHERE minor_tag = '大龄失业人员';
+UPDATE focus_personnel SET minor_tag = '2'  WHERE minor_tag = '残疾人员';
+UPDATE focus_personnel SET minor_tag = '3'  WHERE minor_tag = '享受最低生活保障待遇人员';
+UPDATE focus_personnel SET minor_tag = '4'  WHERE minor_tag = '城镇零就业家庭人员';
+UPDATE focus_personnel SET minor_tag = '5'  WHERE minor_tag = '农村零转移就业原建档立卡贫困家庭人员';
+UPDATE focus_personnel SET minor_tag = '6'  WHERE minor_tag = '失地农民';
+UPDATE focus_personnel SET minor_tag = '7'  WHERE minor_tag = '连续失业1年以上人员';
+UPDATE focus_personnel SET minor_tag = '8'  WHERE minor_tag = '戒毒康复人员';
+UPDATE focus_personnel SET minor_tag = '9'  WHERE minor_tag = '刑满释放人员';
+UPDATE focus_personnel SET minor_tag = '10' WHERE minor_tag = '精神障碍康复人员';
+UPDATE focus_personnel SET minor_tag = '11' WHERE minor_tag = '失业6个月以上退役军人';
+UPDATE focus_personnel SET minor_tag = '12' WHERE minor_tag = '需赡养患重病直系亲属的人员';
+UPDATE focus_personnel SET minor_tag = '13' WHERE minor_tag = '省人民政府规定的其他人员';
+UPDATE focus_personnel SET minor_tag = '14' WHERE minor_tag = '脱贫不稳定户';
+UPDATE focus_personnel SET minor_tag = '15' WHERE minor_tag = '边缘易致贫户';
+UPDATE focus_personnel SET minor_tag = '16' WHERE minor_tag = '突发严重困难户';
+
+-- ============================================================
+-- 3. 创建自定义标签字典 (focus_custom_tag)
+-- ============================================================
+INSERT INTO sys_dict (id, dict_name, dict_code, description, del_flag, create_by, create_time, update_by, update_time, type)
+SELECT '178060400000003', '自定义标签', 'focus_custom_tag', '重点关注人员的自定义标签(页面级别,仅在此页面使用)', 0, 'admin', '2026-06-04 14:00:00', NULL, NULL, 0
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM sys_dict WHERE dict_code = 'focus_custom_tag');
+
+-- 验证数据迁移结果
+SELECT 'major_tag 迁移结果' AS 检查项, major_tag, COUNT(*) AS 记录数
+FROM focus_personnel GROUP BY major_tag ORDER BY major_tag;
+
+SELECT 'minor_tag 迁移结果' AS 检查项, minor_tag, COUNT(*) AS 记录数
+FROM focus_personnel GROUP BY minor_tag ORDER BY minor_tag;

+ 26 - 0
jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnel.api.ts

@@ -12,6 +12,9 @@ enum Api {
   importExcel = '/focusPersonnel/importExcel',
   exportXls = '/focusPersonnel/exportXls',
   queryDetailById = '/focusPersonnel/queryDetailById',
+  listCustomTags = '/focusPersonnel/listCustomTags',
+  addCustomTagItem = '/focusPersonnel/addCustomTagItem',
+  deleteCustomTagItem = '/focusPersonnel/deleteCustomTagItem',
 }
 
 /**
@@ -78,4 +81,27 @@ export const saveOrUpdate = (params, isUpdate) => {
  */
 export const queryDetailById = (params) => {
   return defHttp.get({ url: Api.queryDetailById, params });
+};
+
+/**
+ * 获取所有自定义标签列表(用于搜索下拉框)
+ */
+export const listCustomTags = () => {
+  return defHttp.get({ url: Api.listCustomTags });
+};
+
+/**
+ * 添加自定义标签字典项
+ * @param itemText 标签名称
+ */
+export const addCustomTagItem = (itemText) => {
+  return defHttp.get({ url: Api.addCustomTagItem, params: { itemText } });
+};
+
+/**
+ * 删除自定义标签字典项
+ * @param itemValue 标签值
+ */
+export const deleteCustomTagItem = (itemValue) => {
+  return defHttp.delete({ url: Api.deleteCustomTagItem, params: { itemValue } }, { joinParamsToUrl: true });
 };

+ 11 - 5
jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnel.data.ts

@@ -2,6 +2,14 @@ import { BasicColumn } from '/@/components/Table';
 
 // 列表数据
 export const columns: BasicColumn[] = [
+  {
+    title: '序号',
+    align: 'center',
+    dataIndex: 'index',
+    key: 'rowIndex',
+    width: 60,
+    customRender: ({ index }) => index + 1,
+  },
   {
     title: '姓名',
     align: 'center',
@@ -50,19 +58,17 @@ export const columns: BasicColumn[] = [
   {
     title: '人员大类标签',
     align: 'center',
-    dataIndex: 'majorTag',
-    dictCode: 'focus_major_tag',
+    dataIndex: 'majorTag_dictText',
   },
   {
     title: '人员小类标签',
     align: 'center',
-    dataIndex: 'minorTag',
-    dictCode: 'focus_minor_tag',
+    dataIndex: 'minorTag_dictText',
   },
   {
     title: '自定义标签',
     align: 'center',
-    dataIndex: 'customTag',
+    dataIndex: 'customTag_dictText',
   },
 ];
 

+ 136 - 113
jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnelList.vue

@@ -41,8 +41,12 @@
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
-            <a-form-item label="年龄" name="age">
-              <a-input-number placeholder="请输入年龄" v-model:value="queryParam.age" :min="0" :max="150" allow-clear style="width: 100%" />
+            <a-form-item label="年龄" name="ageRange">
+              <a-space>
+                <a-input-number v-model:value="queryParam.ageBegin" :max="200" :min="0" placeholder="请输入起始年龄" style="width: 105px" />
+                <span>~</span>
+                <a-input-number v-model:value="queryParam.ageEnd" :max="200" :min="0" placeholder="请输入结束年龄" style="width: 105px" />
+              </a-space>
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
@@ -57,7 +61,9 @@
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="自定义标签" name="customTag">
-              <a-input placeholder="请输入自定义标签" v-model:value="queryParam.customTag" allow-clear></a-input>
+              <a-select placeholder="请选择自定义标签" v-model:value="queryParam.customTag" allow-clear>
+                <a-select-option v-for="tag in customTagOptions" :key="tag" :value="tag">{{ tag }}</a-select-option>
+              </a-select>
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
@@ -86,53 +92,80 @@
     <BasicTable @register="registerTable" :rowSelection="rowSelection">
       <!--插槽:table标题-->
       <template #tableTitle>
-        <a-button v-auth="'focus_personnel:refresh'" preIcon="ant-design:sync-outlined" type="primary" @click="handleRefresh"> 刷新生成</a-button>
-        <a-button v-auth="'focus_personnel:add'" preIcon="ant-design:plus-outlined" type="primary" @click="handleAdd"> 新增</a-button>
-        <a-button v-auth="'focus_personnel:exportXls'" preIcon="ant-design:export-outlined" type="primary" @click="onExportXls"> 导出</a-button>
-        <a-dropdown v-if="selectedRowKeys.length > 0">
-          <template #overlay>
-            <a-menu>
-              <a-menu-item key="1" @click="batchHandleDelete">
-                <Icon icon="ant-design:delete-outlined"></Icon>
-                批量删除
-              </a-menu-item>
-              <a-menu-item key="2" @click="handleBatchMessage">
-                <Icon icon="ant-design:message-outlined"></Icon>
-                消息推送
-              </a-menu-item>
-            </a-menu>
-          </template>
-          <a-button v-auth="'focus_personnel:deleteBatch'"
-            >批量操作
-            <Icon icon="mdi:chevron-down"></Icon>
-          </a-button>
-        </a-dropdown>
+        <a-row type="flex" justify="space-between" style="width: 100%">
+          <a-col>
+            <a-button v-auth="'focus_personnel:refresh'" preIcon="ant-design:sync-outlined" type="primary" @click="handleRefresh"> 刷新生成</a-button>
+          </a-col>
+          <a-col>
+            <a-button preIcon="ant-design:tags-outlined" @click="handleCustomTag"> 自定义标签</a-button>
+            <a-button preIcon="ant-design:message-outlined" style="margin-left: 8px" @click="handleMessagePush"> 推送消息</a-button>
+            <a-button v-auth="'focus_personnel:exportXls'" preIcon="ant-design:export-outlined" type="primary" style="margin-left: 8px" @click="onExportXls"> 导出</a-button>
+          </a-col>
+        </a-row>
       </template>
       <!--操作栏-->
       <template #action="{ record }">
-        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
+        <TableAction :actions="getTableAction(record)" />
       </template>
     </BasicTable>
     <!-- 表单区域 -->
     <FocusPersonnelModal ref="registerModal" @success="handleSuccess"></FocusPersonnelModal>
+    <!-- 自定义标签管理弹窗 -->
+    <a-modal
+      :visible="customTagVisible"
+      title="自定义标签管理"
+      @cancel="customTagVisible = false"
+      :footer="null"
+      width="500px"
+    >
+      <div style="margin-bottom: 12px">
+        <a-input-search
+          v-model:value="newTagName"
+          placeholder="输入新标签名称后添加"
+          enter-button="添加"
+          @search="handleAddCustomTag"
+        />
+      </div>
+      <a-list :dataSource="customTagOptions" size="small" bordered>
+        <template #renderItem="{ item }">
+          <a-list-item>
+            <a-tag color="orange" style="margin-right: 8px">{{ item }}</a-tag>
+            <template #actions>
+              <a-popconfirm title="确认删除此标签?" @confirm="handleDeleteCustomTag(item)">
+                <a-button type="link" danger size="small">删除</a-button>
+              </a-popconfirm>
+            </template>
+          </a-list-item>
+        </template>
+        <template v-if="customTagOptions.length === 0">
+          <a-list-empty />
+        </template>
+      </a-list>
+    </a-modal>
   </div>
 </template>
 
 <script lang="ts" name="focusPersonnel" setup>
-  import { reactive, ref } from 'vue';
+  import { reactive, ref, onMounted } from 'vue';
   import { BasicTable, TableAction } from '/@/components/Table';
   import { useListPage } from '/@/hooks/system/useListPage';
   import { columns } from './FocusPersonnel.data';
-  import { batchDelete, deleteOne, getExportUrl, getImportUrl, list } from './FocusPersonnel.api';
+  import { getExportUrl, getImportUrl, list, listCustomTags, addCustomTagItem, deleteCustomTagItem } from './FocusPersonnel.api';
   import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
   import FocusPersonnelModal from './components/FocusPersonnelModal.vue';
   import { useMessage } from '/@/hooks/web/useMessage';
+  import { initDictOptions } from '/@/utils/dict/index';
 
   const formRef = ref();
   const queryParam = reactive<any>({});
   const registerModal = ref();
   const { createMessage, createConfirm } = useMessage();
 
+  // 自定义标签相关
+  const customTagOptions = ref<string[]>([]);
+  const customTagVisible = ref<boolean>(false);
+  const newTagName = ref<string>('');
+
   //注册table数据
   const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
     tableProps: {
@@ -142,7 +175,7 @@
       canResize: true,
       useSearchForm: false,
       actionColumn: {
-        width: 200,
+        width: 300,
         fixed: 'right',
       },
       beforeFetch: async (params) => {
@@ -172,27 +205,11 @@
     sm: 18,
   });
 
-  /**
-   * 新增事件
-   */
-  function handleAdd() {
-    registerModal.value.disableSubmit = false;
-    registerModal.value.add();
-  }
-
-  /**
-   * 编辑事件
-   */
-  function handleEdit(record: Recordable) {
-    registerModal.value.disableSubmit = false;
-    registerModal.value.edit(record);
-  }
-
   /**
    * 详情
    */
   function handleDetail(record: Recordable) {
-    registerModal.value.detail(record);
+    registerModal.value?.detail(record);
   }
 
   /**
@@ -210,105 +227,108 @@
     });
   }
 
-  /**
-   * 批量消息推送
-   */
-  function handleBatchMessage() {
-    createMessage.warning('消息推送功能待开发');
-  }
-
-  /**
-   * 删除事件
-   */
-  async function handleDelete(record) {
-    await deleteOne({ id: record.id }, handleSuccess);
-  }
-
-  /**
-   * 批量删除事件
-   */
-  async function batchHandleDelete() {
-    await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
-  }
-
   /**
    * 成功回调
    */
   function handleSuccess() {
-    (selectedRowKeys.value = []) && reload();
+    reload();
   }
 
   /**
-   * 操作栏
+   * 操作栏 - 直接显示三个按钮
    */
   function getTableAction(record) {
     return [
       {
-        label: '编辑',
-        onClick: handleEdit.bind(null, record),
-        auth: 'focus_personnel:edit',
-      },
-    ];
-  }
-
-  /**
-   * 下拉操作栏
-   */
-  function getDropDownAction(record) {
-    return [
-      {
-        label: '详情',
+        label: '查看',
         onClick: handleDetail.bind(null, record),
       },
-      {
-        label: '服务跟进',
-        onClick: handleServiceFollow.bind(null, record),
-        auth: 'focus_personnel:serviceFollow',
-      },
       {
         label: '岗位推送',
         onClick: handleJobPush.bind(null, record),
-        auth: 'focus_personnel:jobPush',
-      },
-      {
-        label: '消息推送',
-        onClick: handleSingleMessage.bind(null, record),
-        auth: 'focus_personnel:messagePush',
       },
       {
-        label: '自定义标签',
-        onClick: handleCustomTag.bind(null, record),
-        auth: 'focus_personnel:customTag',
-      },
-      {
-        label: '删除',
-        popConfirm: {
-          title: '是否确认删除',
-          confirm: handleDelete.bind(null, record),
-          placement: 'topLeft',
-        },
-        auth: 'focus_personnel:delete',
+        label: '服务跟进',
+        onClick: handleServiceFollow.bind(null, record),
       },
     ];
   }
 
-  /** 服务跟进占位 */
+  /** 服务跟进占位(待开发) */
   function handleServiceFollow(record) {
     createMessage.warning('服务跟进功能待开发');
   }
-  /** 岗位推送占位 */
+  /** 岗位推送占位(待开发) */
   function handleJobPush(record) {
     createMessage.warning('岗位推送功能待开发');
   }
-  /** 单条消息推送占位 */
-  function handleSingleMessage(record) {
-    createMessage.warning('消息推送功能待开发');
+
+  /** 加载自定义标签选项(从后端获取所有标签) */
+  async function loadCustomTagOptions() {
+    try {
+      const res = await listCustomTags();
+      if (Array.isArray(res)) {
+        customTagOptions.value = res;
+      }
+    } catch (e) {
+      console.error('加载自定义标签失败', e);
+    }
+  }
+
+  /** 打开自定义标签管理弹窗 */
+  function handleCustomTag() {
+    loadCustomTagOptions();
+    customTagVisible.value = true;
   }
-  /** 自定义标签占位 */
-  function handleCustomTag(record) {
-    createMessage.warning('自定义标签功能待开发');
+
+  /** 推送消息(待开发) */
+  function handleMessagePush() {
+    createMessage.warning('推送消息功能待开发');
   }
 
+  /** 添加新自定义标签(调用后端持久化到字典) */
+  async function handleAddCustomTag() {
+    const tagName = newTagName.value?.trim();
+    if (!tagName) {
+      createMessage.warning('请输入标签名称');
+      return;
+    }
+    if (customTagOptions.value.includes(tagName)) {
+      createMessage.warning('该标签已存在');
+      return;
+    }
+    try {
+      await addCustomTagItem(tagName);
+      customTagOptions.value.push(tagName);
+      customTagOptions.value.sort();
+      newTagName.value = '';
+      createMessage.success(`已添加自定义标签: ${tagName}`);
+    } catch (e) {
+      createMessage.error('添加失败,请重试');
+    }
+  }
+
+  /** 删除自定义标签字典项 */
+  async function handleDeleteCustomTag(itemValue) {
+    try {
+      await deleteCustomTagItem(itemValue);
+      customTagOptions.value = customTagOptions.value.filter(tag => tag !== itemValue);
+      createMessage.success(`已删除自定义标签: ${itemValue}`);
+    } catch (e) {
+      // 显示后端返回的具体错误信息(如"该标签已被用户绑定,无法删除")
+      createMessage.error(e.message || '删除失败,请重试');
+    }
+  }
+
+  // 页面加载时获取自定义标签选项并预加载字典
+  onMounted(() => {
+    loadCustomTagOptions();
+    // 预加载字典(确保列表列翻译生效)
+    initDictOptions('focus_major_tag').catch(() => {});
+    initDictOptions('focus_minor_tag').catch(() => {});
+    initDictOptions('focus_custom_tag').catch(() => {});
+  });
+
   /**
    * 查询
    */
@@ -321,6 +341,9 @@
    */
   function searchReset() {
     formRef.value.resetFields();
+    // 手动清空年龄范围(不在表单管控范围内)
+    queryParam.ageBegin = undefined;
+    queryParam.ageEnd = undefined;
     selectedRowKeys.value = [];
     reload();
   }

+ 110 - 41
jeecgboot-vue3/src/views/focuspersonnel/components/FocusPersonnelDetail.vue

@@ -15,8 +15,8 @@
             <span class="age" v-if="detailData.age != null">{{ detailData.age }}岁</span>
           </div>
           <div class="header-tags">
-            <a-tag v-if="detailData.majorTag" color="blue">{{ detailData.majorTag }}</a-tag>
-            <a-tag v-if="detailData.minorTag" color="green">{{ detailData.minorTag }}</a-tag>
+            <a-tag v-if="detailData.majorTag" color="blue">{{ getDictText('focus_major_tag', detailData.majorTag) }}</a-tag>
+            <a-tag v-if="detailData.minorTag" color="green">{{ getDictText('focus_minor_tag', detailData.minorTag) }}</a-tag>
             <template v-if="detailData.customTag">
               <a-tag v-for="tag in customTagList" :key="tag" color="orange">{{ tag }}</a-tag>
             </template>
@@ -81,38 +81,40 @@
         <a-tab-pane key="tags" tab="标签信息">
           <a-descriptions bordered :column="1" size="small">
             <a-descriptions-item label="人员大类标签">
-              <a-tag v-if="detailData.majorTag" color="blue">{{ detailData.majorTag }}</a-tag>
+              <a-tag v-if="detailData.majorTag" color="blue">{{ getDictText('focus_major_tag', detailData.majorTag) }}</a-tag>
               <span v-else>-</span>
             </a-descriptions-item>
             <a-descriptions-item label="人员小类标签">
-              <a-tag v-if="detailData.minorTag" color="green">{{ detailData.minorTag }}</a-tag>
+              <a-tag v-if="detailData.minorTag" color="green">{{ getDictText('focus_minor_tag', detailData.minorTag) }}</a-tag>
               <span v-else>-</span>
             </a-descriptions-item>
             <a-descriptions-item label="自定义标签">
-              <template v-if="customTagList.length > 0">
-                <a-tag v-for="tag in customTagList" :key="tag" color="orange" style="margin-bottom: 4px">{{ tag }}</a-tag>
+              <template v-if="!editingCustomTag">
+                <template v-if="customTagList.length > 0">
+                  <a-tag v-for="tag in customTagList" :key="tag" color="orange" style="margin-bottom: 4px">{{ tag }}</a-tag>
+                </template>
+                <span v-else>-</span>
+                <a-button type="link" size="small" style="margin-left: 8px" @click="startEditCustomTag">编辑</a-button>
+              </template>
+              <template v-else>
+                <div>
+                  <a-select
+                    v-model:value="editingCustomTagList"
+                    mode="multiple"
+                    placeholder="请选择自定义标签"
+                    style="width: 100%; max-width: 400px"
+                    :options="customTagDictOptions"
+                    allow-clear
+                  />
+                </div>
+                <div style="margin-top: 8px">
+                  <a-button type="primary" size="small" :loading="saving" @click="saveCustomTags">保存</a-button>
+                  <a-button size="small" style="margin-left: 8px" @click="cancelEditCustomTag">取消</a-button>
+                </div>
               </template>
-              <span v-else>-</span>
             </a-descriptions-item>
           </a-descriptions>
         </a-tab-pane>
-
-        <!-- Tab5: 服务跟进 -->
-        <a-tab-pane key="service" tab="服务跟进">
-          <div style="margin-bottom: 12px">
-            <a-button type="primary" size="small" @click="handleAddServiceFollow" v-auth="'focus_personnel:serviceFollow'">
-              新增跟进
-            </a-button>
-          </div>
-          <a-table
-            :columns="serviceFollowColumns"
-            :dataSource="serviceFollowList"
-            :pagination="false"
-            size="small"
-            :locale="{ emptyText: '暂无跟进记录' }"
-            rowKey="id"
-          />
-        </a-tab-pane>
       </a-tabs>
     </div>
     <div v-else-if="!loading" style="text-align: center; padding: 40px; color: #999">
@@ -123,27 +125,70 @@
 
 <script lang="ts" setup>
   import { computed, ref, watch } from 'vue';
-  import { queryDetailById } from '../FocusPersonnel.api';
+  import { queryDetailById, saveOrUpdate, listCustomTags } from '../FocusPersonnel.api';
+  import { initDictOptions, ajaxGetDictItems } from '/@/utils/dict/index';
   import dayjs from 'dayjs';
+  import { useMessage } from '/@/hooks/web/useMessage';
+
+  const { createMessage } = useMessage();
 
   const props = defineProps({
     record: { type: Object, default: () => ({}) },
   });
 
   const loading = ref<boolean>(false);
+  const saving = ref<boolean>(false);
   const detailData = ref<any>(null);
   const activeTab = ref<string>('basic');
 
-  /** 服务跟进列表(暂为空,待后端接口开发) */
-  const serviceFollowList = ref<any[]>([]);
+  // 字典数据(key=dictCode, value=Map<item_value, item_text>)
+  const dictMap = ref<Record<string, Record<string, string>>>({});
+
+  // 自定义标签字典选项(用于下拉多选)
+  const customTagDictOptions = ref<any[]>([]);
+
+  // 自定义标签编辑相关
+  const editingCustomTag = ref<boolean>(false);
+  const editingCustomTagList = ref<string[]>([]);
+
+  /** 加载字典数据,构建查询映射 */
+  async function loadDictData() {
+    try {
+      // 加载自定义标签字典选项
+      const tags = await listCustomTags();
+      if (Array.isArray(tags)) {
+        customTagDictOptions.value = tags.map(tag => ({ label: tag, value: tag }));
+      }
+
+      // 加载大类标签字典(使用 ajaxGetDictItems 绕过前端缓存,确保获取最新字典数据)
+      const majorDict = await ajaxGetDictItems('focus_major_tag');
+      if (Array.isArray(majorDict)) {
+        const map: Record<string, string> = {};
+        majorDict.forEach(item => { map[item.value] = item.text || item.title || item.label; });
+        dictMap.value['focus_major_tag'] = map;
+      }
+
+      // 加载小类标签字典(使用 ajaxGetDictItems 绕过前端缓存)
+      const minorDict = await ajaxGetDictItems('focus_minor_tag');
+      if (Array.isArray(minorDict)) {
+        const map: Record<string, string> = {};
+        minorDict.forEach(item => { map[item.value] = item.text || item.title || item.label; });
+        dictMap.value['focus_minor_tag'] = map;
+      }
+    } catch (e) {
+      console.error('加载字典数据失败', e);
+    }
+  }
 
-  /** 服务跟进表格列 */
-  const serviceFollowColumns = [
-    { title: '姓名', dataIndex: 'fullName', width: 100 },
-    { title: '服务内容', dataIndex: 'serviceContent' },
-    { title: '服务时间', dataIndex: 'serviceTime', width: 120 },
-    { title: '服务人员', dataIndex: 'servicePerson', width: 100 },
-  ];
+  /** 翻译字典值 */
+  function getDictText(dictCode: string, value: string): string {
+    if (!value) return '-';
+    const map = dictMap.value[dictCode];
+    if (map && map[value]) {
+      return map[value];
+    }
+    return value;
+  }
 
   /** 自定义标签列表(逗号分隔转数组) */
   const customTagList = computed(() => {
@@ -157,21 +202,44 @@
     return dayjs(date).format('YYYY-MM-DD');
   }
 
-  /** 新增服务跟进(占位) */
-  function handleAddServiceFollow() {
-    // 待开发:打开服务跟进表单弹窗
+  /** 开始编辑自定义标签:将当前标签列表复制到编辑区 */
+  function startEditCustomTag() {
+    editingCustomTagList.value = [...customTagList.value];
+    editingCustomTag.value = true;
+  }
+
+  /** 取消编辑 */
+  function cancelEditCustomTag() {
+    editingCustomTag.value = false;
+    editingCustomTagList.value = [];
+  }
+
+  /** 保存自定义标签 */
+  async function saveCustomTags() {
+    const id = detailData.value?.id;
+    if (!id) return;
+    saving.value = true;
+    try {
+      const customTagStr = editingCustomTagList.value.join(',');
+      await saveOrUpdate({ id, customTag: customTagStr }, true);
+      detailData.value.customTag = customTagStr;
+      editingCustomTag.value = false;
+      createMessage.success('自定义标签保存成功');
+    } catch (e) {
+      console.error('保存自定义标签失败', e);
+      createMessage.error('保存失败');
+    } finally {
+      saving.value = false;
+    }
   }
 
-  /**
-   * 加载详情数据
-   */
+  /** 加载详情数据 */
   async function loadDetail() {
     if (!props.record.id) return;
     loading.value = true;
     activeTab.value = 'basic';
     try {
       const res = await queryDetailById({ id: props.record.id });
-      // defHttp默认isTransformResponse=true,返回的res已经是result数据
       if (res) {
         detailData.value = res;
       }
@@ -187,6 +255,7 @@
     () => props.record,
     () => {
       loadDetail();
+      loadDictData();
     },
     { immediate: true, deep: true }
   );

+ 3 - 3
jeecgboot-vue3/src/views/focuspersonnel/components/FocusPersonnelModal.vue

@@ -46,7 +46,7 @@
     title.value = '新增重点关注人员';
     visible.value = true;
     nextTick(() => {
-      registerForm.value.add();
+      registerForm.value?.add();
     });
   }
 
@@ -59,7 +59,7 @@
     visible.value = true;
     currentRecord.value = record;
     nextTick(() => {
-      registerForm.value.edit(record);
+      registerForm.value?.edit(record);
     });
   }
 
@@ -81,7 +81,7 @@
       handleCancel();
       return;
     }
-    registerForm.value.submitForm();
+    registerForm.value?.submitForm();
   }
 
   /**