Browse Source

执法端-计划

dev-V1.0.1
庄锐波 5 months ago
parent
commit
7ef1273d3b
  1. 2
      script/sql/zfjd-refactoring-v1/013-计划任务表设计.sql
  2. 6
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java
  3. 13
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpReserveVo.java
  4. 113
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java
  5. 76
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java
  6. 4
      zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpReserveMapper.xml
  7. 16
      zdxt-web-client/zdxt-efcode-enforcement-app/src/api/enforcement/task.js
  8. 4
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_list.vue
  9. 169
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_mine.vue
  10. 18
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/reserve/cp_reserve_manual.vue

2
script/sql/zfjd-refactoring-v1/013-计划任务表设计.sql

@ -24,7 +24,7 @@ CREATE TABLE `efcode_cp_plan`
`is_union` tinyint NOT NULL DEFAULT 1 COMMENT '是否联合执法:1=是,0=否', `is_union` tinyint NOT NULL DEFAULT 1 COMMENT '是否联合执法:1=是,0=否',
`union_bureau_dept_names` varchar(500) DEFAULT NULL COMMENT '联合执法单位(多个逗号分隔)', `union_bureau_dept_names` varchar(500) DEFAULT NULL COMMENT '联合执法单位(多个逗号分隔)',
`reserve_status` tinyint NOT NULL DEFAULT 0 COMMENT '预约状态:0=草稿,1=待预约(未发起联合),2=预约中,3=预约完成,4=重新预约中,5=待预约(还有位置可以加入),6=待预约(人数已满)', `reserve_status` tinyint NOT NULL DEFAULT 0 COMMENT '预约状态:0=草稿,1=待预约(未发起联合),2=预约中,3=预约完成,4=重新预约中,5=待预约(还有位置可以加入),6=待预约(人数已满),7=预约完成(人数已满)',
`task_id` varchar(50) NULL DEFAULT '' COMMENT '任务id:空串表示还没生成任务', `task_id` varchar(50) NULL DEFAULT '' COMMENT '任务id:空串表示还没生成任务',
`system_reserved` tinyint NOT NULL DEFAULT 0 COMMENT '是否已经经过系统推荐预约:1=是,0=否', `system_reserved` tinyint NOT NULL DEFAULT 0 COMMENT '是否已经经过系统推荐预约:1=是,0=否',

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

@ -236,5 +236,11 @@ public class EfcodeCpPlanVo implements Serializable {
@com.baomidou.mybatisplus.annotation.TableField(exist = false) @com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Boolean canReserve; private Boolean canReserve;
/**
* 关联的预约记录列表(非数据库字段,后置填充)
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private java.util.List<EfcodeCpReserveVo> reserves;
} }

13
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpReserveVo.java

@ -96,6 +96,16 @@ public class EfcodeCpReserveVo implements Serializable {
@ExcelProperty(value = "来自:系统匹配=1,手动匹配=0") @ExcelProperty(value = "来自:系统匹配=1,手动匹配=0")
private Long resFrom; private Long resFrom;
/**
* 发送者用户ID
*/
private String senderId;
/**
* 接收者用户ID
*/
private String receiverId;
// ===== 计划关联字段(非数据库映射,由服务层填充) ===== // ===== 计划关联字段(非数据库映射,由服务层填充) =====
/** 计划日期 */ /** 计划日期 */
@ -116,5 +126,8 @@ public class EfcodeCpReserveVo implements Serializable {
/** 执法人员姓名 */ /** 执法人员姓名 */
private String enforceUserNames; private String enforceUserNames;
/** 执法人员电话号码 */
private String enforceUserTels;
} }

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

