Ver Fonte

就业援助、政策推送和岗位信息变动推送、就业状态自动感知和信息智能推送模块第一版

kk há 5 dias atrás
pai
commit
2d24359d8e
36 ficheiros alterados com 1786 adições e 490 exclusões
  1. 6 6
      .docs/sql/新增模块测试数据.sql
  2. 12 0
      .docs/开发记录_见习岗位与见习人员管理模块.md
  3. 109 0
      .docs/模块开发说明-就业状态自动感知.md
  4. 6 0
      jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisPlusSaasConfig.java
  5. 213 150
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/careerguidancedocument/controller/CareerGuidanceDocumentController.java
  6. 8 1
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/dictionary/service/impl/DictionaryItemServiceImpl.java
  7. 2 0
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprisestatus/controller/EnterpriseStatusController.java
  8. 4 1
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprisestatus/mapper/xml/EnterpriseStatusLocalMapper.xml
  9. 4 2
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personalstatus/controller/PersonalStatusController.java
  10. 6 3
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personalstatus/mapper/xml/PersonalStatusLocalMapper.xml
  11. 94 3
      jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/policyfile/controller/PolicyFileController.java
  12. 9 5
      jeecgboot-vue3/src/hooks/dictionary/useDict.ts
  13. 2 0
      jeecgboot-vue3/src/views/careerguidancedocument/CareerGuidanceDocument.api.ts
  14. 65 64
      jeecgboot-vue3/src/views/careerguidancedocument/CareerGuidanceDocumentList.vue
  15. 1 1
      jeecgboot-vue3/src/views/employmentassistance/EmploymentAssistanceList.vue
  16. 2 2
      jeecgboot-vue3/src/views/enterprisestatus/EnterpriseStatus.api.ts
  17. 212 8
      jeecgboot-vue3/src/views/enterprisestatus/EnterpriseStatusList.vue
  18. 1 1
      jeecgboot-vue3/src/views/enterprisestatus/components/EnterpriseStatusDetail.vue
  19. 1 0
      jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnel.data.ts
  20. 184 29
      jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnelList.vue
  21. 29 12
      jeecgboot-vue3/src/views/focuspersonnel/components/FocusPersonnelDetail.vue
  22. 1 0
      jeecgboot-vue3/src/views/internshippersonnel/InternshipPersonnel.data.ts
  23. 210 31
      jeecgboot-vue3/src/views/internshippersonnel/InternshipPersonnelList.vue
  24. 11 3
      jeecgboot-vue3/src/views/internshippersonnel/components/InternshipPersonnelDetail.vue
  25. 0 9
      jeecgboot-vue3/src/views/internshippost/InternshipPost.data.ts
  26. 91 33
      jeecgboot-vue3/src/views/internshippost/InternshipPostList.vue
  27. 0 7
      jeecgboot-vue3/src/views/jobrecommend/JobRecommend.data.ts
  28. 24 31
      jeecgboot-vue3/src/views/jobrecommend/JobRecommendList.vue
  29. 2 2
      jeecgboot-vue3/src/views/personalstatus/PersonalStatus.api.ts
  30. 211 7
      jeecgboot-vue3/src/views/personalstatus/PersonalStatusList.vue
  31. 12 0
      jeecgboot-vue3/src/views/policyfile/PolicyFile.api.ts
  32. 79 34
      jeecgboot-vue3/src/views/policyfile/PolicyFileList.vue
  33. 2 2
      jeecgboot-vue3/src/views/profilechange/ProfileChange.api.ts
  34. 149 3
      jeecgboot-vue3/src/views/profilechange/ProfileChangeList.vue
  35. 0 8
      jeecgboot-vue3/src/views/welfarepost/WelfarePost.data.ts
  36. 24 32
      jeecgboot-vue3/src/views/welfarepost/WelfarePostList.vue

Diff do ficheiro suprimidas por serem muito extensas
+ 6 - 6
.docs/sql/新增模块测试数据.sql


+ 12 - 0
.docs/开发记录_见习岗位与见习人员管理模块.md

@@ -489,3 +489,15 @@ List<String> codeList = metaList.stream()
 - 公益性岗位管理:导出按钮补 `v-auth="welfare_post:exportXls"`
 - 岗位推荐:导出按钮补 `v-auth="job_recommend:exportXls"`
 - 重点关注人员:户口所在地/现居住地加XZQH字典翻译,性别改用字典
+
+## 八、2026-06-11 修复 — 见习岗位/公益性岗位导出按钮不显示
+
+### 问题现象
+信息智能匹配推送 → 见习岗位管理、公益性岗位管理 — 导出按钮不显示
+
+### 根本原因
+后端 `SysPermissionController` 构建权限码列表(`codeList`)时过滤条件为 `status = '1'`,但 Flyway 脚本 `V20260603_5` 和 `V20260603_7` 插入按钮权限时 `status` 字段值为 `NULL`,导致后端不返回这些权限码。前端 `v-auth` 指令检查不到权限则从 DOM 中移除按钮。
+
+### 修复方案
+1. 执行 `.docs/sql/修复见习岗位和公益性岗位导出按钮权限.sql`
+2. **重新登录系统**(权限数据修改后需重新登录让后端重新加载权限码列表)

+ 109 - 0
.docs/模块开发说明-就业状态自动感知.md

@@ -240,3 +240,112 @@ LEFT JOIN (SELECT PERSONAL_ID, COUNT(*) AS UNEMPLOY_COUNT FROM UNEMPLOYMENT_RECO
 - PERSONAL_INFO:20人,全部使用数字编码,覆盖湛江8区县
 - ENTERPRISE_INFO:10家,含对应标签
 - 旧人员数据(200-209)更新为湛江区县 + 多样化性别/学历/求职状态
+
+## 十、2026-06-11 修复与优化记录
+
+### 搜索表单优化
+**企业就业状态感知:**
+- 所属区县 → 树形下拉(XZQH 懒加载,程序化定位湛江市区县)
+- 所属行业 → `<a-select>` 下拉(EconomicIndustry 字典,value 使用 label 文本匹配 DB)
+- 企业状态 → `<a-select>` 下拉(BusinessStatus 字典,与企业基本信息模块经营状态保持一致)
+- 表格列 `enterpriseStatus` 去掉 `|| text` 回退(字典未匹配时显示空白)
+- 新增"统一社会信用代码"搜索字段(模糊匹配)
+- 行业搜索从 `=` 改为 `LIKE` 模糊匹配
+
+**个人就业状态感知:**
+- 户口所属区县 → 树形下拉(XZQH 懒加载,程序化定位湛江市区县)
+- 年龄 → 改为范围搜索(`ageBegin ~ ageEnd`,后端同步改造)
+- 就业状态 → `<a-select>` 下拉(employment_status 字典,value 使用 label 文本匹配)
+- `householdDistrict` 搜索从 `=` 改为 `LIKE` 模糊匹配
+
+### 自定义标签编辑
+- `EnterpriseStatusList.vue` / `PersonalStatusList.vue` — "修改标签"按钮弹出模态框,使用 `<a-textarea>` 编辑逗号分隔标签
+- 通过 `saveTag`/`getTag` API 读写 `enterprise_status_local`/`personal_status_local` 表的 `custom_tags` 字段
+
+### 发送消息功能
+**共用消息表:** `notification_record`,支持多模块复用
+
+| 字段 | 说明 |
+|------|------|
+| MODULE_TYPE | 模块类型(enterprise_status/personal_status/profile_change 等) |
+| TARGET_ID | 目标ID(企业ID或个人ID) |
+| TARGET_NAME | 目标名称(冗余展示) |
+| SUBJECT | 消息主题 |
+| CONTENT | 消息内容 |
+| SENDER | 推送人(自动绑定当前登录用户) |
+| SEND_TIME | 推送时间(自动记录) |
+
+**后端新增:**
+- `notification/entity/NotificationRecord.java` — 消息记录实体
+- `notification/mapper/NotificationRecordMapper.java` + XML
+- `notification/service/INotificationRecordService.java` + Impl
+- `notification/controller/NotificationController.java` — 统一接口:
+  - `POST /notification/send` — 发送消息(写记录 + 更新 local 表 last_notice_time)
+  - `GET /notification/list` — 分页查询消息记录
+  - `GET /notification/myList` — 查询当前用户发送的消息(支持关键字/模块/日期筛选)
+
+**前端新增:**
+- `notification/SentMessagesList.vue` — 已发送消息列表页(搜索:关键字、来源模块、推送时间范围;详情弹窗)
+- `notification/SentMessages.data.ts` — 列定义
+- `notification/SentMessages.api.ts` — API
+
+**企业/个人就业状态感知:**
+- "发送消息"按钮弹出表单弹窗(接收对象、主题、内容、推送人、推送时间)
+- 调用 `/notification/send`,自动更新 `last_notice_time`
+
+**菜单:**
+- 新增"消息中心"顶级菜单(sort_no: 3.00, icon: message-outlined)
+- 下挂"已发送消息"子菜单(sort_no: 1.00, icon: send-outlined)
+
+**字典:**
+- `notification_module`(消息通知来源模块):企业就业状态感知、个人就业状态感知、画像变动管理、重点关注人员管理、就业援助、初步符合人员
+
+**数据库表:**
+- DM8:`notification_record`(含索引 idx_notice_target、idx_notice_time)
+- Flyway MySQL:`V20260611_1__create_notification_record.sql`、`V20260611_2__menu_insert_SentMessages.sql`
+
+### 架构优化:消息与接收人分离
+
+将原有 `notification_record` 重构为消息主表 + 接收人关联表:
+
+```
+notification_record          notification_target
+┌──────────────┐            ┌────────────────────┐
+│ id           │◄───1:N────│ notification_id    │
+│ module_type  │            │ target_type        │ (personal/enterprise)
+│ subject      │            │ target_id          │
+│ content      │            └────────────────────┘
+│ sender       │
+│ send_time    │
+└──────────────┘
+```
+
+- 批量发送:1 条 record + N 条 target,subject/content 不重复
+- `target_name` 不存储,查询时从 `PERSONAL_INFO`/`ENTERPRISE_INFO` 批量关联获取 `resolveTargetNames()`
+- 统一 `/notification/send` 端点,接收 `targets: [{targetType, targetId}]` 数组,单条传 1 个元素,批量传多个
+- `NotificationTarget` 实体(mapper: `NotificationTargetMapper`)
+- `NotificationTarget.targetName` 为 `@TableField(exist = false)` 瞬态字段
+
+### 画像变动管理 同步实现
+- `ProfileChangeList.vue` — "修改标签"和"发送消息"功能实现,与个人就业状态感知共用 `personal_status_local` 表
+- `ProfileChange.api.ts` — `saveTag` 修复为 `data` 请求体
+
+### 模块类型字典
+- `DICTIONARY` 新增 `notification_module`(消息通知来源模块)
+- `DICTIONARY_ITEM` 写入 6 项:enterprise_status / personal_status / profile_change / focus_personnel / employment_assistance / preliminary_eligible
+- 前端 `SentMessagesList.vue` 通过 `useDict('notification_module')` 加载,`moduleTypeMap` 动态映射 code→label
+
+### 搜索条件优化补充
+- 个人就业状态感知 `householdDistrict` 改用 XZQH 树形下拉(程序化定位湛江市区县)
+- `PersonalStatusLocalMapper.xml` — `household_district` 从 `=` 改为 `LIKE`
+- `PersonalStatusController.java` — 年龄从单值 `age` 改为范围 `ageBegin`/`ageEnd`
+
+### 批量消息推送(信息智能匹配推送)
+- `FocusPersonnelList.vue` / `InternshipPersonnelList.vue` — "推送消息"按钮替换为模态框(显示已选N条,富文本内容,自动推送人/时间)
+- 勾选多条 → 从 `getDataSource()` 提取 `personalId` → 调用 `/notification/send`,`targets` 数组批量发送
+- `FocusPersonnel.data.ts` / `InternshipPersonnel.data.ts` — 新增隐藏列 `personalId`(`ifShow: false`)确保数据可用
+- `DICTIONARY_ITEM` 补充 `internship_personnel` 字典项
+
+### 表结构优化
+- `notification_record` 新增 `SENDER_ID VARCHAR(36)`(关联 `sys_user.id`),`sender` 存姓名冗余展示
+- `create_by` 为 JeecgBoot 标准字段(存 `sys_user.username`)

+ 6 - 0
jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisPlusSaasConfig.java

@@ -142,6 +142,12 @@ public class MybatisPlusSaasConfig {
         if (dbType!=null && (dbType == DbType.SQL_SERVER || dbType == DbType.SQL_SERVER2005)) {
             // 如果是SQL Server则覆盖为2005分页方式
             interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQL_SERVER2005));
+        } else if (dbType == DbType.DM) {
+            //update-begin---author:kk ---date:2026-06-11  for:达梦8支持LIMIT语法,使用MYSQL方言避免ROWNUM嵌套查询语法错误---
+            // 达梦8原生支持 LIMIT m,n 和 LIMIT n OFFSET m 语法,
+            // 使用MYSQL方言生成 LIMIT m,n 分页SQL,避免Oracle风格的ROWNUM嵌套子查询在达梦中报-2111语法错误
+            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+            //update-end---author:kk ---date:2026-06-11  for:达梦8支持LIMIT语法,使用MYSQL方言避免ROWNUM嵌套查询语法错误---
         } else {
             interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
         }

+ 213 - 150
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/careerguidancedocument/controller/CareerGuidanceDocumentController.java

@@ -1,18 +1,18 @@
 package org.jeecg.modules.zjrs.careerguidancedocument.controller;
 
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
 import java.net.URLDecoder;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.jeecg.common.api.vo.Result;
+import org.jeecg.config.JeecgBaseConfig;
 import org.jeecg.common.system.query.QueryGenerator;
 import org.jeecg.common.util.oConvertUtils;
+import org.jeecg.modules.zjrs.careerguidancedocument.entity.CareerGuidanceCategory;
 import org.jeecg.modules.zjrs.careerguidancedocument.entity.CareerGuidanceDocument;
