Browse Source

执法端-计划

dev-V1.0.1
庄锐波 5 months ago
parent
commit
2ed54b8ab5
  1. 48
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/EnterpriseSearchController.java
  2. 91
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnfCpNoticeController.java
  3. 7
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpNotice.java
  4. 10
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java
  5. 15
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpNoticeBo.java
  6. 10
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java
  7. 5
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpNoticeMessageVo.java
  8. 10
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpNoticeVo.java
  9. 12
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java
  10. 20
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpNoticeService.java
  11. 2
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpNoticeMessageServiceImpl.java
  12. 184
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpNoticeServiceImpl.java
  13. 45
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java
  14. 4
      zdxt-web-client/zdxt-efcode-enforcement-app/src/api/enforcement/plan.js
  15. 27
      zdxt-web-client/zdxt-efcode-enforcement-app/src/router/enforcement-app/enf-main-router.js
  16. 6
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/enforcementMy.vue
  17. 888
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_edit.vue
  18. 231
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_list.vue
  19. 277
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_message.vue
  20. 423
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_tab_content.vue
  21. 162
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_edit.vue

48
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/EnterpriseSearchController.java

@ -1,8 +1,13 @@
package com.zdxt.enforcementcode.controller;
import com.zdxt.enforcementcode.domain.vo.EnterpriseSearchVo;
import com.zdxt.enforcementcode.domain.vo.EnterpriseInformationVo;
import com.zdxt.enforcementcode.domain.vo.SsztJbxx110497NewVo;
import com.zdxt.enforcementcode.service.IEnterpriseInformationService;
import com.zdxt.enforcementcode.service.ISsztJbxx110497NewService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.springframework.validation.annotation.Validated;
@ -11,18 +16,22 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 企业搜索
*/
@Validated
@RequiredArgsConstructor
@Slf4j
@RestController
@RequestMapping("/enforcementcode/enterpriseSearch")
public class EnterpriseSearchController extends BaseController {
private final IEnterpriseInformationService enterpriseInformationService;
private final ISsztJbxx110497NewService ssztJbxx110497NewService;
/**
* 根据企业名称模糊搜索
@ -83,4 +92,43 @@ public class EnterpriseSearchController extends BaseController {
public R<List<EnterpriseSearchVo>> searchSsztByNumber(@RequestParam String enterpriseNumber) {
return R.ok(enterpriseInformationService.searchSsztByNumber(enterpriseNumber));
}
/**
* 根据企业信用代码查询联系人和联系电话
* 优先查 enterprise_information,找不到再查 sszt_jbxx110497_new
*
* @param enterpriseNumber 企业统一社会信用代码
* @return { contactPerson, contactPhone }
*/
@GetMapping("/getContactInfo")
public R<Map<String, String>> getContactInfo(@RequestParam String enterpriseNumber) {
Map<String, String> result = new HashMap<>();
if (StringUtils.isBlank(enterpriseNumber)) {
return R.ok(result);
}
try {
// 1. 先查 enterprise_information 表
EnterpriseInformationVo entInfo = enterpriseInformationService.queryByEnterpriseNumber(enterpriseNumber);
if (entInfo != null) {
if (StringUtils.isNotBlank(entInfo.getLegalPerson())) {
result.put("contactPerson", entInfo.getLegalPerson());
}
if (StringUtils.isNotBlank(entInfo.getEnterpriseTel())) {
result.put("contactPhone", entInfo.getEnterpriseTel());
}
return R.ok(result);
}
// 2. 未找到,查 sszt_jbxx110497_new 表
List<SsztJbxx110497NewVo> ssztList = ssztJbxx110497NewService.queryByTyshxydm(enterpriseNumber);
if (ssztList != null && !ssztList.isEmpty()) {
SsztJbxx110497NewVo sszt = ssztList.get(0);
if (StringUtils.isNotBlank(sszt.getFddbr())) {
result.put("contactPerson", sszt.getFddbr());
}
}
} catch (Exception e) {
log.warn("[企业搜索] 查询联系人信息失败, enterpriseNumber={}, error={}", enterpriseNumber, e.getMessage());
}
return R.ok(result);
}
}

91
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnfCpNoticeController.java

