src/main/java/com/zy/asrs/controller/WrkCyclePlanController.java
New file @@ -0,0 +1,178 @@ package com.zy.asrs.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.core.annotations.ManagerAuth; import com.core.common.R; import com.zy.asrs.domain.param.CreateCyclePlanParam; import com.zy.asrs.entity.BasCrnp; import com.zy.asrs.entity.BasDualCrnp; import com.zy.asrs.entity.WrkCyclePlan; import com.zy.asrs.entity.WrkCyclePlanLoc; import com.zy.asrs.service.BasCrnpService; import com.zy.asrs.service.BasDualCrnpService; import com.zy.asrs.service.WrkCyclePlanLocService; import com.zy.asrs.service.WrkCyclePlanService; import com.zy.common.web.BaseController; import com.zy.core.enums.SlaveType; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Slf4j @RestController public class WrkCyclePlanController extends BaseController { @Autowired private WrkCyclePlanService wrkCyclePlanService; @Autowired private WrkCyclePlanLocService wrkCyclePlanLocService; @Autowired private BasCrnpService basCrnpService; @Autowired private BasDualCrnpService basDualCrnpService; @RequestMapping(value = "/wrkCyclePlan/list/auth") @ManagerAuth public R list(@RequestParam(defaultValue = "1") Integer curr, @RequestParam(defaultValue = "10") Integer limit) { QueryWrapper<WrkCyclePlan> wrapper = new QueryWrapper<>(); wrapper.orderByDesc("id"); return R.ok(wrkCyclePlanService.page(new Page<>(curr, limit), wrapper)); } @RequestMapping(value = "/wrkCyclePlan/locList/auth") @ManagerAuth public R locList(@RequestParam Long planId, @RequestParam(defaultValue = "1") Integer curr, @RequestParam(defaultValue = "20") Integer limit) { QueryWrapper<WrkCyclePlanLoc> wrapper = new QueryWrapper<>(); wrapper.eq("plan_id", planId).orderByAsc("seq"); return R.ok(wrkCyclePlanLocService.page(new Page<>(curr, limit), wrapper)); } @PostMapping(value = "/wrkCyclePlan/create/auth") @ManagerAuth public R create(@RequestBody CreateCyclePlanParam param) { try { WrkCyclePlan plan = wrkCyclePlanService.createPlan(param); return R.ok(plan); } catch (Exception e) { return R.error(e.getMessage()); } } @PostMapping(value = "/wrkCyclePlan/start/auth") @ManagerAuth public R start(@RequestBody Map<String, Long> param) { try { wrkCyclePlanService.startPlan(param.get("planId")); return R.ok(); } catch (Exception e) { return R.error(e.getMessage()); } } @PostMapping(value = "/wrkCyclePlan/pause/auth") @ManagerAuth public R pause(@RequestBody Map<String, Long> param) { try { wrkCyclePlanService.pausePlan(param.get("planId")); return R.ok(); } catch (Exception e) { return R.error(e.getMessage()); } } @PostMapping(value = "/wrkCyclePlan/resume/auth") @ManagerAuth public R resume(@RequestBody Map<String, Long> param) { try { wrkCyclePlanService.resumePlan(param.get("planId")); return R.ok(); } catch (Exception e) { return R.error(e.getMessage()); } } @PostMapping(value = "/wrkCyclePlan/reset/auth") @ManagerAuth public R reset(@RequestBody Map<String, Long> param) { try { wrkCyclePlanService.resetPlan(param.get("planId")); return R.ok(); } catch (Exception e) { return R.error(e.getMessage()); } } @PostMapping(value = "/wrkCyclePlan/delete/auth") @ManagerAuth public R delete(@RequestBody Map<String, Long> param) { try { wrkCyclePlanService.deletePlan(param.get("planId")); return R.ok(); } catch (Exception e) { return R.error(e.getMessage()); } } @RequestMapping(value = "/wrkCyclePlan/craneOptions/auth") @ManagerAuth public R craneOptions() { List<Map<String, Object>> options = new ArrayList<>(); List<BasCrnp> crnps = basCrnpService.list(new QueryWrapper<>()); for (BasCrnp crnp : crnps) { Map<String, Object> opt = new HashMap<>(); opt.put("crnNo", crnp.getCrnNo()); opt.put("crnType", SlaveType.Crn.name()); opt.put("label", "堆垛机" + crnp.getCrnNo()); options.add(opt); } List<BasDualCrnp> dualCrnps = basDualCrnpService.list(new QueryWrapper<>()); for (BasDualCrnp dualCrnp : dualCrnps) { Map<String, Object> opt = new HashMap<>(); opt.put("crnNo", dualCrnp.getCrnNo()); opt.put("crnType", SlaveType.DualCrn.name()); opt.put("label", "双工位堆垛机" + dualCrnp.getCrnNo()); options.add(opt); } return R.ok(options); } @PostMapping(value = "/wrkCyclePlan/availableRows/auth") @ManagerAuth public R availableRows(@RequestBody List<CreateCyclePlanParam.CrnOption> crnOptions) { List<Integer> rows = new ArrayList<>(); for (CreateCyclePlanParam.CrnOption opt : crnOptions) { if (SlaveType.Crn.name().equals(opt.getCrnType())) { BasCrnp crnp = basCrnpService.getOne(new QueryWrapper<BasCrnp>().eq("crn_no", opt.getCrnNo())); if (crnp != null) { for (List<Integer> group : crnp.getControlRows$()) { rows.addAll(group); } } } else if (SlaveType.DualCrn.name().equals(opt.getCrnType())) { BasDualCrnp dualCrnp = basDualCrnpService.getOne(new QueryWrapper<BasDualCrnp>().eq("crn_no", opt.getCrnNo())); if (dualCrnp != null) { for (List<Integer> group : dualCrnp.getControlRows$()) { rows.addAll(group); } } } } Collections.sort(rows); return R.ok(rows.stream().distinct().collect(java.util.stream.Collectors.toList())); } } src/main/java/com/zy/asrs/domain/param/CreateCyclePlanParam.java
New file @@ -0,0 +1,52 @@ package com.zy.asrs.domain.param; import lombok.Data; import java.util.List; @Data public class CreateCyclePlanParam { /** * 堆垛机号列表 */ private List<CrnOption> crnOptions; /** * 选中的排号列表 */ private List<Integer> rows; /** * 列范围-起 */ private Integer bayFrom; /** * 列范围-止 */ private Integer bayTo; /** * 层范围-起 */ private Integer levFrom; /** * 层范围-止 */ private Integer levTo; @Data public static class CrnOption { /** * 堆垛机号 */ private Integer crnNo; /** * 堆垛机类型:Crn 或 DualCrn */ private String crnType; } } src/main/java/com/zy/asrs/entity/WrkCyclePlan.java
New file @@ -0,0 +1,62 @@ package com.zy.asrs.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.zy.core.enums.CyclePlanStatus; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("asr_wrk_cycle_plan") public class WrkCyclePlan implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "主键ID") @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty(value = "计划编号") private String planNo; @ApiModelProperty(value = "堆垛机列表JSON") private String crnList; @ApiModelProperty(value = "计划状态 0=新建 1=运行中 2=暂停 3=已完成 4=已取消") private Integer planSts; @ApiModelProperty(value = "总库位数") private Integer totalCount; @ApiModelProperty(value = "已完成数") private Integer completedCount; @ApiModelProperty(value = "备注") private String memo; @ApiModelProperty(value = "创建人") private Long appeUser; @ApiModelProperty(value = "创建时间") private Date appeTime; @ApiModelProperty(value = "修改人") private Long modiUser; @ApiModelProperty(value = "修改时间") private Date modiTime; @TableField(exist = false) private String planSts$; public String getPlanSts$() { CyclePlanStatus status = CyclePlanStatus.get(this.planSts); return status != null ? status.desc : String.valueOf(this.planSts); } } src/main/java/com/zy/asrs/entity/WrkCyclePlanLoc.java
New file @@ -0,0 +1,65 @@ package com.zy.asrs.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.zy.core.enums.CyclePlanLocStatus; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("asr_wrk_cycle_plan_loc") public class WrkCyclePlanLoc implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "主键ID") @TableId(value = "id", type = IdType.AUTO) private Long id; @ApiModelProperty(value = "计划ID") private Long planId; @ApiModelProperty(value = "源库位号") private String locNo; @ApiModelProperty(value = "目标库位号") private String destLocNo; @ApiModelProperty(value = "执行序号") private Integer seq; @ApiModelProperty(value = "堆垛机号") private Integer crnNo; @ApiModelProperty(value = "双工位堆垛机号") private Integer dualCrnNo; @ApiModelProperty(value = "库位计划状态 0=待执行 1=执行中 2=已完成 -1=跳过") private Integer locPlanSts; @ApiModelProperty(value = "工作号") private Integer wrkNo; @ApiModelProperty(value = "托盘码快照") private String barcode; @ApiModelProperty(value = "创建时间") private Date appeTime; @ApiModelProperty(value = "修改时间") private Date modiTime; @TableField(exist = false) private String locPlanSts$; public String getLocPlanSts$() { CyclePlanLocStatus status = CyclePlanLocStatus.get(this.locPlanSts); return status != null ? status.desc : String.valueOf(this.locPlanSts); } } src/main/java/com/zy/asrs/mapper/WrkCyclePlanLocMapper.java
New file @@ -0,0 +1,12 @@ package com.zy.asrs.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.zy.asrs.entity.WrkCyclePlanLoc; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface WrkCyclePlanLocMapper extends BaseMapper<WrkCyclePlanLoc> { } src/main/java/com/zy/asrs/mapper/WrkCyclePlanMapper.java
New file @@ -0,0 +1,12 @@ package com.zy.asrs.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.zy.asrs.entity.WrkCyclePlan; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface WrkCyclePlanMapper extends BaseMapper<WrkCyclePlan> { } src/main/java/com/zy/asrs/service/WrkCyclePlanLocService.java
New file @@ -0,0 +1,8 @@ package com.zy.asrs.service; import com.baomidou.mybatisplus.extension.service.IService; import com.zy.asrs.entity.WrkCyclePlanLoc; public interface WrkCyclePlanLocService extends IService<WrkCyclePlanLoc> { } src/main/java/com/zy/asrs/service/WrkCyclePlanService.java
New file @@ -0,0 +1,25 @@ package com.zy.asrs.service; import com.baomidou.mybatisplus.extension.service.IService; import com.zy.asrs.domain.param.CreateCyclePlanParam; import com.zy.asrs.entity.WrkCyclePlan; public interface WrkCyclePlanService extends IService<WrkCyclePlan> { WrkCyclePlan createPlan(CreateCyclePlanParam param); void startPlan(Long planId); void pausePlan(Long planId); void resumePlan(Long planId); void resetPlan(Long planId); void advancePlan(Long planId); void onWrkMastComplete(Integer wrkNo); void deletePlan(Long planId); } src/main/java/com/zy/asrs/service/impl/WrkCyclePlanLocServiceImpl.java
New file @@ -0,0 +1,12 @@ package com.zy.asrs.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.zy.asrs.entity.WrkCyclePlanLoc; import com.zy.asrs.mapper.WrkCyclePlanLocMapper; import com.zy.asrs.service.WrkCyclePlanLocService; import org.springframework.stereotype.Service; @Service public class WrkCyclePlanLocServiceImpl extends ServiceImpl<WrkCyclePlanLocMapper, WrkCyclePlanLoc> implements WrkCyclePlanLocService { } src/main/java/com/zy/asrs/service/impl/WrkCyclePlanServiceImpl.java
New file @@ -0,0 +1,487 @@ package com.zy.asrs.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.core.common.Cools; import com.core.exception.CoolException; import com.zy.asrs.domain.param.CreateCyclePlanParam; import com.zy.asrs.domain.param.CreateLocMoveTaskParam; import com.zy.asrs.entity.*; import com.zy.asrs.mapper.WrkCyclePlanMapper; import com.zy.asrs.service.*; import com.zy.common.service.CommonService; import com.zy.core.enums.CyclePlanLocStatus; import com.zy.core.enums.CyclePlanStatus; import com.zy.core.enums.LocStsType; import com.zy.core.enums.SlaveType; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; @Slf4j @Service public class WrkCyclePlanServiceImpl extends ServiceImpl<WrkCyclePlanMapper, WrkCyclePlan> implements WrkCyclePlanService { @Autowired private WrkCyclePlanLocService wrkCyclePlanLocService; @Autowired private BasCrnpService basCrnpService; @Autowired private BasDualCrnpService basDualCrnpService; @Autowired private LocMastService locMastService; @Autowired private CommonService commonService; @Autowired private WrkMastService wrkMastService; @Override @Transactional(rollbackFor = Exception.class) public WrkCyclePlan createPlan(CreateCyclePlanParam param) { if (param.getCrnOptions() == null || param.getCrnOptions().isEmpty()) { throw new CoolException("请选择堆垛机"); } if (param.getBayFrom() == null || param.getBayTo() == null) { throw new CoolException("请填写列范围"); } if (param.getLevFrom() == null || param.getLevTo() == null) { throw new CoolException("请填写层范围"); } if (param.getBayFrom() > param.getBayTo()) { throw new CoolException("列范围起始不能大于结束"); } if (param.getLevFrom() > param.getLevTo()) { throw new CoolException("层范围起始不能大于结束"); } // 检查是否有同堆垛机的运行中/暂停计划 for (CreateCyclePlanParam.CrnOption crnOption : param.getCrnOptions()) { checkCrnAvailable(crnOption.getCrnNo(), crnOption.getCrnType()); } // 按堆垛机逐个校验并生成移转计划 List<WrkCyclePlanLoc> planLocs = new ArrayList<>(); int seq = 1; Set<Integer> userSelectedRows = (param.getRows() != null && !param.getRows().isEmpty()) ? new HashSet<>(param.getRows()) : null; for (CreateCyclePlanParam.CrnOption crnOption : param.getCrnOptions()) { List<Integer> crnRows = getCrnControlRows(crnOption.getCrnNo(), crnOption.getCrnType()); // 过滤用户选择的排号 List<Integer> rows; if (userSelectedRows != null) { rows = new ArrayList<>(); for (Integer row : crnRows) { if (userSelectedRows.contains(row)) { rows.add(row); } } } else { rows = crnRows; } if (rows.isEmpty()) { continue; } // 查询该堆垛机范围内的库位 QueryWrapper<LocMast> locQuery = new QueryWrapper<>(); locQuery.in("row1", rows) .between("bay1", param.getBayFrom(), param.getBayTo()) .between("lev1", param.getLevFrom(), param.getLevTo()) .eq("status", 1); List<LocMast> locs = locMastService.list(locQuery); if (locs.isEmpty()) { continue; } // 校验:该堆垛机范围内应恰好有1个在库托盘 List<LocMast> fullLocs = new ArrayList<>(); List<LocMast> emptyLocs = new ArrayList<>(); int otherCount = 0; for (LocMast loc : locs) { if ("F".equals(loc.getLocSts())) { fullLocs.add(loc); } else if ("O".equals(loc.getLocSts())) { emptyLocs.add(loc); } else { otherCount++; } } String crnLabel = crnOption.getCrnType().equals(SlaveType.Crn.name()) ? "堆垛机" + crnOption.getCrnNo() : "双工位堆垛机" + crnOption.getCrnNo(); if (fullLocs.isEmpty()) { throw new CoolException(crnLabel + "范围内没有在库托盘"); } if (fullLocs.size() > 1) { throw new CoolException(crnLabel + "范围内应恰好有1个在库托盘,当前有" + fullLocs.size() + "个"); } if (emptyLocs.isEmpty()) { throw new CoolException(crnLabel + "范围内没有空库位,无法进行移转"); } if (otherCount > 0) { throw new CoolException(crnLabel + "范围内有" + otherCount + "个库位状态异常,请检查"); } // 生成移转计划:将托盘依次移到每个空库位 LocMast sourceLoc = fullLocs.get(0); emptyLocs.sort(Comparator.comparingInt(LocMast::getBay1).thenComparingInt(LocMast::getLev1)); String currentLocNo = sourceLoc.getLocNo(); String barcode = sourceLoc.getBarcode(); for (LocMast emptyLoc : emptyLocs) { WrkCyclePlanLoc planLoc = new WrkCyclePlanLoc(); planLoc.setLocNo(currentLocNo); planLoc.setDestLocNo(emptyLoc.getLocNo()); planLoc.setSeq(seq++); planLoc.setLocPlanSts(CyclePlanLocStatus.PENDING.id); planLoc.setBarcode(barcode); planLoc.setAppeTime(new Date()); if (SlaveType.Crn.name().equals(crnOption.getCrnType())) { planLoc.setCrnNo(crnOption.getCrnNo()); } else { planLoc.setDualCrnNo(crnOption.getCrnNo()); } planLocs.add(planLoc); currentLocNo = emptyLoc.getLocNo(); } } if (planLocs.isEmpty()) { throw new CoolException("所选范围内没有可移转的库位"); } // 创建计划 String planNo = "CYCLE_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); WrkCyclePlan plan = new WrkCyclePlan(); plan.setPlanNo(planNo); plan.setCrnList(JSON.toJSONString(param.getCrnOptions())); plan.setPlanSts(CyclePlanStatus.NEW.id); plan.setTotalCount(planLocs.size()); plan.setCompletedCount(0); plan.setAppeTime(new Date()); this.save(plan); // 保存明细 for (WrkCyclePlanLoc planLoc : planLocs) { planLoc.setPlanId(plan.getId()); } wrkCyclePlanLocService.saveBatch(planLocs); return plan; } @Override @Transactional(rollbackFor = Exception.class) public void startPlan(Long planId) { WrkCyclePlan plan = getById(planId); if (plan == null) { throw new CoolException("计划不存在"); } if (plan.getPlanSts() != CyclePlanStatus.NEW.id && plan.getPlanSts() != CyclePlanStatus.PAUSED.id) { throw new CoolException("当前状态不允许启动"); } plan.setPlanSts(CyclePlanStatus.RUNNING.id); plan.setModiTime(new Date()); updateById(plan); } @Override @Transactional(rollbackFor = Exception.class) public void pausePlan(Long planId) { WrkCyclePlan plan = getById(planId); if (plan == null) { throw new CoolException("计划不存在"); } if (plan.getPlanSts() != CyclePlanStatus.RUNNING.id) { throw new CoolException("当前状态不允许暂停"); } plan.setPlanSts(CyclePlanStatus.PAUSED.id); plan.setModiTime(new Date()); updateById(plan); } @Override public void resumePlan(Long planId) { startPlan(planId); } @Override @Transactional(rollbackFor = Exception.class) public void resetPlan(Long planId) { WrkCyclePlan plan = getById(planId); if (plan == null) { throw new CoolException("计划不存在"); } // 取消活跃的WrkMast任务 List<WrkCyclePlanLoc> activeLocs = wrkCyclePlanLocService.list( new QueryWrapper<WrkCyclePlanLoc>() .eq("plan_id", planId) .eq("loc_plan_sts", CyclePlanLocStatus.MOVING.id) ); for (WrkCyclePlanLoc loc : activeLocs) { if (loc.getWrkNo() != null) { try { cancelWrkMast(loc.getWrkNo()); } catch (Exception e) { log.error("跑库重置取消任务失败, wrkNo={}", loc.getWrkNo(), e); } } } // 重置所有明细(包括MOVING状态的) wrkCyclePlanLocService.update(null, new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<WrkCyclePlanLoc>() .eq("plan_id", planId) .set("loc_plan_sts", CyclePlanLocStatus.PENDING.id) .set("wrk_no", null) .set("modi_time", new Date())); // 重置计划 plan.setPlanSts(CyclePlanStatus.NEW.id); plan.setCompletedCount(0); plan.setModiTime(new Date()); updateById(plan); } @Override @Transactional(rollbackFor = Exception.class) public void advancePlan(Long planId) { WrkCyclePlan plan = getById(planId); if (plan == null || plan.getPlanSts() != CyclePlanStatus.RUNNING.id) { return; } // 按堆垛机分组推进 List<WrkCyclePlanLoc> pendingLocs = wrkCyclePlanLocService.list( new QueryWrapper<WrkCyclePlanLoc>() .eq("plan_id", planId) .eq("loc_plan_sts", CyclePlanLocStatus.PENDING.id) .orderByAsc("seq") ); if (pendingLocs.isEmpty()) { // 没有待执行的,检查是否全部完成 long movingCount = wrkCyclePlanLocService.count( new QueryWrapper<WrkCyclePlanLoc>() .eq("plan_id", planId) .eq("loc_plan_sts", CyclePlanLocStatus.MOVING.id) ); if (movingCount == 0) { plan.setPlanSts(CyclePlanStatus.COMPLETED.id); plan.setModiTime(new Date()); updateById(plan); } return; } // 按堆垛机分组,每组取第一个待执行的 Map<String, List<WrkCyclePlanLoc>> groupedByCrn = pendingLocs.stream() .collect(Collectors.groupingBy(this::getCrnKey, LinkedHashMap::new, Collectors.toList())); for (Map.Entry<String, List<WrkCyclePlanLoc>> entry : groupedByCrn.entrySet()) { // 检查该堆垛机是否有正在执行的任务 boolean hasActive = hasActiveTaskForCrn(planId, entry.getKey()); if (hasActive) { continue; } // 提交第一个待执行的任务 WrkCyclePlanLoc planLoc = entry.getValue().get(0); submitLocMoveTask(plan, planLoc); } } @Override public void onWrkMastComplete(Integer wrkNo) { try { WrkCyclePlanLoc planLoc = wrkCyclePlanLocService.getOne( new QueryWrapper<WrkCyclePlanLoc>().eq("wrk_no", wrkNo) ); if (planLoc == null) { return; } planLoc.setLocPlanSts(CyclePlanLocStatus.DONE.id); planLoc.setModiTime(new Date()); wrkCyclePlanLocService.updateById(planLoc); // 原子递增 completedCount,避免并发丢失更新 WrkCyclePlan plan = getById(planLoc.getPlanId()); if (plan != null) { update(new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<WrkCyclePlan>() .eq("id", plan.getId()) .setSql("completed_count = completed_count + 1")); // 重新读取判断是否全部完成 plan = getById(plan.getId()); if (plan != null && plan.getCompletedCount() >= plan.getTotalCount()) { plan.setPlanSts(CyclePlanStatus.COMPLETED.id); plan.setModiTime(new Date()); updateById(plan); } } } catch (Exception e) { log.error("跑库计划onWrkMastComplete处理异常, wrkNo={}", wrkNo, e); } } private void submitLocMoveTask(WrkCyclePlan plan, WrkCyclePlanLoc planLoc) { try { // 检测托盘实际位置:在该堆垛机控制范围内查找在库库位 String actualLocNo = planLoc.getLocNo(); List<Integer> crnRows = getCrnControlRows( planLoc.getCrnNo() != null ? planLoc.getCrnNo() : planLoc.getDualCrnNo(), planLoc.getCrnNo() != null ? SlaveType.Crn.name() : SlaveType.DualCrn.name() ); LocMast actualLoc = locMastService.getOne( new QueryWrapper<LocMast>() .in("row1", crnRows) .eq("loc_sts", String.valueOf(LocStsType.F)) .eq("status", 1) .last("LIMIT 1") ); if (actualLoc != null) { actualLocNo = actualLoc.getLocNo(); } CreateLocMoveTaskParam param = new CreateLocMoveTaskParam(); param.setSourceLocNo(actualLocNo); param.setLocNo(planLoc.getDestLocNo()); WrkMast wrkMast = commonService.createLocMoveTaskReturnMast(param); // 记录实际移出库位 planLoc.setLocNo(actualLocNo); planLoc.setWrkNo(wrkMast.getWrkNo()); planLoc.setLocPlanSts(CyclePlanLocStatus.MOVING.id); planLoc.setModiTime(new Date()); wrkCyclePlanLocService.updateById(planLoc); } catch (Exception e) { log.error("跑库计划提交移库任务失败, planId={}, locNo={}", plan.getId(), planLoc.getLocNo(), e); } } private boolean hasActiveTaskForCrn(Long planId, String crnKey) { QueryWrapper<WrkCyclePlanLoc> query = new QueryWrapper<WrkCyclePlanLoc>() .eq("plan_id", planId) .eq("loc_plan_sts", CyclePlanLocStatus.MOVING.id); List<WrkCyclePlanLoc> movingLocs = wrkCyclePlanLocService.list(query); for (WrkCyclePlanLoc loc : movingLocs) { if (crnKey.equals(getCrnKey(loc))) { return true; } } return false; } private String getCrnKey(WrkCyclePlanLoc loc) { if (loc.getCrnNo() != null) { return "Crn_" + loc.getCrnNo(); } else if (loc.getDualCrnNo() != null) { return "DualCrn_" + loc.getDualCrnNo(); } return "unknown"; } private List<Integer> getCrnControlRows(Integer crnNo, String crnType) { if (SlaveType.Crn.name().equals(crnType)) { BasCrnp crnp = basCrnpService.getOne(new QueryWrapper<BasCrnp>().eq("crn_no", crnNo)); if (crnp == null) { throw new CoolException("堆垛机" + crnNo + "不存在"); } List<List<Integer>> rowGroups = crnp.getControlRows$(); List<Integer> rows = new ArrayList<>(); for (List<Integer> group : rowGroups) { rows.addAll(group); } return rows; } else if (SlaveType.DualCrn.name().equals(crnType)) { BasDualCrnp dualCrnp = basDualCrnpService.getOne(new QueryWrapper<BasDualCrnp>().eq("crn_no", crnNo)); if (dualCrnp == null) { throw new CoolException("双工位堆垛机" + crnNo + "不存在"); } List<List<Integer>> rowGroups = dualCrnp.getControlRows$(); List<Integer> rows = new ArrayList<>(); for (List<Integer> group : rowGroups) { rows.addAll(group); } return rows; } throw new CoolException("未知堆垛机类型:" + crnType); } private void checkCrnAvailable(Integer crnNo, String crnType) { // 检查是否有同堆垛机的运行中或暂停计划 List<WrkCyclePlan> existingPlans = list( new QueryWrapper<WrkCyclePlan>() .in("plan_sts", CyclePlanStatus.RUNNING.id, CyclePlanStatus.PAUSED.id) ); for (WrkCyclePlan existingPlan : existingPlans) { List<CreateCyclePlanParam.CrnOption> crnOptions = JSON.parseArray(existingPlan.getCrnList(), CreateCyclePlanParam.CrnOption.class); if (crnOptions != null) { for (CreateCyclePlanParam.CrnOption opt : crnOptions) { if (crnNo.equals(opt.getCrnNo()) && crnType.equals(opt.getCrnType())) { throw new CoolException("堆垛机" + crnNo + "已有运行中或暂停的跑库计划(" + existingPlan.getPlanNo() + ")"); } } } } } private void cancelWrkMast(Integer wrkNo) { WrkMast wrkMast = wrkMastService.selectByWorkNo(wrkNo); if (wrkMast == null) { return; } wrkMastService.update(null, new com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper<WrkMast>() .eq("wrk_no", wrkNo) .set("mk", "taskForceCancel") .set("memo", "跑库计划重置") .set("modi_time", new Date())); } @Override @Transactional(rollbackFor = Exception.class) public void deletePlan(Long planId) { WrkCyclePlan plan = getById(planId); if (plan == null) { throw new CoolException("计划不存在"); } if (plan.getPlanSts() == CyclePlanStatus.RUNNING.id) { throw new CoolException("运行中的计划不能删除,请先暂停或重置"); } // 取消活跃任务 List<WrkCyclePlanLoc> activeLocs = wrkCyclePlanLocService.list( new QueryWrapper<WrkCyclePlanLoc>() .eq("plan_id", planId) .eq("loc_plan_sts", CyclePlanLocStatus.MOVING.id) ); for (WrkCyclePlanLoc loc : activeLocs) { if (loc.getWrkNo() != null) { try { cancelWrkMast(loc.getWrkNo()); } catch (Exception e) { log.error("跑库删除取消任务失败, wrkNo={}", loc.getWrkNo(), e); } } } // 删除明细 wrkCyclePlanLocService.remove(new QueryWrapper<WrkCyclePlanLoc>().eq("plan_id", planId)); // 删除计划 removeById(planId); } } src/main/java/com/zy/asrs/task/CyclePlanScheduler.java
New file @@ -0,0 +1,46 @@ package com.zy.asrs.task; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.zy.asrs.entity.WrkCyclePlan; import com.zy.asrs.service.WrkCyclePlanService; import com.zy.asrs.mapper.WrkCyclePlanMapper; import com.zy.core.enums.CyclePlanStatus; import com.zy.core.task.MainProcessTaskSubmitter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; @Component @Slf4j public class CyclePlanScheduler { private static final String LANE_PREFIX = "cycle-plan-"; @Autowired private WrkCyclePlanMapper wrkCyclePlanMapper; @Autowired private MainProcessTaskSubmitter mainProcessTaskSubmitter; @Autowired private WrkCyclePlanService wrkCyclePlanService; @Scheduled(cron = "0/1 * * * * ?") public void advanceRunningPlans() { List<WrkCyclePlan> plans = wrkCyclePlanMapper.selectList( new QueryWrapper<WrkCyclePlan>().eq("plan_sts", CyclePlanStatus.RUNNING.id) ); for (WrkCyclePlan plan : plans) { boolean submitted = mainProcessTaskSubmitter.submitKeyedSerialTask( LANE_PREFIX, plan.getId(), "cycle-advance-" + plan.getId(), 0, () -> wrkCyclePlanService.advancePlan(plan.getId()) ); if (!submitted) { log.error("CyclePlanScheduler提交失败, planId={}", plan.getId()); } } } } src/main/java/com/zy/asrs/task/WrkMastFinalizeProcessor.java
@@ -11,6 +11,7 @@ import com.zy.asrs.service.LocMastService; import com.zy.asrs.service.WrkAnalysisService; import com.zy.asrs.service.WrkMastLogService; import com.zy.asrs.service.WrkCyclePlanService; import com.zy.asrs.service.WrkMastService; import com.zy.asrs.utils.NotifyUtils; import com.zy.common.utils.RedisUtil; @@ -47,6 +48,7 @@ private final StationOperateProcessUtils stationOperateProcessUtils; private final BasStationService basStationService; private final RedisUtil redisUtil; private final WrkCyclePlanService wrkCyclePlanService; public WrkMastFinalizeProcessor(WrkMastService wrkMastService, WrkMastLogService wrkMastLogService, @@ -55,7 +57,8 @@ NotifyUtils notifyUtils, StationOperateProcessUtils stationOperateProcessUtils, BasStationService basStationService, RedisUtil redisUtil) { RedisUtil redisUtil, WrkCyclePlanService wrkCyclePlanService) { this.wrkMastService = wrkMastService; this.wrkMastLogService = wrkMastLogService; this.wrkAnalysisService = wrkAnalysisService; @@ -64,6 +67,7 @@ this.stationOperateProcessUtils = stationOperateProcessUtils; this.basStationService = basStationService; this.redisUtil = redisUtil; this.wrkCyclePlanService = wrkCyclePlanService; } @Transactional(rollbackFor = Exception.class) @@ -204,6 +208,8 @@ } notifyUtils.notify("task", 1, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.TASK_COMPLETE, JSON.toJSONString(wrkMast)); wrkCyclePlanService.onWrkMastComplete(wrkNo); }); } src/main/java/com/zy/common/service/CommonService.java
@@ -415,6 +415,86 @@ return true; } //移库任务(返回WrkMast,供跑库等场景获取工作号) public WrkMast createLocMoveTaskReturnMast(CreateLocMoveTaskParam param) { Date now = new Date(); LocMast sourceLocMast = locMastService.queryByLoc(param.getSourceLocNo()); if (null == sourceLocMast) { throw new CoolException(param.getSourceLocNo() + "源库位不存在"); } if (!sourceLocMast.getLocSts().equals("F")) { throw new CoolException(sourceLocMast.getLocNo() + "源库位不处于在库状态"); } LocMast locMast = locMastService.queryByLoc(param.getLocNo()); if (null == locMast) { throw new CoolException(param.getLocNo() + "目标库位不存在"); } if (!locMast.getLocSts().equals("O")) { throw new CoolException(locMast.getLocNo() + "目标库位不处于空库状态"); } double ioPri = 800D; if (param.getTaskPri() != null) { ioPri = param.getTaskPri().doubleValue(); } FindCrnNoResult sourceCrnResult = this.findCrnNoByLocNo(sourceLocMast.getLocNo()); if (sourceCrnResult == null) { throw new CoolException("未找到对应堆垛机"); } FindCrnNoResult targetCrnResult = this.findCrnNoByLocNo(locMast.getLocNo()); if (targetCrnResult == null) { throw new CoolException("未找到对应堆垛机"); } if (!sourceCrnResult.getCrnNo().equals(targetCrnResult.getCrnNo())) { throw new CoolException("源库位和目标库位不在同一巷道"); } int workNo = getWorkNo(WrkIoType.LOC_MOVE.id); WrkMast wrkMast = new WrkMast(); wrkMast.setWrkNo(workNo); wrkMast.setIoTime(now); wrkMast.setWrkSts(WrkStsType.NEW_LOC_MOVE.sts); wrkMast.setIoType(WrkIoType.LOC_MOVE.id); wrkMast.setIoPri(ioPri); wrkMast.setSourceLocNo(param.getSourceLocNo()); wrkMast.setLocNo(param.getLocNo()); wrkMast.setWmsWrkNo(param.getTaskNo()); wrkMast.setBarcode(sourceLocMast.getBarcode()); wrkMast.setAppeTime(now); wrkMast.setModiTime(now); if (targetCrnResult.getCrnType().equals(SlaveType.Crn)) { wrkMast.setCrnNo(targetCrnResult.getCrnNo()); } else if (targetCrnResult.getCrnType().equals(SlaveType.DualCrn)) { wrkMast.setDualCrnNo(targetCrnResult.getCrnNo()); } else { throw new CoolException("未知设备类型"); } boolean res = wrkMastService.save(wrkMast); if (!res) { News.error("移库任务 --- 保存工作档失败!"); throw new CoolException("保存工作档失败"); } wrkAnalysisService.initForTask(wrkMast); sourceLocMast.setLocSts("R"); sourceLocMast.setModiTime(new Date()); locMastService.updateById(sourceLocMast); locMast.setLocSts("S"); locMast.setModiTime(new Date()); locMastService.updateById(locMast); return wrkMast; } //入库任务 public WrkMast createInTask(CreateInTaskParam param) { Date now = new Date(); src/main/java/com/zy/core/enums/CyclePlanLocStatus.java
New file @@ -0,0 +1,33 @@ package com.zy.core.enums; import com.core.common.Cools; public enum CyclePlanLocStatus { PENDING(0, "待执行"), MOVING(1, "执行中"), DONE(2, "已完成"), SKIPPED(-1, "跳过"), ; CyclePlanLocStatus(int id, String desc) { this.id = id; this.desc = desc; } public int id; public String desc; public static CyclePlanLocStatus get(int id) { if (Cools.isEmpty(id)) { return null; } for (CyclePlanLocStatus value : CyclePlanLocStatus.values()) { if (value.id == id) { return value; } } return null; } } src/main/java/com/zy/core/enums/CyclePlanStatus.java
New file @@ -0,0 +1,46 @@ package com.zy.core.enums; import com.core.common.Cools; public enum CyclePlanStatus { NEW(0, "新建"), RUNNING(1, "运行中"), PAUSED(2, "暂停"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"), ; CyclePlanStatus(int id, String desc) { this.id = id; this.desc = desc; } public int id; public String desc; public static CyclePlanStatus get(int id) { if (Cools.isEmpty(id)) { return null; } for (CyclePlanStatus value : CyclePlanStatus.values()) { if (value.id == id) { return value; } } return null; } public static CyclePlanStatus get(String desc) { if (Cools.isEmpty(desc)) { return null; } for (CyclePlanStatus value : CyclePlanStatus.values()) { if (value.desc.equals(desc)) { return value; } } return null; } } src/main/resources/application.yml
@@ -1,6 +1,6 @@ # 系统版本信息 app: version: 3.0.1.8 version: 3.0.1.9 version-type: prd # prd 或 dev i18n: default-locale: zh-CN src/main/resources/sql/20260505_add_wrk_cycle_plan_menu.sql
New file @@ -0,0 +1,72 @@ -- 将 跑库管理 菜单挂载到:作业流程 -- 说明:执行本脚本后,请在"角色授权"里给对应角色勾选新菜单和"查看"权限。 SET @wrk_cycle_plan_parent_id := ( SELECT id FROM sys_resource WHERE code = 'workFlow' AND level = 1 ORDER BY id LIMIT 1 ); SET @wrk_cycle_plan_sort := COALESCE( ( SELECT MAX(COALESCE(sort, 0)) + 1 FROM sys_resource WHERE resource_id = @wrk_cycle_plan_parent_id AND level = 2 ), 1 ); INSERT INTO sys_resource(code, name, resource_id, level, sort, status) SELECT 'wrkCyclePlan/wrkCyclePlan.html', '跑库管理', @wrk_cycle_plan_parent_id, 2, @wrk_cycle_plan_sort, 1 FROM dual WHERE @wrk_cycle_plan_parent_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM sys_resource WHERE code = 'wrkCyclePlan/wrkCyclePlan.html' AND level = 2 ); UPDATE sys_resource SET name = '跑库管理', resource_id = @wrk_cycle_plan_parent_id, level = 2, sort = @wrk_cycle_plan_sort, status = 1 WHERE code = 'wrkCyclePlan/wrkCyclePlan.html' AND level = 2; SET @wrk_cycle_plan_id := ( SELECT id FROM sys_resource WHERE code = 'wrkCyclePlan/wrkCyclePlan.html' AND level = 2 ORDER BY id LIMIT 1 ); INSERT INTO sys_resource(code, name, resource_id, level, sort, status) SELECT 'wrkCyclePlan/wrkCyclePlan.html#view', '查看', @wrk_cycle_plan_id, 3, 1, 1 FROM dual WHERE @wrk_cycle_plan_id IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM sys_resource WHERE code = 'wrkCyclePlan/wrkCyclePlan.html#view' AND level = 3 ); UPDATE sys_resource SET name = '查看', resource_id = @wrk_cycle_plan_id, level = 3, sort = 1, status = 1 WHERE code = 'wrkCyclePlan/wrkCyclePlan.html#view' AND level = 3; SELECT id, code, name, resource_id, level, sort, status FROM sys_resource WHERE code IN ( 'wrkCyclePlan/wrkCyclePlan.html', 'wrkCyclePlan/wrkCyclePlan.html#view' ) ORDER BY level, sort, id; src/main/resources/sql/20260505_create_wrk_cycle_plan.sql
New file @@ -0,0 +1,33 @@ -- 跑库计划表 CREATE TABLE IF NOT EXISTS asr_wrk_cycle_plan ( id BIGINT AUTO_INCREMENT PRIMARY KEY, plan_no VARCHAR(50) NOT NULL COMMENT '计划编号', crn_list TEXT NOT NULL COMMENT '堆垛机列表JSON', plan_sts INT NOT NULL DEFAULT 0 COMMENT '0=新建 1=运行中 2=暂停 3=已完成 4=已取消', total_count INT NOT NULL DEFAULT 0 COMMENT '总库位数', completed_count INT NOT NULL DEFAULT 0 COMMENT '已完成数', memo VARCHAR(255) DEFAULT NULL, appe_user BIGINT DEFAULT NULL, appe_time DATETIME DEFAULT NULL, modi_user BIGINT DEFAULT NULL, modi_time DATETIME DEFAULT NULL, UNIQUE KEY uk_plan_no (plan_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跑库计划'; -- 跑库计划库位明细表 CREATE TABLE IF NOT EXISTS asr_wrk_cycle_plan_loc ( id BIGINT AUTO_INCREMENT PRIMARY KEY, plan_id BIGINT NOT NULL COMMENT '计划ID', loc_no VARCHAR(50) NOT NULL COMMENT '源库位号', dest_loc_no VARCHAR(50) NOT NULL COMMENT '目标库位号', seq INT NOT NULL COMMENT '执行序号', crn_no INT DEFAULT NULL COMMENT '堆垛机号', dual_crn_no INT DEFAULT NULL COMMENT '双工位堆垛机号', loc_plan_sts INT NOT NULL DEFAULT 0 COMMENT '0=待执行 1=执行中 2=已完成 -1=跳过', wrk_no INT DEFAULT NULL COMMENT '工作号', barcode VARCHAR(100) DEFAULT NULL COMMENT '托盘码快照', appe_time DATETIME DEFAULT NULL, modi_time DATETIME DEFAULT NULL, KEY idx_plan_id (plan_id), KEY idx_plan_seq (plan_id, seq) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跑库计划库位明细'; src/main/webapp/static/js/wrkCyclePlan/wrkCyclePlan.js
New file @@ -0,0 +1,259 @@ (function () { var token = localStorage.getItem("token") || ""; function ajax(method, url, data) { return $.ajax({ method: method, url: baseUrl + url, contentType: "application/json", headers: { token: token }, data: data ? JSON.stringify(data) : undefined }); } function parseCrnListJson(crnListStr) { try { return JSON.parse(crnListStr) || []; } catch (e) { return []; } } new Vue({ el: "#app", data: function () { return { creating: false, rowsLoading: false, craneOptions: [], availableRows: [], form: { crnOptions: [], rows: [], bayFrom: 1, bayTo: 10, levFrom: 1, levTo: 5 }, planList: [], planPage: { curr: 1, limit: 10, total: 0 }, selectedPlan: null, locList: [], locPage: { curr: 1, limit: 20, total: 0 }, refreshTimer: null }; }, mounted: function () { this.loadCraneOptions(); this.loadPlans(); }, beforeDestroy: function () { this.stopAutoRefresh(); }, methods: { loadCraneOptions: function () { var self = this; ajax("GET", "/wrkCyclePlan/craneOptions/auth").done(function (res) { if (res.code === 200) { self.craneOptions = res.data || []; } }); }, loadPlans: function () { var self = this; ajax("GET", "/wrkCyclePlan/list/auth?curr=" + self.planPage.curr + "&limit=" + self.planPage.limit).done(function (res) { if (res.code === 200 && res.data) { self.planList = res.data.records || []; self.planPage.total = res.data.total || 0; } }); }, loadLocs: function () { if (!this.selectedPlan) return; var self = this; ajax("GET", "/wrkCyclePlan/locList/auth?planId=" + self.selectedPlan.id + "&curr=" + self.locPage.curr + "&limit=" + self.locPage.limit).done(function (res) { if (res.code === 200 && res.data) { self.locList = res.data.records || []; self.locPage.total = res.data.total || 0; } }); }, handleCrnChange: function (val) { this.form.rows = []; this.availableRows = []; if (!val || !val.length) return; var self = this; var crnOpts = val.map(function (item) { var parts = item.split("|"); return { crnNo: parseInt(parts[0]), crnType: parts[1] }; }); self.rowsLoading = true; ajax("POST", "/wrkCyclePlan/availableRows/auth", crnOpts).done(function (res) { if (res.code === 200) { self.availableRows = res.data || []; } }).always(function () { self.rowsLoading = false; }); }, handleCreate: function () { if (!this.form.crnOptions.length) { this.$message.warning("请选择堆垛机"); return; } var crnOpts = this.form.crnOptions.map(function (item) { var parts = item.split("|"); return { crnNo: parseInt(parts[0]), crnType: parts[1] }; }); var param = { crnOptions: crnOpts, rows: this.form.rows.length ? this.form.rows : null, bayFrom: this.form.bayFrom, bayTo: this.form.bayTo, levFrom: this.form.levFrom, levTo: this.form.levTo }; var self = this; self.creating = true; ajax("POST", "/wrkCyclePlan/create/auth", param).done(function (res) { if (res.code === 200) { self.$message.success("创建成功"); self.planPage.curr = 1; self.loadPlans(); } else { self.$message.error(res.msg || "创建失败"); } }).always(function () { self.creating = false; }); }, handleStart: function (plan) { var self = this; ajax("POST", "/wrkCyclePlan/start/auth", { planId: plan.id }).done(function (res) { if (res.code === 200) { self.$message.success("已启动"); self.loadPlans(); self.startAutoRefresh(); } else { self.$message.error(res.msg || "操作失败"); } }); }, handlePause: function (plan) { var self = this; ajax("POST", "/wrkCyclePlan/pause/auth", { planId: plan.id }).done(function (res) { if (res.code === 200) { self.$message.success("已暂停"); self.loadPlans(); self.stopAutoRefresh(); } else { self.$message.error(res.msg || "操作失败"); } }); }, handleReset: function (plan) { var self = this; self.$confirm("确定重置该计划?已执行的任务将被取消。", "提示", { type: "warning" }).then(function () { ajax("POST", "/wrkCyclePlan/reset/auth", { planId: plan.id }).done(function (res) { if (res.code === 200) { self.$message.success("已重置"); self.loadPlans(); if (self.selectedPlan && self.selectedPlan.id === plan.id) { self.loadLocs(); } } else { self.$message.error(res.msg || "操作失败"); } }); }).catch(function () {}); }, handleDelete: function (plan) { var self = this; self.$confirm("确定删除该计划?此操作不可恢复。", "提示", { type: "warning" }).then(function () { ajax("POST", "/wrkCyclePlan/delete/auth", { planId: plan.id }).done(function (res) { if (res.code === 200) { self.$message.success("已删除"); if (self.selectedPlan && self.selectedPlan.id === plan.id) { self.selectedPlan = null; self.locList = []; } self.loadPlans(); } else { self.$message.error(res.msg || "删除失败"); } }); }).catch(function () {}); }, handlePlanClick: function (row) { this.selectedPlan = row; this.locPage.curr = 1; this.loadLocs(); }, handlePlanPageChange: function (page) { this.planPage.curr = page; this.loadPlans(); }, handleLocPageChange: function (page) { this.locPage.curr = page; this.loadLocs(); }, parseCrnList: function (crnListStr) { var list = parseCrnListJson(crnListStr); return list.map(function (item) { var label = item.crnType === "Crn" ? "堆垛机" + item.crnNo : "双工位" + item.crnNo; return { crnNo: item.crnNo, crnType: item.crnType, label: label }; }); }, planPercent: function (plan) { if (!plan.totalCount) return 0; return Math.round(plan.completedCount / plan.totalCount * 100); }, planStsType: function (sts) { var map = { 0: "info", 1: "success", 2: "warning", 3: "success", 4: "danger" }; return map[sts] != null ? map[sts] : "info"; }, locStsType: function (sts) { var map = { 0: "info", 1: "warning", 2: "success", "-1": "danger" }; return map[sts] || "info"; }, formatDate: function (val) { if (!val) return ""; var d = new Date(val); if (isNaN(d.getTime())) return val; var pad = function (n) { return n < 10 ? "0" + n : String(n); }; return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate()) + " " + pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()); }, startAutoRefresh: function () { this.stopAutoRefresh(); var self = this; self.refreshTimer = setInterval(function () { self.loadPlans(); if (self.selectedPlan) { self.loadLocs(); } }, 2000); }, stopAutoRefresh: function () { if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null; } } }, watch: { planList: function (list) { var hasRunning = list.some(function (p) { return p.planSts === 1; }); if (hasRunning) { this.startAutoRefresh(); } else { this.stopAutoRefresh(); } } } }); })(); src/main/webapp/views/wrkCyclePlan/wrkCyclePlan.html
New file @@ -0,0 +1,184 @@ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>跑库管理</title> <meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <link rel="stylesheet" href="../../static/vue/element/element.css"> <link rel="stylesheet" href="../../static/css/cool.css"> <style> :root { --card-bg: rgba(255, 255, 255, 0.94); --card-border: rgba(216, 226, 238, 0.95); --text-main: #243447; } [v-cloak] { display: none; } html, body { margin: 0; min-height: 100%; color: var(--text-main); font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; background: linear-gradient(180deg, #f2f6fb 0%, #f8fafc 100%); } .page-shell { max-width: 1700px; margin: 0 auto; padding: 14px; box-sizing: border-box; } .card-shell { border-radius: 24px; border: 1px solid var(--card-border); background: var(--card-bg); box-shadow: 0 16px 32px rgba(44, 67, 96, 0.08); overflow: hidden; margin-bottom: 14px; } .card-title { padding: 16px 20px 12px; font-size: 16px; font-weight: 700; border-bottom: 1px solid rgba(222, 230, 239, 0.92); } .card-body { padding: 16px 20px; } .form-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; } .form-label { font-size: 13px; color: #53677d; white-space: nowrap; } .table-wrap { padding: 10px 16px; } .table-shell { border-radius: 20px; overflow: hidden; border: 1px solid rgba(217, 227, 238, 0.98); background: rgba(255, 255, 255, 0.95); } .table-shell .el-table { border-radius: 20px; overflow: hidden; } .table-shell .el-table th { background: #f7fafc; color: #53677d; font-weight: 700; } .pager-bar { padding: 0 16px 16px; display: flex; justify-content: flex-end; } .action-btns { display: flex; gap: 6px; } .progress-cell { display: flex; align-items: center; gap: 4px; } .progress-text { font-size: 11px; color: #53677d; white-space: nowrap; flex-shrink: 0; } .progress-cell .el-progress { min-width: 0; } .plan-detail { padding: 0 16px 16px; } </style> </head> <body> <div id="app" class="page-shell" v-cloak> <!-- 创建计划 --> <section class="card-shell"> <div class="card-title">创建跑库计划</div> <div class="card-body"> <div class="form-row"> <span class="form-label">堆垛机:</span> <el-select v-model="form.crnOptions" multiple filterable size="small" placeholder="选择堆垛机" style="width: 360px;" @change="handleCrnChange"> <el-option v-for="item in craneOptions" :key="item.crnNo + '-' + item.crnType" :label="item.label" :value="item.crnNo + '|' + item.crnType"> </el-option> </el-select> <span class="form-label">排:</span> <el-select v-model="form.rows" multiple filterable size="small" placeholder="选择排号(不选则全部)" style="width: 260px;" :loading="rowsLoading"> <el-option v-for="r in availableRows" :key="r" :label="'第' + r + '排'" :value="r"></el-option> </el-select> <span class="form-label">列范围:</span> <el-input-number v-model="form.bayFrom" size="small" :min="1" controls-position="right" style="width: 100px;"></el-input-number> <span class="form-label">~</span> <el-input-number v-model="form.bayTo" size="small" :min="1" controls-position="right" style="width: 100px;"></el-input-number> <span class="form-label">层范围:</span> <el-input-number v-model="form.levFrom" size="small" :min="1" controls-position="right" style="width: 100px;"></el-input-number> <span class="form-label">~</span> <el-input-number v-model="form.levTo" size="small" :min="1" controls-position="right" style="width: 100px;"></el-input-number> <el-button size="small" type="primary" icon="el-icon-plus" :loading="creating" @click="handleCreate">创建计划</el-button> </div> </div> </section> <!-- 计划列表 --> <section class="card-shell"> <div class="card-title"> 跑库计划列表 <el-button size="mini" style="float:right;" icon="el-icon-refresh" @click="loadPlans">刷新</el-button> </div> <div class="table-wrap"> <div class="table-shell"> <el-table :data="planList" border stripe size="small" @row-click="handlePlanClick" row-key="id" :highlight-current-row="true"> <el-table-column prop="planNo" label="计划编号" min-width="200"></el-table-column> <el-table-column label="堆垛机" min-width="200"> <template slot-scope="scope"> <el-tag v-for="c in parseCrnList(scope.row.crnList)" :key="c.crnNo + c.crnType" size="mini" style="margin-right:4px;">{{ c.label || (c.crnType + c.crnNo) }}</el-tag> </template> </el-table-column> <el-table-column label="状态" width="100" align="center"> <template slot-scope="scope"> <el-tag :type="planStsType(scope.row.planSts)" size="mini">{{ scope.row['planSts$'] }}</el-tag> </template> </el-table-column> <el-table-column label="进度" width="180"> <template slot-scope="scope"> <div class="progress-cell"> <el-progress :percentage="planPercent(scope.row)" :stroke-width="8" :show-text="false" style="flex:1;"></el-progress> <span class="progress-text">{{ scope.row.completedCount }}/{{ scope.row.totalCount }} ({{ planPercent(scope.row) }}%)</span> </div> </template> </el-table-column> <el-table-column label="创建时间" width="160"> <template slot-scope="scope">{{ formatDate(scope.row.appeTime) }}</template> </el-table-column> <el-table-column label="操作" width="240" align="center"> <template slot-scope="scope"> <div class="action-btns"> <el-button v-if="scope.row.planSts === 0 || scope.row.planSts === 2" size="mini" type="success" @click.stop="handleStart(scope.row)">启动</el-button> <el-button v-if="scope.row.planSts === 1" size="mini" type="warning" @click.stop="handlePause(scope.row)">暂停</el-button> <el-button v-if="scope.row.planSts !== 3 && scope.row.planSts !== 4" size="mini" type="danger" @click.stop="handleReset(scope.row)">重置</el-button> <el-button v-if="scope.row.planSts !== 1" size="mini" type="danger" plain @click.stop="handleDelete(scope.row)">删除</el-button> </div> </template> </el-table-column> </el-table> </div> </div> <div class="pager-bar"> <el-pagination small background layout="total, prev, pager, next" :current-page="planPage.curr" :page-size="planPage.limit" :total="planPage.total" @current-change="handlePlanPageChange"> </el-pagination> </div> </section> <!-- 库位明细 --> <section v-if="selectedPlan" class="card-shell"> <div class="card-title"> 库位明细 - {{ selectedPlan.planNo }} <el-button size="mini" style="float:right;" icon="el-icon-refresh" @click="loadLocs">刷新</el-button> </div> <div class="plan-detail"> <div class="table-shell"> <el-table :data="locList" border stripe size="small"> <el-table-column prop="seq" label="序号" width="70" align="center"></el-table-column> <el-table-column prop="locNo" label="源库位" min-width="140"></el-table-column> <el-table-column prop="destLocNo" label="目标库位" min-width="140"></el-table-column> <el-table-column label="堆垛机" width="120" align="center"> <template slot-scope="scope"> <span v-if="scope.row.crnNo">Crn-{{ scope.row.crnNo }}</span> <span v-else-if="scope.row.dualCrnNo">Dual-{{ scope.row.dualCrnNo }}</span> </template> </el-table-column> <el-table-column prop="barcode" label="托盘码" min-width="150"></el-table-column> <el-table-column label="状态" width="100" align="center"> <template slot-scope="scope"> <el-tag :type="locStsType(scope.row.locPlanSts)" size="mini">{{ scope.row['locPlanSts$'] }}</el-tag> </template> </el-table-column> <el-table-column prop="wrkNo" label="任务号" width="100" align="center"></el-table-column> </el-table> </div> <div class="pager-bar"> <el-pagination small background layout="total, prev, pager, next" :current-page="locPage.curr" :page-size="locPage.limit" :total="locPage.total" @current-change="handleLocPageChange"> </el-pagination> </div> </div> </section> </div> </body> <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> <script type="text/javascript" src="../../static/js/common.js?v=20260309_i18n_fix1"></script> <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> <script type="text/javascript" src="../../static/vue/element/element.js"></script> <script type="text/javascript" src="../../static/js/wrkCyclePlan/wrkCyclePlan.js"></script> </html>