#
Junjie
2026-01-21 d565b3796eb2029797d4bc3f962a7daf38fcaf00
#
7个文件已添加
6个文件已修改
640 ■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/BasStationTvController.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/OpenController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasStation.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasStationTv.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/BasStationTvMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/BasStationTvService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/BasStationTvServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/BasStationMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/BasStationTvMapper.xml 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basStation/basStation.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basStation/basStation.html 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basStationTv/basStationTv.html 364 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/index.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>'
    });