src/main/java/com/zy/asrs/controller/BasMapController.java
@@ -2,28 +2,18 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.core.common.DateUtils; import com.zy.asrs.entity.BasDevp; import com.zy.asrs.domain.BasMapEditorDoc; import com.zy.asrs.entity.BasMap; import com.zy.asrs.entity.BasStation; import com.zy.asrs.entity.DeviceConfig; import com.zy.asrs.service.BasDevpService; import com.zy.asrs.service.BasMapEditorService; import com.zy.asrs.service.BasMapService; import com.core.annotations.ManagerAuth; import com.core.common.BaseRes; import com.core.common.Cools; import com.core.common.R; import com.zy.asrs.service.BasStationService; import com.zy.asrs.service.DeviceConfigService; import com.zy.asrs.utils.MapExcelUtils; import com.zy.common.utils.RedisUtil; import com.zy.common.web.BaseController; import com.zy.core.enums.SlaveType; import com.zy.core.model.StationObjModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -38,15 +28,7 @@ @Autowired private BasMapService basMapService; @Autowired private BasDevpService basDevpService; @Autowired private DeviceConfigService deviceConfigService; @Autowired private RedisUtil redisUtil; @Autowired private MapExcelUtils mapExcelUtils; @Autowired private BasStationService basStationService; private BasMapEditorService basMapEditorService; @RequestMapping(value = "/basMap/{id}/auth") @ManagerAuth @@ -167,224 +149,66 @@ @PostMapping("/basMap/crn/upload") public R uploadExcel(@RequestParam("file") MultipartFile file) { File tempFile = null; try { // 保存上传的文件到临时位置 String filePath = System.getProperty("java.io.tmpdir") + file.getOriginalFilename(); file.transferTo(new File(filePath)); HashMap<Integer, List<List<HashMap<String, Object>>>> dataMap = mapExcelUtils.readExcel(filePath); HashMap<Integer, List<StationObjModel>> deviceStationMap = new HashMap<>(); HashMap<Integer, List<StationObjModel>> barcodeStationMap = new HashMap<>(); HashMap<Integer, List<StationObjModel>> inStationMap = new HashMap<>(); HashMap<Integer, List<StationObjModel>> outStationMap = new HashMap<>(); HashMap<Integer, List<StationObjModel>> runBlockReassignStationMap = new HashMap<>(); HashMap<Integer, List<StationObjModel>> isOutOrderStationMap = new HashMap<>(); HashMap<Integer, List<StationObjModel>> isLiftTransferStationMap = new HashMap<>(); for (Map.Entry<Integer, List<List<HashMap<String, Object>>>> entry : dataMap.entrySet()) { Integer lev = entry.getKey(); List<List<HashMap<String, Object>>> dataList = new ArrayList<>(); List<List<HashMap<String, Object>>> list = entry.getValue(); for (int i = 0; i < list.size(); i++) { List<HashMap<String, Object>> bayList = list.get(i); List<HashMap<String, Object>> arrayList = new ArrayList<>(); for (int j = 0; j < bayList.size(); j++) { HashMap<String, Object> map = bayList.get(j); HashMap<String, Object> nodeData = new HashMap<>(); nodeData.put("value", map.get("value")); String bgColor = map.get("bgColor").toString(); String nodeType = map.get("nodeType").toString(); if (nodeType.equals("shelf")) { //货架 nodeData.put("type", "shelf"); } else if (nodeType.equals("crn")) { //堆垛机 nodeData.put("type", "crn"); } else if (nodeType.equals("dualCrn")) { //双工位堆垛机 nodeData.put("type", "dualCrn"); } else if (nodeType.equals("devp")) { //输送线 nodeData.put("type", "devp"); JSONObject value = JSON.parseObject(String.valueOf(map.get("value"))); Integer deviceNo = value.getInteger("deviceNo"); StationObjModel stationObjModel = new StationObjModel(); stationObjModel.setDeviceNo(deviceNo); stationObjModel.setStationId(value.getInteger("stationId")); stationObjModel.setStationLev(lev); List<StationObjModel> stationList = deviceStationMap.getOrDefault(deviceNo, new ArrayList<>()); stationList.add(stationObjModel); deviceStationMap.put(deviceNo, stationList); Integer isBarcodeStation = value.getInteger("isBarcodeStation"); if (isBarcodeStation != null && isBarcodeStation == 1) { StationObjModel barcodeStationModel = new StationObjModel(); barcodeStationModel.setDeviceNo(deviceNo); barcodeStationModel.setStationId(value.getInteger("stationId")); barcodeStationModel.setBarcodeIdx(value.getInteger("barcodeIdx")); if (value.getInteger("backStation") != null) { StationObjModel backStation = new StationObjModel(); barcodeStationModel.setBackStation(backStation); backStation.setDeviceNo(value.getInteger("backStationDeviceNo")); backStation.setStationId(value.getInteger("backStation")); } List<StationObjModel> barcodeStationList = barcodeStationMap.getOrDefault(deviceNo, new ArrayList<>()); barcodeStationList.add(barcodeStationModel); barcodeStationMap.put(deviceNo, barcodeStationList); } Integer isInStation = value.getInteger("isInStation"); if (isInStation != null && isInStation == 1) { StationObjModel inStationModel = new StationObjModel(); StationObjModel barcodeStation = new StationObjModel(); inStationModel.setDeviceNo(deviceNo); inStationModel.setStationId(value.getInteger("stationId")); inStationModel.setBarcodeStation(barcodeStation); barcodeStation.setDeviceNo(value.getInteger("barcodeStationDeviceNo")); barcodeStation.setStationId(value.getInteger("barcodeStation")); List<StationObjModel> inStationList = inStationMap.getOrDefault(deviceNo, new ArrayList<>()); inStationList.add(inStationModel); inStationMap.put(deviceNo, inStationList); } Integer isOutStation = value.getInteger("isOutStation"); if (isOutStation != null && isOutStation == 1) { List<StationObjModel> outStationList = outStationMap.getOrDefault(deviceNo, new ArrayList<>()); outStationList.add(stationObjModel); outStationMap.put(deviceNo, outStationList); } Integer runBlockReassign = value.getInteger("runBlockReassign"); if (runBlockReassign != null && runBlockReassign == 1) { List<StationObjModel> runBlockReassignStationList = runBlockReassignStationMap.getOrDefault(deviceNo, new ArrayList<>()); runBlockReassignStationList.add(stationObjModel); runBlockReassignStationMap.put(deviceNo, runBlockReassignStationList); } Integer isOutOrder = value.getInteger("isOutOrder"); if (isOutOrder != null && isOutOrder == 1) { List<StationObjModel> isOutOrderStationList = isOutOrderStationMap.getOrDefault(deviceNo, new ArrayList<>()); isOutOrderStationList.add(stationObjModel); isOutOrderStationMap.put(deviceNo, isOutOrderStationList); } Integer isLiftTransfer = value.getInteger("isLiftTransfer"); if (isLiftTransfer != null && isLiftTransfer == 1) { List<StationObjModel> isLiftTransferStationList = isLiftTransferStationMap.getOrDefault(deviceNo, new ArrayList<>()); isLiftTransferStationList.add(stationObjModel); isLiftTransferStationMap.put(deviceNo, isLiftTransferStationList); } } else if (nodeType.equals("rgv")) { //RGV nodeData.put("type", "rgv"); } else if (nodeType.equals("none")) { //空白区域 nodeData.put("type", "none"); } else if (nodeType.equals("merge")) { //合并区域 nodeData.put("type", "merge"); nodeData.put("mergeType", map.get("mergeType")); } nodeData.put("cellWidth", map.get("cellWidth")); nodeData.put("cellHeight", map.get("cellHeight")); nodeData.put("rowSpan", map.get("rowSpan")); nodeData.put("colSpan", map.get("colSpan")); arrayList.add(nodeData); } dataList.add(arrayList); } BasMap basMap = basMapService.getOne(new QueryWrapper<BasMap>().eq("lev", lev)); if (basMap == null) { basMap = new BasMap(); } basMap.setData(JSON.toJSONString(dataList)); basMap.setOriginData(JSON.toJSONString(dataList)); basMap.setCreateTime(new Date()); basMap.setUpdateTime(new Date()); basMap.setLev(lev); basMapService.saveOrUpdate(basMap); } basStationService.remove(new QueryWrapper<>()); deviceStationMap.forEach((deviceNo, stationList) -> { BasDevp basDevp = basDevpService.getOne(new QueryWrapper<BasDevp>().eq("devp_no", deviceNo)); if (basDevp == null) { basDevp = new BasDevp(); basDevp.setDevpNo(deviceNo); basDevp.setCreateTime(new Date()); basDevp.setStatus(1); } List<StationObjModel> barcodeStationList = barcodeStationMap.get(deviceNo); List<StationObjModel> inStationList = inStationMap.get(deviceNo); List<StationObjModel> outStationList = outStationMap.get(deviceNo); List<StationObjModel> runBlockReassignStationList = runBlockReassignStationMap.get(deviceNo); List<StationObjModel> isOutOrderStationList = isOutOrderStationMap.get(deviceNo); List<StationObjModel> isLiftTransferStationList = isLiftTransferStationMap.get(deviceNo); if (barcodeStationList != null) { basDevp.setBarcodeStationList(JSON.toJSONString(barcodeStationList, SerializerFeature.DisableCircularReferenceDetect)); } if (inStationList != null) { basDevp.setInStationList(JSON.toJSONString(inStationList, SerializerFeature.DisableCircularReferenceDetect)); } if (outStationList != null) { basDevp.setOutStationList(JSON.toJSONString(outStationList, SerializerFeature.DisableCircularReferenceDetect)); } if (runBlockReassignStationList != null) { basDevp.setRunBlockReassignLocStationList(JSON.toJSONString(runBlockReassignStationList, SerializerFeature.DisableCircularReferenceDetect)); } if (isOutOrderStationList != null) { basDevp.setIsOutOrderList(JSON.toJSONString(isOutOrderStationList, SerializerFeature.DisableCircularReferenceDetect)); } if (isLiftTransferStationList != null) { basDevp.setIsLiftTransferList(JSON.toJSONString(isLiftTransferStationList, SerializerFeature.DisableCircularReferenceDetect)); } basDevp.setStationList(JSON.toJSONString(stationList, SerializerFeature.DisableCircularReferenceDetect)); basDevp.setUpdateTime(new Date()); basDevpService.saveOrUpdate(basDevp); DeviceConfig deviceConfig = deviceConfigService.getOne(new QueryWrapper<DeviceConfig>().eq("device_no", deviceNo).eq("device_type", String.valueOf(SlaveType.Devp))); if (deviceConfig != null) { deviceConfig.setFakeInitStatus(JSON.toJSONString(stationList)); deviceConfigService.updateById(deviceConfig); } for (StationObjModel stationObjModel : stationList) { BasStation basStation = new BasStation(); basStation.setStationId(stationObjModel.getStationId()); basStation.setDeviceNo(stationObjModel.getDeviceNo()); basStation.setStationLev(stationObjModel.getStationLev()); basStation.setCreateTime(new Date()); basStation.setStatus(1); basStationService.save(basStation); } }); tempFile = createTempUploadFile(file); basMapEditorService.importExcelAndPersist(tempFile.getAbsolutePath()); } catch (Exception e) { e.printStackTrace(); return R.error(e.getMessage()); } finally { deleteQuietly(tempFile); } return R.ok(); } @GetMapping("/basMap/editor/{lev}/auth") @ManagerAuth public R getEditorDoc(@PathVariable("lev") Integer lev) { BasMapEditorDoc doc = basMapEditorService.getEditorDoc(lev); if (doc == null) { return R.error("地图不存在"); } return R.ok().add(doc); } @PostMapping("/basMap/editor/save/auth") @ManagerAuth public R saveEditorDoc(@RequestBody BasMapEditorDoc doc) { basMapEditorService.saveEditorDoc(doc); return R.ok(); } @PostMapping("/basMap/editor/importExcel/auth") @ManagerAuth public R importExcelToEditor(@RequestParam("file") MultipartFile file) { File tempFile = null; try { tempFile = createTempUploadFile(file); return R.ok().add(basMapEditorService.importExcelToEditorDocs(tempFile.getAbsolutePath())); } catch (Exception e) { e.printStackTrace(); return R.error(e.getMessage()); } finally { deleteQuietly(tempFile); } } private File createTempUploadFile(MultipartFile file) throws IOException { String originalName = file == null ? null : file.getOriginalFilename(); String suffix = ".xlsx"; if (!Cools.isEmpty(originalName) && originalName.contains(".")) { suffix = originalName.substring(originalName.lastIndexOf('.')); } File tempFile = File.createTempFile("bas_map_", suffix); file.transferTo(tempFile); return tempFile; } private void deleteQuietly(File file) { if (file != null && file.exists()) { file.delete(); } } } src/main/java/com/zy/asrs/domain/BasMapEditorCell.java
New file @@ -0,0 +1,21 @@ package com.zy.asrs.domain; import lombok.Data; import java.io.Serializable; @Data public class BasMapEditorCell implements Serializable { private static final long serialVersionUID = 1L; private String type; private String value; private Integer rowSpan; private Integer colSpan; private String mergeType; } src/main/java/com/zy/asrs/domain/BasMapEditorDoc.java
New file @@ -0,0 +1,29 @@ package com.zy.asrs.domain; import lombok.Data; import java.io.Serializable; import java.util.ArrayList; import java.util.List; @Data public class BasMapEditorDoc implements Serializable { private static final long serialVersionUID = 1L; private Integer lev; private String editorMode; private Double canvasWidth; private Double canvasHeight; private List<BasMapEditorElement> elements = new ArrayList<>(); private List<Integer> rowHeights = new ArrayList<>(); private List<Integer> colWidths = new ArrayList<>(); private List<List<BasMapEditorCell>> cells = new ArrayList<>(); } src/main/java/com/zy/asrs/domain/BasMapEditorElement.java
New file @@ -0,0 +1,25 @@ package com.zy.asrs.domain; import lombok.Data; import java.io.Serializable; @Data public class BasMapEditorElement implements Serializable { private static final long serialVersionUID = 1L; private String id; private String type; private Double x; private Double y; private Double width; private Double height; private String value; } src/main/java/com/zy/asrs/service/BasMapEditorService.java
New file @@ -0,0 +1,17 @@ package com.zy.asrs.service; import com.zy.asrs.domain.BasMapEditorDoc; import java.io.IOException; import java.util.List; public interface BasMapEditorService { BasMapEditorDoc getEditorDoc(Integer lev); List<BasMapEditorDoc> importExcelToEditorDocs(String filePath) throws IOException; void importExcelAndPersist(String filePath) throws IOException; void saveEditorDoc(BasMapEditorDoc doc); } src/main/java/com/zy/asrs/service/impl/BasMapEditorServiceImpl.java
New file @@ -0,0 +1,920 @@ package com.zy.asrs.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.TypeReference; import com.alibaba.fastjson.serializer.SerializerFeature; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.core.common.Cools; import com.core.exception.CoolException; import com.zy.asrs.domain.BasMapEditorDoc; import com.zy.asrs.domain.BasMapEditorElement; import com.zy.asrs.entity.BasDevp; import com.zy.asrs.entity.BasMap; import com.zy.asrs.entity.BasStation; import com.zy.asrs.entity.DeviceConfig; import com.zy.asrs.service.BasDevpService; import com.zy.asrs.service.BasMapEditorService; import com.zy.asrs.service.BasMapService; import com.zy.asrs.service.BasStationService; import com.zy.asrs.service.DeviceConfigService; import com.zy.asrs.utils.MapExcelUtils; import com.zy.common.utils.RedisUtil; import com.zy.core.enums.RedisKeyType; import com.zy.core.enums.SlaveType; import com.zy.core.model.StationObjModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; @Service("basMapEditorService") public class BasMapEditorServiceImpl implements BasMapEditorService { private static final String FREE_EDITOR_MODE = "free-v1"; private static final Set<String> RUNTIME_TYPES = new HashSet<>(Arrays.asList( "shelf", "crn", "dualCrn", "devp", "rgv" )); private static final int DEFAULT_ROW_HEIGHT = 200; private static final int DEFAULT_COL_WIDTH = 1000; private static final double DEFAULT_CANVAS_WIDTH = 4200D; private static final double DEFAULT_CANVAS_HEIGHT = 5200D; private static final int X_SCALE = 40; private static final int Y_SCALE = 8; @Autowired private BasMapService basMapService; @Autowired private BasDevpService basDevpService; @Autowired private DeviceConfigService deviceConfigService; @Autowired private BasStationService basStationService; @Autowired private MapExcelUtils mapExcelUtils; @Autowired private RedisUtil redisUtil; @Override public BasMapEditorDoc getEditorDoc(Integer lev) { BasMap basMap = basMapService.getOne(new QueryWrapper<BasMap>().eq("lev", lev)); if (basMap == null) { return null; } BasMapEditorDoc editorDoc = parseEditorDocJson(lev, basMap.getOriginData()); if (editorDoc != null) { return editorDoc; } if (Cools.isEmpty(basMap.getData())) { return null; } return toFreeEditorDoc(lev, parseStoredMapData(basMap.getData())); } @Override public List<BasMapEditorDoc> importExcelToEditorDocs(String filePath) throws IOException { HashMap<Integer, List<List<HashMap<String, Object>>>> rawDataMap = mapExcelUtils.readExcel(filePath); List<Integer> levList = new ArrayList<>(rawDataMap.keySet()); Collections.sort(levList); List<BasMapEditorDoc> result = new ArrayList<>(); for (Integer lev : levList) { List<List<HashMap<String, Object>>> storedData = convertRawExcelData(rawDataMap.get(lev)); result.add(toFreeEditorDoc(lev, storedData)); } return result; } @Override @Transactional(rollbackFor = Exception.class) public void importExcelAndPersist(String filePath) throws IOException { HashMap<Integer, List<List<HashMap<String, Object>>>> rawDataMap = mapExcelUtils.readExcel(filePath); List<Integer> levList = new ArrayList<>(rawDataMap.keySet()); Collections.sort(levList); for (Integer lev : levList) { List<List<HashMap<String, Object>>> storedData = convertRawExcelData(rawDataMap.get(lev)); persistFloorMap(lev, storedData, toFreeEditorDoc(lev, storedData)); } rebuildDeviceAndStationSync(); clearMapCaches(); } @Override @Transactional(rollbackFor = Exception.class) public void saveEditorDoc(BasMapEditorDoc doc) { BasMapEditorDoc normalizedDoc = normalizeFreeDoc(doc); List<List<HashMap<String, Object>>> storedData = compileToStoredMapData(normalizedDoc); persistFloorMap(normalizedDoc.getLev(), storedData, normalizedDoc); rebuildDeviceAndStationSync(); clearMapCaches(); } private void persistFloorMap(Integer lev, List<List<HashMap<String, Object>>> storedData, BasMapEditorDoc editorDoc) { if (lev == null || lev <= 0) { throw new CoolException("楼层不能为空"); } String dataJson = JSON.toJSONString(storedData); String editorJson = JSON.toJSONString(editorDoc); BasMap basMap = basMapService.getOne(new QueryWrapper<BasMap>().eq("lev", lev)); Date now = new Date(); if (basMap == null) { basMap = new BasMap(); basMap.setCreateTime(now); } else { basMap.setLastData(basMap.getData()); } basMap.setLev(lev); basMap.setData(dataJson); basMap.setOriginData(editorJson); basMap.setUpdateTime(now); basMapService.saveOrUpdate(basMap); } private BasMapEditorDoc parseEditorDocJson(Integer lev, String json) { if (Cools.isEmpty(json)) { return null; } try { JSONObject object = JSON.parseObject(json); if (object == null || !FREE_EDITOR_MODE.equals(object.getString("editorMode"))) { return null; } BasMapEditorDoc doc = JSON.toJavaObject(object, BasMapEditorDoc.class); if (doc == null) { return null; } if (doc.getLev() == null || doc.getLev() <= 0) { doc.setLev(lev); } return normalizeFreeDoc(doc); } catch (Exception ignore) { return null; } } private BasMapEditorDoc toFreeEditorDoc(Integer lev, List<List<HashMap<String, Object>>> storedData) { BasMapEditorDoc doc = new BasMapEditorDoc(); doc.setLev(lev); doc.setEditorMode(FREE_EDITOR_MODE); doc.setElements(new ArrayList<>()); if (storedData == null) { doc.setCanvasWidth(DEFAULT_CANVAS_WIDTH); doc.setCanvasHeight(DEFAULT_CANVAS_HEIGHT); return doc; } int rowCount = storedData.size(); int colCount = 0; for (List<HashMap<String, Object>> row : storedData) { if (row != null && row.size() > colCount) { colCount = row.size(); } } List<Integer> rowHeights = new ArrayList<>(); for (int r = 0; r < rowCount; r++) { rowHeights.add(extractRowHeight(storedData, r)); } List<Integer> colWidths = new ArrayList<>(); for (int c = 0; c < colCount; c++) { colWidths.add(extractColWidth(storedData, c)); } int[] yOffsets = new int[rowCount]; int[] xOffsets = new int[colCount]; int totalHeight = 0; for (int r = 0; r < rowCount; r++) { yOffsets[r] = totalHeight; totalHeight += rowHeights.get(r); } int totalWidth = 0; for (int c = 0; c < colCount; c++) { xOffsets[c] = totalWidth; totalWidth += colWidths.get(c); } List<BasMapEditorElement> elements = new ArrayList<>(); for (int r = 0; r < rowCount; r++) { List<HashMap<String, Object>> row = storedData.get(r); if (row == null) { continue; } for (int c = 0; c < row.size(); c++) { HashMap<String, Object> cell = row.get(c); String type = normalizeType(getString(cell, "type", "none")); if (!RUNTIME_TYPES.contains(type)) { continue; } int rowSpan = Math.max(1, toInt(cell == null ? null : cell.get("rowSpan"), 1)); int colSpan = Math.max(1, toInt(cell == null ? null : cell.get("colSpan"), 1)); int widthRaw = 0; for (int cc = c; cc < Math.min(c + colSpan, colWidths.size()); cc++) { widthRaw += colWidths.get(cc); } int heightRaw = 0; for (int rr = r; rr < Math.min(r + rowSpan, rowHeights.size()); rr++) { heightRaw += rowHeights.get(rr); } BasMapEditorElement element = new BasMapEditorElement(); element.setId("el_" + r + "_" + c); element.setType(type); element.setX(xOffsets[c] / (double) X_SCALE); element.setY(yOffsets[r] / (double) Y_SCALE); element.setWidth(widthRaw / (double) X_SCALE); element.setHeight(heightRaw / (double) Y_SCALE); element.setValue(stringifyValue(cell == null ? null : cell.get("value"))); elements.add(element); } } doc.setElements(elements); doc.setCanvasWidth(Math.max(DEFAULT_CANVAS_WIDTH, totalWidth / (double) X_SCALE + 120D)); doc.setCanvasHeight(Math.max(DEFAULT_CANVAS_HEIGHT, totalHeight / (double) Y_SCALE + 120D)); return normalizeFreeDoc(doc); } private BasMapEditorDoc normalizeFreeDoc(BasMapEditorDoc source) { if (source == null || source.getLev() == null || source.getLev() <= 0) { throw new CoolException("楼层不能为空"); } BasMapEditorDoc doc = new BasMapEditorDoc(); doc.setLev(source.getLev()); doc.setEditorMode(FREE_EDITOR_MODE); doc.setCanvasWidth(toPositiveCanvasValue(source.getCanvasWidth(), DEFAULT_CANVAS_WIDTH, "画布宽度")); doc.setCanvasHeight(toPositiveCanvasValue(source.getCanvasHeight(), DEFAULT_CANVAS_HEIGHT, "画布高度")); List<BasMapEditorElement> elements = new ArrayList<>(); List<BasMapEditorElement> sourceElements = source.getElements(); if (sourceElements != null) { for (int i = 0; i < sourceElements.size(); i++) { elements.add(normalizeElement(sourceElements.get(i), i + 1)); } } ensureNoRectOverlap(elements); doc.setElements(elements); return doc; } private BasMapEditorElement normalizeElement(BasMapEditorElement source, int index) { BasMapEditorElement element = new BasMapEditorElement(); element.setId(Cools.isEmpty(source == null ? null : source.getId()) ? "el_" + index : source.getId().trim()); element.setType(normalizeType(source == null ? null : source.getType())); if (!RUNTIME_TYPES.contains(element.getType())) { throw new CoolException("存在不支持的节点类型: " + element.getType()); } double x = toNonNegativeDouble(source == null ? null : source.getX(), "元素 X 坐标"); double y = toNonNegativeDouble(source == null ? null : source.getY(), "元素 Y 坐标"); double width = toPositiveCanvasValue(source == null ? null : source.getWidth(), 0D, "元素宽度"); double height = toPositiveCanvasValue(source == null ? null : source.getHeight(), 0D, "元素高度"); element.setX(x); element.setY(y); element.setWidth(width); element.setHeight(height); element.setValue(stringifyValue(source == null ? null : source.getValue())); if ("devp".equals(element.getType())) { validateDevpValue(element.getValue(), index); } return element; } private void ensureNoRectOverlap(List<BasMapEditorElement> elements) { for (int i = 0; i < elements.size(); i++) { BasMapEditorElement a = elements.get(i); for (int j = i + 1; j < elements.size(); j++) { BasMapEditorElement b = elements.get(j); if (rectanglesOverlap(a, b)) { throw new CoolException("元素存在重叠: " + a.getId() + " 与 " + b.getId()); } } } } private boolean rectanglesOverlap(BasMapEditorElement a, BasMapEditorElement b) { int aLeft = toRawWidth(a.getX()); int aTop = toRawHeight(a.getY()); int aRight = toRawWidth(safeDouble(a.getX()) + safeDouble(a.getWidth())); int aBottom = toRawHeight(safeDouble(a.getY()) + safeDouble(a.getHeight())); int bLeft = toRawWidth(b.getX()); int bTop = toRawHeight(b.getY()); int bRight = toRawWidth(safeDouble(b.getX()) + safeDouble(b.getWidth())); int bBottom = toRawHeight(safeDouble(b.getY()) + safeDouble(b.getHeight())); return aLeft < bRight && aRight > bLeft && aTop < bBottom && aBottom > bTop; } private List<List<HashMap<String, Object>>> compileToStoredMapData(BasMapEditorDoc doc) { List<BasMapEditorElement> elements = doc.getElements() == null ? new ArrayList<BasMapEditorElement>() : doc.getElements(); if (elements.isEmpty()) { return buildEmptyStoredMapData(doc); } TreeSet<Integer> xBounds = new TreeSet<>(); TreeSet<Integer> yBounds = new TreeSet<>(); xBounds.add(0); yBounds.add(0); List<CompiledRect> rects = new ArrayList<>(); for (BasMapEditorElement element : elements) { CompiledRect rect = toCompiledRect(element); rects.add(rect); xBounds.add(rect.left); xBounds.add(rect.right); yBounds.add(rect.top); yBounds.add(rect.bottom); } List<Integer> xList = new ArrayList<>(xBounds); List<Integer> yList = new ArrayList<>(yBounds); if (xList.size() < 2 || yList.size() < 2) { return buildEmptyStoredMapData(doc); } int rowCount = yList.size() - 1; int colCount = xList.size() - 1; String[][] occupancy = new String[rowCount][colCount]; Map<String, Integer> xIndexMap = buildBoundaryIndexMap(xList); Map<String, Integer> yIndexMap = buildBoundaryIndexMap(yList); List<List<HashMap<String, Object>>> stored = new ArrayList<>(); for (int r = 0; r < rowCount; r++) { List<HashMap<String, Object>> row = new ArrayList<>(); int cellHeight = yList.get(r + 1) - yList.get(r); for (int c = 0; c < colCount; c++) { int cellWidth = xList.get(c + 1) - xList.get(c); row.add(createStoredCell("none", "", cellWidth, cellHeight, 1, 1, "")); } stored.add(row); } for (CompiledRect rect : rects) { Integer rowStart = yIndexMap.get(String.valueOf(rect.top)); Integer rowEndIndex = yIndexMap.get(String.valueOf(rect.bottom)); Integer colStart = xIndexMap.get(String.valueOf(rect.left)); Integer colEndIndex = xIndexMap.get(String.valueOf(rect.right)); if (rowStart == null || rowEndIndex == null || colStart == null || colEndIndex == null) { throw new CoolException("地图编译失败: 元素边界无法映射到网格"); } int rowSpan = rowEndIndex - rowStart; int colSpan = colEndIndex - colStart; if (rowSpan <= 0 || colSpan <= 0) { throw new CoolException("地图编译失败: 元素尺寸无效 " + rect.id); } for (int r = rowStart; r < rowStart + rowSpan; r++) { for (int c = colStart; c < colStart + colSpan; c++) { if (occupancy[r][c] != null) { throw new CoolException("地图编译失败: 元素重叠 " + rect.id + " 与 " + occupancy[r][c]); } occupancy[r][c] = rect.id; } } stored.get(rowStart).set(colStart, createStoredCell( rect.type, rect.value, xList.get(colStart + 1) - xList.get(colStart), yList.get(rowStart + 1) - yList.get(rowStart), rowSpan, colSpan, "" )); for (int r = rowStart; r < rowStart + rowSpan; r++) { for (int c = colStart; c < colStart + colSpan; c++) { if (r == rowStart && c == colStart) { continue; } stored.get(r).set(c, createStoredCell( "merge", rect.value, xList.get(c + 1) - xList.get(c), yList.get(r + 1) - yList.get(r), 1, 1, rect.type )); } } } return stored; } private List<List<HashMap<String, Object>>> buildEmptyStoredMapData(BasMapEditorDoc doc) { int widthRaw = Math.max(DEFAULT_COL_WIDTH, toRawWidth(doc == null ? null : doc.getCanvasWidth())); int heightRaw = Math.max(DEFAULT_ROW_HEIGHT, toRawHeight(doc == null ? null : doc.getCanvasHeight())); List<List<HashMap<String, Object>>> stored = new ArrayList<>(); List<HashMap<String, Object>> row = new ArrayList<>(); row.add(createStoredCell("none", "", widthRaw, heightRaw, 1, 1, "")); stored.add(row); return stored; } private CompiledRect toCompiledRect(BasMapEditorElement element) { CompiledRect rect = new CompiledRect(); rect.id = element.getId(); rect.type = element.getType(); rect.value = normalizeCellValue(element.getType(), element.getValue()); rect.left = toRawWidth(element.getX()); rect.top = toRawHeight(element.getY()); rect.right = toRawWidth(safeDouble(element.getX()) + safeDouble(element.getWidth())); rect.bottom = toRawHeight(safeDouble(element.getY()) + safeDouble(element.getHeight())); if (rect.right <= rect.left || rect.bottom <= rect.top) { throw new CoolException("元素尺寸无效: " + element.getId()); } return rect; } private Map<String, Integer> buildBoundaryIndexMap(List<Integer> bounds) { Map<String, Integer> result = new LinkedHashMap<>(); for (int i = 0; i < bounds.size(); i++) { result.put(String.valueOf(bounds.get(i)), i); } return result; } private HashMap<String, Object> createStoredCell(String type, String value, int cellWidth, int cellHeight, int rowSpan, int colSpan, String mergeType) { HashMap<String, Object> cell = new HashMap<>(); cell.put("type", normalizeType(type)); cell.put("value", stringifyValue(value)); cell.put("cellWidth", Math.max(1, cellWidth)); cell.put("cellHeight", Math.max(1, cellHeight)); cell.put("rowSpan", Math.max(1, rowSpan)); cell.put("colSpan", Math.max(1, colSpan)); if ("merge".equals(type)) { cell.put("mergeType", normalizeType(mergeType)); } return cell; } private List<List<HashMap<String, Object>>> parseStoredMapData(String json) { if (Cools.isEmpty(json)) { return new ArrayList<>(); } List<List<HashMap<String, Object>>> data = JSON.parseObject(json, new TypeReference<List<List<HashMap<String, Object>>>>() {}); return data == null ? new ArrayList<List<HashMap<String, Object>>>() : data; } private List<List<HashMap<String, Object>>> convertRawExcelData(List<List<HashMap<String, Object>>> rawData) { List<List<HashMap<String, Object>>> result = new ArrayList<>(); if (rawData == null) { return result; } for (List<HashMap<String, Object>> row : rawData) { List<HashMap<String, Object>> rowResult = new ArrayList<>(); if (row != null) { for (HashMap<String, Object> cell : row) { rowResult.add(convertRawExcelCell(cell)); } } result.add(rowResult); } return result; } private HashMap<String, Object> convertRawExcelCell(HashMap<String, Object> rawCell) { HashMap<String, Object> target = new HashMap<>(); String nodeType = getString(rawCell, "nodeType", "none"); String normalizedType; if ("shelf".equals(nodeType)) { normalizedType = "shelf"; } else if ("crn".equals(nodeType)) { normalizedType = "crn"; } else if ("dualCrn".equals(nodeType) || "dualcrn".equals(nodeType)) { normalizedType = "dualCrn"; } else if ("devp".equals(nodeType)) { normalizedType = "devp"; } else if ("rgv".equals(nodeType)) { normalizedType = "rgv"; } else if ("merge".equals(nodeType)) { normalizedType = "merge"; } else { normalizedType = "none"; } target.put("type", normalizedType); target.put("value", stringifyValue(rawCell == null ? null : rawCell.get("value"))); target.put("cellWidth", Math.max(1, toInt(rawCell == null ? null : rawCell.get("cellWidth"), DEFAULT_COL_WIDTH))); target.put("cellHeight", Math.max(1, toInt(rawCell == null ? null : rawCell.get("cellHeight"), DEFAULT_ROW_HEIGHT))); target.put("rowSpan", Math.max(1, toInt(rawCell == null ? null : rawCell.get("rowSpan"), 1))); target.put("colSpan", Math.max(1, toInt(rawCell == null ? null : rawCell.get("colSpan"), 1))); if ("merge".equals(normalizedType)) { target.put("mergeType", normalizeType(getString(rawCell, "mergeType", "none"))); } return target; } private int extractRowHeight(List<List<HashMap<String, Object>>> storedData, int rowIndex) { List<HashMap<String, Object>> row = storedData.get(rowIndex); if (row == null) { return DEFAULT_ROW_HEIGHT; } for (HashMap<String, Object> cell : row) { int height = toInt(cell == null ? null : cell.get("cellHeight"), 0); if (height > 0) { return height; } } return DEFAULT_ROW_HEIGHT; } private int extractColWidth(List<List<HashMap<String, Object>>> storedData, int colIndex) { for (List<HashMap<String, Object>> row : storedData) { if (row == null || colIndex >= row.size()) { continue; } HashMap<String, Object> cell = row.get(colIndex); int width = toInt(cell == null ? null : cell.get("cellWidth"), 0); if (width > 0) { return width; } } return DEFAULT_COL_WIDTH; } private void validateDevpValue(String value, int elementIndex) { if (Cools.isEmpty(value)) { throw new CoolException("输送线节点缺少配置: 元素#" + elementIndex); } JSONObject jsonObject; try { jsonObject = JSON.parseObject(value); } catch (Exception ex) { throw new CoolException("输送线节点配置不是合法JSON: 元素#" + elementIndex); } if (jsonObject == null || jsonObject.getInteger("stationId") == null || jsonObject.getInteger("deviceNo") == null) { throw new CoolException("输送线节点必须包含stationId和deviceNo: 元素#" + elementIndex); } Integer isInStation = jsonObject.getInteger("isInStation"); Integer isBarcodeStation = jsonObject.getInteger("isBarcodeStation"); Integer barcodeIdx = jsonObject.getInteger("barcodeIdx"); Integer barcodeStation = jsonObject.getInteger("barcodeStation"); Integer barcodeStationDeviceNo = jsonObject.getInteger("barcodeStationDeviceNo"); Integer backStation = jsonObject.getInteger("backStation"); Integer backStationDeviceNo = jsonObject.getInteger("backStationDeviceNo"); if (isInStation != null && isInStation == 1) { if (!isPositiveInteger(barcodeStation) || !isPositiveInteger(barcodeStationDeviceNo)) { throw new CoolException("入站点必须包含barcodeStation和barcodeStationDeviceNo: 元素#" + elementIndex); } } if (isBarcodeStation != null && isBarcodeStation == 1) { if (!isPositiveInteger(barcodeIdx) || !isPositiveInteger(backStation) || !isPositiveInteger(backStationDeviceNo)) { throw new CoolException("条码站必须包含barcodeIdx、backStation和backStationDeviceNo: 元素#" + elementIndex); } } } private boolean isPositiveInteger(Integer value) { return value != null && value > 0; } private double toPositiveCanvasValue(Double rawValue, double defaultValue, String fieldLabel) { double value = safeDouble(rawValue); if (value <= 0) { if (defaultValue > 0) { return defaultValue; } throw new CoolException(fieldLabel + "必须大于0"); } return value; } private double toNonNegativeDouble(Double rawValue, String fieldLabel) { double value = safeDouble(rawValue); if (value < 0) { throw new CoolException(fieldLabel + "不能小于0"); } return value; } private int toRawWidth(Double displayValue) { return Math.max(0, (int) Math.round(safeDouble(displayValue) * X_SCALE)); } private int toRawHeight(Double displayValue) { return Math.max(0, (int) Math.round(safeDouble(displayValue) * Y_SCALE)); } private double safeDouble(Double value) { return value == null ? 0D : value; } private void rebuildDeviceAndStationSync() { SyncContext context = new SyncContext(); List<BasMap> basMapList = basMapService.list(new QueryWrapper<BasMap>().orderByAsc("lev")); for (BasMap basMap : basMapList) { collectStationsFromMap(context, basMap.getLev(), parseStoredMapData(basMap.getData())); } basStationService.remove(new QueryWrapper<BasStation>()); syncBasDevp(context); syncBasStation(context); } private void collectStationsFromMap(SyncContext context, Integer lev, List<List<HashMap<String, Object>>> storedData) { if (storedData == null) { return; } for (List<HashMap<String, Object>> row : storedData) { if (row == null) { continue; } for (HashMap<String, Object> cell : row) { String type = normalizeType(getString(cell, "type", "none")); if (!"devp".equals(type)) { continue; } JSONObject value = parseJsonObject(stringifyValue(cell.get("value"))); if (value == null) { continue; } Integer deviceNo = value.getInteger("deviceNo"); Integer stationId = value.getInteger("stationId"); if (deviceNo == null || stationId == null) { continue; } StationObjModel stationObjModel = new StationObjModel(); stationObjModel.setDeviceNo(deviceNo); stationObjModel.setStationId(stationId); stationObjModel.setStationLev(lev); addStationModel(context.deviceStationMap, context.deviceStationDedup, deviceNo, buildStationKey(deviceNo, stationId, lev), stationObjModel); context.stationRegistry.put(stationId, stationObjModel); Integer isBarcodeStation = value.getInteger("isBarcodeStation"); if (isBarcodeStation != null && isBarcodeStation == 1) { StationObjModel barcodeStationModel = new StationObjModel(); barcodeStationModel.setDeviceNo(deviceNo); barcodeStationModel.setStationId(stationId); barcodeStationModel.setBarcodeIdx(value.getInteger("barcodeIdx")); if (value.getInteger("backStation") != null) { StationObjModel backStation = new StationObjModel(); backStation.setDeviceNo(value.getInteger("backStationDeviceNo")); backStation.setStationId(value.getInteger("backStation")); barcodeStationModel.setBackStation(backStation); } addStationModel(context.barcodeStationMap, context.barcodeStationDedup, deviceNo, buildStationKey(deviceNo, stationId, lev), barcodeStationModel); } Integer isInStation = value.getInteger("isInStation"); if (isInStation != null && isInStation == 1) { StationObjModel inStationModel = new StationObjModel(); inStationModel.setDeviceNo(deviceNo); inStationModel.setStationId(stationId); StationObjModel barcodeStation = new StationObjModel(); barcodeStation.setDeviceNo(value.getInteger("barcodeStationDeviceNo")); barcodeStation.setStationId(value.getInteger("barcodeStation")); inStationModel.setBarcodeStation(barcodeStation); addStationModel(context.inStationMap, context.inStationDedup, deviceNo, buildStationKey(deviceNo, stationId, lev), inStationModel); } Integer isOutStation = value.getInteger("isOutStation"); if (isOutStation != null && isOutStation == 1) { addStationModel(context.outStationMap, context.outStationDedup, deviceNo, buildStationKey(deviceNo, stationId, lev), cloneStationModel(stationObjModel)); } Integer runBlockReassign = value.getInteger("runBlockReassign"); if (runBlockReassign != null && runBlockReassign == 1) { addStationModel(context.runBlockReassignStationMap, context.runBlockReassignDedup, deviceNo, buildStationKey(deviceNo, stationId, lev), cloneStationModel(stationObjModel)); } Integer isOutOrder = value.getInteger("isOutOrder"); if (isOutOrder != null && isOutOrder == 1) { addStationModel(context.outOrderStationMap, context.outOrderDedup, deviceNo, buildStationKey(deviceNo, stationId, lev), cloneStationModel(stationObjModel)); } Integer isLiftTransfer = value.getInteger("isLiftTransfer"); if (isLiftTransfer != null && isLiftTransfer == 1) { addStationModel(context.liftTransferStationMap, context.liftTransferDedup, deviceNo, buildStationKey(deviceNo, stationId, lev), cloneStationModel(stationObjModel)); } } } } private void syncBasDevp(SyncContext context) { Map<Integer, BasDevp> existingMap = new LinkedHashMap<>(); List<BasDevp> existingList = basDevpService.list(); for (BasDevp basDevp : existingList) { existingMap.put(basDevp.getDevpNo(), basDevp); } Set<Integer> allDeviceNos = new LinkedHashSet<>(); allDeviceNos.addAll(existingMap.keySet()); allDeviceNos.addAll(context.deviceStationMap.keySet()); Date now = new Date(); for (Integer deviceNo : allDeviceNos) { BasDevp basDevp = existingMap.get(deviceNo); if (basDevp == null) { basDevp = new BasDevp(); basDevp.setDevpNo(deviceNo); basDevp.setStatus(1); basDevp.setCreateTime(now); } List<StationObjModel> stationList = context.deviceStationMap.get(deviceNo); List<StationObjModel> barcodeStationList = context.barcodeStationMap.get(deviceNo); List<StationObjModel> inStationList = context.inStationMap.get(deviceNo); List<StationObjModel> outStationList = context.outStationMap.get(deviceNo); List<StationObjModel> runBlockReassignStationList = context.runBlockReassignStationMap.get(deviceNo); List<StationObjModel> outOrderStationList = context.outOrderStationMap.get(deviceNo); List<StationObjModel> liftTransferStationList = context.liftTransferStationMap.get(deviceNo); basDevp.setStationList(toJsonOrNull(stationList)); basDevp.setBarcodeStationList(toJsonOrNull(barcodeStationList)); basDevp.setInStationList(toJsonOrNull(inStationList)); basDevp.setOutStationList(toJsonOrNull(outStationList)); basDevp.setRunBlockReassignLocStationList(toJsonOrNull(runBlockReassignStationList)); basDevp.setIsOutOrderList(toJsonOrNull(outOrderStationList)); basDevp.setIsLiftTransferList(toJsonOrNull(liftTransferStationList)); basDevp.setUpdateTime(now); basDevpService.saveOrUpdate(basDevp); DeviceConfig deviceConfig = deviceConfigService.getOne(new QueryWrapper<DeviceConfig>() .eq("device_no", deviceNo) .eq("device_type", String.valueOf(SlaveType.Devp))); if (deviceConfig != null) { deviceConfig.setFakeInitStatus(toJsonOrNull(stationList)); deviceConfigService.updateById(deviceConfig); } } } private void syncBasStation(SyncContext context) { Date now = new Date(); for (StationObjModel stationObjModel : context.stationRegistry.values()) { BasStation basStation = new BasStation(); basStation.setStationId(stationObjModel.getStationId()); basStation.setDeviceNo(stationObjModel.getDeviceNo()); basStation.setStationLev(stationObjModel.getStationLev()); basStation.setCreateTime(now); basStation.setStatus(1); basStationService.save(basStation); } } private void addStationModel(Map<Integer, List<StationObjModel>> targetMap, Map<Integer, Set<String>> dedupMap, Integer deviceNo, String dedupKey, StationObjModel stationObjModel) { Set<String> set = dedupMap.get(deviceNo); if (set == null) { set = new LinkedHashSet<>(); dedupMap.put(deviceNo, set); } if (!set.add(dedupKey)) { return; } List<StationObjModel> list = targetMap.get(deviceNo); if (list == null) { list = new ArrayList<>(); targetMap.put(deviceNo, list); } list.add(stationObjModel); } private StationObjModel cloneStationModel(StationObjModel source) { StationObjModel clone = new StationObjModel(); clone.setDeviceNo(source.getDeviceNo()); clone.setStationId(source.getStationId()); clone.setStationLev(source.getStationLev()); clone.setBarcodeIdx(source.getBarcodeIdx()); clone.setDeviceBay(source.getDeviceBay()); clone.setDeviceLev(source.getDeviceLev()); clone.setDeviceRow(source.getDeviceRow()); clone.setDualCrnExecuteStation(source.getDualCrnExecuteStation()); clone.setBarcodeStation(source.getBarcodeStation()); clone.setBackStation(source.getBackStation()); return clone; } private String toJsonOrNull(List<StationObjModel> list) { if (list == null || list.isEmpty()) { return null; } return JSON.toJSONString(list, SerializerFeature.DisableCircularReferenceDetect); } private void clearMapCaches() { redisUtil.del(RedisKeyType.LOC_MAP_BASE.key); redisUtil.del(RedisKeyType.LOC_MAST_MAP_LIST.key); } private String normalizeType(String type) { if (type == null) { return "none"; } String value = String.valueOf(type).trim(); if ("dualcrn".equalsIgnoreCase(value)) { return "dualCrn"; } if (Cools.isEmpty(value)) { return "none"; } return value; } private String normalizeCellValue(String type, String value) { if ("none".equals(type)) { return ""; } return stringifyValue(value); } private String stringifyValue(Object value) { if (value == null) { return ""; } if (value instanceof String) { return (String) value; } if (value instanceof JSONObject || value instanceof Map || value instanceof List) { return JSON.toJSONString(value); } return String.valueOf(value); } private String getString(Map<String, Object> map, String key, String defaultValue) { if (map == null || !map.containsKey(key) || map.get(key) == null) { return defaultValue; } String value = stringifyValue(map.get(key)); return Cools.isEmpty(value) ? defaultValue : value; } private int toInt(Object value, int defaultValue) { if (value == null) { return defaultValue; } if (value instanceof Number) { return ((Number) value).intValue(); } try { return (int) Math.round(Double.parseDouble(String.valueOf(value))); } catch (Exception ignore) { return defaultValue; } } private JSONObject parseJsonObject(String json) { if (Cools.isEmpty(json)) { return null; } try { return JSON.parseObject(json); } catch (Exception ignore) { return null; } } private String buildStationKey(Integer deviceNo, Integer stationId, Integer lev) { return deviceNo + "_" + stationId + "_" + lev; } private static class CompiledRect { private String id; private String type; private String value; private int left; private int top; private int right; private int bottom; } private static class SyncContext { private final Map<Integer, List<StationObjModel>> deviceStationMap = new LinkedHashMap<>(); private final Map<Integer, List<StationObjModel>> barcodeStationMap = new LinkedHashMap<>(); private final Map<Integer, List<StationObjModel>> inStationMap = new LinkedHashMap<>(); private final Map<Integer, List<StationObjModel>> outStationMap = new LinkedHashMap<>(); private final Map<Integer, List<StationObjModel>> runBlockReassignStationMap = new LinkedHashMap<>(); private final Map<Integer, List<StationObjModel>> outOrderStationMap = new LinkedHashMap<>(); private final Map<Integer, List<StationObjModel>> liftTransferStationMap = new LinkedHashMap<>(); private final Map<Integer, StationObjModel> stationRegistry = new LinkedHashMap<>(); private final Map<Integer, Set<String>> deviceStationDedup = new LinkedHashMap<>(); private final Map<Integer, Set<String>> barcodeStationDedup = new LinkedHashMap<>(); private final Map<Integer, Set<String>> inStationDedup = new LinkedHashMap<>(); private final Map<Integer, Set<String>> outStationDedup = new LinkedHashMap<>(); private final Map<Integer, Set<String>> runBlockReassignDedup = new LinkedHashMap<>(); private final Map<Integer, Set<String>> outOrderDedup = new LinkedHashMap<>(); private final Map<Integer, Set<String>> liftTransferDedup = new LinkedHashMap<>(); } } src/main/webapp/components/MapCanvas.js
@@ -77,6 +77,8 @@ }, pixiShelfMap: new Map(), pixiTrackMap: new Map(), pixiCrnTextureMap: new Map(), pixiRgvTextureMap: new Map(), pixiDevpTextureMap: new Map(), pixiCrnColorTextureMap: new Map(), pixiDevpTextureMap: new Map(), @@ -787,8 +789,7 @@ }); this.crnList.forEach((item) => { if (this.graphicsCrn == null) { this.graphicsCrn = this.createCrnTexture(item.width * 0.9, item.height * 0.9); } let sprite = new PIXI.Sprite(this.graphicsCrn); let sprite = this.createCrnSprite(item.width * 0.9, item.height * 0.9); const deviceNo = this.getDeviceNo(item.value); const taskNo = this.getTaskNo(item.value); const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 12, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 }); @@ -820,8 +821,7 @@ }); this.dualCrnList.forEach((item) => { if (this.graphicsCrn == null) { this.graphicsCrn = this.createCrnTexture(item.width * 0.9, item.height * 0.9); } let sprite = new PIXI.Sprite(this.graphicsCrn); let sprite = this.createCrnSprite(item.width * 0.9, item.height * 0.9); const deviceNo = this.getDeviceNo(item.value); const taskNo = this.getTaskNo(item.value); const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 12, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 }); @@ -853,8 +853,7 @@ }); this.rgvList.forEach((item) => { if (this.graphicsRgv == null) { this.graphicsRgv = this.createRgvTexture(item.width * 0.9, item.height * 0.9); } let sprite = new PIXI.Sprite(this.graphicsRgv); let sprite = this.createRgvSprite(item.width * 0.9, item.height * 0.9); const deviceNo = this.getDeviceNo(item.value); const taskNo = this.getTaskNo(item.value); const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 12, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 }); @@ -1227,6 +1226,28 @@ if (texture == undefined) { texture = this.createTrackTexture(width, height, trackMask); this.pixiTrackMap.set(idx, texture); } return new PIXI.Sprite(texture); }, createCrnSprite(width, height) { const w = Math.max(1, Math.round(width)); const h = Math.max(1, Math.round(height)); const key = w + "-" + h; let texture = this.pixiCrnTextureMap.get(key); if (texture == undefined) { texture = this.createCrnTexture(w, h); this.pixiCrnTextureMap.set(key, texture); } return new PIXI.Sprite(texture); }, createRgvSprite(width, height) { const w = Math.max(1, Math.round(width)); const h = Math.max(1, Math.round(height)); const key = w + "-" + h; let texture = this.pixiRgvTextureMap.get(key); if (texture == undefined) { texture = this.createRgvTexture(w, h); this.pixiRgvTextureMap.set(key, texture); } return new PIXI.Sprite(texture); }, @@ -2799,7 +2820,6 @@ } } }); src/main/webapp/static/js/basMap/basMap.js
@@ -1170,6 +1170,30 @@ } }); }, openVisualEditor: function (row) { var lev = row && row.lev ? Number(row.lev) : 0; if (!lev) { this.$message.warning('当前记录缺少楼层'); return; } window.open(baseUrl + '/views/basMap/editor.html?lev=' + encodeURIComponent(lev), '_blank'); }, openVisualEditorByPrompt: function () { var self = this; self.$prompt('请输入需要打开的楼层', '可视化编辑', { confirmButtonText: '打开', cancelButtonText: '取消', inputPattern: /^\d+$/, inputErrorMessage: '请输入数字层数' }).then(function (payload) { var lev = Number($.trim(payload && payload.value ? payload.value : '')); if (!lev) { self.$message.warning('请输入有效楼层'); return; } window.open(baseUrl + '/views/basMap/editor.html?lev=' + encodeURIComponent(lev), '_blank'); }).catch(function () {}); }, promptInitLocMast: function () { var self = this; self.$prompt('请输入初始化库位层数', '初始化库位', { src/main/webapp/static/js/basMap/editor.js
New file Diff too large src/main/webapp/views/basMap/basMap.html
@@ -423,6 +423,13 @@ <el-button size="small" plain icon="el-icon-edit-outline" @click="openVisualEditorByPrompt"> 可视化编辑 </el-button> <el-button size="small" plain icon="el-icon-refresh" :loading="initializingLocMast" @click="promptInitLocMast"> @@ -568,8 +575,9 @@ <span v-else>{{ valueOrDash(getTableValue(scope.row, field)) }}</span> </template> </el-table-column> <el-table-column label="操作" width="160" fixed="right" align="center"> <el-table-column label="操作" width="220" fixed="right" align="center"> <template slot-scope="scope"> <el-button type="text" @click="openVisualEditor(scope.row)">可视化编辑</el-button> <el-button type="text" @click="openEditDialog(scope.row)">修改</el-button> <el-button type="text" style="color:#f56c6c;" @click="removeRows([scope.row[primaryKeyField]])">删除</el-button> </template> src/main/webapp/views/basMap/editor.html
New file @@ -0,0 +1,814 @@ <!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 { --page-bg: radial-gradient(1180px 540px at -10% -16%, rgba(24, 113, 181, 0.14), transparent 58%), radial-gradient(920px 480px at 110% -12%, rgba(14, 148, 136, 0.12), transparent 56%), linear-gradient(180deg, #eef4f9 0%, #f8fbfd 100%); --card-bg: rgba(255, 255, 255, 0.94); --card-border: rgba(216, 226, 238, 0.96); --text-main: #213448; --text-sub: #63788e; --primary: #2f79d6; --accent: #169a82; --warn: #f08a3c; --danger: #d85a5a; } [v-cloak] { display: none; } html, body { margin: 0; height: 100%; font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; color: var(--text-main); background: var(--page-bg); overflow: hidden; } .editor-shell { width: 100%; height: 100vh; margin: 0 auto; padding: 8px; box-sizing: border-box; display: flex; flex-direction: column; gap: 0; } .panel-card { border-radius: 24px; border: 1px solid var(--card-border); background: var(--card-bg); box-shadow: 0 16px 32px rgba(39, 62, 92, 0.08); } .workspace { min-height: 0; flex: 1 1 auto; display: flex; } .panel-card { display: flex; flex-direction: column; overflow: hidden; } .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 18px 20px 14px; border-bottom: 1px solid rgba(221, 230, 239, 0.94); } .panel-head h2 { margin: 0; font-size: 16px; font-weight: 700; } .panel-body { padding: 16px 18px 18px; display: flex; flex-direction: column; gap: 14px; min-height: 0; overflow: auto; } .tool-section, .status-stack, .action-list { display: flex; flex-direction: column; gap: 8px; } .tool-section-label { font-size: 12px; color: var(--text-sub); letter-spacing: 0.08em; text-transform: uppercase; } .tool-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } .tool-card-btn { appearance: none; border: 1px solid rgba(193, 205, 219, 0.9); background: rgba(255, 255, 255, 0.96); border-radius: 14px; padding: 10px 12px; text-align: left; cursor: pointer; transition: all 0.18s ease; color: var(--text-main); display: flex; flex-direction: column; gap: 4px; min-height: 68px; } .tool-card-btn.active { border-color: rgba(55, 127, 212, 0.45); background: rgba(235, 244, 253, 0.98); box-shadow: 0 8px 18px rgba(47, 121, 214, 0.14); } .tool-card-btn strong { font-size: 13px; font-weight: 700; } .tool-card-btn span { font-size: 12px; color: var(--text-sub); line-height: 1.5; } .status-card, .selection-summary, .note-card { border-radius: 16px; border: 1px solid rgba(218, 227, 236, 0.92); background: rgba(248, 251, 254, 0.92); padding: 12px 14px; } .status-card strong, .selection-summary strong, .note-card strong { display: block; font-size: 13px; margin-bottom: 6px; } .status-card span, .selection-summary span, .note-card span { display: block; font-size: 12px; color: var(--text-sub); line-height: 1.65; } .selection-summary strong { font-size: 14px; color: var(--text-main); } .canvas-toolbar { padding: 16px 18px 14px; border-bottom: 1px solid rgba(221, 230, 239, 0.94); display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; } .canvas-toolbar-main { flex: 1 1 420px; display: flex; flex-direction: column; gap: 8px; } .canvas-toolbar-title { display: flex; flex-direction: column; gap: 4px; } .canvas-toolbar-title h1 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: 0.3px; } .canvas-toolbar-title span { font-size: 13px; line-height: 1.65; color: var(--text-sub); } .canvas-toolbar-meta, .canvas-toolbar-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .canvas-toolbar-actions { justify-content: flex-end; flex: 0 1 760px; } .canvas-toolbar-actions .el-input__inner, .canvas-toolbar-actions .el-button { border-radius: 10px; } .canvas-meta { font-size: 12px; color: var(--text-sub); } .canvas-card { flex: 1 1 auto; min-width: 0; min-height: 0; } .canvas-wrap { position: relative; flex: 1 1 auto; min-height: 0; background: linear-gradient(180deg, rgba(245, 249, 252, 0.95) 0%, rgba(251, 252, 254, 0.98) 100%); } .canvas-stage { position: absolute; inset: 0; overflow: hidden; background: #f6f9fc; } .canvas-host { position: absolute; inset: 0; background: #f6f9fc; } .canvas-overlay-layer { position: absolute; inset: 0; pointer-events: none; z-index: 5; } .canvas-loading-mask { position: absolute; inset: 0; z-index: 4; display: flex; align-items: center; justify-content: center; background: rgba(246, 249, 252, 0.82); backdrop-filter: blur(2px); } .canvas-loading-card { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 220px; padding: 18px 22px; border-radius: 18px; background: rgba(255, 255, 255, 0.96); border: 1px solid rgba(55, 127, 212, 0.16); box-shadow: 0 18px 40px rgba(66, 94, 136, 0.12); color: var(--text-main); } .canvas-loading-card strong { font-size: 18px; } .canvas-loading-card span { font-size: 13px; color: var(--text-sub); } .overlay-panel { position: absolute; top: 14px; bottom: 14px; width: 300px; display: flex; flex-direction: column; border-radius: 22px; border: 1px solid rgba(214, 224, 236, 0.96); background: #ffffff; box-shadow: 0 8px 18px rgba(31, 55, 82, 0.06); overflow: hidden; pointer-events: auto; contain: layout paint; } .overlay-left { left: 14px; } .overlay-right { right: 14px; width: 340px; } .overlay-panel.collapsed { width: 68px; bottom: auto; } .overlay-panel.collapsed .panel-body { display: none; } .overlay-panel.collapsed .panel-head { align-items: flex-start; padding: 12px 10px; } .overlay-panel.collapsed .panel-head h2 { writing-mode: vertical-rl; text-orientation: mixed; font-size: 14px; line-height: 1; } .overlay-panel.collapsed .canvas-meta { display: none; } .panel-head-actions { display: flex; align-items: center; gap: 8px; } .panel-toggle { appearance: none; border: 1px solid rgba(190, 203, 217, 0.96); background: rgba(255, 255, 255, 0.96); color: var(--text-main); width: 30px; height: 30px; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 700; line-height: 1; } .panel-toggle:hover { border-color: rgba(55, 127, 212, 0.4); color: var(--primary); } .prop-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } .prop-grid .span-2 { grid-column: span 2; } .field-stack { display: flex; flex-direction: column; gap: 6px; } .field-label { font-size: 12px; color: var(--text-sub); line-height: 1.4; } .field-required { color: #d85b52; font-weight: 700; } .field-help { font-size: 12px; color: var(--text-sub); line-height: 1.6; } .direction-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } .direction-chip { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 34px; border: 1px solid rgba(55, 127, 212, 0.18); border-radius: 12px; background: #fff; color: var(--text-main); cursor: pointer; transition: all 0.16s ease; } .direction-chip:hover { border-color: rgba(55, 127, 212, 0.45); color: var(--primary); } .direction-chip.active { border-color: rgba(55, 127, 212, 0.85); background: rgba(85, 145, 227, 0.12); color: var(--primary); box-shadow: inset 0 0 0 1px rgba(85, 145, 227, 0.1); } .direction-arrow { font-size: 16px; font-weight: 700; line-height: 1; } .check-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; } .json-box { font-family: Menlo, Monaco, Consolas, "Liberation Mono", monospace; } .footer-note { font-size: 12px; color: var(--text-sub); line-height: 1.75; } @media (max-width: 1380px) { .overlay-left { width: 260px; } .overlay-right { width: 300px; } } @media (max-width: 980px) { .canvas-card { min-height: 0; } .canvas-wrap { min-height: 0; } .overlay-left, .overlay-right { width: min(280px, calc(100% - 28px)); } } </style> </head> <body> <div id="app" class="editor-shell" v-cloak> <section class="workspace"> <main class="panel-card canvas-panel canvas-card"> <div class="canvas-toolbar"> <div class="canvas-toolbar-main"> <div class="canvas-toolbar-title"> <h1>PixiJS 自由画布地图编辑器</h1> <span>编辑态使用自由画布 JSON,保存时再编译成现有运行地图,所以 `MapCanvas` 和后端算法继续只读 `BasMap.data`。</span> </div> <div class="canvas-toolbar-meta"> <span class="canvas-meta">楼层: {{ currentLev ? currentLev + 'F' : '--' }}</span> <span class="canvas-meta">缩放: {{ viewPercent }}%</span> <span class="canvas-meta">模式: {{ toolLabel(activeTool) }}</span> <span class="canvas-meta">选中: {{ selectedIds.length }}</span> <span class="canvas-meta">元素: {{ doc ? doc.elements.length : 0 }}</span> <span class="canvas-meta">画布: {{ Math.round(doc ? doc.canvasWidth : 0) }} x {{ Math.round(doc ? doc.canvasHeight : 0) }}</span> <span class="canvas-meta">渲染倍率: {{ formatNumber(pixiResolution) }}x</span> <span class="canvas-meta">FPS: {{ fpsText }}</span> </div> </div> <div class="canvas-toolbar-actions"> <el-select v-model="floorPickerLev" size="small" placeholder="选择楼层" @change="handleFloorChange" style="width: 120px;"> <el-option v-for="lev in levOptions" :key="'lev-' + lev" :label="lev + 'F'" :value="lev"></el-option> </el-select> <el-button size="small" plain @click="openBlankDialog">新建自由画布</el-button> <el-button size="small" plain @click="triggerImportExcel">导入 Excel</el-button> <el-button size="small" plain @click="triggerImportMap">导入地图</el-button> <el-button size="small" plain @click="exportMapPackage">导出地图</el-button> <el-button size="small" plain @click="loadCurrentFloor">重新读取</el-button> <el-button size="small" plain @click="fitContent">适配全图</el-button> <el-button size="small" plain @click="resetView">回到画布</el-button> <el-button size="small" @click="undo" :disabled="undoStack.length === 0">撤销</el-button> <el-button size="small" @click="redo" :disabled="redoStack.length === 0">重做</el-button> <el-button size="small" type="primary" plain :loading="savingAll" :disabled="dirtyDraftCount === 0 || saving" @click="saveAllDocs">保存全部楼层<span v-if="dirtyDraftCount > 0">({{ dirtyDraftCount }})</span></el-button> <el-button size="small" type="primary" :loading="saving" :disabled="savingAll" @click="saveDoc">保存当前楼层</el-button> </div> </div> <div class="canvas-wrap"> <div class="canvas-stage" ref="canvasStage"> <div class="canvas-host" ref="canvasHost"></div> <div v-if="loadingFloor" class="canvas-loading-mask"> <div class="canvas-loading-card"> <strong>正在加载 {{ switchingFloorLev || floorPickerLev || currentLev || '--' }}F</strong> <span>画布和缓存正在切换,请稍候。</span> </div> </div> </div> <div class="canvas-overlay-layer"> <aside class="overlay-panel overlay-left" :class="{ collapsed: toolPanelCollapsed }"> <div class="panel-head"> <h2>工具面板</h2> <div class="panel-head-actions"> <span class="canvas-meta">{{ toolLabel(activeTool) }}</span> <button type="button" class="panel-toggle" @click="toggleToolPanel">{{ toolPanelCollapsed ? '>' : '<' }}</button> </div> </div> <div class="panel-body"> <div class="tool-section"> <div class="tool-section-label">交互</div> <div class="tool-grid"> <button v-for="tool in interactionTools" :key="tool.key" type="button" class="tool-card-btn" :class="{ active: activeTool === tool.key }" @click="setTool(tool.key)"> <strong>{{ tool.label }}</strong> <span>{{ tool.desc }}</span> </button> </div> </div> <div class="tool-section"> <div class="tool-section-label">绘制元素</div> <div class="tool-grid"> <button v-for="tool in drawTools" :key="tool.key" type="button" class="tool-card-btn" :class="{ active: activeTool === tool.key }" @click="setTool(tool.key)"> <strong>{{ tool.label }}</strong> <span>{{ tool.desc }}</span> </button> </div> </div> <div class="tool-section"> <div class="tool-section-label">编辑动作</div> <div class="action-list"> <el-button size="small" plain @click="copySelection" :disabled="selectedIds.length === 0">复制</el-button> <el-button size="small" plain @click="pasteClipboard" :disabled="clipboard.length === 0">粘贴</el-button> <el-button size="small" plain @click="duplicateSelection" :disabled="selectedIds.length === 0">复制偏移</el-button> <el-button size="small" plain @click="fitSelection" :disabled="selectedIds.length === 0">聚焦选中</el-button> <el-button size="small" type="danger" plain @click="deleteSelection" :disabled="selectedIds.length === 0">删除选中</el-button> </div> </div> <div class="status-stack"> <div class="status-card"> <strong>快捷键</strong> <span>`Delete` 删除,`Ctrl/Cmd + Z` 撤销,`Ctrl/Cmd + Shift + Z` / `Ctrl/Cmd + Y` 重做。</span> <span>`Ctrl/Cmd + C / V` 复制粘贴,按住空格可临时拖动画布,`Shift + 点击` 可增减单个选中。</span> <span>`阵列` 工具: 先选中一个货架 / 轨道模板,再拖一条水平或竖直线自动补齐一排;货架会按 `排-列` 规则继续编号。</span> </div> <div class="status-card"> <strong>当前状态</strong> <span>楼层: {{ currentLev ? currentLev + 'F' : '--' }}</span> <span>指针: {{ pointerStatus }}</span> <span v-if="arrayPreviewCount > 0">阵列预览: 将生成 {{ arrayPreviewCount }} 个</span> <span>未保存: {{ isDirty ? '是' : '否' }}</span> </div> <div class="note-card"> <strong>运行边界</strong> <span>画布里是自由拖拉拽,但运行侧仍只接受轴对齐矩形元素。保存时会编译回当前运行地图,不支持斜线、旋转和任意多边形。</span> </div> </div> </div> </aside> <aside class="overlay-panel overlay-right" :class="{ collapsed: inspectorPanelCollapsed }"> <div class="panel-head"> <h2>属性面板</h2> <div class="panel-head-actions"> <span class="canvas-meta" v-if="singleSelectedElement">{{ singleSelectedElement.type }}</span> <span class="canvas-meta" v-else>{{ selectedIds.length > 1 ? '多选' : '画布' }}</span> <button type="button" class="panel-toggle" @click="toggleInspectorPanel">{{ inspectorPanelCollapsed ? '<' : '>' }}</button> </div> </div> <div class="panel-body"> <div class="selection-summary"> <strong v-if="singleSelectedElement">单元素编辑</strong> <strong v-else-if="selectedIds.length > 1">多选编辑</strong> <strong v-else>未选中元素</strong> <span v-if="singleSelectedElement">位置 {{ formatNumber(singleSelectedElement.x) }}, {{ formatNumber(singleSelectedElement.y) }} | 尺寸 {{ formatNumber(singleSelectedElement.width) }} x {{ formatNumber(singleSelectedElement.height) }}</span> <span v-else-if="selectedIds.length > 1">当前已选 {{ selectedIds.length }} 个元素,可整体移动、复制或删除。</span> <span v-else-if="activeTool === 'array'">先选中一个货架 / 轨道元素,再拖一条线生成阵列。</span> <span v-else>选择工具后点击元素,或切换框选工具框出一组元素。</span> </div> <div class="tool-section"> <div class="tool-section-label">画布设置</div> <div class="prop-grid"> <el-input v-model.trim="canvasForm.width" size="small" placeholder="画布宽度"></el-input> <el-input v-model.trim="canvasForm.height" size="small" placeholder="画布高度"></el-input> <el-button class="span-2" size="small" plain @click="applyCanvasSize">应用画布尺寸</el-button> </div> </div> <template v-if="singleSelectedElement"> <div class="tool-section"> <div class="tool-section-label">几何属性</div> <div class="prop-grid"> <el-input size="small" :value="singleSelectedElement.type" disabled></el-input> <el-input size="small" :value="singleSelectedElement.id" disabled></el-input> <el-input v-model.trim="geometryForm.x" size="small" placeholder="X"></el-input> <el-input v-model.trim="geometryForm.y" size="small" placeholder="Y"></el-input> <el-input v-model.trim="geometryForm.width" size="small" placeholder="宽度"></el-input> <el-input v-model.trim="geometryForm.height" size="small" placeholder="高度"></el-input> <el-button class="span-2" size="small" plain @click="applyGeometry">应用几何</el-button> </div> </div> <div v-if="singleSelectedElement.type === 'devp'" class="tool-section"> <div class="tool-section-label">输送站点配置</div> <div class="prop-grid"> <div class="field-stack"> <span class="field-label">站号</span> <el-input v-model.trim="devpForm.stationId" size="small" placeholder="请输入输送站点站号"></el-input> </div> <div class="field-stack"> <span class="field-label">PLC 编号</span> <el-input v-model.trim="devpForm.deviceNo" size="small" placeholder="请输入输送站点 PLC 编号"></el-input> </div> <div class="field-stack span-2"> <span class="field-label">方向</span> <div class="direction-grid"> <button v-for="item in devpDirectionOptions" :key="item.key" type="button" class="direction-chip" :class="{ active: isDevpDirectionActive(item.key) }" @click="toggleDevpDirection(item.key)"> <span class="direction-arrow">{{ item.arrow }}</span> <span>{{ item.label }}</span> </button> </div> <div class="field-help">点击箭头切换方向,可同时选择多个方向。</div> </div> <div class="field-stack span-2"> <span class="field-label">站点类型</span> <div class="check-grid"> <el-checkbox v-model="devpForm.isBarcodeStation">条码站</el-checkbox> <el-checkbox v-model="devpForm.isInStation">入站点</el-checkbox> <el-checkbox v-model="devpForm.isOutStation">出站点</el-checkbox> <el-checkbox v-model="devpForm.runBlockReassign">堵塞重分配</el-checkbox> <el-checkbox v-model="devpForm.isOutOrder">出库排序</el-checkbox> <el-checkbox v-model="devpForm.isLiftTransfer">顶升移栽</el-checkbox> </div> </div> <div class="field-stack"> <span class="field-label">条码索引<span v-if="devpRequiresBarcodeIndex" class="field-required"> 必填</span></span> <el-input v-model.trim="devpForm.barcodeIdx" size="small" placeholder="条码站时必填,例如 1"></el-input> </div> <div class="field-stack"> <span class="field-label">条码站站号<span v-if="devpRequiresBarcodeLink" class="field-required"> 必填</span></span> <el-input v-model.trim="devpForm.barcodeStation" size="small" placeholder="入站点时必填,填写条码站站号"></el-input> </div> <div class="field-stack"> <span class="field-label">条码站 PLC 编号<span v-if="devpRequiresBarcodeLink" class="field-required"> 必填</span></span> <el-input v-model.trim="devpForm.barcodeStationDeviceNo" size="small" placeholder="入站点时必填,填写条码站 PLC 编号"></el-input> </div> <div class="field-stack"> <span class="field-label">退回站站号<span v-if="devpRequiresBackStation" class="field-required"> 必填</span></span> <el-input v-model.trim="devpForm.backStation" size="small" placeholder="条码站时必填,填写退回站站号"></el-input> </div> <div class="field-stack"> <span class="field-label">退回站 PLC 编号<span v-if="devpRequiresBackStation" class="field-required"> 必填</span></span> <el-input v-model.trim="devpForm.backStationDeviceNo" size="small" placeholder="条码站时必填,填写退回站 PLC 编号"></el-input> </div> <div class="footer-note span-2"> 勾选“入站点”后,必须填写条码站站号和条码站 PLC 编号。 勾选“条码站”后,必须填写条码索引、退回站站号和退回站 PLC 编号。 </div> <el-button class="span-2" size="small" type="primary" plain @click="applyDevpForm">应用输送线配置</el-button> </div> </div> <div v-if="singleSelectedDeviceElement" class="tool-section"> <div class="tool-section-label">{{ getDeviceConfigLabel(singleSelectedDeviceElement.type) }}</div> <div class="prop-grid"> <el-input size="small" :value="getDeviceConfigKeyLabel(singleSelectedDeviceElement.type, deviceForm.valueKey)" disabled></el-input> <el-input v-model.trim="deviceForm.deviceNo" size="small" placeholder="设备编号"></el-input> <el-button class="span-2" size="small" type="primary" plain @click="applyDeviceForm">应用设备参数</el-button> </div> <div class="footer-note" style="padding-top: 8px;"> 这里只改设备编号相关键,原始 JSON 里的其他字段会保留;下面仍可直接查看或手工修改 JSON。 </div> </div> <div class="tool-section"> <div class="tool-section-label">{{ singleSelectedElement.type === 'devp' ? '原始 JSON 预览' : (singleSelectedDeviceElement ? '原始 JSON 预览 / 手工编辑' : '值 / JSON 编辑') }}</div> <el-input class="json-box" type="textarea" :rows="8" v-model="valueEditorText" :readonly="singleSelectedElement.type === 'devp'"> </el-input> <el-button v-if="singleSelectedElement.type !== 'devp'" size="small" type="primary" plain @click="applyRawValue" >应用值</el-button> </div> </template> <div v-if="selectedShelfElements.length > 0" class="tool-section"> <div class="tool-section-label">货架自动填充</div> <div class="prop-grid"> <el-input v-model.trim="shelfFillForm.startValue" size="small" placeholder="起始值,例如 12-1"></el-input> <el-input size="small" :value="'已选货架 ' + selectedShelfElements.length + ' 个'" disabled></el-input> <el-select v-model="shelfFillForm.rowStep" size="small" placeholder="排方向"> <el-option label="上到下递减" value="desc"></el-option> <el-option label="上到下递增" value="asc"></el-option> </el-select> <el-select v-model="shelfFillForm.colStep" size="small" placeholder="列方向"> <el-option label="左到右递增" value="asc"></el-option> <el-option label="左到右递减" value="desc"></el-option> </el-select> <el-button class="span-2" size="small" type="primary" plain @click="applyShelfAutoFill">按排列填充货架值</el-button> </div> <div class="footer-note" style="padding-top: 8px;"> 会按选中货架的实际空间排列分组填充。默认规则是上到下排号递减、左到右列号递增。 </div> </div> <div class="footer-note"> 编辑器只负责自由画布编辑,运行地图继续走当前 `BasMap.data`。所以这里允许自由拖拉矩形元素,但保存前会校验重叠、尺寸越界和 `devp` 必填字段,防止影响现有显示和算法。 </div> </div> </aside> </div> </div> </main> </section> <el-dialog title="新建自由画布" :visible.sync="blankDialogVisible" width="420px" class="dialog-panel" append-to-body> <el-form label-width="90px" size="small"> <el-form-item label="楼层"> <el-input v-model.trim="blankForm.lev"></el-input> </el-form-item> <el-form-item label="宽度"> <el-input v-model.trim="blankForm.width"></el-input> </el-form-item> <el-form-item label="高度"> <el-input v-model.trim="blankForm.height"></el-input> </el-form-item> </el-form> <div slot="footer"> <el-button @click="blankDialogVisible = false">取消</el-button> <el-button type="primary" @click="createBlankMap">创建</el-button> </div> </el-dialog> <input ref="importInput" type="file" style="display:none;" @change="handleImportExcel"> <input ref="mapImportInput" type="file" accept=".json,application/json" style="display:none;" @change="handleImportMap"> </div> <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></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/pixi-legacy.min.js"></script> <script type="text/javascript" src="../../static/js/basMap/editor.js?v=20260321c"></script> </body> </html> src/main/webapp/views/watch/console.html
@@ -553,7 +553,7 @@ <script src="../../components/WatchDualCrnCard.js"></script> <script src="../../components/DevpCard.js"></script> <script src="../../components/WatchRgvCard.js"></script> <script src="../../components/MapCanvas.js?v=20260319_fake_trace_overlay1"></script> <script src="../../components/MapCanvas.js?v=20260320_crn_size_fix1"></script> <script> let ws; var app = new Vue({ src/main/webapp/views/watch/fakeTrace.html
@@ -517,7 +517,7 @@ </div> </div> <script src="../../components/MapCanvas.js?v=20260319_fake_trace_overlay1"></script> <script src="../../components/MapCanvas.js?v=20260320_crn_size_fix1"></script> <script> var fakeTraceWs = null;