From 614c94e0b079b4d51e96bc02add92b04100bd07b Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期六, 21 三月 2026 09:12:23 +0800
Subject: [PATCH] #
---
src/main/webapp/components/MapCanvas.js | 34
src/main/webapp/views/watch/console.html | 2
src/main/webapp/views/watch/fakeTrace.html | 2
src/main/java/com/zy/asrs/domain/BasMapEditorElement.java | 25
src/main/java/com/zy/asrs/controller/BasMapController.java | 290 --
src/main/webapp/static/js/basMap/basMap.js | 24
src/main/java/com/zy/asrs/service/impl/BasMapEditorServiceImpl.java | 920 ++++++++
src/main/webapp/static/js/basMap/editor.js | 4051 ++++++++++++++++++++++++++++++++++++++
src/main/java/com/zy/asrs/domain/BasMapEditorCell.java | 21
src/main/webapp/views/basMap/basMap.html | 10
src/main/java/com/zy/asrs/service/BasMapEditorService.java | 17
src/main/java/com/zy/asrs/domain/BasMapEditorDoc.java | 29
src/main/webapp/views/basMap/editor.html | 814 +++++++
13 files changed, 5,996 insertions(+), 243 deletions(-)
diff --git a/src/main/java/com/zy/asrs/controller/BasMapController.java b/src/main/java/com/zy/asrs/controller/BasMapController.java
index 1d8b4b2..a26a4b3 100644
--- a/src/main/java/com/zy/asrs/controller/BasMapController.java
+++ b/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();
+ }
+ }
+
}
diff --git a/src/main/java/com/zy/asrs/domain/BasMapEditorCell.java b/src/main/java/com/zy/asrs/domain/BasMapEditorCell.java
new file mode 100644
index 0000000..7af5df1
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/BasMapEditorCell.java
@@ -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;
+}
diff --git a/src/main/java/com/zy/asrs/domain/BasMapEditorDoc.java b/src/main/java/com/zy/asrs/domain/BasMapEditorDoc.java
new file mode 100644
index 0000000..6a80a67
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/BasMapEditorDoc.java
@@ -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<>();
+}
diff --git a/src/main/java/com/zy/asrs/domain/BasMapEditorElement.java b/src/main/java/com/zy/asrs/domain/BasMapEditorElement.java
new file mode 100644
index 0000000..a2c25fa
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/BasMapEditorElement.java
@@ -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;
+}
diff --git a/src/main/java/com/zy/asrs/service/BasMapEditorService.java b/src/main/java/com/zy/asrs/service/BasMapEditorService.java
new file mode 100644
index 0000000..e98b4fc
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/BasMapEditorService.java
@@ -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);
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/BasMapEditorServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/BasMapEditorServiceImpl.java
new file mode 100644
index 0000000..fe0f13a
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/BasMapEditorServiceImpl.java
@@ -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鍜宒eviceNo: 鍏冪礌#" + 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("鍏ョ珯鐐瑰繀椤诲寘鍚玝arcodeStation鍜宐arcodeStationDeviceNo: 鍏冪礌#" + elementIndex);
+ }
+ }
+ if (isBarcodeStation != null && isBarcodeStation == 1) {
+ if (!isPositiveInteger(barcodeIdx) || !isPositiveInteger(backStation) || !isPositiveInteger(backStationDeviceNo)) {
+ throw new CoolException("鏉$爜绔欏繀椤诲寘鍚玝arcodeIdx銆乥ackStation鍜宐ackStationDeviceNo: 鍏冪礌#" + 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<>();
+ }
+}
diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index 4c2292e..a4c0a7d 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/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 @@
}
}
});
-
diff --git a/src/main/webapp/static/js/basMap/basMap.js b/src/main/webapp/static/js/basMap/basMap.js
index bf30ac2..6cada5b 100644
--- a/src/main/webapp/static/js/basMap/basMap.js
+++ b/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('璇疯緭鍏ュ垵濮嬪寲搴撲綅灞傛暟', '鍒濆鍖栧簱浣�', {
diff --git a/src/main/webapp/static/js/basMap/editor.js b/src/main/webapp/static/js/basMap/editor.js
new file mode 100644
index 0000000..3d28b60
--- /dev/null
+++ b/src/main/webapp/static/js/basMap/editor.js
@@ -0,0 +1,4051 @@
+(function () {
+ var FREE_EDITOR_MODE = 'free-v1';
+ var MAP_TRANSFER_FORMAT = 'bas-map-editor-transfer-v2';
+ var HISTORY_LIMIT = 60;
+ var DEFAULT_CANVAS_WIDTH = 6400;
+ var DEFAULT_CANVAS_HEIGHT = 3600;
+ var MIN_ELEMENT_SIZE = 24;
+ var HANDLE_SCREEN_SIZE = 10;
+ var DRAG_START_THRESHOLD = 5;
+ var EDGE_SNAP_SCREEN_TOLERANCE = 8;
+ var COORD_EPSILON = 0.01;
+ var DEFERRED_STATIC_REBUILD_DELAY = 120;
+ var PAN_LABEL_REFRESH_DELAY = 160;
+ var ZOOM_REFRESH_DELAY = 220;
+ var POINTER_STATUS_UPDATE_INTERVAL = 48;
+ var SPATIAL_BUCKET_SIZE = 240;
+ var STATIC_VIEW_PADDING = 120;
+ var MIN_LABEL_SCALE = 0.17;
+ var ABS_MIN_LABEL_SCREEN_WIDTH = 26;
+ var ABS_MIN_LABEL_SCREEN_HEIGHT = 14;
+ var STATIC_SPRITE_SCALE_THRESHOLD = 0.85;
+ var STATIC_SIMPLIFY_SCALE_THRESHOLD = 0.22;
+ var DENSE_SIMPLIFY_SCALE_THRESHOLD = 0.8;
+ var DENSE_SIMPLIFY_ELEMENT_THRESHOLD = 1200;
+ var DENSE_LABEL_HIDE_SCALE_THRESHOLD = 1.05;
+ var DENSE_LABEL_HIDE_ELEMENT_THRESHOLD = 1200;
+ var STATIC_SPRITE_POOL_SLACK = 96;
+ var MIN_LABEL_COUNT = 180;
+ var MAX_LABEL_COUNT = 360;
+ var DRAW_TYPES = ['shelf', 'devp', 'crn', 'dualCrn', 'rgv'];
+ var ARRAY_TEMPLATE_TYPES = ['shelf', 'crn', 'dualCrn', 'rgv'];
+ var DEVICE_CONFIG_TYPES = ['crn', 'dualCrn', 'rgv'];
+ var DEVP_DIRECTION_OPTIONS = [
+ { key: 'top', label: '涓�', arrow: '鈫�' },
+ { key: 'right', label: '鍙�', arrow: '鈫�' },
+ { key: 'bottom', label: '涓�', arrow: '鈫�' },
+ { key: 'left', label: '宸�', arrow: '鈫�' }
+ ];
+ var idSeed = Date.now();
+
+ var TYPE_META = {
+ shelf: { label: '璐ф灦', shortLabel: 'SHELF', fill: 0x7d96bf, border: 0x4f6486 },
+ devp: { label: '杈撻�佺嚎', shortLabel: 'DEVP', fill: 0xf0b06f, border: 0xa45f21 },
+ crn: { label: '鍫嗗灈鏈鸿建閬�', shortLabel: 'CRN', fill: 0x68bfd0, border: 0x1d6e81 },
+ dualCrn: { label: '鍙屽伐浣嶈建閬�', shortLabel: 'DCRN', fill: 0x54c1a4, border: 0x0f7b62 },
+ rgv: { label: 'RGV杞ㄩ亾', shortLabel: 'RGV', fill: 0xc691e9, border: 0x744b98 }
+ };
+
+ function nextId() {
+ idSeed += 1;
+ return 'el_' + idSeed;
+ }
+
+ function deepClone(obj) {
+ return JSON.parse(JSON.stringify(obj == null ? null : obj));
+ }
+
+ function padNumber(value) {
+ return value < 10 ? ('0' + value) : String(value);
+ }
+
+ function authHeaders() {
+ return {
+ token: localStorage.getItem('token')
+ };
+ }
+
+ function getQueryParam(name) {
+ var search = window.location.search || '';
+ if (!search) {
+ return '';
+ }
+ var target = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ var match = search.match(new RegExp('(?:[?&])' + target + '=([^&]*)'));
+ return match ? decodeURIComponent(match[1]) : '';
+ }
+
+ function toNumber(value, defaultValue) {
+ if (value === null || value === undefined || value === '') {
+ return defaultValue;
+ }
+ var parsed = Number(value);
+ return isFinite(parsed) ? parsed : defaultValue;
+ }
+
+ function toInt(value, defaultValue) {
+ return Math.round(toNumber(value, defaultValue));
+ }
+
+ function clamp(value, min, max) {
+ return Math.max(min, Math.min(max, value));
+ }
+
+ function roundCoord(value) {
+ return Math.round(value * 1000) / 1000;
+ }
+
+ function normalizeValue(value) {
+ if (value === null || value === undefined) {
+ return '';
+ }
+ return typeof value === 'string' ? value : JSON.stringify(value);
+ }
+
+ function parseShelfLocationValue(value) {
+ var text = normalizeValue(value).trim();
+ var matched = text.match(/^(-?\d+)\s*-\s*(-?\d+)$/);
+ if (!matched) {
+ return null;
+ }
+ return {
+ row: toInt(matched[1], 0),
+ col: toInt(matched[2], 0)
+ };
+ }
+
+ function formatShelfLocationValue(row, col) {
+ return String(toInt(row, 0)) + '-' + String(toInt(col, 0));
+ }
+
+ function safeParseJson(text) {
+ if (!text || typeof text !== 'string') {
+ return null;
+ }
+ try {
+ return JSON.parse(text);
+ } catch (e) {
+ return null;
+ }
+ }
+
+ function boolFlag(value) {
+ return value === true || value === 1 || value === '1';
+ }
+
+ function normalizeDirectionList(direction) {
+ var list = Array.isArray(direction) ? direction : String(direction || '').split(/[,\s|/]+/);
+ var result = [];
+ var seen = {};
+ for (var i = 0; i < list.length; i++) {
+ var item = String(list[i] || '').trim().toLowerCase();
+ if (!item || seen[item]) {
+ continue;
+ }
+ seen[item] = true;
+ result.push(item);
+ }
+ return result;
+ }
+
+ function directionTokenToArrow(token) {
+ if (token === 'top' || token === 'up' || token === 'north' || token === 'n') {
+ return '鈫�';
+ }
+ if (token === 'right' || token === 'east' || token === 'e') {
+ return '鈫�';
+ }
+ if (token === 'bottom' || token === 'down' || token === 'south' || token === 's') {
+ return '鈫�';
+ }
+ if (token === 'left' || token === 'west' || token === 'w') {
+ return '鈫�';
+ }
+ return '';
+ }
+
+ function formatDirectionArrows(direction) {
+ var list = normalizeDirectionList(direction);
+ var arrows = [];
+ for (var i = 0; i < list.length; i++) {
+ var arrow = directionTokenToArrow(list[i]);
+ if (arrow) {
+ arrows.push(arrow);
+ }
+ }
+ return arrows.join('');
+ }
+
+ function isDeviceConfigType(type) {
+ return DEVICE_CONFIG_TYPES.indexOf(type) >= 0;
+ }
+
+ function pickDeviceValueKey(type, json) {
+ if (json && json.deviceNo != null) {
+ return 'deviceNo';
+ }
+ if ((type === 'crn' || type === 'dualCrn') && json && json.crnNo != null) {
+ return 'crnNo';
+ }
+ if (type === 'rgv' && json && json.rgvNo != null) {
+ return 'rgvNo';
+ }
+ return 'deviceNo';
+ }
+
+ function isInputLike(target) {
+ if (!target || !target.tagName) {
+ return false;
+ }
+ var tag = String(target.tagName || '').toLowerCase();
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || !!target.isContentEditable;
+ }
+
+ function rectsOverlap(a, b) {
+ return a.x < b.x + b.width - COORD_EPSILON && a.x + a.width > b.x + COORD_EPSILON
+ && a.y < b.y + b.height - COORD_EPSILON && a.y + a.height > b.y + COORD_EPSILON;
+ }
+
+ function rectIntersects(a, b) {
+ return a.x <= b.x + b.width && a.x + a.width >= b.x
+ && a.y <= b.y + b.height && a.y + a.height >= b.y;
+ }
+
+ function isRectWithinCanvas(rect, canvasWidth, canvasHeight) {
+ return rect.x >= -COORD_EPSILON && rect.y >= -COORD_EPSILON
+ && rect.x + rect.width <= canvasWidth + COORD_EPSILON
+ && rect.y + rect.height <= canvasHeight + COORD_EPSILON;
+ }
+
+ function findDocOverlapId(doc) {
+ if (!doc || !doc.elements || !doc.elements.length) {
+ return '';
+ }
+ var buckets = {};
+ var elements = doc.elements;
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE);
+ var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE);
+ var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE);
+ var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE);
+ for (var bx = minX; bx <= maxX; bx++) {
+ for (var by = minY; by <= maxY; by++) {
+ var key = bucketKey(bx, by);
+ var bucket = buckets[key];
+ if (!bucket || !bucket.length) {
+ continue;
+ }
+ for (var j = 0; j < bucket.length; j++) {
+ if (rectsOverlap(element, bucket[j])) {
+ return element.id || ('el_' + i);
+ }
+ }
+ }
+ }
+ for (bx = minX; bx <= maxX; bx++) {
+ for (by = minY; by <= maxY; by++) {
+ key = bucketKey(bx, by);
+ if (!buckets[key]) {
+ buckets[key] = [];
+ }
+ buckets[key].push(element);
+ }
+ }
+ }
+ return '';
+ }
+
+ function buildRectFromPoints(a, b) {
+ var left = Math.min(a.x, b.x);
+ var top = Math.min(a.y, b.y);
+ var right = Math.max(a.x, b.x);
+ var bottom = Math.max(a.y, b.y);
+ return {
+ x: roundCoord(left),
+ y: roundCoord(top),
+ width: roundCoord(right - left),
+ height: roundCoord(bottom - top)
+ };
+ }
+
+ function getTypeMeta(type) {
+ return TYPE_META[type] || TYPE_META.shelf;
+ }
+
+ function rangesNearOrOverlap(a1, a2, b1, b2, tolerance) {
+ return a1 <= b2 + tolerance && a2 >= b1 - tolerance;
+ }
+
+ function bucketKey(x, y) {
+ return x + ':' + y;
+ }
+
+ function getPreferredResolution() {
+ return Math.min(window.devicePixelRatio || 1, 1.25);
+ }
+
+ new Vue({
+ el: '#app',
+ data: function () {
+ return {
+ remoteLevOptions: [],
+ levOptions: [],
+ currentLev: null,
+ floorPickerLev: null,
+ draftDocs: {},
+ doc: null,
+ activeTool: 'select',
+ toolPanelCollapsed: false,
+ inspectorPanelCollapsed: false,
+ interactionTools: [
+ { key: 'select', label: '閫夋嫨 / 绉诲姩', desc: '鐐瑰嚮鍏冪礌閫夋嫨锛屾嫋鎷界Щ鍔紝绌虹櫧澶勬嫋鍔ㄧ敾甯�' },
+ { key: 'marquee', label: '妗嗛��', desc: '鍦ㄧ敾甯冧腑妗嗛�変竴缁勫厓绱�' },
+ { key: 'array', label: '闃靛垪', desc: '閫変腑涓�涓揣鏋� / 杞ㄩ亾鍚庢嫋涓�鏉$嚎鑷姩鐢熸垚涓�鎺�' },
+ { key: 'pan', label: '骞崇Щ', desc: '涓撻棬鐢ㄤ簬鎷栧姩鐢诲竷鍜岃瀵熷叏鍥�' }
+ ],
+ drawTools: [
+ { key: 'shelf', label: '璐ф灦', desc: '鑷敱鎷夊嚭璐ф灦鐭╁舰' },
+ { key: 'devp', label: '杈撻�佺嚎', desc: '鎷夊嚭绔欑偣 / 杈撻�佺嚎鐭╁舰' },
+ { key: 'crn', label: 'CRN', desc: '鎷夊嚭鍫嗗灈鏈鸿建閬撶煩褰�' },
+ { key: 'dualCrn', label: '鍙屽伐浣�', desc: '鎷夊嚭鍙屽伐浣嶈建閬撶煩褰�' },
+ { key: 'rgv', label: 'RGV', desc: '鎷夊嚭 RGV 杞ㄩ亾鐭╁舰' }
+ ],
+ pixiApp: null,
+ mapRoot: null,
+ gridLayer: null,
+ trackLayer: null,
+ nodeLayer: null,
+ patchObjectLayer: null,
+ activeLayer: null,
+ labelLayer: null,
+ selectionLayer: null,
+ guideLayer: null,
+ guideText: null,
+ hoverLayer: null,
+ labelPool: [],
+ renderQueued: false,
+ gridSceneDirty: true,
+ staticSceneDirty: true,
+ spatialIndexDirty: true,
+ spatialBuckets: null,
+ gridRenderRect: null,
+ gridRenderKey: '',
+ staticRenderRect: null,
+ staticRenderKey: '',
+ staticExcludedKey: '',
+ camera: {
+ x: 80,
+ y: 80,
+ scale: 1
+ },
+ viewZoom: 1,
+ selectedIds: [],
+ clipboard: [],
+ hoverElementId: '',
+ pointerStatus: '--',
+ lastPointerStatusUpdateTs: 0,
+ pixiResolution: getPreferredResolution(),
+ fpsValue: 0,
+ fpsFrameCount: 0,
+ fpsSampleStartTs: 0,
+ fpsTickerHandler: null,
+ interactionState: null,
+ isZooming: false,
+ isPanning: false,
+ zoomRefreshTimer: null,
+ panRefreshTimer: null,
+ pendingViewportRefresh: false,
+ pendingStaticCommit: null,
+ deferredStaticRebuildTimer: null,
+ currentPointerId: null,
+ boundCanvasHandlers: null,
+ boundWindowHandlers: null,
+ resizeObserver: null,
+ labelCapability: {
+ maxWidth: 0,
+ maxHeight: 0
+ },
+ labelCapabilityDirty: true,
+ undoStack: [],
+ redoStack: [],
+ savedSnapshot: '',
+ isDirty: false,
+ saving: false,
+ savingAll: false,
+ loadingFloor: false,
+ switchingFloorLev: null,
+ floorRequestSeq: 0,
+ activeFloorRequestSeq: 0,
+ blankDialogVisible: false,
+ blankForm: {
+ lev: '',
+ width: String(DEFAULT_CANVAS_WIDTH),
+ height: String(DEFAULT_CANVAS_HEIGHT)
+ },
+ canvasForm: {
+ width: String(DEFAULT_CANVAS_WIDTH),
+ height: String(DEFAULT_CANVAS_HEIGHT)
+ },
+ geometryForm: {
+ x: '',
+ y: '',
+ width: '',
+ height: ''
+ },
+ devpForm: {
+ stationId: '',
+ deviceNo: '',
+ direction: [],
+ isBarcodeStation: false,
+ barcodeIdx: '',
+ backStation: '',
+ backStationDeviceNo: '',
+ isInStation: false,
+ barcodeStation: '',
+ barcodeStationDeviceNo: '',
+ isOutStation: false,
+ runBlockReassign: false,
+ isOutOrder: false,
+ isLiftTransfer: false
+ },
+ deviceForm: {
+ valueKey: '',
+ deviceNo: ''
+ },
+ devpDirectionOptions: DEVP_DIRECTION_OPTIONS,
+ shelfFillForm: {
+ startValue: '',
+ rowStep: 'desc',
+ colStep: 'asc'
+ },
+ valueEditorText: '',
+ spacePressed: false,
+ lastCursor: 'default'
+ };
+ },
+ computed: {
+ singleSelectedElement: function () {
+ if (!this.doc || this.selectedIds.length !== 1) {
+ return null;
+ }
+ return this.findElementById(this.selectedIds[0]);
+ },
+ singleSelectedDeviceElement: function () {
+ if (!this.singleSelectedElement || !isDeviceConfigType(this.singleSelectedElement.type)) {
+ return null;
+ }
+ return this.singleSelectedElement;
+ },
+ selectedShelfElements: function () {
+ if (!this.doc || !this.selectedIds.length) {
+ return [];
+ }
+ return this.getSelectedElements().filter(function (item) {
+ return item && item.type === 'shelf';
+ });
+ },
+ devpRequiresBarcodeLink: function () {
+ return !!(this.devpForm && this.devpForm.isInStation);
+ },
+ devpRequiresBarcodeIndex: function () {
+ return !!(this.devpForm && this.devpForm.isBarcodeStation);
+ },
+ devpRequiresBackStation: function () {
+ return !!(this.devpForm && this.devpForm.isBarcodeStation);
+ },
+ arrayPreviewCount: function () {
+ if (!this.interactionState || this.interactionState.type !== 'array') {
+ return 0;
+ }
+ return this.interactionState.previewItems ? this.interactionState.previewItems.length : 0;
+ },
+ viewPercent: function () {
+ return Math.round(this.viewZoom * 100);
+ },
+ fpsText: function () {
+ return this.fpsValue > 0 ? String(this.fpsValue) : '--';
+ },
+ dirtyDraftLevs: function () {
+ var result = [];
+ var seen = {};
+ if (this.doc && this.doc.lev && this.isDirty) {
+ var currentLev = toInt(this.doc.lev, 0);
+ if (currentLev > 0) {
+ seen[currentLev] = true;
+ result.push(currentLev);
+ }
+ }
+ var self = this;
+ Object.keys(this.draftDocs || {}).forEach(function (key) {
+ var lev = toInt(key, 0);
+ if (lev <= 0 || seen[lev]) {
+ return;
+ }
+ if (self.hasDirtyDraft(lev)) {
+ seen[lev] = true;
+ result.push(lev);
+ }
+ });
+ result.sort(function (a, b) { return a - b; });
+ return result;
+ },
+ dirtyDraftCount: function () {
+ return this.dirtyDraftLevs.length;
+ }
+ },
+ mounted: function () {
+ this.initPixi();
+ this.attachEvents();
+ this.loadLevOptions();
+ var lev = toInt(getQueryParam('lev'), 0);
+ if (lev > 0) {
+ this.floorPickerLev = lev;
+ this.fetchFloor(lev);
+ } else {
+ this.createLocalBlankDoc(1, DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT, '');
+ }
+ },
+ beforeDestroy: function () {
+ this.detachEvents();
+ if (this.zoomRefreshTimer) {
+ window.clearTimeout(this.zoomRefreshTimer);
+ this.zoomRefreshTimer = null;
+ }
+ if (this.panRefreshTimer) {
+ window.clearTimeout(this.panRefreshTimer);
+ this.panRefreshTimer = null;
+ }
+ this.clearDeferredStaticCommit();
+ this.stopFpsTicker();
+ if (this.pixiApp) {
+ this.pixiApp.destroy(true, { children: true });
+ this.pixiApp = null;
+ }
+ },
+ methods: {
+ showMessage: function (type, message) {
+ if (this.$message) {
+ this.$message({ type: type, message: message });
+ }
+ },
+ formatNumber: function (value) {
+ var num = toNumber(value, 0);
+ if (Math.abs(num) >= 1000 || num === Math.round(num)) {
+ return String(Math.round(num));
+ }
+ return String(Math.round(num * 100) / 100);
+ },
+ syncFloorQueryParam: function (lev) {
+ lev = toInt(lev, 0);
+ if (lev <= 0 || !window.history || !window.history.replaceState || !window.URL) {
+ return;
+ }
+ try {
+ var url = new URL(window.location.href);
+ url.searchParams.set('lev', String(lev));
+ window.history.replaceState(null, '', url.toString());
+ } catch (e) {
+ // Ignore URL sync failures and keep editor usable.
+ }
+ },
+ toolLabel: function (tool) {
+ var list = this.interactionTools.concat(this.drawTools);
+ for (var i = 0; i < list.length; i++) {
+ if (list[i].key === tool) {
+ return list[i].label;
+ }
+ }
+ return tool || '--';
+ },
+ toggleToolPanel: function () {
+ this.toolPanelCollapsed = !this.toolPanelCollapsed;
+ },
+ toggleInspectorPanel: function () {
+ this.inspectorPanelCollapsed = !this.inspectorPanelCollapsed;
+ },
+ startFpsTicker: function () {
+ if (!this.pixiApp || !this.pixiApp.ticker || this.fpsTickerHandler) {
+ return;
+ }
+ var self = this;
+ this.fpsValue = 0;
+ this.fpsFrameCount = 0;
+ this.fpsSampleStartTs = (window.performance && performance.now) ? performance.now() : Date.now();
+ this.fpsTickerHandler = function () {
+ var now = (window.performance && performance.now) ? performance.now() : Date.now();
+ self.fpsFrameCount += 1;
+ var elapsed = now - self.fpsSampleStartTs;
+ if (elapsed < 400) {
+ return;
+ }
+ self.fpsValue = Math.max(0, Math.round(self.fpsFrameCount * 1000 / elapsed));
+ self.fpsFrameCount = 0;
+ self.fpsSampleStartTs = now;
+ };
+ this.pixiApp.ticker.add(this.fpsTickerHandler);
+ },
+ stopFpsTicker: function () {
+ if (this.pixiApp && this.pixiApp.ticker && this.fpsTickerHandler) {
+ this.pixiApp.ticker.remove(this.fpsTickerHandler);
+ }
+ this.fpsTickerHandler = null;
+ this.fpsFrameCount = 0;
+ this.fpsSampleStartTs = 0;
+ },
+ initPixi: function () {
+ var host = this.$refs.canvasHost;
+ if (!host) {
+ return;
+ }
+ var resolution = getPreferredResolution();
+ this.pixiResolution = resolution;
+ var app = new PIXI.Application({
+ width: Math.max(host.clientWidth, 320),
+ height: Math.max(host.clientHeight, 320),
+ antialias: false,
+ autoDensity: true,
+ backgroundAlpha: 1,
+ backgroundColor: 0xf6f9fc,
+ resolution: resolution,
+ powerPreference: 'high-performance'
+ });
+ host.innerHTML = '';
+ host.appendChild(app.view);
+ app.view.style.width = '100%';
+ app.view.style.height = '100%';
+ app.view.style.touchAction = 'none';
+ app.view.style.background = '#f6f9fc';
+ app.renderer.roundPixels = true;
+
+ this.pixiApp = app;
+ this.mapRoot = new PIXI.Container();
+ app.stage.addChild(this.mapRoot);
+
+ this.gridLayer = new PIXI.Graphics();
+ this.staticLayer = new PIXI.Container();
+ this.staticTrackSpriteLayer = null;
+ this.staticNodeSpriteLayer = null;
+ this.trackLayer = new PIXI.Graphics();
+ this.nodeLayer = new PIXI.Graphics();
+ this.eraseLayer = new PIXI.Graphics();
+ this.patchObjectLayer = new PIXI.Graphics();
+ this.activeLayer = new PIXI.Graphics();
+ this.labelLayer = new PIXI.Container();
+ this.selectionLayer = new PIXI.Graphics();
+ this.guideLayer = new PIXI.Graphics();
+ this.guideText = new PIXI.Text('', {
+ fontFamily: 'PingFang SC, Microsoft YaHei, sans-serif',
+ fontSize: 14,
+ fontWeight: '700',
+ fill: 0x1f4f86,
+ stroke: 0xffffff,
+ strokeThickness: 4,
+ lineJoin: 'round'
+ });
+ this.guideText.anchor.set(0.5, 1);
+ this.guideText.visible = false;
+ this.hoverLayer = new PIXI.Graphics();
+ this.staticTrackSpritePool = [];
+ this.staticNodeSpritePool = [];
+
+ this.mapRoot.addChild(this.gridLayer);
+ this.staticTrackSpriteLayer = new PIXI.ParticleContainer(12000, {
+ position: true,
+ scale: true,
+ alpha: true,
+ tint: true
+ }, 16384, true);
+ this.staticNodeSpriteLayer = new PIXI.ParticleContainer(12000, {
+ position: true,
+ scale: true,
+ alpha: true,
+ tint: true
+ }, 16384, true);
+ this.staticLayer.addChild(this.staticTrackSpriteLayer);
+ this.staticLayer.addChild(this.trackLayer);
+ this.staticLayer.addChild(this.staticNodeSpriteLayer);
+ this.staticLayer.addChild(this.nodeLayer);
+ this.mapRoot.addChild(this.staticLayer);
+ this.mapRoot.addChild(this.eraseLayer);
+ this.mapRoot.addChild(this.patchObjectLayer);
+ this.mapRoot.addChild(this.activeLayer);
+ this.mapRoot.addChild(this.labelLayer);
+ this.mapRoot.addChild(this.hoverLayer);
+ this.mapRoot.addChild(this.selectionLayer);
+ this.mapRoot.addChild(this.guideLayer);
+ this.mapRoot.addChild(this.guideText);
+
+ this.boundCanvasHandlers = {
+ pointerdown: this.onCanvasPointerDown.bind(this),
+ wheel: this.onCanvasWheel.bind(this)
+ };
+ app.view.addEventListener('pointerdown', this.boundCanvasHandlers.pointerdown);
+ app.view.addEventListener('wheel', this.boundCanvasHandlers.wheel, { passive: false });
+
+ if (window.ResizeObserver) {
+ this.resizeObserver = new ResizeObserver(this.handleResize.bind(this));
+ this.resizeObserver.observe(host);
+ }
+ this.startFpsTicker();
+ this.handleResize();
+ },
+ attachEvents: function () {
+ this.boundWindowHandlers = {
+ pointermove: this.onWindowPointerMove.bind(this),
+ pointerup: this.onWindowPointerUp.bind(this),
+ pointercancel: this.onWindowPointerUp.bind(this),
+ keydown: this.onWindowKeyDown.bind(this),
+ keyup: this.onWindowKeyUp.bind(this),
+ beforeunload: this.onBeforeUnload.bind(this),
+ resize: this.handleResize.bind(this)
+ };
+ window.addEventListener('pointermove', this.boundWindowHandlers.pointermove);
+ window.addEventListener('pointerup', this.boundWindowHandlers.pointerup);
+ window.addEventListener('pointercancel', this.boundWindowHandlers.pointercancel);
+ window.addEventListener('keydown', this.boundWindowHandlers.keydown);
+ window.addEventListener('keyup', this.boundWindowHandlers.keyup);
+ window.addEventListener('beforeunload', this.boundWindowHandlers.beforeunload);
+ window.addEventListener('resize', this.boundWindowHandlers.resize);
+ },
+ detachEvents: function () {
+ if (this.pixiApp && this.boundCanvasHandlers) {
+ this.pixiApp.view.removeEventListener('pointerdown', this.boundCanvasHandlers.pointerdown);
+ this.pixiApp.view.removeEventListener('wheel', this.boundCanvasHandlers.wheel);
+ }
+ if (this.resizeObserver) {
+ this.resizeObserver.disconnect();
+ this.resizeObserver = null;
+ }
+ if (!this.boundWindowHandlers) {
+ return;
+ }
+ window.removeEventListener('pointermove', this.boundWindowHandlers.pointermove);
+ window.removeEventListener('pointerup', this.boundWindowHandlers.pointerup);
+ window.removeEventListener('pointercancel', this.boundWindowHandlers.pointercancel);
+ window.removeEventListener('keydown', this.boundWindowHandlers.keydown);
+ window.removeEventListener('keyup', this.boundWindowHandlers.keyup);
+ window.removeEventListener('beforeunload', this.boundWindowHandlers.beforeunload);
+ window.removeEventListener('resize', this.boundWindowHandlers.resize);
+ },
+ handleResize: function () {
+ if (!this.pixiApp || !this.$refs.canvasHost) {
+ return;
+ }
+ var host = this.$refs.canvasHost;
+ var width = Math.max(host.clientWidth, 320);
+ var height = Math.max(host.clientHeight, 320);
+ this.pixiApp.renderer.resize(width, height);
+ this.markGridSceneDirty();
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ },
+ loadLevOptions: function () {
+ var self = this;
+ $.ajax({
+ url: baseUrl + '/basMap/getLevList',
+ method: 'GET',
+ headers: authHeaders(),
+ success: function (res) {
+ if (res && res.code === 200 && Array.isArray(res.data)) {
+ self.remoteLevOptions = res.data.map(function (item) {
+ return toInt(item, 0);
+ }).filter(function (item) {
+ return item > 0;
+ });
+ self.refreshLevOptions();
+ }
+ }
+ });
+ },
+ refreshLevOptions: function () {
+ var set = {};
+ var result = [];
+ var pushLev = function (lev) {
+ lev = toInt(lev, 0);
+ if (lev <= 0 || set[lev]) {
+ return;
+ }
+ set[lev] = true;
+ result.push(lev);
+ };
+ this.remoteLevOptions.forEach(pushLev);
+ Object.keys(this.draftDocs || {}).forEach(pushLev);
+ pushLev(this.currentLev);
+ pushLev(this.floorPickerLev);
+ result.sort(function (a, b) { return a - b; });
+ this.levOptions = result;
+ },
+ exportDoc: function (doc) {
+ var source = doc || this.doc || {};
+ return {
+ lev: toInt(source.lev, 0),
+ editorMode: FREE_EDITOR_MODE,
+ canvasWidth: roundCoord(toNumber(source.canvasWidth, DEFAULT_CANVAS_WIDTH)),
+ canvasHeight: roundCoord(toNumber(source.canvasHeight, DEFAULT_CANVAS_HEIGHT)),
+ elements: (source.elements || []).map(function (item, index) {
+ return {
+ id: item && item.id ? String(item.id) : ('el_' + (index + 1)),
+ type: DRAW_TYPES.indexOf(item && item.type) >= 0 ? item.type : 'shelf',
+ x: roundCoord(Math.max(0, toNumber(item && item.x, 0))),
+ y: roundCoord(Math.max(0, toNumber(item && item.y, 0))),
+ width: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.width, MIN_ELEMENT_SIZE))),
+ height: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(item && item.height, MIN_ELEMENT_SIZE))),
+ value: normalizeValue(item && item.value)
+ };
+ })
+ };
+ },
+ normalizeDoc: function (doc) {
+ var normalized = this.exportDoc(doc || {});
+ if (normalized.lev <= 0) {
+ normalized.lev = toInt(this.currentLev, 1) || 1;
+ }
+ return normalized;
+ },
+ snapshotDoc: function (doc) {
+ return JSON.stringify(this.exportDoc(doc));
+ },
+ syncDirty: function () {
+ var currentSnapshot = this.snapshotDoc(this.doc);
+ this.isDirty = currentSnapshot !== this.savedSnapshot;
+ },
+ setDraftDocEntry: function (lev, doc, savedSnapshot) {
+ lev = toInt(lev, 0);
+ if (lev <= 0 || !doc) {
+ return;
+ }
+ var entry = {
+ doc: this.exportDoc(doc),
+ savedSnapshot: savedSnapshot != null ? savedSnapshot : ''
+ };
+ if (this.$set) {
+ this.$set(this.draftDocs, lev, entry);
+ } else {
+ this.draftDocs[lev] = entry;
+ }
+ },
+ removeDraftDocEntry: function (lev) {
+ lev = toInt(lev, 0);
+ if (lev <= 0) {
+ return;
+ }
+ if (this.$delete) {
+ this.$delete(this.draftDocs, lev);
+ } else {
+ delete this.draftDocs[lev];
+ }
+ },
+ cacheCurrentDraft: function () {
+ if (!this.doc || !this.doc.lev) {
+ return;
+ }
+ this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot);
+ this.refreshLevOptions();
+ },
+ clearCurrentDraftIfSaved: function () {
+ if (!this.doc || !this.doc.lev) {
+ return;
+ }
+ this.setDraftDocEntry(this.doc.lev, this.doc, this.savedSnapshot);
+ },
+ clearFloorTransientState: function () {
+ this.clearDeferredStaticCommit();
+ this.interactionState = null;
+ this.currentPointerId = null;
+ this.hoverElementId = '';
+ this.pointerStatus = '--';
+ this.lastPointerStatusUpdateTs = 0;
+ this.selectedIds = [];
+ this.isPanning = false;
+ this.isZooming = false;
+ this.pendingViewportRefresh = false;
+ if (this.zoomRefreshTimer) {
+ window.clearTimeout(this.zoomRefreshTimer);
+ this.zoomRefreshTimer = null;
+ }
+ if (this.panRefreshTimer) {
+ window.clearTimeout(this.panRefreshTimer);
+ this.panRefreshTimer = null;
+ }
+ },
+ resetRenderLayers: function () {
+ if (this.gridLayer) {
+ this.gridLayer.clear();
+ }
+ if (this.trackLayer) {
+ this.trackLayer.clear();
+ }
+ if (this.nodeLayer) {
+ this.nodeLayer.clear();
+ }
+ if (this.eraseLayer) {
+ this.eraseLayer.clear();
+ }
+ if (this.patchObjectLayer) {
+ this.patchObjectLayer.clear();
+ }
+ if (this.activeLayer) {
+ this.activeLayer.clear();
+ }
+ if (this.selectionLayer) {
+ this.selectionLayer.clear();
+ }
+ if (this.guideLayer) {
+ this.guideLayer.clear();
+ }
+ if (this.hoverLayer) {
+ this.hoverLayer.clear();
+ }
+ if (this.guideText) {
+ this.guideText.visible = false;
+ this.guideText.text = '';
+ }
+ if (this.labelLayer) {
+ this.labelLayer.visible = false;
+ }
+ for (var i = 0; i < this.labelPool.length; i++) {
+ this.labelPool[i].visible = false;
+ this.labelPool[i].text = '';
+ }
+ this.hideUnusedStaticSprites(this.staticTrackSpritePool || [], 0);
+ this.hideUnusedStaticSprites(this.staticNodeSpritePool || [], 0);
+ if (this.staticTrackSpriteLayer) {
+ this.staticTrackSpriteLayer.removeChildren();
+ this.staticTrackSpritePool = [];
+ }
+ if (this.staticNodeSpriteLayer) {
+ this.staticNodeSpriteLayer.removeChildren();
+ this.staticNodeSpritePool = [];
+ }
+ if (this.staticTrackSpriteLayer) {
+ this.staticTrackSpriteLayer.visible = false;
+ }
+ if (this.staticNodeSpriteLayer) {
+ this.staticNodeSpriteLayer.visible = false;
+ }
+ },
+ hasDirtyDraft: function (lev) {
+ lev = toInt(lev, 0);
+ if (lev <= 0) {
+ return false;
+ }
+ var entry = this.draftDocs[lev];
+ if (!entry || !entry.doc) {
+ return false;
+ }
+ var snapshot = this.snapshotDoc(entry.doc);
+ return snapshot !== (entry.savedSnapshot || '');
+ },
+ markStaticSceneDirty: function () {
+ this.staticSceneDirty = true;
+ },
+ markGridSceneDirty: function () {
+ this.gridSceneDirty = true;
+ },
+ clearRenderCaches: function () {
+ this.gridRenderRect = null;
+ this.gridRenderKey = '';
+ this.staticRenderRect = null;
+ this.staticRenderKey = '';
+ this.staticExcludedKey = '';
+ },
+ scheduleZoomRefresh: function () {
+ if (this.zoomRefreshTimer) {
+ window.clearTimeout(this.zoomRefreshTimer);
+ }
+ this.isZooming = true;
+ this.zoomRefreshTimer = window.setTimeout(function () {
+ this.zoomRefreshTimer = null;
+ this.isZooming = false;
+ if (this.isPanning || (this.interactionState && this.interactionState.type === 'pan')) {
+ this.pendingViewportRefresh = true;
+ return;
+ }
+ this.markGridSceneDirty();
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ }.bind(this), ZOOM_REFRESH_DELAY);
+ },
+ cancelPanRefresh: function () {
+ if (this.panRefreshTimer) {
+ window.clearTimeout(this.panRefreshTimer);
+ this.panRefreshTimer = null;
+ }
+ },
+ schedulePanRefresh: function () {
+ this.cancelPanRefresh();
+ this.isPanning = true;
+ this.panRefreshTimer = window.setTimeout(function () {
+ this.panRefreshTimer = null;
+ this.isPanning = false;
+ if (this.pendingViewportRefresh) {
+ this.pendingViewportRefresh = false;
+ this.markGridSceneDirty();
+ this.markStaticSceneDirty();
+ }
+ this.scheduleRender();
+ }.bind(this), PAN_LABEL_REFRESH_DELAY);
+ },
+ rebuildLabelCapability: function () {
+ var maxWidth = 0;
+ var maxHeight = 0;
+ var elements = this.doc && this.doc.elements ? this.doc.elements : [];
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ if (element.width > maxWidth) {
+ maxWidth = element.width;
+ }
+ if (element.height > maxHeight) {
+ maxHeight = element.height;
+ }
+ }
+ this.labelCapability = {
+ maxWidth: maxWidth,
+ maxHeight: maxHeight
+ };
+ this.labelCapabilityDirty = false;
+ },
+ ensureLabelCapability: function () {
+ if (this.labelCapabilityDirty) {
+ this.rebuildLabelCapability();
+ }
+ return this.labelCapability;
+ },
+ markSpatialIndexDirty: function () {
+ this.spatialIndexDirty = true;
+ },
+ rebuildSpatialIndex: function () {
+ var buckets = {};
+ var elements = this.doc && this.doc.elements ? this.doc.elements : [];
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ var minX = Math.floor(element.x / SPATIAL_BUCKET_SIZE);
+ var maxX = Math.floor((element.x + element.width) / SPATIAL_BUCKET_SIZE);
+ var minY = Math.floor(element.y / SPATIAL_BUCKET_SIZE);
+ var maxY = Math.floor((element.y + element.height) / SPATIAL_BUCKET_SIZE);
+ for (var bx = minX; bx <= maxX; bx++) {
+ for (var by = minY; by <= maxY; by++) {
+ var key = bucketKey(bx, by);
+ if (!buckets[key]) {
+ buckets[key] = [];
+ }
+ buckets[key].push(element);
+ }
+ }
+ }
+ this.spatialBuckets = buckets;
+ this.spatialIndexDirty = false;
+ },
+ ensureSpatialIndex: function () {
+ if (this.spatialIndexDirty || !this.spatialBuckets) {
+ this.rebuildSpatialIndex();
+ }
+ },
+ querySpatialCandidates: function (rect, padding, excludeIds) {
+ if (!this.doc || !rect) {
+ return [];
+ }
+ this.ensureSpatialIndex();
+ var excludeMap = {};
+ excludeIds = excludeIds || [];
+ for (var i = 0; i < excludeIds.length; i++) {
+ excludeMap[excludeIds[i]] = true;
+ }
+ var seen = {};
+ var result = [];
+ var pad = Math.max(0, padding || 0);
+ var minX = Math.floor((rect.x - pad) / SPATIAL_BUCKET_SIZE);
+ var maxX = Math.floor((rect.x + rect.width + pad) / SPATIAL_BUCKET_SIZE);
+ var minY = Math.floor((rect.y - pad) / SPATIAL_BUCKET_SIZE);
+ var maxY = Math.floor((rect.y + rect.height + pad) / SPATIAL_BUCKET_SIZE);
+ for (var bx = minX; bx <= maxX; bx++) {
+ for (var by = minY; by <= maxY; by++) {
+ var key = bucketKey(bx, by);
+ var bucket = this.spatialBuckets[key];
+ if (!bucket || !bucket.length) {
+ continue;
+ }
+ for (var j = 0; j < bucket.length; j++) {
+ var element = bucket[j];
+ if (!element || seen[element.id] || excludeMap[element.id]) {
+ continue;
+ }
+ seen[element.id] = true;
+ result.push(element);
+ }
+ }
+ }
+ return result;
+ },
+ cancelDeferredStaticRebuild: function () {
+ if (this.deferredStaticRebuildTimer) {
+ window.clearTimeout(this.deferredStaticRebuildTimer);
+ this.deferredStaticRebuildTimer = null;
+ }
+ },
+ stageDeferredStaticCommit: function (ids, eraseRects) {
+ this.pendingStaticCommit = {
+ ids: (ids || []).slice(),
+ eraseRects: (eraseRects || []).map(function (item) {
+ return {
+ x: item.x,
+ y: item.y,
+ width: item.width,
+ height: item.height
+ };
+ })
+ };
+ },
+ clearDeferredStaticCommit: function () {
+ this.cancelDeferredStaticRebuild();
+ this.pendingStaticCommit = null;
+ },
+ scheduleDeferredStaticRebuild: function () {
+ this.cancelDeferredStaticRebuild();
+ this.deferredStaticRebuildTimer = window.setTimeout(function () {
+ this.deferredStaticRebuildTimer = null;
+ this.pendingStaticCommit = null;
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ }.bind(this), DEFERRED_STATIC_REBUILD_DELAY);
+ },
+ selectionKey: function (ids) {
+ return (ids || []).slice().sort().join('|');
+ },
+ setSelectedIds: function (ids, options) {
+ options = options || {};
+ var nextIds = (ids || []).filter(Boolean);
+ this.selectedIds = nextIds.slice();
+ if (options.refreshInspector !== false) {
+ this.refreshInspector();
+ }
+ },
+ setCurrentDoc: function (doc, options) {
+ options = options || {};
+ var normalized = this.normalizeDoc(doc);
+ this.clearFloorTransientState();
+ this.resetRenderLayers();
+ this.clearRenderCaches();
+ this.doc = normalized;
+ this.markSpatialIndexDirty();
+ this.labelCapabilityDirty = true;
+ this.pendingViewportRefresh = false;
+ this.currentLev = normalized.lev;
+ this.floorPickerLev = normalized.lev;
+ this.switchingFloorLev = null;
+ this.loadingFloor = false;
+ this.syncFloorQueryParam(normalized.lev);
+ this.markGridSceneDirty();
+ this.markStaticSceneDirty();
+ this.undoStack = [];
+ this.redoStack = [];
+ this.savedSnapshot = options.savedSnapshot != null ? options.savedSnapshot : this.snapshotDoc(normalized);
+ this.syncDirty();
+ this.refreshInspector();
+ this.refreshLevOptions();
+ this.$nextTick(function () {
+ this.fitContent();
+ this.scheduleRender();
+ }.bind(this));
+ },
+ replaceDocFromSnapshot: function (snapshot) {
+ if (!snapshot) {
+ return;
+ }
+ try {
+ this.clearFloorTransientState();
+ this.resetRenderLayers();
+ this.clearRenderCaches();
+ this.doc = this.normalizeDoc(JSON.parse(snapshot));
+ this.markSpatialIndexDirty();
+ this.labelCapabilityDirty = true;
+ this.pendingViewportRefresh = false;
+ } catch (e) {
+ this.showMessage('error', '鍘嗗彶璁板綍鎭㈠澶辫触');
+ return;
+ }
+ this.markGridSceneDirty();
+ this.markStaticSceneDirty();
+ this.floorPickerLev = this.doc.lev;
+ this.currentLev = this.doc.lev;
+ this.refreshInspector();
+ this.syncDirty();
+ this.cacheCurrentDraft();
+ this.scheduleRender();
+ },
+ pushUndoSnapshot: function (snapshot) {
+ if (!snapshot) {
+ return;
+ }
+ if (this.undoStack.length > 0 && this.undoStack[this.undoStack.length - 1] === snapshot) {
+ return;
+ }
+ this.undoStack.push(snapshot);
+ if (this.undoStack.length > HISTORY_LIMIT) {
+ this.undoStack.shift();
+ }
+ },
+ commitMutation: function (beforeSnapshot, options) {
+ options = options || {};
+ var afterSnapshot = this.snapshotDoc(this.doc);
+ if (beforeSnapshot === afterSnapshot) {
+ this.scheduleRender();
+ this.refreshInspector();
+ return false;
+ }
+ this.pushUndoSnapshot(beforeSnapshot);
+ this.redoStack = [];
+ this.markSpatialIndexDirty();
+ this.labelCapabilityDirty = true;
+ if (options.staticSceneDirty !== false) {
+ this.clearDeferredStaticCommit();
+ this.markStaticSceneDirty();
+ }
+ this.syncDirty();
+ this.cacheCurrentDraft();
+ this.refreshInspector();
+ this.scheduleRender();
+ return true;
+ },
+ runMutation: function (mutator) {
+ if (!this.doc) {
+ return false;
+ }
+ var beforeSnapshot = this.snapshotDoc(this.doc);
+ mutator();
+ return this.commitMutation(beforeSnapshot);
+ },
+ undo: function () {
+ if (this.undoStack.length === 0 || !this.doc) {
+ return;
+ }
+ var currentSnapshot = this.snapshotDoc(this.doc);
+ var snapshot = this.undoStack.pop();
+ this.redoStack.push(currentSnapshot);
+ this.replaceDocFromSnapshot(snapshot);
+ },
+ redo: function () {
+ if (this.redoStack.length === 0 || !this.doc) {
+ return;
+ }
+ var currentSnapshot = this.snapshotDoc(this.doc);
+ var snapshot = this.redoStack.pop();
+ this.pushUndoSnapshot(currentSnapshot);
+ this.replaceDocFromSnapshot(snapshot);
+ },
+ createLocalBlankDoc: function (lev, width, height, savedSnapshot) {
+ var doc = {
+ lev: toInt(lev, 1),
+ editorMode: FREE_EDITOR_MODE,
+ canvasWidth: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(width, DEFAULT_CANVAS_WIDTH)),
+ canvasHeight: Math.max(MIN_ELEMENT_SIZE * 4, toNumber(height, DEFAULT_CANVAS_HEIGHT)),
+ elements: []
+ };
+ this.setCurrentDoc(doc, {
+ savedSnapshot: savedSnapshot != null ? savedSnapshot : ''
+ });
+ this.cacheCurrentDraft();
+ this.syncDirty();
+ },
+ openBlankDialog: function () {
+ var lev = this.currentLev || 1;
+ this.blankForm = {
+ lev: String(lev),
+ width: String(Math.round(this.doc ? this.doc.canvasWidth : DEFAULT_CANVAS_WIDTH)),
+ height: String(Math.round(this.doc ? this.doc.canvasHeight : DEFAULT_CANVAS_HEIGHT))
+ };
+ this.blankDialogVisible = true;
+ },
+ createBlankMap: function () {
+ var lev = toInt(this.blankForm.lev, 0);
+ var width = toNumber(this.blankForm.width, DEFAULT_CANVAS_WIDTH);
+ var height = toNumber(this.blankForm.height, DEFAULT_CANVAS_HEIGHT);
+ if (lev <= 0) {
+ this.showMessage('warning', '妤煎眰涓嶈兘涓虹┖');
+ return;
+ }
+ if (width <= 0 || height <= 0) {
+ this.showMessage('warning', '鐢诲竷灏哄蹇呴』澶т簬 0');
+ return;
+ }
+ this.blankDialogVisible = false;
+ this.createLocalBlankDoc(lev, width, height, '');
+ },
+ buildTransferPayload: function () {
+ var doc = this.exportDoc(this.doc);
+ return {
+ format: MAP_TRANSFER_FORMAT,
+ exportedAt: new Date().toISOString(),
+ source: {
+ lev: doc.lev,
+ editorMode: doc.editorMode
+ },
+ docs: [doc]
+ };
+ },
+ buildTransferFilename: function (docs) {
+ var levs = (docs || []).map(function (item) {
+ return toInt(item && item.lev, 0);
+ }).filter(function (lev) {
+ return lev > 0;
+ }).sort(function (a, b) {
+ return a - b;
+ });
+ var scope = levs.length <= 1
+ ? (String(levs[0] || (this.currentLev || 1)) + 'F')
+ : ('all-' + levs.length + '-floors');
+ var now = new Date();
+ return [
+ 'bas-map',
+ scope,
+ now.getFullYear(),
+ padNumber(now.getMonth() + 1),
+ padNumber(now.getDate()),
+ padNumber(now.getHours()),
+ padNumber(now.getMinutes()),
+ padNumber(now.getSeconds())
+ ].join('-') + '.json';
+ },
+ requestEditorDoc: function (lev) {
+ return new Promise(function (resolve, reject) {
+ $.ajax({
+ url: baseUrl + '/basMap/editor/' + lev + '/auth',
+ method: 'GET',
+ headers: authHeaders(),
+ success: function (res) {
+ if (!res || res.code !== 200 || !res.data) {
+ reject(new Error((res && res.msg) ? res.msg : ('鍔犺浇 ' + lev + 'F 鍦板浘澶辫触')));
+ return;
+ }
+ resolve(res.data);
+ },
+ error: function () {
+ reject(new Error('鍔犺浇 ' + lev + 'F 鍦板浘澶辫触'));
+ }
+ });
+ });
+ },
+ collectAllTransferDocs: function () {
+ var self = this;
+ var levMap = {};
+ (this.remoteLevOptions || []).forEach(function (lev) {
+ lev = toInt(lev, 0);
+ if (lev > 0) {
+ levMap[lev] = true;
+ }
+ });
+ Object.keys(this.draftDocs || {}).forEach(function (key) {
+ var lev = toInt(key, 0);
+ if (lev > 0) {
+ levMap[lev] = true;
+ }
+ });
+ if (this.doc && this.doc.lev) {
+ levMap[toInt(this.doc.lev, 0)] = true;
+ }
+ var levs = Object.keys(levMap).map(function (key) {
+ return toInt(key, 0);
+ }).filter(function (lev) {
+ return lev > 0;
+ }).sort(function (a, b) {
+ return a - b;
+ });
+ if (!levs.length) {
+ return Promise.resolve([]);
+ }
+ return Promise.all(levs.map(function (lev) {
+ if (self.doc && self.doc.lev === lev) {
+ return Promise.resolve(self.exportDoc(self.doc));
+ }
+ if (self.draftDocs[lev] && self.draftDocs[lev].doc) {
+ return Promise.resolve(self.exportDoc(self.draftDocs[lev].doc));
+ }
+ return self.requestEditorDoc(lev).then(function (doc) {
+ return self.normalizeDoc(doc);
+ });
+ }));
+ },
+ exportMapPackage: function () {
+ var self = this;
+ if (!this.doc && (!this.remoteLevOptions || !this.remoteLevOptions.length)) {
+ this.showMessage('warning', '褰撳墠娌℃湁鍙鍑虹殑鍦板浘');
+ return;
+ }
+ this.collectAllTransferDocs().then(function (docs) {
+ if (!docs || !docs.length) {
+ self.showMessage('warning', '褰撳墠娌℃湁鍙鍑虹殑鍦板浘');
+ return;
+ }
+ var payload = {
+ format: MAP_TRANSFER_FORMAT,
+ exportedAt: new Date().toISOString(),
+ source: {
+ lev: self.currentLev || (docs[0] && docs[0].lev) || 1,
+ editorMode: FREE_EDITOR_MODE
+ },
+ docs: docs.map(function (doc) {
+ return self.exportDoc(doc);
+ })
+ };
+ var blob = new Blob([JSON.stringify(payload, null, 2)], {
+ type: 'application/json;charset=utf-8'
+ });
+ var href = window.URL.createObjectURL(blob);
+ var link = document.createElement('a');
+ link.href = href;
+ link.download = self.buildTransferFilename(payload.docs);
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.setTimeout(function () {
+ window.URL.revokeObjectURL(href);
+ }, 0);
+ self.showMessage('success', '宸插鍑� ' + payload.docs.length + ' 涓ゼ灞傜殑鍦板浘鍖�');
+ }).catch(function (error) {
+ self.showMessage('error', error && error.message ? error.message : '瀵煎嚭鍦板浘澶辫触');
+ });
+ },
+ triggerImportMap: function () {
+ if (this.$refs.mapImportInput) {
+ this.$refs.mapImportInput.value = '';
+ this.$refs.mapImportInput.click();
+ }
+ },
+ parseTransferPackage: function (raw) {
+ if (!raw) {
+ return null;
+ }
+ if (raw.format === MAP_TRANSFER_FORMAT && Array.isArray(raw.docs) && raw.docs.length) {
+ return {
+ docs: raw.docs,
+ activeLev: toInt(raw.source && raw.source.lev, 0)
+ };
+ }
+ if ((raw.format === 'bas-map-editor-transfer-v1' || raw.format === MAP_TRANSFER_FORMAT) && raw.doc) {
+ return {
+ docs: [raw.doc],
+ activeLev: toInt(raw.source && raw.source.lev, 0)
+ };
+ }
+ if (raw.editorMode === FREE_EDITOR_MODE && Array.isArray(raw.elements)) {
+ return {
+ docs: [raw],
+ activeLev: toInt(raw.lev, 0)
+ };
+ }
+ return null;
+ },
+ importMapPackage: function (payload, options) {
+ options = options || {};
+ if (!payload || !Array.isArray(payload.docs) || !payload.docs.length) {
+ this.showMessage('error', '瀵煎叆鏂囦欢鏍煎紡涓嶆纭�');
+ return;
+ }
+ if (this.isDirty && options.skipConfirm !== true) {
+ if (!window.confirm('瀵煎叆鍦板浘浼氭浛鎹㈠綋鍓嶇紪杈戞�佹湭淇濆瓨鍐呭锛屾槸鍚︾户缁紵')) {
+ return;
+ }
+ }
+ if (this.doc) {
+ this.cacheCurrentDraft();
+ }
+ var self = this;
+ var normalizedDocs = payload.docs.map(function (item) {
+ return self.normalizeDoc(item);
+ }).sort(function (a, b) {
+ return toInt(a.lev, 0) - toInt(b.lev, 0);
+ });
+ normalizedDocs.forEach(function (doc) {
+ self.setDraftDocEntry(doc.lev, doc, '');
+ });
+ var activeLev = toInt(payload.activeLev, 0);
+ var targetDoc = normalizedDocs[0];
+ for (var i = 0; i < normalizedDocs.length; i++) {
+ if (normalizedDocs[i].lev === activeLev) {
+ targetDoc = normalizedDocs[i];
+ break;
+ }
+ }
+ this.refreshLevOptions();
+ this.floorPickerLev = targetDoc.lev;
+ this.setCurrentDoc(targetDoc, { savedSnapshot: '' });
+ if (normalizedDocs.length > 1) {
+ this.showMessage('success', '鍦板浘鍖呭凡瀵煎叆 ' + normalizedDocs.length + ' 涓ゼ灞傦紝鍙偣鍑烩�滀繚瀛樺叏閮ㄦゼ灞傗�濊惤搴�');
+ return;
+ }
+ this.showMessage('success', '鍦板浘鍖呭凡瀵煎叆锛屼繚瀛樺悗鎵嶄細瑕嗙洊杩愯鍦板浘');
+ },
+ handleImportMap: function (event) {
+ var file = event && event.target && event.target.files ? event.target.files[0] : null;
+ if (!file) {
+ return;
+ }
+ var self = this;
+ var reader = new FileReader();
+ reader.onload = function (loadEvent) {
+ try {
+ var text = loadEvent && loadEvent.target ? loadEvent.target.result : '';
+ var raw = JSON.parse(text || '{}');
+ var payload = self.parseTransferPackage(raw);
+ self.importMapPackage(payload);
+ } catch (e) {
+ self.showMessage('error', '鍦板浘鏂囦欢瑙f瀽澶辫触');
+ }
+ };
+ reader.onerror = function () {
+ self.showMessage('error', '鍦板浘鏂囦欢璇诲彇澶辫触');
+ };
+ reader.readAsText(file, 'utf-8');
+ },
+ triggerImportExcel: function () {
+ if (this.$refs.importInput) {
+ this.$refs.importInput.value = '';
+ this.$refs.importInput.click();
+ }
+ },
+ handleImportExcel: function (event) {
+ var self = this;
+ var file = event && event.target && event.target.files ? event.target.files[0] : null;
+ if (!file) {
+ return;
+ }
+ var formData = new FormData();
+ formData.append('file', file);
+ $.ajax({
+ url: baseUrl + '/basMap/editor/importExcel/auth',
+ method: 'POST',
+ headers: authHeaders(),
+ data: formData,
+ processData: false,
+ contentType: false,
+ success: function (res) {
+ if (!res || res.code !== 200 || !Array.isArray(res.data) || res.data.length === 0) {
+ self.showMessage('error', (res && res.msg) ? res.msg : 'Excel 瀵煎叆澶辫触');
+ return;
+ }
+ res.data.forEach(function (item) {
+ var doc = self.normalizeDoc(item);
+ self.setDraftDocEntry(doc.lev, doc, '');
+ });
+ self.refreshLevOptions();
+ self.floorPickerLev = toInt(res.data[0].lev, 0);
+ self.setCurrentDoc(res.data[0], { savedSnapshot: '' });
+ self.showMessage('success', 'Excel 宸插鍏ュ埌缂栬緫鍣紝淇濆瓨鍚庢墠浼氳鐩栬繍琛屽湴鍥�');
+ },
+ error: function () {
+ self.showMessage('error', 'Excel 瀵煎叆澶辫触');
+ }
+ });
+ },
+ handleFloorChange: function (lev) {
+ lev = toInt(lev, 0);
+ if (lev <= 0) {
+ return;
+ }
+ this.floorPickerLev = lev;
+ if (this.doc && this.doc.lev === lev && !this.loadingFloor) {
+ this.switchingFloorLev = null;
+ return;
+ }
+ if (this.doc) {
+ this.cacheCurrentDraft();
+ }
+ this.clearFloorTransientState();
+ this.resetRenderLayers();
+ this.switchingFloorLev = lev;
+ this.markGridSceneDirty();
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ this.fetchFloor(lev);
+ },
+ loadCurrentFloor: function () {
+ if (!this.currentLev) {
+ this.showMessage('warning', '璇峰厛閫夋嫨妤煎眰');
+ return;
+ }
+ if (this.isDirty && !window.confirm('閲嶆柊璇诲彇浼氫涪寮冨綋鍓嶆ゼ灞傛湭淇濆瓨鐨勮嚜鐢辩敾甯冪紪杈戯紝鏄惁缁х画锛�')) {
+ return;
+ }
+ this.removeDraftDocEntry(this.currentLev);
+ this.refreshLevOptions();
+ this.fetchFloor(this.currentLev);
+ },
+ fetchFloor: function (lev) {
+ var self = this;
+ lev = toInt(lev, 0);
+ if (lev <= 0) {
+ return;
+ }
+ var requestSeq = ++this.floorRequestSeq;
+ this.activeFloorRequestSeq = requestSeq;
+ this.loadingFloor = true;
+ this.switchingFloorLev = lev;
+ $.ajax({
+ url: baseUrl + '/basMap/editor/' + lev + '/auth',
+ method: 'GET',
+ headers: authHeaders(),
+ success: function (res) {
+ if (requestSeq !== self.activeFloorRequestSeq) {
+ return;
+ }
+ self.loadingFloor = false;
+ if (!res || res.code !== 200 || !res.data) {
+ self.switchingFloorLev = null;
+ self.floorPickerLev = self.currentLev;
+ self.markGridSceneDirty();
+ self.markStaticSceneDirty();
+ self.scheduleRender();
+ self.showMessage('error', (res && res.msg) ? res.msg : '鍔犺浇鍦板浘澶辫触');
+ return;
+ }
+ var normalized = self.normalizeDoc(res.data);
+ self.setDraftDocEntry(normalized.lev, normalized, self.snapshotDoc(normalized));
+ self.setCurrentDoc(normalized, {
+ savedSnapshot: self.snapshotDoc(normalized)
+ });
+ },
+ error: function () {
+ if (requestSeq !== self.activeFloorRequestSeq) {
+ return;
+ }
+ self.loadingFloor = false;
+ self.switchingFloorLev = null;
+ self.floorPickerLev = self.currentLev;
+ self.markGridSceneDirty();
+ self.markStaticSceneDirty();
+ self.scheduleRender();
+ self.showMessage('error', '鍔犺浇鍦板浘澶辫触');
+ }
+ });
+ },
+ validateDocBeforeSave: function (doc) {
+ var source = this.normalizeDoc(doc);
+ if (!source || !source.lev) {
+ return '妤煎眰涓嶈兘涓虹┖';
+ }
+ if (toNumber(source.canvasWidth, 0) <= 0 || toNumber(source.canvasHeight, 0) <= 0) {
+ return '鐢诲竷灏哄蹇呴』澶т簬 0';
+ }
+ var elements = source.elements || [];
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ if (element.width <= 0 || element.height <= 0) {
+ return '瀛樺湪灏哄鏃犳晥鐨勫厓绱�';
+ }
+ if (element.x < 0 || element.y < 0) {
+ return '鍏冪礌鍧愭爣涓嶈兘灏忎簬 0';
+ }
+ if (!isRectWithinCanvas(element, source.canvasWidth, source.canvasHeight)) {
+ return '瀛樺湪瓒呭嚭鐢诲竷杈圭晫鐨勫厓绱�: ' + element.id;
+ }
+ if (element.type === 'devp') {
+ var value = safeParseJson(element.value);
+ if (!value || toInt(value.stationId, 0) <= 0 || toInt(value.deviceNo, 0) <= 0) {
+ return '杈撻�佺嚎鍏冪礌蹇呴』閰嶇疆鏈夋晥鐨� stationId 鍜� deviceNo';
+ }
+ }
+ }
+ var overlapId = findDocOverlapId(source);
+ if (overlapId) {
+ return '瀛樺湪閲嶅彔鍏冪礌: ' + overlapId;
+ }
+ return '';
+ },
+ validateBeforeSave: function () {
+ return this.validateDocBeforeSave(this.doc);
+ },
+ requestSaveDoc: function (doc) {
+ return new Promise(function (resolve, reject) {
+ $.ajax({
+ url: baseUrl + '/basMap/editor/save/auth',
+ method: 'POST',
+ headers: $.extend({
+ 'Content-Type': 'application/json;charset=UTF-8'
+ }, authHeaders()),
+ data: JSON.stringify(doc),
+ success: function (res) {
+ if (!res || res.code !== 200) {
+ reject(new Error((res && res.msg) ? res.msg : '淇濆瓨澶辫触'));
+ return;
+ }
+ resolve(res);
+ },
+ error: function () {
+ reject(new Error('淇濆瓨澶辫触'));
+ }
+ });
+ });
+ },
+ collectDirtyDocsForSave: function () {
+ var result = [];
+ var seen = {};
+ if (this.doc && this.doc.lev && this.isDirty) {
+ var currentDoc = this.exportDoc(this.doc);
+ result.push(currentDoc);
+ seen[currentDoc.lev] = true;
+ }
+ var self = this;
+ Object.keys(this.draftDocs || {}).forEach(function (key) {
+ var lev = toInt(key, 0);
+ if (lev <= 0 || seen[lev]) {
+ return;
+ }
+ var entry = self.draftDocs[lev];
+ if (!entry || !entry.doc) {
+ return;
+ }
+ var snapshot = self.snapshotDoc(entry.doc);
+ if (snapshot === (entry.savedSnapshot || '')) {
+ return;
+ }
+ var doc = self.exportDoc(entry.doc);
+ result.push(doc);
+ seen[doc.lev] = true;
+ });
+ result.sort(function (a, b) {
+ return toInt(a.lev, 0) - toInt(b.lev, 0);
+ });
+ return result;
+ },
+ markDocSavedState: function (doc) {
+ var normalized = this.normalizeDoc(doc);
+ var savedSnapshot = this.snapshotDoc(normalized);
+ this.setDraftDocEntry(normalized.lev, normalized, savedSnapshot);
+ if (this.doc && this.doc.lev === normalized.lev) {
+ this.savedSnapshot = savedSnapshot;
+ this.syncDirty();
+ }
+ },
+ saveDoc: function () {
+ var self = this;
+ if (!this.doc) {
+ return;
+ }
+ var error = this.validateBeforeSave();
+ if (error) {
+ this.showMessage('warning', error);
+ return;
+ }
+ this.saving = true;
+ var payload = this.exportDoc(this.doc);
+ this.requestSaveDoc(payload).then(function () {
+ self.saving = false;
+ self.savedSnapshot = self.snapshotDoc(self.doc);
+ self.syncDirty();
+ self.clearCurrentDraftIfSaved();
+ self.refreshLevOptions();
+ self.showMessage('success', '褰撳墠妤煎眰宸蹭繚瀛樺苟缂栬瘧鍒拌繍琛屽湴鍥�');
+ }).catch(function (error) {
+ self.saving = false;
+ self.showMessage('error', error && error.message ? error.message : '淇濆瓨澶辫触');
+ });
+ },
+ saveAllDocs: function () {
+ var self = this;
+ if (this.saving || this.savingAll) {
+ return;
+ }
+ var docs = this.collectDirtyDocsForSave();
+ if (!docs.length) {
+ this.showMessage('warning', '褰撳墠娌℃湁闇�瑕佷繚瀛樼殑妤煎眰');
+ return;
+ }
+ if (docs.length > 1 && !window.confirm('灏嗕繚瀛� ' + docs.length + ' 涓ゼ灞傚埌杩愯鍦板浘锛屾槸鍚︾户缁紵')) {
+ return;
+ }
+ for (var i = 0; i < docs.length; i++) {
+ var error = this.validateDocBeforeSave(docs[i]);
+ if (error) {
+ this.showMessage('warning', docs[i].lev + 'F 淇濆瓨鍓嶆牎楠屽け璐�: ' + error);
+ return;
+ }
+ }
+ this.savingAll = true;
+ var index = 0;
+ var total = docs.length;
+ var next = function () {
+ if (index >= total) {
+ self.savingAll = false;
+ self.refreshLevOptions();
+ self.showMessage('success', '宸蹭繚瀛� ' + total + ' 涓ゼ灞傚埌杩愯鍦板浘');
+ return;
+ }
+ var doc = docs[index++];
+ self.requestSaveDoc(doc).then(function () {
+ self.markDocSavedState(doc);
+ next();
+ }).catch(function (error) {
+ self.savingAll = false;
+ self.showMessage('error', doc.lev + 'F 淇濆瓨澶辫触: ' + (error && error.message ? error.message : '淇濆瓨澶辫触'));
+ });
+ };
+ next();
+ },
+ setTool: function (tool) {
+ this.activeTool = tool;
+ this.updateCursor();
+ },
+ findElementById: function (id) {
+ if (!this.doc || !id) {
+ return null;
+ }
+ var elements = this.doc.elements || [];
+ for (var i = 0; i < elements.length; i++) {
+ if (elements[i].id === id) {
+ return elements[i];
+ }
+ }
+ return null;
+ },
+ getSelectedElements: function () {
+ var self = this;
+ return this.selectedIds.map(function (id) {
+ return self.findElementById(id);
+ }).filter(Boolean);
+ },
+ refreshInspector: function () {
+ var element = this.singleSelectedElement;
+ if (!this.doc) {
+ this.canvasForm = {
+ width: String(DEFAULT_CANVAS_WIDTH),
+ height: String(DEFAULT_CANVAS_HEIGHT)
+ };
+ this.valueEditorText = '';
+ this.resetDevpForm();
+ this.resetDeviceForm();
+ return;
+ }
+ this.canvasForm = {
+ width: String(Math.round(this.doc.canvasWidth)),
+ height: String(Math.round(this.doc.canvasHeight))
+ };
+ if (!element) {
+ this.geometryForm = { x: '', y: '', width: '', height: '' };
+ this.valueEditorText = '';
+ this.resetDevpForm();
+ this.resetDeviceForm();
+ return;
+ }
+ this.geometryForm = {
+ x: String(this.formatNumber(element.x)),
+ y: String(this.formatNumber(element.y)),
+ width: String(this.formatNumber(element.width)),
+ height: String(this.formatNumber(element.height))
+ };
+ this.valueEditorText = element.value || '';
+ if (element.type === 'devp') {
+ this.loadDevpForm(element.value);
+ } else {
+ this.resetDevpForm();
+ }
+ if (isDeviceConfigType(element.type)) {
+ this.loadDeviceForm(element.type, element.value);
+ } else {
+ this.resetDeviceForm();
+ }
+ this.ensureShelfFillStartValue();
+ },
+ resetDevpForm: function () {
+ this.devpForm = {
+ stationId: '',
+ deviceNo: '',
+ direction: [],
+ isBarcodeStation: false,
+ barcodeIdx: '',
+ backStation: '',
+ backStationDeviceNo: '',
+ isInStation: false,
+ barcodeStation: '',
+ barcodeStationDeviceNo: '',
+ isOutStation: false,
+ runBlockReassign: false,
+ isOutOrder: false,
+ isLiftTransfer: false
+ };
+ },
+ resetDeviceForm: function () {
+ this.deviceForm = {
+ valueKey: '',
+ deviceNo: ''
+ };
+ },
+ ensureShelfFillStartValue: function () {
+ var element = this.singleSelectedElement;
+ if (!element || element.type !== 'shelf') {
+ return;
+ }
+ if (!this.shelfFillForm.startValue || !parseShelfLocationValue(this.shelfFillForm.startValue)) {
+ this.shelfFillForm.startValue = normalizeValue(element.value || '');
+ }
+ },
+ loadDevpForm: function (value) {
+ this.resetDevpForm();
+ var json = safeParseJson(value);
+ if (!json) {
+ return;
+ }
+ this.devpForm.stationId = json.stationId != null ? String(json.stationId) : '';
+ this.devpForm.deviceNo = json.deviceNo != null ? String(json.deviceNo) : '';
+ this.devpForm.direction = normalizeDirectionList(json.direction);
+ this.devpForm.isBarcodeStation = boolFlag(json.isBarcodeStation);
+ this.devpForm.barcodeIdx = json.barcodeIdx != null ? String(json.barcodeIdx) : '';
+ this.devpForm.backStation = json.backStation != null ? String(json.backStation) : '';
+ this.devpForm.backStationDeviceNo = json.backStationDeviceNo != null ? String(json.backStationDeviceNo) : '';
+ this.devpForm.isInStation = boolFlag(json.isInStation);
+ this.devpForm.barcodeStation = json.barcodeStation != null ? String(json.barcodeStation) : '';
+ this.devpForm.barcodeStationDeviceNo = json.barcodeStationDeviceNo != null ? String(json.barcodeStationDeviceNo) : '';
+ this.devpForm.isOutStation = boolFlag(json.isOutStation);
+ this.devpForm.runBlockReassign = boolFlag(json.runBlockReassign);
+ this.devpForm.isOutOrder = boolFlag(json.isOutOrder);
+ this.devpForm.isLiftTransfer = boolFlag(json.isLiftTransfer);
+ },
+ getDeviceConfigLabel: function (type) {
+ var meta = getTypeMeta(type);
+ return meta.label + '鍙傛暟';
+ },
+ getDeviceConfigKeyLabel: function (type, valueKey) {
+ if (valueKey === 'crnNo') {
+ return 'crnNo';
+ }
+ if (valueKey === 'rgvNo') {
+ return 'rgvNo';
+ }
+ return type === 'rgv' ? 'deviceNo / rgvNo' : 'deviceNo / crnNo';
+ },
+ loadDeviceForm: function (type, value) {
+ this.resetDeviceForm();
+ if (!isDeviceConfigType(type)) {
+ return;
+ }
+ var json = safeParseJson(value);
+ var valueKey = pickDeviceValueKey(type, json);
+ var deviceNo = '';
+ if (json && json[valueKey] != null) {
+ deviceNo = String(json[valueKey]);
+ }
+ this.deviceForm = {
+ valueKey: valueKey,
+ deviceNo: deviceNo
+ };
+ },
+ isDevpDirectionActive: function (directionKey) {
+ return this.devpForm.direction.indexOf(directionKey) >= 0;
+ },
+ toggleDevpDirection: function (directionKey) {
+ if (!directionKey) {
+ return;
+ }
+ var next = this.devpForm.direction.slice();
+ var index = next.indexOf(directionKey);
+ if (index >= 0) {
+ next.splice(index, 1);
+ } else {
+ next.push(directionKey);
+ }
+ this.devpForm.direction = DEVP_DIRECTION_OPTIONS.map(function (item) {
+ return item.key;
+ }).filter(function (item) {
+ return next.indexOf(item) >= 0;
+ });
+ },
+ applyCanvasSize: function () {
+ var self = this;
+ if (!this.doc) {
+ return;
+ }
+ var width = toNumber(this.canvasForm.width, 0);
+ var height = toNumber(this.canvasForm.height, 0);
+ if (width <= 0 || height <= 0) {
+ this.showMessage('warning', '鐢诲竷灏哄蹇呴』澶т簬 0');
+ return;
+ }
+ var bounds = this.getElementBounds((this.doc.elements || []).map(function (item) {
+ return item.id;
+ }));
+ if (bounds && (width < bounds.x + bounds.width || height < bounds.y + bounds.height)) {
+ this.showMessage('warning', '鐢诲竷涓嶈兘灏忎簬褰撳墠鍏冪礌鍗犵敤鑼冨洿');
+ return;
+ }
+ this.runMutation(function () {
+ self.doc.canvasWidth = roundCoord(width);
+ self.doc.canvasHeight = roundCoord(height);
+ });
+ },
+ applyGeometry: function () {
+ var self = this;
+ var element = this.singleSelectedElement;
+ if (!element) {
+ return;
+ }
+ var next = {
+ x: roundCoord(Math.max(0, toNumber(this.geometryForm.x, element.x))),
+ y: roundCoord(Math.max(0, toNumber(this.geometryForm.y, element.y))),
+ width: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.width, element.width))),
+ height: roundCoord(Math.max(MIN_ELEMENT_SIZE, toNumber(this.geometryForm.height, element.height)))
+ };
+ if (!this.isWithinCanvas(next)) {
+ this.showMessage('warning', '鍑犱綍灞炴�ц秴鍑哄綋鍓嶇敾甯冭寖鍥�');
+ return;
+ }
+ var preview = deepClone(element);
+ preview.x = next.x;
+ preview.y = next.y;
+ preview.width = next.width;
+ preview.height = next.height;
+ if (this.hasOverlap(preview, [preview.id])) {
+ this.showMessage('warning', '璋冩暣鍚庝細涓庡叾浠栧厓绱犻噸鍙�');
+ return;
+ }
+ this.runMutation(function () {
+ element.x = next.x;
+ element.y = next.y;
+ element.width = next.width;
+ element.height = next.height;
+ });
+ },
+ applyRawValue: function () {
+ var self = this;
+ var element = this.singleSelectedElement;
+ if (!element || element.type === 'devp') {
+ return;
+ }
+ this.runMutation(function () {
+ element.value = normalizeValue(self.valueEditorText);
+ });
+ },
+ applyDeviceForm: function () {
+ var self = this;
+ var element = this.singleSelectedDeviceElement;
+ if (!element) {
+ return;
+ }
+ var deviceNo = toInt(this.deviceForm.deviceNo, 0);
+ if (deviceNo <= 0) {
+ this.showMessage('warning', '璁惧缂栧彿蹇呴』澶т簬 0');
+ return;
+ }
+ var valueKey = this.deviceForm.valueKey || pickDeviceValueKey(element.type, safeParseJson(element.value));
+ this.runMutation(function () {
+ var payload = safeParseJson(element.value) || {};
+ delete payload.deviceNo;
+ delete payload.crnNo;
+ delete payload.rgvNo;
+ payload[valueKey] = deviceNo;
+ element.value = JSON.stringify(payload);
+ self.valueEditorText = element.value;
+ });
+ },
+ applyDevpForm: function () {
+ var self = this;
+ var element = this.singleSelectedElement;
+ if (!element || element.type !== 'devp') {
+ return;
+ }
+ var stationId = toInt(this.devpForm.stationId, 0);
+ var deviceNo = toInt(this.devpForm.deviceNo, 0);
+ if (stationId <= 0 || deviceNo <= 0) {
+ this.showMessage('warning', '绔欏彿鍜� PLC 缂栧彿蹇呴』澶т簬 0');
+ return;
+ }
+ var payload = {
+ stationId: stationId,
+ deviceNo: deviceNo
+ };
+ var directionList = normalizeDirectionList(this.devpForm.direction);
+ if (directionList.length > 0) {
+ payload.direction = directionList;
+ }
+ var barcodeIdx = this.devpForm.barcodeIdx === '' ? 0 : toInt(this.devpForm.barcodeIdx, 0);
+ var backStation = this.devpForm.backStation === '' ? 0 : toInt(this.devpForm.backStation, 0);
+ var backStationDeviceNo = this.devpForm.backStationDeviceNo === '' ? 0 : toInt(this.devpForm.backStationDeviceNo, 0);
+ var barcodeStation = this.devpForm.barcodeStation === '' ? 0 : toInt(this.devpForm.barcodeStation, 0);
+ var barcodeStationDeviceNo = this.devpForm.barcodeStationDeviceNo === '' ? 0 : toInt(this.devpForm.barcodeStationDeviceNo, 0);
+ if (this.devpForm.isInStation && (barcodeStation <= 0 || barcodeStationDeviceNo <= 0)) {
+ this.showMessage('warning', '鍏ョ珯鐐瑰繀椤诲~鍐欐潯鐮佺珯鍜屾潯鐮佺珯 PLC 缂栧彿');
+ return;
+ }
+ if (this.devpForm.isBarcodeStation && (backStation <= 0 || backStationDeviceNo <= 0 || barcodeIdx <= 0)) {
+ this.showMessage('warning', '鏉$爜绔欏繀椤诲~鍐欐潯鐮佺储寮曘�侀��鍥炵珯鍜岄��鍥炵珯 PLC 缂栧彿');
+ return;
+ }
+ if (this.devpForm.isBarcodeStation) {
+ payload.isBarcodeStation = 1;
+ }
+ if (barcodeIdx > 0) {
+ payload.barcodeIdx = barcodeIdx;
+ }
+ if (backStation > 0) {
+ payload.backStation = backStation;
+ }
+ if (backStationDeviceNo > 0) {
+ payload.backStationDeviceNo = backStationDeviceNo;
+ }
+ if (this.devpForm.isInStation) {
+ payload.isInStation = 1;
+ }
+ if (barcodeStation > 0) {
+ payload.barcodeStation = barcodeStation;
+ }
+ if (barcodeStationDeviceNo > 0) {
+ payload.barcodeStationDeviceNo = barcodeStationDeviceNo;
+ }
+ if (this.devpForm.isOutStation) {
+ payload.isOutStation = 1;
+ }
+ if (this.devpForm.runBlockReassign) {
+ payload.runBlockReassign = 1;
+ }
+ if (this.devpForm.isOutOrder) {
+ payload.isOutOrder = 1;
+ }
+ if (this.devpForm.isLiftTransfer) {
+ payload.isLiftTransfer = 1;
+ }
+ this.runMutation(function () {
+ element.value = JSON.stringify(payload);
+ self.valueEditorText = element.value;
+ });
+ },
+ deleteSelection: function () {
+ var self = this;
+ if (!this.doc || this.selectedIds.length === 0) {
+ return;
+ }
+ var ids = this.selectedIds.slice();
+ this.runMutation(function () {
+ self.doc.elements = self.doc.elements.filter(function (item) {
+ return ids.indexOf(item.id) === -1;
+ });
+ self.selectedIds = [];
+ });
+ },
+ copySelection: function () {
+ var elements = this.getSelectedElements();
+ if (!elements.length) {
+ return;
+ }
+ this.clipboard = deepClone(elements);
+ this.showMessage('success', '宸插鍒� ' + elements.length + ' 涓厓绱�');
+ },
+ getElementListBounds: function (elements) {
+ if (!elements || !elements.length) {
+ return null;
+ }
+ var minX = elements[0].x;
+ var minY = elements[0].y;
+ var maxX = elements[0].x + elements[0].width;
+ var maxY = elements[0].y + elements[0].height;
+ for (var i = 1; i < elements.length; i++) {
+ var element = elements[i];
+ minX = Math.min(minX, element.x);
+ minY = Math.min(minY, element.y);
+ maxX = Math.max(maxX, element.x + element.width);
+ maxY = Math.max(maxY, element.y + element.height);
+ }
+ return {
+ x: minX,
+ y: minY,
+ width: maxX - minX,
+ height: maxY - minY
+ };
+ },
+ getPasteTargetWorld: function () {
+ if (!this.doc) {
+ return { x: 0, y: 0 };
+ }
+ var visible = this.getVisibleCanvasRect ? this.getVisibleCanvasRect() : this.getVisibleWorldRect();
+ var fallback = {
+ x: visible.x + visible.width / 2,
+ y: visible.y + visible.height / 2
+ };
+ if (!this.lastPointerWorld) {
+ return fallback;
+ }
+ return {
+ x: clamp(this.lastPointerWorld.x, 0, this.doc.canvasWidth),
+ y: clamp(this.lastPointerWorld.y, 0, this.doc.canvasHeight),
+ screenX: this.lastPointerWorld.screenX,
+ screenY: this.lastPointerWorld.screenY
+ };
+ },
+ pasteClipboard: function () {
+ var self = this;
+ if (!this.doc || !this.clipboard.length) {
+ return;
+ }
+ var sourceBounds = this.getElementListBounds(this.clipboard);
+ if (!sourceBounds) {
+ return;
+ }
+ var target = this.getPasteTargetWorld();
+ var offsetX = target.x - (sourceBounds.x + sourceBounds.width / 2);
+ var offsetY = target.y - (sourceBounds.y + sourceBounds.height / 2);
+ var minOffsetX = -sourceBounds.x;
+ var maxOffsetX = this.doc.canvasWidth - (sourceBounds.x + sourceBounds.width);
+ var minOffsetY = -sourceBounds.y;
+ var maxOffsetY = this.doc.canvasHeight - (sourceBounds.y + sourceBounds.height);
+ offsetX = clamp(offsetX, minOffsetX, maxOffsetX);
+ offsetY = clamp(offsetY, minOffsetY, maxOffsetY);
+ var copies = deepClone(this.clipboard).map(function (item) {
+ item.id = nextId();
+ item.x = roundCoord(item.x + offsetX);
+ item.y = roundCoord(item.y + offsetY);
+ return item;
+ });
+ if (!this.canPlaceElements(copies, [])) {
+ this.showMessage('warning', '绮樿创鍚庣殑鍏冪礌涓庣幇鏈夊厓绱犻噸鍙犳垨瓒呭嚭鐢诲竷');
+ return;
+ }
+ this.runMutation(function () {
+ self.doc.elements = self.doc.elements.concat(copies);
+ self.selectedIds = copies.map(function (item) { return item.id; });
+ });
+ },
+ canArrayFromElement: function (element) {
+ return !!(element && ARRAY_TEMPLATE_TYPES.indexOf(element.type) >= 0);
+ },
+ getShelfFillSteps: function () {
+ return {
+ row: this.shelfFillForm.rowStep === 'asc' ? 1 : -1,
+ col: this.shelfFillForm.colStep === 'desc' ? -1 : 1
+ };
+ },
+ applyShelfSequenceToArrayCopies: function (template, copies) {
+ if (!template || template.type !== 'shelf' || !copies || !copies.length) {
+ return copies;
+ }
+ var base = parseShelfLocationValue(template.value) || parseShelfLocationValue(this.shelfFillForm.startValue);
+ if (!base) {
+ return copies;
+ }
+ var steps = this.getShelfFillSteps();
+ var horizontal = Math.abs(copies[0].x - template.x) >= Math.abs(copies[0].y - template.y);
+ var direction = 1;
+ if (horizontal) {
+ direction = copies[0].x >= template.x ? 1 : -1;
+ } else {
+ direction = copies[0].y >= template.y ? 1 : -1;
+ }
+ for (var i = 0; i < copies.length; i++) {
+ var offset = i + 1;
+ var row = base.row;
+ var col = base.col;
+ if (horizontal) {
+ col = base.col + steps.col * direction * offset;
+ } else {
+ row = base.row + steps.row * direction * offset;
+ }
+ copies[i].value = formatShelfLocationValue(row, col);
+ }
+ return copies;
+ },
+ buildShelfGridAssignments: function (elements) {
+ if (!elements || !elements.length) {
+ return null;
+ }
+ var clusterAxis = function (list, axis, sizeKey) {
+ var sorted = list.map(function (item) {
+ return {
+ id: item.id,
+ center: item[axis] + item[sizeKey] / 2,
+ size: item[sizeKey]
+ };
+ }).sort(function (a, b) {
+ return a.center - b.center;
+ });
+ var avgSize = sorted.reduce(function (sum, item) {
+ return sum + item.size;
+ }, 0) / sorted.length;
+ var tolerance = Math.max(6, avgSize * 0.45);
+ var groups = [];
+ for (var i = 0; i < sorted.length; i++) {
+ var current = sorted[i];
+ var last = groups.length ? groups[groups.length - 1] : null;
+ if (!last || Math.abs(current.center - last.center) > tolerance) {
+ groups.push({
+ center: current.center,
+ items: [current]
+ });
+ } else {
+ last.items.push(current);
+ last.center = last.items.reduce(function (sum, item) {
+ return sum + item.center;
+ }, 0) / last.items.length;
+ }
+ }
+ var indexById = {};
+ for (var groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+ for (var itemIndex = 0; itemIndex < groups[groupIndex].items.length; itemIndex++) {
+ indexById[groups[groupIndex].items[itemIndex].id] = groupIndex;
+ }
+ }
+ return indexById;
+ };
+ return {
+ rowById: clusterAxis(elements, 'y', 'height'),
+ colById: clusterAxis(elements, 'x', 'width')
+ };
+ },
+ applyShelfAutoFill: function () {
+ var self = this;
+ var shelves = this.selectedShelfElements.slice();
+ if (!shelves.length) {
+ this.showMessage('warning', '璇峰厛閫変腑鑷冲皯涓�涓揣鏋�');
+ return;
+ }
+ var start = parseShelfLocationValue(this.shelfFillForm.startValue);
+ if (!start) {
+ this.showMessage('warning', '璧峰鍊兼牸寮忓繀椤绘槸 鎺�-鍒楋紝渚嬪 12-1');
+ return;
+ }
+ var grid = this.buildShelfGridAssignments(shelves);
+ if (!grid) {
+ return;
+ }
+ var steps = this.getShelfFillSteps();
+ this.runMutation(function () {
+ shelves.forEach(function (item) {
+ var rowIndex = grid.rowById[item.id] || 0;
+ var colIndex = grid.colById[item.id] || 0;
+ item.value = formatShelfLocationValue(
+ start.row + rowIndex * steps.row,
+ start.col + colIndex * steps.col
+ );
+ });
+ if (self.singleSelectedElement && self.singleSelectedElement.type === 'shelf') {
+ self.valueEditorText = self.singleSelectedElement.value || '';
+ }
+ });
+ },
+ buildArrayCopies: function (template, startWorld, currentWorld) {
+ if (!this.doc || !template || !startWorld || !currentWorld || !this.canArrayFromElement(template)) {
+ return [];
+ }
+ var deltaX = currentWorld.x - startWorld.x;
+ var deltaY = currentWorld.y - startWorld.y;
+ if (Math.abs(deltaX) < COORD_EPSILON && Math.abs(deltaY) < COORD_EPSILON) {
+ return [];
+ }
+ var horizontal = Math.abs(deltaX) >= Math.abs(deltaY);
+ var step = horizontal ? template.width : template.height;
+ if (step <= COORD_EPSILON) {
+ return [];
+ }
+ var direction = (horizontal ? deltaX : deltaY) >= 0 ? 1 : -1;
+ var distance;
+ if (horizontal) {
+ distance = direction > 0
+ ? currentWorld.x - (template.x + template.width)
+ : template.x - currentWorld.x;
+ } else {
+ distance = direction > 0
+ ? currentWorld.y - (template.y + template.height)
+ : template.y - currentWorld.y;
+ }
+ var count = Math.max(0, Math.floor((distance + step * 0.5) / step));
+ if (count <= 0) {
+ return [];
+ }
+ var copies = [];
+ for (var i = 1; i <= count; i++) {
+ copies.push({
+ type: template.type,
+ x: roundCoord(template.x + (horizontal ? direction * template.width * i : 0)),
+ y: roundCoord(template.y + (horizontal ? 0 : direction * template.height * i)),
+ width: template.width,
+ height: template.height,
+ value: template.value
+ });
+ }
+ return this.applyShelfSequenceToArrayCopies(template, copies);
+ },
+ duplicateSelection: function () {
+ this.copySelection();
+ this.pasteClipboard();
+ },
+ getElementBounds: function (ids) {
+ if (!this.doc) {
+ return null;
+ }
+ var elements = ids && ids.length ? this.getSelectedElements() : (this.doc.elements || []);
+ if (ids && ids.length) {
+ elements = ids.map(function (id) {
+ return this.findElementById(id);
+ }, this).filter(Boolean);
+ }
+ if (!elements.length) {
+ return null;
+ }
+ var minX = elements[0].x;
+ var minY = elements[0].y;
+ var maxX = elements[0].x + elements[0].width;
+ var maxY = elements[0].y + elements[0].height;
+ for (var i = 1; i < elements.length; i++) {
+ var element = elements[i];
+ minX = Math.min(minX, element.x);
+ minY = Math.min(minY, element.y);
+ maxX = Math.max(maxX, element.x + element.width);
+ maxY = Math.max(maxY, element.y + element.height);
+ }
+ return {
+ x: minX,
+ y: minY,
+ width: maxX - minX,
+ height: maxY - minY
+ };
+ },
+ fitContent: function () {
+ if (!this.doc || !this.pixiApp) {
+ return;
+ }
+ var contentBounds = this.getElementBounds();
+ if (contentBounds && contentBounds.width > 0 && contentBounds.height > 0) {
+ this.fitRect(contentBounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height);
+ return;
+ }
+ this.fitCanvas();
+ },
+ fitCanvas: function () {
+ if (!this.doc || !this.pixiApp) {
+ return;
+ }
+ var renderer = this.pixiApp.renderer;
+ var target = {
+ x: 0,
+ y: 0,
+ width: Math.max(1, this.doc.canvasWidth),
+ height: Math.max(1, this.doc.canvasHeight)
+ };
+ this.fitRect(target, renderer.width, renderer.height);
+ },
+ fitSelection: function () {
+ if (!this.selectedIds.length || !this.pixiApp) {
+ return;
+ }
+ var bounds = this.getElementBounds(this.selectedIds);
+ if (!bounds) {
+ return;
+ }
+ this.fitRect(bounds, this.pixiApp.renderer.width, this.pixiApp.renderer.height);
+ },
+ fitRect: function (rect, viewportWidth, viewportHeight) {
+ var padding = 80;
+ var scale = Math.min(
+ (viewportWidth - padding * 2) / Math.max(rect.width, 1),
+ (viewportHeight - padding * 2) / Math.max(rect.height, 1)
+ );
+ scale = clamp(scale, 0.06, 4);
+ this.camera.scale = scale;
+ this.camera.x = Math.round((viewportWidth - rect.width * scale) / 2 - rect.x * scale);
+ this.camera.y = Math.round((viewportHeight - rect.height * scale) / 2 - rect.y * scale);
+ this.viewZoom = scale;
+ this.markGridSceneDirty();
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ },
+ resetView: function () {
+ this.fitCanvas();
+ },
+ getVisibleWorldRect: function () {
+ if (!this.pixiApp) {
+ return {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ };
+ }
+ return {
+ x: (-this.camera.x) / this.camera.scale,
+ y: (-this.camera.y) / this.camera.scale,
+ width: this.pixiApp.renderer.width / this.camera.scale,
+ height: this.pixiApp.renderer.height / this.camera.scale
+ };
+ },
+ getVisibleCanvasRect: function () {
+ if (!this.doc) {
+ return {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ };
+ }
+ var visible = this.getVisibleWorldRect();
+ var left = clamp(visible.x, 0, this.doc.canvasWidth);
+ var top = clamp(visible.y, 0, this.doc.canvasHeight);
+ var right = clamp(visible.x + visible.width, 0, this.doc.canvasWidth);
+ var bottom = clamp(visible.y + visible.height, 0, this.doc.canvasHeight);
+ return {
+ x: left,
+ y: top,
+ width: Math.max(0, right - left),
+ height: Math.max(0, bottom - top)
+ };
+ },
+ getWorldRectWithPadding: function (screenPadding) {
+ if (!this.doc) {
+ return {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0
+ };
+ }
+ var visible = this.getVisibleWorldRect();
+ var padding = Math.max(screenPadding / this.camera.scale, 24);
+ var left = Math.max(0, visible.x - padding);
+ var top = Math.max(0, visible.y - padding);
+ var right = Math.min(this.doc.canvasWidth, visible.x + visible.width + padding);
+ var bottom = Math.min(this.doc.canvasHeight, visible.y + visible.height + padding);
+ return {
+ x: left,
+ y: top,
+ width: Math.max(0, right - left),
+ height: Math.max(0, bottom - top)
+ };
+ },
+ worldRectContains: function (outer, inner) {
+ if (!outer || !inner) {
+ return false;
+ }
+ return inner.x >= outer.x - COORD_EPSILON
+ && inner.y >= outer.y - COORD_EPSILON
+ && inner.x + inner.width <= outer.x + outer.width + COORD_EPSILON
+ && inner.y + inner.height <= outer.y + outer.height + COORD_EPSILON;
+ },
+ getGridRenderKey: function () {
+ var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200);
+ return minorStep + '|' + (Math.round(this.camera.scale * 8) / 8);
+ },
+ getStaticRenderKey: function () {
+ return (this.camera.scale >= 0.85 ? 'round' : 'flat') + '|' + (Math.round(this.camera.scale * 8) / 8);
+ },
+ scheduleRender: function () {
+ if (this.renderQueued) {
+ return;
+ }
+ this.renderQueued = true;
+ window.requestAnimationFrame(function () {
+ this.renderQueued = false;
+ this.renderScene();
+ }.bind(this));
+ },
+ renderScene: function () {
+ if (!this.pixiApp || !this.doc) {
+ return;
+ }
+ this.mapRoot.position.set(this.camera.x, this.camera.y);
+ this.mapRoot.scale.set(this.camera.scale, this.camera.scale);
+ this.viewZoom = this.camera.scale;
+ var visible = this.getVisibleCanvasRect();
+ var viewportSettled = !this.isZooming && !this.isPanning && !(this.interactionState && this.interactionState.type === 'pan');
+ var gridKeyChanged = this.gridRenderKey !== this.getGridRenderKey();
+ if (this.gridSceneDirty || !this.gridRenderRect || (viewportSettled && gridKeyChanged) || (viewportSettled && !this.worldRectContains(this.gridRenderRect, visible))) {
+ this.renderGrid(this.getWorldRectWithPadding(STATIC_VIEW_PADDING));
+ this.gridSceneDirty = false;
+ }
+ var excludedKey = this.selectionKey(this.getStaticExcludedIds());
+ var staticKeyChanged = this.staticRenderKey !== this.getStaticRenderKey();
+ if (this.staticSceneDirty || !this.staticRenderRect || (viewportSettled && staticKeyChanged)
+ || this.staticExcludedKey !== excludedKey || (viewportSettled && !this.worldRectContains(this.staticRenderRect, visible))) {
+ this.renderStaticElements(this.getWorldRectWithPadding(STATIC_VIEW_PADDING), excludedKey);
+ this.staticSceneDirty = false;
+ }
+ this.renderActiveElements();
+ this.renderLabels();
+ this.renderHover();
+ this.renderSelection();
+ this.renderGuide();
+ this.updateCursor();
+ },
+ getStaticExcludedIds: function () {
+ if (!this.interactionState) {
+ return [];
+ }
+ if (this.interactionState.type === 'move' && this.selectedIds.length) {
+ return this.selectedIds.slice();
+ }
+ if (this.interactionState.type === 'resize' && this.interactionState.elementId) {
+ return [this.interactionState.elementId];
+ }
+ return [];
+ },
+ getRenderableElements: function (excludeIds, renderRect) {
+ if (!this.doc) {
+ return [];
+ }
+ var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING);
+ var candidates = this.querySpatialCandidates(rect, 0, excludeIds);
+ var result = [];
+ for (var i = 0; i < candidates.length; i++) {
+ if (rectIntersects(rect, candidates[i])) {
+ result.push(candidates[i]);
+ }
+ }
+ return result;
+ },
+ renderGrid: function (renderRect) {
+ if (!this.gridLayer || !this.doc) {
+ return;
+ }
+ var visible = renderRect || this.getVisibleWorldRect();
+ var width = this.doc.canvasWidth;
+ var height = this.doc.canvasHeight;
+ var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200);
+ var majorStep = minorStep * 5;
+ var lineWidth = 1 / this.camera.scale;
+ var xStart = Math.max(0, Math.floor(visible.x / minorStep) * minorStep);
+ var yStart = Math.max(0, Math.floor(visible.y / minorStep) * minorStep);
+ var xEnd = Math.min(width, visible.x + visible.width);
+ var yEnd = Math.min(height, visible.y + visible.height);
+
+ this.gridLayer.clear();
+ this.gridLayer.beginFill(0xfafcff, 1);
+ this.gridLayer.drawRect(0, 0, width, height);
+ this.gridLayer.endFill();
+
+ this.gridLayer.lineStyle(lineWidth, 0xdbe4ee, 1);
+ this.gridLayer.drawRect(0, 0, width, height);
+
+ for (var x = xStart; x <= xEnd; x += minorStep) {
+ var colorX = (x % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
+ this.gridLayer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75);
+ this.gridLayer.moveTo(x, 0);
+ this.gridLayer.lineTo(x, height);
+ }
+ for (var y = yStart; y <= yEnd; y += minorStep) {
+ var colorY = (y % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
+ this.gridLayer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75);
+ this.gridLayer.moveTo(0, y);
+ this.gridLayer.lineTo(width, y);
+ }
+ this.gridRenderRect = {
+ x: visible.x,
+ y: visible.y,
+ width: visible.width,
+ height: visible.height
+ };
+ this.gridRenderKey = this.getGridRenderKey();
+ },
+ drawGridPatch: function (rects, layer) {
+ if (!this.doc || !layer || !rects || !rects.length) {
+ return;
+ }
+ var width = this.doc.canvasWidth;
+ var height = this.doc.canvasHeight;
+ var minorStep = this.camera.scale > 1.5 ? 50 : (this.camera.scale > 0.45 ? 100 : 200);
+ var majorStep = minorStep * 5;
+ var lineWidth = 1 / this.camera.scale;
+ for (var i = 0; i < rects.length; i++) {
+ var rect = rects[i];
+ var left = clamp(rect.x - lineWidth, 0, width);
+ var top = clamp(rect.y - lineWidth, 0, height);
+ var right = clamp(rect.x + rect.width + lineWidth, 0, width);
+ var bottom = clamp(rect.y + rect.height + lineWidth, 0, height);
+ if (right <= left || bottom <= top) {
+ continue;
+ }
+ layer.lineStyle(0, 0, 0, 0);
+ layer.beginFill(0xfafcff, 1);
+ layer.drawRect(left, top, right - left, bottom - top);
+ layer.endFill();
+ if (right - left < minorStep || bottom - top < minorStep) {
+ continue;
+ }
+ var xStart = Math.floor(left / minorStep) * minorStep;
+ var yStart = Math.floor(top / minorStep) * minorStep;
+ for (var x = xStart; x <= right; x += minorStep) {
+ if (x < left || x > right) {
+ continue;
+ }
+ var colorX = (x % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
+ layer.lineStyle(lineWidth, colorX, x % majorStep === 0 ? 0.95 : 0.75);
+ layer.moveTo(x, top);
+ layer.lineTo(x, bottom);
+ }
+ for (var y = yStart; y <= bottom; y += minorStep) {
+ if (y < top || y > bottom) {
+ continue;
+ }
+ var colorY = (y % majorStep === 0) ? 0xc9d7e6 : 0xe4ebf3;
+ layer.lineStyle(lineWidth, colorY, y % majorStep === 0 ? 0.95 : 0.75);
+ layer.moveTo(left, y);
+ layer.lineTo(right, y);
+ }
+ }
+ },
+ drawPatchObjects: function (rects, excludeIds) {
+ if (!rects || !rects.length || !this.patchObjectLayer) {
+ return;
+ }
+ var seen = {};
+ var elements = [];
+ for (var i = 0; i < rects.length; i++) {
+ var candidates = this.querySpatialCandidates(rects[i], 0, excludeIds);
+ for (var j = 0; j < candidates.length; j++) {
+ var item = candidates[j];
+ if (!seen[item.id] && rectIntersects(rects[i], item)) {
+ seen[item.id] = true;
+ elements.push(item);
+ }
+ }
+ }
+ if (!elements.length) {
+ return;
+ }
+ this.drawElementsToLayers(elements, this.patchObjectLayer, this.patchObjectLayer);
+ },
+ drawElementsToLayers: function (elements, trackLayer, nodeLayer) {
+ var lineWidth = 1 / this.camera.scale;
+ var useRounded = this.camera.scale >= 0.85;
+ var radius = Math.max(6 / this.camera.scale, 2);
+ var buckets = {};
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ var bucketKey = (element.type === 'shelf' ? 'node' : 'track') + ':' + element.type;
+ if (!buckets[bucketKey]) {
+ buckets[bucketKey] = [];
+ }
+ buckets[bucketKey].push(element);
+ }
+ for (var bucketKey in buckets) {
+ if (!buckets.hasOwnProperty(bucketKey)) {
+ continue;
+ }
+ var parts = bucketKey.split(':');
+ var type = parts[1];
+ var meta = getTypeMeta(type);
+ var layer = parts[0] === 'node' ? nodeLayer : trackLayer;
+ layer.lineStyle(lineWidth, meta.border, 1);
+ layer.beginFill(meta.fill, 0.92);
+ var bucket = buckets[bucketKey];
+ for (var j = 0; j < bucket.length; j++) {
+ var item = bucket[j];
+ if (useRounded) {
+ layer.drawRoundedRect(item.x, item.y, item.width, item.height, radius);
+ } else {
+ layer.drawRect(item.x, item.y, item.width, item.height);
+ }
+ }
+ layer.endFill();
+ }
+ },
+ ensureStaticSprite: function (poolName, index) {
+ var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool;
+ var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer;
+ if (pool[index]) {
+ return pool[index];
+ }
+ var sprite = new PIXI.Sprite(PIXI.Texture.WHITE);
+ sprite.position.set(0, 0);
+ sprite.anchor.set(0, 0);
+ sprite.visible = false;
+ sprite.alpha = 0;
+ layer.addChild(sprite);
+ pool[index] = sprite;
+ return sprite;
+ },
+ hideUnusedStaticSprites: function (pool, fromIndex) {
+ for (var i = fromIndex; i < pool.length; i++) {
+ pool[i].visible = false;
+ pool[i].alpha = 0;
+ pool[i].width = 0;
+ pool[i].height = 0;
+ pool[i].position.set(-99999, -99999);
+ }
+ },
+ pruneStaticSpritePool: function (poolName, keepCount, slack) {
+ var pool = poolName === 'node' ? this.staticNodeSpritePool : this.staticTrackSpritePool;
+ var layer = poolName === 'node' ? this.staticNodeSpriteLayer : this.staticTrackSpriteLayer;
+ var target = Math.max(0, keepCount + Math.max(0, slack || 0));
+ if (!pool || !layer || pool.length <= target) {
+ return;
+ }
+ for (var i = pool.length - 1; i >= target; i--) {
+ var sprite = pool[i];
+ layer.removeChild(sprite);
+ if (sprite && sprite.destroy) {
+ sprite.destroy();
+ }
+ pool.pop();
+ }
+ },
+ drawElementsToSpriteLayers: function (elements) {
+ var trackCount = 0;
+ var nodeCount = 0;
+ for (var i = 0; i < elements.length; i++) {
+ var item = elements[i];
+ var meta = getTypeMeta(item.type);
+ var poolName = item.type === 'shelf' ? 'node' : 'track';
+ var sprite = this.ensureStaticSprite(poolName, poolName === 'node' ? nodeCount : trackCount);
+ sprite.visible = true;
+ sprite.position.set(item.x, item.y);
+ sprite.width = item.width;
+ sprite.height = item.height;
+ sprite.tint = meta.fill;
+ sprite.alpha = 0.92;
+ if (poolName === 'node') {
+ nodeCount += 1;
+ } else {
+ trackCount += 1;
+ }
+ }
+ this.hideUnusedStaticSprites(this.staticTrackSpritePool, trackCount);
+ this.hideUnusedStaticSprites(this.staticNodeSpritePool, nodeCount);
+ if (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD) {
+ this.pruneStaticSpritePool('track', trackCount, STATIC_SPRITE_POOL_SLACK);
+ this.pruneStaticSpritePool('node', nodeCount, STATIC_SPRITE_POOL_SLACK);
+ }
+ },
+ simplifyRenderableElements: function (elements) {
+ if (!elements || elements.length < 2) {
+ return elements || [];
+ }
+ var sorted = elements.slice().sort(function (a, b) {
+ if (a.type !== b.type) {
+ return a.type < b.type ? -1 : 1;
+ }
+ if (Math.abs(a.y - b.y) > COORD_EPSILON) {
+ return a.y - b.y;
+ }
+ if (Math.abs(a.height - b.height) > COORD_EPSILON) {
+ return a.height - b.height;
+ }
+ return a.x - b.x;
+ });
+ var result = [];
+ var current = null;
+ for (var i = 0; i < sorted.length; i++) {
+ var item = sorted[i];
+ if (!current) {
+ current = {
+ type: item.type,
+ x: item.x,
+ y: item.y,
+ width: item.width,
+ height: item.height
+ };
+ continue;
+ }
+ var currentRight = current.x + current.width;
+ var itemRight = item.x + item.width;
+ var sameBand = current.type === item.type
+ && Math.abs(current.y - item.y) <= 0.5
+ && Math.abs(current.height - item.height) <= 0.5;
+ var joinable = item.x <= currentRight + 0.5;
+ if (sameBand && joinable) {
+ current.width = roundCoord(Math.max(currentRight, itemRight) - current.x);
+ } else {
+ result.push(current);
+ current = {
+ type: item.type,
+ x: item.x,
+ y: item.y,
+ width: item.width,
+ height: item.height
+ };
+ }
+ }
+ if (current) {
+ result.push(current);
+ }
+ return result;
+ },
+ renderStaticElements: function (renderRect, excludedKey) {
+ if (!this.doc) {
+ return;
+ }
+ this.trackLayer.clear();
+ this.nodeLayer.clear();
+ this.eraseLayer.clear();
+ this.patchObjectLayer.clear();
+ var renderableElements = this.getRenderableElements(this.getStaticExcludedIds(), renderRect);
+ var useSpriteMode = this.camera.scale < STATIC_SPRITE_SCALE_THRESHOLD;
+ var shouldSimplify = this.camera.scale < STATIC_SIMPLIFY_SCALE_THRESHOLD
+ || (this.camera.scale < DENSE_SIMPLIFY_SCALE_THRESHOLD && renderableElements.length > DENSE_SIMPLIFY_ELEMENT_THRESHOLD);
+ this.staticTrackSpriteLayer.visible = useSpriteMode;
+ this.staticNodeSpriteLayer.visible = useSpriteMode;
+ this.trackLayer.visible = !useSpriteMode;
+ this.nodeLayer.visible = !useSpriteMode;
+ if (useSpriteMode) {
+ if (shouldSimplify) {
+ renderableElements = this.simplifyRenderableElements(renderableElements);
+ }
+ this.drawElementsToSpriteLayers(renderableElements);
+ } else {
+ this.hideUnusedStaticSprites(this.staticTrackSpritePool, 0);
+ this.hideUnusedStaticSprites(this.staticNodeSpritePool, 0);
+ this.drawElementsToLayers(renderableElements, this.trackLayer, this.nodeLayer);
+ }
+ var rect = renderRect || this.getWorldRectWithPadding(STATIC_VIEW_PADDING);
+ this.staticRenderRect = {
+ x: rect.x,
+ y: rect.y,
+ width: rect.width,
+ height: rect.height
+ };
+ this.staticRenderKey = this.getStaticRenderKey();
+ this.staticExcludedKey = excludedKey != null ? excludedKey : this.selectionKey(this.getStaticExcludedIds());
+ this.pendingStaticCommit = null;
+ },
+ renderActiveElements: function () {
+ this.activeLayer.clear();
+ this.eraseLayer.clear();
+ this.patchObjectLayer.clear();
+ var activeIds = this.getStaticExcludedIds();
+ if (!activeIds.length) {
+ return;
+ }
+ var activeElements = [];
+ for (var idx = 0; idx < activeIds.length; idx++) {
+ var element = this.findElementById(activeIds[idx]);
+ if (element) {
+ activeElements.push(element);
+ }
+ }
+ if (!activeElements.length) {
+ return;
+ }
+ this.drawElementsToLayers(activeElements, this.activeLayer, this.activeLayer);
+ },
+ getLabelText: function (element) {
+ var meta = getTypeMeta(element.type);
+ var value = safeParseJson(element.value);
+ if (element.type === 'devp' && value) {
+ var station = value.stationId != null ? String(value.stationId) : '';
+ var arrows = formatDirectionArrows(value.direction);
+ if (station && arrows) {
+ return element.height > element.width * 1.15 ? (station + '\n' + arrows) : (station + ' ' + arrows);
+ }
+ if (station) {
+ return station;
+ }
+ if (arrows) {
+ return arrows;
+ }
+ return meta.shortLabel;
+ }
+ if ((element.type === 'crn' || element.type === 'dualCrn' || element.type === 'rgv') && value) {
+ if (value.deviceNo != null) {
+ return meta.shortLabel + ' ' + value.deviceNo;
+ }
+ if (value.crnNo != null) {
+ return meta.shortLabel + ' ' + value.crnNo;
+ }
+ if (value.rgvNo != null) {
+ return meta.shortLabel + ' ' + value.rgvNo;
+ }
+ }
+ if (element.value && element.value.length <= 18 && element.value.indexOf('{') !== 0) {
+ return element.value;
+ }
+ return meta.shortLabel;
+ },
+ ensureLabelSprite: function (index) {
+ if (this.labelPool[index]) {
+ return this.labelPool[index];
+ }
+ var label = new PIXI.Text('', {
+ fontFamily: 'Avenir Next, PingFang SC, Microsoft YaHei, sans-serif',
+ fontSize: 12,
+ fontWeight: '600',
+ fill: 0x223448,
+ align: 'center'
+ });
+ label.anchor.set(0.5);
+ this.labelLayer.addChild(label);
+ this.labelPool[index] = label;
+ return label;
+ },
+ getLabelRenderBudget: function () {
+ if (!this.pixiApp || !this.pixiApp.renderer) {
+ return MIN_LABEL_COUNT;
+ }
+ var renderer = this.pixiApp.renderer;
+ var viewportArea = renderer.width * renderer.height;
+ return clamp(Math.round(viewportArea / 12000), MIN_LABEL_COUNT, MAX_LABEL_COUNT);
+ },
+ getLabelMinScreenWidth: function (text) {
+ var lines = String(text || '').split('\n');
+ var length = 0;
+ for (var i = 0; i < lines.length; i++) {
+ length = Math.max(length, String(lines[i] || '').trim().length);
+ }
+ if (length <= 4) {
+ return 26;
+ }
+ if (length <= 8) {
+ return 40;
+ }
+ if (length <= 12) {
+ return 52;
+ }
+ return 64;
+ },
+ getLabelMinScreenHeight: function (text) {
+ var lines = String(text || '').split('\n');
+ var length = 0;
+ for (var i = 0; i < lines.length; i++) {
+ length = Math.max(length, String(lines[i] || '').trim().length);
+ }
+ var lineHeight = length <= 4 ? 14 : 18;
+ return lineHeight * Math.max(lines.length, 1);
+ },
+ renderLabels: function () {
+ if (!this.doc) {
+ return;
+ }
+ var capability = this.ensureLabelCapability();
+ if (capability.maxWidth * this.camera.scale < ABS_MIN_LABEL_SCREEN_WIDTH
+ || capability.maxHeight * this.camera.scale < ABS_MIN_LABEL_SCREEN_HEIGHT) {
+ this.labelLayer.visible = false;
+ return;
+ }
+ if (this.isZooming || this.isPanning || this.camera.scale < MIN_LABEL_SCALE
+ || (this.interactionState && (this.interactionState.type === 'move' || this.interactionState.type === 'resize' || this.interactionState.type === 'pan'))) {
+ this.labelLayer.visible = false;
+ return;
+ }
+ this.labelLayer.visible = true;
+ var visible = this.getVisibleWorldRect();
+ var elements = this.querySpatialCandidates(visible, 0, []);
+ if (elements.length > DENSE_LABEL_HIDE_ELEMENT_THRESHOLD && this.camera.scale < DENSE_LABEL_HIDE_SCALE_THRESHOLD) {
+ this.labelLayer.visible = false;
+ return;
+ }
+ var hasRoomForAnyLabel = false;
+ for (var roomIdx = 0; roomIdx < elements.length; roomIdx++) {
+ var candidate = elements[roomIdx];
+ if (candidate.width * this.camera.scale >= ABS_MIN_LABEL_SCREEN_WIDTH
+ && candidate.height * this.camera.scale >= ABS_MIN_LABEL_SCREEN_HEIGHT) {
+ hasRoomForAnyLabel = true;
+ break;
+ }
+ }
+ if (!hasRoomForAnyLabel) {
+ this.labelLayer.visible = false;
+ return;
+ }
+ var visibleElements = [];
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ var text = this.getLabelText(element);
+ if (!text) {
+ continue;
+ }
+ if (!rectIntersects(visible, element)) {
+ continue;
+ }
+ if (element.width * this.camera.scale < this.getLabelMinScreenWidth(text) || element.height * this.camera.scale < this.getLabelMinScreenHeight(text)) {
+ continue;
+ }
+ visibleElements.push({
+ element: element,
+ text: text
+ });
+ }
+ visibleElements.sort(function (a, b) {
+ return (b.element.width * b.element.height) - (a.element.width * a.element.height);
+ });
+ var labelBudget = this.getLabelRenderBudget();
+ if (visibleElements.length > labelBudget) {
+ visibleElements = visibleElements.slice(0, labelBudget);
+ }
+ for (var j = 0; j < visibleElements.length; j++) {
+ var item = visibleElements[j].element;
+ var label = this.ensureLabelSprite(j);
+ label.visible = true;
+ label.text = visibleElements[j].text;
+ label.position.set(item.x + item.width / 2, item.y + item.height / 2);
+ label.scale.set(1 / this.camera.scale, 1 / this.camera.scale);
+ label.alpha = this.selectedIds.indexOf(item.id) >= 0 ? 1 : 0.88;
+ }
+ for (var k = visibleElements.length; k < this.labelPool.length; k++) {
+ this.labelPool[k].visible = false;
+ }
+ },
+ renderHover: function () {
+ this.hoverLayer.clear();
+ if (this.interactionState || !this.hoverElementId || this.selectedIds.indexOf(this.hoverElementId) >= 0) {
+ return;
+ }
+ var element = this.findElementById(this.hoverElementId);
+ if (!element) {
+ return;
+ }
+ var lineWidth = 2 / this.camera.scale;
+ this.hoverLayer.lineStyle(lineWidth, 0x2f79d6, 0.95);
+ this.hoverLayer.drawRoundedRect(element.x, element.y, element.width, element.height, Math.max(6 / this.camera.scale, 2));
+ },
+ renderSelection: function () {
+ this.selectionLayer.clear();
+ if (!this.selectedIds.length || (this.interactionState && (this.interactionState.type === 'move' || this.interactionState.type === 'resize'))) {
+ return;
+ }
+ var elements = this.getSelectedElements();
+ var lineWidth = 2 / this.camera.scale;
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i];
+ this.selectionLayer.lineStyle(lineWidth, 0x2568b8, 1);
+ this.selectionLayer.beginFill(0x2f79d6, 0.07);
+ this.selectionLayer.drawRoundedRect(element.x, element.y, element.width, element.height, Math.max(6 / this.camera.scale, 2));
+ this.selectionLayer.endFill();
+ }
+ if (elements.length !== 1) {
+ return;
+ }
+ var handleSize = HANDLE_SCREEN_SIZE / this.camera.scale;
+ var handlePositions = this.getHandlePositions(elements[0]);
+ this.selectionLayer.lineStyle(1 / this.camera.scale, 0x1d5ea9, 1);
+ this.selectionLayer.beginFill(0xffffff, 1);
+ for (var key in handlePositions) {
+ if (!handlePositions.hasOwnProperty(key)) {
+ continue;
+ }
+ var pos = handlePositions[key];
+ this.selectionLayer.drawRect(pos.x - handleSize / 2, pos.y - handleSize / 2, handleSize, handleSize);
+ }
+ this.selectionLayer.endFill();
+ },
+ renderGuide: function () {
+ this.guideLayer.clear();
+ if (this.guideText) {
+ this.guideText.visible = false;
+ }
+ if (!this.interactionState) {
+ return;
+ }
+ var state = this.interactionState;
+ if (state.type === 'draw' && state.rect && state.rect.width > 0 && state.rect.height > 0) {
+ var drawMeta = getTypeMeta(state.elementType);
+ this.guideLayer.lineStyle(2 / this.camera.scale, drawMeta.border, 0.95);
+ this.guideLayer.beginFill(drawMeta.fill, 0.18);
+ this.guideLayer.drawRoundedRect(state.rect.x, state.rect.y, state.rect.width, state.rect.height, Math.max(6 / this.camera.scale, 2));
+ this.guideLayer.endFill();
+ return;
+ }
+ if (state.type === 'array' && state.template) {
+ var previewItems = state.previewItems || [];
+ var arrayMeta = getTypeMeta(state.template.type);
+ var lineWidth = 2 / this.camera.scale;
+ var templateCenterX = state.template.x + state.template.width / 2;
+ var templateCenterY = state.template.y + state.template.height / 2;
+ this.guideLayer.lineStyle(lineWidth, arrayMeta.border, 0.9);
+ this.guideLayer.moveTo(templateCenterX, templateCenterY);
+ this.guideLayer.lineTo(state.currentWorld.x, state.currentWorld.y);
+ if (!previewItems.length) {
+ return;
+ }
+ this.guideLayer.lineStyle(1 / this.camera.scale, arrayMeta.border, 0.8);
+ this.guideLayer.beginFill(arrayMeta.fill, 0.2);
+ for (var previewIndex = 0; previewIndex < previewItems.length; previewIndex++) {
+ var preview = previewItems[previewIndex];
+ this.guideLayer.drawRoundedRect(preview.x, preview.y, preview.width, preview.height, Math.max(6 / this.camera.scale, 2));
+ }
+ this.guideLayer.endFill();
+ if (this.guideText) {
+ this.guideText.text = '灏嗙敓鎴� ' + previewItems.length + ' 涓�';
+ this.guideText.position.set(state.currentWorld.x, state.currentWorld.y - 10 / this.camera.scale);
+ this.guideText.scale.set(1 / this.camera.scale);
+ this.guideText.visible = true;
+ }
+ return;
+ }
+ if (state.type === 'marquee') {
+ var rect = buildRectFromPoints(state.startWorld, state.currentWorld);
+ if (rect.width <= 0 || rect.height <= 0) {
+ return;
+ }
+ this.guideLayer.lineStyle(2 / this.camera.scale, 0x2f79d6, 0.92);
+ this.guideLayer.beginFill(0x2f79d6, 0.06);
+ this.guideLayer.drawRect(rect.x, rect.y, rect.width, rect.height);
+ this.guideLayer.endFill();
+ }
+ },
+ pointerToWorld: function (event) {
+ var rect = this.pixiApp.view.getBoundingClientRect();
+ var screenX = event.clientX - rect.left;
+ var screenY = event.clientY - rect.top;
+ return {
+ screenX: screenX,
+ screenY: screenY,
+ x: roundCoord((screenX - this.camera.x) / this.camera.scale),
+ y: roundCoord((screenY - this.camera.y) / this.camera.scale)
+ };
+ },
+ isWithinCanvas: function (rect) {
+ if (!this.doc) {
+ return false;
+ }
+ return rect.x >= -COORD_EPSILON && rect.y >= -COORD_EPSILON
+ && rect.x + rect.width <= this.doc.canvasWidth + COORD_EPSILON
+ && rect.y + rect.height <= this.doc.canvasHeight + COORD_EPSILON;
+ },
+ canPlaceElements: function (elements, excludeIds) {
+ excludeIds = excludeIds || [];
+ for (var i = 0; i < elements.length; i++) {
+ if (!this.isWithinCanvas(elements[i])) {
+ return false;
+ }
+ if (this.hasOverlap(elements[i], excludeIds.concat([elements[i].id]))) {
+ return false;
+ }
+ }
+ return true;
+ },
+ hasOverlap: function (candidate, excludeIds) {
+ if (!this.doc) {
+ return false;
+ }
+ var elements = this.querySpatialCandidates(candidate, COORD_EPSILON, excludeIds);
+ for (var i = 0; i < elements.length; i++) {
+ var item = elements[i];
+ if (rectsOverlap(candidate, item)) {
+ return true;
+ }
+ }
+ return false;
+ },
+ snapToleranceWorld: function () {
+ return Math.max(1, EDGE_SNAP_SCREEN_TOLERANCE / this.camera.scale);
+ },
+ collectMoveSnap: function (baseItems, dx, dy, excludeIds) {
+ if (!this.doc || !baseItems || !baseItems.length) {
+ return { dx: 0, dy: 0 };
+ }
+ var tolerance = this.snapToleranceWorld();
+ var bestDx = null;
+ var bestDy = null;
+ for (var i = 0; i < baseItems.length; i++) {
+ var moving = baseItems[i];
+ var movedLeft = moving.x + dx;
+ var movedRight = movedLeft + moving.width;
+ var movedTop = moving.y + dy;
+ var movedBottom = movedTop + moving.height;
+ var candidates = this.querySpatialCandidates({
+ x: movedLeft,
+ y: movedTop,
+ width: moving.width,
+ height: moving.height
+ }, tolerance, excludeIds);
+ for (var j = 0; j < candidates.length; j++) {
+ var other = candidates[j];
+ var otherLeft = other.x;
+ var otherRight = other.x + other.width;
+ var otherTop = other.y;
+ var otherBottom = other.y + other.height;
+ if (rangesNearOrOverlap(movedTop, movedBottom, otherTop, otherBottom, tolerance)) {
+ var horizontalCandidates = [
+ otherLeft - movedRight,
+ otherRight - movedLeft,
+ otherLeft - movedLeft,
+ otherRight - movedRight
+ ];
+ for (var hx = 0; hx < horizontalCandidates.length; hx++) {
+ var deltaX = horizontalCandidates[hx];
+ if (Math.abs(deltaX) <= tolerance && (bestDx === null || Math.abs(deltaX) < Math.abs(bestDx))) {
+ bestDx = deltaX;
+ }
+ }
+ }
+ if (rangesNearOrOverlap(movedLeft, movedRight, otherLeft, otherRight, tolerance)) {
+ var verticalCandidates = [
+ otherTop - movedBottom,
+ otherBottom - movedTop,
+ otherTop - movedTop,
+ otherBottom - movedBottom
+ ];
+ for (var vy = 0; vy < verticalCandidates.length; vy++) {
+ var deltaY = verticalCandidates[vy];
+ if (Math.abs(deltaY) <= tolerance && (bestDy === null || Math.abs(deltaY) < Math.abs(bestDy))) {
+ bestDy = deltaY;
+ }
+ }
+ }
+ }
+ }
+ return {
+ dx: bestDx == null ? 0 : bestDx,
+ dy: bestDy == null ? 0 : bestDy
+ };
+ },
+ collectResizeSnap: function (rect, handle, excludeIds) {
+ if (!this.doc || !rect) {
+ return null;
+ }
+ var tolerance = this.snapToleranceWorld();
+ var left = rect.x;
+ var right = rect.x + rect.width;
+ var top = rect.y;
+ var bottom = rect.y + rect.height;
+ var bestLeft = null;
+ var bestRight = null;
+ var bestTop = null;
+ var bestBottom = null;
+ function pickBest(current, candidate) {
+ if (candidate == null) {
+ return current;
+ }
+ if (current == null || Math.abs(candidate) < Math.abs(current)) {
+ return candidate;
+ }
+ return current;
+ }
+ if (handle.indexOf('w') >= 0) {
+ bestLeft = pickBest(bestLeft, -left);
+ }
+ if (handle.indexOf('e') >= 0) {
+ bestRight = pickBest(bestRight, this.doc.canvasWidth - right);
+ }
+ if (handle.indexOf('n') >= 0) {
+ bestTop = pickBest(bestTop, -top);
+ }
+ if (handle.indexOf('s') >= 0) {
+ bestBottom = pickBest(bestBottom, this.doc.canvasHeight - bottom);
+ }
+ var elements = this.querySpatialCandidates(rect, tolerance, excludeIds);
+ for (var i = 0; i < elements.length; i++) {
+ var other = elements[i];
+ var otherLeft = other.x;
+ var otherRight = other.x + other.width;
+ var otherTop = other.y;
+ var otherBottom = other.y + other.height;
+ if (rangesNearOrOverlap(top, bottom, otherTop, otherBottom, tolerance)) {
+ if (handle.indexOf('w') >= 0) {
+ bestLeft = pickBest(bestLeft, otherLeft - left);
+ bestLeft = pickBest(bestLeft, otherRight - left);
+ }
+ if (handle.indexOf('e') >= 0) {
+ bestRight = pickBest(bestRight, otherLeft - right);
+ bestRight = pickBest(bestRight, otherRight - right);
+ }
+ }
+ if (rangesNearOrOverlap(left, right, otherLeft, otherRight, tolerance)) {
+ if (handle.indexOf('n') >= 0) {
+ bestTop = pickBest(bestTop, otherTop - top);
+ bestTop = pickBest(bestTop, otherBottom - top);
+ }
+ if (handle.indexOf('s') >= 0) {
+ bestBottom = pickBest(bestBottom, otherTop - bottom);
+ bestBottom = pickBest(bestBottom, otherBottom - bottom);
+ }
+ }
+ }
+ if (bestLeft != null && Math.abs(bestLeft) > tolerance) {
+ bestLeft = null;
+ }
+ if (bestRight != null && Math.abs(bestRight) > tolerance) {
+ bestRight = null;
+ }
+ if (bestTop != null && Math.abs(bestTop) > tolerance) {
+ bestTop = null;
+ }
+ if (bestBottom != null && Math.abs(bestBottom) > tolerance) {
+ bestBottom = null;
+ }
+ return {
+ left: bestLeft,
+ right: bestRight,
+ top: bestTop,
+ bottom: bestBottom
+ };
+ },
+ hitTestElement: function (point) {
+ if (!this.doc) {
+ return null;
+ }
+ var candidates = this.querySpatialCandidates({
+ x: point.x,
+ y: point.y,
+ width: 0,
+ height: 0
+ }, 0, []);
+ if (!candidates.length) {
+ return null;
+ }
+ var candidateMap = {};
+ for (var c = 0; c < candidates.length; c++) {
+ candidateMap[candidates[c].id] = true;
+ }
+ var elements = this.doc.elements || [];
+ for (var i = elements.length - 1; i >= 0; i--) {
+ var element = elements[i];
+ if (!candidateMap[element.id]) {
+ continue;
+ }
+ if (point.x >= element.x && point.x <= element.x + element.width
+ && point.y >= element.y && point.y <= element.y + element.height) {
+ return element;
+ }
+ }
+ return null;
+ },
+ getHandlePositions: function (element) {
+ var x = element.x;
+ var y = element.y;
+ var w = element.width;
+ var h = element.height;
+ var cx = x + w / 2;
+ var cy = y + h / 2;
+ return {
+ nw: { x: x, y: y },
+ n: { x: cx, y: y },
+ ne: { x: x + w, y: y },
+ e: { x: x + w, y: cy },
+ se: { x: x + w, y: y + h },
+ s: { x: cx, y: y + h },
+ sw: { x: x, y: y + h },
+ w: { x: x, y: cy }
+ };
+ },
+ getResizeHandleAt: function (point, element) {
+ var handlePositions = this.getHandlePositions(element);
+ var baseTolerance = HANDLE_SCREEN_SIZE / this.camera.scale;
+ var sizeLimitedTolerance = Math.max(Math.min(element.width, element.height) / 4, 3 / this.camera.scale);
+ var tolerance = Math.min(baseTolerance, sizeLimitedTolerance);
+ var bestHandle = '';
+ var bestDistance = Infinity;
+ for (var key in handlePositions) {
+ if (!handlePositions.hasOwnProperty(key)) {
+ continue;
+ }
+ var pos = handlePositions[key];
+ var dx = Math.abs(point.x - pos.x);
+ var dy = Math.abs(point.y - pos.y);
+ if (dx <= tolerance && dy <= tolerance) {
+ var distance = dx + dy;
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestHandle = key;
+ }
+ }
+ }
+ return bestHandle;
+ },
+ cursorForHandle: function (handle) {
+ if (handle === 'nw' || handle === 'se') {
+ return 'nwse-resize';
+ }
+ if (handle === 'ne' || handle === 'sw') {
+ return 'nesw-resize';
+ }
+ if (handle === 'n' || handle === 's') {
+ return 'ns-resize';
+ }
+ if (handle === 'e' || handle === 'w') {
+ return 'ew-resize';
+ }
+ return 'default';
+ },
+ updateCursor: function () {
+ if (!this.pixiApp) {
+ return;
+ }
+ var cursor = 'default';
+ if (this.interactionState) {
+ if (this.interactionState.type === 'pan') {
+ cursor = 'grabbing';
+ } else if (this.interactionState.type === 'draw' || this.interactionState.type === 'marquee') {
+ cursor = 'crosshair';
+ } else if (this.interactionState.type === 'array') {
+ cursor = 'crosshair';
+ } else if (this.interactionState.type === 'move') {
+ cursor = 'move';
+ } else if (this.interactionState.type === 'movePending') {
+ cursor = 'grab';
+ } else if (this.interactionState.type === 'resize') {
+ cursor = this.cursorForHandle(this.interactionState.handle);
+ }
+ } else if (this.spacePressed || this.activeTool === 'pan') {
+ cursor = 'grab';
+ } else if (DRAW_TYPES.indexOf(this.activeTool) >= 0 || this.activeTool === 'marquee' || this.activeTool === 'array') {
+ cursor = 'crosshair';
+ } else if (this.singleSelectedElement) {
+ var point = this.lastPointerWorld || null;
+ if (point) {
+ var handle = this.getResizeHandleAt(point, this.singleSelectedElement);
+ cursor = handle ? this.cursorForHandle(handle) : 'default';
+ }
+ if (cursor === 'default' && this.hoverElementId) {
+ cursor = 'move';
+ } else if (cursor === 'default') {
+ cursor = 'grab';
+ }
+ } else {
+ cursor = this.hoverElementId ? 'move' : 'grab';
+ }
+ if (cursor !== this.lastCursor) {
+ this.lastCursor = cursor;
+ this.pixiApp.view.style.cursor = cursor;
+ }
+ },
+ startPan: function (point) {
+ this.cancelDeferredStaticRebuild();
+ this.cancelPanRefresh();
+ if (this.zoomRefreshTimer) {
+ window.clearTimeout(this.zoomRefreshTimer);
+ this.zoomRefreshTimer = null;
+ this.isZooming = false;
+ this.pendingViewportRefresh = true;
+ }
+ this.isPanning = true;
+ this.interactionState = {
+ type: 'pan',
+ startScreen: {
+ x: point.screenX,
+ y: point.screenY
+ },
+ startCamera: {
+ x: this.camera.x,
+ y: this.camera.y
+ }
+ };
+ this.updateCursor();
+ },
+ startMarquee: function (point, additive) {
+ this.cancelDeferredStaticRebuild();
+ this.interactionState = {
+ type: 'marquee',
+ additive: !!additive,
+ startWorld: { x: point.x, y: point.y },
+ currentWorld: { x: point.x, y: point.y }
+ };
+ this.updateCursor();
+ },
+ startDraw: function (point) {
+ this.cancelDeferredStaticRebuild();
+ this.interactionState = {
+ type: 'draw',
+ beforeSnapshot: this.snapshotDoc(this.doc),
+ elementType: this.activeTool,
+ startWorld: { x: point.x, y: point.y },
+ rect: { x: point.x, y: point.y, width: 0, height: 0 }
+ };
+ this.updateCursor();
+ },
+ startArray: function (point, element) {
+ if (!this.canArrayFromElement(element)) {
+ this.showMessage('warning', '闃靛垪宸ュ叿褰撳墠鍙敮鎸佽揣鏋躲�丆RN銆佸弻宸ヤ綅鍜� RGV');
+ return;
+ }
+ this.cancelDeferredStaticRebuild();
+ this.interactionState = {
+ type: 'array',
+ beforeSnapshot: this.snapshotDoc(this.doc),
+ template: {
+ id: element.id,
+ type: element.type,
+ x: element.x,
+ y: element.y,
+ width: element.width,
+ height: element.height,
+ value: element.value
+ },
+ startWorld: { x: point.x, y: point.y },
+ currentWorld: { x: point.x, y: point.y },
+ previewItems: []
+ };
+ this.updateCursor();
+ },
+ startMove: function (point) {
+ var selected = this.getSelectedElements();
+ if (!selected.length) {
+ return;
+ }
+ this.cancelDeferredStaticRebuild();
+ var baseItems = selected.map(function (item) {
+ return {
+ id: item.id,
+ x: item.x,
+ y: item.y,
+ width: item.width,
+ height: item.height,
+ value: item.value,
+ type: item.type
+ };
+ });
+ this.interactionState = {
+ type: 'movePending',
+ beforeSnapshot: this.snapshotDoc(this.doc),
+ startScreen: { x: point.screenX, y: point.screenY },
+ startWorld: { x: point.x, y: point.y },
+ baseItems: baseItems
+ };
+ this.updateCursor();
+ },
+ startResize: function (point, element, handle) {
+ this.cancelDeferredStaticRebuild();
+ this.interactionState = {
+ type: 'resize',
+ handle: handle,
+ elementId: element.id,
+ beforeSnapshot: this.snapshotDoc(this.doc),
+ baseRect: {
+ x: element.x,
+ y: element.y,
+ width: element.width,
+ height: element.height
+ }
+ };
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ this.updateCursor();
+ },
+ onCanvasPointerDown: function (event) {
+ if (!this.doc || !this.pixiApp) {
+ return;
+ }
+ if (event.button !== 0 && event.button !== 1) {
+ return;
+ }
+ if (this.pixiApp.view.setPointerCapture && event.pointerId != null) {
+ try {
+ this.pixiApp.view.setPointerCapture(event.pointerId);
+ } catch (ignore) {
+ }
+ }
+ this.currentPointerId = event.pointerId;
+ var point = this.pointerToWorld(event);
+ this.lastPointerWorld = point;
+ this.pointerStatus = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y);
+ if (this.spacePressed || this.activeTool === 'pan' || event.button === 1) {
+ this.startPan(point);
+ return;
+ }
+ if (DRAW_TYPES.indexOf(this.activeTool) >= 0) {
+ this.startDraw(point);
+ return;
+ }
+ if (this.activeTool === 'marquee') {
+ this.startMarquee(point, event.shiftKey);
+ return;
+ }
+ if (this.activeTool === 'array') {
+ var arrayHit = this.hitTestElement(point);
+ var arrayTemplate = arrayHit || this.singleSelectedElement;
+ if (arrayHit && this.selectedIds.indexOf(arrayHit.id) < 0) {
+ this.setSelectedIds([arrayHit.id]);
+ arrayTemplate = arrayHit;
+ }
+ if (!arrayTemplate) {
+ this.showMessage('warning', '璇峰厛閫変腑涓�涓揣鏋舵垨杞ㄩ亾浣滀负闃靛垪妯℃澘');
+ return;
+ }
+ this.startArray(point, arrayTemplate);
+ return;
+ }
+
+ var selected = this.singleSelectedElement;
+ var handle = selected ? this.getResizeHandleAt(point, selected) : '';
+ if (handle) {
+ this.startResize(point, selected, handle);
+ return;
+ }
+
+ var hit = this.hitTestElement(point);
+ if (hit) {
+ if (event.shiftKey) {
+ var index = this.selectedIds.indexOf(hit.id);
+ if (index >= 0) {
+ var nextIds = this.selectedIds.slice();
+ nextIds.splice(index, 1);
+ this.setSelectedIds(nextIds);
+ } else {
+ this.setSelectedIds(this.selectedIds.concat([hit.id]));
+ }
+ this.scheduleRender();
+ return;
+ }
+ if (this.selectedIds.indexOf(hit.id) < 0) {
+ this.setSelectedIds([hit.id]);
+ this.scheduleRender();
+ }
+ this.startMove(point);
+ return;
+ }
+
+ if (this.selectedIds.length) {
+ this.setSelectedIds([]);
+ this.scheduleRender();
+ }
+ this.startPan(point);
+ },
+ onCanvasWheel: function (event) {
+ if (!this.pixiApp || !this.doc) {
+ return;
+ }
+ event.preventDefault();
+ var point = this.pointerToWorld(event);
+ var delta = event.deltaY < 0 ? 1.12 : 0.89;
+ var nextScale = clamp(this.camera.scale * delta, 0.06, 4);
+ this.camera.scale = nextScale;
+ this.camera.x = Math.round(point.screenX - point.x * nextScale);
+ this.camera.y = Math.round(point.screenY - point.y * nextScale);
+ this.viewZoom = nextScale;
+ this.scheduleZoomRefresh();
+ this.scheduleRender();
+ },
+ onWindowPointerMove: function (event) {
+ if (!this.pixiApp || !this.doc) {
+ return;
+ }
+ var point = this.pointerToWorld(event);
+ this.lastPointerWorld = point;
+ var pointerText = this.formatNumber(point.x) + ', ' + this.formatNumber(point.y);
+ var now = (window.performance && performance.now) ? performance.now() : Date.now();
+ if (pointerText !== this.pointerStatus && (now - this.lastPointerStatusUpdateTs >= POINTER_STATUS_UPDATE_INTERVAL || this.pointerStatus === '--')) {
+ this.pointerStatus = pointerText;
+ this.lastPointerStatusUpdateTs = now;
+ }
+ if (!this.interactionState) {
+ var hover = this.hitTestElement(point);
+ var hoverId = hover ? hover.id : '';
+ if (hoverId !== this.hoverElementId) {
+ this.hoverElementId = hoverId;
+ this.scheduleRender();
+ }
+ this.updateCursor();
+ return;
+ }
+
+ var state = this.interactionState;
+ if (state.type === 'pan') {
+ this.camera.x = Math.round(state.startCamera.x + (point.screenX - state.startScreen.x));
+ this.camera.y = Math.round(state.startCamera.y + (point.screenY - state.startScreen.y));
+ this.scheduleRender();
+ return;
+ }
+
+ if (state.type === 'marquee') {
+ state.currentWorld = { x: point.x, y: point.y };
+ this.scheduleRender();
+ return;
+ }
+
+ if (state.type === 'draw') {
+ var rawRect = buildRectFromPoints(state.startWorld, point);
+ var clipped = {
+ x: clamp(rawRect.x, 0, this.doc.canvasWidth),
+ y: clamp(rawRect.y, 0, this.doc.canvasHeight),
+ width: clamp(rawRect.width, 0, this.doc.canvasWidth),
+ height: clamp(rawRect.height, 0, this.doc.canvasHeight)
+ };
+ if (clipped.x + clipped.width > this.doc.canvasWidth) {
+ clipped.width = roundCoord(this.doc.canvasWidth - clipped.x);
+ }
+ if (clipped.y + clipped.height > this.doc.canvasHeight) {
+ clipped.height = roundCoord(this.doc.canvasHeight - clipped.y);
+ }
+ state.rect = clipped;
+ this.scheduleRender();
+ return;
+ }
+ if (state.type === 'array') {
+ state.currentWorld = { x: point.x, y: point.y };
+ state.previewItems = this.buildArrayCopies(state.template, state.startWorld, state.currentWorld);
+ this.scheduleRender();
+ return;
+ }
+
+ if (state.type === 'movePending') {
+ var dragDistance = Math.max(Math.abs(point.screenX - state.startScreen.x), Math.abs(point.screenY - state.startScreen.y));
+ if (dragDistance < DRAG_START_THRESHOLD) {
+ return;
+ }
+ state.type = 'move';
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ this.updateCursor();
+ }
+
+ if (state.type === 'move') {
+ var dx = point.x - state.startWorld.x;
+ var dy = point.y - state.startWorld.y;
+ var minDx = -Infinity;
+ var maxDx = Infinity;
+ var minDy = -Infinity;
+ var maxDy = Infinity;
+ for (var i = 0; i < state.baseItems.length; i++) {
+ var base = state.baseItems[i];
+ minDx = Math.max(minDx, -base.x);
+ minDy = Math.max(minDy, -base.y);
+ maxDx = Math.min(maxDx, this.doc.canvasWidth - (base.x + base.width));
+ maxDy = Math.min(maxDy, this.doc.canvasHeight - (base.y + base.height));
+ }
+ dx = clamp(dx, minDx, maxDx);
+ dy = clamp(dy, minDy, maxDy);
+ var snapDelta = this.collectMoveSnap(state.baseItems, dx, dy, this.selectedIds.slice());
+ dx = clamp(dx + snapDelta.dx, minDx, maxDx);
+ dy = clamp(dy + snapDelta.dy, minDy, maxDy);
+ for (var j = 0; j < state.baseItems.length; j++) {
+ var baseItem = state.baseItems[j];
+ var element = this.findElementById(baseItem.id);
+ if (!element) {
+ continue;
+ }
+ element.x = roundCoord(baseItem.x + dx);
+ element.y = roundCoord(baseItem.y + dy);
+ }
+ this.scheduleRender();
+ return;
+ }
+
+ if (state.type === 'resize') {
+ var target = this.findElementById(state.elementId);
+ if (!target) {
+ return;
+ }
+ var baseRect = state.baseRect;
+ var left = baseRect.x;
+ var right = baseRect.x + baseRect.width;
+ var top = baseRect.y;
+ var bottom = baseRect.y + baseRect.height;
+ if (state.handle.indexOf('w') >= 0) {
+ left = clamp(point.x, 0, right - MIN_ELEMENT_SIZE);
+ }
+ if (state.handle.indexOf('e') >= 0) {
+ right = clamp(point.x, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth);
+ }
+ if (state.handle.indexOf('n') >= 0) {
+ top = clamp(point.y, 0, bottom - MIN_ELEMENT_SIZE);
+ }
+ if (state.handle.indexOf('s') >= 0) {
+ bottom = clamp(point.y, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight);
+ }
+ var snapped = this.collectResizeSnap({
+ x: left,
+ y: top,
+ width: right - left,
+ height: bottom - top
+ }, state.handle, [target.id]);
+ if (snapped) {
+ if (state.handle.indexOf('w') >= 0 && snapped.left != null) {
+ left = clamp(left + snapped.left, 0, right - MIN_ELEMENT_SIZE);
+ }
+ if (state.handle.indexOf('e') >= 0 && snapped.right != null) {
+ right = clamp(right + snapped.right, left + MIN_ELEMENT_SIZE, this.doc.canvasWidth);
+ }
+ if (state.handle.indexOf('n') >= 0 && snapped.top != null) {
+ top = clamp(top + snapped.top, 0, bottom - MIN_ELEMENT_SIZE);
+ }
+ if (state.handle.indexOf('s') >= 0 && snapped.bottom != null) {
+ bottom = clamp(bottom + snapped.bottom, top + MIN_ELEMENT_SIZE, this.doc.canvasHeight);
+ }
+ }
+ target.x = roundCoord(left);
+ target.y = roundCoord(top);
+ target.width = roundCoord(right - left);
+ target.height = roundCoord(bottom - top);
+ this.scheduleRender();
+ }
+ },
+ onWindowPointerUp: function (event) {
+ if (!this.interactionState) {
+ return;
+ }
+ if (this.currentPointerId != null && event.pointerId != null && this.currentPointerId !== event.pointerId) {
+ return;
+ }
+ if (this.pixiApp && this.pixiApp.view.releasePointerCapture && event.pointerId != null) {
+ try {
+ this.pixiApp.view.releasePointerCapture(event.pointerId);
+ } catch (ignore) {
+ }
+ }
+ this.currentPointerId = null;
+
+ var state = this.interactionState;
+ this.interactionState = null;
+
+ if (state.type === 'pan') {
+ this.updateCursor();
+ this.schedulePanRefresh();
+ this.scheduleRender();
+ return;
+ }
+
+ if (state.type === 'marquee') {
+ var rect = buildRectFromPoints(state.startWorld, state.currentWorld);
+ if (rect.width > 2 && rect.height > 2) {
+ var matched = (this.doc.elements || []).filter(function (item) {
+ return rectIntersects(rect, item);
+ }).map(function (item) {
+ return item.id;
+ });
+ this.setSelectedIds(state.additive ? Array.from(new Set(this.selectedIds.concat(matched))) : matched);
+ }
+ this.scheduleRender();
+ return;
+ }
+
+ if (state.type === 'movePending') {
+ this.updateCursor();
+ return;
+ }
+
+ if (state.type === 'draw') {
+ var drawRect = state.rect;
+ if (drawRect && drawRect.width >= MIN_ELEMENT_SIZE && drawRect.height >= MIN_ELEMENT_SIZE) {
+ var newElement = {
+ id: nextId(),
+ type: state.elementType,
+ x: roundCoord(drawRect.x),
+ y: roundCoord(drawRect.y),
+ width: roundCoord(drawRect.width),
+ height: roundCoord(drawRect.height),
+ value: ''
+ };
+ if (this.hasOverlap(newElement, [])) {
+ this.showMessage('warning', '鏂板厓绱犱笉鑳戒笌宸叉湁鍏冪礌閲嶅彔');
+ } else if (!this.isWithinCanvas(newElement)) {
+ this.showMessage('warning', '鏂板厓绱犺秴鍑虹敾甯冭寖鍥�');
+ } else {
+ this.doc.elements.push(newElement);
+ this.selectedIds = [newElement.id];
+ this.commitMutation(state.beforeSnapshot);
+ this.refreshInspector();
+ return;
+ }
+ }
+ this.refreshInspector();
+ this.scheduleRender();
+ return;
+ }
+ if (state.type === 'array') {
+ var copies = state.previewItems && state.previewItems.length
+ ? state.previewItems
+ : this.buildArrayCopies(state.template, state.startWorld, state.currentWorld || state.startWorld);
+ if (!copies.length) {
+ this.scheduleRender();
+ return;
+ }
+ if (!this.canPlaceElements(copies, [])) {
+ this.showMessage('warning', '闃靛垪鐢熸垚鍚庝細閲嶅彔鎴栬秴鍑虹敾甯冿紝宸插彇娑�');
+ this.scheduleRender();
+ return;
+ }
+ var finalizedCopies = copies.map(function (item) {
+ return $.extend({}, item, { id: nextId() });
+ });
+ var self = this;
+ this.runMutation(function () {
+ self.doc.elements = self.doc.elements.concat(finalizedCopies);
+ self.selectedIds = [finalizedCopies[finalizedCopies.length - 1].id];
+ });
+ return;
+ }
+
+ if (state.type === 'move') {
+ var movedElements = this.getSelectedElements();
+ if (!this.canPlaceElements(movedElements, this.selectedIds.slice())) {
+ for (var i = 0; i < state.baseItems.length; i++) {
+ var base = state.baseItems[i];
+ var element = this.findElementById(base.id);
+ if (!element) {
+ continue;
+ }
+ element.x = base.x;
+ element.y = base.y;
+ }
+ this.showMessage('warning', '绉诲姩鍚庝細閲嶅彔鎴栬秴鍑虹敾甯冿紝宸叉仮澶�');
+ this.refreshInspector();
+ this.scheduleRender();
+ return;
+ }
+ if (!this.commitMutation(state.beforeSnapshot)) {
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ }
+ return;
+ }
+
+ if (state.type === 'resize') {
+ var resized = this.findElementById(state.elementId);
+ if (resized) {
+ if (!this.isWithinCanvas(resized) || this.hasOverlap(resized, [resized.id])) {
+ resized.x = state.baseRect.x;
+ resized.y = state.baseRect.y;
+ resized.width = state.baseRect.width;
+ resized.height = state.baseRect.height;
+ this.showMessage('warning', '缂╂斁鍚庝細閲嶅彔鎴栬秴鍑虹敾甯冿紝宸叉仮澶�');
+ this.refreshInspector();
+ this.scheduleRender();
+ return;
+ }
+ }
+ if (!this.commitMutation(state.beforeSnapshot)) {
+ this.markStaticSceneDirty();
+ this.scheduleRender();
+ }
+ return;
+ }
+
+ this.scheduleRender();
+ },
+ onWindowKeyDown: function (event) {
+ if (event.key === ' ' && !isInputLike(event.target)) {
+ this.spacePressed = true;
+ this.updateCursor();
+ event.preventDefault();
+ }
+ if (!this.doc) {
+ return;
+ }
+ if (isInputLike(event.target)) {
+ return;
+ }
+ var ctrl = event.ctrlKey || event.metaKey;
+ if (event.key === 'Delete' || event.key === 'Backspace') {
+ event.preventDefault();
+ this.deleteSelection();
+ return;
+ }
+ if (ctrl && (event.key === 'z' || event.key === 'Z')) {
+ event.preventDefault();
+ if (event.shiftKey) {
+ this.redo();
+ } else {
+ this.undo();
+ }
+ return;
+ }
+ if (ctrl && (event.key === 'y' || event.key === 'Y')) {
+ event.preventDefault();
+ this.redo();
+ return;
+ }
+ if (ctrl && (event.key === 'c' || event.key === 'C')) {
+ event.preventDefault();
+ this.copySelection();
+ return;
+ }
+ if (ctrl && (event.key === 'v' || event.key === 'V')) {
+ event.preventDefault();
+ this.pasteClipboard();
+ return;
+ }
+ if (event.key === 'Escape') {
+ this.interactionState = null;
+ this.setSelectedIds([]);
+ this.hoverElementId = '';
+ this.scheduleRender();
+ }
+ },
+ onWindowKeyUp: function (event) {
+ if (event.key === ' ') {
+ this.spacePressed = false;
+ this.updateCursor();
+ }
+ },
+ onBeforeUnload: function (event) {
+ if (!this.isDirty) {
+ return;
+ }
+ event.preventDefault();
+ event.returnValue = '';
+ }
+ }
+ });
+})();
diff --git a/src/main/webapp/views/basMap/basMap.html b/src/main/webapp/views/basMap/basMap.html
index c001131..f152002 100644
--- a/src/main/webapp/views/basMap/basMap.html
+++ b/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>
diff --git a/src/main/webapp/views/basMap/editor.html b/src/main/webapp/views/basMap/editor.html
new file mode 100644
index 0000000..4e182b9
--- /dev/null
+++ b/src/main/webapp/views/basMap/editor.html
@@ -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>姝e湪鍔犺浇 {{ 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>
diff --git a/src/main/webapp/views/watch/console.html b/src/main/webapp/views/watch/console.html
index d2adab1..284561b 100644
--- a/src/main/webapp/views/watch/console.html
+++ b/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({
diff --git a/src/main/webapp/views/watch/fakeTrace.html b/src/main/webapp/views/watch/fakeTrace.html
index ecf2c92..2110046 100644
--- a/src/main/webapp/views/watch/fakeTrace.html
+++ b/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;
--
Gitblit v1.9.1