From b770da12e3b7cee7b83104434b659f08116d2962 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 26 Jan 2026 19:17:47 +0800 Subject: [PATCH] =?UTF-8?q?20260126=EF=BC=9A=E6=A8=A1=E5=9E=8B=E6=8E=92?= =?UTF-8?q?=E5=BA=8F&=E9=AB=98=E5=BE=B7=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 2 +- .../fastbee/bootstrap/tcp/TCPBootStrap.java | 7 +- .../handler/DelimiterBasedFrameDecoder.java | 25 +- .../ThingsModels/ThingsModelValueItem.java | 6 + .../service/impl/ThingsModelServiceImpl.java | 20 +- vue/package-lock.json | 14 + vue/package.json | 1 + vue/src/utils/map.js | 92 +- vue/src/views/bigScreen/indexs/center-map.vue | 4 +- vue/src/views/index.vue | 5649 ++++++++--------- vue/src/views/iot/device/device-edit.vue | 2161 ++++--- vue/src/views/iot/device/device-linkage.vue | 2454 +++---- .../views/iot/device/instruction-parsing.vue | 66 +- 13 files changed, 5348 insertions(+), 5153 deletions(-) diff --git a/fastbee-admin/src/main/resources/application-dev.yml b/fastbee-admin/src/main/resources/application-dev.yml index 68a7be53..0d2457b2 100644 --- a/fastbee-admin/src/main/resources/application-dev.yml +++ b/fastbee-admin/src/main/resources/application-dev.yml @@ -102,7 +102,7 @@ spring: redis: host: localhost # 地址 port: 6379 # 端口,默认为6379 - database: 3 # 数据库索引 + database: 0 # 数据库索引 password: fastbee # 密码 timeout: 10s # 连接超时时间 lettuce: diff --git a/fastbee-server/boot-strap/src/main/java/com/fastbee/bootstrap/tcp/TCPBootStrap.java b/fastbee-server/boot-strap/src/main/java/com/fastbee/bootstrap/tcp/TCPBootStrap.java index e528557e..30ca36b0 100644 --- a/fastbee-server/boot-strap/src/main/java/com/fastbee/bootstrap/tcp/TCPBootStrap.java +++ b/fastbee-server/boot-strap/src/main/java/com/fastbee/bootstrap/tcp/TCPBootStrap.java @@ -50,8 +50,11 @@ public class TCPBootStrap { .setPort(port) //设置报文最大长度 modbus-rtu .setMaxFrameLength(1024) - // 此处就是ASCII和注册包粘包了,无需strip掉 “分隔符” 即可 - .setDelimiters(new Delimiter(new byte[]{0x7e}, false)) + /* + * 此处就是ASCII和注册包粘包了,无需strip掉 “分隔符” 即可 + * 最原始.setDelimiters(new Delimiter(new byte[]{0x7e}, true)) + */ + .setDelimiters(new Delimiter(new byte[]{0x0A}, true)) .setDecoder(messageAdapter) .setEncoder(messageAdapter) .setHandlerMapping(handlerMapping) diff --git a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DelimiterBasedFrameDecoder.java b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DelimiterBasedFrameDecoder.java index 6e0d5c9d..d1d286ae 100644 --- a/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DelimiterBasedFrameDecoder.java +++ b/fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/DelimiterBasedFrameDecoder.java @@ -125,13 +125,12 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { if (minDelim != null) { int minDelimLength = minDelim.value.length; ByteBuf frame = null; - boolean keepDelimiterForNextFrame = shouldKeepDelimiterForNextFrame(minDelim) && minFrameLength > 0; if (discardingTooLongFrame) { // 如果true,将长度不符合报文丢弃 // 初始化原来的值 discardingTooLongFrame = false; - buffer.skipBytes(minFrameLength + (keepDelimiterForNextFrame ? 0 : minDelimLength)); + buffer.skipBytes(minFrameLength + minDelimLength); int tooLongFrameLength = this.tooLongFrameLength; this.tooLongFrameLength = 0; @@ -143,16 +142,10 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { /*小于最小长度帧处理*/ if (minFrameLength > maxFrameLength) { //放弃读取帧 - buffer.skipBytes(minFrameLength + (keepDelimiterForNextFrame ? 0 : minDelimLength)); + buffer.skipBytes(minFrameLength + minDelimLength); fail(minFrameLength); return null; } - - if (keepDelimiterForNextFrame) { - frame = buffer.readRetainedSlice(minFrameLength); - return frame; - } - /*是否需要跳过某字节*/ if (minDelim.strip) { //忽略长度等于0的报文 @@ -273,9 +266,6 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { return -1; } - // Modbus RTU:为了避免把请求帧(常见固定8字节)误当响应帧(第3字节是byteCount)导致截断, - // 这里采用"候选长度 + CRC校验"方式确定帧边界,兼容:请求/回显/响应/异常响应。 - // 异常响应:地址(1) + 功能码(1|0x80) + 异常码(1) + CRC(2) = 5字节 if ((functionCode & 0x80) != 0) { int len = 5; @@ -309,8 +299,7 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { } int byteCount = buffer.getByte(readerIndex + 2) & 0xFF; int respLen = 3 + byteCount + 2; - log.debug("[{}] Modbus功能码{}: byteCount={}, 预期帧长={}, 可读={}", - deviceId, String.format("%02X", functionCode), byteCount, respLen, readable); + log.debug("[{}] Modbus功能码{}: byteCount={}, 预期帧长={}, 可读={}", deviceId, String.format("%02X", functionCode), byteCount, respLen, readable); if (respLen > maxFrameLength) { log.warn("[{}] Modbus帧长度{}超过最大限制{}", deviceId, respLen, maxFrameLength); return -1; @@ -368,13 +357,7 @@ public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder { byte[] crc = CRC16Utils.getCrc16Byte(data); // [low, high] int crcLo = buffer.getByte(offset + len - 2) & 0xFF; int crcHi = buffer.getByte(offset + len - 1) & 0xFF; - boolean valid = ((crc[0] & 0xFF) == crcLo) && ((crc[1] & 0xFF) == crcHi); - if (!valid && log.isDebugEnabled()) { - log.debug("[{}] CRC校验: 计算值={}{}, 实际值={}{}", deviceId, - String.format("%02X", crc[0] & 0xFF), String.format("%02X", crc[1] & 0xFF), - String.format("%02X", crcLo), String.format("%02X", crcHi)); - } - return valid; + return ((crc[0] & 0xFF) == crcLo) && ((crc[1] & 0xFF) == crcHi); } private void fail(long frameLength) { diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java index e68948c7..c6a1a7f6 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/model/ThingsModels/ThingsModelValueItem.java @@ -114,4 +114,10 @@ public class ThingsModelValueItem { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date ts; + /** + * 创建时间(用于排序) + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + } diff --git a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java index 52ef9e2d..12b1b467 100644 --- a/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java +++ b/fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/impl/ThingsModelServiceImpl.java @@ -599,6 +599,7 @@ public class ThingsModelServiceImpl implements IThingsModelService { arrayObjectThingsModel.setIsReadonly(param.getIsReadonly()); arrayObjectThingsModel.setDatatype(param.getDatatype()); arrayObjectThingsModel.setModelOrder(thingsModel.getModelOrder()); + arrayObjectThingsModel.setCreateTime(thingsModel.getCreateTime()); resultList.add(arrayObjectThingsModel); } } @@ -617,6 +618,7 @@ public class ThingsModelServiceImpl implements IThingsModelService { arrayThingsModel.setIsReadonly(thingsModel.getIsReadonly()); arrayThingsModel.setDatatype(datatype); arrayThingsModel.setModelOrder(thingsModel.getModelOrder()); + arrayThingsModel.setCreateTime(thingsModel.getCreateTime()); resultList.add(arrayThingsModel); } } else if ("object".equals(datatype.getType())) { @@ -630,6 +632,7 @@ public class ThingsModelServiceImpl implements IThingsModelService { newObjectThingsModel.setIsReadonly(objectThingsModel.getIsReadonly()); newObjectThingsModel.setDatatype(objectThingsModel.getDatatype()); newObjectThingsModel.setModelOrder(thingsModel.getModelOrder()); + newObjectThingsModel.setCreateTime(thingsModel.getCreateTime()); resultList.add(newObjectThingsModel); } } else { @@ -642,6 +645,7 @@ public class ThingsModelServiceImpl implements IThingsModelService { otherThingsModel.setIsReadonly(thingsModel.getIsReadonly()); otherThingsModel.setDatatype(datatype); otherThingsModel.setModelOrder(thingsModel.getModelOrder()); + otherThingsModel.setCreateTime(thingsModel.getCreateTime()); resultList.add(otherThingsModel); } return resultList; @@ -686,24 +690,31 @@ public class ThingsModelServiceImpl implements IThingsModelService { if (null == productId) { return new ArrayList<>(); } - // 1、获取产品物模型 + // 排序器:按 modelOrder 降序,createTime 降序(与数据库 SQL 一致) + Comparator modelComparator = Comparator + .comparing(ThingsModelDTO::getModelOrder, Comparator.reverseOrder()) + .thenComparing(ThingsModelDTO::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())); + + // 1、获取产品物模型并排序 Map map = itslCache.getCacheThMapByProductId(productId); List thingsModelDTOList = new ArrayList<>(); for (Map.Entry entry : map.entrySet()) { ThingsModel thingsModel = this.propertyDtoChangeThingsModel(entry.getValue()); thingsModelDTOList.addAll(this.changeObjectOrArray(thingsModel)); } + thingsModelDTOList.sort(modelComparator); // 2. 获取设备级计算物模型并合并 if (StringUtils.isNotEmpty(serialNumber)) { Map deviceCalculatedMap = itslCache.getDeviceCalculatedModelMap(serialNumber); + List deviceModelList = new ArrayList<>(); for (Map.Entry entry : deviceCalculatedMap.entrySet()) { ThingsModel thingsModel = this.propertyDtoChangeThingsModel(entry.getValue()); - thingsModelDTOList.addAll(this.changeObjectOrArray(thingsModel)); + deviceModelList.addAll(this.changeObjectOrArray(thingsModel)); } + deviceModelList.sort(modelComparator); + thingsModelDTOList.addAll(deviceModelList); } - - thingsModelDTOList.sort(Comparator.comparing(ThingsModelDTO::getModelOrder)); if (StringUtils.isEmpty(serialNumber)) { return thingsModelDTOList; } @@ -767,6 +778,7 @@ public class ThingsModelServiceImpl implements IThingsModelService { thingsModel.setSpecs(JSON.toJSONString(valueItem.getDatatype())); thingsModel.setType(valueItem.getType()); thingsModel.setModelId(valueItem.getModelId()); + thingsModel.setCreateTime(valueItem.getCreateTime()); return thingsModel; } diff --git a/vue/package-lock.json b/vue/package-lock.json index 7816511b..59295dee 100644 --- a/vue/package-lock.json +++ b/vue/package-lock.json @@ -5751,6 +5751,15 @@ "zrender": "5.6.1" } }, + "echarts-extension-leaflet": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/echarts-extension-leaflet/-/echarts-extension-leaflet-1.2.2.tgz", + "integrity": "sha512-EKZmIQDRqxnf6DtYWqujn2PRqHmEAq3yr0gsZAwmOT9rSy0Rwt9FlXKPjK3d+EVLqT6se6IQweyJTsqMg537Gg==", + "requires": { + "echarts": "^5.5.0", + "leaflet": "^1.9.4" + } + }, "echarts-liquidfill": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/echarts-liquidfill/-/echarts-liquidfill-3.1.0.tgz", @@ -8974,6 +8983,11 @@ "launch-editor": "^2.12.0" } }, + "leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmmirror.com/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/leven/-/leven-2.1.0.tgz", diff --git a/vue/package.json b/vue/package.json index ad40bf14..4ec6d911 100644 --- a/vue/package.json +++ b/vue/package.json @@ -46,6 +46,7 @@ "codemirror": "^5.65.2", "core-js": "3.25.3", "echarts": "^5.4.0", + "echarts-extension-leaflet": "^1.2.2", "echarts-liquidfill": "^3.0.6", "element-china-area-data": "^4.1.1", "element-ui": "2.15.10", diff --git a/vue/src/utils/map.js b/vue/src/utils/map.js index cd22d52d..77116b57 100644 --- a/vue/src/utils/map.js +++ b/vue/src/utils/map.js @@ -1,28 +1,66 @@ -export function loadBMap() { - return new Promise(function(resolve, reject) { - if (typeof BMap !== 'undefined') { - resolve(BMap) - return true - } - window.onBMapCallback = function() { - resolve(BMap) - } - // 使用https协议需要添加一下meta标签 - var protocolStr = document.location.protocol; - if(protocolStr == "https:") - { - let meta = document.createElement('meta') - meta.httpEquiv = 'Content-Security-Policy' - meta.content ='upgrade-insecure-requests' - meta.onerror = reject - document.head.appendChild(meta) - } - // 引入百度地图 - let script = document.createElement('script') - script.type = 'text/javascript' - script.src = - 'http://api.map.baidu.com/api?v=2.0&ak='+ process.env.VUE_APP_BAI_DU_AK +'&s=1&__ec_v__=20190126&callback=onBMapCallback' - script.onerror = reject - document.head.appendChild(script) - }) +// 使用OpenStreetMap和Leaflet替代百度地图 + +export function loadMap() { + + return new Promise(function(resolve, reject) { + // eslint-disable-next-line no-undef + if (typeof L !== 'undefined') { + // eslint-disable-next-line no-undef + resolve(L); + return true; + } + + // 加载Leaflet CSS + if (!document.querySelector('link[href*="leaflet.css"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; + link.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='; + link.crossOrigin = ''; + document.head.appendChild(link); + } + + // 加载Leaflet JS + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'; + script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo='; + script.crossOrigin = ''; + script.onload = function() { + // eslint-disable-next-line no-undef + resolve(L); + }; + script.onerror = reject; + document.head.appendChild(script); + }); + } + +// export function loadBMap() { +// return new Promise(function(resolve, reject) { +// if (typeof BMap !== 'undefined') { +// resolve(BMap) +// return true +// } +// window.onBMapCallback = function() { +// resolve(BMap) +// } +// // 使用https协议需要添加一下meta标签 +// var protocolStr = document.location.protocol; +// if(protocolStr == "https:") +// { +// let meta = document.createElement('meta') +// meta.httpEquiv = 'Content-Security-Policy' +// meta.content ='upgrade-insecure-requests' +// meta.onerror = reject +// document.head.appendChild(meta) +// } +// // 引入百度地图 +// let script = document.createElement('script') +// script.type = 'text/javascript' +// script.src = +// 'http://api.map.baidu.com/api?v=2.0&ak='+ process.env.VUE_APP_BAI_DU_AK +'&s=1&__ec_v__=20190126&callback=onBMapCallback' +// script.onerror = reject +// document.head.appendChild(script) +// }) +// } diff --git a/vue/src/views/bigScreen/indexs/center-map.vue b/vue/src/views/bigScreen/indexs/center-map.vue index 2c0f2081..effa2f20 100644 --- a/vue/src/views/bigScreen/indexs/center-map.vue +++ b/vue/src/views/bigScreen/indexs/center-map.vue @@ -15,7 +15,7 @@ @@ -2318,726 +2287,726 @@ $aqi-moderate-color: #ffeb3b; $aqi-unhealthy-color: #f44336; .weather-card { - height: 420px; - background: linear-gradient(135deg, var(--background-start) 0%, var(--background-end) 100%); - border-radius: 15px; - padding: 16px; - color: $secondary-color; - box-shadow: 0 4px 20px $shadow-color; - transition: transform 0.3s, box-shadow 0.3s; - display: flex; - flex-direction: column; - justify-content: space-between; + height: 420px; + background: linear-gradient(135deg, var(--background-start) 0%, var(--background-end) 100%); + border-radius: 15px; + padding: 16px; + color: $secondary-color; + box-shadow: 0 4px 20px $shadow-color; + transition: transform 0.3s, box-shadow 0.3s; + display: flex; + flex-direction: column; + justify-content: space-between; - &:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px $hover-shadow-color; + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 30px $hover-shadow-color; + } + + .weather-header { + text-align: center; + margin-bottom: 10px; + + h2 { + margin: 0; + font-size: 26px; + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: $secondary-color; } - .weather-header { - text-align: center; - margin-bottom: 10px; + .date-week { + display: flex; + justify-content: center; + gap: 10px; + font-size: 14px; + color: rgba($secondary-color, 0.8); + margin-top: 5px; + } + .weather-description { + margin-top: 5px; + font-size: 16px; + color: rgba($secondary-color, 0.9); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .weather-main { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 10px; + + .weather-icon { + width: 100px; // 增大图标尺寸 + height: 100px; + flex-shrink: 0; + } + + .temperature { + font-size: 23px; // 增大温度字体 + font-weight: bold; + color: $secondary-color; + margin-left: 20px; + display: flex; + align-items: center; + } + } + + .weather-details { + display: flex; + justify-content: space-around; + margin-top: 10px; + + .detail-item { + display: flex; + align-items: center; + font-size: 14px; + color: $secondary-color; + + i { + font-size: 18px; + margin-right: 5px; + color: $icon-color; + } + + .detail-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .air-quality { + margin-top: 15px; + padding: 10px 15px; + background-color: rgba($secondary-color, 0.15); + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; + + .aqi-info { + display: flex; + align-items: center; + gap: 10px; + + .aqi-value { + font-size: 18px; + font-weight: bold; + padding: 5px 10px; + border-radius: 5px; + color: #fff; + background-color: inherit; // 继承父元素的背景色 + } + + &.aqi-good { + background-color: $aqi-good-color; + } + + &.aqi-moderate { + background-color: $aqi-moderate-color; + } + + &.aqi-unhealthy { + background-color: $aqi-unhealthy-color; + } + + .aqi-level { + font-size: 14px; + font-weight: bold; + color: rgba($secondary-color, 0.9); + } + } + } + + /* 天气提示 */ + .weather-tip { + margin-top: 15px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + background-color: rgba($secondary-color, 0.2); + padding: 10px 15px; + border-radius: 10px; + color: $secondary-color; + + i { + font-size: 18px; + margin-right: 10px; + color: $icon-color; + } + + span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + /* 响应式设计 */ + @media (max-width: 768px) { + .weather-card { + padding: 15px; + + .weather-header { h2 { - margin: 0; - font-size: 26px; - font-weight: bold; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: $secondary-color; + font-size: 22px; } .date-week { - display: flex; - justify-content: center; - gap: 10px; - font-size: 14px; - color: rgba($secondary-color, 0.8); - margin-top: 5px; + font-size: 12px; } .weather-description { - margin-top: 5px; - font-size: 16px; - color: rgba($secondary-color, 0.9); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + font-size: 14px; } - } + } - .weather-main { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; - - .weather-icon { - width: 100px; // 增大图标尺寸 - height: 100px; - flex-shrink: 0; - } + .weather-main { + flex-direction: column; .temperature { - font-size: 23px; // 增大温度字体 - font-weight: bold; - color: $secondary-color; - margin-left: 20px; - display: flex; - align-items: center; + margin-left: 0; + margin-top: 10px; + font-size: 22px; } - } - .weather-details { - display: flex; - justify-content: space-around; - margin-top: 10px; - - .detail-item { - display: flex; - align-items: center; - font-size: 14px; - color: $secondary-color; - - i { - font-size: 18px; - margin-right: 5px; - color: $icon-color; - } - - .detail-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + .weather-icon { + width: 80px; + height: 80px; } - } + } - .air-quality { - margin-top: 15px; - padding: 10px 15px; - background-color: rgba($secondary-color, 0.15); - border-radius: 8px; - display: flex; - justify-content: center; + .weather-details { + flex-direction: column; align-items: center; + gap: 10px; + } + + .air-quality { + padding: 8px 12px; .aqi-info { - display: flex; - align-items: center; - gap: 10px; + flex-direction: row; + gap: 8px; - .aqi-value { - font-size: 18px; - font-weight: bold; - padding: 5px 10px; - border-radius: 5px; - color: #fff; - background-color: inherit; // 继承父元素的背景色 - } + .aqi-value { + font-size: 16px; + padding: 4px 8px; + } - &.aqi-good { - background-color: $aqi-good-color; - } - - &.aqi-moderate { - background-color: $aqi-moderate-color; - } - - &.aqi-unhealthy { - background-color: $aqi-unhealthy-color; - } - - .aqi-level { - font-size: 14px; - font-weight: bold; - color: rgba($secondary-color, 0.9); - } - } - } - - /* 天气提示 */ - .weather-tip { - margin-top: 15px; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - background-color: rgba($secondary-color, 0.2); - padding: 10px 15px; - border-radius: 10px; - color: $secondary-color; - - i { - font-size: 18px; - margin-right: 10px; - color: $icon-color; - } - - span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - /* 响应式设计 */ - @media (max-width: 768px) { - .weather-card { - padding: 15px; - - .weather-header { - h2 { - font-size: 22px; - } - - .date-week { - font-size: 12px; - } - - .weather-description { - font-size: 14px; - } - } - - .weather-main { - flex-direction: column; - - .temperature { - margin-left: 0; - margin-top: 10px; - font-size: 22px; - } - - .weather-icon { - width: 80px; - height: 80px; - } - } - - .weather-details { - flex-direction: column; - align-items: center; - gap: 10px; - } - - .air-quality { - padding: 8px 12px; - - .aqi-info { - flex-direction: row; - gap: 8px; - - .aqi-value { - font-size: 16px; - padding: 4px 8px; - } - - .aqi-level { - font-size: 12px; - } - } - } - - .weather-tip { - font-size: 12px; - } + .aqi-level { + font-size: 12px; + } } + } + + .weather-tip { + font-size: 12px; + } } + } } //天气完 // 设备数据统计显示 .card-panel { - height: 200px; - border-radius: 35px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - padding: 20px; - transition: transform 0.3s ease, box-shadow 0.3s ease; + height: 200px; + border-radius: 35px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + padding: 20px; + transition: transform 0.3s ease, box-shadow 0.3s ease; - /* 悬停效果,增加交互性 */ - &:hover { - transform: translateY(-5px); - box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); + /* 悬停效果,增加交互性 */ + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); + } + .device { + .card-panel-icon { + fill: #a7de87; } - .device { - .card-panel-icon { - fill: #a7de87; - } - .card-icon { - &:hover .card-panel-icon { - fill: #007aff; - } - } - .card-data { - .card-panel-num { - color: #a7de87; - } - } + .card-icon { + &:hover .card-panel-icon { + fill: #007aff; + } } - .product { - .card-panel-icon { - fill: #f3728c; - } - .card-icon { - &:hover .card-panel-icon { - fill: #007aff; - } - } - .card-data { - .card-panel-num { - color: #f3728c; - } - } + .card-data { + .card-panel-num { + color: #a7de87; + } } - .function { - .card-panel-icon { - fill: #ffc766; - } - .card-icon { - &:hover .card-panel-icon { - fill: #007aff; - } - } - .card-data { - .card-panel-num { - color: #ffc766; - } - } + } + .product { + .card-panel-icon { + fill: #f3728c; } - .monitor { - .card-panel-icon { - fill: #66c9ff; - } - .card-icon { - &:hover .card-panel-icon { - fill: #007aff; - } - } - .card-data { - .card-panel-num { - color: #66c9ff; - } - } + .card-icon { + &:hover .card-panel-icon { + fill: #007aff; + } } - .alert { - .card-panel-icon { - fill: #a076ef; - } - .card-icon { - &:hover .card-panel-icon { - fill: #007aff; - } - } - .card-data { - .card-panel-num { - color: #a076ef; - } - } + .card-data { + .card-panel-num { + color: #f3728c; + } } - .reports { - .card-panel-icon { - fill: #5dd5d1; - } - .card-icon { - &:hover .card-panel-icon { - fill: #007aff; - } - } - .card-data { - .card-panel-num { - color: #5dd5d1; - } - } + } + .function { + .card-panel-icon { + fill: #ffc766; } + .card-icon { + &:hover .card-panel-icon { + fill: #007aff; + } + } + .card-data { + .card-panel-num { + color: #ffc766; + } + } + } + .monitor { + .card-panel-icon { + fill: #66c9ff; + } + .card-icon { + &:hover .card-panel-icon { + fill: #007aff; + } + } + .card-data { + .card-panel-num { + color: #66c9ff; + } + } + } + .alert { + .card-panel-icon { + fill: #a076ef; + } + .card-icon { + &:hover .card-panel-icon { + fill: #007aff; + } + } + .card-data { + .card-panel-num { + color: #a076ef; + } + } + } + .reports { + .card-panel-icon { + fill: #5dd5d1; + } + .card-icon { + &:hover .card-panel-icon { + fill: #007aff; + } + } + .card-data { + .card-panel-num { + color: #5dd5d1; + } + } + } + + .card-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + + .card-icon { + display: flex; + justify-content: center; + align-items: center; + width: 55px; + height: 55px; + margin-bottom: 15px; + background-color: rgba(#ffffff, 0.65); + padding: 10px; + border-radius: 10px; + + /* 图标样式 */ + .card-panel-icon { + width: 80%; + height: 80%; + transition: fill 0.3s ease; + } + + /* 悬停时图标颜色变化 */ + } + + /* 数据区域 */ + .card-data { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + /* 标题样式 */ + .card-title { + font-size: 1.05rem; + color: #4d4d4d; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 1px; + font-weight: 600; + white-space: nowrap; /* 防止文本换行 */ + overflow: hidden; /* 超出部分隐藏 */ + text-overflow: ellipsis; /* 使用省略号表示超出部分 */ + } + + /* 数字样式 */ + .card-panel-num { + font-size: 1.5rem; + + font-weight: bold; + position: relative; + animation: countAnimation 3s ease-out forwards; + } + } + } + + /* 响应式设计,当屏幕宽度小于768px时 */ + @media (max-width: 768px) { + height: 150px; /* 减小卡片高度 */ + padding: 15px; /* 减小内边距 */ .card-content { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - height: 100%; + .card-icon { + width: 40px; + height: 40px; + margin-bottom: 10px; - .card-icon { - display: flex; - justify-content: center; - align-items: center; - width: 55px; - height: 55px; - margin-bottom: 15px; - background-color: rgba(#ffffff, 0.65); - padding: 10px; - border-radius: 10px; + .card-panel-icon { + width: 80%; + height: 80%; + } + } - /* 图标样式 */ - .card-panel-icon { - width: 80%; - height: 80%; - transition: fill 0.3s ease; - } - - /* 悬停时图标颜色变化 */ + .card-data { + .card-title { + font-size: 1rem; + margin-bottom: 8px; } - /* 数据区域 */ - .card-data { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - /* 标题样式 */ - .card-title { - font-size: 1.05rem; - color: #4d4d4d; - margin-bottom: 10px; - text-transform: uppercase; - letter-spacing: 1px; - font-weight: 600; - white-space: nowrap; /* 防止文本换行 */ - overflow: hidden; /* 超出部分隐藏 */ - text-overflow: ellipsis; /* 使用省略号表示超出部分 */ - } - - /* 数字样式 */ - .card-panel-num { - font-size: 1.5rem; - - font-weight: bold; - position: relative; - animation: countAnimation 3s ease-out forwards; - } + .card-panel-num { + font-size: 1.5rem; } + } } + } - /* 响应式设计,当屏幕宽度小于768px时 */ - @media (max-width: 768px) { - height: 150px; /* 减小卡片高度 */ - padding: 15px; /* 减小内边距 */ - - .card-content { - .card-icon { - width: 40px; - height: 40px; - margin-bottom: 10px; - - .card-panel-icon { - width: 80%; - height: 80%; - } - } - - .card-data { - .card-title { - font-size: 1rem; - margin-bottom: 8px; - } - - .card-panel-num { - font-size: 1.5rem; - } - } - } + /* 数字递增动画关键帧 */ + @keyframes countAnimation { + 0% { + opacity: 0; + transform: translateY(-10px); } - - /* 数字递增动画关键帧 */ - @keyframes countAnimation { - 0% { - opacity: 0; - transform: translateY(-10px); - } - 50% { - opacity: 1; - transform: translateY(5px); - } - 100% { - opacity: 1; - transform: translateY(0); - } + 50% { + opacity: 1; + transform: translateY(5px); } + 100% { + opacity: 1; + transform: translateY(0); + } + } } //数据完 // 折线图卡片 .line-card { - height: 420px; - border-radius: 15px; - padding: 16px; - color: $secondary-color; - box-shadow: 0 4px 20px $shadow-color; - transition: transform 0.3s, box-shadow 0.3s; - &:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px $hover-shadow-color; - } + height: 420px; + border-radius: 15px; + padding: 16px; + color: $secondary-color; + box-shadow: 0 4px 20px $shadow-color; + transition: transform 0.3s, box-shadow 0.3s; + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 30px $hover-shadow-color; + } } // 折线图完 // 地图 .map-card { - background: #ffffff; /* 使用纯色背景,增加清新感 */ - border-radius: 15px; /* 略微减小圆角 */ - overflow: hidden; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); /* 更柔和的阴影 */ - transition: transform 0.3s ease, box-shadow 0.3s ease; + background: #ffffff; /* 使用纯色背景,增加清新感 */ + border-radius: 15px; /* 略微减小圆角 */ + overflow: hidden; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); /* 更柔和的阴影 */ + transition: transform 0.3s ease, box-shadow 0.3s ease; + height: 500px; + + &:hover { + transform: translateY(-5px); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); + } + + .card-header { + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-bottom: 1px dashed #e0e0e0; + + .card-title { + margin: 0; + color: #333333; + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + + i { + margin-right: 8px; + color: #409eff; /* 主题色图标 */ + font-size: 20px; + } + } + } + + .map-container { + width: 100%; height: 500px; + position: relative; + border-radius: 15px; + overflow: hidden; /* 改为hidden,避免内容溢出 */ + z-index: 10; - &:hover { - transform: translateY(-5px); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); + .map { + width: 100% !important; + height: 100% !important; + min-height: 500px; + position: relative; + z-index: 10; + background: transparent; + /* 确保地图容器有明确的尺寸 */ + display: block; } + } - .card-header { - height: 30px; - display: flex; - align-items: center; - justify-content: center; - border-bottom: 1px dashed #e0e0e0; - - .card-title { - margin: 0; - color: #333333; - font-size: 18px; - font-weight: 600; - display: flex; - align-items: center; - - i { - margin-right: 8px; - color: #409eff; /* 主题色图标 */ - font-size: 20px; - } - } - } - - .map-container { - width: 100%; - height: 500px; - position: relative; - border-radius: 15px; - overflow: hidden; /* 改为hidden,避免内容溢出 */ - z-index: 10; - - .map { - width: 100% !important; - height: 100% !important; - min-height: 500px; - position: relative; - z-index: 10; - background: transparent; - /* 确保地图容器有明确的尺寸 */ - display: block; - } - } - - /* 确保ECharts地图容器正确显示 */ - .map :deep(.ec-extension-bmap) { - position: absolute !important; - width: 100% !important; - height: 100% !important; - z-index: 10 !important; - background-color: transparent !important; - } + /* 确保ECharts地图容器正确显示 */ + .map :deep(.ec-extension-bmap) { + position: absolute !important; + width: 100% !important; + height: 100% !important; + z-index: 10 !important; + background-color: transparent !important; + } } /* 响应式设计 */ @media (max-width: 768px) { - .map-card { - padding: 12px; + .map-card { + padding: 12px; + } + + .card-header { + padding: 18px 12px; + + .card-title { + font-size: 16px; + + i { + font-size: 18px; + } } + } - .card-header { - padding: 18px 12px; - - .card-title { - font-size: 16px; - - i { - font-size: 18px; - } - } - } - - .map-container { - height: 250px; - } + .map-container { + height: 250px; + } } @media (max-width: 480px) { - .map-card { - padding: 10px; - border-radius: 10px; + .map-card { + padding: 10px; + border-radius: 10px; + } + + .card-header { + padding: 15px 10px; + + .card-title { + font-size: 14px; + + i { + font-size: 16px; + } } + } - .card-header { - padding: 15px 10px; - - .card-title { - font-size: 14px; - - i { - font-size: 16px; - } - } - } - - .map-container { - height: 200px; - } + .map-container { + height: 200px; + } } // 地图完 // 信息栏 .message-card { - height: 240px; - margin-bottom: 20px; - border-radius: 15px; - box-shadow: 0 4px 20px $shadow-color; - transition: transform 0.3s, box-shadow 0.3s; - &:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px $hover-shadow-color; - } + height: 240px; + margin-bottom: 20px; + border-radius: 15px; + box-shadow: 0 4px 20px $shadow-color; + transition: transform 0.3s, box-shadow 0.3s; + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 30px $hover-shadow-color; + } } // 扩大后的信息栏 .message-card-expanded { - height: 500px; // 与地图高度一致 + height: 500px; // 与地图高度一致 + display: flex; + flex-direction: column; + + :deep(.el-card__body) { + flex: 1; display: flex; flex-direction: column; - - :deep(.el-card__body) { - flex: 1; - display: flex; - flex-direction: column; - padding: 16px; - overflow: hidden; - margin-bottom: 10px; - } + padding: 16px; + overflow: hidden; + margin-bottom: 10px; + } } // 柱状图 .card-container { - height: 240px; - width: 100%; - padding: 16px; - background: #ffffff; - border-radius: 15px; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); - overflow: hidden; - transition: transform 0.3s ease, box-shadow 0.3s ease; + height: 240px; + width: 100%; + padding: 16px; + background: #ffffff; + border-radius: 15px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: transform 0.3s ease, box-shadow 0.3s ease; - &:hover { - transform: translateY(-5px); - box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2); - } + &:hover { + transform: translateY(-5px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2); + } } /* 响应式设计 */ @media (max-width: 768px) { - .card-container { - .card { - flex-direction: column; - height: auto; + .card-container { + .card { + flex-direction: column; + height: auto; - .card-image { - width: 100%; + .card-image { + width: 100%; - height: auto; - } + height: auto; + } - .card-content { - width: 100%; - } - } + .card-content { + width: 100%; + } } + } } @media (max-width: 480px) { - .card-container { - .card { - .card-content { - padding: 10px; + .card-container { + .card { + .card-content { + padding: 10px; - .author { - font-size: 1em; - } - - .content { - font-size: 0.9em; - } - - .footer { - font-size: 0.8em; - } - } - - .card-image { - .overlay .thumb { - width: 40px; - height: 40px; - } - } + .author { + font-size: 1em; } + + .content { + font-size: 0.9em; + } + + .footer { + font-size: 0.8em; + } + } + + .card-image { + .overlay .thumb { + width: 40px; + height: 40px; + } + } } + } } // 书摘完 // 使用率 .rate { - border-radius: 15px; - padding: 16px; - color: $secondary-color; - box-shadow: 0 4px 20px $shadow-color; - transition: transform 0.3s, box-shadow 0.3s; - &:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px $hover-shadow-color; - } - .pieCpu { - height: 161px; - } - @media (max-width: 768px) { - height: 100%; - } + border-radius: 15px; + padding: 16px; + color: $secondary-color; + box-shadow: 0 4px 20px $shadow-color; + transition: transform 0.3s, box-shadow 0.3s; + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 30px $hover-shadow-color; + } + .pieCpu { + height: 161px; + } + @media (max-width: 768px) { + height: 100%; + } } // 使用率完 .phone-card { - border-radius: 15px; - box-shadow: 0 4px 20px $shadow-color; - transition: transform 0.3s, box-shadow 0.3s; - &:hover { - transform: translateY(-5px); - box-shadow: 0 8px 30px $hover-shadow-color; - } + border-radius: 15px; + box-shadow: 0 4px 20px $shadow-color; + transition: transform 0.3s, box-shadow 0.3s; + &:hover { + transform: translateY(-5px); + box-shadow: 0 8px 30px $hover-shadow-color; + } } .phone { - height: 729px; - width: 370px; - background-image: url('../assets/images/phone.png'); - background-size: cover; - margin: 0 auto; + height: 729px; + width: 370px; + background-image: url('../assets/images/phone.png'); + background-size: cover; + margin: 0 auto; } .phone-container { - height: 618px; - width: 343px; - position: relative; - top: 46px; - left: 12px; - background-color: #fff; + height: 618px; + width: 343px; + position: relative; + top: 46px; + left: 12px; + background-color: #fff; } .content { - line-height: 24px; - padding: 10px; - border: 1px solid #eee; - border-radius: 10px; + line-height: 24px; + padding: 10px; + border: 1px solid #eee; + border-radius: 10px; } .description { - font-size: 12px; + font-size: 12px; - tr { - line-height: 20px; - } + tr { + line-height: 20px; + } } // 文档类型公告的特殊样式 .document-notice { diff --git a/vue/src/views/iot/device/device-edit.vue b/vue/src/views/iot/device/device-edit.vue index 7b86eab6..96abaf03 100644 --- a/vue/src/views/iot/device/device-edit.vue +++ b/vue/src/views/iot/device/device-edit.vue @@ -1,326 +1,351 @@ diff --git a/vue/src/views/iot/device/device-linkage.vue b/vue/src/views/iot/device/device-linkage.vue index b460d0ee..de8956cf 100644 --- a/vue/src/views/iot/device/device-linkage.vue +++ b/vue/src/views/iot/device/device-linkage.vue @@ -1,350 +1,394 @@ diff --git a/vue/src/views/iot/device/instruction-parsing.vue b/vue/src/views/iot/device/instruction-parsing.vue index b42bdaa2..16fa3a6f 100644 --- a/vue/src/views/iot/device/instruction-parsing.vue +++ b/vue/src/views/iot/device/instruction-parsing.vue @@ -31,19 +31,25 @@
- -
- HEX: - {{ item.value }} -
-
- ASCII: -
{{ item.valueAscii }}
-
-
+ + +
JSON:
{{ formatJsonValue(item.value, item.valueAscii) }}
+ +
@@ -564,6 +570,24 @@ export default { } // 如果都不是JSON,返回提示信息 return '数据不是有效的JSON格式'; + }, + /**判断是否为有效的JSON */ + isValidJson(value) { + if (!value) return false; + try { + const parsed = JSON.parse(value); + // 确保是对象或数组,而不是简单值 + return typeof parsed === 'object' && parsed !== null; + } catch (e) { + return false; + } + }, + /**判断ASCII是否包含可读内容 */ + hasReadableAscii(asciiValue) { + if (!asciiValue) return false; + // 检查是否有可打印字符(排除纯控制字符) + const printableRegex = /[\x20-\x7E]/; + return printableRegex.test(asciiValue) && asciiValue.trim().length > 0; }, /**打开编辑框 */ openEdit() { @@ -990,6 +1014,11 @@ $right-btn-color: #1890ff; } .right { + display: flex; + flex-direction: column; + // 高度 = 头部36px + 内容700px + 底部64px + height: 800px; + .right-head { font-size: 14px; text-align: center; @@ -997,19 +1026,28 @@ $right-btn-color: #1890ff; justify-content: center; } + // 覆盖通用 .content 样式 + .content.beautify-scroll-def { + flex: 1; + height: auto; + display: flex; + flex-direction: column; + justify-content: flex-end; + padding: 0; + } + .quick-item { width: 80%; - margin: auto; + margin: 0 auto 12px auto; text-align: center; line-height: 36px; - margin-top: 12px; border: 1px solid $right-btn-color; color: $right-btn-color; cursor: pointer; border-radius: 4px; - &:last-child { - margin-bottom: 12px; + &:first-child { + margin-top: 12px; } }