Browse Source

执法端-计划

dev-V1.0.1
庄锐波 5 months ago
parent
commit
dbfeaca337
  1. 51
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnfCpTaskController.java
  2. 7
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpPlanVo.java
  3. 92
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpTaskVo.java
  4. 30
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/EfcodeCpTaskMapper.java
  5. 37
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpTaskService.java
  6. 88
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpPlanServiceImpl.java
  7. 50
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpReserveServiceImpl.java
  8. 53
      zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpTaskServiceImpl.java
  9. 81
      zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpTaskMapper.xml
  10. 7
      zdxt-web-client/zdxt-efcode-enforcement-app/src/api/enforcement/plan.js
  11. 18
      zdxt-web-client/zdxt-efcode-enforcement-app/src/router/enforcement-app/enf-main-router.js
  12. 4
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/enforcementMy.vue
  13. 63
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_edit.vue
  14. 14
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_mine.vue
  15. 10
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_other.vue
  16. 392
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/task/cp_task_detail.vue
  17. 578
      zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/task/cp_task_mine.vue

51
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/controller/app/enforcement/AppEnfCpTaskController.java

@ -1,10 +1,18 @@
package com.zdxt.enforcementcode.controller.app.enforcement;
import com.zdxt.enforcementcode.controller.web.supervision.EfcodeCpTaskController;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpTaskVo;
import com.zdxt.enforcementcode.service.IEfcodeCpTaskService;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
/**
* 行政执法检查计划-任务
*
@ -16,7 +24,50 @@ import org.springframework.web.bind.annotation.*;
@RequestMapping("/app-enf/cpTask")
public class AppEnfCpTaskController extends EfcodeCpTaskController {
@Autowired
private IEfcodeCpTaskService cpTaskService;
public AppEnfCpTaskController(IEfcodeCpTaskService efcodeCpTaskService) {
super(efcodeCpTaskService);
this.cpTaskService = efcodeCpTaskService;
}
/**
* 查询我的任务列表(指定日期)
*
* @param planDate 计划执行日期(可选,格式 yyyy-MM-dd)
* @return 任务列表
*/
@GetMapping("/myList")
public R<List<EfcodeCpTaskVo>> myList(
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date planDate) {
Long userId = LoginHelper.getUserId();
List<EfcodeCpTaskVo> list = cpTaskService.queryMyTaskList(userId, planDate);
return R.ok(list);
}
/**
* 查询指定月份中有任务的日期列表(用于日历打点)
*
* @param yearMonth 年月,格式如 "2026-03"
* @return 日期字符串列表
*/
@GetMapping("/myTaskDates")
public R<List<String>> myTaskDates(@RequestParam String yearMonth) {
Long userId = LoginHelper.getUserId();
List<String> dates = cpTaskService.queryMyTaskDates(userId, yearMonth);
return R.ok(dates);
}
/**
* 查询我的任务详情(通过 planId 查询任务+计划完整信息)
*
* @param planId 计划ID
* @return 任务详情
*/
@GetMapping("/myDetail")
public R<EfcodeCpTaskVo> myDetail(@RequestParam String planId) {
EfcodeCpTaskVo detail = cpTaskService.queryMyTaskDetail(planId);
return R.ok(detail);
}
}

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

@ -169,6 +169,13 @@ public class EfcodeCpPlanVo implements Serializable {
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long editEnable;
/**
* 是否可以删除:1=是,0=否(非数据库字段,后置计算)
* <p>规则:可编辑时可删除;不联合执法且执行时间未到时也可删除</p>
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long deleteEnable;
/**
* 执法单位ID
*/

92
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/domain/vo/EfcodeCpTaskVo.java

@ -46,5 +46,97 @@ public class EfcodeCpTaskVo implements Serializable {
@ExcelProperty(value = "执法单位ID")
private String bureauDeptId;
// ========== 以下为关联计划表字段,通过 SQL JOIN 填充,非数据库字段 ==========
/**
* 关联计划ID
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String planId;
/**
* 计划执行日期
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Date planDate;
/**
* 执行时段:1=上午,2=下午,3=其他
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long planTime;
/**
* 经过预约后最终的计划执行日期
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Date planActualDate;
/**
* 经过预约后最终的执行时段:1=上午,2=下午,3=其他
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long planActualTime;
/**
* 执法事项
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String enforceContent;
/**
* 被检查企业/机构名称
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String enterpriseName;
/**
* 是否联合执法:1=是,0=否
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long isUnion;
/**
* 执法单位名称
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String bureauDeptName;
/**
* 计划类别:1=日常计划,2=专项计划
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long planType;
/**
* 执法人员姓名(多个逗号分隔)
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String enforceUserNames;
/**
* 被检查企业/机构信用代码
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String enterpriseNumber;
/**
* 联合执法单位(多个逗号分隔)
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String unionBureauDeptNames;
/**
* 预约状态:0=草稿,1=待预约,2=预约中,3=预约完成
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private Long reserveStatus;
/**
* 备注
*/
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
private String remark;
}

30
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/mapper/EfcodeCpTaskMapper.java

@ -2,8 +2,12 @@ package com.zdxt.enforcementcode.mapper;
import com.zdxt.enforcementcode.domain.EfcodeCpTask;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpTaskVo;
import org.apache.ibatis.annotations.Param;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import java.util.Date;
import java.util.List;
/**
* 行政执法检查计划-任务Mapper接口
*
@ -12,4 +16,30 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
*/
public interface EfcodeCpTaskMapper extends BaseMapperPlus<EfcodeCpTask, EfcodeCpTaskVo> {
/**
* 查询我的任务列表(通过计划表关联,筛选当前用户参与的任务)
*
* @param userId 当前用户ID
* @param planDate 计划执行日期(可为null)
* @return 任务列表
*/
List<EfcodeCpTaskVo> selectMyTaskList(@Param("userId") Long userId, @Param("planDate") Date planDate);
/**
* 查询指定月份中有任务的日期列表(用于日历打点)
*
* @param userId 当前用户ID
* @param yearMonth 年月,格式如 "2026-03"
* @return 日期字符串列表,格式如 ["2026-03-02", "2026-03-10"]
*/
List<String> selectMyTaskDates(@Param("userId") Long userId, @Param("yearMonth") String yearMonth);
/**
* 查询我的任务详情(通过 planId 查询任务+计划完整信息)
*
* @param planId 计划ID
* @return 任务详情
*/
EfcodeCpTaskVo selectMyTaskDetail(@Param("planId") String planId);
}

37
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/IEfcodeCpTaskService.java

@ -5,7 +5,10 @@ import com.zdxt.enforcementcode.domain.bo.EfcodeCpTaskBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.zdxt.enforcementcode.domain.EfcodeCpPlan;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
@ -65,4 +68,38 @@ public interface IEfcodeCpTaskService {
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid);
/**
* 查询我的任务列表(通过计划表关联,筛选当前用户参与的任务)
*
* @param userId 当前用户ID
* @param planDate 计划执行日期(可为null)
* @return 任务列表
*/
List<EfcodeCpTaskVo> queryMyTaskList(Long userId, Date planDate);
/**
* 查询指定月份中有任务的日期列表(用于日历打点)
*
* @param userId 当前用户ID
* @param yearMonth 年月,格式如 "2026-03"
* @return 日期字符串列表
*/
List<String> queryMyTaskDates(Long userId, String yearMonth);
/**
* 查询我的任务详情(通过 planId 查询任务+计划完整信息)
*
* @param planId 计划ID
* @return 任务详情
*/
EfcodeCpTaskVo queryMyTaskDetail(String planId);
/**
* 强制创建任务(忽略其他条件),根据计划信息直接生成任务记录并回写 plan.task_id
*
* @param plan 来源计划
* @return 新建任务ID
*/
String forceGenerateTask(EfcodeCpPlan plan);
}

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