+import org.jeecg.modules.zjrs.careerguidancedocument.service.ICareerGuidanceCategoryService;
 import org.jeecg.modules.zjrs.careerguidancedocument.service.ICareerGuidanceDocumentService;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -50,150 +50,213 @@ import java.net.URLEncoder;
 @RequestMapping("/careerguidancedocument/careerGuidanceDocument")
 @Slf4j
 public class CareerGuidanceDocumentController extends JeecgController<CareerGuidanceDocument, ICareerGuidanceDocumentService> {
-	@Autowired
-	private ICareerGuidanceDocumentService careerGuidanceDocumentService;
-	
-	/**
-	 * 分页列表查询
-	 *
-	 * @param careerGuidanceDocument
-	 * @param pageNo
-	 * @param pageSize
-	 * @param req
-	 * @return
-	 */
-	@AutoLog(value = "职业指导文档-分页列表查询")
-	@Operation(summary="职业指导文档-分页列表查询", description="职业指导文档-分页列表查询")
-	@GetMapping(value = "/list")
-	public Result<IPage<CareerGuidanceDocument>> queryPageList(CareerGuidanceDocument careerGuidanceDocument,
-								   @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
-								   @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
-								   HttpServletRequest req) {
-		QueryWrapper<CareerGuidanceDocument> queryWrapper = QueryGenerator.initQueryWrapper(careerGuidanceDocument, req.getParameterMap());
-		Page<CareerGuidanceDocument> page = new Page<CareerGuidanceDocument>(pageNo, pageSize);
-		IPage<CareerGuidanceDocument> pageList = careerGuidanceDocumentService.page(page, queryWrapper);
-		return Result.OK(pageList);
-	}
-	
-	/**
-	 *   添加
-	 *
-	 * @param careerGuidanceDocument
-	 * @return
-	 */
-	@AutoLog(value = "职业指导文档-添加")
-	@Operation(summary="职业指导文档-添加", description="职业指导文档-添加")
-	@PostMapping(value = "/add")
-	public Result<String> add(@RequestBody CareerGuidanceDocument careerGuidanceDocument) {
-		careerGuidanceDocumentService.save(careerGuidanceDocument);
-		return Result.OK("添加成功!");
-	}
-	
-	/**
-	 *  编辑
-	 *
-	 * @param careerGuidanceDocument
-	 * @return
-	 */
-	@AutoLog(value = "职业指导文档-编辑")
-	@Operation(summary="职业指导文档-编辑", description="职业指导文档-编辑")
-	@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
-	public Result<String> edit(@RequestBody CareerGuidanceDocument careerGuidanceDocument) {
-		careerGuidanceDocumentService.updateById(careerGuidanceDocument);
-		return Result.OK("编辑成功!");
-	}
-	
-	/**
-	 *   通过id删除
-	 *
-	 * @param id
-	 * @return
-	 */
-	@AutoLog(value = "职业指导文档-通过id删除")
-	@Operation(summary="职业指导文档-通过id删除", description="职业指导文档-通过id删除")
-	@DeleteMapping(value = "/delete")
-	public Result<String> delete(@RequestParam(name="id",required=true) String id) {
-		careerGuidanceDocumentService.removeById(id);
-		return Result.OK("删除成功!");
-	}
-	
-	/**
-	 *  批量删除
-	 *
-	 * @param ids
-	 * @return
-	 */
-	@AutoLog(value = "职业指导文档-批量删除")
-	@Operation(summary="职业指导文档-批量删除", description="职业指导文档-批量删除")
-	@DeleteMapping(value = "/deleteBatch")
-	public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
-		this.careerGuidanceDocumentService.removeByIds(Arrays.asList(ids.split(",")));
-		return Result.OK("批量删除成功!");
-	}
-	
-	/**
-	 * 通过id查询
-	 *
-	 * @param id
-	 * @return
-	 */
-	@Operation(summary="职业指导文档-通过id查询", description="职业指导文档-通过id查询")
-	@GetMapping(value = "/queryById")
-	public Result<CareerGuidanceDocument> queryById(@RequestParam(name="id",required=true) String id) {
-		CareerGuidanceDocument careerGuidanceDocument = careerGuidanceDocumentService.getById(id);
-		if(careerGuidanceDocument==null) {
-			return Result.error("未找到对应数据");
+		@Autowired
+		private ICareerGuidanceDocumentService careerGuidanceDocumentService;
+
+		//update-begin---author:kk ---date:2026-06-11  for:【职业指导公开文件】注入目录service用于级联查询-----------
+		@Autowired
+		private ICareerGuidanceCategoryService careerGuidanceCategoryService;
+
+		@Autowired
+		private JeecgBaseConfig jeecgBaseConfig;
+		//update-end---author:kk ---date:2026-06-11  for:【职业指导公开文件】注入目录service用于级联查询-----------
+
+		/**
+		 * 分页列表查询
+		 *
+		 * @param careerGuidanceDocument
+		 * @param pageNo
+		 * @param pageSize
+		 * @param req
+		 * @return
+		 */
+		@AutoLog(value = "职业指导文档-分页列表查询")
+		@Operation(summary="职业指导文档-分页列表查询", description="职业指导文档-分页列表查询")
+		@GetMapping(value = "/list")
+		public Result<IPage<CareerGuidanceDocument>> queryPageList(CareerGuidanceDocument careerGuidanceDocument,
+									   @RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
+									   @RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
+									   HttpServletRequest req) {
+			//update-begin---author:kk ---date:2026-06-11  for:【职业指导公开文件】目录级联查询+文件名搜索-----------
+			QueryWrapper<CareerGuidanceDocument> queryWrapper = new QueryWrapper<>();
+			String categoryId = req.getParameter("categoryId");
+			if (categoryId != null && !categoryId.isEmpty()) {
+				Set<String> allIds = getAllChildCategoryIds(categoryId);
+				queryWrapper.in("category_id", allIds);
+			}
+			String name = req.getParameter("name");
+			if (name != null && !name.isEmpty()) {
+				queryWrapper.like("name", name);
+			}
+			queryWrapper.orderByAsc("sort_no");
+			//update-end---author:kk ---date:2026-06-11  for:【职业指导公开文件】目录级联查询+文件名搜索-----------
+			Page<CareerGuidanceDocument> page = new Page<CareerGuidanceDocument>(pageNo, pageSize);
+			IPage<CareerGuidanceDocument> pageList = careerGuidanceDocumentService.page(page, queryWrapper);
+			return Result.OK(pageList);
+		}
+
+		//update-begin---author:kk ---date:2026-06-11  for:【职业指导公开文件】递归获取所有子目录ID-----------
+		private Set<String> getAllChildCategoryIds(String parentId) {
+			Set<String> ids = new LinkedHashSet<>();
+			ids.add(parentId);
+			List<CareerGuidanceCategory> all = careerGuidanceCategoryService.list();
+			collectChildren(parentId, all, ids);
+			return ids;
+		}
+
+		private void collectChildren(String parentId, List<CareerGuidanceCategory> all, Set<String> result) {
+			for (CareerGuidanceCategory cat : all) {
+				if (parentId.equals(cat.getParentId())) {
+					if (result.add(cat.getId())) {
+						collectChildren(cat.getId(), all, result);
+					}
+				}
+			}
+		}
+		//update-end---author:kk ---date:2026-06-11  for:【职业指导公开文件】递归获取所有子目录ID-----------
+
+		/**
+		 *   添加
+		 *
+		 * @param careerGuidanceDocument
+		 * @return
+		 */
+		@AutoLog(value = "职业指导文档-添加")
+		@Operation(summary="职业指导文档-添加", description="职业指导文档-添加")
+		@PostMapping(value = "/add")
+		public Result<String> add(@RequestBody CareerGuidanceDocument careerGuidanceDocument) {
+			careerGuidanceDocumentService.save(careerGuidanceDocument);
+			return Result.OK("添加成功!");
+		}
+
+		/**
+		 *  编辑
+		 *
+		 * @param careerGuidanceDocument
+		 * @return
+		 */
+		@AutoLog(value = "职业指导文档-编辑")
+		@Operation(summary="职业指导文档-编辑", description="职业指导文档-编辑")
+		@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
+		public Result<String> edit(@RequestBody CareerGuidanceDocument careerGuidanceDocument) {
+			careerGuidanceDocumentService.updateById(careerGuidanceDocument);
+			return Result.OK("编辑成功!");
 		}
-		return Result.OK(careerGuidanceDocument);
-	}
-
-    /**
-    * 导出excel
-    *
-    * @param request
-    * @param careerGuidanceDocument
-    */
-    @RequestMapping(value = "/exportXls")
-    public ModelAndView exportXls(HttpServletRequest request, CareerGuidanceDocument careerGuidanceDocument) {
-        return super.exportXls(request, careerGuidanceDocument, CareerGuidanceDocument.class, "职业指导文档");
-    }
-
-    /**
-      * 通过excel导入数据
-    *
-    * @param request
-    * @param response
-    * @return
-    */
-    @RequestMapping(value = "/importExcel", method = RequestMethod.POST)
-    public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
-        return super.importExcel(request, response, CareerGuidanceDocument.class);
-    }
-
-    /**
-     * 下载文件
-     */
-    @Operation(summary = "职业指导文档-下载", description = "职业指导文档-下载")
-    @GetMapping(value = "/download")
-    public void download(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
-        CareerGuidanceDocument doc = careerGuidanceDocumentService.getById(id);
-        if (doc == null || doc.getFileUrl() == null) {
-            response.setStatus(404);
-            return;
-        }
-        try {
-            String fileUrl = doc.getFileUrl();
-            java.io.File file = new java.io.File(fileUrl);
-            if (!file.exists()) {
-                response.setStatus(404);
-                return;
-            }
-            response.setContentType("application/octet-stream");
-            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(doc.getName() + ".pdf", "UTF-8"));
-            org.springframework.util.FileCopyUtils.copy(new java.io.FileInputStream(file), response.getOutputStream());
-        } catch (Exception e) {
-            log.error("文件下载失败", e);
-            response.setStatus(500);
-        }
-    }
+
+		/**
+		 *   通过id删除
+		 *
+		 * @param id
+		 * @return
+		 */
+		@AutoLog(value = "职业指导文档-通过id删除")
+		@Operation(summary="职业指导文档-通过id删除", description="职业指导文档-通过id删除")
+		@DeleteMapping(value = "/delete")
+		public Result<String> delete(@RequestParam(name="id",required=true) String id) {
+			careerGuidanceDocumentService.removeById(id);
+			return Result.OK("删除成功!");
+		}
+
+		/**
+		 *  批量删除
+		 *
+		 * @param ids
+		 * @return
+		 */
+		@AutoLog(value = "职业指导文档-批量删除")
+		@Operation(summary="职业指导文档-批量删除", description="职业指导文档-批量删除")
+		@DeleteMapping(value = "/deleteBatch")
+		public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
+			this.careerGuidanceDocumentService.removeByIds(Arrays.asList(ids.split(",")));
+			return Result.OK("批量删除成功!");
+		}
+
+		/**
+		 * 通过id查询
+		 *
+		 * @param id
+		 * @return
+		 */
+		@Operation(summary="职业指导文档-通过id查询", description="职业指导文档-通过id查询")
+		@GetMapping(value = "/queryById")
+		public Result<CareerGuidanceDocument> queryById(@RequestParam(name="id",required=true) String id) {
+			CareerGuidanceDocument careerGuidanceDocument = careerGuidanceDocumentService.getById(id);
+			if(careerGuidanceDocument==null) {
+				return Result.error("未找到对应数据");
+			}
+			return Result.OK(careerGuidanceDocument);
+		}
+
+	    /**
+	    * 导出excel
+	    *
+	    * @param request
+	    * @param careerGuidanceDocument
+	    */
+	    @RequestMapping(value = "/exportXls")
+	    public ModelAndView exportXls(HttpServletRequest request, CareerGuidanceDocument careerGuidanceDocument) {
+	        return super.exportXls(request, careerGuidanceDocument, CareerGuidanceDocument.class, "职业指导文档");
+	    }
+
+	    /**
+	      * 通过excel导入数据
+	    *
+	    * @param request
+	    * @param response
+	    * @return
+	    */
+	    @RequestMapping(value = "/importExcel", method = RequestMethod.POST)
+	    public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
+	        return super.importExcel(request, response, CareerGuidanceDocument.class);
+	    }
+
+	    //update-begin---author:kk ---date:2026-06-11  for:【职业指导公开文件】文件在线查看-----------
+	    @Operation(summary = "职业指导文档-在线查看", description = "职业指导文档-在线查看")
+	    @GetMapping(value = "/view")
+	    public void view(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
+	        serveFile(id, response, true);
+	    }
+	    //update-end---author:kk ---date:2026-06-11  for:【职业指导公开文件】文件在线查看-----------
+
+	    /**
+	     * 下载文件
+	     */
+	    @Operation(summary = "职业指导文档-下载", description = "职业指导文档-下载")
+	    @GetMapping(value = "/download")
+	    public void download(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
+	        serveFile(id, response, false);
+	    }
+
+	    //update-begin---author:kk ---date:2026-06-11  for:【职业指导公开文件】统一文件读取,支持相对路径-----------
+	    private void serveFile(String id, HttpServletResponse response, boolean inline) {
+	        CareerGuidanceDocument doc = careerGuidanceDocumentService.getById(id);
+	        if (doc == null || doc.getFileUrl() == null) {
+	            response.setStatus(404);
+	            return;
+	        }
+	        try {
+	            String fileUrl = doc.getFileUrl();
+	            java.io.File f = new java.io.File(fileUrl);
+	            if (!f.isAbsolute()) {
+	                String uploadPath = jeecgBaseConfig.getPath().getUpload();
+	                f = new java.io.File(uploadPath, fileUrl);
+	            }
+	            if (!f.exists()) {
+	                response.setStatus(404);
+	                return;
+	            }
+	            if (inline) {
+	                response.setContentType("application/pdf");
+	                response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(doc.getName() + ".pdf", "UTF-8"));
+	            } else {
+	                response.setContentType("application/octet-stream");
+	                response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(doc.getName() + ".pdf", "UTF-8"));
+	            }
+	            org.springframework.util.FileCopyUtils.copy(new java.io.FileInputStream(f), response.getOutputStream());
+	        } catch (Exception e) {
+	            log.error("文件操作失败", e);
+	            response.setStatus(500);
+	        }
+	    }
+	    //update-end---author:kk ---date:2026-06-11  for:【职业指导公开文件】统一文件读取,支持相对路径-----------
 }

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

@@ -27,6 +27,7 @@ public class DictionaryItemServiceImpl extends ServiceImpl<DictionaryItemMapper,
                     Map<String, Object> map = new HashMap<>();
                     map.put("label", item.getName());
                     map.put("value", item.getValue());
+                    map.put("code", item.getCode());
                     return map;
                 })
                 .collect(Collectors.toList());
@@ -46,7 +47,13 @@ public class DictionaryItemServiceImpl extends ServiceImpl<DictionaryItemMapper,
                         DictionaryItem::getDictionaryCode,
                         LinkedHashMap::new,
                         Collectors.mapping(
-                                item -> Map.of("label", item.getName(), "value", item.getValue()),
+                                item -> {
+                                    Map<String, Object> map = new java.util.LinkedHashMap<>();
+                                    map.put("label", item.getName());
+                                    map.put("value", item.getValue());
+                                    map.put("code", item.getCode());
+                                    return map;
+                                },
                                 Collectors.toList()
                         )
                 ));

+ 2 - 0
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprisestatus/controller/EnterpriseStatusController.java

