fix: 修正设备详情字段映射并增强设备列表展示

- DeviceItem: 修复 lon/lat 注解大小写导致经纬度恒为空的问题
- DeviceItem: 补全视联网(viot_*)与机卡协同上报(up_*)等缺失字段
- index.ftl: 设备详情弹窗补全字段展示,枚举值友好映射
- index.ftl: 设备列表重点标记测试设备(整行高亮+测试徽章)
- index.ftl: 新增"最近"列,按最后在线时间显示今天/昨天/前天/N天前

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-17 17:25:40 +08:00
co-authored by Claude
parent 0845c38b72
commit 8bd45a4813
2 changed files with 165 additions and 14 deletions
@@ -3,23 +3,27 @@ package com.yixiong.iot.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 设备列表中单个设备的数据结构
* 设备详情 / 设备列表项的数据结构
* 字段对应 OneNET 平台 /device/detail 接口返回的 data 节点,
* 同时兼容 /device/list 列表项(列表仅返回部分字段,缺失字段为 null)。
*/
@Data
public class DeviceItem {
/** 设备 ID */
/** 设备 ID(平台返回 int/string,统一用 String 接收,避免前端 Long 精度丢失) */
private String did;
/** 产品 ID */
private String pid;
/** 接入协议2-MQTT4-LwM2M */
/** 接入协议,取值见 OneNET 接入协议取值说明表(如 51) */
@JsonProperty("access_pt")
private Integer accessPt;
/** 数据协议 */
/** 数据协议,取值见 OneNET 数据协议取值说明表 */
@JsonProperty("data_pt")
private Integer dataPt;
@@ -29,6 +33,10 @@ public class DeviceItem {
/** 设备描述 */
private String desc;
/** 设备鉴权信息 */
@JsonProperty("auth_info")
private String authInfo;
/**
* 设备状态
* 0-离线;1-在线;2-未激活
@@ -79,14 +87,20 @@ public class DeviceItem {
@JsonProperty("enable_status")
private Boolean enableStatus;
/** 经度 */
@JsonProperty("Lon")
/** 设备标签列表 */
private List<String> tags;
/** 经度(注意:平台返回小写 lon,勿用大写注解) */
@JsonProperty("lon")
private String lon;
/** 纬度 */
@JsonProperty("Lat")
/** 纬度(注意:平台返回小写 lat,勿用大写注解) */
@JsonProperty("lat")
private String lat;
/** 设备芯片信息 */
private Integer chip;
/** 设备资源自动订阅是否启用 */
private Boolean obsv;
@@ -100,17 +114,106 @@ public class DeviceItem {
/** 设备 IMSI 历史变更记录 */
@JsonProperty("imsi_old")
private java.util.List<String> imsiOld;
private List<String> imsiOld;
/** 设备 IMSI 最近一次修改时间 */
@JsonProperty("imsi_mt")
private String imsiMt;
/** 移动视联网设备唯一标识 */
// ===== 视联网设备字段 =====
/** 视频设备编码,视联网设备返回 */
@JsonProperty("viot_device_sn")
private String viotDeviceSn;
/** 移动视联网设备接入协议 */
/**
* 视频设备接入协议,视联网设备返回
* 1-GB28181 协议;2-千里眼协议
*/
@JsonProperty("viot_protocol")
private Integer viotProtocol;
/**
* 视频设备类型,视联网设备返回
* 1-IPC2-NVR
*/
@JsonProperty("viot_device_type")
private Integer viotDeviceType;
// ===== 视联网国标协议(GB28181)设备字段 =====
/** SIP 服务器 ID,国标摄像头设备返回 */
@JsonProperty("viot_sip_server_id")
private String viotSipServerId;
/** SIP 服务器域,国标摄像头设备返回 */
@JsonProperty("viot_sip_server_domain")
private String viotSipServerDomain;
/** SIP 服务器 IP,国标摄像头设备返回 */
@JsonProperty("viot_sip_server_ip")
private String viotSipServerIp;
/** SIP 服务器端口,国标摄像头设备返回 */
@JsonProperty("viot_sip_server_port")
private String viotSipServerPort;
/** 登录用户名,国标摄像头设备返回 */
@JsonProperty("viot_login_name")
private String viotLoginName;
/** 登录密码,国标摄像头设备返回 */
@JsonProperty("viot_login_pwd")
private String viotLoginPwd;
// ===== 视联网千里眼协议设备字段 =====
/** 设备 modelId,如:cmiot_E4702_002 */
@JsonProperty("viot_module_id")
private String viotModuleId;
/** 软件版本,如:2.860.20250512 */
@JsonProperty("viot_camera_app_version")
private String viotCameraAppVersion;
/** 固件版本,如:2.860.20250512 */
@JsonProperty("viot_firmware_version")
private String viotFirmwareVersion;
/** 千里眼设备 imeicmei),如:119094672043670 */
@JsonProperty("viot_imei")
private String viotImei;
// ===== 机卡协同设备上报字段(设备上报后返回) =====
/** 设备上报的 iccid,如:898604310000000000 */
@JsonProperty("up_iccid")
private String upIccid;
/** 设备上报的 msisdn,如:+8613800000000 */
@JsonProperty("up_msisdn")
private String upMsisdn;
/** 设备上报的 sn */
@JsonProperty("up_sn")
private String upSn;
/** 设备上报的 imei,如:119094672043670 */
@JsonProperty("up_imei")
private String upImei;
/** 设备上报的 imsi,如:460010000000000 */
@JsonProperty("up_imsi")
private String upImsi;
/** 设备上报的 mac,如:08:00:27:01:02:03 */
@JsonProperty("up_mac")
private String upMac;
/**
* 设备上报的网络类型
* 0-非蜂窝网;1-移动;2-电信;3-联通;4-广电
*/
@JsonProperty("up_network")
private Integer upNetwork;
}
+51 -3
View File
@@ -49,6 +49,13 @@
.tag-info { background: #e6f4ff; color: #1677ff; }
.tag-warn { background: #fff7e6; color: #d46b08; }
.tag-error { background: #fff0f0; color: #d32f2f; }
/* 测试设备标记 */
.tag-test { background: #fff7e6; color: #d46b08; border: 1px solid #ffd591; margin-left: 6px; }
.row-test td { background: #fffbe6 !important; }
/* 最后在线相对时间配色:今天绿色、昨天/前天橙色、更久灰色 */
.rel-today { color: #389e0d; font-weight: 600; }
.rel-recent { color: #d46b08; }
.rel-old { color: #999; }
.pagination { display: flex; align-items: center; gap: 8px; margin-top: 14px; font-size: 13px; color: #666; }
.page-btn { padding: 5px 12px; border: 1px solid #ddd; border-radius: 4px; background: #fff; cursor: pointer; font-size: 13px; }
.page-btn:hover:not(:disabled) { border-color: #1677ff; color: #1677ff; }
@@ -251,6 +258,22 @@
if (isNaN(d.getTime())) return val;
return d.toLocaleString('zh-CN', {hour12: false});
}
// 将 ISO/时间戳格式化为相对今天的描述:今天 / 昨天 / 前天 / N天前
// 返回带颜色 class 的 span,今天绿色,昨天/前天橙色,更久灰色
function fmtRel(val) {
if (!val) return '<span class="rel-old">-</span>';
const t = new Date(val);
if (isNaN(t.getTime())) return '<span class="rel-old">-</span>';
// 取本地日期零点,按日历日计算天数差,避免时分秒干扰
const startOfDay = d => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
const diff = Math.floor((startOfDay(new Date()) - startOfDay(t)) / 86400000);
let text, cls;
if (diff <= 0) { text = '今天'; cls = 'rel-today'; } // 今天或异常未来时间兜底
else if (diff === 1) { text = '昨天'; cls = 'rel-recent'; }
else if (diff === 2) { text = '前天'; cls = 'rel-recent'; }
else { text = diff + '天前'; cls = 'rel-old'; }
return '<span class="' + cls + '">' + text + '</span>';
}
function switchTab(name) {
document.querySelectorAll('.tab-btn').forEach((b, i) => b.classList.toggle('active', ['product','device'][i] === name));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
@@ -288,6 +311,9 @@
// ===== 设备列表 =====
let _curDeviceName = '', _curDeviceLimit = 10;
// 重点关注的测试设备(按 imei 或 设备名称 命中),列表中整行高亮 + 名称后加测试徽章
const TEST_DEVICES = new Set(['863079075181776', '863079075146761']);
const isTestDevice = d => TEST_DEVICES.has(d.imei) || TEST_DEVICES.has(d.name);
function queryDeviceList(offset) {
_curDeviceName = document.getElementById('device-name-input').value.trim();
_curDeviceLimit = parseInt(document.getElementById('device-limit-select').value);
@@ -300,14 +326,15 @@
const list = res.data.list || [];
if (!list.length) { el.innerHTML = '<div class="tip">暂无数据</div>'; return; }
const statusTag = s => s===1?'<span class="tag tag-online">在线</span>':s===0?'<span class="tag tag-offline">离线</span>':'<span class="tag tag-inactive">未激活</span>';
el.innerHTML = '<div class="table-wrap"><table><thead><tr><th>设备 ID</th><th>设备名称</th><th>状态</th><th>创建时间</th><th>激活时间</th><th>最后在线</th><th>操作</th></tr></thead><tbody>'
+ list.map(d => '<tr>'
el.innerHTML = '<div class="table-wrap"><table><thead><tr><th>设备 ID</th><th>设备名称</th><th>状态</th><th>创建时间</th><th>激活时间</th><th>最后在线</th><th>最近</th><th>操作</th></tr></thead><tbody>'
+ list.map(d => '<tr class="' + (isTestDevice(d)?'row-test':'') + '">'
+ '<td>' + (d.did||'-') + '</td>'
+ '<td>' + (d.name||'-') + '</td>'
+ '<td>' + (d.name||'-') + (isTestDevice(d)?'<span class="tag tag-test">测试</span>':'') + '</td>'
+ '<td>' + statusTag(d.status) + '</td>'
+ '<td>' + fmtDate(d.create_time) + '</td>'
+ '<td>' + fmtDate(d.activate_time) + '</td>'
+ '<td>' + fmtDate(d.last_time) + '</td>'
+ '<td>' + fmtRel(d.last_time) + '</td>'
+ '<td><button class="btn-sm btn-detail" onclick="showDeviceDetail(\'' + d.name + '\')">详情</button>'
+ '<button class="btn-sm btn-data" onclick="showDatapoints(\'' + (d.name||'') + '\',\'' + (d.did||'') + '\')">数据点</button>'
+ '<button class="btn-sm btn-event" onclick="showEventFromList(\'' + (d.name||'') + '\',\'' + (d.did||'') + '\')">事件记录</button>'
@@ -330,14 +357,35 @@
if (res.code !== 0) { document.getElementById('modal-detail-body').innerHTML = '<div class="err">错误:' + res.msg + '</div>'; return; }
const d = res.data || {};
const statusTag = s => s===1?'<span class="tag tag-online">在线</span>':s===0?'<span class="tag tag-offline">离线</span>':'<span class="tag tag-inactive">未激活</span>';
// 视联网协议 / 上报网络类型枚举映射
const viotProtocol = {1:'GB28181', 2:'千里眼'};
const viotDeviceType = {1:'IPC', 2:'NVR'};
const upNetwork = {0:'非蜂窝', 1:'移动', 2:'电信', 3:'联通', 4:'广电'};
document.getElementById('modal-detail-body').innerHTML = '<div class="detail-grid">' + [
['设备 ID', d.did], ['设备名称', d.name], ['描述', d.desc||'-'],
['产品 ID', d.pid], ['状态', statusTag(d.status)],
['接入协议', d.access_pt], ['数据协议', d.data_pt],
['鉴权信息', d.auth_info||'-'], ['标签', (d.tags&&d.tags.length)?d.tags.join(','):'-'],
['创建时间', fmtDate(d.create_time)], ['激活时间', fmtDate(d.activate_time)],
['最后在线', fmtDate(d.last_time)], ['IMEI', d.imei||'-'],
['IMSI', d.imsi||'-'], ['经度', d.lon||'-'], ['纬度', d.lat||'-'],
['分组 ID', d.group_id||'-'], ['鉴权 Key', d.sec_key||'-'],
['启用状态', d.enable_status?'启用':'禁用'], ['自动订阅', d.obsv?'是':'否'],
['订阅状态', d.obsv_st?'是':'否'], ['私密性', d.private?'是':'否'],
// 视联网设备字段
['视频编码', d.viot_device_sn||'-'],
['视频协议', d.viot_protocol!=null?(viotProtocol[d.viot_protocol]??d.viot_protocol):'-'],
['视频类型', d.viot_device_type!=null?(viotDeviceType[d.viot_device_type]??d.viot_device_type):'-'],
['SIP 服务器 ID', d.viot_sip_server_id||'-'], ['SIP 域', d.viot_sip_server_domain||'-'],
['SIP IP', d.viot_sip_server_ip||'-'], ['SIP 端口', d.viot_sip_server_port||'-'],
['登录用户名', d.viot_login_name||'-'], ['登录密码', d.viot_login_pwd||'-'],
['ModelId', d.viot_module_id||'-'], ['软件版本', d.viot_camera_app_version||'-'],
['固件版本', d.viot_firmware_version||'-'], ['千里眼 IMEI', d.viot_imei||'-'],
// 机卡协同上报字段
['上报 ICCID', d.up_iccid||'-'], ['上报 MSISDN', d.up_msisdn||'-'],
['上报 SN', d.up_sn||'-'], ['上报 IMEI', d.up_imei||'-'],
['上报 IMSI', d.up_imsi||'-'], ['上报 MAC', d.up_mac||'-'],
['上报网络', d.up_network!=null?(upNetwork[d.up_network]??d.up_network):'-'],
].map(([k,v]) => '<div class="detail-item"><span class="detail-label">'+k+'</span><span class="detail-value">'+(v??'-')+'</span></div>').join('') + '</div>';
}).catch(e => { document.getElementById('modal-detail-body').innerHTML = '<div class="err">请求失败:' + e.message + '</div>'; });
}