@ -1,22 +1,99 @@
package com.zdxt.enforcementcode.controller.app.enforcement;
import com.zdxt.enforcementcode.controller.web.supervision.EfcodeCpNoticeController;
import com.zdxt.enforcementcode.domain.bo.EfcodeCpNoticeBo;
import com.zdxt.enforcementcode.domain.bo.EfcodeCpNoticeMessageBo;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpNoticeMessageVo;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpNoticeVo;
import com.zdxt.enforcementcode.service.IEfcodeCpNoticeMessageService;
import com.zdxt.enforcementcode.service.IEfcodeCpNoticeService;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 行政执法检查计划-预约
* 执法端-预告管理
*
* @author Lion Li
* @date 2026-05-16
* @author zdxt
* @date 2026-05-28
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app-enf/cpNotice")
public class AppEnfCpNoticeController extends EfcodeCpNoticeController {
public class AppEnfCpNoticeController {
private final IEfcodeCpNoticeService efcodeCpNoticeService;
private final IEfcodeCpNoticeMessageService efcodeCpNoticeMessageService;
/**
* 查询我的预告列表(分页,只返回当前用户创建的预告)
* 通过status区分tab:0=草稿, 1=待执法(已发布), 3=历史(已执行)
*/
@GetMapping("/myList")
public TableDataInfo<EfcodeCpNoticeVo> myList(EfcodeCpNoticeBo bo, PageQuery pageQuery) {
return efcodeCpNoticeService.queryMyPageList(bo, pageQuery);
}
/**
* 获取预告详情
*/
@GetMapping("/{id}")
public R<EfcodeCpNoticeVo> getInfo(@NotBlank(message = "主键不能为空") @PathVariable String id) {
return R.ok(efcodeCpNoticeService.queryById(id));
}
/**
* 保存预告(草稿/发布)
* status=0 存为草稿, status=1 发布预告
*/
@PostMapping()
public R<String> save(@RequestBody EfcodeCpNoticeBo bo) {
return R.ok(efcodeCpNoticeService.saveNotice(bo));
}
/**
* 修改预告(仅草稿状态可修改)
*/
@PutMapping()
public R<String> update(@RequestBody EfcodeCpNoticeBo bo) {
return R.ok(efcodeCpNoticeService.updateNotice(bo));
}
/**
* 删除预告(仅草稿状态可删除)
*/
@DeleteMapping("/{ids}")
public R<String> remove(@NotEmpty(message = "主键不能为空") @PathVariable String[] ids) {
return R.ok(efcodeCpNoticeService.deleteNotice(List.of(ids)));
}
// =================== 留言相关 ===================
/**
* 查询某个预告的留言列表
*/
@GetMapping("/message/list")
public R<List<EfcodeCpNoticeMessageVo>> messageList(@RequestParam String noticeId) {
EfcodeCpNoticeMessageBo bo = new EfcodeCpNoticeMessageBo();
bo.setNoticeId(noticeId);
bo.setIsDelete(0L);
return R.ok(efcodeCpNoticeMessageService.queryList(bo));
}
public AppEnfCpNoticeController(IEfcodeCpNoticeService efcodeCpNoticeService) {
super(efcodeCpNoticeService);
/**
* 新增留言
*/
@PostMapping("/message")
public R<Void> addMessage(@RequestBody EfcodeCpNoticeMessageBo bo) {
bo.setIsDelete(0L);
efcodeCpNoticeMessageService.insertByBo(bo);
return R.ok();
}
}

7
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpNotice.java

@ -80,10 +80,15 @@ public class EfcodeCpNotice extends BaseEntity {
private String enforceUserNames;
/**
* 执法人员姓名(多个逗号分隔)
* 执法人员ID(多个逗号分隔)
*/
private String enforceUserIds;
/**
* 执法人员电话(多个逗号分隔)
*/
private String enforceUserTels;
/**
* 执法人员证件编号(多个逗号分隔)
*/

10
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/EfcodeCpPlan.java

@ -74,6 +74,16 @@ public class EfcodeCpPlan extends BaseEntity {
*/
private String enterpriseId;
/**
* 企业联系人
*/
private String contactPerson;
/**
* 企业联系电话
*/
private String contactPhone;
/**
* 执法人员姓名(多个逗号分隔)
*/

15
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpNoticeBo.java

@ -42,7 +42,6 @@ public class EfcodeCpNoticeBo extends BaseEntity {
/**
* 执法时段:1=上午,2=下午,3=其他
*/
@NotNull(message = "执法时段:1=上午,2=下午,3=其他不能为空", groups = { AddGroup.class, EditGroup.class })
private Long enforceTime;
/**
@ -84,19 +83,21 @@ public class EfcodeCpNoticeBo extends BaseEntity {
/**
* 执法人员姓名(多个逗号分隔)
*/
@NotBlank(message = "执法人员姓名(多个逗号分隔)不能为空", groups = { AddGroup.class, EditGroup.class })
private String enforceUserNames;
/**
* 执法人员姓名(多个逗号分隔)
* 执法人员ID(多个逗号分隔)
*/
@NotBlank(message = "执法人员姓名(多个逗号分隔)不能为空", groups = { AddGroup.class, EditGroup.class })
private String enforceUserIds;
/**
* 执法人员电话(多个逗号分隔)
*/
private String enforceUserTels;
/**
* 执法人员证件编号(多个逗号分隔)
*/
@NotBlank(message = "执法人员证件编号(多个逗号分隔)不能为空", groups = { AddGroup.class, EditGroup.class })
private String enforceUserCodes;
/**
@ -119,13 +120,11 @@ public class EfcodeCpNoticeBo extends BaseEntity {
/**
* 企业联系人
*/
@NotBlank(message = "企业联系人不能为空", groups = { AddGroup.class, EditGroup.class })
private String contactPerson;
/**
* 企业联系电话
*/
@NotBlank(message = "企业联系电话不能为空", groups = { AddGroup.class, EditGroup.class })
private String contactPhone;
/**
@ -148,13 +147,11 @@ public class EfcodeCpNoticeBo extends BaseEntity {
/**
* 预告状态:0=草稿,1=已发布,2=已取消,3=已执行
*/
@NotNull(message = "预告状态:0=草稿,1=已发布,2=已取消,3=已执行不能为空", groups = { AddGroup.class, EditGroup.class })
private Long status;
/**
* 是否删除:0=否,1=是
*/
@NotNull(message = "是否删除:0=否,1=是不能为空", groups = { AddGroup.class, EditGroup.class })
private Long isDelete;
/**

10
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/bo/EfcodeCpPlanBo.java

@ -82,6 +82,16 @@ public class EfcodeCpPlanBo extends BaseEntity {
@NotBlank(message = "被检查企业/机构Id不能为空", groups = { AddGroup.class, EditGroup.class })
private String enterpriseId;
/**
* 企业联系人
*/
private String contactPerson;
/**
* 企业联系电话
*/
private String contactPhone;
/**
* 执法人员姓名(多个逗号分隔)
*/

5
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpNoticeMessageVo.java

@ -70,5 +70,10 @@ public class EfcodeCpNoticeMessageVo implements Serializable {
@ExcelProperty(value = "是否删除:0=否,1=是")
private Long isDelete;
/**
* 创建时间
*/
private Date createTime;
}

10
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpNoticeVo.java

@ -112,6 +112,11 @@ public class EfcodeCpNoticeVo implements Serializable {
@ExcelDictFormat(readConverterExp = "多=个逗号分隔")
private String enforceUserCodes;
/**
* 执法人员电话(多个逗号分隔)
*/
private String enforceUserTels;
/**
* 被检查企业ID
*/
@ -178,5 +183,10 @@ public class EfcodeCpNoticeVo implements Serializable {
@ExcelProperty(value = "备注")
private String remark;
/**
* 创建时间
*/
private Date createTime;
}

12
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java

@ -93,6 +93,18 @@ public class EfcodeCpPlanVo implements Serializable {
@ExcelProperty(value = "被检查企业/机构Id")
private String enterpriseId;
/**
* 企业联系人
*/
@ExcelProperty(value = "企业联系人")
private String contactPerson;
/**
* 企业联系电话
*/
@ExcelProperty(value = "企业联系电话")
private String contactPhone;
/**
* 执法人员姓名(多个逗号分隔)
*/

20
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpNoticeService.java

@ -65,4 +65,24 @@ public interface IEfcodeCpNoticeService {
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid);
/**
* 查询当前用户的预告列表(分页)
*/
TableDataInfo<EfcodeCpNoticeVo> queryMyPageList(EfcodeCpNoticeBo bo, PageQuery pageQuery);
/**
* 保存预告(新增草稿或发布)并同步写入 notice_users 表
*/
String saveNotice(EfcodeCpNoticeBo bo);
/**
* 修改预告(仅草稿可修改)并同步更新 notice_users 表
*/
String updateNotice(EfcodeCpNoticeBo bo);
/**
* 删除预告(仅草稿可删除)
*/
String deleteNotice(Collection<String> ids);
}

2
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpNoticeMessageServiceImpl.java

@ -73,7 +73,7 @@ public class EfcodeCpNoticeMessageServiceImpl implements IEfcodeCpNoticeMessageS
private LambdaQueryWrapper<EfcodeCpNoticeMessage> buildQueryWrapper(EfcodeCpNoticeMessageBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<EfcodeCpNoticeMessage> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(EfcodeCpNoticeMessage::getId);
lqw.orderByAsc(EfcodeCpNoticeMessage::getCreateTime);
lqw.eq(StringUtils.isNotBlank(bo.getNoticeId()), EfcodeCpNoticeMessage::getNoticeId, bo.getNoticeId());
lqw.eq(StringUtils.isNotBlank(bo.getUserId()), EfcodeCpNoticeMessage::getUserId, bo.getUserId());
lqw.like(StringUtils.isNotBlank(bo.getUserName()), EfcodeCpNoticeMessage::getUserName, bo.getUserName());

184
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpNoticeServiceImpl.java

@ -1,24 +1,27 @@
package com.zdxt.enforcementcode.service.impl;
import cn.hutool.core.util.IdUtil;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.satoken.utils.LoginHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.zdxt.enforcementcode.domain.bo.EfcodeCpNoticeBo;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpNoticeVo;
import com.zdxt.enforcementcode.domain.EfcodeCpNotice;
import com.zdxt.enforcementcode.domain.EfcodeCpNoticeUsers;
import com.zdxt.enforcementcode.mapper.EfcodeCpNoticeMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpNoticeUsersMapper;
import com.zdxt.enforcementcode.service.IEfcodeCpNoticeService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.*;
/**
* 行政执法检查计划-预告Service业务层处理
@ -32,24 +35,18 @@ import java.util.Collection;
public class EfcodeCpNoticeServiceImpl implements IEfcodeCpNoticeService {
private final EfcodeCpNoticeMapper baseMapper;
private final EfcodeCpNoticeUsersMapper noticeUsersMapper;
/**
* 查询行政执法检查计划-预告
*
* @param id 主键
* @return 行政执法检查计划-预告
*/
@Override
public EfcodeCpNoticeVo queryById(String id){
public EfcodeCpNoticeVo queryById(String id) {
return baseMapper.selectVoById(id);
}
/**
* 分页查询行政执法检查计划-预告列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 行政执法检查计划-预告分页列表
*/
@Override
public TableDataInfo<EfcodeCpNoticeVo> queryPageList(EfcodeCpNoticeBo bo, PageQuery pageQuery) {
@ -60,9 +57,6 @@ public class EfcodeCpNoticeServiceImpl implements IEfcodeCpNoticeService {
/**
* 查询符合条件的行政执法检查计划-预告列表
*
* @param bo 查询条件
* @return 行政执法检查计划-预告列表
*/
@Override
public List<EfcodeCpNoticeVo> queryList(EfcodeCpNoticeBo bo) {
@ -70,30 +64,141 @@ public class EfcodeCpNoticeServiceImpl implements IEfcodeCpNoticeService {
return baseMapper.selectVoList(lqw);
}
/**
* 查询当前用户的预告列表(分页)
* 过滤条件:create_by = 当前用户ID,is_delete = 0
*/
@Override
public TableDataInfo<EfcodeCpNoticeVo> queryMyPageList(EfcodeCpNoticeBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<EfcodeCpNotice> lqw = buildQueryWrapper(bo);
// 只看自己发布的预告
Long userId = LoginHelper.getUserId();
lqw.eq(EfcodeCpNotice::getCreateBy, userId);
// 过滤已删除
lqw.eq(EfcodeCpNotice::getIsDelete, 0);
// 按创建时间倒序
lqw.orderByDesc(EfcodeCpNotice::getCreateTime);
Page<EfcodeCpNoticeVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 保存预告(新增草稿或发布)并同步写入 notice_users 表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String saveNotice(EfcodeCpNoticeBo bo) {
EfcodeCpNotice entity = MapstructUtils.convert(bo, EfcodeCpNotice.class);
// 生成主键
if (StringUtils.isBlank(entity.getId())) {
entity.setId(IdUtil.getSnowflakeNextIdStr());
}
// 默认未删除
entity.setIsDelete(0L);
// 如果是发布(status=1),设置发布时间
if (entity.getStatus() != null && entity.getStatus() == 1L) {
entity.setPublishTime(new Date());
}
baseMapper.insert(entity);
// 同步写入 notice_users 表
syncNoticeUsers(entity.getId(), bo.getEnforceUserNames(), bo.getEnforceUserIds(), bo.getEnforceUserTels());
return "操作成功";
}
/**
* 修改预告(仅草稿可修改)并同步更新 notice_users 表
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String updateNotice(EfcodeCpNoticeBo bo) {
// 校验:只有草稿(status=0)能修改
EfcodeCpNotice existing = baseMapper.selectById(bo.getId());
if (existing == null) {
return "预告不存在";
}
if (existing.getStatus() != null && existing.getStatus() != 0L) {
return "非草稿状态的预告不能修改";
}
EfcodeCpNotice update = MapstructUtils.convert(bo, EfcodeCpNotice.class);
// 如果此次操作是发布(status=1),设置发布时间
if (update.getStatus() != null && update.getStatus() == 1L) {
update.setPublishTime(new Date());
}
baseMapper.updateById(update);
// 同步更新 notice_users 表(先删后插)
syncNoticeUsers(bo.getId(), bo.getEnforceUserNames(), bo.getEnforceUserIds(), bo.getEnforceUserTels());
return "操作成功";
}
/**
* 删除预告(仅草稿可删除)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String deleteNotice(Collection<String> ids) {
for (String id : ids) {
EfcodeCpNotice existing = baseMapper.selectById(id);
if (existing == null) {
continue;
}
if (existing.getStatus() != null && existing.getStatus() != 0L) {
return "非草稿状态的预告不能删除";
}
}
// 逻辑删除(更新 is_delete=1)
for (String id : ids) {
EfcodeCpNotice update = new EfcodeCpNotice();
update.setId(id);
update.setIsDelete(1L);
baseMapper.updateById(update);
}
// 同步删除 notice_users
for (String id : ids) {
LambdaQueryWrapper<EfcodeCpNoticeUsers> delWrapper = Wrappers.lambdaQuery();
delWrapper.eq(EfcodeCpNoticeUsers::getNoticeId, id);
noticeUsersMapper.delete(delWrapper);
}
return "操作成功";
}
/**
* 同步写入/更新 notice_users 表
*/
private void syncNoticeUsers(String noticeId, String names, String userIds, String tels) {
// 先删除旧记录
LambdaQueryWrapper<EfcodeCpNoticeUsers> delWrapper = Wrappers.lambdaQuery();
delWrapper.eq(EfcodeCpNoticeUsers::getNoticeId, noticeId);
noticeUsersMapper.delete(delWrapper);
// 逐个插入
if (StringUtils.isBlank(userIds)) {
return;
}
String[] idArr = userIds.split(",");
String[] nameArr = StringUtils.isNotBlank(names) ? names.split(",") : new String[0];
String[] telArr = StringUtils.isNotBlank(tels) ? tels.split(",") : new String[0];
for (int i = 0; i < idArr.length; i++) {
EfcodeCpNoticeUsers user = new EfcodeCpNoticeUsers();
user.setId(IdUtil.getSnowflakeNextIdStr());
user.setNoticeId(noticeId);
user.setUserId(idArr[i].trim());
user.setUserName(i < nameArr.length ? nameArr[i].trim() : "");
user.setUserTel(i < telArr.length ? telArr[i].trim() : "");
noticeUsersMapper.insert(user);
}
}
private LambdaQueryWrapper<EfcodeCpNotice> buildQueryWrapper(EfcodeCpNoticeBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<EfcodeCpNotice> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(EfcodeCpNotice::getId);
lqw.eq(bo.getPlanId() != null, EfcodeCpNotice::getPlanId, bo.getPlanId());
lqw.eq(StringUtils.isNotBlank(bo.getEnforceDate()), EfcodeCpNotice::getEnforceDate, bo.getEnforceDate());
lqw.eq(bo.getEnforceTime() != null, EfcodeCpNotice::getEnforceTime, bo.getEnforceTime());
lqw.eq(StringUtils.isNotBlank(bo.getBureauDeptId()), EfcodeCpNotice::getBureauDeptId, bo.getBureauDeptId());
lqw.like(StringUtils.isNotBlank(bo.getBureauDeptName()), EfcodeCpNotice::getBureauDeptName, bo.getBureauDeptName());
lqw.eq(StringUtils.isNotBlank(bo.getOrganId()), EfcodeCpNotice::getOrganId, bo.getOrganId());
lqw.like(StringUtils.isNotBlank(bo.getOrganName()), EfcodeCpNotice::getOrganName, bo.getOrganName());
lqw.eq(StringUtils.isNotBlank(bo.getDeptId()), EfcodeCpNotice::getDeptId, bo.getDeptId());
lqw.like(StringUtils.isNotBlank(bo.getDeptName()), EfcodeCpNotice::getDeptName, bo.getDeptName());
lqw.eq(StringUtils.isNotBlank(bo.getEnforceUserNames()), EfcodeCpNotice::getEnforceUserNames, bo.getEnforceUserNames());
lqw.eq(StringUtils.isNotBlank(bo.getEnforceUserIds()), EfcodeCpNotice::getEnforceUserIds, bo.getEnforceUserIds());
lqw.eq(StringUtils.isNotBlank(bo.getEnforceUserCodes()), EfcodeCpNotice::getEnforceUserCodes, bo.getEnforceUserCodes());
lqw.eq(StringUtils.isNotBlank(bo.getEnterpriseId()), EfcodeCpNotice::getEnterpriseId, bo.getEnterpriseId());
lqw.like(StringUtils.isNotBlank(bo.getEnterpriseName()), EfcodeCpNotice::getEnterpriseName, bo.getEnterpriseName());
lqw.eq(StringUtils.isNotBlank(bo.getEnterpriseNumber()), EfcodeCpNotice::getEnterpriseNumber, bo.getEnterpriseNumber());
lqw.eq(StringUtils.isNotBlank(bo.getContactPerson()), EfcodeCpNotice::getContactPerson, bo.getContactPerson());
lqw.eq(StringUtils.isNotBlank(bo.getContactPhone()), EfcodeCpNotice::getContactPhone, bo.getContactPhone());
lqw.eq(StringUtils.isNotBlank(bo.getEnforceContent()), EfcodeCpNotice::getEnforceContent, bo.getEnforceContent());
lqw.eq(StringUtils.isNotBlank(bo.getEnforceRequirement()), EfcodeCpNotice::getEnforceRequirement, bo.getEnforceRequirement());
lqw.eq(bo.getPublishTime() != null, EfcodeCpNotice::getPublishTime, bo.getPublishTime());
lqw.eq(bo.getStatus() != null, EfcodeCpNotice::getStatus, bo.getStatus());
lqw.eq(bo.getIsDelete() != null, EfcodeCpNotice::getIsDelete, bo.getIsDelete());
return lqw;
@ -101,14 +206,10 @@ public class EfcodeCpNoticeServiceImpl implements IEfcodeCpNoticeService {
/**
* 新增行政执法检查计划-预告
*
* @param bo 行政执法检查计划-预告
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(EfcodeCpNoticeBo bo) {
EfcodeCpNotice add = MapstructUtils.convert(bo, EfcodeCpNotice.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
@ -118,35 +219,20 @@ public class EfcodeCpNoticeServiceImpl implements IEfcodeCpNoticeService {
/**
* 修改行政执法检查计划-预告
*
* @param bo 行政执法检查计划-预告
* @return 是否修改成功
*/
@Override
public Boolean updateByBo(EfcodeCpNoticeBo bo) {
EfcodeCpNotice update = MapstructUtils.convert(bo, EfcodeCpNotice.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(EfcodeCpNotice entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 校验并批量删除行政执法检查计划-预告信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
if (isValid) {
// 校验只有草稿可删除
}
return baseMapper.deleteByIds(ids) > 0;
}

45
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java

@ -40,6 +40,10 @@ import com.zdxt.enforcementcode.domain.EfcodeCpSetting;
import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpSettingMapper;
import com.zdxt.enforcementcode.service.IEfcodeCpTaskService;
import com.zdxt.enforcementcode.service.IEnterpriseInformationService;
import com.zdxt.enforcementcode.service.ISsztJbxx110497NewService;
import com.zdxt.enforcementcode.domain.vo.EnterpriseInformationVo;
import com.zdxt.enforcementcode.domain.vo.SsztJbxx110497NewVo;
/**
* 行政执法检查计划-计划Service业务层处理
@ -57,6 +61,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
private final EfcodeCpReserveMapper efcodeCpReserveMapper;
private final EfcodeCpSettingMapper efcodeCpSettingMapper;
private final IEfcodeCpTaskService cpTaskService;
private final IEnterpriseInformationService enterpriseInformationService;
private final ISsztJbxx110497NewService ssztJbxx110497NewService;
/**
* 查询行政执法检查计划-计划
@ -314,6 +320,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
// 重复校验:同一人+同一日期+同一时段+同一企业 不可重复
checkDuplicate(bo);
EfcodeCpPlan add = MapstructUtils.convert(bo, EfcodeCpPlan.class);
// 尝试根据企业信用代码填充联系人和电话
fillContactInfo(add);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
@ -338,6 +346,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
// 重复校验:同一人+同一日期+同一时段+同一企业 不可重复
checkDuplicate(bo);
EfcodeCpPlan update = MapstructUtils.convert(bo, EfcodeCpPlan.class);
// 尝试根据企业信用代码填充联系人和电话
fillContactInfo(update);
validEntityBeforeSave(update);
boolean flag = baseMapper.updateById(update) > 0;
if (flag) {
@ -469,6 +479,41 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
}
}
/**
* 根据企业统一社会信用代码(enterpriseNumber)尝试填充联系人和电话
* <p>优先查 enterprise_information 表,找不到则查 sszt_jbxx110497_new 表</p>
*/
private void fillContactInfo(EfcodeCpPlan entity) {
String enterpriseNumber = entity.getEnterpriseNumber();
if (StringUtils.isBlank(enterpriseNumber)) {
return;
}
try {
// 1. 先查 enterprise_information 表
EnterpriseInformationVo entInfo = enterpriseInformationService.queryByEnterpriseNumber(enterpriseNumber);
if (entInfo != null) {
if (StringUtils.isNotBlank(entInfo.getLegalPerson())) {
entity.setContactPerson(entInfo.getLegalPerson());
}
if (StringUtils.isNotBlank(entInfo.getEnterpriseTel())) {
entity.setContactPhone(entInfo.getEnterpriseTel());
}
return;
}
// 2. enterprise_information 未找到,查 sszt_jbxx110497_new 表
List<SsztJbxx110497NewVo> ssztList = ssztJbxx110497NewService.queryByTyshxydm(enterpriseNumber);
if (ssztList != null && !ssztList.isEmpty()) {
SsztJbxx110497NewVo sszt = ssztList.get(0);
if (StringUtils.isNotBlank(sszt.getFddbr())) {
entity.setContactPerson(sszt.getFddbr());
}
// sszt_jbxx110497_new 表无电话字段,contactPhone 留空
}
} catch (Exception e) {
log.warn("[计划] 填充企业联系人信息失败, enterpriseNumber={}, error={}", enterpriseNumber, e.getMessage());
}
}
/**
* 保存前的数据校验
*/

4
zdxt-web-client/zdxt-efcode-enforcement-app/src/api/enforcement/plan.js

@ -53,6 +53,10 @@ const enterpriseSearchBaseUrl = "/enforcementcode/enterpriseSearch";
export const searchUnionByEnterpriseName = (enterpriseName, success, error) =>
_get(enterpriseSearchBaseUrl + "/searchUnionByEnterpriseName", { enterpriseName }, success, error);
// 根据企业信用代码查询联系人和联系电话
export const getEnterpriseContactInfo = (enterpriseNumber, success, error) =>
_get(enterpriseSearchBaseUrl + "/getContactInfo", { enterpriseNumber }, success, error);
// endregion
// region 单位人员检索接口

27
zdxt-web-client/zdxt-efcode-enforcement-app/src/router/enforcement-app/enf-main-router.js

@ -134,6 +134,33 @@ const routes = [
isKeepAlive: false,
},
},
// 执法端-预告列表
{
path: "/enforcement/notice",
name: "cpNoticeList",
component: () => import("@/views/enforcement/my/notice/cp_notice_list.vue"),
meta: {
isKeepAlive: false,
},
},
// 执法端-预告编辑(新增/编辑)
{
path: "/enforcement/notice/edit",
name: "cpNoticeEdit",
component: () => import("@/views/enforcement/my/notice/cp_notice_edit.vue"),
meta: {
isKeepAlive: false,
},
},
// 执法端-预告留言
{
path: "/enforcement/notice/message",
name: "cpNoticeMessage",
component: () => import("@/views/enforcement/my/notice/cp_notice_message.vue"),
meta: {
isKeepAlive: false,
},
},
// 执法端-登记页面
{
path: "/enforcement/enforcementRegister",

6
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/enforcementMy.vue

@ -119,11 +119,7 @@ const settingItem = {
const onMenuClick = (item) => {
switch (item.key) {
case 'notice':
// 跳至消息中心待办 tab,并带类型过滤参数
router.push({
path: '/enforcement/message',
query: { tab: 'todo', type: item.key },
});
router.push({ path: '/enforcement/notice' });
break;
case 'task':
router.push({ path: '/enforcement/task' });

888
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_edit.vue

@ -0,0 +1,888 @@
<template>
<div class="notice-edit-page app_content">
<!-- 顶部导航 -->
<van-nav-bar
:title="pageTitle"
left-arrow
@click-left="onBack"
/>
<div class="content">
<!-- 表单 -->
<div class="form-card">
<!-- 从计划导入 -->
<van-field
v-model="planImportDisplay"
label="从计划导入"
placeholder="请选择计划"
readonly
is-link
@click="showPlanPicker = true"
/>
<!-- 执法日期 -->
<van-field
v-model="form.enforceDate"
label="执法日期"
placeholder="请选择"
required
readonly
is-link
@click="showDatePicker = true"
/>
<!-- 执法时段 -->
<van-field
v-model="enforceTimeDisplay"
label="执法时段"
placeholder="请选择"
required
readonly
is-link
@click="showTimePicker = true"
/>
<!-- 执法单位 -->
<van-field
v-model="form.bureauDeptName"
label="执法单位"
required
readonly
/>
<!-- 执法人员 -->
<van-field label="执法人员" required>
<template #input>
<div class="person-box">
<div
v-for="(person, index) in personList"
:key="index"
class="person-line"
>
<span class="person-name">{{ person.name }}</span>
<span class="person-phone">{{ person.phone }}</span>
<van-icon
v-if="personList.length > 1"
name="close"
class="person-remove"
@click="removePerson(index)"
/>
</div>
<div class="add-person-line" @click="onAddPerson">
<span class="placeholder-text">请输入</span>
<van-icon name="add-o" color="#2382cf" size="20" />
</div>
</div>
</template>
</van-field>
<!-- 企业名称 -->
<van-field
v-model="form.enterpriseName"
label="企业名称"
placeholder="请选择企业"
required
readonly
is-link
@click="onOpenEnterprisePicker"
/>
<!-- 企业联系人 -->
<van-field
v-model="form.contactPerson"
label="企业联系人"
placeholder="请输入联系人"
/>
<!-- 企业联系电话 -->
<van-field
v-model="form.contactPhone"
label="企业联系电话"
placeholder="请输入联系电话"
/>
<!-- 执法事项 -->
<van-field
v-model="form.enforceContent"
label="执法事项"
type="textarea"
placeholder="请输入执法事项"
required
:maxlength="1000"
show-word-limit
rows="3"
autosize
/>
<!-- 执法要求 -->
<van-field
v-model="form.enforceRequirement"
label="执法要求"
type="textarea"
placeholder="请输入执法要求"
required
:maxlength="1000"
show-word-limit
rows="2"
autosize
/>
</div>
<!-- 底部按钮 -->
<div class="submit-wrap">
<van-button
plain
type="default"
class="draft-btn"
:loading="savingDraft"
loading-text="保存中..."
@click="onSaveDraft"
>
保存草稿
</van-button>
<van-button
type="primary"
class="publish-btn"
:loading="publishing"
loading-text="发布中..."
@click="onPublish"
>
提交预告
</van-button>
</div>
</div>
<!-- 日期+时段选择器 -->
<van-popup v-model:show="showDatePicker" position="bottom" round>
<van-date-picker
v-model="datePickerValue"
title="选择执法日期"
:min-date="minDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
/>
</van-popup>
<!-- 时段选择 -->
<van-popup v-model:show="showTimePicker" position="bottom" round>
<van-picker
:columns="timeColumns"
@confirm="onTimeConfirm"
@cancel="showTimePicker = false"
/>
</van-popup>
<!-- 计划选择弹层 -->
<van-popup v-model:show="showPlanPicker" position="bottom" round :style="{ height: '60%' }">
<div class="plan-picker">
<div class="plan-picker-header">
<span class="plan-picker-title">选择我的计划</span>
<van-icon name="cross" @click="showPlanPicker = false" />
</div>
<div class="plan-picker-body">
<div v-if="planLoading" class="plan-loading">
<van-loading size="18" />&nbsp;加载中...
</div>
<template v-else>
<div
v-for="(plan, idx) in planOptions"
:key="plan.id || idx"
class="plan-item"
@click="onSelectPlan(plan)"
>
<div class="plan-item-title">{{ plan.enforceContent || '无事项' }}</div>
<div class="plan-item-meta">
{{ plan.planDate }} | {{ plan.enterpriseName }}
</div>
</div>
<van-empty v-if="!planOptions.length" description="暂无可导入计划" image-size="80" />
</template>
</div>
</div>
</van-popup>
<!-- 执法人员检索弹层 -->
<van-popup
v-model:show="showPersonPicker"
position="bottom"
round
:style="{ height: '70%' }"
>
<div class="person-picker">
<div class="person-picker-header">
<span class="person-picker-title">选择执法人员</span>
<van-icon name="cross" @click="showPersonPicker = false" />
</div>
<van-search
v-model="personSearchKeyword"
placeholder="输入姓名/账号检索单位人员"
shape="round"
@update:model-value="onPersonSearchInput"
/>
<div class="person-picker-body">
<div v-if="personSearching" class="person-loading">
<van-loading size="18" />&nbsp;加载中...
</div>
<template v-else>
<div
v-for="(user, idx) in personSearchResult"
:key="user.userId || idx"
class="person-item"
@click="onSelectPerson(user)"
>
<div class="person-item-name">{{ user.fullname || user.userName }}</div>
<div class="person-item-meta">
<span v-if="user.mobileNumber">{{ user.mobileNumber }}</span>
<span v-if="user.loginName" class="person-item-login">@{{ user.loginName }}</span>
</div>
</div>
<van-empty v-if="!personSearchResult.length" description="未检索到人员" image-size="80" />
</template>
</div>
</div>
</van-popup>
<!-- 企业检索弹层 -->
<van-popup
v-model:show="showEnterprisePicker"
position="bottom"
round
:style="{ height: '70%' }"
>
<div class="enterprise-picker">
<div class="enterprise-picker-header">
<span class="enterprise-picker-title">选择企业</span>
<van-icon name="cross" @click="showEnterprisePicker = false" />
</div>
<van-search
v-model="enterpriseSearchKeyword"
placeholder="输入≥ 3 个字符检索企业,不要输入深圳,深圳市"
shape="round"
@update:model-value="onEnterpriseSearchInput"
/>
<div class="enterprise-picker-body">
<div v-if="enterpriseSearching" class="enterprise-loading">
<van-loading size="18" />&nbsp;检索中...
</div>
<template v-else>
<div
v-for="(item, idx) in enterpriseOptions"
:key="idx"
class="enterprise-item"
@click="onSelectEnterprise(item)"
>
<div class="enterprise-item-name">{{ item.enterpriseName }}</div>
<div class="enterprise-item-number">{{ item.enterpriseNumber }}</div>
</div>
<van-empty v-if="enterpriseSearchKeyword.length >= 3 && !enterpriseOptions.length" description="未检索到匹配企业" image-size="80" />
</template>
</div>
</div>
</van-popup>
</div>
</template>
<script setup name="cpNoticeEdit">
import { useRouter, useRoute } from 'vue-router';
import { showToast, showSuccessToast, showConfirmDialog } from 'vant';
import { addNotice, editNotice, getNoticeDetail } from '@/api/enforcement/notice';
import { getPlanList, searchUnionByEnterpriseName, searchBureauUnitUsers, getEnterpriseContactInfo } from '@/api/enforcement/plan';
const router = useRouter();
const route = useRoute();
const { useCommon } = $globalStore;
const isEdit = computed(() => !!route.query.id);
const isReinitiate = computed(() => !!route.query.reinitiate);
const pageTitle = computed(() => {
if (isReinitiate.value) return '新增预告';
return isEdit.value ? '编辑预告' : '新增预告';
});
const savingDraft = ref(false);
const publishing = ref(false);
const showDatePicker = ref(false);
const showTimePicker = ref(false);
const showPlanPicker = ref(false);
// 表单数据
const form = ref({
id: '',
planId: null,
enforceDate: '',
enforceTime: 1,
bureauDeptId: '',
bureauDeptName: '',
organId: '',
organName: '',
deptId: '',
deptName: '',
enforceUserNames: '',
enforceUserIds: '',
enforceUserTels: '',
enforceUserCodes: '',
enterpriseId: '',
enterpriseName: '',
enterpriseNumber: '',
contactPerson: '',
contactPhone: '',
enforceContent: '',
enforceRequirement: '',
status: 0,
});
// 人员列表
const personList = ref([]);
const maxPersonNum = 3;
// 日期相关
const today = new Date();
const minDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const datePickerValue = ref([
String(today.getFullYear()),
String(today.getMonth() + 1).padStart(2, '0'),
String(today.getDate()).padStart(2, '0'),
]);
const timeColumns = [
{ text: '上午', value: 1 },
{ text: '下午', value: 2 },
{ text: '其他', value: 3 },
];
// 日期+时段展示
const enforceTimeDisplay = computed(() => {
const timeText = { 1: '上午', 2: '下午', 3: '其他' };
return timeText[form.value.enforceTime] || '';
});
// 计划导入展示
const planImportDisplay = computed(() => {
if (form.value.planId) return '已导入计划';
return '';
});
// 企业搜索状态
const enterpriseOptions = ref([]);
const showEnterprisePicker = ref(false);
const enterpriseSearchKeyword = ref('');
const enterpriseSearching = ref(false);
let enterpriseSearchTimer = null;
let enterpriseSearchSeq = 0;
// 执法人员检索
const showPersonPicker = ref(false);
const personSearchKeyword = ref('');
const personSearchResult = ref([]);
const personSearching = ref(false);
let personSearchTimer = null;
let personSearchSeq = 0;
// 计划列表
const planOptions = ref([]);
const planLoading = ref(false);
onMounted(() => {
// 默认绑定当前用户的执法单位
form.value.bureauDeptId = useCommon.bureauUnitId || '';
form.value.bureauDeptName = useCommon.bureauUnitName || '';
form.value.organId = useCommon.organId || '';
form.value.organName = useCommon.organName || '';
form.value.deptId = useCommon.deptId || '';
form.value.deptName = useCommon.deptName || '';
// 默认绑定当前用户为首位执法人员
bindCurrentUserAsDefaultPerson();
if (isEdit.value) {
loadDetail(route.query.id);
} else if (isReinitiate.value) {
loadDetail(route.query.reinitiate, true);
}
});
const bindCurrentUserAsDefaultPerson = () => {
const name = useCommon.nickName || useCommon.userName || '';
const phone = useCommon.phonenumber || '';
const id = useCommon.userId || '';
if (!name && !id) return;
personList.value = [{ id, name, phone, code: '' }];
syncPersonData();
};
const loadDetail = (id, isReinitiateMode = false) => {
getNoticeDetail(id, (res) => {
if (res.code === 200 && res.data) {
const data = res.data;
Object.keys(form.value).forEach(key => {
if (data[key] !== undefined && data[key] !== null) {
form.value[key] = data[key];
}
});
// 如果是再次发起,清除id,变为新增
if (isReinitiateMode) {
form.value.id = '';
form.value.status = 0;
form.value.planId = null;
}
// 解析人员
if (data.enforceUserNames) {
const names = data.enforceUserNames.split(',');
const ids = data.enforceUserIds ? data.enforceUserIds.split(',') : [];
const tels = data.enforceUserTels ? data.enforceUserTels.split(',') : [];
personList.value = names.map((name, i) => ({
id: i < ids.length ? ids[i] : '',
name,
phone: i < tels.length ? tels[i] : '',
code: '',
}));
}
}
});
};
// 日期确认
const onDateConfirm = ({ selectedValues }) => {
form.value.enforceDate = selectedValues.join('-');
showDatePicker.value = false;
};
// 时段确认
const onTimeConfirm = ({ selectedValues }) => {
form.value.enforceTime = selectedValues[0];
showTimePicker.value = false;
};
// 打开企业检索弹层
const onOpenEnterprisePicker = () => {
enterpriseSearchKeyword.value = '';
enterpriseOptions.value = [];
showEnterprisePicker.value = true;
};
// 企业检索
const onEnterpriseSearchInput = (val) => {
if (enterpriseSearchTimer) clearTimeout(enterpriseSearchTimer);
const keyword = (val || '').trim();
if (keyword.length < 3) {
enterpriseOptions.value = [];
return;
}
enterpriseSearchTimer = setTimeout(() => {
const seq = ++enterpriseSearchSeq;
enterpriseSearching.value = true;
searchUnionByEnterpriseName(keyword, (res) => {
if (seq !== enterpriseSearchSeq) return;
enterpriseSearching.value = false;
if (res && res.code === 200 && Array.isArray(res.data)) {
enterpriseOptions.value = res.data;
} else {
enterpriseOptions.value = [];
}
}, () => {
if (seq !== enterpriseSearchSeq) return;
enterpriseSearching.value = false;
enterpriseOptions.value = [];
});
}, 350);
};
const onSelectEnterprise = (item) => {
form.value.enterpriseName = item.enterpriseName || '';
form.value.enterpriseNumber = item.enterpriseNumber || '';
form.value.enterpriseId = item.enterpriseNumber || '';
showEnterprisePicker.value = false;
enterpriseOptions.value = [];
// 根据企业信用代码拉取联系人和联系电话
if (item.enterpriseNumber) {
getEnterpriseContactInfo(item.enterpriseNumber, (res) => {
if (res && res.code === 200 && res.data) {
if (res.data.contactPerson) {
form.value.contactPerson = res.data.contactPerson;
}
if (res.data.contactPhone) {
form.value.contactPhone = res.data.contactPhone;
}
}
});
}
};
// 人员添加
const onAddPerson = () => {
if (personList.value.length >= maxPersonNum) {
showToast(`最多只能添加 ${maxPersonNum} 名执法人员`);
return;
}
personSearchKeyword.value = '';
personSearchResult.value = [];
showPersonPicker.value = true;
triggerPersonSearch('');
};
const onPersonSearchInput = (val) => {
if (personSearchTimer) clearTimeout(personSearchTimer);
const keyword = (val || '').trim();
personSearchTimer = setTimeout(() => triggerPersonSearch(keyword), 300);
};
const triggerPersonSearch = (keyword) => {
const seq = ++personSearchSeq;
personSearching.value = true;
searchBureauUnitUsers(
{ sysDeptId: useCommon.bureauUnitId || '', keyword, globalSearch: false, pageNum: 1, pageSize: 30 },
(res) => {
if (seq !== personSearchSeq) return;
personSearching.value = false;
if (res && res.code === 200 && Array.isArray(res.rows)) {
personSearchResult.value = res.rows;
} else {
personSearchResult.value = [];
}
},
() => {
if (seq !== personSearchSeq) return;
personSearching.value = false;
personSearchResult.value = [];
}
);
};
const onSelectPerson = (user) => {
const uid = user.userId != null ? String(user.userId) : '';
const exists = personList.value.some(p => String(p.id || '') === uid);
if (exists) { showToast('该人员已添加'); return; }
if (personList.value.length >= maxPersonNum) { showToast(`最多只能添加 ${maxPersonNum} 名执法人员`); return; }
personList.value.push({ id: uid, name: user.fullname || user.userName || '', phone: user.mobileNumber || '', code: '' });
syncPersonData();
showPersonPicker.value = false;
};
const removePerson = (index) => {
personList.value.splice(index, 1);
syncPersonData();
};
const syncPersonData = () => {
form.value.enforceUserNames = personList.value.map(p => p.name).join(',');
form.value.enforceUserIds = personList.value.map(p => p.id || '').join(',');
form.value.enforceUserTels = personList.value.map(p => p.phone || '').join(',');
form.value.enforceUserCodes = personList.value.map(p => p.code || '').join(',');
};
// 计划导入
watch(showPlanPicker, (val) => {
if (val && planOptions.value.length === 0) {
loadPlans();
}
});
const loadPlans = () => {
planLoading.value = true;
getPlanList({ pageNum: 1, pageSize: 50 }, (res) => {
planLoading.value = false;
if (res.code === 200) {
planOptions.value = res.rows || [];
}
}, () => { planLoading.value = false; });
};
const onSelectPlan = (plan) => {
form.value.planId = plan.id;
form.value.enforceDate = plan.planDate || '';
form.value.enforceTime = plan.planTime || 1;
form.value.enterpriseName = plan.enterpriseName || '';
form.value.enterpriseNumber = plan.enterpriseNumber || '';
form.value.enterpriseId = plan.enterpriseId || plan.enterpriseNumber || '';
form.value.contactPerson = plan.contactPerson || '';
form.value.contactPhone = plan.contactPhone || '';
form.value.enforceContent = plan.enforceContent || '';
// 同步人员
if (plan.enforceUserNames) {
const names = plan.enforceUserNames.split(',');
const ids = plan.enforceUserIds ? plan.enforceUserIds.split(',') : [];
const tels = plan.enforceUserTels ? plan.enforceUserTels.split(',') : [];
personList.value = names.map((name, i) => ({
id: i < ids.length ? ids[i] : '',
name,
phone: i < tels.length ? tels[i] : '',
code: '',
}));
syncPersonData();
}
showPlanPicker.value = false;
};
// 返回
const onBack = () => { router.back(); };
// 表单校验
const validate = (isDraft) => {
if (!isDraft) {
if (!form.value.enforceDate) { showToast('请选择执法日期'); return false; }
if (!form.value.enforceTime) { showToast('请选择执法时段'); return false; }
if (!form.value.enforceContent) { showToast('请输入执法事项'); return false; }
if (!form.value.enforceRequirement) { showToast('请输入执法要求'); return false; }
if (!form.value.enterpriseName) { showToast('请输入企业名称'); return false; }
if (personList.value.length === 0) { showToast('请添加执法人员'); return false; }
}
return true;
};
// 存为草稿
const onSaveDraft = () => {
if (!validate(true)) return;
form.value.status = 0;
doSubmit(true);
};
// 发布预告
const onPublish = () => {
if (!validate(false)) return;
showConfirmDialog({
title: '提示',
message: '预告发布后,不能删除,不能修改。确认发布?',
confirmButtonText: '确认发布',
cancelButtonText: '取消',
}).then(() => {
form.value.status = 1;
doSubmit(false);
}).catch(() => {});
};
// 实际提交
const doSubmit = (isDraft) => {
if (isDraft) { savingDraft.value = true; } else { publishing.value = true; }
// 兜底字段
form.value.organId = form.value.organId || useCommon.organId || '-';
form.value.organName = form.value.organName || useCommon.organName || '-';
form.value.deptId = form.value.deptId || useCommon.deptId || '-';
form.value.deptName = form.value.deptName || useCommon.deptName || '-';
form.value.bureauDeptId = form.value.bureauDeptId || useCommon.bureauUnitId || '-';
form.value.bureauDeptName = form.value.bureauDeptName || useCommon.bureauUnitName || '-';
if (!form.value.enterpriseId) form.value.enterpriseId = form.value.enterpriseNumber || '-';
const api = isEdit.value ? editNotice : addNotice;
api(
form.value,
(res) => {
savingDraft.value = false;
publishing.value = false;
if (res.code === 200) {
showSuccessToast(isDraft ? '已保存草稿' : '发布成功');
setTimeout(() => router.back(), 800);
} else {
showToast(res.msg || '操作失败');
}
},
(errRes) => {
savingDraft.value = false;
publishing.value = false;
showToast(errRes?.msg || '操作失败,请重试');
}
);
};
</script>
<style lang="less" scoped>
.notice-edit-page {
display: flex;
flex-direction: column;
height: 100%;
background: #eaf4ff;
overflow: hidden;
}
:deep(.van-nav-bar) {
background: #347bc3;
flex-shrink: 0;
.van-nav-bar__title { color: #fff; font-size: 18px; font-weight: 600; }
.van-icon { color: #fff; }
}
.content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.form-card {
background: #fff;
border-radius: 14px;
overflow: hidden;
box-shadow: 0 4px 14px rgba(44, 116, 190, 0.08);
:deep(.van-field) { padding: 14px; }
:deep(.van-field__label) { width: 90px; font-size: 15px; color: #333; }
}
.person-box {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.person-line {
display: flex;
align-items: center;
font-size: 15px;
color: #666;
gap: 8px;
.person-name { margin-right: 4px; }
.person-phone { color: #999; }
.person-remove { margin-left: auto; color: #999; font-size: 16px; }
}
.add-person-line {
display: flex;
align-items: center;
justify-content: space-between;
color: #c4c9cf;
font-size: 15px;
cursor: pointer;
.placeholder-text { color: #c4c9cf; }
}
.enterprise-picker {
display: flex;
flex-direction: column;
height: 100%;
.enterprise-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px 8px;
.enterprise-picker-title { font-size: 16px; font-weight: 600; color: #333; }
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; }
}
.enterprise-picker-body {
flex: 1;
overflow-y: auto;
padding: 0 12px 12px;
}
.enterprise-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
color: #999;
font-size: 14px;
}
.enterprise-item {
padding: 12px 14px;
background: #fff;
border: 1px solid #e6edf5;
border-radius: 10px;
margin-bottom: 8px;
cursor: pointer;
&:active { background: #f5faff; }
.enterprise-item-name { font-size: 15px; color: #333; font-weight: 500; }
.enterprise-item-number { font-size: 12px; color: #999; margin-top: 4px; }
}
}
.submit-wrap {
display: flex;
gap: 16px;
padding: 22px 0 0;
.draft-btn {
flex: 1;
height: 50px;
font-size: 17px;
font-weight: 500;
border-radius: 25px;
}
.publish-btn {
flex: 1;
height: 50px;
font-size: 17px;
font-weight: 500;
border-radius: 25px;
background: linear-gradient(135deg, #1890ff, #0b63d1);
border: none;
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.28);
}
}
.plan-picker {
display: flex;
flex-direction: column;
height: 100%;
.plan-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px 8px;
.plan-picker-title { font-size: 16px; font-weight: 600; color: #333; }
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; }
}
.plan-picker-body {
flex: 1;
overflow-y: auto;
padding: 0 12px 12px;
}
.plan-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
color: #999;
font-size: 14px;
}
.plan-item {
padding: 12px 14px;
background: #fff;
border: 1px solid #e6edf5;
border-radius: 10px;
margin-bottom: 8px;
cursor: pointer;
&:active { background: #f5faff; }
.plan-item-title { font-size: 15px; color: #333; font-weight: 500; }
.plan-item-meta { font-size: 12px; color: #999; margin-top: 4px; }
}
}
.person-picker {
display: flex;
flex-direction: column;
height: 100%;
.person-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px 8px;
.person-picker-title { font-size: 16px; font-weight: 600; color: #333; }
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; }
}
.person-picker-body {
flex: 1;
overflow-y: auto;
padding: 0 12px 12px;
}
.person-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
color: #999;
font-size: 14px;
}
.person-item {
padding: 12px 14px;
background: #fff;
border: 1px solid #e6edf5;
border-radius: 10px;
margin-bottom: 8px;
cursor: pointer;
&:active { background: #f5faff; }
.person-item-name { font-size: 15px; color: #333; font-weight: 500; }
.person-item-meta {
display: flex;
gap: 12px;
margin-top: 4px;
font-size: 12px;
color: #999;
.person-item-login { color: #b8c1cc; }
}
}
}
</style>

231
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_list.vue

@ -1,11 +1,234 @@
<script setup>
<template>
<div class="notice-page app_content">
<!-- 顶部导航 -->
<van-nav-bar
title="预告"
left-arrow
@click-left="onBack"
/>
<!-- Tab 切换 -->
<van-tabs
v-model:active="activeTab"
title-active-color="#111"
title-inactive-color="#666"
color="#2f8bd3"
line-width="36px"
line-height="2px"
:border="false"
@change="onTabChange"
>
<van-tab title="草稿" name="draft">
<notice-tab-content
ref="draftRef"
:status="0"
:can-publish="true"
:can-edit="true"
:can-delete="true"
:can-message="false"
:can-reinitiate="false"
@publish="onPublish"
@edit="onEdit"
@delete="onDelete"
/>
</van-tab>
<van-tab title="待执法预告" name="pending">
<notice-tab-content
ref="pendingRef"
:status="1"
:can-edit="false"
:can-delete="false"
:can-message="true"
:can-reinitiate="false"
@message="onMessage"
/>
</van-tab>
<van-tab title="历史预告" name="history">
<notice-tab-content
ref="historyRef"
:status="3"
:can-edit="false"
:can-delete="false"
:can-message="true"
:can-reinitiate="true"
:message-readonly="true"
@message="onMessage"
@reinitiate="onReinitiate"
/>
</van-tab>
</van-tabs>
<!-- 悬浮新增按钮(仅草稿Tab显示) -->
<div v-if="activeTab === 'draft'" class="add-btn" @click="onAdd">
<van-icon name="plus" size="22" color="#fff" />
</div>
</div>
</template>
<script setup name="cpNoticeList">
import { useRouter } from 'vue-router';
import { showConfirmDialog, showToast } from 'vant';
import { deleteNotice, editNotice } from '@/api/enforcement/notice';
import NoticeTabContent from './cp_notice_tab_content.vue';
const router = useRouter();
const activeTab = ref('draft');
const draftRef = ref(null);
const pendingRef = ref(null);
const historyRef = ref(null);
const onBack = () => {
router.back();
};
const onTabChange = () => {
// Tab切换后自动刷新(子组件内部onMounted会自行加载)
};
const onAdd = () => {
router.push({ path: '/enforcement/notice/edit' });
};
const onEdit = (item) => {
router.push({ path: '/enforcement/notice/edit', query: { id: item.id } });
};
const onDelete = (item) => {
showConfirmDialog({
title: '提示',
message: '确定要删除该预告吗?',
}).then(() => {
deleteNotice(item.id, (res) => {
if (res.code === 200) {
showToast('删除成功');
draftRef.value && draftRef.value.refresh();
} else {
showToast(res.msg || '删除失败');
}
});
}).catch(() => {});
};
const onMessage = (item) => {
const readonly = activeTab.value === 'history' ? '1' : '0';
router.push({ path: '/enforcement/notice/message', query: { id: item.id, readonly } });
};
const onReinitiate = (item) => {
// 再次发起:进入新增页面并带入已有信息
router.push({
path: '/enforcement/notice/edit',
query: { reinitiate: item.id },
});
};
const onPublish = (item) => {
// 发布前验证必填字段
if (!item.enforceDate) { showToast('该预告缺少执法日期,请编辑后再发布'); return; }
if (!item.enforceTime) { showToast('该预告缺少执法时段,请编辑后再发布'); return; }
if (!item.enterpriseName) { showToast('该预告缺少企业名称,请编辑后再发布'); return; }
if (!item.enforceUserNames) { showToast('该预告缺少执法人员,请编辑后再发布'); return; }
if (!item.enforceContent) { showToast('该预告缺少执法事项,请编辑后再发布'); return; }
if (!item.enforceRequirement) { showToast('该预告缺少执法要求,请编辑后再发布'); return; }
showConfirmDialog({
title: '提示',
message: '预告发布后,不能删除,不能修改。确认发布?',
confirmButtonText: '确认发布',
cancelButtonText: '取消',
}).then(() => {
const submitData = { ...item, status: 1 };
editNotice(submitData, (res) => {
if (res.code === 200) {
showToast('发布成功');
draftRef.value && draftRef.value.refresh();
} else {
showToast(res.msg || '发布失败');
}
}, (errRes) => {
showToast(errRes?.msg || '发布失败,请重试');
});
}).catch(() => {});
};
</script>
<template>
<style lang="less" scoped>
.notice-page {
display: flex;
flex-direction: column;
height: 100%;
background: #eaf4ff;
position: relative;
overflow: hidden;
}
</template>
:deep(.van-nav-bar) {
background: #347bc3;
flex-shrink: 0;
.van-nav-bar__title {
color: #fff;
font-size: 18px;
font-weight: 600;
}
.van-icon {
color: #fff;
}
}
:deep(.van-tabs) {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
:deep(.van-tabs__wrap) {
height: 54px;
background: #fff;
border-bottom: 1px solid #e4edf7;
flex-shrink: 0;
}
:deep(.van-tab) {
font-size: 16px;
}
:deep(.van-tab--active) {
font-weight: 600;
}
:deep(.van-tabs__content),
:deep(.van-tabs__track),
:deep(.van-tab__panel-wrapper),
:deep(.van-tab__panel) {
flex: 1;
min-height: 0;
height: 100%;
}
:deep(.van-tabs__content) {
overflow: hidden;
}
<style scoped lang="less">
.add-btn {
position: fixed;
right: 22px;
bottom: 28px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #1890ff, #0b63d1);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.38);
z-index: 100;
&:active {
transform: scale(0.96);
}
}
</style>

277
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_message.vue

@ -0,0 +1,277 @@
<template>
<div class="notice-message-page app_content">
<!-- 顶部导航 -->
<van-nav-bar
title="预告留言"
left-arrow
@click-left="onBack"
/>
<!-- 消息列表区域 -->
<div class="message-area" ref="messageAreaRef">
<div v-if="loading" class="loading-wrap">
<van-loading size="18" />&nbsp;加载中...
</div>
<template v-else>
<van-empty v-if="!messageList.length" description="暂无留言" image-size="100" />
<div
v-for="(msg, idx) in messageList"
:key="msg.id || idx"
class="message-item"
:class="{ 'message-right': msg.msgFrom === 0, 'message-left': msg.msgFrom === 1 }"
>
<!-- 左侧(企业消息) -->
<div v-if="msg.msgFrom === 1" class="msg-avatar left-avatar">企</div>
<div class="msg-bubble" :class="{ 'bubble-left': msg.msgFrom === 1, 'bubble-right': msg.msgFrom === 0 }">
<div class="msg-sender">{{ msg.userName }}</div>
<div class="msg-content">{{ msg.message }}</div>
<div class="msg-time">{{ formatTime(msg.createTime) }}</div>
</div>
<!-- 右侧(执法单位消息) -->
<div v-if="msg.msgFrom === 0" class="msg-avatar right-avatar">执</div>
</div>
</template>
</div>
<!-- 底部输入区域(非只读时显示) -->
<div v-if="!isReadonly" class="input-area">
<van-field
v-model="inputMessage"
type="textarea"
placeholder="输入留言内容..."
rows="1"
autosize
class="input-field"
/>
<van-button
type="primary"
size="small"
class="send-btn"
:disabled="!inputMessage.trim()"
:loading="sending"
@click="onSend"
>
发送
</van-button>
</div>
</div>
</template>
<script setup name="cpNoticeMessage">
import { useRouter, useRoute } from 'vue-router';
import { showToast } from 'vant';
import { getNoticeMessages, addNoticeMessage } from '@/api/enforcement/notice';
const router = useRouter();
const route = useRoute();
const { useCommon } = $globalStore;
const noticeId = computed(() => route.query.id || '');
const isReadonly = computed(() => route.query.readonly === '1');
const messageList = ref([]);
const loading = ref(false);
const inputMessage = ref('');
const sending = ref(false);
const messageAreaRef = ref(null);
onMounted(() => {
loadMessages();
});
const loadMessages = () => {
if (!noticeId.value) return;
loading.value = true;
getNoticeMessages(noticeId.value, (res) => {
loading.value = false;
if (res.code === 200 && Array.isArray(res.data)) {
messageList.value = res.data;
scrollToBottom();
}
}, () => {
loading.value = false;
});
};
const onSend = () => {
const text = inputMessage.value.trim();
if (!text) return;
sending.value = true;
addNoticeMessage({
noticeId: noticeId.value,
userId: String(useCommon.userId || ''),
userName: useCommon.nickName || useCommon.userName || '',
msgFrom: 0, // 执法单位=0
message: text,
}, (res) => {
sending.value = false;
if (res.code === 200) {
inputMessage.value = '';
loadMessages();
} else {
showToast(res.msg || '发送失败');
}
}, () => {
sending.value = false;
showToast('发送失败,请重试');
});
};
const scrollToBottom = () => {
nextTick(() => {
if (messageAreaRef.value) {
messageAreaRef.value.scrollTop = messageAreaRef.value.scrollHeight;
}
});
};
const formatTime = (dateStr) => {
if (!dateStr) return '';
return String(dateStr).substring(0, 16).replace('T', ' ');
};
const onBack = () => {
router.back();
};
</script>
<style lang="less" scoped>
.notice-message-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f0f6ff;
overflow: hidden;
}
:deep(.van-nav-bar) {
background: #347bc3;
flex-shrink: 0;
.van-nav-bar__title { color: #fff; font-size: 18px; font-weight: 600; }
.van-icon { color: #fff; }
}
.message-area {
flex: 1;
overflow-y: auto;
padding: 16px 12px;
}
.loading-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
color: #999;
font-size: 14px;
}
.message-item {
display: flex;
align-items: flex-start;
margin-bottom: 16px;
}
.message-left {
flex-direction: row;
}
.message-right {
flex-direction: row-reverse;
}
.msg-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: #fff;
flex-shrink: 0;
}
.left-avatar {
background: linear-gradient(135deg, #f5a623, #f7c948);
margin-right: 10px;
}
.right-avatar {
background: linear-gradient(135deg, #1890ff, #0b63d1);
margin-left: 10px;
}
.msg-bubble {
max-width: 70%;
padding: 10px 14px;
border-radius: 12px;
position: relative;
}
.bubble-left {
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-top-left-radius: 4px;
}
.bubble-right {
background: linear-gradient(135deg, #1890ff, #0b63d1);
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
border-top-right-radius: 4px;
.msg-sender { color: rgba(255, 255, 255, 0.8); }
.msg-content { color: #fff; }
.msg-time { color: rgba(255, 255, 255, 0.6); }
}
.msg-sender {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.msg-content {
font-size: 15px;
color: #333;
line-height: 1.5;
word-break: break-all;
}
.msg-time {
font-size: 11px;
color: #bbb;
margin-top: 4px;
text-align: right;
}
.input-area {
display: flex;
align-items: flex-end;
gap: 10px;
padding: 10px 12px;
background: #fff;
border-top: 1px solid #e4edf7;
flex-shrink: 0;
.input-field {
flex: 1;
background: #f5f7fa;
border-radius: 20px;
padding: 8px 14px;
:deep(.van-field__control) {
min-height: 24px;
}
}
.send-btn {
height: 36px;
border-radius: 18px;
padding: 0 18px;
font-size: 14px;
background: linear-gradient(135deg, #1890ff, #0b63d1);
border: none;
}
}
</style>

423
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/notice/cp_notice_tab_content.vue

@ -0,0 +1,423 @@
<template>
<div class="notice-tab-content">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="notice-list">
<section
v-for="item in noticeList"
:key="item.id"
class="notice-card"
>
<div class="card-header">
<div class="status-tag" :class="'status-' + item.status">
{{ getStatusText(item.status) }}
</div>
<div class="card-date">{{ formatDate(item.createTime) }}</div>
</div>
<div class="notice-title">{{ item.enforceContent || '暂无执法事项' }}</div>
<div class="info-row">
<div class="label">执法日期:</div>
<div class="value">{{ item.enforceDate || '-' }} {{ getTimeText(item.enforceTime) }}</div>
</div>
<div class="info-row">
<div class="label">执法单位:</div>
<div class="value">{{ item.bureauDeptName || '-' }}</div>
</div>
<div class="info-row">
<div class="label">企业名称:</div>
<div class="value">{{ item.enterpriseName || '-' }}</div>
</div>
<div class="info-row">
<div class="label">执法人员:</div>
<div class="value">{{ item.enforceUserNames || '-' }}</div>
</div>
<div class="info-row">
<div class="label">联系人:</div>
<div class="value">{{ item.contactPerson || '-' }}</div>
</div>
<div class="info-row">
<div class="label">联系电话:</div>
<div class="value">{{ item.contactPhone || '-' }}</div>
</div>
<!-- 操作按钮 -->
<div class="card-actions" v-if="canPublish || canEdit || canDelete || canMessage || canReinitiate || item.planId">
<div v-if="item.planId" class="action-item blue-action" @click="onViewPlan(item.planId)">
<van-icon name="orders-o" />
<span>关联计划</span>
</div>
<div v-if="canPublish" class="action-item green-action" @click="$emit('publish', item)">
<van-icon name="passed" />
<span>发布</span>
</div>
<div v-if="canEdit" class="action-item blue-action" @click="$emit('edit', item)">
<van-icon name="edit" />
<span>编辑</span>
</div>
<div v-if="canDelete" class="action-item red-action" @click="$emit('delete', item)">
<van-icon name="delete-o" />
<span>删除</span>
</div>
<div v-if="canMessage" class="action-item blue-action" @click="$emit('message', item)">
<van-icon name="chat-o" />
<span>留言</span>
</div>
<div v-if="canReinitiate" class="action-item green-action" @click="$emit('reinitiate', item)">
<van-icon name="replay" />
<span>再次发起</span>
</div>
</div>
</section>
</div>
<!-- 空状态 -->
<van-empty v-if="!loading && noticeList.length === 0" description="暂无预告" />
</van-list>
</van-pull-refresh>
<!-- 关联计划详情弹层 -->
<van-popup v-model:show="showPlanPopup" position="bottom" round :style="{ height: '65%' }">
<div class="plan-detail-popup">
<div class="plan-detail-header">
<span class="plan-detail-title">关联计划详情</span>
<van-icon name="cross" @click="showPlanPopup = false" />
</div>
<div class="plan-detail-body">
<div v-if="planDetailLoading" class="plan-detail-loading">
<van-loading size="18" />&nbsp;加载中...
</div>
<template v-else-if="planDetail">
<div class="plan-info-row">
<div class="plan-info-label">计划日期</div>
<div class="plan-info-value">{{ planDetail.planDate || '-' }} {{ getPlanTimeText(planDetail.planTime) }}</div>
</div>
<div class="plan-info-row">
<div class="plan-info-label">计划类别</div>
<div class="plan-info-value">{{ planDetail.planType === 1 ? '日常计划' : '专项计划' }}</div>
</div>
<div class="plan-info-row">
<div class="plan-info-label">企业名称</div>
<div class="plan-info-value">{{ planDetail.enterpriseName || '-' }}</div>
</div>
<div class="plan-info-row">
<div class="plan-info-label">企业信用代码</div>
<div class="plan-info-value">{{ planDetail.enterpriseNumber || '-' }}</div>
</div>
<div class="plan-info-row">
<div class="plan-info-label">企业联系人</div>
<div class="plan-info-value">{{ planDetail.contactPerson || '-' }}</div>
</div>
<div class="plan-info-row">
<div class="plan-info-label">联系电话</div>
<div class="plan-info-value">{{ planDetail.contactPhone || '-' }}</div>
</div>
<div class="plan-info-row">
<div class="plan-info-label">执法人员</div>
<div class="plan-info-value">{{ planDetail.enforceUserNames || '-' }}</div>
</div>
<div class="plan-info-row">
<div class="plan-info-label">执法事项</div>
<div class="plan-info-value">{{ planDetail.enforceContent || '-' }}</div>
</div>
</template>
<van-empty v-else description="未查询到计划信息" image-size="80" />
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { getMyNoticeList } from '@/api/enforcement/notice';
import { getPlanDetail } from '@/api/enforcement/plan';
const props = defineProps({
status: { type: Number, required: true },
canPublish: { type: Boolean, default: false },
canEdit: { type: Boolean, default: false },
canDelete: { type: Boolean, default: false },
canMessage: { type: Boolean, default: false },
canReinitiate: { type: Boolean, default: false },
messageReadonly: { type: Boolean, default: false },
});
const emit = defineEmits(['publish', 'edit', 'delete', 'message', 'reinitiate']);
const noticeList = ref([]);
const loading = ref(false);
const finished = ref(false);
const refreshing = ref(false);
const pageNum = ref(1);
const pageSize = 10;
const getStatusText = (status) => {
const map = { 0: '草稿', 1: '已发布', 2: '已取消', 3: '已执行' };
return map[status] || '未知';
};
const getTimeText = (time) => {
const map = { 1: '上午', 2: '下午', 3: '其他' };
return map[time] || '';
};
const formatDate = (dateStr) => {
if (!dateStr) return '';
return String(dateStr).substring(0, 16).replace('T', ' ');
};
const onLoad = () => {
fetchList();
};
const onRefresh = () => {
pageNum.value = 1;
finished.value = false;
noticeList.value = [];
fetchList();
};
const refresh = () => {
onRefresh();
};
const fetchList = () => {
loading.value = true;
const params = {
status: props.status,
pageNum: pageNum.value,
pageSize,
};
getMyNoticeList(
params,
(res) => {
refreshing.value = false;
loading.value = false;
if (res.code === 200) {
const rows = res.rows || [];
if (pageNum.value === 1) {
noticeList.value = rows;
} else {
noticeList.value.push(...rows);
}
if (rows.length < pageSize) {
finished.value = true;
} else {
pageNum.value++;
}
} else {
finished.value = true;
}
},
() => {
refreshing.value = false;
loading.value = false;
finished.value = true;
}
);
};
// 关联计划弹层
const showPlanPopup = ref(false);
const planDetail = ref(null);
const planDetailLoading = ref(false);
const getPlanTimeText = (time) => {
const map = { 1: '上午', 2: '下午', 3: '其他' };
return map[time] || '';
};
const onViewPlan = (planId) => {
showPlanPopup.value = true;
planDetail.value = null;
planDetailLoading.value = true;
getPlanDetail(planId, (res) => {
planDetailLoading.value = false;
if (res.code === 200 && res.data) {
planDetail.value = res.data;
}
}, () => {
planDetailLoading.value = false;
});
};
// 暴露 refresh 方法给父组件
defineExpose({ refresh });
</script>
<style lang="less" scoped>
.notice-tab-content {
height: 100%;
overflow-y: auto;
padding: 12px;
box-sizing: border-box;
}
.notice-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.notice-card {
background: #fff;
border-radius: 14px;
padding: 16px;
box-shadow: 0 4px 14px rgba(44, 116, 190, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.status-tag {
font-size: 12px;
font-weight: 500;
padding: 3px 10px;
border-radius: 10px;
}
.status-0 {
background: #f0f0f0;
color: #666;
}
.status-1 {
background: #e6f7ff;
color: #1890ff;
}
.status-2 {
background: #fff2e8;
color: #fa8c16;
}
.status-3 {
background: #f6ffed;
color: #52c41a;
}
.card-date {
font-size: 12px;
color: #999;
}
.notice-title {
font-size: 16px;
font-weight: 700;
color: #111;
line-height: 1.5;
margin-bottom: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-row {
display: flex;
align-items: flex-start;
margin-bottom: 6px;
font-size: 14px;
line-height: 1.5;
}
.label {
width: 72px;
flex-shrink: 0;
color: #333;
font-weight: 500;
}
.value {
flex: 1;
color: #8a8a8a;
}
.card-actions {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.06);
display: flex;
justify-content: flex-end;
gap: 20px;
}
.action-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
cursor: pointer;
}
.blue-action {
color: #2382cf;
}
.red-action {
color: #f56c6c;
}
.green-action {
color: #52c41a;
}
.plan-detail-popup {
display: flex;
flex-direction: column;
height: 100%;
.plan-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px 8px;
.plan-detail-title { font-size: 16px; font-weight: 600; color: #333; }
:deep(.van-icon) { font-size: 18px; color: #999; cursor: pointer; }
}
.plan-detail-body {
flex: 1;
overflow-y: auto;
padding: 8px 16px 16px;
}
.plan-detail-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
color: #999;
font-size: 14px;
}
.plan-info-row {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child { border-bottom: none; }
.plan-info-label {
width: 90px;
flex-shrink: 0;
font-size: 14px;
color: #666;
}
.plan-info-value {
flex: 1;
font-size: 14px;
color: #333;
word-break: break-all;
}
}
}
</style>

162
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_edit.vue

@ -80,30 +80,12 @@
<van-field
v-model="form.enterpriseName"
label="企业名称"
placeholder="输入≥3个字符,不要输入深圳,区名"
placeholder="请选择企业"
required
@update:model-value="onEnterpriseNameInput"
readonly
is-link
@click="onOpenEnterprisePicker"
/>
<!-- 企业检索结果下拉 -->
<div v-if="showEnterpriseSuggest" class="enterprise-suggest">
<div v-if="enterpriseSearching" class="suggest-empty">
<van-loading size="16" />&nbsp;检索中...
</div>
<template v-else>
<div
v-for="(item, idx) in enterpriseOptions"
:key="idx"
class="suggest-item"
@click="onSelectEnterprise(item)"
>
<div class="suggest-name">{{ item.enterpriseName }}</div>
<div class="suggest-number">{{ item.enterpriseNumber }}</div>
</div>
<div v-if="!enterpriseOptions.length" class="suggest-empty">
未检索到匹配企业
</div>
</template>
</div>
<!-- 执法人员 -->
<van-field label="执法人员" required>
@ -223,6 +205,44 @@
</div>
</div>
</van-popup>
<!-- 企业检索弹层 -->
<van-popup
v-model:show="showEnterprisePicker"
position="bottom"
round
:style="{ height: '70%' }"
>
<div class="enterprise-picker">
<div class="enterprise-picker-header">
<span class="enterprise-picker-title">选择企业</span>
<van-icon name="cross" @click="showEnterprisePicker = false" />
</div>
<van-search
v-model="enterpriseSearchKeyword"
placeholder="输入≥ 3 个字符检索企业,不要输入深圳,深圳市"
shape="round"
@update:model-value="onEnterpriseSearchInput"
/>
<div class="enterprise-picker-body">
<div v-if="enterpriseSearching" class="enterprise-loading">
<van-loading size="18" />&nbsp;检索中...
</div>
<template v-else>
<div
v-for="(item, idx) in enterpriseOptions"
:key="idx"
class="enterprise-item"
@click="onSelectEnterprise(item)"
>
<div class="enterprise-item-name">{{ item.enterpriseName }}</div>
<div class="enterprise-item-number">{{ item.enterpriseNumber }}</div>
</div>
<van-empty v-if="enterpriseSearchKeyword.length >= 3 && !enterpriseOptions.length" description="未检索到匹配企业" image-size="80" />
</template>
</div>
</div>
</van-popup>
</div>
</template>
@ -315,7 +335,8 @@ const maxPersonNum = ref(3);
// 企业搜索状态
const enterpriseOptions = ref([]);
const showEnterpriseSuggest = ref(false);
const showEnterprisePicker = ref(false);
const enterpriseSearchKeyword = ref('');
const enterpriseSearching = ref(false);
let enterpriseSearchTimer = null;
let enterpriseSearchSeq = 0;
@ -502,26 +523,27 @@ const onDateConfirm = ({ selectedValues }) => {
showDatePicker.value = false;
};
// 打开企业检索弹层
const onOpenEnterprisePicker = () => {
enterpriseSearchKeyword.value = '';
enterpriseOptions.value = [];
showEnterprisePicker.value = true;
};
// 企业名称输入(防抖检索)
const onEnterpriseNameInput = (val) => {
// 手动修改名称后,原先绑定的 enterpriseNumber/enterpriseId 失效,需重新选择
form.value.enterpriseNumber = '';
form.value.enterpriseId = '';
const onEnterpriseSearchInput = (val) => {
if (enterpriseSearchTimer) {
clearTimeout(enterpriseSearchTimer);
}
const keyword = (val || '').trim();
if (keyword.length < 3) {
showEnterpriseSuggest.value = false;
enterpriseOptions.value = [];
return;
}
enterpriseSearchTimer = setTimeout(() => {
const seq = ++enterpriseSearchSeq;
showEnterpriseSuggest.value = true;
enterpriseSearching.value = true;
searchUnionByEnterpriseName(keyword, (res) => {
// 仅处理最新一次请求的结果
if (seq !== enterpriseSearchSeq) return;
enterpriseSearching.value = false;
if (res && res.code === 200 && Array.isArray(res.data)) {
@ -541,9 +563,8 @@ const onEnterpriseNameInput = (val) => {
const onSelectEnterprise = (item) => {
form.value.enterpriseName = item.enterpriseName || '';
form.value.enterpriseNumber = item.enterpriseNumber || '';
// 企业检索来源VO无独立id字段(市监同步表无id),用信用代码占位以满足后端@NotBlank校验
form.value.enterpriseId = item.enterpriseNumber || '';
showEnterpriseSuggest.value = false;
showEnterprisePicker.value = false;
enterpriseOptions.value = [];
};
@ -860,50 +881,69 @@ const doSubmit = () => {
}
}
.enterprise-suggest {
background: #fff;
margin: -4px 14px 8px;
border: 1px solid #e6edf5;
border-radius: 8px;
max-height: 220px;
overflow-y: auto;
box-shadow: 0 4px 10px rgba(44, 116, 190, 0.08);
.enterprise-picker {
display: flex;
flex-direction: column;
height: 100%;
.suggest-item {
padding: 10px 14px;
border-bottom: 1px solid #f0f2f5;
cursor: pointer;
.enterprise-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px 8px;
&:last-child {
border-bottom: none;
.enterprise-picker-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
:deep(.van-icon) {
font-size: 18px;
color: #999;
cursor: pointer;
}
}
.enterprise-picker-body {
flex: 1;
overflow-y: auto;
padding: 0 12px 12px;
}
.enterprise-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30px 0;
color: #999;
font-size: 14px;
}
.enterprise-item {
padding: 12px 14px;
background: #fff;
border: 1px solid #e6edf5;
border-radius: 10px;
margin-bottom: 8px;
cursor: pointer;
&:active {
background: #f5faff;
}
.suggest-name {
font-size: 14px;
.enterprise-item-name {
font-size: 15px;
color: #333;
line-height: 1.4;
font-weight: 500;
}
.suggest-number {
.enterprise-item-number {
font-size: 12px;
color: #999;
margin-top: 2px;
margin-top: 4px;
}
}
.suggest-empty {
padding: 14px;
text-align: center;
color: #999;
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
}
}
.submit-wrap {

Loading…
Cancel
Save