| src/main/java/com/zy/asrs/controller/BasStationDeviceController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/entity/BasStationDevice.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/mapper/BasStationDeviceMapper.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/BasStationDeviceService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/service/impl/BasStationDeviceServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/asrs/utils/Utils.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/mapper/BasStationDeviceMapper.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/static/js/basStationDevice/basStationDevice.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/basStationDevice/basStationDevice.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/asrs/controller/BasStationDeviceController.java
New file @@ -0,0 +1,110 @@ package com.zy.asrs.controller; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.zy.asrs.entity.BasCrnp; import com.zy.asrs.entity.BasDevp; import com.zy.asrs.entity.BasDualCrnp; import com.zy.asrs.entity.BasStation; import com.zy.asrs.entity.BasStationDevice; import com.zy.asrs.service.BasCrnpService; import com.zy.asrs.service.BasDevpService; import com.zy.asrs.service.BasDualCrnpService; import com.zy.asrs.service.BasStationDeviceService; import com.zy.asrs.service.BasStationService; import com.core.annotations.ManagerAuth; import com.core.common.R; 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 java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController @RequestMapping("/basStationDevice") public class BasStationDeviceController extends BaseController { @Autowired private BasStationDeviceService basStationDeviceService; @Autowired private BasDevpService basDevpService; @Autowired private BasStationService basStationService; @Autowired private BasCrnpService basCrnpService; @Autowired private BasDualCrnpService basDualCrnpService; @RequestMapping("/list/auth") @ManagerAuth public R list() { return R.ok(basStationDeviceService.selectList(new EntityWrapper<>())); } @RequestMapping("/save/auth") @ManagerAuth public R save(@RequestBody List<BasStationDevice> list) { // Full replacement logic for simplicity in this specific UI case // First delete all existing, then insert new ones. // Note: In a production environment with high concurrency, this should be transactional and more granular. // But for a configuration page, this is acceptable. // However, to be safer, we should probably only delete for the stations involved or delete all if it's a full save. // Let's assume the UI sends the full current state of configuration. basStationDeviceService.delete(new EntityWrapper<>()); if (list != null && !list.isEmpty()) { basStationDeviceService.insertBatch(list); } return R.ok(); } @RequestMapping("/data/auth") @ManagerAuth public R getData() { Map<String, Object> data = new HashMap<>(); List<Integer> stationList = new ArrayList<>(); List<BasDevp> devps = basDevpService.selectList(new EntityWrapper<BasDevp>().eq("status", 1)); for (BasDevp devp : devps) { for (StationObjModel stationObjModel : devp.getBarcodeStationList$()) { stationList.add(stationObjModel.getStationId()); } } List<BasStation> stations = basStationService.selectList(new EntityWrapper<BasStation>().in("station_id", stationList)); data.put("stations", stations); // Get Devices (Crn and DualCrn) List<Map<String, Object>> devices = new ArrayList<>(); List<BasCrnp> crns = basCrnpService.selectList(new EntityWrapper<BasCrnp>().eq("status", 1)); for (BasCrnp crn : crns) { Map<String, Object> d = new HashMap<>(); d.put("deviceNo", crn.getCrnNo()); d.put("type", SlaveType.Crn.toString()); d.put("name", "堆垛机 " + crn.getCrnNo()); devices.add(d); } List<BasDualCrnp> dualCrns = basDualCrnpService.selectList(new EntityWrapper<BasDualCrnp>().eq("status", 1)); for (BasDualCrnp dualCrn : dualCrns) { Map<String, Object> d = new HashMap<>(); d.put("deviceNo", dualCrn.getCrnNo()); d.put("type", SlaveType.DualCrn.toString()); d.put("name", "双工位堆垛机 " + dualCrn.getCrnNo()); devices.add(d); } data.put("devices", devices); // Get existing relations List<BasStationDevice> relations = basStationDeviceService.selectList(new EntityWrapper<>()); data.put("relations", relations); return R.ok(data); } } src/main/java/com/zy/asrs/entity/BasStationDevice.java
New file @@ -0,0 +1,41 @@ package com.zy.asrs.entity; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; @Data @TableName("asr_bas_station_device") public class BasStationDevice implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 站点编号 */ @ApiModelProperty(value= "站点编号") @TableField("station_id") private Integer stationId; /** * 设备编号 */ @ApiModelProperty(value= "设备编号") @TableField("device_no") private Integer deviceNo; /** * 设备类型 (Crn/DualCrn) */ @ApiModelProperty(value= "设备类型") @TableField("device_type") private String deviceType; } src/main/java/com/zy/asrs/mapper/BasStationDeviceMapper.java
New file @@ -0,0 +1,11 @@ package com.zy.asrs.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.zy.asrs.entity.BasStationDevice; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; @Mapper @Repository public interface BasStationDeviceMapper extends BaseMapper<BasStationDevice> { } src/main/java/com/zy/asrs/service/BasStationDeviceService.java
New file @@ -0,0 +1,7 @@ package com.zy.asrs.service; import com.baomidou.mybatisplus.service.IService; import com.zy.asrs.entity.BasStationDevice; public interface BasStationDeviceService extends IService<BasStationDevice> { } src/main/java/com/zy/asrs/service/impl/BasStationDeviceServiceImpl.java
New file @@ -0,0 +1,11 @@ package com.zy.asrs.service.impl; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.zy.asrs.entity.BasStationDevice; import com.zy.asrs.mapper.BasStationDeviceMapper; import com.zy.asrs.service.BasStationDeviceService; import org.springframework.stereotype.Service; @Service public class BasStationDeviceServiceImpl extends ServiceImpl<BasStationDeviceMapper, BasStationDevice> implements BasStationDeviceService { } src/main/java/com/zy/asrs/utils/Utils.java
@@ -11,9 +11,11 @@ import com.core.exception.CoolException; import com.zy.asrs.entity.BasCrnp; import com.zy.asrs.entity.BasDualCrnp; import com.zy.asrs.entity.BasStationDevice; import com.zy.asrs.entity.WrkMast; import com.zy.asrs.service.BasCrnpService; import com.zy.asrs.service.BasDualCrnpService; import com.zy.asrs.service.BasStationDeviceService; import com.zy.asrs.service.WrkMastService; import com.zy.common.model.NavigateNode; import com.zy.common.utils.NavigateUtils; @@ -168,6 +170,18 @@ NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class); WrkMastService wrkMastService = SpringUtils.getBean(WrkMastService.class); BasCrnpService basCrnpService = SpringUtils.getBean(BasCrnpService.class); BasStationDeviceService basStationDeviceService = SpringUtils.getBean(BasStationDeviceService.class); List<BasStationDevice> stationDevices = basStationDeviceService.selectList(new EntityWrapper<BasStationDevice>().eq("station_id", stationId)); boolean hasConfig = !stationDevices.isEmpty(); List<Integer> allowedCrnNos = new ArrayList<>(); if (hasConfig) { for (BasStationDevice sd : stationDevices) { if (SlaveType.Crn.toString().equals(sd.getDeviceType())) { allowedCrnNos.add(sd.getDeviceNo()); } } } Integer currentCircleTaskCrnNo = null; Object object = redisUtil.get(RedisKeyType.CURRENT_CIRCLE_TASK_CRN_NO.key); @@ -216,6 +230,9 @@ List<BasCrnp> enabledCrnps = new ArrayList<>(); for (BasCrnp basCrnp : baseList) { if (hasConfig && !allowedCrnNos.contains(basCrnp.getCrnNo())) { continue; } CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo()); if (crnThread == null) { continue; @@ -277,6 +294,18 @@ NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class); WrkMastService wrkMastService = SpringUtils.getBean(WrkMastService.class); BasDualCrnpService basDualCrnpService = SpringUtils.getBean(BasDualCrnpService.class); BasStationDeviceService basStationDeviceService = SpringUtils.getBean(BasStationDeviceService.class); List<BasStationDevice> stationDevices = basStationDeviceService.selectList(new EntityWrapper<BasStationDevice>().eq("station_id", stationId)); boolean hasConfig = !stationDevices.isEmpty(); List<Integer> allowedCrnNos = new ArrayList<>(); if (hasConfig) { for (BasStationDevice sd : stationDevices) { if (SlaveType.Crn.toString().equals(sd.getDeviceType())) { allowedCrnNos.add(sd.getDeviceNo()); } } } Wrapper<BasDualCrnp> wrapper = new EntityWrapper<BasDualCrnp>() .eq("in_enable", "Y") @@ -304,6 +333,10 @@ List<BasDualCrnp> enabledCrnps = new ArrayList<>(); for (BasDualCrnp basDualCrnp : baseList) { if (hasConfig && !allowedCrnNos.contains(basDualCrnp.getCrnNo())) { continue; } DualCrnThread dualCrnThread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, basDualCrnp.getCrnNo()); if (dualCrnThread == null) { continue; src/main/resources/mapper/BasStationDeviceMapper.xml
New file @@ -0,0 +1,14 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zy.asrs.mapper.BasStationDeviceMapper"> <!-- 通用查询映射结果 --> <resultMap id="BaseResultMap" type="com.zy.asrs.entity.BasStationDevice"> <id column="id" property="id" /> <result column="station_id" property="stationId" /> <result column="device_no" property="deviceNo" /> <result column="device_type" property="deviceType" /> </resultMap> </mapper> src/main/webapp/static/js/basStationDevice/basStationDevice.js
New file @@ -0,0 +1,214 @@ var app = new Vue({ el: '#app', data: { stations: [], devices: [], relations: [], // {stationId, deviceNo, deviceType} lines: [], // {x1, y1, x2, y2, stationId, deviceNo} draggingItem: null, // Currently dragged item draggingLine: null, // Temporary line for visual feedback saving: false, containerOffset: { top: 0, left: 0 } }, mounted: function() { this.loadData(); // Listen to window resize to redraw lines window.addEventListener('resize', this.updateLines); // Listen to scroll events on lists to redraw lines this.$refs.stationList.addEventListener('scroll', this.updateLines); this.$refs.deviceList.addEventListener('scroll', this.updateLines); }, beforeDestroy: function() { window.removeEventListener('resize', this.updateLines); }, methods: { loadData: function() { var that = this; var loading = this.$loading({ lock: true, text: '加载数据中...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); $.ajax({ url: baseUrl + '/basStationDevice/data/auth', headers: { 'token': localStorage.getItem('token') }, method: 'GET', success: function(res) { loading.close(); if (res.code === 200) { that.stations = res.data.stations || []; that.devices = res.data.devices || []; that.relations = res.data.relations || []; // Delay to wait for DOM rendering that.$nextTick(function() { that.updateLines(); }); } else if (res.code === 403) { top.location.href = baseUrl + "/"; } else { that.$message.error('加载数据失败: ' + res.msg); } }, error: function() { loading.close(); that.$message.error('加载请求异常'); } }); }, saveData: function() { var that = this; this.saving = true; $.ajax({ url: baseUrl + '/basStationDevice/save/auth', type: 'POST', headers: { 'token': localStorage.getItem('token') }, contentType: 'application/json', data: JSON.stringify(this.relations), success: function (res) { that.saving = false; if (res.code === 200) { that.$message.success('保存成功'); that.loadData(); } else { that.$message.error('保存失败: ' + res.msg); } }, error: function () { that.saving = false; that.$message.error('保存请求异常'); } }); }, isStationConnected: function(stationId) { return this.relations.some(function(r) { return r.stationId == stationId; }); }, isDeviceConnected: function(deviceNo, type) { return this.relations.some(function(r) { return r.deviceNo == deviceNo && r.deviceType == type; }); }, // --- Drag and Drop Logic --- onDragStart: function(event, item, type) { this.draggingItem = { item: item, type: type }; event.dataTransfer.effectAllowed = 'link'; // Optional: set custom drag image }, onDragEnd: function() { this.draggingItem = null; }, onDrop: function(event, targetDevice) { if (!this.draggingItem || this.draggingItem.type !== 'station') { return; } var station = this.draggingItem.item; // Check if exists var exists = this.relations.some(function(r) { return r.stationId == station.stationId && r.deviceNo == targetDevice.deviceNo && r.deviceType == targetDevice.type; }); if (exists) { this.$message.warning('该关联已存在'); return; } // Add relation this.relations.push({ stationId: station.stationId, deviceNo: targetDevice.deviceNo, deviceType: targetDevice.type }); this.$message.success('已建立关联: 站点 ' + station.stationId + ' -> ' + targetDevice.name); this.$nextTick(this.updateLines); }, confirmDelete: function(index) { var that = this; var link = this.relations[index]; // Note: lines index must match relations index? No. // We need to map line click to actual relation. // The lines array is rebuilt from relations, so we can store the index or relation object in the line object. // But let's check updateLines logic first. // In updateLines, we iterate relations. So index matches if filtered correctly? // Wait, updateLines maps relations 1-to-1 to lines IF DOM exists. // If DOM doesn't exist (scrolled out?), line might not exist. // But we should draw all lines? // Actually, if scrolled out, we might want to hide lines or point to edge. // For simplicity, we only draw lines for visible nodes or calculate positions for all. // Since lists are scrollable, calculating positions for invisible nodes is tricky (top/bottom are relative to viewport). // Better approach: Store the relation object in the line object. var rel = this.lines[index].relation; this.$confirm('确定删除该关联吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(function() { // Find index in relations array var idx = that.relations.indexOf(rel); if (idx > -1) { that.relations.splice(idx, 1); that.$nextTick(that.updateLines); that.$message.success('已删除'); } }).catch(function() {}); }, updateLines: function() { var that = this; var newLines = []; // Get SVG offset var svgElement = this.$el.querySelector('svg'); if (!svgElement) return; var svgRect = svgElement.getBoundingClientRect(); // We need positions relative to the SVG this.relations.forEach(function(rel) { var sDom = document.getElementById('station-' + rel.stationId); var dDom = document.getElementById('device-' + rel.deviceType + '-' + rel.deviceNo); if (sDom && dDom) { var sRect = sDom.getBoundingClientRect(); var dRect = dDom.getBoundingClientRect(); // Coordinates relative to SVG var x1 = sRect.right - svgRect.left; var y1 = sRect.top + sRect.height / 2 - svgRect.top; var x2 = dRect.left - svgRect.left; var y2 = dRect.top + dRect.height / 2 - svgRect.top; newLines.push({ x1: x1, y1: y1, x2: x2, y2: y2, relation: rel, stationId: rel.stationId, deviceNo: rel.deviceNo }); } }); this.lines = newLines; } } }); src/main/webapp/views/basStationDevice/basStationDevice.html
New file @@ -0,0 +1,269 @@ <!DOCTYPE html> <html lang="en"> <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"> <!-- Element UI CSS --> <link rel="stylesheet" href="../../static/vue/element/element.css"> <link rel="stylesheet" href="../../static/css/cool.css"> <style> html, body { height: 100%; margin: 0; padding: 0; background-color: #f0f2f5; } #app { height: 100%; padding: 20px; box-sizing: border-box; display: flex; flex-direction: column; } .main-card { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .flow-container { flex: 1; display: flex; position: relative; background: #fff; border: 1px solid #ebeef5; overflow: hidden; /* Hide scrollbars of container */ } .column { width: 300px; display: flex; flex-direction: column; border-right: 1px solid #ebeef5; background-color: #fcfcfc; z-index: 10; } .column.right { border-right: none; border-left: 1px solid #ebeef5; position: absolute; right: 0; top: 0; bottom: 0; } .column-header { padding: 15px; text-align: center; font-weight: bold; background-color: #f5f7fa; border-bottom: 1px solid #ebeef5; color: #606266; } .list-container { flex: 1; overflow-y: auto; padding: 10px; } .node-item { padding: 12px; margin-bottom: 10px; background-color: #fff; border: 1px solid #dcdfe6; border-radius: 4px; cursor: grab; transition: all 0.3s; position: relative; color: #606266; font-size: 14px; display: flex; justify-content: space-between; align-items: center; } .node-item:hover { box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1); border-color: #409eff; } .node-item.active { background-color: #ecf5ff; border-color: #409eff; color: #409eff; } /* Specific styles for Station nodes */ .station-node { border-left: 4px solid #67c23a; } /* Specific styles for Device nodes */ .device-node { border-left: 4px solid #e6a23c; } /* Middle canvas area */ .canvas-area { flex: 1; position: relative; margin-right: 300px; /* Space for right column */ background-color: #fafafa; overflow: hidden; } svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; /* Let clicks pass through */ } svg line { stroke: #909399; stroke-width: 2; cursor: pointer; pointer-events: stroke; /* Capture events on the stroke itself */ transition: stroke-width 0.2s, stroke 0.2s; } svg line:hover { stroke: #f56c6c; stroke-width: 4; } /* Drag ghost image customization if needed */ .dragging { opacity: 0.5; } /* Connection Point Indicators */ .connector { width: 10px; height: 10px; background-color: #909399; border-radius: 50%; position: absolute; top: 50%; transform: translateY(-50%); } .station-node .connector { right: -16px; /* Position outside the box */ display: none; /* Only show when necessary or active */ } .device-node .connector { left: -16px; display: none; } .toolbar { padding: 10px 0; display: flex; justify-content: space-between; align-items: center; } </style> </head> <body> <div id="app" v-cloak> <el-card class="main-card" :body-style="{ padding: '0px', display: 'flex', flexDirection: 'column', height: '100%' }"> <div slot="header" class="clearfix"> <span style="font-weight: bold; font-size: 16px;">站点与堆垛机任务关联配置</span> <div style="float: right;"> <el-button type="primary" icon="el-icon-check" size="small" @click="saveData" :loading="saving">保存配置</el-button> <el-button icon="el-icon-refresh" size="small" @click="loadData">刷新</el-button> </div> </div> <div class="toolbar" style="padding: 10px 20px; border-bottom: 1px solid #ebeef5;"> <el-alert title="操作说明:拖拽左侧的【站点】到右侧的【设备】上即可建立关联。点击连线可删除关联。" type="info" show-icon :closable="false"> </el-alert> </div> <div class="flow-container" ref="flowContainer"> <!-- Left: Stations --> <div class="column"> <div class="column-header">站点列表 ({{ stations.length }})</div> <div class="list-container" ref="stationList"> <div v-for="item in stations" :key="'s-' + item.stationId" :id="'station-' + item.stationId" class="node-item station-node" :class="{ active: isStationConnected(item.stationId) }" draggable="true" @dragstart="onDragStart($event, item, 'station')" @dragend="onDragEnd" > <span>{{ item.stationId }}</span> </div> </div> </div> <!-- Middle: Canvas --> <div class="canvas-area"> <svg width="100%" height="100%"> <line v-for="(link, index) in lines" :key="index" :x1="link.x1" :y1="link.y1" :x2="link.x2" :y2="link.y2" @click="confirmDelete(index)" > <title>点击删除关联: 站点 {{ link.stationId }} -> 设备 {{ link.deviceNo }}</title> </line> <!-- Dragging Line Preview --> <line v-if="draggingLine" :x1="draggingLine.x1" :y1="draggingLine.y1" :x2="draggingLine.x2" :y2="draggingLine.y2" stroke="#409eff" stroke-dasharray="5,5" stroke-width="2" /> </svg> </div> <!-- Right: Devices --> <div class="column right"> <div class="column-header">堆垛机设备 ({{ devices.length }})</div> <div class="list-container" ref="deviceList"> <div v-for="item in devices" :key="'d-' + item.type + '-' + item.deviceNo" :id="'device-' + item.type + '-' + item.deviceNo" class="node-item device-node" :class="{ active: isDeviceConnected(item.deviceNo, item.type) }" @dragover.prevent @drop="onDrop($event, item)" > <span>{{ item.name }}</span> <el-tag size="mini" :type="item.type === 'Crn' ? 'success' : 'warning'">{{ item.type === 'Crn' ? '堆垛机' : '双工位' }}</el-tag> </div> </div> </div> </div> </el-card> </div> <!-- Libraries --> <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></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/common.js" charset="utf-8"></script> <script type="text/javascript" src="../../static/js/basStationDevice/basStationDevice.js"></script> </body> </html>