@ -13,6 +13,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo; import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpPlanVo; import com.zdxt.enforcementcode.domain.vo.EfcodeCpPlanVo;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpReserveVo;
import com.zdxt.enforcementcode.domain.EfcodeCpPlan; import com.zdxt.enforcementcode.domain.EfcodeCpPlan;
import com.zdxt.enforcementcode.domain.EfcodeCpPlanUsers; import com.zdxt.enforcementcode.domain.EfcodeCpPlanUsers;
import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper; import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper;
@ -81,6 +82,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
Page<EfcodeCpPlanVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw); Page<EfcodeCpPlanVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
// 后置计算 editEnable // 后置计算 editEnable
computeEditEnable(result.getRecords()); computeEditEnable(result.getRecords());
// 后置填充关联预约记录
fillReserves(result.getRecords());
return TableDataInfo.build(result); return TableDataInfo.build(result);
} }
@ -162,6 +165,109 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
} }
} }
/**
* 后置填充关联预约记录:通过plan_group_id查询同组的其他计划信息,组装为1:N结构
* <p>展示逻辑:对于我的每条计划,找到同一plan_group_id下的其他计划,将对方的单位/人员/电话/执法事项填充到reserve VO中</p>
*/
private void fillReserves(List<EfcodeCpPlanVo> records) {
if (records == null || records.isEmpty()) {
return;
}
List<String> myPlanIds = records.stream().map(EfcodeCpPlanVo::getId).collect(Collectors.toList());
if (myPlanIds.isEmpty()) {
return;
}
// 1. 查询我的计划关联的预约记录,获取plan_group_id
List<EfcodeCpReserve> myReserves = efcodeCpReserveMapper.selectList(
Wrappers.<EfcodeCpReserve>lambdaQuery()
.in(EfcodeCpReserve::getPlanId, myPlanIds)
);
if (myReserves.isEmpty()) {
records.forEach(vo -> vo.setReserves(new ArrayList<>()));
return;
}
// 2. 建立 我的planId → plan_group_id 的映射
Map<String, String> planToGroupMap = new HashMap<>();
Set<String> groupIds = new HashSet<>();
for (EfcodeCpReserve r : myReserves) {
if (StringUtils.isNotBlank(r.getPlanGroupId())) {
planToGroupMap.put(r.getPlanId(), r.getPlanGroupId());
groupIds.add(r.getPlanGroupId());
}
}
if (groupIds.isEmpty()) {
records.forEach(vo -> vo.setReserves(new ArrayList<>()));
return;
}
// 3. 查询同组的其他预约记录(plan_id不是我的计划)
List<EfcodeCpReserve> otherReserves = efcodeCpReserveMapper.selectList(
Wrappers.<EfcodeCpReserve>lambdaQuery()
.in(EfcodeCpReserve::getPlanGroupId, groupIds)
.notIn(EfcodeCpReserve::getPlanId, myPlanIds)
.orderByDesc(EfcodeCpReserve::getReserveTime)
);
// 4. 查询其他计划的详情(单位、人员、电话、执法事项)
Set<String> otherPlanIds = otherReserves.stream()
.map(EfcodeCpReserve::getPlanId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
Map<String, EfcodeCpPlan> otherPlanMap = new HashMap<>();
if (!otherPlanIds.isEmpty()) {
List<EfcodeCpPlan> otherPlans = baseMapper.selectByIds(new ArrayList<>(otherPlanIds));
for (EfcodeCpPlan p : otherPlans) {
otherPlanMap.put(p.getId(), p);
}
}
// 5. 按plan_group_id分组其他预约记录
Map<String, List<EfcodeCpReserve>> groupToOtherReserves = otherReserves.stream()
.collect(Collectors.groupingBy(EfcodeCpReserve::getPlanGroupId));
// 6. 填充到每条计划VO
for (EfcodeCpPlanVo vo : records) {
String groupId = planToGroupMap.get(vo.getId());
if (StringUtils.isBlank(groupId)) {
vo.setReserves(new ArrayList<>());
continue;
}
List<EfcodeCpReserve> others = groupToOtherReserves.getOrDefault(groupId, new ArrayList<>());
List<EfcodeCpReserveVo> voList = others.stream().map(r -> {
EfcodeCpReserveVo rv = new EfcodeCpReserveVo();
rv.setId(r.getId());
rv.setPlanId(r.getPlanId());
rv.setPlanGroupId(r.getPlanGroupId());
rv.setReserveTime(r.getReserveTime());
rv.setOpStatus(r.getOpStatus());
rv.setOpOrder(r.getOpOrder());
rv.setOpTime(r.getOpTime());
rv.setOpMessage(r.getOpMessage());
rv.setOpSetDate(r.getOpSetDate());
rv.setOpSetTime(r.getOpSetTime());
rv.setResFrom(r.getResFrom());
rv.setSenderId(r.getSenderId());
rv.setReceiverId(r.getReceiverId());
// 填充对方计划详情
EfcodeCpPlan otherPlan = otherPlanMap.get(r.getPlanId());
if (otherPlan != null) {
rv.setBureauDeptName(otherPlan.getBureauDeptName());
rv.setEnforceUserNames(otherPlan.getEnforceUserNames());
rv.setEnforceUserTels(otherPlan.getEnforceUserTels());
rv.setEnforceContent(otherPlan.getEnforceContent());
rv.setPlanDate(otherPlan.getPlanDate());
rv.setPlanTime(otherPlan.getPlanTime());
rv.setEnterpriseName(otherPlan.getEnterpriseName());
}
return rv;
}).collect(Collectors.toList());
vo.setReserves(voList);
}
}
private LambdaQueryWrapper<EfcodeCpPlan> buildQueryWrapper(EfcodeCpPlanBo bo) { private LambdaQueryWrapper<EfcodeCpPlan> buildQueryWrapper(EfcodeCpPlanBo bo) {
Map<String, Object> params = bo.getParams(); Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<EfcodeCpPlan> lqw = Wrappers.lambdaQuery(); LambdaQueryWrapper<EfcodeCpPlan> lqw = Wrappers.lambdaQuery();
@ -327,7 +433,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
* @throws ServiceException 如果存在重复记录则抛出 * @throws ServiceException 如果存在重复记录则抛出
*/ */
private void checkDuplicate(EfcodeCpPlanBo bo) { private void checkDuplicate(EfcodeCpPlanBo bo) {
if (bo.getPlanType() == null || bo.getPlanDate() == null || bo.getPlanTime() == null if (bo.getPlanDate() == null || bo.getPlanTime() == null
|| StringUtils.isBlank(bo.getEnterpriseId())) { || StringUtils.isBlank(bo.getEnterpriseId())) {
return; return;
} }
@ -336,9 +442,8 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
if (StringUtils.isBlank(currentUserId) || "null".equals(currentUserId)) { if (StringUtils.isBlank(currentUserId) || "null".equals(currentUserId)) {
return; return;
} }
// 1) 查找同计划类型+同日期+同时段+同企业的已有计划 // 1) 查找同日期+同时段+同企业的已有计划(不限制计划类型)
LambdaQueryWrapper<EfcodeCpPlan> lqw = Wrappers.lambdaQuery(); LambdaQueryWrapper<EfcodeCpPlan> lqw = Wrappers.lambdaQuery();
lqw.eq(EfcodeCpPlan::getPlanType, bo.getPlanType());
lqw.eq(EfcodeCpPlan::getPlanDate, bo.getPlanDate()); lqw.eq(EfcodeCpPlan::getPlanDate, bo.getPlanDate());
lqw.eq(EfcodeCpPlan::getPlanTime, bo.getPlanTime()); lqw.eq(EfcodeCpPlan::getPlanTime, bo.getPlanTime());
lqw.eq(EfcodeCpPlan::getEnterpriseId, bo.getEnterpriseId()); lqw.eq(EfcodeCpPlan::getEnterpriseId, bo.getEnterpriseId());
@ -360,7 +465,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
.eq(EfcodeCpPlanUsers::getUserId, currentUserId) .eq(EfcodeCpPlanUsers::getUserId, currentUserId)
); );
if (count > 0) { if (count > 0) {
throw new ServiceException("您在该日期该时段对该企业已有同类型执法计划,不可重复提交"); throw new ServiceException("您在该日期该时段对该企业已有执法计划,不可重复提交");
} }
} }

76
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java

@ -167,7 +167,7 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
} }
/** /**
* 预约前置检查 * 预约前置检查(包含所有前置校验逻辑)
*/ */
@Override @Override
public R<Void> checkCanReserve(String targetPlanId) { public R<Void> checkCanReserve(String targetPlanId) {
@ -177,11 +177,48 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
if (targetPlan == null) { if (targetPlan == null) {
return R.fail("目标计划不存在"); return R.fail("目标计划不存在");
} }
// 1.1 校验目标计划状态:只有待预约(1)、已取消(4)、预约中可加入(5)的计划才允许预约
Long reserveStatus = targetPlan.getReserveStatus();
if (reserveStatus == null || !Arrays.asList(1L, 4L, 5L).contains(reserveStatus)) {
return R.fail("该计划当前状态不允许预约");
}
// 2. 判断当前用户是否有同一时间+同一时段+同一企业的计划 // 2. 判断当前用户是否有同一时间+同一时段+同一企业的计划
EfcodeCpPlan myPlan = findMyMatchingPlan(targetPlan, currentUserId); EfcodeCpPlan myPlan = findMyMatchingPlan(targetPlan, currentUserId);
if (myPlan == null) { if (myPlan == null) {
return R.fail("请先添加我的计划再进行预约"); return R.fail("请先添加我的计划再进行预约");
} }
// 2.1 校验我的计划状态:只有待预约(1)、已取消(4)、预约中可加入(5)的计划才允许发起预约
Long myReserveStatus = myPlan.getReserveStatus();
if (myReserveStatus == null || !Arrays.asList(1L, 4L, 5L).contains(myReserveStatus)) {
return R.fail("您的计划当前状态不允许发起预约");
}
// 2.2 我的计划在efcode_cp_reserve只能存在一条op_status为3的记录
long pendingCount = baseMapper.selectCount(
Wrappers.<EfcodeCpReserve>lambdaQuery()
.eq(EfcodeCpReserve::getPlanId, myPlan.getId())
.eq(EfcodeCpReserve::getOpStatus, 3L)
);
if (pendingCount > 0) {
return R.fail("您的这条计划已有一条待确认的预约记录,请等待对方处理后再发起新预约");
}
// 2.3 如果我的计划已经作为plan_group_id而存在,则不允许发起预约,除非其他的所有记录的op_status都是2或4
String myPlanId = myPlan.getId();
long groupCount = baseMapper.selectCount(
Wrappers.<EfcodeCpReserve>lambdaQuery()
.eq(EfcodeCpReserve::getPlanGroupId, myPlanId)
);
if (groupCount > 0) {
List<EfcodeCpReserve> otherReserves = baseMapper.selectList(
Wrappers.<EfcodeCpReserve>lambdaQuery()
.eq(EfcodeCpReserve::getPlanGroupId, myPlanId)
.ne(EfcodeCpReserve::getPlanId, myPlanId)
);
boolean allRejectedOrCancelled = otherReserves.stream()
.allMatch(r -> r.getOpStatus() != null && (r.getOpStatus() == 2L || r.getOpStatus() == 4L));
if (!allRejectedOrCancelled) {
return R.fail("您的计划已有进行中的预约分组,无法再发起新预约");
}
}
// 3. 判断 plan_group_id 人数是否已达上限 // 3. 判断 plan_group_id 人数是否已达上限
String planGroupId = findExistingPlanGroupId(targetPlanId); String planGroupId = findExistingPlanGroupId(targetPlanId);
if (planGroupId != null) { if (planGroupId != null) {
@ -205,17 +242,16 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public R<Void> doReserve(String targetPlanId, String opMessage) { public R<Void> doReserve(String targetPlanId, String opMessage) {
// 前置校验(与 checkCanReserve 一致)
R<Void> checkResult = checkCanReserve(targetPlanId);
if (checkResult.getCode() != 200) {
return checkResult;
}
Long currentUserId = LoginHelper.getUserId(); Long currentUserId = LoginHelper.getUserId();
// 1. 查询目标计划
EfcodeCpPlan targetPlan = cpPlanMapper.selectById(targetPlanId); EfcodeCpPlan targetPlan = cpPlanMapper.selectById(targetPlanId);
if (targetPlan == null) {
return R.fail("目标计划不存在");
}
// 2. 查找当前用户匹配的计划
EfcodeCpPlan myPlan = findMyMatchingPlan(targetPlan, currentUserId); EfcodeCpPlan myPlan = findMyMatchingPlan(targetPlan, currentUserId);
if (myPlan == null) {
return R.fail("请先添加我的计划再进行预约");
}
// 3. 判断目标计划在reserve表中是否已存在 // 3. 判断目标计划在reserve表中是否已存在
String planGroupId = findExistingPlanGroupId(targetPlanId); String planGroupId = findExistingPlanGroupId(targetPlanId);
int maxUnionUnit = cpSettingService.getMaxUnionUnit(); int maxUnionUnit = cpSettingService.getMaxUnionUnit();
@ -229,9 +265,9 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
String senderIdStr = String.valueOf(currentUserId); String senderIdStr = String.valueOf(currentUserId);
String receiverIdStr = String.valueOf(targetPlan.getCreateBy()); String receiverIdStr = String.valueOf(targetPlan.getCreateBy());
// 目标计划记录 // 目标计划记录
insertReserveRecord(targetPlanId, planGroupId, "", targetPlan.getPlanDate(), targetPlan.getPlanTime(), 0L, senderIdStr, receiverIdStr); insertReserveRecord(targetPlanId, planGroupId, "", targetPlan.getPlanDate(), targetPlan.getPlanTime(), 0L, 1L, "manual_system_sender", receiverIdStr);
// 我的计划记录 // 我的计划记录
insertReserveRecord(myPlan.getId(), planGroupId, opMessage, targetPlan.getPlanDate(), targetPlan.getPlanTime(), 0L, senderIdStr, receiverIdStr); insertReserveRecord(myPlan.getId(), planGroupId, opMessage, targetPlan.getPlanDate(), targetPlan.getPlanTime(), 0L, 3L, senderIdStr, receiverIdStr);
} else { } else {
// 已存在:检查人数上限 // 已存在:检查人数上限
long activeCount = countActiveReserves(planGroupId); long activeCount = countActiveReserves(planGroupId);
@ -244,7 +280,7 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
return R.fail("您已经预约过该计划"); return R.fail("您已经预约过该计划");
} }
// 插入1条记录 // 插入1条记录
insertReserveRecord(myPlan.getId(), planGroupId, opMessage, targetPlan.getPlanDate(), targetPlan.getPlanTime(), 0L, String.valueOf(currentUserId), String.valueOf(targetPlan.getCreateBy())); insertReserveRecord(myPlan.getId(), planGroupId, opMessage, targetPlan.getPlanDate(), targetPlan.getPlanTime(), 0L, 3L, String.valueOf(currentUserId), String.valueOf(targetPlan.getCreateBy()));
} }
// ===== 插入预约记录后:更新两个计划的 reserve_status ===== // ===== 插入预约记录后:更新两个计划的 reserve_status =====
@ -323,16 +359,17 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
/** /**
* 插入一条预约记录 * 插入一条预约记录
* *
* @param opStatus 操作状态:1=同意,2=拒绝,3=待确认,4=超期取消
* @param senderId 发送者用户ID * @param senderId 发送者用户ID
* @param receiverId 接收者用户ID * @param receiverId 接收者用户ID
*/ */
private void insertReserveRecord(String planId, String planGroupId, String opMessage, Date opSetDate, Long opSetTime, Long resFrom, private void insertReserveRecord(String planId, String planGroupId, String opMessage, Date opSetDate, Long opSetTime, Long resFrom,
String senderId, String receiverId) { Long opStatus, String senderId, String receiverId) {
EfcodeCpReserve record = new EfcodeCpReserve(); EfcodeCpReserve record = new EfcodeCpReserve();
record.setPlanId(planId); record.setPlanId(planId);
record.setPlanGroupId(planGroupId); record.setPlanGroupId(planGroupId);
record.setReserveTime(new Date()); record.setReserveTime(new Date());
record.setOpStatus(3L); // 待确认 record.setOpStatus(opStatus);
record.setOpOrder(0L); record.setOpOrder(0L);
record.setOpMessage(opMessage); record.setOpMessage(opMessage);
record.setOpSetDate(opSetDate); record.setOpSetDate(opSetDate);
@ -389,6 +426,17 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
*/ */
private void afterOperate(EfcodeCpReserve reserve, String action) { private void afterOperate(EfcodeCpReserve reserve, String action) {
log.info("预约记录[{}]执行{}操作后置处理, planGroupId={}", reserve.getId(), action, reserve.getPlanGroupId()); log.info("预约记录[{}]执行{}操作后置处理, planGroupId={}", reserve.getId(), action, reserve.getPlanGroupId());
// 同步更新对应计划的 reserve_status:同意→3(预约完成),拒绝→4(重新预约中)
if (StringUtils.isNotBlank(reserve.getPlanId())) {
Long newPlanStatus = "approve".equals(action) ? 3L : 4L;
EfcodeCpPlan planUpdate = new EfcodeCpPlan();
planUpdate.setId(reserve.getPlanId());
planUpdate.setReserveStatus(newPlanStatus);
cpPlanMapper.updateById(planUpdate);
log.info("[预约后置] 计划[{}] reserve_status 更新为 {}", reserve.getPlanId(), newPlanStatus);
}
// 同意操作后:检查同一 planGroupId 已同意数量是否达到最大联合单位数 // 同意操作后:检查同一 planGroupId 已同意数量是否达到最大联合单位数
if ("approve".equals(action) && StringUtils.isNotBlank(reserve.getPlanGroupId())) { if ("approve".equals(action) && StringUtils.isNotBlank(reserve.getPlanGroupId())) {
int maxUnionUnit = cpSettingService.getMaxUnionUnit(); int maxUnionUnit = cpSettingService.getMaxUnionUnit();

4
zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpReserveMapper.xml

@ -23,10 +23,12 @@
p.enterprise_name, p.enterprise_name,
p.enforce_content, p.enforce_content,
p.bureau_dept_name, p.bureau_dept_name,
p.enforce_user_names p.enforce_user_names,
p.enforce_user_tels
FROM efcode_cp_reserve r FROM efcode_cp_reserve r
LEFT JOIN efcode_cp_plan p ON r.plan_id = p.id LEFT JOIN efcode_cp_plan p ON r.plan_id = p.id
<where> <where>
r.sender_id != 'manual_system_sender'
<!-- 按来源过滤:0=手动预约,1=系统匹配 --> <!-- 按来源过滤:0=手动预约,1=系统匹配 -->
<if test="resFrom != null"> <if test="resFrom != null">
AND r.res_from = #{resFrom} AND r.res_from = #{resFrom}

16
zdxt-web-client/zdxt-efcode-enforcement-app/src/api/enforcement/task.js

@ -0,0 +1,16 @@
import { _get } from "@/http/index";
// region 任务管理接口
const taskBaseUrl = "/app-enf/cpTask";
// 我的任务列表(指定日期)
export const getMyTaskList = (params, success, error) => _get(taskBaseUrl + "/myList", params, success, error);
// 指定月份中有任务的日期列表(日历打点)
export const getMyTaskDates = (yearMonth, success, error) => _get(taskBaseUrl + "/myTaskDates", { yearMonth }, success, error);
// 我的任务详情(通过 planId 查询)
export const getMyTaskDetail = (planId, success, error) => _get(taskBaseUrl + "/myDetail", { planId }, success, error);
// endregion

4
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_list.vue

@ -25,8 +25,8 @@
</van-tab> </van-tab>
</van-tabs> </van-tabs>
<!-- 悬浮新增按钮 --> <!-- 悬浮新增按钮(仅我的计划Tab显示) -->
<div class="add-btn" @click="onAdd"> <div v-if="activeTab === 'mine'" class="add-btn" @click="onAdd">
<van-icon name="plus" size="22" color="#fff" /> <van-icon name="plus" size="22" color="#fff" />
</div> </div>
</div> </div>

169
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_mine.vue

@ -39,7 +39,11 @@
v-for="(item, index) in planList" v-for="(item, index) in planList"
:key="item.id" :key="item.id"
class="plan-card normal-card" class="plan-card normal-card"
:class="{ 'pending-card': item.reserveStatus === 1 }" :class="{
'pending-card': item.reserveStatus === 1,
'expanded-card': expandedId === item.id
}"
@click="onToggleExpand(item)"
> >
<div class="card-header"> <div class="card-header">
<div class="card-type">{{ item.isUnion === 1 ? '联合执法' : '非联合执法' }}</div> <div class="card-type">{{ item.isUnion === 1 ? '联合执法' : '非联合执法' }}</div>
@ -74,11 +78,53 @@
<div class="value gray">{{ item.enterpriseName }}</div> <div class="value gray">{{ item.enterpriseName }}</div>
</div> </div>
<!-- 展开的预约信息区域 -->
<div v-if="expandedId === item.id && item.reserves && item.reserves.length > 0" class="reserve-section">
<div class="reserve-section-title">关联预约信息</div>
<div
v-for="(rv, ri) in item.reserves"
:key="rv.id || ri"
class="reserve-item"
>
<div class="reserve-row">
<span class="reserve-label">状态:</span>
<span class="reserve-value" :class="'op-status-' + rv.opStatus">{{ getOpStatusText(rv.opStatus) }}</span>
</div>
<div v-if="rv.bureauDeptName" class="reserve-row">
<span class="reserve-label">对方单位:</span>
<span class="reserve-value">{{ rv.bureauDeptName }}</span>
</div>
<div v-if="rv.enforceUserNames" class="reserve-row">
<span class="reserve-label">执法人员:</span>
<span class="reserve-value">{{ rv.enforceUserNames }}</span>
</div>
<div v-if="rv.enforceUserTels" class="reserve-row">
<span class="reserve-label">联系电话:</span>
<span class="reserve-value">{{ rv.enforceUserTels }}</span>
</div>
<div v-if="rv.enforceContent" class="reserve-row">
<span class="reserve-label">执法事项:</span>
<span class="reserve-value">{{ rv.enforceContent }}</span>
</div>
<div v-if="rv.opMessage" class="reserve-row">
<span class="reserve-label">留言:</span>
<span class="reserve-value">{{ rv.opMessage }}</span>
</div>
<div class="reserve-row">
<span class="reserve-label">来源:</span>
<span class="reserve-value">{{ rv.resFrom === 1 ? '系统匹配' : '手动预约' }}</span>
</div>
</div>
</div>
<div v-if="expandedId === item.id && (!item.reserves || item.reserves.length === 0)" class="reserve-section">
<div class="reserve-empty">暂无关联预约记录</div>
</div>
<div v-if="item.editEnable !== 0 || item.deleteEnable !== 0" class="card-actions"> <div v-if="item.editEnable !== 0 || item.deleteEnable !== 0" class="card-actions">
<div <div
v-if="item.editEnable !== 0" v-if="item.editEnable !== 0"
class="action-item blue-action" class="action-item blue-action"
@click="onEditPlan(item)" @click.stop="onEditPlan(item)"
> >
<van-icon name="edit" /> <van-icon name="edit" />
<span>编辑</span> <span>编辑</span>
@ -86,7 +132,7 @@
<div <div
v-if="item.deleteEnable !== 0" v-if="item.deleteEnable !== 0"
class="action-item blue-action" class="action-item blue-action"
@click="onDeletePlan(item)" @click.stop="onDeletePlan(item)"
> >
<van-icon name="delete-o" /> <van-icon name="delete-o" />
<span>删除</span> <span>删除</span>
@ -239,10 +285,16 @@ const finished = ref(false);
const refreshing = ref(false); const refreshing = ref(false);
const pageNum = ref(1); const pageNum = ref(1);
const pageSize = 10; const pageSize = 10;
const expandedId = ref(null); // 当前展开的卡片ID
// 切换展开/收起
const onToggleExpand = (item) => {
expandedId.value = expandedId.value === item.id ? null : item.id;
};
// 状态文本映射 // 状态文本映射
const getStatusText = (status) => { const getStatusText = (status) => {
const map = { 0: '草稿', 1: '待预约', 2: '预约中', 3: '预约完成' }; const map = { 0: '草稿', 1: '待预约(未发起联合)', 2: '预约中', 3: '预约完成', 4: '重新预约中', 5: '待预约(还有位置可以加入)', 6: '待预约(人数已满)', 7: '预约完成(人数已满)' };
return map[status] || '待确认'; return map[status] || '待确认';
}; };
@ -258,6 +310,18 @@ const getPersonList = (item) => {
return item.enforceUserNames.split(','); return item.enforceUserNames.split(',');
}; };
// 预约操作状态文本
const getOpStatusText = (status) => {
const map = { 1: '已同意', 2: '已拒绝', 3: '待确认', 4: '超期取消' };
return map[status] || '未知';
};
// 格式化日期时间
const formatDateTime = (dateStr) => {
if (!dateStr) return '-';
return String(dateStr).substring(0, 16).replace('T', ' ');
};
// 加载数据 // 加载数据
const onLoad = () => { const onLoad = () => {
fetchList(); fetchList();
@ -500,6 +564,103 @@ const onDeletePlan = (item) => {
border: 1.5px solid rgba(35, 130, 207, 0.35); border: 1.5px solid rgba(35, 130, 207, 0.35);
} }
/* 展开状态卡片:深蓝渐变背景(同预约处理选中效果) */
.expanded-card {
background: radial-gradient(circle at 78% 5%, rgba(65, 188, 255, 0.75) 0, rgba(65, 188, 255, 0.25) 20%, transparent 42%),
linear-gradient(135deg, #064ec6 0%, #0b63d9 48%, #268ee5 100%) !important;
border-color: transparent;
box-shadow: 0 6px 20px rgba(6, 78, 198, 0.25) !important;
.card-type {
background: rgba(255, 255, 255, 0.15) !important;
color: rgba(255, 255, 255, 0.9) !important;
}
.plan-title {
color: #fff !important;
}
.label {
color: #fff !important;
}
.value {
color: rgba(255, 255, 255, 0.7) !important;
}
.status {
color: #f0b66c !important;
}
.card-actions {
border-top-color: rgba(255, 255, 255, 0.18);
}
.action-item {
color: rgba(255, 255, 255, 0.85);
}
}
/* 预约信息展开区域 */
.reserve-section {
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed rgba(255, 255, 255, 0.25);
.reserve-section-title {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 10px;
}
.reserve-empty {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
text-align: center;
padding: 8px 0;
}
}
.reserve-item {
background: rgba(255, 255, 255, 0.12);
border-radius: 10px;
padding: 10px 12px;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
.reserve-row {
display: flex;
align-items: center;
margin-bottom: 4px;
font-size: 13px;
&:last-child {
margin-bottom: 0;
}
.reserve-label {
width: 70px;
flex-shrink: 0;
color: rgba(255, 255, 255, 0.75);
font-weight: 500;
}
.reserve-value {
flex: 1;
color: rgba(255, 255, 255, 0.85);
}
.op-status-1 { color: #7dffaa; font-weight: 500; }
.op-status-2 { color: #ff8a8a; font-weight: 500; }
.op-status-3 { color: #f0b66c; font-weight: 500; }
.op-status-4 { color: rgba(255, 255, 255, 0.5); font-weight: 500; }
}
/* 普通卡片 */ /* 普通卡片 */
.normal-card { .normal-card {
background: #fff; background: #fff;

18
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/reserve/cp_reserve_manual.vue

@ -44,7 +44,17 @@
<div class="info-row"> <div class="info-row">
<div class="label">计划时间:</div> <div class="label">计划时间:</div>
<div class="value">{{ item.planDate || '-' }} {{ getPlanTimeText(item.planTime) }}</div> <div class="value">{{ formatPlanDate(item.planDate) }} {{ getPlanTimeText(item.planTime) }}</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.enforceUserTels || '-' }}</div>
</div> </div>
<div class="info-row"> <div class="info-row">
@ -124,6 +134,12 @@ const getPlanTimeText = (time) => {
return map[time] || ''; return map[time] || '';
}; };
// 格式化计划日期(只取年月日,去掉时分秒)
const formatPlanDate = (dateStr) => {
if (!dateStr) return '-';
return String(dateStr).substring(0, 10);
};
// 格式化日期 // 格式化日期
const formatDate = (dateStr) => { const formatDate = (dateStr) => {
if (!dateStr) return '-'; if (!dateStr) return '-';

Loading…
Cancel
Save