src/main/java/com/zy/asrs/controller/BasStationTvController.java
New file @@ -0,0 +1,88 @@ package com.zy.asrs.controller; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.annotations.ManagerAuth; import com.core.common.R; import com.core.controller.AbstractBaseController; import com.zy.asrs.entity.BasStation; import com.zy.asrs.entity.BasStationTv; import com.zy.asrs.entity.TvDevice; import com.zy.asrs.service.BasStationService; import com.zy.asrs.service.BasStationTvService; import com.zy.asrs.service.TvDeviceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 站台与电视机绑定配置 Controller */ @RestController @RequestMapping("/basStationTv") public class BasStationTvController extends AbstractBaseController { @Autowired private BasStationService basStationService; @Autowired private TvDeviceService tvDeviceService; @Autowired private BasStationTvService basStationTvService; /** * 获取配置数据 (站点、电视机、绑定关系) */ @RequestMapping(value = "/data/auth", method = RequestMethod.GET) @ManagerAuth public R getData() { // 1. 获取所有站点 List<BasStation> stations = basStationService .selectList(new EntityWrapper<BasStation>().orderBy("station_id", true)); // 2. 获取所有电视机 (按名称排序) List<TvDevice> tvs = tvDeviceService.selectList(new EntityWrapper<TvDevice>().orderBy("name", true)); // 3. 获取所有绑定关系 List<BasStationTv> relations = basStationTvService.selectList(new EntityWrapper<>()); Map<String, Object> data = new HashMap<>(); data.put("stations", stations); data.put("tvs", tvs); data.put("relations", relations); return R.ok(data); } /** * 保存配置 */ @RequestMapping(value = "/save/auth", method = RequestMethod.POST) @ManagerAuth public R save(@RequestBody JSONObject params) { try { // 获取提交的关系列表 List<BasStationTv> relations = params.getJSONArray("relations").toJavaList(BasStationTv.class); // 全量替换策略:先删除所有旧关系,再插入新关系 // 注意:这里没有事务控制,如果要求严格一致性,建议在Service层加@Transactional basStationTvService.delete(new EntityWrapper<>()); if (relations != null && !relations.isEmpty()) { basStationTvService.insertBatch(relations); } return R.ok(); } catch (Exception e) { e.printStackTrace(); return R.error("保存失败:" + e.getMessage()); } } } src/main/java/com/zy/asrs/controller/OpenController.java
@@ -4,6 +4,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; @Slf4j @@ -29,4 +31,15 @@ return R.ok().add(map); } // @GetMapping("/getLedInfos") // public R getLedInfos(HttpServletRequest request) { // String remoteAddr = request.getRemoteAddr(); // for (LedSlave slave : slaveProperties.getLed()) { // if (slave.getIp().equals(remoteAddr)) { // return R.ok().add(slave); // } // } // return R.ok(); // } } src/main/java/com/zy/asrs/entity/BasStation.java
@@ -89,30 +89,9 @@ @ApiModelProperty(value= "备注") private String memo; /** * 站点楼层 */ @ApiModelProperty(value= "站点楼层") @TableField("station_lev") private Integer stationLev; /** * 设备编号 */ @ApiModelProperty(value= "设备编号") @TableField("device_no") private Integer deviceNo; /** * 站点别名 */ @ApiModelProperty(value= "站点别名") @TableField("station_alias") private String stationAlias; public BasStation() {} public BasStation(Integer status,Integer wrkNo,String inEnable,String outEnable,Long createBy,Date createTime,Long updateBy,Date updateTime,String memo,Integer stationLev) { public BasStation(Integer status,Integer wrkNo,String inEnable,String outEnable,Long createBy,Date createTime,Long updateBy,Date updateTime,String memo) { this.status = status; this.wrkNo = wrkNo; this.inEnable = inEnable; @@ -122,7 +101,6 @@ this.updateBy = updateBy; this.updateTime = updateTime; this.memo = memo; this.stationLev = stationLev; } // BasStation basStation = new BasStation( src/main/java/com/zy/asrs/entity/BasStationTv.java
New file @@ -0,0 +1,36 @@ package com.zy.asrs.entity; import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableName; import lombok.Data; import java.io.Serializable; /** * 站台与电视机绑定关系 */ @Data @TableName("asr_bas_station_tv") public class BasStationTv implements Serializable { private static final long serialVersionUID = 1L; /** * 站台ID */ @TableField("station_id") private Integer stationId; /** * 电视机ID */ @TableField("tv_id") private Long tvId; public BasStationTv() { } public BasStationTv(Integer stationId, Long tvId) { this.stationId = stationId; this.tvId = tvId; } } src/main/java/com/zy/asrs/mapper/BasStationTvMapper.java
New file @@ -0,0 +1,13 @@ package com.zy.asrs.mapper; import com.baomidou.mybatisplus.mapper.BaseMapper; import com.zy.asrs.entity.BasStationTv; import org.apache.ibatis.annotations.Mapper; /** * 站台与电视机绑定关联 Mapper */ @Mapper public interface BasStationTvMapper extends BaseMapper<BasStationTv> { } src/main/java/com/zy/asrs/service/BasStationTvService.java
New file @@ -0,0 +1,11 @@ package com.zy.asrs.service; import com.baomidou.mybatisplus.service.IService; import com.zy.asrs.entity.BasStationTv; /** * 站台与电视机绑定服务接口 */ public interface BasStationTvService extends IService<BasStationTv> { } src/main/java/com/zy/asrs/service/impl/BasStationTvServiceImpl.java
New file @@ -0,0 +1,16 @@ package com.zy.asrs.service.impl; import com.baomidou.mybatisplus.service.impl.ServiceImpl; import com.zy.asrs.entity.BasStationTv; import com.zy.asrs.mapper.BasStationTvMapper; import com.zy.asrs.service.BasStationTvService; import org.springframework.stereotype.Service; /** * 站台与电视机绑定服务实现类 */ @Service public class BasStationTvServiceImpl extends ServiceImpl<BasStationTvMapper, BasStationTv> implements BasStationTvService { } src/main/resources/mapper/BasStationMapper.xml
@@ -14,9 +14,6 @@ <result column="update_by" property="updateBy" /> <result column="update_time" property="updateTime" /> <result column="memo" property="memo" /> <result column="station_lev" property="stationLev" /> <result column="device_no" property="deviceNo" /> <result column="station_alias" property="stationAlias" /> </resultMap> src/main/resources/mapper/BasStationTvMapper.xml
New file @@ -0,0 +1,11 @@ <?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.BasStationTvMapper"> <!-- 通用查询映射结果 --> <resultMap id="BaseResultMap" type="com.zy.asrs.entity.BasStationTv"> <result column="station_id" property="stationId" /> <result column="tv_id" property="tvId" /> </resultMap> </mapper> src/main/webapp/static/js/basStation/basStation.js
@@ -24,7 +24,6 @@ {type: 'checkbox'} ,{field: 'stationId', align: 'center',title: '编号'} ,{field: 'status$', align: 'center',title: '状态'} // ,{field: 'wrkNo', align: 'center',title: '工作号'} ,{field: 'inEnable', align: 'center',title: '可入(checkBox)'} ,{field: 'outEnable', align: 'center',title: '可出(checkBox)'} // ,{field: 'createBy', align: 'center',title: '创建人员'} @@ -32,9 +31,6 @@ // ,{field: 'updateBy', align: 'center',title: '修改人员'} // ,{field: 'updateTime$', align: 'center',title: '修改时间'} ,{field: 'memo', align: 'center',title: '备注'} ,{field: 'stationLev', align: 'center',title: '站点楼层'} ,{field: 'deviceNo', align: 'center',title: '设备编号'} ,{field: 'stationAlias', align: 'center',title: '站点别名'} ,{fixed: 'right', title:'操作', align: 'center', toolbar: '#operate', width:120} ]], src/main/webapp/views/basStation/basStation.html
@@ -64,9 +64,14 @@ <!-- 表单弹窗 --> <script type="text/html" id="editDialog"> <form id="detail" lay-filter="detail" class="layui-form admin-form model-form"> <input name="stationId" type="hidden"> <div class="layui-row"> <div class="layui-col-md12"> <div class="layui-form-item"> <label class="layui-form-label">站点编号: </label> <div class="layui-input-block"> <input class="layui-input" name="stationId" placeholder="站点编号"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">状态: </label> <div class="layui-input-block"> @@ -77,12 +82,6 @@ </select> </div> </div> <!-- <div class="layui-form-item"> <label class="layui-form-label">工作号: </label> <div class="layui-input-block"> <input class="layui-input" name="wrkNo" placeholder="请输入工作号"> </div> </div> --> <div class="layui-form-item"> <label class="layui-form-label">可入(checkBox): </label> <div class="layui-input-block"> @@ -95,52 +94,10 @@ <input class="layui-input" name="outEnable" placeholder="请输入可出(checkBox)"> </div> </div> <!-- <div class="layui-form-item"> <label class="layui-form-label">创建人员: </label> <div class="layui-input-block"> <input class="layui-input" name="createBy" placeholder="请输入创建人员"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">创建时间: </label> <div class="layui-input-block"> <input class="layui-input" name="createTime" id="createTime$" placeholder="请输入创建时间"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">修改人员: </label> <div class="layui-input-block"> <input class="layui-input" name="updateBy" placeholder="请输入修改人员"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">修改时间: </label> <div class="layui-input-block"> <input class="layui-input" name="updateTime" id="updateTime$" placeholder="请输入修改时间"> </div> </div> --> <div class="layui-form-item"> <label class="layui-form-label">备注: </label> <div class="layui-input-block"> <input class="layui-input" name="memo" placeholder="请输入备注"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">站点楼层: </label> <div class="layui-input-block"> <input class="layui-input" name="stationLev" placeholder="请输入站点楼层"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">设备编号: </label> <div class="layui-input-block"> <input class="layui-input" name="deviceNo" placeholder="请输入设备编号"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">站点别名: </label> <div class="layui-input-block"> <input class="layui-input" name="stationAlias" placeholder="请输入站点别名"> </div> </div> src/main/webapp/views/basStationTv/basStationTv.html
New file @@ -0,0 +1,364 @@ <!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> 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; } .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; } .station-node { border-left: 4px solid #67c23a; } .tv-node { border-left: 4px solid #e6a23c; } .canvas-area { flex: 1; position: relative; margin-right: 300px; background-color: #fafafa; overflow: hidden; } svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } svg line { stroke: #909399; stroke-width: 2; cursor: pointer; pointer-events: stroke; transition: stroke-width 0.2s, stroke 0.2s; } svg line:hover { stroke: #f56c6c; stroke-width: 4; } .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"> <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: isStationBound(item.stationId) }" draggable="true" @dragstart="onDragStart($event, item, 'station')" @dragend="onDragEnd"> <span>{{ item.stationId }} - {{ item.memo || '无备注' }}</span> </div> </div> </div> <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.tvName }}</title> </line> <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> <div class="column right"> <div class="column-header">电视机 ({{ tvs.length }})</div> <div class="list-container" ref="tvList"> <div v-for="item in tvs" :key="'t-' + item.id" :id="'tv-' + item.id" class="node-item tv-node" :class="{ active: isTvBound(item.id) }" @dragover.prevent @drop="onDrop($event, item)"> <span>{{ item.name }} ({{ item.ip }})</span> </div> </div> </div> </div> </el-card> </div> <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> var app = new Vue({ el: '#app', data: { stations: [], tvs: [], relations: [], lines: [], draggingItem: null, draggingLine: null, saving: false }, mounted: function () { this.loadData(); window.addEventListener('resize', this.updateLines); this.$nextTick(function () { if (this.$refs.stationList) this.$refs.stationList.addEventListener('scroll', this.updateLines); if (this.$refs.tvList) this.$refs.tvList.addEventListener('scroll', this.updateLines); }.bind(this)); }, 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 + '/basStationTv/data/auth', headers: { 'token': localStorage.getItem('token') }, method: 'GET', success: function (res) { loading.close(); if (res.code === 200) { that.stations = res.data.stations || []; that.tvs = res.data.tvs || []; that.relations = res.data.relations || []; 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; var payload = { relations: this.relations }; $.ajax({ url: baseUrl + '/basStationTv/save/auth', type: 'POST', headers: { 'token': localStorage.getItem('token') }, contentType: 'application/json', data: JSON.stringify(payload), 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('保存请求异常'); } }); }, isStationBound: function (stationId) { return this.relations.some(function (r) { return r.stationId == stationId; }); }, isTvBound: function (tvId) { return this.relations.some(function (r) { return r.tvId == tvId; }); }, onDragStart: function (event, item, type) { this.draggingItem = { item: item, type: type }; event.dataTransfer.effectAllowed = 'link'; }, onDragEnd: function () { this.draggingItem = null; this.draggingLine = null; }, onDrop: function (event, targetTv) { if (!this.draggingItem || this.draggingItem.type !== 'station') return; var station = this.draggingItem.item; var exists = this.relations.some(function (r) { return r.stationId == station.stationId && r.tvId == targetTv.id; }); if (exists) { this.$message.warning('该绑定已存在'); return; } this.relations.push({ stationId: station.stationId, tvId: targetTv.id }); this.$message.success('已建立绑定: 站点 ' + station.stationId + ' -> 电视 ' + targetTv.name); this.$nextTick(this.updateLines); }, confirmDelete: function (index) { var rel = this.lines[index] && this.lines[index].relation; if (!rel) return; this.$confirm('确定要删除该绑定吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(function () { this.relations = this.relations.filter(function (r) { return !(r.stationId == rel.stationId && r.tvId == rel.tvId); }); this.$nextTick(this.updateLines); }.bind(this)).catch(function () { }); }, updateLines: function () { var svg = document.querySelector('svg'); if (!svg) return; var svgRect = svg.getBoundingClientRect(); var newLines = []; var that = this; this.relations.forEach(function (rel) { var sDom = document.getElementById('station-' + rel.stationId); var tDom = document.getElementById('tv-' + rel.tvId); if (sDom && tDom) { var sRect = sDom.getBoundingClientRect(); var tRect = tDom.getBoundingClientRect(); var x1 = sRect.right - svgRect.left; var y1 = sRect.top + sRect.height / 2 - svgRect.top; var x2 = tRect.left - svgRect.left; var y2 = tRect.top + tRect.height / 2 - svgRect.top; var tvName = 'Unknown'; var tv = that.tvs.find(function (t) { return t.id == rel.tvId; }); if (tv) { tvName = tv.name; } newLines.push({ x1: x1, y1: y1, x2: x2, y2: y2, relation: rel, stationId: rel.stationId, tvName: tvName }); } }); this.lines = newLines; } } }); </script> </body> </html> src/main/webapp/views/index.html
@@ -239,7 +239,7 @@ // 默认加载主页 index.loadHome({ menuPath: baseUrl + '/views/watch/console.html', menuPath: baseUrl + '/views/tvDevice/tvDevice.html', menuName: '<i class="layui-icon layui-icon-home"></i>' });