From 706eabba4750cf92282378ae5d2414f497a4578c Mon Sep 17 00:00:00 2001
From: Junjie <DELL@qq.com>
Date: 星期四, 08 一月 2026 12:51:23 +0800
Subject: [PATCH] #

---
 src/main/resources/mapper/BasStationDeviceMapper.xml                    |   14 +
 src/main/webapp/static/js/basStationDevice/basStationDevice.js          |  214 +++++++++++++++++
 src/main/java/com/zy/asrs/controller/BasStationDeviceController.java    |  110 +++++++++
 src/main/java/com/zy/asrs/service/impl/BasStationDeviceServiceImpl.java |   11 
 src/main/webapp/views/basStationDevice/basStationDevice.html            |  269 ++++++++++++++++++++++
 src/main/java/com/zy/asrs/utils/Utils.java                              |   33 ++
 src/main/java/com/zy/asrs/entity/BasStationDevice.java                  |   41 +++
 src/main/java/com/zy/asrs/mapper/BasStationDeviceMapper.java            |   11 
 src/main/java/com/zy/asrs/service/BasStationDeviceService.java          |    7 
 9 files changed, 710 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/BasStationDeviceController.java b/src/main/java/com/zy/asrs/controller/BasStationDeviceController.java
new file mode 100644
index 0000000..700b00b
--- /dev/null
+++ b/src/main/java/com/zy/asrs/controller/BasStationDeviceController.java
@@ -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);
+    }
+}
diff --git a/src/main/java/com/zy/asrs/entity/BasStationDevice.java b/src/main/java/com/zy/asrs/entity/BasStationDevice.java
new file mode 100644
index 0000000..d2f529a
--- /dev/null
+++ b/src/main/java/com/zy/asrs/entity/BasStationDevice.java
@@ -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;
+}
diff --git a/src/main/java/com/zy/asrs/mapper/BasStationDeviceMapper.java b/src/main/java/com/zy/asrs/mapper/BasStationDeviceMapper.java
new file mode 100644
index 0000000..41e9265
--- /dev/null
+++ b/src/main/java/com/zy/asrs/mapper/BasStationDeviceMapper.java
@@ -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> {
+}
diff --git a/src/main/java/com/zy/asrs/service/BasStationDeviceService.java b/src/main/java/com/zy/asrs/service/BasStationDeviceService.java
new file mode 100644
index 0000000..98c5fba
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/BasStationDeviceService.java
@@ -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> {
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/BasStationDeviceServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/BasStationDeviceServiceImpl.java
new file mode 100644
index 0000000..6cca0f8
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/BasStationDeviceServiceImpl.java
@@ -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 {
+}
diff --git a/src/main/java/com/zy/asrs/utils/Utils.java b/src/main/java/com/zy/asrs/utils/Utils.java
index 6fd6c90..f2c7d43 100644
--- a/src/main/java/com/zy/asrs/utils/Utils.java
+++ b/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;
diff --git a/src/main/resources/mapper/BasStationDeviceMapper.xml b/src/main/resources/mapper/BasStationDeviceMapper.xml
new file mode 100644
index 0000000..d745190
--- /dev/null
+++ b/src/main/resources/mapper/BasStationDeviceMapper.xml
@@ -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>
diff --git a/src/main/webapp/static/js/basStationDevice/basStationDevice.js b/src/main/webapp/static/js/basStationDevice/basStationDevice.js
new file mode 100644
index 0000000..8b19f8e
--- /dev/null
+++ b/src/main/webapp/static/js/basStationDevice/basStationDevice.js
@@ -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;
+        }
+    }
+});
diff --git a/src/main/webapp/views/basStationDevice/basStationDevice.html b/src/main/webapp/views/basStationDevice/basStationDevice.html
new file mode 100644
index 0000000..3346886
--- /dev/null
+++ b/src/main/webapp/views/basStationDevice/basStationDevice.html
@@ -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>

--
Gitblit v1.9.1