@@ -43,11 +43,13 @@ public class EnterpriseStatusController {
             HttpServletRequest req) {
         Map<String, String> params = new HashMap<>();
         String companyName = req.getParameter("companyName");
+        String unifiedCreditCode = req.getParameter("unifiedCreditCode");
         String district = req.getParameter("district");
         String industry = req.getParameter("industry");
         String enterpriseStatus = req.getParameter("enterpriseStatus");
         String customTags = req.getParameter("customTags");
         if (companyName != null && !companyName.isEmpty()) params.put("companyName", companyName);
+        if (unifiedCreditCode != null && !unifiedCreditCode.isEmpty()) params.put("unifiedCreditCode", unifiedCreditCode);
         if (district != null && !district.isEmpty()) params.put("district", district);
         if (industry != null && !industry.isEmpty()) params.put("industry", industry);
         if (enterpriseStatus != null && !enterpriseStatus.isEmpty()) params.put("enterpriseStatus", enterpriseStatus);

+ 4 - 1
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/enterprisestatus/mapper/xml/EnterpriseStatusLocalMapper.xml

@@ -9,11 +9,14 @@
             <if test="params.companyName != null and params.companyName != ''">
                 AND company_name LIKE CONCAT('%', #{params.companyName}, '%')
             </if>
+            <if test="params.unifiedCreditCode != null and params.unifiedCreditCode != ''">
+                AND unified_credit_code LIKE CONCAT('%', #{params.unifiedCreditCode}, '%')
+            </if>
             <if test="params.district != null and params.district != ''">
                 AND district = #{params.district}
             </if>
             <if test="params.industry != null and params.industry != ''">
-                AND industry = #{params.industry}
+                AND industry LIKE CONCAT('%', #{params.industry}, '%')
             </if>
             <if test="params.enterpriseStatus != null and params.enterpriseStatus != ''">
                 AND enterprise_status = #{params.enterpriseStatus}

+ 4 - 2
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personalstatus/controller/PersonalStatusController.java

@@ -45,14 +45,16 @@ public class PersonalStatusController {
         String householdDistrict = req.getParameter("householdDistrict");
         String education = req.getParameter("education");
         String gender = req.getParameter("gender");
-        String age = req.getParameter("age");
+        String ageBegin = req.getParameter("ageBegin");
+        String ageEnd = req.getParameter("ageEnd");
         String jobSearchStatus = req.getParameter("jobSearchStatus");
         String employmentStatus = req.getParameter("employmentStatus");
         if (fullName != null && !fullName.isEmpty()) params.put("fullName", fullName);
         if (householdDistrict != null && !householdDistrict.isEmpty()) params.put("householdDistrict", householdDistrict);
         if (education != null && !education.isEmpty()) params.put("education", education);
         if (gender != null && !gender.isEmpty()) params.put("gender", gender);
-        if (age != null && !age.isEmpty()) params.put("age", age);
+        if (ageBegin != null && !ageBegin.isEmpty()) params.put("ageBegin", ageBegin);
+        if (ageEnd != null && !ageEnd.isEmpty()) params.put("ageEnd", ageEnd);
         if (jobSearchStatus != null && !jobSearchStatus.isEmpty()) params.put("jobSearchStatus", jobSearchStatus);
         if (employmentStatus != null && !employmentStatus.isEmpty()) params.put("employmentStatus", employmentStatus);
         Page<PersonalStatusPageVo> page = new Page<>(pageNo, pageSize);

+ 6 - 3
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/personalstatus/mapper/xml/PersonalStatusLocalMapper.xml

@@ -10,7 +10,7 @@
                 AND full_name LIKE CONCAT('%', #{params.fullName}, '%')
             </if>
             <if test="params.householdDistrict != null and params.householdDistrict != ''">
-                AND household_district = #{params.householdDistrict}
+                AND household_district LIKE CONCAT('%', #{params.householdDistrict}, '%')
             </if>
             <if test="params.education != null and params.education != ''">
                 AND education = #{params.education}
@@ -21,8 +21,11 @@
             <if test="params.employmentStatus != null and params.employmentStatus != ''">
                 AND employment_status = #{params.employmentStatus}
             </if>
-            <if test="params.age != null and params.age != ''">
-                AND age = #{params.age}
+            <if test="params.ageBegin != null and params.ageBegin != ''">
+                AND age &gt;= #{params.ageBegin}
+            </if>
+            <if test="params.ageEnd != null and params.ageEnd != ''">
+                AND age &lt;= #{params.ageEnd}
             </if>
             <if test="params.jobSearchStatus != null and params.jobSearchStatus != ''">
                 AND job_search_status = #{params.jobSearchStatus}

+ 94 - 3
jeecg-boot/jeecg-boot-module/jeecg-module-zjrs/src/main/java/org/jeecg/modules/zjrs/policyfile/controller/PolicyFileController.java

@@ -7,14 +7,19 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
 import org.jeecg.common.api.vo.Result;
-import org.jeecg.common.system.query.QueryGenerator;
+import org.jeecg.config.JeecgBaseConfig;
+import org.jeecg.modules.zjrs.policyfile.entity.PolicyCategory;
 import org.jeecg.modules.zjrs.policyfile.entity.PolicyFile;
+import org.jeecg.modules.zjrs.policyfile.service.IPolicyCategoryService;
 import org.jeecg.modules.zjrs.policyfile.service.IPolicyFileService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import jakarta.servlet.http.HttpServletRequest;
-import java.util.Arrays;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.*;
+import java.net.URLEncoder;
+import java.util.*;
 
 @Slf4j
 @Tag(name = "政策文件管理")
@@ -25,18 +30,58 @@ public class PolicyFileController {
     @Autowired
     private IPolicyFileService policyFileService;
 
+    @Autowired
+    private IPolicyCategoryService policyCategoryService;
+
+    @Autowired
+    private JeecgBaseConfig jeecgBaseConfig;
+
     @Operation(summary = "分页查询")
     @GetMapping(value = "/list")
     public Result<IPage<PolicyFile>> queryPageList(PolicyFile policyFile,
+                                                    @RequestParam(name = "categoryId", required = false) String categoryId,
                                                     @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
                                                     @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
                                                     HttpServletRequest req) {
-        QueryWrapper<PolicyFile> queryWrapper = QueryGenerator.initQueryWrapper(policyFile, req.getParameterMap());
+        QueryWrapper<PolicyFile> queryWrapper = new QueryWrapper<>();
+        //update-begin---author:kk ---date:2026-06-11  for:【政策公开文件】目录级联查询+文件名搜索-----------
+        // 目录级联:选择一级目录时,包含该目录及其所有子目录的文件
+        if (categoryId != null && !categoryId.isEmpty()) {
+            Set<String> allIds = getAllChildCategoryIds(categoryId);
+            queryWrapper.in("category_id", allIds);
+        }
+        // 文件名模糊搜索
+        String name = req.getParameter("name");
+        if (name != null && !name.isEmpty()) {
+            queryWrapper.like("name", name);
+        }
+        queryWrapper.orderByAsc("sort_no");
+        //update-end---author:kk ---date:2026-06-11  for:【政策公开文件】目录级联查询+文件名搜索-----------
         Page<PolicyFile> page = new Page<>(pageNo, pageSize);
         IPage<PolicyFile> pageList = policyFileService.page(page, queryWrapper);
         return Result.OK(pageList);
     }
 
+    //update-begin---author:kk ---date:2026-06-11  for:【政策公开文件】递归获取所有子目录ID-----------
+    private Set<String> getAllChildCategoryIds(String parentId) {
+        Set<String> ids = new LinkedHashSet<>();
+        ids.add(parentId);
+        List<PolicyCategory> all = policyCategoryService.list();
+        collectChildren(parentId, all, ids);
+        return ids;
+    }
+
+    private void collectChildren(String parentId, List<PolicyCategory> all, Set<String> result) {
+        for (PolicyCategory cat : all) {
+            if (parentId.equals(cat.getParentId())) {
+                if (result.add(cat.getId())) {
+                    collectChildren(cat.getId(), all, result);
+                }
+            }
+        }
+    }
+    //update-end---author:kk ---date:2026-06-11  for:【政策公开文件】递归获取所有子目录ID-----------
+
     @Operation(summary = "添加")
     @PostMapping(value = "/add")
     public Result<String> add(@RequestBody PolicyFile policyFile) {
@@ -64,4 +109,50 @@ public class PolicyFileController {
         policyFileService.removeByIds(Arrays.asList(ids.split(",")));
         return Result.OK("批量删除成功!");
     }
+
+    //update-begin---author:kk ---date:2026-06-11  for:【政策公开文件】文件在线查看+下载-----------
+    @Operation(summary = "在线查看文件")
+    @GetMapping(value = "/view")
+    public void view(@RequestParam(name = "id") String id, HttpServletResponse response) {
+        serveFile(id, response, true);
+    }
+
+    @Operation(summary = "下载文件")
+    @GetMapping(value = "/download")
+    public void download(@RequestParam(name = "id") String id, HttpServletResponse response) {
+        serveFile(id, response, false);
+    }
+
+    private void serveFile(String id, HttpServletResponse response, boolean inline) {
+        PolicyFile file = policyFileService.getById(id);
+        if (file == null || file.getFileUrl() == null) {
+            response.setStatus(404);
+            return;
+        }
+        try {
+            String fileUrl = file.getFileUrl();
+            java.io.File f = new java.io.File(fileUrl);
+            // 相对路径则拼接上传根目录
+            if (!f.isAbsolute()) {
+                String uploadPath = jeecgBaseConfig.getPath().getUpload();
+                f = new java.io.File(uploadPath, fileUrl);
+            }
+            if (!f.exists()) {
+                response.setStatus(404);
+                return;
+            }
+            if (inline) {
+                response.setContentType("application/pdf");
+                response.setHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(file.getName() + ".pdf", "UTF-8"));
+            } else {
+                response.setContentType("application/octet-stream");
+                response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName() + ".pdf", "UTF-8"));
+            }
+            org.springframework.util.FileCopyUtils.copy(new java.io.FileInputStream(f), response.getOutputStream());
+        } catch (Exception e) {
+            log.error("文件操作失败", e);
+            response.setStatus(500);
+        }
+    }
+    //update-end---author:kk ---date:2026-06-11  for:【政策公开文件】文件在线查看+下载-----------
 }

+ 9 - 5
jeecgboot-vue3/src/hooks/dictionary/useDict.ts

@@ -38,10 +38,11 @@ export function useDict(dictCodes: string[]) {
           const items = result[code] || [];
           // 后端已返回 {label, value} 格式,直接存入缓存供 a-select 使用
           dictCache[code] = items.map((item) => ({
-            // 后端 DictionaryItem.value 为 Integer,PersonalInfo 实体中字典字段为 String
-            // 统一转为字符串以确保 a-select 的 v-model 能匹配上选项
-            value: String(item.value),
+            // Code 字段用于存储非数值类字典的实际值(如文本),Value 为 INT 仅作排序
+            // 优先使用 Code 作为选项值,确保 a-select 的 v-model 与数据库文本值匹配
+            value: item.code ? item.code : String(item.value),
             label: item.label,
+            code: item.code || String(item.value),
           }));
         }
       } catch (e) {
@@ -54,11 +55,14 @@ export function useDict(dictCodes: string[]) {
     await loadPromise;
   }
 
-  // 根据字典编码和值获取对应的文本(字符串/数字类型不敏感比较
+  // 根据字典编码和值获取对应的文本(优先value匹配,其次code匹配
   function getDictText(code: string, value: any): string {
     const items = dictCache[code];
     if (!items) return value;
-    const item = items.find((i) => String(i.value) === String(value));
+    const item = items.find((i) =>
+      String(i.value) === String(value) ||
+      (i.code && String(i.code) === String(value))
+    );
     return item?.label !== undefined ? item.label : '';
   }
 

+ 2 - 0
jeecgboot-vue3/src/views/careerguidancedocument/CareerGuidanceDocument.api.ts

@@ -13,6 +13,7 @@ enum Api {
   importExcel = '/careerguidancedocument/careerGuidanceDocument/importExcel',
   exportXls = '/careerguidancedocument/careerGuidanceDocument/exportXls',
   download = '/careerguidancedocument/careerGuidanceDocument/download',
+  view = '/careerguidancedocument/careerGuidanceDocument/view',
 
   // 目录管理
   categoryTree = '/careerguidancedocument/careerGuidanceCategory/treeList',
@@ -53,6 +54,7 @@ export const saveOrUpdate = (params, isUpdate) => {
 export const getExportUrl = Api.exportXls;
 export const getImportUrl = Api.importExcel;
 export const getDownloadUrl = (id: string) => `${Api.download}?id=${id}`;
+export const getViewUrl = (id: string) => `${Api.view}?id=${id}`;
 
 // 目录管理 API
 export const getCategoryTree = () => defHttp.get({ url: Api.categoryTree });

+ 65 - 64
jeecgboot-vue3/src/views/careerguidancedocument/CareerGuidanceDocumentList.vue

@@ -51,7 +51,7 @@
         </a-card>
       </a-col>
 
-      <!-- 展开目录按钮(面板隐藏时显示) -->
+      <!-- 展开目录按钮 -->
       <a-col v-if="!showCategoryPanel" :xl="1" :lg="1" :md="24" style="margin-bottom: 10px">
         <a-button type="dashed" @click="showCategoryPanel = true" style="height: 100%; min-height: 200px; width: 100%">
           <Icon icon="ant-design:double-right-outlined" />
@@ -62,7 +62,28 @@
 
       <!-- 右侧文件列表 -->
       <a-col :flex="showCategoryPanel ? 'auto' : '1 1 auto'" :xl="showCategoryPanel ? 18 : 23" :lg="showCategoryPanel ? 16 : 23" :md="24" style="margin-bottom: 10px">
-        <BasicTable @register="registerTable" :rowSelection="rowSelection">
+        <!-- 搜索表单 -->
+        <div class="jeecg-basic-table-form-container">
+          <a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
+            <a-row :gutter="24">
+              <a-col :lg="6" :md="8" :sm="24">
+                <a-form-item label="文件名称" name="name">
+                  <a-input v-model:value="queryParam.name" placeholder="搜索文件名称" allow-clear />
+                </a-form-item>
+              </a-col>
+              <a-col :lg="6" :md="8" :sm="24">
+                <a-form-item>
+                  <a-space>
+                    <a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
+                    <a-button preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button>
+                  </a-space>
+                </a-form-item>
+              </a-col>
+            </a-row>
+          </a-form>
+        </div>
+
+        <BasicTable @register="registerTable">
           <template #tableTitle>
             <a-row type="flex" justify="space-between" style="width: 100%">
               <a-col>
@@ -71,14 +92,17 @@
                 </span>
                 <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAddFile"> 新增</a-button>
               </a-col>
-              <a-col>
-                <a-button preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
-                <j-upload-button preIcon="ant-design:import-outlined" style="margin-left: 8px" @click="onImportXls">导入</j-upload-button>
-              </a-col>
             </a-row>
           </template>
           <template #action="{ record }">
-            <TableAction :actions="getTableAction(record)" />
+            <a-space>
+              <a-button type="link" size="small" @click="handleView(record)">查看</a-button>
+              <a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
+              <a-popconfirm title="是否确认删除" @confirm="handleDelete(record)">
+                <a-button type="link" size="small" danger>删除</a-button>
+              </a-popconfirm>
+              <a-button type="link" size="small" @click="handleDownload(record)">下载</a-button>
+            </a-space>
           </template>
         </BasicTable>
       </a-col>
@@ -93,18 +117,16 @@
 </template>
 
 <script lang="ts" setup name="careerguidancedocument">
-  import { ref, computed, onMounted } from 'vue';
-  import { BasicTable, TableAction } from '/@/components/Table';
+  import { ref, reactive, computed, onMounted } from 'vue';
+  import { BasicTable } from '/@/components/Table';
   import { useModal } from '/@/components/Modal';
   import { useListPage } from '/@/hooks/system/useListPage';
-  import { columns, searchFormSchema } from './CareerGuidanceDocument.data';
+  import { columns } from './CareerGuidanceDocument.data';
   import {
     list,
     deleteOne,
-    batchDelete,
-    getImportUrl,
-    getExportUrl,
     getDownloadUrl,
+    getViewUrl,
     getCategoryTree,
     deleteCategory,
   } from './CareerGuidanceDocument.api';
@@ -114,6 +136,9 @@
   const [registerModal, { openModal }] = useModal();
   const [registerCategoryModal, { openModal: openCategoryModal }] = useModal();
 
+  const formRef = ref();
+  const queryParam = reactive<any>({});
+
   // 目录树
   const categoryTree = ref<any[]>([]);
   const selectedCategoryId = ref<string>('');
@@ -121,7 +146,6 @@
   const categorySearchText = ref<string>('');
   const showCategoryPanel = ref<boolean>(true);
 
-  // 按搜索文本过滤目录树
   const filteredCategoryTree = computed(() => {
     if (!categorySearchText.value) return categoryTree.value;
     return filterTree(categoryTree.value, categorySearchText.value);
@@ -140,7 +164,6 @@
       .filter(Boolean);
   }
 
-  // 构建目录树
   function buildTree(list: any[]): any[] {
     const map: Record<string, any> = {};
     const tree: any[] = [];
@@ -157,16 +180,13 @@
     return tree;
   }
 
-  // 加载目录树
   async function loadCategoryTree() {
     const res = await getCategoryTree();
     categoryTree.value = buildTree(res || []);
   }
 
-  // 选择目录
   function onCategorySelect(keys: any[]) {
     selectedCategoryId.value = keys[0] || '';
-    // 查找选中的目录名
     const findName = (tree: any[], id: string): string => {
       for (const node of tree) {
         if (node.id === id) return node.name;
@@ -181,36 +201,39 @@
     reload();
   }
 
-  const { tableContext, onExportXls, onImportXls } = useListPage({
+  const { tableContext } = useListPage({
     tableProps: {
-      title: '职业指导文档',
+      title: '职业指导公开文件',
       api: list,
       columns,
       canResize: false,
-      useSearchForm: true,
-      formConfig: searchFormSchema,
+      useSearchForm: false,
       actionColumn: {
-        width: 180,
+        width: 260,
         fixed: 'right',
       },
       beforeFetch: (params) => {
         if (selectedCategoryId.value) {
           params.categoryId = selectedCategoryId.value;
         }
+        if (queryParam.name) {
+          params.name = queryParam.name;
+        }
         return params;
       },
     },
-    exportConfig: {
-      name: '职业指导文档列表',
-      url: getExportUrl,
-    },
-    importConfig: {
-      url: getImportUrl,
-      success: handleSuccess,
-    },
   });
 
-  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+  const [registerTable, { reload }] = tableContext;
+  const labelCol = reactive({ xs: 24, sm: 6, md: 8, xl: 6, xxl: 6 });
+  const wrapperCol = reactive({ xs: 24, sm: 18 });
+
+  function searchQuery() { reload(); }
+  function searchReset() {
+    formRef.value?.resetFields();
+    queryParam.name = '';
+    reload();
+  }
 
   onMounted(() => {
     loadCategoryTree();
@@ -266,24 +289,16 @@
     });
   }
 
-  function handleDetail(record: Recordable) {
-    openModal(true, {
-      record,
-      isUpdate: true,
-      showFooter: false,
-    });
+  function handleView(record: Recordable) {
+    window.open(getViewUrl(record.id), '_blank');
   }
 
   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 handleDownload(record) {
@@ -291,27 +306,6 @@
       window.open(getDownloadUrl(record.id), '_blank');
     }
   }
-
-  function getTableAction(record) {
-    const actions = [
-      {
-        label: '下载',
-        onClick: handleDownload.bind(null, record),
-      },
-      {
-        label: '编辑',
-        onClick: handleEdit.bind(null, record),
-      },
-      {
-        label: '删除',
-        popConfirm: {
-          title: '是否确认删除',
-          confirm: handleDelete.bind(null, record),
-        },
-      },
-    ];
-    return actions;
-  }
 </script>
 <style scoped>
   .p-4 {
@@ -331,4 +325,11 @@
     display: inline-flex;
     gap: 4px;
   }
+  .jeecg-basic-table-form-container {
+    padding: 0 0 8px 0;
+  }
+  .jeecg-basic-table-form-container .ant-form-item:not(.ant-form-item-with-help) {
+    margin-bottom: 16px;
+    height: 32px;
+  }
 </style>

+ 1 - 1
jeecgboot-vue3/src/views/employmentassistance/EmploymentAssistanceList.vue

@@ -89,7 +89,7 @@
 
   const xzqhMap = computed(() => {
     const map: Record<string, string> = {};
-    const items = (dictMap.value && dictMap.value['XZQH']) || [];
+    const items = dictMap['XZQH'] || [];
     for (const item of items) {
       if (item.code) map[item.code] = item.name || item.label || item.code;
     }

+ 2 - 2
jeecgboot-vue3/src/views/enterprisestatus/EnterpriseStatus.api.ts

@@ -11,8 +11,8 @@ export function list(params) {
   return defHttp.get({ url: Api.List, params });
 }
 
-export function saveTag(params) {
-  return defHttp.post({ url: Api.SaveTag, params });
+export function saveTag(data) {
+  return defHttp.post({ url: Api.SaveTag, data });
 }
 
 export function getTag(params) {

+ 212 - 8
jeecgboot-vue3/src/views/enterprisestatus/EnterpriseStatusList.vue

@@ -8,14 +8,27 @@
               <a-input placeholder="请输入企业名称" v-model:value="queryParam.companyName" allow-clear></a-input>
             </a-form-item>
           </a-col>
+          <a-col :lg="6" :md="8" :sm="24">
+            <a-form-item label="统一社会信用代码" name="unifiedCreditCode">
+              <a-input placeholder="请输入统一社会信用代码" v-model:value="queryParam.unifiedCreditCode" allow-clear></a-input>
+            </a-form-item>
+          </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="所属区县" name="district">
-              <a-input placeholder="请输入所属区县" v-model:value="queryParam.district" allow-clear></a-input>
+              <a-tree-select
+                v-model:value="queryParam.district"
+                :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="6" :md="8" :sm="24">
             <a-form-item label="所属行业" name="industry">
-              <a-input placeholder="请输入所属行业" v-model:value="queryParam.industry" allow-clear></a-input>
+              <a-select v-model:value="queryParam.industry" :options="industryOptions" placeholder="请选择所属行业" allow-clear show-search />
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
@@ -48,7 +61,7 @@
           {{ xzqhMap[text] || text }}
         </template>
         <template v-else-if="column.dataIndex === 'enterpriseStatus'">
-          {{ getDictText('BusinessStatus', text) || text }}
+          {{ getDictText('BusinessStatus', text) }}
         </template>
         <template v-else-if="column.dataIndex === 'enterpriseScale'">
           {{ getDictText('enterprise_scale', text) || text }}
@@ -60,6 +73,50 @@
     </BasicTable>
 
     <EnterpriseStatusModal ref="detailModalRef" />
+
+    <a-modal
+      v-model:visible="tagModalVisible"
+      title="修改自定义标签"
+      @cancel="closeTagModal"
+      @ok="handleTagSubmit"
+      :confirmLoading="tagSaving"
+      width="520px"
+    >
+      <div v-if="tagModalRecord" class="tag-modal">
+        <div class="tag-modal-header">
+          <span>当前企业:<em>{{ tagModalRecord.companyName }}</em></span>
+        </div>
+        <a-divider style="margin: 8px 0" />
+        <a-textarea v-model:value="tagInput" placeholder="请输入自定义标签,多个标签用逗号分隔" :rows="3" />
+      </div>
+    </a-modal>
+
+    <a-modal
+      v-model:visible="msgModalVisible"
+      title="发送消息"
+      @cancel="closeMsgModal"
+      @ok="handleMsgSubmit"
+      :confirmLoading="msgSaving"
+      width="800px"
+    >
+      <a-form v-if="msgRecord" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
+        <a-form-item label="接收企业">
+          <a-input :value="msgRecord.companyName" disabled />
+        </a-form-item>
+        <a-form-item label="消息主题" required>
+          <a-input v-model:value="msgForm.subject" placeholder="请输入消息主题" />
+        </a-form-item>
+        <a-form-item label="消息内容" required>
+          <Tinymce v-model="msgForm.content" :height="300" toolbar="undo redo | styleselect | bold italic underline strikethrough | forecolor backcolor | fontsize | alignleft aligncenter alignright | bullist numlist | removeformat" plugins="lists" />
+        </a-form-item>
+        <a-form-item label="推送人">
+          <a-input :value="msgForm.sender" disabled />
+        </a-form-item>
+        <a-form-item label="推送时间">
+          <a-input :value="msgForm.sendTime" disabled />
+        </a-form-item>
+      </a-form>
+    </a-modal>
   </div>
 </template>
 
@@ -69,19 +126,22 @@
   import { useListPage } from '/@/hooks/system/useListPage';
   import { useDict } from '/@/hooks/dictionary/useDict';
   import { columns } from './EnterpriseStatus.data';
-  import { list, getExportUrl } from './EnterpriseStatus.api';
+  import { list, getExportUrl, saveTag, getTag } from './EnterpriseStatus.api';
   import { useMessage } from '/@/hooks/web/useMessage';
+import { useUserStore } from '/@/store/modules/user';
+import { defHttp } from '/@/utils/http/axios';
 import EnterpriseStatusModal from './components/EnterpriseStatusModal.vue';
 
   const formRef = ref();
   const queryParam = reactive<any>({});
   const { createMessage } = useMessage();
+  const userStore = useUserStore();
 
-  const { dictMap, getDictText, getDictOptions } = useDict(['BusinessStatus', 'enterprise_scale', 'XZQH']);
+  const { dictMap, getDictText, getDictOptions } = useDict(['BusinessStatus', 'enterprise_scale', 'XZQH', 'EconomicIndustry']);
 
   const xzqhMap = computed(() => {
     const map: Record<string, string> = {};
-    const items = (dictMap.value && dictMap.value['XZQH']) || [];
+    const items = dictMap['XZQH'] || [];
     for (const item of items) {
       if (item.code) map[item.code] = item.name || item.label || item.code;
     }
@@ -89,6 +149,51 @@ import EnterpriseStatusModal from './components/EnterpriseStatusModal.vue';
   });
 
   const businessStatusOptions = computed(() => getDictOptions('BusinessStatus'));
+  const industryOptions = computed(() =>
+    getDictOptions('EconomicIndustry').map((item) => ({ ...item, value: item.label }))
+  );
+
+  const searchTreeData = ref<any[]>([]);
+
+  async function loadZhanjiangDistricts() {
+    try {
+      const rootRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: '' },
+      }, { isTransformResponse: false });
+      if (!rootRes.success || !rootRes.result?.length) return;
+
+      const gdNode = rootRes.result.find((n: any) => n.title?.includes('广东'));
+      if (!gdNode) return;
+
+      const gdRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: gdNode.key },
+      }, { isTransformResponse: false });
+      if (!gdRes.success || !gdRes.result?.length) return;
+
+      const zjNode = gdRes.result.find((n: any) => n.title?.includes('湛江'));
+      if (!zjNode) return;
+
+      const zjRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: zjNode.key },
+      }, { isTransformResponse: false });
+      if (!zjRes.success || !zjRes.result?.length) return;
+
+      searchTreeData.value = zjRes.result.map((item: any) => ({
+        key: item.key, title: item.title, value: item.key, isLeaf: true,
+      }));
+    } catch (e) {
+      console.error('加载湛江区县失败:', e);
+    }
+  }
+
+  async function loadSearchTreeChildren(treeNode) {
+    // 区县已标记为叶子节点,不会触发懒加载
+  }
+
+  loadZhanjiangDistricts();
 
   const { tableContext, onExportXls } = useListPage({
     tableProps: {
@@ -115,6 +220,97 @@ import EnterpriseStatusModal from './components/EnterpriseStatusModal.vue';
 
   const detailModalRef = ref();
 
+  const tagModalVisible = ref<boolean>(false);
+  const tagModalRecord = ref<any>(null);
+  const tagSaving = ref<boolean>(false);
+  const tagInput = ref<string>('');
+
+  const msgModalVisible = ref<boolean>(false);
+  const msgRecord = ref<any>(null);
+  const msgSaving = ref<boolean>(false);
+  const msgForm = reactive({ subject: '', content: '', sender: '', sendTime: '' });
+
+  function openMsgModal(record: Recordable) {
+    msgRecord.value = record;
+    msgForm.subject = '';
+    msgForm.content = '';
+    msgForm.sender = userStore.getUserInfo?.realname || userStore.userInfo?.realname || '';
+    msgForm.sendTime = new Date().toLocaleString('zh-CN');
+    msgModalVisible.value = true;
+  }
+
+  function closeMsgModal() {
+    msgModalVisible.value = false;
+    msgRecord.value = null;
+  }
+
+  async function handleMsgSubmit() {
+    if (!msgForm.subject.trim()) {
+      createMessage.warning('请输入消息主题');
+      return;
+    }
+    if (!msgForm.content.trim()) {
+      createMessage.warning('请输入消息内容');
+      return;
+    }
+    msgSaving.value = true;
+    try {
+      await defHttp.post({
+        url: '/notification/send',
+        data: {
+          moduleType: 'enterprise_status',
+          subject: msgForm.subject,
+          content: msgForm.content,
+          targets: [{ targetType: 'enterprise', targetId: msgRecord.value.id }],
+        },
+      });
+      createMessage.success('消息发送成功');
+      closeMsgModal();
+      reload();
+    } catch (e) {
+      createMessage.error(e.message || '消息发送失败');
+    } finally {
+      msgSaving.value = false;
+    }
+  }
+
+  async function openTagModal(record: Recordable) {
+    tagModalRecord.value = record;
+    tagInput.value = '';
+    tagModalVisible.value = true;
+    try {
+      const res = await getTag({ enterpriseId: record.id });
+      if (res?.customTags) {
+        tagInput.value = res.customTags;
+      }
+    } catch (e) {
+      console.error('加载标签失败:', e);
+    }
+  }
+
+  function closeTagModal() {
+    tagModalVisible.value = false;
+    tagModalRecord.value = null;
+    tagInput.value = '';
+  }
+
+  async function handleTagSubmit() {
+    tagSaving.value = true;
+    try {
+      await saveTag({
+        enterpriseId: tagModalRecord.value.id,
+        customTags: tagInput.value,
+      });
+      createMessage.success('标签保存成功');
+      closeTagModal();
+      reload();
+    } catch (e) {
+      createMessage.error(e.message || '标签保存失败');
+    } finally {
+      tagSaving.value = false;
+    }
+  }
+
   function handleDetail(record: Recordable) {
     detailModalRef.value?.detail(record);
   }
@@ -122,8 +318,8 @@ import EnterpriseStatusModal from './components/EnterpriseStatusModal.vue';
   function getTableAction(record) {
     return [
       { label: '查看', onClick: handleDetail.bind(null, record) },
-      { label: '发送消息', onClick: () => createMessage.warning('发送消息功能待开发') },
-      { label: '修改标签', onClick: () => createMessage.warning('修改标签功能待开发') },
+      { label: '发送消息', onClick: () => openMsgModal(record) },
+      { label: '修改标签', onClick: () => openTagModal(record) },
     ];
   }
 
@@ -140,4 +336,12 @@ import EnterpriseStatusModal from './components/EnterpriseStatusModal.vue';
     .ant-form-item:not(.ant-form-item-with-help) { margin-bottom: 16px; height: 32px; }
     :deep(.ant-picker), :deep(.ant-input-number) { width: 100%; }
   }
+
+  .tag-modal {
+    .tag-modal-header {
+      font-size: 14px;
+      margin-bottom: 4px;
+      em { font-style: normal; color: #1890ff; font-weight: 600; }
+    }
+  }
 </style>

+ 1 - 1
jeecgboot-vue3/src/views/enterprisestatus/components/EnterpriseStatusDetail.vue

@@ -55,7 +55,7 @@
 
   const xzqhMap = computed(() => {
     const map: Record<string, string> = {};
-    const items = (dictMap.value && dictMap.value['XZQH']) || [];
+    const items = dictMap['XZQH'] || [];
     for (const item of items) {
       if (item.code) map[item.code] = item.name || item.label || item.code;
     }

+ 1 - 0
jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnel.data.ts

@@ -70,6 +70,7 @@ export const columns: BasicColumn[] = [
     align: 'center',
     dataIndex: 'customTag',
   },
+  { title: '', dataIndex: 'personalId', width: 0, ifShow: false },
 ];
 
 // 高级查询数据(暂未启用,需求要求不显示高级查询)

+ 184 - 29
jeecgboot-vue3/src/views/focuspersonnel/FocusPersonnelList.vue

@@ -11,33 +11,38 @@
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="性别" name="gender">
-              <a-select placeholder="请选择性别" v-model:value="queryParam.gender" allow-clear>
-                <a-select-option value="1">男性</a-select-option>
-                <a-select-option value="2">女性</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.gender" :options="genderOptions" placeholder="请选择性别" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="学历" name="education">
-              <a-select placeholder="请选择学历" v-model:value="queryParam.education" allow-clear>
-                <a-select-option value="博士研究生">博士研究生</a-select-option>
-                <a-select-option value="硕士研究生">硕士研究生</a-select-option>
-                <a-select-option value="大学本科">大学本科</a-select-option>
-                <a-select-option value="大学专科">大学专科</a-select-option>
-                <a-select-option value="中专">中专</a-select-option>
-                <a-select-option value="高中">高中</a-select-option>
-                <a-select-option value="初中及以下">初中及以下</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.education" :options="educationOptions" placeholder="请选择学历" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="户口所在地" name="householdLocation">
-              <a-input placeholder="请输入户口所在地" v-model:value="queryParam.householdLocation" allow-clear></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="6" :md="8" :sm="24">
             <a-form-item label="现居住地" name="currentResidence">
-              <a-input placeholder="请输入现居住地" v-model:value="queryParam.currentResidence" allow-clear></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="6" :md="8" :sm="24">
@@ -98,13 +103,16 @@
       <!--字典翻译插槽-->
       <template #bodyCell="{ column, text }">
         <template v-if="column.dataIndex === 'householdLocation'">
-          {{ (xzqhMap[text] || text).replace('广东省湛江市','') }}
+          {{ (xzqhMap[text] || xzqhMap[String(text).substring(0, 6)] || text || '').replace('广东省湛江市','') }}
         </template>
         <template v-else-if="column.dataIndex === 'currentResidence'">
-          {{ (xzqhMap[text] || text).replace('广东省湛江市','') }}
+          {{ xzqhMap[text] || xzqhMap[String(text).substring(0, 6)] || text }}
         </template>
         <template v-else-if="column.dataIndex === 'gender'">
-          {{ getDictText('Gender', text) || genderMap[text] || text }}
+          {{ getDictText('Gender', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'education'">
+          {{ getDictText('Education', text) || text }}
         </template>
         <template v-else-if="column.dataIndex === 'jobSearchStatus'">
           {{ getDictText('JobSeekerStatus', text) || text }}
@@ -119,7 +127,7 @@
           {{ getDictText('focus_minor_tag', text) || text }}
         </template>
         <template v-else-if="column.dataIndex === 'customTag'">
-          {{ text ? String(text).split(',').map(val => getDictText('focus_custom_tag', val) || val).join(', ') : '-' }}
+          {{ text ? String(text).split(',').map(val => customTagMap[val] || getDictText('focus_custom_tag', val) || val).join(', ') : '-' }}
         </template>
       </template>
       <!--操作栏-->
@@ -156,6 +164,36 @@
         <a-empty v-if="customTagOptions.length === 0" description="暂无可用标签" />
       </div>
     </a-modal>
+    <!-- 推送消息弹窗 -->
+    <a-modal
+      v-model:visible="msgModalVisible"
+      title="推送消息"
+      @cancel="closeMsgModal"
+      @ok="handleMsgSubmit"
+      :confirmLoading="msgSaving"
+      width="800px"
+    >
+      <div class="batch-msg-modal">
+        <div class="batch-msg-header">
+          <span>已选择 <em>{{ selectedRowKeys.length }}</em> 条数据</span>
+        </div>
+        <a-divider style="margin: 8px 0" />
+        <a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
+          <a-form-item label="消息主题" required>
+            <a-input v-model:value="msgForm.subject" placeholder="请输入消息主题" />
+          </a-form-item>
+          <a-form-item label="消息内容" required>
+            <Tinymce v-model="msgForm.content" :height="300" toolbar="undo redo | styleselect | bold italic underline strikethrough | forecolor backcolor | fontsize | alignleft aligncenter alignright | bullist numlist | removeformat" plugins="lists" />
+          </a-form-item>
+          <a-form-item label="推送人">
+            <a-input :value="msgForm.sender" disabled />
+          </a-form-item>
+          <a-form-item label="推送时间">
+            <a-input :value="msgForm.sendTime" disabled />
+          </a-form-item>
+        </a-form>
+      </div>
+    </a-modal>
     <!-- 服务跟进记录弹窗 -->
     <FocusPersonnelServiceFollowList
       v-model:visible="serviceFollowVisible"
@@ -174,18 +212,44 @@
   import FocusPersonnelModal from './components/FocusPersonnelModal.vue';
   import FocusPersonnelServiceFollowList from './components/FocusPersonnelServiceFollowList.vue';
   import { useMessage } from '/@/hooks/web/useMessage';
+  import { useUserStore } from '/@/store/modules/user';
+  import { defHttp } from '/@/utils/http/axios';
   import { useDict } from '/@/hooks/dictionary/useDict';
 
   const formRef = ref();
   const queryParam = reactive<any>({});
   const registerModal = ref();
   const { createMessage, createConfirm } = useMessage();
+  const userStore = useUserStore();
+
+  const { dictMap, getDictText, getDictOptions } = useDict(['focus_major_tag', 'focus_minor_tag', 'focus_custom_tag', 'JobSeekerStatus', 'JobSeekerCategory', 'Gender', 'Education', 'XZQH']);
+
+  const genderOptions = computed(() => getDictOptions('Gender'));
+  const educationOptions = computed(() => getDictOptions('Education'));
 
-  // 性别映射(兼容数据库中数字和文本两种存储格式)
-  const genderMap: Record<string, string> = { '1': '男性', '2': '女性', 1: '男性', 2: '女性' };
+  // 自定义标签相关 - 从个人标签库(personal_tag)加载
+  const customTagOptions = ref<any[]>([]);
+  const customTagMap = ref<Record<string, string>>({});
 
-  // 自定义标签相关(从 DICTIONARY_ITEM 表通过 useDict 加载)
-  const customTagOptions = computed(() => getDictOptions('focus_custom_tag'));
+  async function loadCustomTags() {
+    try {
+      // 重点关注人员管理自定义标签的父级ID
+      const parentId = '2064950954964254722';
+      const res = await defHttp.get({
+        url: '/tag/personalTag/listForSelect',
+        params: { parentId, tagLevel: '2' },
+      });
+      const tags = res || [];
+      customTagOptions.value = tags.map((t: any) => ({ value: t.id, label: t.tagName }));
+      customTagMap.value = {};
+      for (const t of tags) {
+        customTagMap.value[t.id] = t.tagName;
+      }
+    } catch (e) {
+      console.error('加载自定义标签失败:', e);
+    }
+  }
+  loadCustomTags();
 
   // 批量添加自定义标签
   const batchTagVisible = ref<boolean>(false);
@@ -219,17 +283,52 @@
   }
 
   // 从自定义 DICTIONARY_ITEM 表加载标签字典(含求职人员类别字典)
-  const { dictMap, getDictText, getDictOptions } = useDict(['focus_major_tag', 'focus_minor_tag', 'focus_custom_tag', 'JobSeekerStatus', 'JobSeekerCategory', 'Gender', 'XZQH']);
-
   const xzqhMap = computed(() => {
     const map: Record<string, string> = {};
-    const items = (dictMap.value && dictMap.value['XZQH']) || [];
+    const items = dictMap['XZQH'] || [];
     for (const item of items) {
       if (item.code) map[item.code] = item.name || item.label || item.code;
     }
     return map;
   });
 
+  const searchTreeData = ref<any[]>([]);
+
+  async function loadZhanjiangDistricts() {
+    try {
+      const rootRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: '' },
+      }, { isTransformResponse: false });
+      if (!rootRes.success || !rootRes.result?.length) return;
+      const gdNode = rootRes.result.find((n: any) => n.title?.includes('广东'));
+      if (!gdNode) return;
+      const gdRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: gdNode.key },
+      }, { isTransformResponse: false });
+      if (!gdRes.success || !gdRes.result?.length) return;
+      const zjNode = gdRes.result.find((n: any) => n.title?.includes('湛江'));
+      if (!zjNode) return;
+      const zjRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: zjNode.key },
+      }, { isTransformResponse: false });
+      if (!zjRes.success || !zjRes.result?.length) return;
+      searchTreeData.value = zjRes.result.map((item: any) => ({
+        key: item.key, title: item.title, value: item.key, isLeaf: true,
+      }));
+    } catch (e) {
+      console.error('加载湛江区县失败:', e);
+    }
+  }
+
+  async function loadSearchTreeChildren(_treeNode: any) {
+    // 区县已标记为叶子节点,不会触发懒加载
+  }
+
+  loadZhanjiangDistricts();
+
   // 大类标签和小类标签下拉选项
   const majorTagOptions = computed(() => getDictOptions('focus_major_tag'));
   const minorTagOptions = computed(() => getDictOptions('focus_minor_tag'));
@@ -264,7 +363,7 @@
       success: handleSuccess,
     },
   });