@ -21,6 +21,7 @@ import com.zdxt.enforcementcode.service.IEfcodeCpPlanService;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -37,6 +38,7 @@ import com.zdxt.enforcementcode.domain.EfcodeCpReserve;
import com.zdxt.enforcementcode.domain.EfcodeCpSetting;
import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpSettingMapper;
import com.zdxt.enforcementcode.service.IEfcodeCpTaskService;
/**
* 行政执法检查计划-计划Service业务层处理
@ -53,6 +55,7 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
private final EfcodeCpPlanUsersMapper efcodeCpPlanUsersMapper;
private final EfcodeCpReserveMapper efcodeCpReserveMapper;
private final EfcodeCpSettingMapper efcodeCpSettingMapper;
private final IEfcodeCpTaskService cpTaskService;
/**
* 查询行政执法检查计划-计划
@ -130,8 +133,9 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
}
/**
* 后置计算 editEnable:判断记录是否可编辑
* <p>规则:预约状态为草稿(0)或待预约(1)时可编辑,其他状态不可编辑</p>
* 后置计算 editEnable / deleteEnable:判断记录是否可编辑、可删除
* <p>editEnable 规则:预约状态为草稿(0)或待预约(1)时可编辑,其他状态不可编辑</p>
* <p>deleteEnable 规则:可编辑时可删除;或者不联合执法(isUnion=0)且计划执行时间还未到时也可删除</p>
*
* @param records 查询结果列表
*/
@ -139,10 +143,22 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
if (records == null || records.isEmpty()) {
return;
}
LocalDate today = LocalDate.now();
for (EfcodeCpPlanVo vo : records) {
Long status = vo.getReserveStatus();
// 草稿(0)或待预约(1)可编辑,其他状态不可编辑
vo.setEditEnable(status != null && status <= 1L ? 1L : 0L);
boolean canEdit = status != null && status <= 1L;
vo.setEditEnable(canEdit ? 1L : 0L);
// 删除条件:可编辑 OR (不联合执法 AND 执行时间未到)
boolean canDelete = canEdit;
if (!canDelete && vo.getIsUnion() != null && vo.getIsUnion() == 0L && vo.getPlanDate() != null) {
LocalDate planLocalDate = vo.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
if (planLocalDate.isAfter(today)) {
canDelete = true;
}
}
vo.setDeleteEnable(canDelete ? 1L : 0L);
}
}
@ -234,7 +250,27 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
* @param entity 已持久化的实体
*/
private void afterInsert(EfcodeCpPlanBo bo, EfcodeCpPlan entity) {
// TODO 后续扩展:如发送通知、记录日志等
// 条件1:不联合的计划,直接生成任务
if (entity.getIsUnion() != null && entity.getIsUnion() == 0L) {
log.info("[计划提交] 不联合计划,直接生成任务, planId={}", entity.getId());
cpTaskService.forceGenerateTask(entity);
return;
}
// 条件2:执行时间距今天 <= CP_SETTING_MANUAL_MATCH_DAYS,直接生成任务
if (entity.getPlanDate() != null) {
String manualMatchDaysStr = getSettingValue("CP_SETTING_MANUAL_MATCH_DAYS");
int manualMatchDays = 3; // 默认值
if (StringUtils.isNotBlank(manualMatchDaysStr)) {
try { manualMatchDays = Integer.parseInt(manualMatchDaysStr.trim()); } catch (NumberFormatException ignored) {}
}
LocalDate planLocalDate = entity.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
long daysUntilPlan = ChronoUnit.DAYS.between(LocalDate.now(), planLocalDate);
if (daysUntilPlan <= manualMatchDays) {
log.info("[计划提交] 执行时间距今{}<={}(CP_SETTING_MANUAL_MATCH_DAYS),直接生成任务, planId={}",
daysUntilPlan, manualMatchDays, entity.getId());
cpTaskService.forceGenerateTask(entity);
}
}
}
/**
@ -345,15 +381,45 @@ public class EfcodeCpPlanServiceImpl implements IEfcodeCpPlanService {
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
if (ids == null || ids.isEmpty()) {
return false;
}
// 先级联删除计划参与人员表关联记录
if (ids != null && !ids.isEmpty()) {
efcodeCpPlanUsersMapper.delete(
Wrappers.<EfcodeCpPlanUsers>lambdaQuery().in(EfcodeCpPlanUsers::getPlanId, ids)
);
// 查询待删除的计划记录
List<EfcodeCpPlan> plans = baseMapper.selectByIds(ids);
if (isValid) {
LocalDate today = LocalDate.now();
for (EfcodeCpPlan plan : plans) {
Long status = plan.getReserveStatus();
boolean canEdit = status != null && status <= 1L;
boolean canDelete = canEdit;
// 不联合执法且执行时间未到,也允许删除
if (!canDelete && plan.getIsUnion() != null && plan.getIsUnion() == 0L && plan.getPlanDate() != null) {
LocalDate planLocalDate = plan.getPlanDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
if (planLocalDate.isAfter(today)) {
canDelete = true;
}
}
if (!canDelete) {
throw new ServiceException("计划["
+ (plan.getEnterpriseName() != null ? plan.getEnterpriseName() : plan.getId())
+ "]当前状态不允许删除");
}
}
}
// 级联删除关联的任务记录
List<String> taskIds = plans.stream()
.map(EfcodeCpPlan::getTaskId)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
if (!taskIds.isEmpty()) {
log.info("[删除计划] 级联删除任务记录, taskIds={}", taskIds);
cpTaskService.deleteWithValidByIds(taskIds, false);
}
// 级联删除计划参与人员表关联记录
efcodeCpPlanUsersMapper.delete(
Wrappers.<EfcodeCpPlanUsers>lambdaQuery().in(EfcodeCpPlanUsers::getPlanId, ids)
);
return baseMapper.deleteByIds(ids) > 0;
}

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

@ -21,6 +21,7 @@ import com.zdxt.enforcementcode.mapper.EfcodeCpReserveMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper;
import com.zdxt.enforcementcode.service.IEfcodeCpReserveService;
import com.zdxt.enforcementcode.service.IEfcodeCpSettingService;
import com.zdxt.enforcementcode.service.IEfcodeCpTaskService;
import java.util.*;
import java.util.stream.Collectors;
@ -39,6 +40,7 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
private final EfcodeCpReserveMapper baseMapper;
private final EfcodeCpPlanMapper cpPlanMapper;
private final IEfcodeCpSettingService cpSettingService;
private final IEfcodeCpTaskService cpTaskService;
/**
* 查询行政执法检查计划-预约
@ -242,6 +244,28 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
// 插入1条记录
insertReserveRecord(myPlan.getId(), planGroupId, opMessage, targetPlan.getPlanDate(), targetPlan.getPlanTime(), 0L);
}
// ===== 插入预约记录后:更新两个计划的 reserve_status =====
// 1. 我的计划 → 2=预约中
EfcodeCpPlan myPlanUpdate = new EfcodeCpPlan();
myPlanUpdate.setId(myPlan.getId());
myPlanUpdate.setReserveStatus(2L);
cpPlanMapper.updateById(myPlanUpdate);
// 2. 对方计划 → 根据当前活跃预约数决定状态
// 活跃数 < maxUnionUnit → 5=待预约(还有位置可以加入)
// 活跃数 = maxUnionUnit → 6=待预约(人数已满)
long currentActiveCount = countActiveReserves(planGroupId);
long newTargetStatus = (currentActiveCount >= maxUnionUnit) ? 6L : 5L;
EfcodeCpPlan targetPlanUpdate = new EfcodeCpPlan();
targetPlanUpdate.setId(targetPlanId);
targetPlanUpdate.setReserveStatus(newTargetStatus);
cpPlanMapper.updateById(targetPlanUpdate);
log.info("[doReserve] 我的计划[{}]→reserve_status=2, 对方计划[{}]→reserve_status={}, 活跃数={}, 最大联合数={}",
myPlan.getId(), targetPlanId, newTargetStatus, currentActiveCount, maxUnionUnit);
return R.ok();
}
@ -356,7 +380,31 @@ public class EfcodeCpReserveServiceImpl implements IEfcodeCpReserveService {
* @param action 操作类型: "approve" 或 "reject"
*/
private void afterOperate(EfcodeCpReserve reserve, String action) {
// TODO: 可在此处扩展后置逻辑,如通知、状态同步等
log.info("预约记录[{}]执行{}操作后置处理, planGroupId={}", reserve.getId(), action, reserve.getPlanGroupId());
// 同意操作后:检查同一 planGroupId 已同意数量是否达到最大联合单位数
if ("approve".equals(action) && StringUtils.isNotBlank(reserve.getPlanGroupId())) {
int maxUnionUnit = cpSettingService.getMaxUnionUnit();
long approvedCount = baseMapper.selectCount(
Wrappers.<EfcodeCpReserve>lambdaQuery()
.eq(EfcodeCpReserve::getPlanGroupId, reserve.getPlanGroupId())
.eq(EfcodeCpReserve::getOpStatus, 1L) // 1=已同意
);
log.info("[预约后置] planGroupId={}, 已同意数量={}, 最大联合数={}", reserve.getPlanGroupId(), approvedCount, maxUnionUnit);
if (approvedCount >= maxUnionUnit) {
// 达标:为该分组所有计划生成任务
List<EfcodeCpReserve> groupReserves = baseMapper.selectList(
Wrappers.<EfcodeCpReserve>lambdaQuery()
.eq(EfcodeCpReserve::getPlanGroupId, reserve.getPlanGroupId())
.eq(EfcodeCpReserve::getOpStatus, 1L)
);
for (EfcodeCpReserve r : groupReserves) {
EfcodeCpPlan plan = cpPlanMapper.selectById(r.getPlanId());
if (plan != null && StringUtils.isBlank(plan.getTaskId())) {
cpTaskService.forceGenerateTask(plan);
}
}
log.info("[预约后置] planGroupId={} 联合数量达标,已为{}条计划生成任务", reserve.getPlanGroupId(), groupReserves.size());
}
}
}
}

