refactor(device): 优化设备详情字段与心跳数据加载逻辑
- 新增设备最后心跳查询接口及类型定义 - 设备在线状态字段统一更名为 online,移除重复字段 - 设备详情首次加载时从心跳接口获取最后上线时间 - WebSocket 推送在线状态时同步更新屏幕状态和最后心跳时间 - 编辑设备弹窗精简为仅可修改昵称,其余字段改为只读展示
This commit is contained in:
@@ -5,6 +5,7 @@ import type {
|
|||||||
DeviceQueryParams,
|
DeviceQueryParams,
|
||||||
DeviceItem,
|
DeviceItem,
|
||||||
DeviceOnlineVO,
|
DeviceOnlineVO,
|
||||||
|
DeviceHeartbeatVO,
|
||||||
DeviceProfileDetail,
|
DeviceProfileDetail,
|
||||||
DeviceProfileForm,
|
DeviceProfileForm,
|
||||||
DevicePasswordChangeForm,
|
DevicePasswordChangeForm,
|
||||||
@@ -488,6 +489,19 @@ const DeviceAPI = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询设备最后心跳记录(来自 TDengine)
|
||||||
|
*
|
||||||
|
* @param sn 设备序列号
|
||||||
|
*/
|
||||||
|
getLastHeartbeat(sn: string) {
|
||||||
|
return request<any, DeviceHeartbeatVO>({
|
||||||
|
url: `${DEVICE_BASE_URL}/online/heartbeat/last`,
|
||||||
|
method: "get",
|
||||||
|
params: { sn },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传应用图标(全局图标库)
|
* 上传应用图标(全局图标库)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -74,6 +74,22 @@ export interface DeviceOnlineVO {
|
|||||||
screenState?: number;
|
screenState?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 设备最后心跳视图对象(来自 TDengine) */
|
||||||
|
export interface DeviceHeartbeatVO {
|
||||||
|
/** 设备序列号 */
|
||||||
|
sn?: string;
|
||||||
|
/** 最后心跳时间 */
|
||||||
|
lastTime?: string;
|
||||||
|
/** 在线状态(1:在线;0:离线) */
|
||||||
|
online?: number;
|
||||||
|
/** 设备 IP */
|
||||||
|
ip?: string;
|
||||||
|
/** 信号强度 */
|
||||||
|
rssi?: string;
|
||||||
|
/** 屏幕是否亮起(1:亮屏;0:熄屏) */
|
||||||
|
screenOn?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** 设备表单对象 */
|
/** 设备表单对象 */
|
||||||
export interface DeviceForm {
|
export interface DeviceForm {
|
||||||
/** 设备ID */
|
/** 设备ID */
|
||||||
@@ -87,7 +103,7 @@ export interface DeviceForm {
|
|||||||
/** 设备手机号 */
|
/** 设备手机号 */
|
||||||
snMobile?: string;
|
snMobile?: string;
|
||||||
/** 设备在线状态(1:在线;0:离线) */
|
/** 设备在线状态(1:在线;0:离线) */
|
||||||
onlineStatus?: number;
|
online?: number;
|
||||||
/** 设备IMEI */
|
/** 设备IMEI */
|
||||||
snImei?: string;
|
snImei?: string;
|
||||||
/** 所属部门 */
|
/** 所属部门 */
|
||||||
|
|||||||
@@ -14,15 +14,28 @@
|
|||||||
<el-tag :type="deviceData.onlineStatus == 1 ? 'success' : 'danger'" size="small">
|
<el-tag :type="deviceData.onlineStatus == 1 ? 'success' : 'danger'" size="small">
|
||||||
{{ deviceData.onlineStatus == 1 ? "在线" : "离线" }}
|
{{ deviceData.onlineStatus == 1 ? "在线" : "离线" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="deviceData.onlineStatus == 1"
|
||||||
|
:type="deviceData.screenState == 1 ? 'warning' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
deviceData.screenState == 1
|
||||||
|
? "亮屏"
|
||||||
|
: deviceData.screenState === 0
|
||||||
|
? "熄屏"
|
||||||
|
: "未知"
|
||||||
|
}}
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">序列号:</span>
|
<span class="label">序列号:</span>
|
||||||
<span class="value">{{ props.serialno }}</span>
|
<span class="value">{{ formatText(props.serialno) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">IMEI:</span>
|
<span class="label">IMEI:</span>
|
||||||
<span class="value">{{ formatText(deviceData.imei) }}</span>
|
<span class="value">{{ formatText(deviceData.imei || networkInfo.mobileImei) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">设备所有者:</span>
|
<span class="label">设备所有者:</span>
|
||||||
@@ -50,11 +63,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">最后上线:</span>
|
<span class="label">最后上线:</span>
|
||||||
<span class="value">{{ formatText(deviceData.lastOnline) }}</span>
|
<span class="value">{{ formatDate(deviceData.lastOnline) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">设备编号:</span>
|
<span class="label">设备编号:</span>
|
||||||
<span class="value">{{ formatText(deviceData.deviceSn) }}</span>
|
<span class="value">{{ formatText() }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -491,9 +504,6 @@
|
|||||||
<el-descriptions-item label="渠道">
|
<el-descriptions-item label="渠道">
|
||||||
{{ formatText(otherInfo.channel) }}
|
{{ formatText(otherInfo.channel) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="设备 ID">
|
|
||||||
{{ formatText(otherInfo.deviceId) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="极光推送ID">
|
<el-descriptions-item label="极光推送ID">
|
||||||
{{ formatText(otherInfo.pushId) }}
|
{{ formatText(otherInfo.pushId) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -509,9 +519,6 @@
|
|||||||
<el-descriptions-item label="ICCID">
|
<el-descriptions-item label="ICCID">
|
||||||
{{ formatText(otherInfo.iccid) }}
|
{{ formatText(otherInfo.iccid) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="IMSI">
|
|
||||||
{{ formatText(otherInfo.imsi) }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="充电中">
|
<el-descriptions-item label="充电中">
|
||||||
{{ formatBool(otherInfo.isCharging) }}
|
{{ formatBool(otherInfo.isCharging) }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -818,6 +825,7 @@ const activeTab = ref("deviceInfo");
|
|||||||
const deviceData = reactive({
|
const deviceData = reactive({
|
||||||
snName: "",
|
snName: "",
|
||||||
onlineStatus: 0,
|
onlineStatus: 0,
|
||||||
|
screenState: null as number | null,
|
||||||
owner: "",
|
owner: "",
|
||||||
department: "",
|
department: "",
|
||||||
group: "",
|
group: "",
|
||||||
@@ -947,6 +955,9 @@ function assignTo<T extends object>(reactiveObj: T, data?: object) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 首次进入时是否已加载过最后心跳时间 */
|
||||||
|
let lastHeartbeatLoaded = false;
|
||||||
|
|
||||||
/** 加载设备头部信息 */
|
/** 加载设备头部信息 */
|
||||||
async function loadDeviceDetail(serialno: string) {
|
async function loadDeviceDetail(serialno: string) {
|
||||||
try {
|
try {
|
||||||
@@ -954,7 +965,7 @@ async function loadDeviceDetail(serialno: string) {
|
|||||||
if (response) {
|
if (response) {
|
||||||
Object.assign(deviceData, {
|
Object.assign(deviceData, {
|
||||||
snName: response.snName,
|
snName: response.snName,
|
||||||
onlineStatus: response.onlineStatus,
|
onlineStatus: response.online,
|
||||||
deviceSn: response.serialno || serialno,
|
deviceSn: response.serialno || serialno,
|
||||||
imei: response.snImei,
|
imei: response.snImei,
|
||||||
department: response.department,
|
department: response.department,
|
||||||
@@ -971,20 +982,38 @@ async function loadDeviceDetail(serialno: string) {
|
|||||||
}
|
}
|
||||||
// 加载完设备基础信息后,单独查询设备在线状态
|
// 加载完设备基础信息后,单独查询设备在线状态
|
||||||
loadOnlineStatus(serialno);
|
loadOnlineStatus(serialno);
|
||||||
|
// 首次进入时从接口获取最后心跳时间作为最后上线时间
|
||||||
|
if (!lastHeartbeatLoaded) {
|
||||||
|
loadLastHeartbeat(serialno);
|
||||||
|
lastHeartbeatLoaded = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询单台设备在线状态 */
|
/** 查询单台设备在线状态(仅更新在线状态和屏幕状态,不更新最后上线时间) */
|
||||||
async function loadOnlineStatus(serialno: string) {
|
async function loadOnlineStatus(serialno: string) {
|
||||||
try {
|
try {
|
||||||
const onlineInfo = await DeviceAPI.getOnlineStatus(serialno);
|
const onlineInfo = await DeviceAPI.getOnlineStatus(serialno);
|
||||||
if (onlineInfo) {
|
if (onlineInfo) {
|
||||||
deviceData.onlineStatus = onlineInfo.online ?? 0;
|
deviceData.onlineStatus = onlineInfo.online ?? 0;
|
||||||
|
deviceData.screenState = onlineInfo.screenState ?? null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("查询设备在线状态失败:", error);
|
console.error("查询设备在线状态失败:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 查询设备最后心跳时间(仅首次进入时调用,作为最后上线时间) */
|
||||||
|
async function loadLastHeartbeat(serialno: string) {
|
||||||
|
try {
|
||||||
|
const heartbeat = await DeviceAPI.getLastHeartbeat(serialno);
|
||||||
|
if (heartbeat?.lastTime) {
|
||||||
|
deviceData.lastOnline = heartbeat.lastTime;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("查询设备最后心跳时间失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 基本信息 */
|
/** 基本信息 */
|
||||||
async function loadBasicInfo(serialno: string) {
|
async function loadBasicInfo(serialno: string) {
|
||||||
loading.basic = true;
|
loading.basic = true;
|
||||||
@@ -1017,6 +1046,10 @@ async function loadNetworkInfo(serialno: string) {
|
|||||||
try {
|
try {
|
||||||
const data = await DeviceAPI.getNetworkInfo(serialno);
|
const data = await DeviceAPI.getNetworkInfo(serialno);
|
||||||
assignTo(networkInfo, data);
|
assignTo(networkInfo, data);
|
||||||
|
// 同步 IMEI 到头部信息
|
||||||
|
if (data?.mobileImei) {
|
||||||
|
deviceData.imei = data.mobileImei;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("加载网络信息失败", e);
|
console.error("加载网络信息失败", e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1357,6 +1390,10 @@ function handleDeviceOnlineStatus(data: DeviceOnlineVO) {
|
|||||||
// 仅当推送的设备 SN 与当前详情页设备匹配时才更新
|
// 仅当推送的设备 SN 与当前详情页设备匹配时才更新
|
||||||
if (data.serialno === props.serialno) {
|
if (data.serialno === props.serialno) {
|
||||||
deviceData.onlineStatus = data.online ?? 0;
|
deviceData.onlineStatus = data.online ?? 0;
|
||||||
|
deviceData.screenState = data.screenState ?? null;
|
||||||
|
if (data.lastHeartbeatTime) {
|
||||||
|
deviceData.lastOnline = data.lastHeartbeatTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="在线状态">
|
<el-descriptions-item label="在线状态">
|
||||||
<el-tag :type="detailData.onlineStatus === 1 ? 'success' : 'info'" effect="dark" round>
|
<el-tag :type="detailData.online === 1 ? 'success' : 'info'" effect="dark" round>
|
||||||
{{ detailData.onlineStatus === 1 ? "在线" : "离线" }}
|
{{ detailData.online === 1 ? "在线" : "离线" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|||||||
@@ -5,16 +5,6 @@ import type { IModalConfig } from "@/components/DeviceSn/types";
|
|||||||
import { DeviceEnum } from "@/enums/settings";
|
import { DeviceEnum } from "@/enums/settings";
|
||||||
import { useAppStore } from "@/stores";
|
import { useAppStore } from "@/stores";
|
||||||
|
|
||||||
/** 只读输入框配置(展示常用参数,不可编辑) */
|
|
||||||
const readonlyInput = (placeholder = "系统自动填充") => ({
|
|
||||||
type: "input" as const,
|
|
||||||
attrs: {
|
|
||||||
placeholder,
|
|
||||||
readonly: true,
|
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const modalConfig: IModalConfig<DeviceForm> = {
|
const modalConfig: IModalConfig<DeviceForm> = {
|
||||||
permPrefix: "sys:device",
|
permPrefix: "sys:device",
|
||||||
component: "drawer",
|
component: "drawer",
|
||||||
@@ -23,11 +13,10 @@ const modalConfig: IModalConfig<DeviceForm> = {
|
|||||||
size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500,
|
size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500,
|
||||||
},
|
},
|
||||||
pk: "id",
|
pk: "id",
|
||||||
// 编辑只回传可修改字段,其余保持不变
|
// 编辑只回传可修改字段(仅设备昵称)
|
||||||
beforeSubmit(data) {
|
beforeSubmit(data) {
|
||||||
return {
|
return {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
serialno: data.serialno,
|
|
||||||
snName: data.snName,
|
snName: data.snName,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
} as DeviceForm;
|
} as DeviceForm;
|
||||||
@@ -43,14 +32,9 @@ const modalConfig: IModalConfig<DeviceForm> = {
|
|||||||
attrs: {
|
attrs: {
|
||||||
placeholder: "设备序列号",
|
placeholder: "设备序列号",
|
||||||
readonly: true,
|
readonly: true,
|
||||||
|
disabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "设备型号",
|
|
||||||
prop: "snModel",
|
|
||||||
rules: [{ required: true, message: "设备型号不能为空", trigger: "blur" }],
|
|
||||||
...readonlyInput("设备型号"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "设备昵称",
|
label: "设备昵称",
|
||||||
prop: "snName",
|
prop: "snName",
|
||||||
@@ -61,43 +45,6 @@ const modalConfig: IModalConfig<DeviceForm> = {
|
|||||||
maxlength: 50,
|
maxlength: 50,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: "手机号码",
|
|
||||||
prop: "snMobile",
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
|
||||||
message: "请输入正确的手机号码",
|
|
||||||
trigger: "blur",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
...readonlyInput("绑定手机号"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "IMEI",
|
|
||||||
prop: "snImei",
|
|
||||||
...readonlyInput("设备IMEI"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "设备类型",
|
|
||||||
prop: "deviceType",
|
|
||||||
...readonlyInput("设备类型"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "来源",
|
|
||||||
prop: "source",
|
|
||||||
...readonlyInput("设备来源"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "激活时间",
|
|
||||||
prop: "activationTime",
|
|
||||||
...readonlyInput("激活时间"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "最后上线",
|
|
||||||
prop: "lastOnline",
|
|
||||||
...readonlyInput("最后上线时间"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "状态",
|
label: "状态",
|
||||||
prop: "status",
|
prop: "status",
|
||||||
|
|||||||
Reference in New Issue
Block a user