-  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+  const [registerTable, { reload, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
   const labelCol = reactive({
     xs: 24,
     sm: 6,
@@ -335,9 +434,58 @@
     batchTagVisible.value = true;
   }
 
-  /** 推送消息(待开发) */
+  // ---------- 推送消息 ----------
+  const msgModalVisible = ref<boolean>(false);
+  const msgSaving = ref<boolean>(false);
+  const msgForm = reactive({ subject: '', content: '', sender: '', sendTime: '' });
+
   function handleMessagePush() {
-    createMessage.warning('推送消息功能待开发');
+    if (!selectedRowKeys.value || selectedRowKeys.value.length === 0) {
+      createMessage.warning('请先勾选要推送的人员');
+      return;
+    }
+    msgForm.subject = '';
+    msgForm.content = '';
+    msgForm.sender = userStore.getUserInfo?.realname || userStore.userInfo?.realname || '';
+    msgForm.sendTime = new Date().toLocaleString('zh-CN');
+    msgModalVisible.value = true;
+  }
+
+  function closeMsgModal() {
+    msgModalVisible.value = false;
+  }
+
+  async function handleMsgSubmit() {
+    if (!msgForm.subject.trim()) {
+      createMessage.warning('请输入消息主题');
+      return;
+    }
+    if (!msgForm.content.trim()) {
+      createMessage.warning('请输入消息内容');
+      return;
+    }
+    msgSaving.value = true;
+    try {
+      const dataSource = getDataSource();
+      const targets = dataSource
+        .filter((row: any) => selectedRowKeys.value.includes(row.id))
+        .map((row: any) => ({ targetType: 'personal', targetId: row.personalId || row.personal_id || row.PERSONAL_ID || row.id }));
+      await defHttp.post({
+        url: '/notification/send',
+        data: {
+          moduleType: 'focus_personnel',
+          subject: msgForm.subject,
+          content: msgForm.content,
+          targets,
+        },
+      });
+      createMessage.success('消息推送成功');
+      closeMsgModal();
+    } catch (e) {
+      createMessage.error(e.message || '消息推送失败');
+    } finally {
+      msgSaving.value = false;
+    }
   }
 
   // useDict 已自动加载字典数据,无需手动初始化
@@ -431,4 +579,11 @@
       margin: 24px 0;
     }
   }
+
+  .batch-msg-modal {
+    .batch-msg-header {
+      font-size: 14px;
+      em { font-style: normal; color: #1890ff; font-weight: 600; }
+    }
+  }
 </style>

+ 29 - 12
jeecgboot-vue3/src/views/focuspersonnel/components/FocusPersonnelDetail.vue

@@ -18,7 +18,7 @@
             <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">{{ getDictText('focus_custom_tag', tag) || tag }}</a-tag>
+              <a-tag v-for="tag in customTagList" :key="tag" color="orange">{{ customTagMap[tag] || getDictText('focus_custom_tag', tag) || tag }}</a-tag>
             </template>
           </div>
           <div class="header-meta">
@@ -57,8 +57,8 @@
         <a-descriptions-item label="QQ号码">{{ detailData.qqNumber || '-' }}</a-descriptions-item>
         <a-descriptions-item label="微信号">{{ detailData.wechatId || '-' }}</a-descriptions-item>
         <a-descriptions-item label="户口性质">{{ detailData.householdType || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="户口所在地">{{ detailData.householdLocation || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="现居住地">{{ detailData.currentResidence || '-' }}</a-descriptions-item>
+        <a-descriptions-item label="户口所在地">{{ (xzqhMap[detailData.householdLocation] || xzqhMap[String(detailData.householdLocation).substring(0, 6)] || detailData.householdLocation || '').replace('广东省湛江市','') || '-' }}</a-descriptions-item>
+        <a-descriptions-item label="现居住地">{{ xzqhMap[detailData.currentResidence] || xzqhMap[String(detailData.currentResidence).substring(0, 6)] || detailData.currentResidence || '-' }}</a-descriptions-item>
         <a-descriptions-item label="现居住地址">{{ detailData.currentAddress || '-' }}</a-descriptions-item>
         <!-- ===== 标签信息 ===== -->
         <a-descriptions-item label="人员大类标签">
@@ -109,7 +109,8 @@
 <script lang="ts" setup>
   import { computed, ref, watch } from 'vue';
   import { EditOutlined } from '@ant-design/icons-vue';
-  import { queryDetailById, saveOrUpdate, listCustomTags } from '../FocusPersonnel.api';
+  import { queryDetailById, saveOrUpdate } from '../FocusPersonnel.api';
+  import { defHttp } from '/@/utils/http/axios';
   import dayjs from 'dayjs';
   import { useMessage } from '/@/hooks/web/useMessage';
   import { useDict } from '/@/hooks/dictionary/useDict';
@@ -117,7 +118,15 @@
   const { createMessage } = useMessage();
 
   // 从自定义 DICTIONARY_ITEM 表加载标签字典(含求职人员类别字典)
-  const { getDictText } = useDict(['focus_major_tag', 'focus_minor_tag', 'focus_custom_tag', 'JobSeekerCategory']);
+  const { getDictText, getDictOptions } = useDict(['focus_major_tag', 'focus_minor_tag', 'focus_custom_tag', 'JobSeekerCategory', 'XZQH']);
+
+  const xzqhMap = computed(() => {
+    const map: Record<string, string> = {};
+    for (const item of getDictOptions('XZQH')) {
+      map[item.value] = item.label;
+    }
+    return map;
+  });
 
   const props = defineProps({
     record: { type: Object, default: () => ({}) },
@@ -127,23 +136,31 @@
   const saving = ref<boolean>(false);
   const detailData = ref<any>(null);
 
-  // 自定义标签字典选项(用于下拉多选)
+  // 自定义标签相关 - 从个人标签库(personal_tag)加载
   const customTagDictOptions = ref<any[]>([]);
+  const customTagMap = ref<Record<string, string>>({});
 
   // 自定义标签编辑相关
   const editingCustomTag = ref<boolean>(false);
   const editingCustomTagList = ref<string[]>([]);
 
-  /** 加载自定义标签字典选项 */
+  /** 加载自定义标签选项(从个人标签库) */
   async function loadCustomTagOptions() {
     try {
-      const tags = await listCustomTags();
-      if (Array.isArray(tags)) {
-        // listCustomTags 返回 DictionaryItem 对象 { value, name, ... }
-        customTagDictOptions.value = tags.map(tag => ({ label: tag.name, value: String(tag.value) }));
+      // 重点关注人员管理自定义标签的父级ID
+      const parentId = '2064950954964254722';
+      const res = await defHttp.get({
+        url: '/tag/personalTag/listForSelect',
+        params: { parentId, tagLevel: '2' },
+      });
+      const tags = res || [];
+      customTagDictOptions.value = tags.map((t: any) => ({ label: t.tagName, value: t.id }));
+      customTagMap.value = {};
+      for (const t of tags) {
+        customTagMap.value[t.id] = t.tagName;
       }
     } catch (e) {
-      console.error('加载自定义标签字典失败', e);
+      console.error('加载自定义标签失败', e);
     }
   }
 

+ 1 - 0
jeecgboot-vue3/src/views/internshippersonnel/InternshipPersonnel.data.ts

@@ -105,6 +105,7 @@ export const columns: BasicColumn[] = [
     align: 'center',
     dataIndex: 'customTag',
   },
+  { title: '', dataIndex: 'personalId', width: 0, ifShow: false },
 ];
 
 // 高级查询数据

+ 210 - 31
jeecgboot-vue3/src/views/internshippersonnel/InternshipPersonnelList.vue

@@ -39,33 +39,38 @@
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="性别" name="gender">
-              <a-select placeholder="请选择性别" v-model:value="queryParam.gender" allow-clear>
-                <a-select-option value="1">男性</a-select-option>
-                <a-select-option value="2">女性</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.gender" :options="genderOptions" placeholder="请选择性别" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="学历" name="education">
-              <a-select placeholder="请选择学历" v-model:value="queryParam.education" allow-clear>
-                <a-select-option value="博士研究生">博士研究生</a-select-option>
-                <a-select-option value="硕士研究生">硕士研究生</a-select-option>
-                <a-select-option value="大学本科">大学本科</a-select-option>
-                <a-select-option value="大学专科">大学专科</a-select-option>
-                <a-select-option value="中专">中专</a-select-option>
-                <a-select-option value="高中">高中</a-select-option>
-                <a-select-option value="初中及以下">初中及以下</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.education" :options="educationOptions" placeholder="请选择学历" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="户口所在地" name="householdLocation">
-              <a-input placeholder="请输入户口所在地" v-model:value="queryParam.householdLocation" allow-clear></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="6" :md="8" :sm="24">
             <a-form-item label="现居住地" name="currentResidence">
-              <a-input placeholder="请输入现居住地" v-model:value="queryParam.currentResidence" allow-clear></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="6" :md="8" :sm="24">
@@ -125,8 +130,23 @@
       </template>
       <!--字典翻译插槽-->
       <template #bodyCell="{ column, text }">
-        <template v-if="column.dataIndex === 'gender'">
-          {{ genderMap[text] || text }}
+        <template v-if="column.dataIndex === 'householdLocation'">
+          {{ (xzqhMap[text] || xzqhMap[String(text).substring(0, 6)] || text || '').replace('广东省湛江市','') }}
+        </template>
+        <template v-else-if="column.dataIndex === 'currentResidence'">
+          {{ xzqhMap[text] || xzqhMap[String(text).substring(0, 6)] || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'gender'">
+          {{ getDictText('Gender', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'education'">
+          {{ getDictText('Education', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'internshipStatus'">
+          {{ getDictText('internship_status', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'auditStatus'">
+          {{ getDictText('audit_status', text) || text }}
         </template>
         <template v-else-if="column.dataIndex === 'jobSearchStatus'">
           {{ getDictText('JobSeekerStatus', text) || text }}
@@ -141,7 +161,7 @@
           {{ getDictText('internship_minor_tag', text) || text }}
         </template>
         <template v-else-if="column.dataIndex === 'customTag'">
-          {{ text ? String(text).split(',').map(val => getDictText('internship_custom_tag', val) || val).join(', ') : '-' }}
+          {{ text ? String(text).split(',').map(val => customTagMap[val] || getDictText('internship_custom_tag', val) || val).join(', ') : '-' }}
         </template>
       </template>
       <!--操作栏-->
@@ -184,6 +204,36 @@
       :record="serviceFollowRecord"
       @close="serviceFollowRecord = null"
     />
+    <!-- 推送消息弹窗 -->
+    <a-modal
+      v-model:visible="msgModalVisible"
+      title="推送消息"
+      @cancel="closeMsgModal"
+      @ok="handleMsgSubmit"
+      :confirmLoading="msgSaving"
+      width="800px"
+    >
+      <div class="batch-msg-modal">
+        <div class="batch-msg-header">
+          <span>已选择 <em>{{ selectedRowKeys.length }}</em> 条数据</span>
+        </div>
+        <a-divider style="margin: 8px 0" />
+        <a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
+          <a-form-item label="消息主题" required>
+            <a-input v-model:value="msgForm.subject" placeholder="请输入消息主题" />
+          </a-form-item>
+          <a-form-item label="消息内容" required>
+            <Tinymce v-model="msgForm.content" :height="300" toolbar="undo redo | styleselect | bold italic underline strikethrough | forecolor backcolor | fontsize | alignleft aligncenter alignright | bullist numlist | removeformat" plugins="lists" />
+          </a-form-item>
+          <a-form-item label="推送人">
+            <a-input :value="msgForm.sender" disabled />
+          </a-form-item>
+          <a-form-item label="推送时间">
+            <a-input :value="msgForm.sendTime" disabled />
+          </a-form-item>
+        </a-form>
+      </div>
+    </a-modal>
   </div>
 </template>
 
@@ -196,31 +246,50 @@
   import InternshipPersonnelModal from './components/InternshipPersonnelModal.vue';
   import InternshipPersonnelServiceFollowList from './components/InternshipPersonnelServiceFollowList.vue';
   import { useMessage } from '/@/hooks/web/useMessage';
+  import { useUserStore } from '/@/store/modules/user';
+  import { defHttp } from '/@/utils/http/axios';
   import { useDict } from '/@/hooks/dictionary/useDict';
 
   const formRef = ref();
   const queryParam = reactive<any>({});
   const registerModal = ref();
   const { createMessage, createConfirm } = useMessage();
+  const userStore = useUserStore();
 
-  // 性别映射(兼容数据库中数字和文本两种存储格式)
-  const genderMap: Record<string, string> = { '1': '男性', '2': '女性', 1: '男性', 2: '女性' };
+  // 自定义标签相关 - 从个人标签库(personal_tag)加载
+  const customTagOptions = ref<any[]>([]);
+  const customTagMap = ref<Record<string, string>>({});
 
-  // 自定义标签相关(从 DICTIONARY_ITEM 表通过 useDict 加载)
-  const customTagOptions = computed(() => getDictOptions('internship_custom_tag'));
+  async function loadCustomTags() {
+    try {
+      // 见习人员管理自定义标签的父级ID
+      const parentId = '2064950598742016001';
+      const res = await defHttp.get({
+        url: '/tag/personalTag/listForSelect',
+        params: { parentId, tagLevel: '2' },
+      });
+      const tags = res || [];
+      customTagOptions.value = tags.map((t: any) => ({ value: t.id, label: t.tagName }));
+      customTagMap.value = {};
+      for (const t of tags) {
+        customTagMap.value[t.id] = t.tagName;
+      }
+    } catch (e) {
+      console.error('加载自定义标签失败:', e);
+    }
+  }
+  loadCustomTags();
 
   // 批量添加自定义标签
   const batchTagVisible = ref<boolean>(false);
   const batchTagSaving = ref<boolean>(false);
   const batchTagChecked = ref<string[]>([]);
 
-  /** 关闭批量标签弹窗 */
   function closeBatchTagModal() {
     batchTagVisible.value = false;
     batchTagChecked.value = [];
   }
 
-  /** 提交批量标签 */
   async function handleBatchTagSubmit() {
     const selectedTags = batchTagChecked.value;
     if (!selectedTags || selectedTags.length === 0) {
@@ -240,15 +309,69 @@
     }
   }
 
-  // 从自定义 DICTIONARY_ITEM 表加载标签字典(含求职人员类别字典)
-  const { getDictText, getDictOptions } = useDict(['internship_major_tag', 'internship_minor_tag', 'internship_custom_tag', 'JobSeekerStatus', 'JobSeekerCategory']);
+  const { getDictText, getDictOptions } = useDict([
+    'Gender', 'Education', 'internship_status', 'audit_status',
+    'internship_major_tag', 'internship_minor_tag', 'internship_custom_tag',
+    'JobSeekerStatus', 'JobSeekerCategory', 'XZQH',
+  ]);
 
-  // 大类标签和小类标签下拉选项
+  const genderOptions = computed(() => getDictOptions('Gender'));
+  const educationOptions = computed(() => getDictOptions('Education'));
   const majorTagOptions = computed(() => getDictOptions('internship_major_tag'));
   const minorTagOptions = computed(() => getDictOptions('internship_minor_tag'));
-  // 求职状态下拉选项(从字典加载)
   const jobSearchStatusOptions = computed(() => getDictOptions('JobSeekerStatus'));
 
+  const xzqhMap = computed(() => {
+    const map: Record<string, string> = {};
+    const items = getDictOptions('XZQH');
+    for (const item of items) {
+      map[item.value] = item.label;
+    }
+    return map;
+  });
+
+  const searchTreeData = ref<any[]>([]);
+
+  async function loadZhanjiangDistricts() {
+    try {
+      const rootRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: '' },
+      }, { isTransformResponse: false });
+      if (!rootRes.success || !rootRes.result?.length) return;
+
+      const gdNode = rootRes.result.find((n: any) => n.title?.includes('广东'));
+      if (!gdNode) return;
+
+      const gdRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: gdNode.key },
+      }, { isTransformResponse: false });
+      if (!gdRes.success || !gdRes.result?.length) return;
+
+      const zjNode = gdRes.result.find((n: any) => n.title?.includes('湛江'));
+      if (!zjNode) return;
+
+      const zjRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: zjNode.key },
+      }, { isTransformResponse: false });
+      if (!zjRes.success || !zjRes.result?.length) return;
+
+      searchTreeData.value = zjRes.result.map((item: any) => ({
+        key: item.key, title: item.title, value: item.key, isLeaf: true,
+      }));
+    } catch (e) {
+      console.error('加载湛江区县失败:', e);
+    }
+  }
+
+  async function loadSearchTreeChildren(_treeNode: any) {
+    // 区县已标记为叶子节点,不会触发懒加载
+  }
+
+  loadZhanjiangDistricts();
+
   //注册table数据
   const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
     tableProps: {
@@ -277,7 +400,7 @@
       success: handleSuccess,
     },
   });
-  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+  const [registerTable, { reload, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext;
   const labelCol = reactive({
     xs: 24,
     sm: 6,
@@ -348,9 +471,58 @@
     batchTagVisible.value = true;
   }
 
-  /** 推送消息(待开发) */
+  // ---------- 推送消息 ----------
+  const msgModalVisible = ref<boolean>(false);
+  const msgSaving = ref<boolean>(false);
+  const msgForm = reactive({ subject: '', content: '', sender: '', sendTime: '' });
+
   function handleMessagePush() {
-    createMessage.warning('推送消息功能待开发');
+    if (!selectedRowKeys.value || selectedRowKeys.value.length === 0) {
+      createMessage.warning('请先勾选要推送的人员');
+      return;
+    }
+    msgForm.subject = '';
+    msgForm.content = '';
+    msgForm.sender = userStore.getUserInfo?.realname || userStore.userInfo?.realname || '';
+    msgForm.sendTime = new Date().toLocaleString('zh-CN');
+    msgModalVisible.value = true;
+  }
+
+  function closeMsgModal() {
+    msgModalVisible.value = false;
+  }
+
+  async function handleMsgSubmit() {
+    if (!msgForm.subject.trim()) {
+      createMessage.warning('请输入消息主题');
+      return;
+    }
+    if (!msgForm.content.trim()) {
+      createMessage.warning('请输入消息内容');
+      return;
+    }
+    msgSaving.value = true;
+    try {
+      const dataSource = getDataSource();
+      const targets = dataSource
+        .filter((row: any) => selectedRowKeys.value.includes(row.id))
+        .map((row: any) => ({ targetType: 'personal', targetId: row.personalId || row.personal_id || row.PERSONAL_ID || row.id }));
+      await defHttp.post({
+        url: '/notification/send',
+        data: {
+          moduleType: 'internship_personnel',
+          subject: msgForm.subject,
+          content: msgForm.content,
+          targets,
+        },
+      });
+      createMessage.success('消息推送成功');
+      closeMsgModal();
+    } catch (e) {
+      createMessage.error(e.message || '消息推送失败');
+    } finally {
+      msgSaving.value = false;
+    }
   }
 
   // useDict 已自动加载字典数据,无需手动初始化
@@ -444,4 +616,11 @@
       margin: 24px 0;
     }
   }
+
+  .batch-msg-modal {
+    .batch-msg-header {
+      font-size: 14px;
+      em { font-style: normal; color: #1890ff; font-weight: 600; }
+    }
+  }
 </style>

+ 11 - 3
jeecgboot-vue3/src/views/internshippersonnel/components/InternshipPersonnelDetail.vue

@@ -65,8 +65,8 @@
         <a-descriptions-item label="工作经验">{{ detailData.workExperience || '-' }}</a-descriptions-item>
         <a-descriptions-item label="职业技能等级">{{ detailData.skillLevel || '-' }}</a-descriptions-item>
         <a-descriptions-item label="户口性质">{{ detailData.householdType || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="户口所在地">{{ detailData.householdLocation || '-' }}</a-descriptions-item>
-        <a-descriptions-item label="现居住地">{{ detailData.currentResidence || '-' }}</a-descriptions-item>
+        <a-descriptions-item label="户口所在地">{{ (xzqhMap[detailData.householdLocation] || xzqhMap[String(detailData.householdLocation).substring(0, 6)] || detailData.householdLocation || '').replace('广东省湛江市','') || '-' }}</a-descriptions-item>
+        <a-descriptions-item label="现居住地">{{ xzqhMap[detailData.currentResidence] || xzqhMap[String(detailData.currentResidence).substring(0, 6)] || detailData.currentResidence || '-' }}</a-descriptions-item>
         <a-descriptions-item label="现居住地址">{{ detailData.currentAddress || '-' }}</a-descriptions-item>
         <a-descriptions-item label="求职人员类别">{{ getDictText('JobSeekerCategory', detailData.jobSeekerCategory) || detailData.jobSeekerCategory || '-' }}</a-descriptions-item>
         <a-descriptions-item label="联系电话">{{ detailData.contactPhone || '-' }}</a-descriptions-item>
@@ -142,7 +142,15 @@
   const { createMessage } = useMessage();
 
   // 从自定义 DICTIONARY_ITEM 表加载标签字典(含求职人员类别字典)
-  const { getDictText } = useDict(['internship_major_tag', 'internship_minor_tag', 'internship_custom_tag', 'JobSeekerCategory']);
+  const { getDictText, getDictOptions } = useDict(['internship_major_tag', 'internship_minor_tag', 'internship_custom_tag', 'JobSeekerCategory', 'XZQH']);
+
+  const xzqhMap = computed(() => {
+    const map: Record<string, string> = {};
+    for (const item of getDictOptions('XZQH')) {
+      map[item.value] = item.label;
+    }
+    return map;
+  });
 
   const props = defineProps({
     record: { type: Object, default: () => ({}) },

+ 0 - 9
jeecgboot-vue3/src/views/internshippost/InternshipPost.data.ts

@@ -1,10 +1,5 @@
 import { BasicColumn } from '/@/components/Table';
 
-// 发布状态映射
-const publishStatusMap: Record<string, string> = { '0': '未发布', '1': '已发布' };
-// 报名状态映射
-const applyStatusMap: Record<string, string> = { '01': '报名中', '02': '人员已满' };
-
 // 列表数据
 export const columns: BasicColumn[] = [
   {
@@ -16,7 +11,6 @@ export const columns: BasicColumn[] = [
     title: '单位性质',
     align: 'center',
     dataIndex: 'companyNature',
-    customRender: ({ text }) => text || '-',
   },
   {
     title: '工作地点',
@@ -32,7 +26,6 @@ export const columns: BasicColumn[] = [
     title: '身份要求',
     align: 'center',
     dataIndex: 'identityRequirement',
-    customRender: ({ text }) => text || '-',
   },
   {
     title: '见习期限',
@@ -58,13 +51,11 @@ export const columns: BasicColumn[] = [
     title: '发布状态',
     align: 'center',
     dataIndex: 'publishStatus',
-    customRender: ({ text }) => publishStatusMap[text] || text || '-',
   },
   {
     title: '报名状态',
     align: 'center',
     dataIndex: 'applyStatus',
-    customRender: ({ text }) => applyStatusMap[text] || text || '-',
   },
 ];
 

+ 91 - 33
jeecgboot-vue3/src/views/internshippost/InternshipPostList.vue

@@ -11,18 +11,20 @@
           </a-col>
           <a-col :lg="6">
             <a-form-item label="单位性质" name="companyNature">
-              <a-select placeholder="请选择单位性质" v-model:value="queryParam.companyNature" allow-clear>
-                <a-select-option value="国有企业">国有企业</a-select-option>
-                <a-select-option value="民营企业">民营企业</a-select-option>
-                <a-select-option value="外资企业">外资企业</a-select-option>
-                <a-select-option value="事业单位">事业单位</a-select-option>
-                <a-select-option value="社会团体">社会团体</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.companyNature" :options="companyNatureOptions" placeholder="请选择单位性质" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
             <a-form-item label="工作地点" name="workLocation">
-              <a-input placeholder="请输入工作地点" v-model:value="queryParam.workLocation" allow-clear></a-input>
+              <a-tree-select
+                v-model:value="queryParam.workLocation"
+                :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="6">
@@ -32,18 +34,12 @@
           </a-col>
           <a-col :lg="6">
             <a-form-item label="发布状态" name="publishStatus">
-              <a-select placeholder="请选择发布状态" v-model:value="queryParam.publishStatus" allow-clear>
-                <a-select-option value="0">未发布</a-select-option>
-                <a-select-option value="1">已发布</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.publishStatus" :options="publishStatusOptions" placeholder="请选择发布状态" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
             <a-form-item label="报名状态" name="applyStatus">
-              <a-select placeholder="请选择报名状态" v-model:value="queryParam.applyStatus" allow-clear>
-                <a-select-option value="01">报名中</a-select-option>
-                <a-select-option value="02">人员已满</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.applyStatus" :options="applyStatusOptions" placeholder="请选择报名状态" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
@@ -61,6 +57,24 @@
       <template #tableTitle>
         <a-button v-auth="'internship_post:exportXls'" preIcon="ant-design:export-outlined" type="primary" @click="onExportXls"> 导出</a-button>
       </template>
+      <!--字典翻译插槽-->
+      <template #bodyCell="{ column, text }">
+        <template v-if="column.dataIndex === 'workLocation'">
+          {{ (xzqhMap[text] || text || '').replace('广东省湛江市','') }}
+        </template>
+        <template v-else-if="column.dataIndex === 'companyNature'">
+          {{ getDictText('company_nature', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'publishStatus'">
+          {{ getDictText('publish_status', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'applyStatus'">
+          {{ getDictText('apply_status', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'identityRequirement'">
+          {{ text ? String(text).split(',').map(val => getDictText('identity_requirement', val.trim()) || val.trim()).join('、') : '-' }}
+        </template>
+      </template>
       <!--操作栏-->
       <template #action="{ record }">
         <TableAction :actions="getTableAction(record)" />
@@ -72,19 +86,79 @@
 </template>
 
 <script lang="ts" name="internshipPost" setup>
-  import { reactive, ref } from 'vue';
+  import { reactive, ref, computed } from 'vue';
   import { BasicTable, TableAction } from '/@/components/Table';
   import { useListPage } from '/@/hooks/system/useListPage';
+  import { useDict } from '/@/hooks/dictionary/useDict';
   import { columns } from './InternshipPost.data';
   import { getExportUrl, list } from './InternshipPost.api';
   import InternshipPostModal from './components/InternshipPostModal.vue';
   import { getDateByPicker } from '/@/utils';
+  import { defHttp } from '/@/utils/http/axios';
 
   const fieldPickers = reactive({});
 
   const formRef = ref();
   const queryParam = reactive<any>({});
   const registerModal = ref();
+
+  const { getDictText, getDictOptions } = useDict(['company_nature', 'publish_status', 'apply_status', 'identity_requirement', 'XZQH']);
+
+  const companyNatureOptions = computed(() => getDictOptions('company_nature'));
+  const publishStatusOptions = computed(() => getDictOptions('publish_status'));
+  const applyStatusOptions = computed(() => getDictOptions('apply_status'));
+
+  const xzqhMap = computed(() => {
+    const map: Record<string, string> = {};
+    const items = getDictOptions('XZQH');
+    for (const item of items) {
+      map[item.value] = item.label;
+    }
+    return map;
+  });
+
+  const searchTreeData = ref<any[]>([]);
+
+  async function loadZhanjiangDistricts() {
+    try {
+      const rootRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: '' },
+      }, { isTransformResponse: false });
+      if (!rootRes.success || !rootRes.result?.length) return;
+
+      const gdNode = rootRes.result.find((n: any) => n.title?.includes('广东'));
+      if (!gdNode) return;
+
+      const gdRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: gdNode.key },
+      }, { isTransformResponse: false });
+      if (!gdRes.success || !gdRes.result?.length) return;
+
+      const zjNode = gdRes.result.find((n: any) => n.title?.includes('湛江'));
+      if (!zjNode) return;
+
+      const zjRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: zjNode.key },
+      }, { isTransformResponse: false });
+      if (!zjRes.success || !zjRes.result?.length) return;
+
+      searchTreeData.value = zjRes.result.map((item: any) => ({
+        key: item.key, title: item.title, value: item.key, isLeaf: true,
+      }));
+    } catch (e) {
+      console.error('加载湛江区县失败:', e);
+    }
+  }
+
+  async function loadSearchTreeChildren(_treeNode: any) {
+    // 区县已标记为叶子节点,不会触发懒加载
+  }
+
+  loadZhanjiangDistricts();
+
   //注册table数据
   const { prefixCls, tableContext, onExportXls } = useListPage({
     tableProps: {
@@ -125,24 +199,15 @@
     sm: 20,
   });
 
-  /**
-   * 详情
-   */
   function handleDetail(record: Recordable) {
     registerModal.value.disableSubmit = true;
     registerModal.value.edit(record);
   }
 
-  /**
-   * 成功回调
-   */
   function handleSuccess() {
     reload();
   }
 
-  /**
-   * 操作栏 - 仅保留查看
-   */
   function getTableAction(record) {
     return [
       {
@@ -152,19 +217,12 @@
     ];
   }
 
-  /**
-   * 查询
-   */
   function searchQuery() {
     reload();
   }
 
-  /**
-   * 重置
-   */
   function searchReset() {
     formRef.value.resetFields();
-    //刷新数据
     reload();
   }
 </script>

+ 0 - 7
jeecgboot-vue3/src/views/jobrecommend/JobRecommend.data.ts

@@ -1,10 +1,5 @@
 import { BasicColumn } from '/@/components/Table';
 
-// 推荐类型映射
-const recommendTypeMap: Record<string, string> = { '1': '岗位推荐到人', '2': '人推荐到岗位' };
-// 推荐状态映射
-const recommendStatusMap: Record<string, string> = { '0': '待查看', '1': '已查看', '2': '已接受', '3': '已拒绝' };
-
 // 列表数据
 export const columns: BasicColumn[] = [
   {
@@ -46,13 +41,11 @@ export const columns: BasicColumn[] = [
     title: '推荐类型',
     align: 'center',
     dataIndex: 'recommendType',
-    customRender: ({ text }) => recommendTypeMap[text] || text || '-',
   },
   {
     title: '推荐状态',
     align: 'center',
     dataIndex: 'recommendStatus',
-    customRender: ({ text }) => recommendStatusMap[text] || text || '-',
   },
   {
     title: '推荐人',

+ 24 - 31
jeecgboot-vue3/src/views/jobrecommend/JobRecommendList.vue

@@ -11,20 +11,12 @@
           </a-col>
           <a-col :lg="6">
             <a-form-item label="推荐类型" name="recommendType">
-              <a-select placeholder="请选择推荐类型" v-model:value="queryParam.recommendType" allow-clear>
-                <a-select-option value="1">岗位推荐到人</a-select-option>
-                <a-select-option value="2">人推荐到岗位</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.recommendType" :options="recommendTypeOptions" placeholder="请选择推荐类型" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
             <a-form-item label="推荐状态" name="recommendStatus">
-              <a-select placeholder="请选择推荐状态" v-model:value="queryParam.recommendStatus" allow-clear>
-                <a-select-option value="0">待查看</a-select-option>
-                <a-select-option value="1">已查看</a-select-option>
-                <a-select-option value="2">已接受</a-select-option>
-                <a-select-option value="3">已拒绝</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.recommendStatus" :options="recommendStatusOptions" placeholder="请选择推荐状态" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
@@ -57,6 +49,21 @@
       <template #tableTitle>
         <a-button v-auth="'job_recommend:exportXls'" preIcon="ant-design:export-outlined" type="primary" @click="onExportXls"> 导出</a-button>
       </template>
+      <!--字典翻译插槽-->
+      <template #bodyCell="{ column, text }">
+        <template v-if="column.dataIndex === 'gender'">
+          {{ getDictText('Gender', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'education'">
+          {{ getDictText('Education', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'recommendType'">
+          {{ getDictText('recommend_type', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'recommendStatus'">
+          {{ getDictText('recommend_status', text) || text }}
+        </template>
+      </template>
       <!--操作栏-->
       <template #action="{ record }">
         <TableAction :actions="getTableAction(record)" />
@@ -68,10 +75,11 @@
 </template>
 
 <script lang="ts" name="jobRecommend" setup>
-  import { reactive, ref } from 'vue';
+  import { reactive, ref, computed } from 'vue';
   import dayjs from 'dayjs';
   import { BasicTable, TableAction } from '/@/components/Table';
   import { useListPage } from '/@/hooks/system/useListPage';
+  import { useDict } from '/@/hooks/dictionary/useDict';
   import { columns } from './JobRecommend.data';
   import { getExportUrl, list } from './JobRecommend.api';
   import JobRecommendModal from './components/JobRecommendModal.vue';
@@ -82,12 +90,13 @@
   const formRef = ref();
   const queryParam = reactive<any>({});
   const registerModal = ref();
-  // 推荐时间范围选择器绑定值
   const recommendTimeRange = ref<any>(null);
 
-  /**
-   * 推荐时间范围变化事件
-   */
+  const { getDictText, getDictOptions } = useDict(['recommend_type', 'recommend_status', 'Gender', 'Education']);
+
+  const recommendTypeOptions = computed(() => getDictOptions('recommend_type'));
+  const recommendStatusOptions = computed(() => getDictOptions('recommend_status'));
+
   function onRecommendTimeChange(dates: any) {
     if (dates && dates.length === 2) {
       queryParam.recommendTimeBegin = dates[0];
@@ -138,24 +147,15 @@
     sm: 20,
   });
 
-  /**
-   * 详情
-   */
   function handleDetail(record: Recordable) {
     registerModal.value.disableSubmit = true;
     registerModal.value.edit(record);
   }
 
-  /**
-   * 成功回调
-   */
   function handleSuccess() {
     reload();
   }
 
-  /**
-   * 操作栏 - 仅保留查看
-   */
   function getTableAction(record) {
     return [
       {
@@ -165,22 +165,15 @@
     ];
   }
 
-  /**
-   * 查询
-   */
   function searchQuery() {
     reload();
   }
 
-  /**
-   * 重置
-   */
   function searchReset() {
     formRef.value.resetFields();
     recommendTimeRange.value = null;
     queryParam.recommendTimeBegin = '';
     queryParam.recommendTimeEnd = '';
-    //刷新数据
     reload();
   }
 </script>

+ 2 - 2
jeecgboot-vue3/src/views/personalstatus/PersonalStatus.api.ts

@@ -11,8 +11,8 @@ export function list(params) {
   return defHttp.get({ url: Api.List, params });
 }
 
-export function saveTag(params) {
-  return defHttp.post({ url: Api.SaveTag, params });
+export function saveTag(data) {
+  return defHttp.post({ url: Api.SaveTag, data });
 }
 
 export function getTag(params) {

+ 211 - 7
jeecgboot-vue3/src/views/personalstatus/PersonalStatusList.vue

@@ -10,7 +10,15 @@
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
             <a-form-item label="户口所属区县" name="householdDistrict">
-              <a-input placeholder="请输入户口所属区县" v-model:value="queryParam.householdDistrict" allow-clear></a-input>
+              <a-tree-select
+                v-model:value="queryParam.householdDistrict"
+                :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="6" :md="8" :sm="24">
@@ -24,8 +32,12 @@
             </a-form-item>
           </a-col>
           <a-col :lg="6" :md="8" :sm="24">
-            <a-form-item label="年龄" name="age">
-              <a-input placeholder="请输入年龄" v-model:value="queryParam.age" allow-clear></a-input>
+            <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">
@@ -76,6 +88,50 @@
     </BasicTable>
 
     <PersonalStatusModal ref="detailModalRef" />
+
+    <a-modal
+      v-model:visible="tagModalVisible"
+      title="修改自定义标签"
+      @cancel="closeTagModal"
+      @ok="handleTagSubmit"
+      :confirmLoading="tagSaving"
+      width="520px"
+    >
+      <div v-if="tagModalRecord" class="tag-modal">
+        <div class="tag-modal-header">
+          <span>当前人员:<em>{{ tagModalRecord.fullName }}</em></span>
+        </div>
+        <a-divider style="margin: 8px 0" />
+        <a-textarea v-model:value="tagInput" placeholder="请输入自定义标签,多个标签用逗号分隔" :rows="3" />
+      </div>
+    </a-modal>
+
+    <a-modal
+      v-model:visible="msgModalVisible"
+      title="发送消息"
+      @cancel="closeMsgModal"
+      @ok="handleMsgSubmit"
+      :confirmLoading="msgSaving"
+      width="800px"
+    >
+      <a-form v-if="msgRecord" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
+        <a-form-item label="接收人员">
+          <a-input :value="msgRecord.fullName" disabled />
+        </a-form-item>
+        <a-form-item label="消息主题" required>
+          <a-input v-model:value="msgForm.subject" placeholder="请输入消息主题" />
+        </a-form-item>
+        <a-form-item label="消息内容" required>
+          <Tinymce v-model="msgForm.content" :height="300" toolbar="undo redo | styleselect | bold italic underline strikethrough | forecolor backcolor | fontsize | alignleft aligncenter alignright | bullist numlist | removeformat" plugins="lists" />
+        </a-form-item>
+        <a-form-item label="推送人">
+          <a-input :value="msgForm.sender" disabled />
+        </a-form-item>
+        <a-form-item label="推送时间">
+          <a-input :value="msgForm.sendTime" disabled />
+        </a-form-item>
+      </a-form>
+    </a-modal>
   </div>
 </template>
 
@@ -85,20 +141,67 @@
   import { useListPage } from '/@/hooks/system/useListPage';
   import { useDict } from '/@/hooks/dictionary/useDict';
   import { columns } from './PersonalStatus.data';
-  import { list, getExportUrl } from './PersonalStatus.api';
+  import { list, getExportUrl, saveTag, getTag } from './PersonalStatus.api';
   import { useMessage } from '/@/hooks/web/useMessage';
+import { useUserStore } from '/@/store/modules/user';
+import { defHttp } from '/@/utils/http/axios';
 import PersonalStatusModal from './components/PersonalStatusModal.vue';
 
   const formRef = ref();
   const queryParam = reactive<any>({});
   const { createMessage } = useMessage();
+  const userStore = useUserStore();
 
   const { getDictText, getDictOptions } = useDict(['Gender', 'Education', 'JobSeekerStatus', 'employment_status']);
 
   const genderOptions = computed(() => getDictOptions('Gender'));
   const educationOptions = computed(() => getDictOptions('Education'));
   const jobSearchStatusOptions = computed(() => getDictOptions('JobSeekerStatus'));
-  const employmentStatusOptions = computed(() => getDictOptions('employment_status'));
+  const employmentStatusOptions = computed(() =>
+    getDictOptions('employment_status').map((item) => ({ ...item, value: item.label }))
+  );
+
+  const searchTreeData = ref<any[]>([]);
+
+  async function loadZhanjiangDistricts() {
+    try {
+      const rootRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: '' },
+      }, { isTransformResponse: false });
+      if (!rootRes.success || !rootRes.result?.length) return;
+
+      const gdNode = rootRes.result.find((n: any) => n.title?.includes('广东'));
+      if (!gdNode) return;
+
+      const gdRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: gdNode.key },
+      }, { isTransformResponse: false });
+      if (!gdRes.success || !gdRes.result?.length) return;
+
+      const zjNode = gdRes.result.find((n: any) => n.title?.includes('湛江'));
+      if (!zjNode) return;
+
+      const zjRes = await defHttp.get({
+        url: '/system/dictionary/getDictTreeChildren',
+        params: { dictionaryCode: 'XZQH', parentCode: zjNode.key },
+      }, { isTransformResponse: false });
+      if (!zjRes.success || !zjRes.result?.length) return;
+
+      searchTreeData.value = zjRes.result.map((item: any) => ({
+        key: item.key, title: item.title, value: item.key, isLeaf: true,
+      }));
+    } catch (e) {
+      console.error('加载湛江区县失败:', e);
+    }
+  }
+
+  async function loadSearchTreeChildren(treeNode) {
+    // 区县已标记为叶子节点,不会触发懒加载
+  }
+
+  loadZhanjiangDistricts();
 
   const { tableContext, onExportXls } = useListPage({
     tableProps: {
@@ -125,6 +228,97 @@ import PersonalStatusModal from './components/PersonalStatusModal.vue';
 
   const detailModalRef = ref();
 
+  const tagModalVisible = ref<boolean>(false);
+  const tagModalRecord = ref<any>(null);
+  const tagSaving = ref<boolean>(false);
+  const tagInput = ref<string>('');
+
+  const msgModalVisible = ref<boolean>(false);
+  const msgRecord = ref<any>(null);
+  const msgSaving = ref<boolean>(false);
+  const msgForm = reactive({ subject: '', content: '', sender: '', sendTime: '' });
+
+  function openMsgModal(record: Recordable) {
+    msgRecord.value = record;
+    msgForm.subject = '';
+    msgForm.content = '';
+    msgForm.sender = userStore.getUserInfo?.realname || userStore.userInfo?.realname || '';
+    msgForm.sendTime = new Date().toLocaleString('zh-CN');
+    msgModalVisible.value = true;
+  }
+
+  function closeMsgModal() {
+    msgModalVisible.value = false;
+    msgRecord.value = null;
+  }
+
+  async function handleMsgSubmit() {
+    if (!msgForm.subject.trim()) {
+      createMessage.warning('请输入消息主题');
+      return;
+    }
+    if (!msgForm.content.trim()) {
+      createMessage.warning('请输入消息内容');
+      return;
+    }
+    msgSaving.value = true;
+    try {
+      await defHttp.post({
+        url: '/notification/send',
+        data: {
+          moduleType: 'personal_status',
+          subject: msgForm.subject,
+          content: msgForm.content,
+          targets: [{ targetType: 'personal', targetId: msgRecord.value.id }],
+        },
+      });
+      createMessage.success('消息发送成功');
+      closeMsgModal();
+      reload();
+    } catch (e) {
+      createMessage.error(e.message || '消息发送失败');
+    } finally {
+      msgSaving.value = false;
+    }
+  }
+
+  async function openTagModal(record: Recordable) {
+    tagModalRecord.value = record;
+    tagInput.value = '';
+    tagModalVisible.value = true;
+    try {
+      const res = await getTag({ personalId: record.id });
+      if (res?.customTags) {
+        tagInput.value = res.customTags;
+      }
+    } catch (e) {
+      console.error('加载标签失败:', e);
+    }
+  }
+
+  function closeTagModal() {
+    tagModalVisible.value = false;
+    tagModalRecord.value = null;
+    tagInput.value = '';
+  }
+
+  async function handleTagSubmit() {
+    tagSaving.value = true;
+    try {
+      await saveTag({
+        personalId: tagModalRecord.value.id,
+        customTags: tagInput.value,
+      });
+      createMessage.success('标签保存成功');
+      closeTagModal();
+      reload();
+    } catch (e) {
+      createMessage.error(e.message || '标签保存失败');
+    } finally {
+      tagSaving.value = false;
+    }
+  }
+
   function handleDetail(record: Recordable) {
     detailModalRef.value?.detail(record);
   }
@@ -132,14 +326,16 @@ import PersonalStatusModal from './components/PersonalStatusModal.vue';
   function getTableAction(record) {
     return [
       { label: '查看', onClick: handleDetail.bind(null, record) },
-      { label: '发送消息', onClick: () => createMessage.warning('发送消息功能待开发') },
-      { label: '修改标签', onClick: () => createMessage.warning('修改标签功能待开发') },
+      { label: '发送消息', onClick: () => openMsgModal(record) },
+      { label: '修改标签', onClick: () => openTagModal(record) },
     ];
   }
 
   function searchQuery() { reload(); }
   function searchReset() {
     formRef.value.resetFields();
+    queryParam.ageBegin = undefined;
+    queryParam.ageEnd = undefined;
     selectedRowKeys.value = [];
     reload();
   }
@@ -150,4 +346,12 @@ import PersonalStatusModal from './components/PersonalStatusModal.vue';
     .ant-form-item:not(.ant-form-item-with-help) { margin-bottom: 16px; height: 32px; }
     :deep(.ant-picker), :deep(.ant-input-number) { width: 100%; }
   }
+
+  .tag-modal {
+    .tag-modal-header {
+      font-size: 14px;
+      margin-bottom: 4px;
+      em { font-style: normal; color: #1890ff; font-weight: 600; }
+    }
+  }
 </style>

+ 12 - 0
jeecgboot-vue3/src/views/policyfile/PolicyFile.api.ts

@@ -11,6 +11,8 @@ enum Api {
   FileEdit = '/policyfile/file/edit',
   FileDelete = '/policyfile/file/delete',
   FileDeleteBatch = '/policyfile/file/deleteBatch',
+  FileView = '/policyfile/file/view',
+  FileDownload = '/policyfile/file/download',
 }
 
 /** 目录树列表 */
@@ -73,3 +75,13 @@ export function saveOrUpdateFile(values, isUpdate) {
 export function saveOrUpdateCategory(values, isUpdate) {
   return isUpdate ? editCategory(values) : addCategory(values);
 }
+
+/** 在线查看文件URL */
+export function getViewUrl(id: string) {
+  return `${Api.FileView}?id=${id}`;
+}
+
+/** 下载文件URL */
+export function getDownloadUrl(id: string) {
+  return `${Api.FileDownload}?id=${id}`;
+}

+ 79 - 34
jeecgboot-vue3/src/views/policyfile/PolicyFileList.vue

@@ -59,26 +59,45 @@
       </a-col>
 
       <a-col :flex="showCategoryPanel ? 'auto' : '1 1 auto'" :xl="showCategoryPanel ? 18 : 23" :lg="showCategoryPanel ? 16 : 23" :md="24" style="margin-bottom: 10px">
-        <BasicTable @register="registerTable" :rowSelection="rowSelection">
+        <!-- 搜索表单 -->
+        <div class="jeecg-basic-table-form-container">
+          <a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
+            <a-row :gutter="24">
+              <a-col :lg="6" :md="8" :sm="24">
+                <a-form-item label="文件名称" name="name">
+                  <a-input v-model:value="queryParam.name" placeholder="搜索文件名称" allow-clear />
+                </a-form-item>
+              </a-col>
+              <a-col :lg="6" :md="8" :sm="24">
+                <a-form-item>
+                  <a-space>
+                    <a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
+                    <a-button preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button>
+                  </a-space>
+                </a-form-item>
+              </a-col>
+            </a-row>
+          </a-form>
+        </div>
+
+        <BasicTable @register="registerTable">
           <template #tableTitle>
             <a-row type="flex" justify="space-between" style="width: 100%">
               <a-col>
                 <span v-if="selectedCategoryName" style="font-weight: 500; margin-right: 12px">当前目录:{{ selectedCategoryName }}</span>
                 <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAddFile"> 新增</a-button>
               </a-col>
-              <a-col>
-                <a-input-search
-                  v-model:value="fileNameSearch"
-                  placeholder="搜索文件名称"
-                  style="width: 220px"
-                  allow-clear
-                  @search="onFileNameSearch"
-                />
-              </a-col>
             </a-row>
           </template>
           <template #action="{ record }">
-            <TableAction :actions="getTableAction(record)" />
+            <a-space>
+              <a-button type="link" size="small" @click="handleView(record)">查看</a-button>
+              <a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
+              <a-popconfirm title="是否确认删除" @confirm="handleDelete(record)">
+                <a-button type="link" size="small" danger>删除</a-button>
+              </a-popconfirm>
+              <a-button type="link" size="small" @click="handleDownload(record)">下载</a-button>
+            </a-space>
           </template>
         </BasicTable>
       </a-col>
@@ -90,26 +109,45 @@
 </template>
 
 <script lang="ts" setup name="policyfile">
-  import { ref, computed, onMounted } from 'vue';
-  import { BasicTable, TableAction } from '/@/components/Table';
+  import { ref, reactive, computed, onMounted } from 'vue';
+  import { BasicTable } from '/@/components/Table';
   import { useModal } from '/@/components/Modal';
   import { useListPage } from '/@/hooks/system/useListPage';
   import { columns } from './PolicyFile.data';
-  import { list, deleteFile, batchDeleteFile, getCategoryTree, deleteCategory } from './PolicyFile.api';
+  import { list, deleteFile, getCategoryTree, deleteCategory, getViewUrl, getDownloadUrl } from './PolicyFile.api';
   import PolicyFileModal from './components/PolicyFileModal.vue';
   import PolicyCategoryModal from './components/PolicyCategoryModal.vue';
 
   const [registerModal, { openModal }] = useModal();
   const [registerCategoryModal, { openModal: openCategoryModal }] = useModal();
 
+  const formRef = ref();
+  const queryParam = reactive<any>({});
   const categoryTree = ref<any[]>([]);
   const selectedCategoryId = ref<string>('');
   const selectedCategoryName = ref<string>('全部文件');
   const categorySearchText = ref<string>('');
   const showCategoryPanel = ref<boolean>(true);
-  const fileNameSearch = ref<string>('');
 
-  function onFileNameSearch() { reload(); }
+  // 递归收集所有子孙目录ID
+  function getAllDescendantIds(tree: any[], id: string): string[] {
+    const ids: string[] = [id];
+    const findAndCollect = (nodes: any[], targetId: string) => {
+      for (const node of nodes) {
+        if (node.id === targetId || ids.includes(node.id)) {
+          if (node.children) {
+            for (const child of node.children) {
+              ids.push(child.id);
+              findAndCollect(node.children, child.id);
+            }
+          }
+        }
+        if (node.children) findAndCollect(node.children, targetId);
+      }
+    };
+    findAndCollect(tree, id);
+    return ids;
+  }
 
   const filteredCategoryTree = computed(() => {
     if (!categorySearchText.value) return categoryTree.value;
@@ -164,20 +202,31 @@
       columns,
       canResize: false,
       useSearchForm: false,
-      actionColumn: { width: 180, fixed: 'right' },
+      actionColumn: { width: 260, fixed: 'right' },
       beforeFetch: (params) => {
+        // 目录级联:传递所有子孙目录ID
         if (selectedCategoryId.value) {
           params.categoryId = selectedCategoryId.value;
         }
-        if (fileNameSearch.value) {
-          params.name = fileNameSearch.value;
+        // 文件名搜索
+        if (queryParam.name) {
+          params.name = queryParam.name;
         }
         return params;
       },
     },
   });
 
-  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+  const [registerTable, { reload }] = tableContext;
+  const labelCol = reactive({ xs: 24, sm: 6, md: 8, xl: 6, xxl: 6 });
+  const wrapperCol = reactive({ xs: 24, sm: 18 });
+
+  function searchQuery() { reload(); }
+  function searchReset() {
+    formRef.value?.resetFields();
+    queryParam.name = '';
+    reload();
+  }
 
   onMounted(() => { loadCategoryTree(); });
 
@@ -210,33 +259,29 @@
     openModal(true, { record, isUpdate: true, showFooter: true });
   }
 
-  function handleDetail(record: Recordable) {
-    openModal(true, { record, isUpdate: true, showFooter: false });
+  function handleView(record: Recordable) {
+    window.open(getViewUrl(record.id), '_blank');
   }
 
-  async function handleDelete(record) {
-    await deleteFile({ id: record.id }, handleSuccess);
+  function handleDownload(record: Recordable) {
+    window.open(getDownloadUrl(record.id), '_blank');
   }
 
-  async function batchHandleDelete() {
-    await batchDeleteFile({ ids: selectedRowKeys.value }, handleSuccess);
+  async function handleDelete(record) {
+    await deleteFile({ id: record.id }, handleSuccess);
   }
 
   function handleSuccess() {
-    selectedRowKeys.value = [];
     reload();
   }
-
-  function getTableAction(record) {
-    return [
-      { label: '编辑', onClick: handleEdit.bind(null, record) },
-      { label: '删除', popConfirm: { title: '是否确认删除', confirm: handleDelete.bind(null, record) } },
-    ];
-  }
 </script>
 <style scoped>
   .p-4 { padding: 16px; }
   .category-tree-node { display: inline-flex; align-items: center; justify-content: space-between; width: 100%; }
   .tree-node-actions { display: none; margin-left: 8px; }
   .category-tree-node:hover .tree-node-actions { display: inline-flex; gap: 4px; }
+  .jeecg-basic-table-form-container {
+    padding: 0 0 8px 0;
+    .ant-form-item:not(.ant-form-item-with-help) { margin-bottom: 16px; height: 32px; }
+  }
 </style>

+ 2 - 2
jeecgboot-vue3/src/views/profilechange/ProfileChange.api.ts

@@ -11,8 +11,8 @@ export function list(params) {
   return defHttp.get({ url: Api.List, params });
 }
 
-export function saveTag(params) {
-  return defHttp.post({ url: Api.SaveTag, params });
+export function saveTag(data) {
+  return defHttp.post({ url: Api.SaveTag, data });
 }
 
 export function getTag(params) {

+ 149 - 3
jeecgboot-vue3/src/views/profilechange/ProfileChangeList.vue

@@ -68,6 +68,50 @@
     </BasicTable>
 
     <ProfileChangeModal ref="detailModalRef" />
+
+    <a-modal
+      v-model:visible="tagModalVisible"
+      title="修改自定义标签"
+      @cancel="closeTagModal"
+      @ok="handleTagSubmit"
+      :confirmLoading="tagSaving"
+      width="520px"
+    >
+      <div v-if="tagModalRecord" class="tag-modal">
+        <div class="tag-modal-header">
+          <span>当前人员:<em>{{ tagModalRecord.fullName }}</em></span>
+        </div>
+        <a-divider style="margin: 8px 0" />
+        <a-textarea v-model:value="tagInput" placeholder="请输入自定义标签,多个标签用逗号分隔" :rows="3" />
+      </div>
+    </a-modal>
+
+    <a-modal
+      v-model:visible="msgModalVisible"
+      title="发送消息"
+      @cancel="closeMsgModal"
+      @ok="handleMsgSubmit"
+      :confirmLoading="msgSaving"
+      width="800px"
+    >
+      <a-form v-if="msgRecord" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
+        <a-form-item label="接收人员">
+          <a-input :value="msgRecord.fullName" disabled />
+        </a-form-item>
+        <a-form-item label="消息主题" required>
+          <a-input v-model:value="msgForm.subject" placeholder="请输入消息主题" />
+        </a-form-item>
+        <a-form-item label="消息内容" required>
+          <Tinymce v-model="msgForm.content" :height="300" toolbar="undo redo | styleselect | bold italic underline strikethrough | forecolor backcolor | fontsize | alignleft aligncenter alignright | bullist numlist | removeformat" plugins="lists" />
+        </a-form-item>
+        <a-form-item label="推送人">
+          <a-input :value="msgForm.sender" disabled />
+        </a-form-item>
+        <a-form-item label="推送时间">
+          <a-input :value="msgForm.sendTime" disabled />
+        </a-form-item>
+      </a-form>
+    </a-modal>
   </div>
 </template>
 
@@ -77,13 +121,16 @@
   import { useListPage } from '/@/hooks/system/useListPage';
   import { useDict } from '/@/hooks/dictionary/useDict';
   import { columns } from './ProfileChange.data';
-  import { list, getExportUrl } from './ProfileChange.api';
+  import { list, getExportUrl, saveTag, getTag } from './ProfileChange.api';
   import { useMessage } from '/@/hooks/web/useMessage';
+import { useUserStore } from '/@/store/modules/user';
+import { defHttp } from '/@/utils/http/axios';
 import ProfileChangeModal from './components/ProfileChangeModal.vue';
 
   const formRef = ref();
   const queryParam = reactive<any>({});
   const { createMessage } = useMessage();
+  const userStore = useUserStore();
 
   const { getDictText, getDictOptions } = useDict(['Gender', 'Education', 'profile_status']);
 
@@ -117,6 +164,97 @@ import ProfileChangeModal from './components/ProfileChangeModal.vue';
 
   const detailModalRef = ref();
 
+  const tagModalVisible = ref<boolean>(false);
+  const tagModalRecord = ref<any>(null);
+  const tagSaving = ref<boolean>(false);
+  const tagInput = ref<string>('');
+
+  async function openTagModal(record: Recordable) {
+    tagModalRecord.value = record;
+    tagInput.value = '';
+    tagModalVisible.value = true;
+    try {
+      const res = await getTag({ personalId: record.personalId });
+      if (res?.customTags) {
+        tagInput.value = res.customTags;
+      }
+    } catch (e) {
+      console.error('加载标签失败:', e);
+    }
+  }
+
+  function closeTagModal() {
+    tagModalVisible.value = false;
+    tagModalRecord.value = null;
+    tagInput.value = '';
+  }
+
+  async function handleTagSubmit() {
+    tagSaving.value = true;
+    try {
+      await saveTag({
+        personalId: tagModalRecord.value.personalId,
+        customTags: tagInput.value,
+      });
+      createMessage.success('标签保存成功');
+      closeTagModal();
+      reload();
+    } catch (e) {
+      createMessage.error(e.message || '标签保存失败');
+    } finally {
+      tagSaving.value = false;
+    }
+  }
+
+  const msgModalVisible = ref<boolean>(false);
+  const msgRecord = ref<any>(null);
+  const msgSaving = ref<boolean>(false);
+  const msgForm = reactive({ subject: '', content: '', sender: '', sendTime: '' });
+
+  function openMsgModal(record: Recordable) {
+    msgRecord.value = record;
+    msgForm.subject = '';
+    msgForm.content = '';
+    msgForm.sender = userStore.getUserInfo?.realname || userStore.userInfo?.realname || '';
+    msgForm.sendTime = new Date().toLocaleString('zh-CN');
+    msgModalVisible.value = true;
+  }
+
+  function closeMsgModal() {
+    msgModalVisible.value = false;
+    msgRecord.value = null;
+  }
+
+  async function handleMsgSubmit() {
+    if (!msgForm.subject.trim()) {
+      createMessage.warning('请输入消息主题');
+      return;
+    }
+    if (!msgForm.content.trim()) {
+      createMessage.warning('请输入消息内容');
+      return;
+    }
+    msgSaving.value = true;
+    try {
+      await defHttp.post({
+        url: '/notification/send',
+        data: {
+          moduleType: 'profile_change',
+          subject: msgForm.subject,
+          content: msgForm.content,
+          targets: [{ targetType: 'personal', targetId: msgRecord.value.personalId }],
+        },
+      });
+      createMessage.success('消息发送成功');
+      closeMsgModal();
+      reload();
+    } catch (e) {
+      createMessage.error(e.message || '消息发送失败');
+    } finally {
+      msgSaving.value = false;
+    }
+  }
+
   function handleDetail(record: Recordable) {
     detailModalRef.value?.detail(record);
   }
@@ -124,8 +262,8 @@ import ProfileChangeModal from './components/ProfileChangeModal.vue';
   function getTableAction(record) {
     return [
       { label: '查看', onClick: handleDetail.bind(null, record) },
-      { label: '发送消息', onClick: () => createMessage.warning('发送消息功能待开发') },
-      { label: '修改标签', onClick: () => createMessage.warning('修改标签功能待开发') },
+      { label: '发送消息', onClick: () => openMsgModal(record) },
+      { label: '修改标签', onClick: () => openTagModal(record) },
     ];
   }
 
@@ -142,4 +280,12 @@ import ProfileChangeModal from './components/ProfileChangeModal.vue';
     .ant-form-item:not(.ant-form-item-with-help) { margin-bottom: 16px; height: 32px; }
     :deep(.ant-picker), :deep(.ant-input-number) { width: 100%; }
   }
+
+  .tag-modal {
+    .tag-modal-header {
+      font-size: 14px;
+      margin-bottom: 4px;
+      em { font-style: normal; color: #1890ff; font-weight: 600; }
+    }
+  }
 </style>

+ 0 - 8
jeecgboot-vue3/src/views/welfarepost/WelfarePost.data.ts

@@ -1,10 +1,5 @@
 import { BasicColumn } from '/@/components/Table';
 
-// 发布状态映射
-const publishStatusMap: Record<string, string> = { '0': '未发布', '1': '已发布' };
-// 岗位状态映射
-const postStatusMap: Record<string, string> = { '招聘中': '招聘中', '已满': '已满', '已关闭': '已关闭' };
-
 // 列表数据
 export const columns: BasicColumn[] = [
   {
@@ -21,7 +16,6 @@ export const columns: BasicColumn[] = [
     title: '岗位类型',
     align: 'center',
     dataIndex: 'postType',
-    customRender: ({ text }) => text || '-',
   },
   {
     title: '工作地点',
@@ -47,13 +41,11 @@ export const columns: BasicColumn[] = [
     title: '发布状态',
     align: 'center',
     dataIndex: 'publishStatus',
-    customRender: ({ text }) => publishStatusMap[text] || text || '-',
   },
   {
     title: '岗位状态',
     align: 'center',
     dataIndex: 'postStatus',
-    customRender: ({ text }) => postStatusMap[text] || text || '-',
   },
 ];
 

+ 24 - 32
jeecgboot-vue3/src/views/welfarepost/WelfarePostList.vue

@@ -16,29 +16,17 @@
           </a-col>
           <a-col :lg="6">
             <a-form-item label="岗位类型" name="postType">
-              <a-select placeholder="请选择岗位类型" v-model:value="queryParam.postType" allow-clear>
-                <a-select-option value="社区服务">社区服务</a-select-option>
-                <a-select-option value="城市管理">城市管理</a-select-option>
-                <a-select-option value="公共管理">公共管理</a-select-option>
-                <a-select-option value="后勤服务">后勤服务</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.postType" :options="postTypeOptions" placeholder="请选择岗位类型" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
             <a-form-item label="发布状态" name="publishStatus">
-              <a-select placeholder="请选择发布状态" v-model:value="queryParam.publishStatus" allow-clear>
-                <a-select-option value="0">未发布</a-select-option>
-                <a-select-option value="1">已发布</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.publishStatus" :options="publishStatusOptions" placeholder="请选择发布状态" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
             <a-form-item label="岗位状态" name="postStatus">
-              <a-select placeholder="请选择岗位状态" v-model:value="queryParam.postStatus" allow-clear>
-                <a-select-option value="招聘中">招聘中</a-select-option>
-                <a-select-option value="已满">已满</a-select-option>
-                <a-select-option value="已关闭">已关闭</a-select-option>
-              </a-select>
+              <a-select v-model:value="queryParam.postStatus" :options="postStatusOptions" placeholder="请选择岗位状态" allow-clear />
             </a-form-item>
           </a-col>
           <a-col :lg="6">
@@ -56,6 +44,18 @@
       <template #tableTitle>
         <a-button v-auth="'welfare_post:exportXls'" preIcon="ant-design:export-outlined" type="primary" @click="onExportXls"> 导出</a-button>
       </template>
+      <!--字典翻译插槽-->
+      <template #bodyCell="{ column, text }">
+        <template v-if="column.dataIndex === 'postType'">
+          {{ getDictText('welfare_post_type', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'publishStatus'">
+          {{ getDictText('publish_status', text) || text }}
+        </template>
+        <template v-else-if="column.dataIndex === 'postStatus'">
+          {{ getDictText('welfare_post_status', text) || text }}
+        </template>
+      </template>
       <!--操作栏-->
       <template #action="{ record }">
         <TableAction :actions="getTableAction(record)" />
@@ -67,9 +67,10 @@
 </template>
 
 <script lang="ts" name="welfarePost" setup>
-  import { reactive, ref } from 'vue';
+  import { reactive, ref, computed } from 'vue';
   import { BasicTable, TableAction } from '/@/components/Table';
   import { useListPage } from '/@/hooks/system/useListPage';
+  import { useDict } from '/@/hooks/dictionary/useDict';
   import { columns } from './WelfarePost.data';
   import { getExportUrl, list } from './WelfarePost.api';
   import WelfarePostModal from './components/WelfarePostModal.vue';
@@ -80,6 +81,13 @@
   const formRef = ref();
   const queryParam = reactive<any>({});
   const registerModal = ref();
+
+  const { getDictText, getDictOptions } = useDict(['welfare_post_type', 'publish_status', 'welfare_post_status']);
+
+  const postTypeOptions = computed(() => getDictOptions('welfare_post_type'));
+  const publishStatusOptions = computed(() => getDictOptions('publish_status'));
+  const postStatusOptions = computed(() => getDictOptions('welfare_post_status'));
+
   //注册table数据
   const { prefixCls, tableContext, onExportXls } = useListPage({
     tableProps: {
@@ -120,24 +128,15 @@
     sm: 20,
   });
 
-  /**
-   * 详情
-   */
   function handleDetail(record: Recordable) {
     registerModal.value.disableSubmit = true;
     registerModal.value.edit(record);
   }
 
-  /**
-   * 成功回调
-   */
   function handleSuccess() {
     reload();
   }
 
-  /**
-   * 操作栏 - 仅保留查看
-   */
   function getTableAction(record) {
     return [
       {
@@ -147,19 +146,12 @@
     ];
   }
 
-  /**
-   * 查询
-   */
   function searchQuery() {
     reload();
   }
 
-  /**
-   * 重置
-   */
   function searchReset() {
     formRef.value.resetFields();
-    //刷新数据
     reload();
   }
 </script>