#
Junjie
1 天以前 706eabba4750cf92282378ae5d2414f497a4578c
#
8个文件已添加
1个文件已修改
710 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/BasStationDeviceController.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasStationDevice.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/BasStationDeviceMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/BasStationDeviceService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/BasStationDeviceServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/utils/Utils.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/BasStationDeviceMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basStationDevice/basStationDevice.js 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basStationDevice/basStationDevice.html 269 ●●●●● 补丁 | 查看 | 原始文档 | 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>