53
zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/service/impl/EfcodeCpTaskServiceImpl.java

@ -13,9 +13,12 @@ import org.springframework.stereotype.Service;
import com.zdxt.enforcementcode.domain.bo.EfcodeCpTaskBo;
import com.zdxt.enforcementcode.domain.vo.EfcodeCpTaskVo;
import com.zdxt.enforcementcode.domain.EfcodeCpTask;
import com.zdxt.enforcementcode.domain.EfcodeCpPlan;
import com.zdxt.enforcementcode.mapper.EfcodeCpTaskMapper;
import com.zdxt.enforcementcode.mapper.EfcodeCpPlanMapper;
import com.zdxt.enforcementcode.service.IEfcodeCpTaskService;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Collection;
@ -32,6 +35,7 @@ import java.util.Collection;
public class EfcodeCpTaskServiceImpl implements IEfcodeCpTaskService {
private final EfcodeCpTaskMapper baseMapper;
private final EfcodeCpPlanMapper cpPlanMapper;
/**
* 查询行政执法检查计划-任务
@ -128,6 +132,55 @@ public class EfcodeCpTaskServiceImpl implements IEfcodeCpTaskService {
return true;
}
/**
* 查询我的任务列表
*/
@Override
public List<EfcodeCpTaskVo> queryMyTaskList(Long userId, Date planDate) {
return baseMapper.selectMyTaskList(userId, planDate);
}
/**
* 查询指定月份中有任务的日期列表
*/
@Override
public List<String> queryMyTaskDates(Long userId, String yearMonth) {
return baseMapper.selectMyTaskDates(userId, yearMonth);
}
/**
* 查询我的任务详情
*/
@Override
public EfcodeCpTaskVo queryMyTaskDetail(String planId) {
return baseMapper.selectMyTaskDetail(planId);
}
/**
* 强制创建任务(忽略其他条件),根据计划信息直接生成任务记录并回写 plan.task_id
*
* @param plan 来源计划
* @return 新建任务ID
*/
@Override
public String forceGenerateTask(EfcodeCpPlan plan) {
// 1. 创建任务记录
EfcodeCpTask task = new EfcodeCpTask();
task.setTaskName(plan.getEnterpriseName() + " - 执法检查任务");
task.setBureauDeptId(plan.getBureauDeptId());
baseMapper.insert(task);
log.info("[强制创建任务] 新建任务 taskId={}, planId={}, 企业={}", task.getId(), plan.getId(), plan.getEnterpriseName());
// 2. 回写计划的 task_id,同时将 reserve_status 设为 3(预约完成/形成任务)
EfcodeCpPlan planUpdate = new EfcodeCpPlan();
planUpdate.setId(plan.getId());
planUpdate.setTaskId(task.getId());
planUpdate.setReserveStatus(3L);
cpPlanMapper.updateById(planUpdate);
return task.getId();
}
/**
* 校验并批量删除行政执法检查计划-任务信息
*

81
zdxt-modules/zdxt-enforcement-code/src/main/resources/mapper/enforcementcode/EfcodeCpTaskMapper.xml

@ -3,4 +3,85 @@
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zdxt.enforcementcode.mapper.EfcodeCpTaskMapper">
<!-- 我的任务列表:通过 plan.task_id 关联任务表,筛选当前用户参与的任务 -->
<select id="selectMyTaskList" resultType="com.zdxt.enforcementcode.domain.vo.EfcodeCpTaskVo">
SELECT
t.id,
t.task_name,
t.bureau_dept_id,
p.id AS plan_id,
p.plan_date,
p.plan_time,
p.plan_actual_date,
p.plan_actual_time,
p.enforce_content,
p.enterprise_name,
p.is_union,
p.bureau_dept_name
FROM efcode_cp_task t
INNER JOIN efcode_cp_plan p ON p.task_id = t.id
<where>
p.is_delete = 0
AND p.reserve_status IN (1, 4, 5)
AND (
p.create_by = #{userId}
OR EXISTS (
SELECT 1 FROM efcode_cp_plan_users pu
WHERE pu.plan_id = p.id AND pu.user_id = #{userId}
)
)
<if test="planDate != null">
AND p.plan_date = #{planDate}
</if>
</where>
ORDER BY p.plan_time ASC, p.plan_date ASC
</select>
<!-- 查询指定月份中有任务的日期列表(用于日历打点) -->
<select id="selectMyTaskDates" resultType="java.lang.String">
SELECT DISTINCT DATE_FORMAT(p.plan_date, '%Y-%m-%d') AS task_date
FROM efcode_cp_task t
INNER JOIN efcode_cp_plan p ON p.task_id = t.id
<where>
p.is_delete = 0
AND (
p.create_by = #{userId}
OR EXISTS (
SELECT 1 FROM efcode_cp_plan_users pu
WHERE pu.plan_id = p.id AND pu.user_id = #{userId}
)
)
AND DATE_FORMAT(p.plan_date, '%Y-%m') = #{yearMonth}
</where>
ORDER BY task_date ASC
</select>
<!-- 我的任务详情:通过 planId 查询任务+计划完整信息 -->
<select id="selectMyTaskDetail" resultType="com.zdxt.enforcementcode.domain.vo.EfcodeCpTaskVo">
SELECT
t.id,
t.task_name,
t.bureau_dept_id,
p.id AS plan_id,
p.plan_type,
p.plan_date,
p.plan_time,
p.plan_actual_date,
p.plan_actual_time,
p.enforce_content,
p.enterprise_name,
p.enterprise_number,
p.enforce_user_names,
p.is_union,
p.union_bureau_dept_names,
p.bureau_dept_name,
p.reserve_status,
p.remark
FROM efcode_cp_plan p
INNER JOIN efcode_cp_task t ON t.id = p.task_id
WHERE p.id = #{planId}
AND p.is_delete = 0
</select>
</mapper>

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

@ -32,7 +32,7 @@ export const doReservePlan = (params, success, error) => _post(planBaseUrl + "/r
// region 计划-设置接口
const settingBaseUrl = "/api/app-enf/cpSetting";
const settingBaseUrl = "/app-enf/cpSetting";
// 下阶段计划提交最晚时间(每月几号前需提交下阶段计划<=)
export const getSubmitDeadline = (success, error) => _get(settingBaseUrl + "/submitDeadline", {}, success, error);
@ -40,11 +40,14 @@ export const getSubmitDeadline = (success, error) => _get(settingBaseUrl + "/sub
// 单位内部单次计划最多参与人数(>=3)
export const getMaxInnerUnitAcPersonNum = (success, error) => _get(settingBaseUrl + "/maxInnerUnitAcPersonNum", {}, success, error);
// 手工匹配距计划开始天数
export const getManualMatchDays = (success, error) => _get(settingBaseUrl + "/manualMatchDays", {}, success, error);
// endregion
// region 企业检索接口
const enterpriseSearchBaseUrl = "/api/enforcementcode/enterpriseSearch";
const enterpriseSearchBaseUrl = "/enforcementcode/enterpriseSearch";
// 按企业名称联合检索(>=3字符)
export const searchUnionByEnterpriseName = (enterpriseName, success, error) =>

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

@ -98,6 +98,24 @@ const routes = [
isKeepAlive: false,
},
},
// 执法端-我的任务列表
{
path: "/enforcement/task",
name: "cpTaskMine",
component: () => import("@/views/enforcement/my/task/cp_task_mine.vue"),
meta: {
isKeepAlive: false,
},
},
// 执法端-任务详情
{
path: "/enforcement/task/detail",
name: "cpTaskDetail",
component: () => import("@/views/enforcement/my/task/cp_task_detail.vue"),
meta: {
isKeepAlive: false,
},
},
// 执法端-预约处理列表
{
path: "/enforcement/reserve",

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

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

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

@ -228,8 +228,8 @@
<script setup name="cpPlanEdit">
import { useRouter, useRoute } from 'vue-router';
import { showToast, showSuccessToast } from 'vant';
import { addPlan, editPlan, getPlanDetail, getSubmitDeadline, getMaxInnerUnitAcPersonNum, searchUnionByEnterpriseName, searchBureauUnitUsers } from '@/api/enforcement/plan';
import { showToast, showSuccessToast, showConfirmDialog } from 'vant';
import { addPlan, editPlan, getPlanDetail, getSubmitDeadline, getManualMatchDays, getMaxInnerUnitAcPersonNum, searchUnionByEnterpriseName, searchBureauUnitUsers } from '@/api/enforcement/plan';
const router = useRouter();
const route = useRoute();
@ -243,6 +243,9 @@ const showDatePicker = ref(false);
// 下阶段计划提交最晚时间(每月几号前需提交下阶段计划)
const submitDeadline = ref(26);
// 手工匹配距计划开始天数
const manualMatchDays = ref(3);
// 计算日期范围:根据计划类别、提交截止日返回 min/max
const dateRange = computed(() => {
const today = new Date();
@ -355,6 +358,8 @@ onMounted(() => {
loadSubmitDeadline();
// 获取单位内部单次计划最多参与人数
loadMaxPersonNum();
// 获取手工匹配距计划开始天数
loadManualMatchDays();
if (isEdit.value) {
loadDetail(route.query.id);
} else {
@ -412,6 +417,18 @@ const loadMaxPersonNum = () => {
});
};
// 拉取手工匹配距计划开始天数设置
const loadManualMatchDays = () => {
getManualMatchDays((res) => {
if (res && res.code === 200 && res.data != null) {
const val = Number(res.data);
if (!isNaN(val) && val > 0) {
manualMatchDays.value = val;
}
}
});
};
// 拉取提交截止日设置
const loadSubmitDeadline = () => {
getSubmitDeadline((res) => {
@ -646,6 +663,48 @@ const validate = () => {
const onSubmit = () => {
if (!validate()) return;
// 检柡是否需要确认Dialog
const isUnion = form.value.isUnion;
const planDateStr = form.value.planDate;
// 条件1:不联合 -> 提示将直接生成任务
if (isUnion === 0 || isUnion === '0') {
showConfirmDialog({
title: '提示',
message: '不联合的计划将直接生成任务,且不能再改为联合,是否确认提交?',
confirmButtonText: '确认提交',
cancelButtonText: '取消',
}).then(() => {
doSubmit();
}).catch(() => {});
return;
}
// 条件2:执行时间 <= 手工匹配天数 -> 提示时间不足
if (planDateStr) {
const planDate = new Date(planDateStr);
const today = new Date();
today.setHours(0, 0, 0, 0);
const diffDays = Math.ceil((planDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays <= manualMatchDays.value) {
showConfirmDialog({
title: '提示',
message: `执行时间小于可预约的最短时间(${manualMatchDays.value}天),提交后将直接生成任务,是否确认提交?`,
confirmButtonText: '确认提交',
cancelButtonText: '取消',
}).then(() => {
doSubmit();
}).catch(() => {});
return;
}
}
// 无需确认,直接提交
doSubmit();
};
// 实际执行提交
const doSubmit = () => {
submitting.value = true;
// ===== 提交前兜底:补齐后端BO @NotBlank/@NotNull 强校验字段 =====

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

@ -42,7 +42,7 @@
:class="{ 'pending-card': item.reserveStatus === 1 }"
>
<div class="card-header">
<div class="card-type">{{ item.isUnion === 1 ? '联合执法' : '普通执法' }}</div>
<div class="card-type">{{ item.isUnion === 1 ? '联合执法' : '非联合执法' }}</div>
<div class="status blue">
{{ getStatusText(item.reserveStatus) }}
</div>
@ -65,7 +65,7 @@
<div class="info-row">
<div class="label dark">计划时间:</div>
<div class="value gray">
{{ item.planDate }} {{ getPlanTimeText(item.planTime) }}
{{ (item.planDate || '').substring(0, 10) }} {{ getPlanTimeText(item.planTime) }}
</div>
</div>
@ -74,8 +74,9 @@
<div class="value gray">{{ item.enterpriseName }}</div>
</div>
<div v-if="item.editEnable !== 0" class="card-actions">
<div v-if="item.editEnable !== 0 || item.deleteEnable !== 0" class="card-actions">
<div
v-if="item.editEnable !== 0"
class="action-item blue-action"
@click="onEditPlan(item)"
>
@ -83,6 +84,7 @@
<span>编辑</span>
</div>
<div
v-if="item.deleteEnable !== 0"
class="action-item blue-action"
@click="onDeletePlan(item)"
>
@ -443,8 +445,12 @@ const onDeletePlan = (item) => {
}
.card-type {
font-size: 17px;
font-size: 14px;
font-weight: 500;
color: #fff !important;
background: linear-gradient(135deg, #1890ff, #0b63d1);
padding: 6px 10px;
border-radius: 10px;
}
.status {

10
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/plan/cp_plan_other.vue

@ -21,7 +21,7 @@
class="plan-card normal-card pending-card"
>
<div class="card-header">
<div class="card-type">{{ item.isUnion === 1 ? '联合执法' : '普通执法' }}</div>
<div class="card-type">{{ item.isUnion === 1 ? '联合执法' : '非联合执法' }}</div>
<div class="status blue">待预约</div>
</div>
@ -42,7 +42,7 @@
<div class="info-row">
<div class="label dark">计划时间:</div>
<div class="value gray">
{{ item.planDate }} {{ getPlanTimeText(item.planTime) }}
{{ (item.planDate || '').substring(0, 10) }} {{ getPlanTimeText(item.planTime) }}
</div>
</div>
@ -437,8 +437,12 @@ const onConfirmReserve = () => {
}
.card-type {
font-size: 17px;
font-size: 14px;
font-weight: 500;
color: #fff !important;
background: linear-gradient(135deg, #1890ff, #0b63d1);
padding: 6px 10px;
border-radius: 10px;
}
.status {

392
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/task/cp_task_detail.vue

@ -0,0 +1,392 @@
<template>
<div class="detail-page">
<!-- 顶部导航 -->
<div class="nav-bar">
<div class="nav-back" @click="onBack">
<van-icon name="arrow-left" size="20" color="#333" />
</div>
<div class="nav-title">任务详情</div>
<div class="nav-right"></div>
</div>
<!-- 加载中 -->
<div v-if="loading" class="detail-loading">
<van-loading size="24" color="#2382cf" />
</div>
<!-- 详情内容 -->
<div v-else-if="detail" class="detail-content">
<!-- 顶部标题区 -->
<div class="header-card">
<div class="header-top">
<span class="plan-type-tag">【{{ planTypeText }}】</span>
<span v-if="detail.isUnion === 1" class="union-badge">联合执法</span>
</div>
<span class="header-title">{{ detail.enforceContent }}</span>
</div>
<!-- 基本信息卡片 -->
<div class="detail-card">
<div class="card-header">
<div class="header-icon-wrap green">
<van-icon name="info-o" size="15" color="#fff" />
</div>
<span>执法计划</span>
</div>
<div class="info-list">
<div class="info-item">
<span class="info-label">计划日期</span>
<span class="info-value">{{ formatDate(detail.planDate) }}</span>
</div>
<div class="info-item">
<span class="info-label">执行时段</span>
<span class="info-value">{{ timeRangeText }}</span>
</div>
<div v-if="detail.planActualDate && (formatDate(detail.planActualDate) !== formatDate(detail.planDate) || detail.planActualTime !== detail.planTime)" class="info-item highlight">
<span class="info-label">实际日期</span>
<span class="info-value">{{ formatDate(detail.planActualDate) }} {{ actualTimeRangeText }}</span>
</div>
</div>
</div>
<!-- 企业信息卡片 -->
<div class="detail-card">
<div class="card-header">
<div class="header-icon-wrap orange">
<van-icon name="wap-home-o" size="15" color="#fff" />
</div>
<span>检查对象</span>
</div>
<div class="info-list">
<div class="info-item">
<span class="info-label">企业名称</span>
<span class="info-value bold">{{ detail.enterpriseName || '-' }}</span>
</div>
<div v-if="detail.enterpriseNumber" class="info-item">
<span class="info-label">信用代码</span>
<span class="info-value bold">{{ detail.enterpriseNumber }}</span>
</div>
</div>
</div>
<!-- 执法人员卡片 -->
<div class="detail-card">
<div class="card-header">
<div class="header-icon-wrap purple">
<van-icon name="friends-o" size="15" color="#fff" />
</div>
<span>执法人员</span>
</div>
<div class="info-list">
<div class="info-item">
<span class="info-label">执法单位</span>
<span class="info-value">{{ detail.bureauDeptName || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">执法人员</span>
<span class="info-value">{{ detail.enforceUserNames || '-' }}</span>
</div>
<div v-if="detail.isUnion === 1 && detail.unionBureauDeptNames" class="info-item">
<span class="info-label">联合单位</span>
<span class="info-value">{{ detail.unionBureauDeptNames }}</span>
</div>
</div>
</div>
<!-- 备注卡片 -->
<div v-if="detail.remark" class="detail-card">
<div class="card-header">
<div class="header-icon-wrap gray">
<van-icon name="edit" size="15" color="#fff" />
</div>
<span>备注</span>
</div>
<div class="content-block">
{{ detail.remark }}
</div>
</div>
</div>
<!-- 空状态 -->
<div v-else class="detail-empty">
<van-empty description="未找到任务信息" />
</div>
</div>
</template>
<script setup name="cpTaskDetail">
import { useRouter, useRoute } from 'vue-router';
import { getMyTaskDetail } from '@/api/enforcement/task';
const router = useRouter();
const route = useRoute();
const loading = ref(true);
const detail = ref(null);
// 返回
const onBack = () => {
router.back();
};
// 格式化日期
const formatDate = (d) => {
if (!d) return '-';
if (typeof d === 'string') return d.substring(0, 10);
const date = new Date(d);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
};
// 时段文字
const getTimeText = (time) => {
if (time === 1) return '上午 (09:00-12:00)';
if (time === 2) return '下午 (14:00-18:00)';
return '其他';
};
const timeRangeText = computed(() => getTimeText(detail.value?.planTime));
const actualTimeRangeText = computed(() => getTimeText(detail.value?.planActualTime));
// 计划类型
const planTypeText = computed(() => {
const t = detail.value?.planType;
if (t === 1) return '日常计划';
if (t === 2) return '专项计划';
return '其他';
});
const statusClass = computed(() => {
const s = detail.value?.reserveStatus;
if (s === 3) return 'status-done';
if (s === 2) return 'status-progress';
if (s === 1) return 'status-pending';
return 'status-draft';
});
// 加载详情
const loadDetail = () => {
const planId = route.query.planId;
if (!planId) {
loading.value = false;
return;
}
loading.value = true;
getMyTaskDetail(planId, (res) => {
loading.value = false;
if (res.code === 200 && res.data) {
detail.value = res.data;
}
}, () => {
loading.value = false;
});
};
onMounted(() => {
loadDetail();
});
</script>
<style lang="less" scoped>
.detail-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f4f7fc;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: linear-gradient(135deg, #2382cf, #1a6db5);
flex-shrink: 0;
.nav-back {
width: 32px;
cursor: pointer;
:deep(.van-icon) {
color: #fff !important;
}
}
.nav-title {
font-size: 17px;
font-weight: 600;
color: #fff;
}
.nav-right {
width: 32px;
}
}
.detail-loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.detail-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* 详情内容 */
.detail-content {
flex: 1;
overflow-y: auto;
padding: 0 14px 24px;
}
/* 顶部标题区 */
.header-card {
background: linear-gradient(135deg, #2382cf, #1a6db5);
border-radius: 16px 16px 16px 16px;
padding: 20px 18px 22px;
margin: 8px 0px;
color: #fff;
.header-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.plan-type-tag {
font-size: 14px;
padding: 6px 10px;
border-radius: 10px;
background: rgba(255,255,255,0.2);
color: #fff;
font-weight: 500;
}
.union-badge {
font-size: 11px;
padding: 2px 10px;
border-radius: 10px;
background: linear-gradient(135deg, #f5a623, #f7c948);
color: #fff;
font-weight: 600;
}
.header-title {
font-size: 18px;
font-weight: 700;
line-height: 1.4;
margin-bottom: 8px;
}
.header-meta {
display: flex;
align-items: center;
gap: 5px;
font-size: 13px;
color: rgba(255,255,255,0.85);
}
}
/* 卡片 */
.detail-card {
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(35, 130, 207, 0.06);
}
/* 卡片头部 */
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 14px;
.header-icon-wrap {
width: 26px;
height: 26px;
border-radius: 7px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.blue { background: linear-gradient(135deg, #2382cf, #5ba8e6); }
&.green { background: linear-gradient(135deg, #36b37e, #57d9a3); }
&.orange { background: linear-gradient(135deg, #f5a623, #f7c948); }
&.purple { background: linear-gradient(135deg, #7b68ee, #a78bfa); }
&.gray { background: linear-gradient(135deg, #8e99a4, #b0bac5); }
}
}
/* 信息列表 */
.info-list {
.info-item {
display: flex;
padding: 8px 0;
border-bottom: 1px dashed #f2f3f5;
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
&:first-child {
padding-top: 0;
}
&.highlight {
background: #fef9ed;
margin: 4px -8px;
padding: 8px;
border-radius: 6px;
border-bottom: none;
}
.info-label {
flex-shrink: 0;
width: 68px;
font-size: 13px;
color: #8c8c8c;
line-height: 1.6;
}
.info-value {
flex: 1;
font-size: 13px;
color: #333;
word-break: break-all;
line-height: 1.6;
&.bold {
font-weight: 600;
color: #1a1a2e;
}
&.mono {
font-family: 'SF Mono', 'Menlo', monospace;
font-size: 12px;
color: #666;
letter-spacing: 0.3px;
}
}
}
}
/* 内容块 */
.content-block {
font-size: 13px;
color: #444;
line-height: 1.8;
word-break: break-all;
white-space: pre-wrap;
background: #f9fafb;
padding: 12px;
border-radius: 8px;
border-left: 3px solid #2382cf;
}
</style>

578
zdxt-web-client/zdxt-efcode-enforcement-app/src/views/enforcement/my/task/cp_task_mine.vue

@ -0,0 +1,578 @@
<template>
<div class="task-page">
<!-- 顶部导航 -->
<div class="nav-bar">
<div class="nav-back" @click="onBack">
<van-icon name="arrow-left" size="20" color="#333" />
</div>
<div class="nav-title">我的任务</div>
<div class="nav-right"></div>
</div>
<!-- 月份切换 -->
<div class="month-switch">
<div class="month-arrow" @click="onPrevMonth">
<van-icon name="arrow-left" size="16" color="#2382cf" />
</div>
<div class="month-text" @click="showMonthPicker = true">
{{ currentYear }}年{{ currentMonth }}月
</div>
<div class="month-arrow" @click="onNextMonth">
<van-icon name="arrow" size="16" color="#2382cf" />
</div>
</div>
<!-- 日历 -->
<div class="calendar">
<div class="week-header">
<div class="week-cell" v-for="w in weekDays" :key="w">{{ w }}</div>
</div>
<div class="date-grid">
<div
v-for="(cell, idx) in calendarCells"
:key="idx"
class="date-cell"
:class="{
'other-month': !cell.isCurrentMonth,
'is-today': cell.isToday,
'is-selected': cell.isSelected,
}"
@click="onSelectDate(cell)"
>
<span class="date-num">{{ cell.day }}</span>
<span class="date-lunar">{{ cell.lunar }}</span>
<span v-if="cell.hasDot && cell.isCurrentMonth" class="date-dot"></span>
</div>
</div>
</div>
<!-- 任务数量 -->
<div class="task-count">
今天共 <span class="count-num">{{ taskList.length }}</span> 个任务
</div>
<!-- 任务列表 -->
<div class="task-list-wrap">
<div v-if="loading" class="task-loading">
<van-loading size="24" color="#2382cf" />
</div>
<div v-else-if="taskList.length === 0" class="task-empty">
<van-empty description="当天暂无任务" />
</div>
<div v-else class="task-list">
<div
v-for="item in taskList"
:key="item.id"
class="task-card"
@click="onTaskClick(item)"
>
<div class="task-time-col">
<div class="time-start">{{ getTimeStart(item) }}</div>
<div class="time-end">{{ getTimeEnd(item) }}</div>
</div>
<div class="task-info-col">
<div class="task-title-row">
<span class="task-title">{{ item.enforceContent }}</span>
<span v-if="item.isUnion === 1" class="union-badge">联</span>
</div>
<div class="task-detail">
<van-icon name="wap-home-o" size="14" />
<span>{{ item.enterpriseName }}</span>
</div>
<div class="task-detail">
<van-icon name="clock-o" size="14" color="#999" />
<span>{{ formatPlanDate(item) }} {{ getTimeRange(item) }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 月份选择弹窗 -->
<van-popup v-model:show="showMonthPicker" position="bottom" round>
<van-date-picker
v-model="monthPickerValue"
title="选择月份"
:columns-type="['year', 'month']"
@confirm="onMonthPickerConfirm"
@cancel="showMonthPicker = false"
/>
</van-popup>
</div>
</template>
<script setup name="cpTaskMine">
import { useRouter } from 'vue-router';
import { getMyTaskList, getMyTaskDates } from '@/api/enforcement/task';
const router = useRouter();
// ========== 日历相关 ==========
const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
const today = new Date();
const currentYear = ref(today.getFullYear());
const currentMonth = ref(today.getMonth() + 1);
const selectedDate = ref(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
const taskDatesSet = ref(new Set());
// 月份选择器
const showMonthPicker = ref(false);
const monthPickerValue = ref([
String(today.getFullYear()),
String(today.getMonth() + 1).padStart(2, '0'),
]);
// 简易农历(仅用于展示,不做精确计算)
const lunarDays = [
'', '初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十',
'十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十',
'廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十',
];
const getLunarDay = (date) => {
// 简化处理:使用日期的日数模拟农历显示
const d = date.getDate();
return lunarDays[d] || '';
};
// 生成日历单元格
const calendarCells = computed(() => {
const year = currentYear.value;
const month = currentMonth.value;
const firstDay = new Date(year, month - 1, 1);
const lastDay = new Date(year, month, 0);
const startWeekDay = firstDay.getDay(); // 0=Sunday
const totalDays = lastDay.getDate();
const cells = [];
// 上个月补位
const prevMonthLastDay = new Date(year, month - 1, 0).getDate();
for (let i = startWeekDay - 1; i >= 0; i--) {
const day = prevMonthLastDay - i;
const d = new Date(year, month - 2, day);
cells.push({
day,
date: d,
lunar: getLunarDay(d),
isCurrentMonth: false,
isToday: false,
isSelected: false,
hasDot: false,
});
}
// 当月
for (let day = 1; day <= totalDays; day++) {
const d = new Date(year, month - 1, day);
const dateStr = formatDateStr(d);
const isToday = dateStr === formatDateStr(today);
const isSelected = dateStr === formatDateStr(selectedDate.value);
cells.push({
day,
date: d,
lunar: getLunarDay(d),
isCurrentMonth: true,
isToday,
isSelected,
hasDot: taskDatesSet.value.has(dateStr),
});
}
// 下个月补位(补满6行)
const remaining = 42 - cells.length;
for (let day = 1; day <= remaining; day++) {
const d = new Date(year, month, day);
cells.push({
day,
date: d,
lunar: getLunarDay(d),
isCurrentMonth: false,
isToday: false,
isSelected: false,
hasDot: false,
});
}
return cells;
});
const formatDateStr = (d) => {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
};
const formatYearMonth = () => {
return `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}`;
};
// 切换月份
const onPrevMonth = () => {
if (currentMonth.value === 1) {
currentYear.value--;
currentMonth.value = 12;
} else {
currentMonth.value--;
}
loadTaskDates();
};
const onNextMonth = () => {
if (currentMonth.value === 12) {
currentYear.value++;
currentMonth.value = 1;
} else {
currentMonth.value++;
}
loadTaskDates();
};
// 月份选择器确认
const onMonthPickerConfirm = ({ selectedValues }) => {
currentYear.value = parseInt(selectedValues[0]);
currentMonth.value = parseInt(selectedValues[1]);
showMonthPicker.value = false;
loadTaskDates();
};
// 选择日期
const onSelectDate = (cell) => {
if (!cell.isCurrentMonth) return;
selectedDate.value = cell.date;
loadTaskList();
};
// 返回
const onBack = () => {
router.back();
};
// 点击任务卡片跳转详情
const onTaskClick = (item) => {
router.push({ path: '/enforcement/task/detail', query: { planId: item.planId } });
};
// ========== 任务列表 ==========
const taskList = ref([]);
const loading = ref(false);
// 时段转时间显示
const getTimeStart = (item) => {
const time = item.planActualTime || item.planTime;
if (time === 1) return '09:00';
if (time === 2) return '14:00';
return '00:00';
};
const getTimeEnd = (item) => {
const time = item.planActualTime || item.planTime;
if (time === 1) return '12:00';
if (time === 2) return '18:00';
return '23:59';
};
const getTimeRange = (item) => {
return `${getTimeStart(item)}-${getTimeEnd(item)}`;
};
const formatPlanDate = (item) => {
const d = item.planActualDate || item.planDate;
if (!d) return '';
if (typeof d === 'string') return d.substring(0, 10);
const date = new Date(d);
return formatDateStr(date);
};
// 加载任务日期(月份打点)
const loadTaskDates = () => {
const ym = formatYearMonth();
getMyTaskDates(ym, (res) => {
if (res.code === 200 && res.data) {
taskDatesSet.value = new Set(res.data);
}
});
// 如果选中日期不在当月,自动选中当月1号
if (selectedDate.value.getFullYear() !== currentYear.value
|| selectedDate.value.getMonth() + 1 !== currentMonth.value) {
selectedDate.value = new Date(currentYear.value, currentMonth.value - 1, 1);
}
loadTaskList();
};
// 加载任务列表
const loadTaskList = () => {
loading.value = true;
const dateStr = formatDateStr(selectedDate.value);
getMyTaskList(
{ planDate: dateStr },
(res) => {
loading.value = false;
if (res.code === 200 && res.data) {
taskList.value = res.data;
} else {
taskList.value = [];
}
},
() => {
loading.value = false;
taskList.value = [];
}
);
};
// 初始化
onMounted(() => {
loadTaskDates();
});
</script>
<style lang="less" scoped>
.task-page {
display: flex;
flex-direction: column;
height: 100%;
background: #f0f6ff;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
/* 导航栏 */
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fff;
flex-shrink: 0;
.nav-back {
width: 32px;
cursor: pointer;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #1a1a2e;
}
.nav-right {
width: 32px;
}
}
/* 月份切换 */
.month-switch {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
padding: 8px 0;
background: #fff;
flex-shrink: 0;
.month-arrow {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.month-text {
font-size: 15px;
font-weight: 600;
color: #2382cf;
cursor: pointer;
}
}
/* 日历 */
.calendar {
background: #fff;
padding: 0 8px 8px;
flex-shrink: 0;
}
.week-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
padding: 4px 0;
.week-cell {
font-size: 11px;
color: #999;
font-weight: 500;
}
}
.date-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px 0;
}
.date-cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3px 0;
min-height: 38px;
cursor: pointer;
position: relative;
border-radius: 6px;
transition: background 0.15s;
.date-num {
font-size: 13px;
font-weight: 600;
color: #333;
line-height: 1.3;
}
.date-lunar {
font-size: 9px;
color: #999;
line-height: 1.1;
}
.date-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: #2382cf;
margin-top: 1px;
}
&.other-month {
.date-num { color: #ccc; }
.date-lunar { color: #ddd; }
}
&.is-today:not(.is-selected) {
.date-num {
color: #2382cf;
font-weight: 700;
}
}
&.is-selected {
background: #2382cf;
.date-num { color: #fff; font-weight: 700; }
.date-lunar { color: rgba(255,255,255,0.8); }
.date-dot { background: #fff; }
}
}
/* 任务计数 */
.task-count {
padding: 12px 16px 8px;
font-size: 14px;
color: #666;
flex-shrink: 0;
.count-num {
font-size: 18px;
font-weight: 700;
color: #2382cf;
}
}
/* 任务列表 */
.task-list-wrap {
flex: 1;
overflow-y: auto;
padding: 0 12px 16px;
}
.task-loading {
display: flex;
justify-content: center;
padding: 40px 0;
}
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.task-card {
display: flex;
background: #fff;
border-radius: 14px;
padding: 16px;
box-shadow: 0 2px 10px rgba(44, 116, 190, 0.07);
gap: 14px;
}
.task-time-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 48px;
flex-shrink: 0;
.time-start, .time-end {
font-size: 15px;
font-weight: 700;
color: #333;
line-height: 1.6;
}
}
.task-info-col {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.task-title-row {
display: flex;
align-items: center;
gap: 6px;
}
.task-title {
font-size: 15px;
font-weight: 600;
color: #1a1a2e;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.union-badge {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 4px;
background: #2382cf;
color: #fff;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.task-detail {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #393838;
overflow: hidden;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
Loading…
Cancel
Save