refactor(device): 优化设备详情字段与心跳数据加载逻辑

- 新增设备最后心跳查询接口及类型定义
- 设备在线状态字段统一更名为 online,移除重复字段
- 设备详情首次加载时从心跳接口获取最后上线时间
- WebSocket 推送在线状态时同步更新屏幕状态和最后心跳时间
- 编辑设备弹窗精简为仅可修改昵称,其余字段改为只读展示
This commit is contained in:
TongTongStudio
2026-08-12 08:28:10 +08:00
parent aebef55e0a
commit 9bdbefcfe2
5 changed files with 84 additions and 70 deletions

View File

@@ -5,6 +5,7 @@ import type {
DeviceQueryParams,
DeviceItem,
DeviceOnlineVO,
DeviceHeartbeatVO,
DeviceProfileDetail,
DeviceProfileForm,
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 },
});
},
/**
* 上传应用图标(全局图标库)
*

View File

@@ -74,6 +74,22 @@ export interface DeviceOnlineVO {
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 {
/** 设备ID */
@@ -87,7 +103,7 @@ export interface DeviceForm {
/** 设备手机号 */
snMobile?: string;
/** 设备在线状态(1:在线;0:离线) */
onlineStatus?: number;
online?: number;
/** 设备IMEI */
snImei?: string;
/** 所属部门 */

View File

@@ -14,15 +14,28 @@
<el-tag :type="deviceData.onlineStatus == 1 ? 'success' : 'danger'" size="small">
{{ deviceData.onlineStatus == 1 ? "在线" : "离线" }}
</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 class="info-grid">
<div class="info-item">
<span class="label">序列号</span>
<span class="value">{{ props.serialno }}</span>
<span class="value">{{ formatText(props.serialno) }}</span>
</div>
<div class="info-item">
<span class="label">IMEI</span>
<span class="value">{{ formatText(deviceData.imei) }}</span>
<span class="value">{{ formatText(deviceData.imei || networkInfo.mobileImei) }}</span>
</div>
<div class="info-item">
<span class="label">设备所有者</span>
@@ -50,11 +63,11 @@
</div>
<div class="info-item">
<span class="label">最后上线</span>
<span class="value">{{ formatText(deviceData.lastOnline) }}</span>
<span class="value">{{ formatDate(deviceData.lastOnline) }}</span>
</div>
<div class="info-item">
<span class="label">设备编号</span>
<span class="value">{{ formatText(deviceData.deviceSn) }}</span>
<span class="value">{{ formatText() }}</span>
</div>
</div>
</div>
@@ -491,9 +504,6 @@
<el-descriptions-item label="渠道">
{{ formatText(otherInfo.channel) }}
</el-descriptions-item>
<el-descriptions-item label="设备 ID">
{{ formatText(otherInfo.deviceId) }}
</el-descriptions-item>
<el-descriptions-item label="极光推送ID">
{{ formatText(otherInfo.pushId) }}
</el-descriptions-item>
@@ -509,9 +519,6 @@
<el-descriptions-item label="ICCID">
{{ formatText(otherInfo.iccid) }}
</el-descriptions-item>
<el-descriptions-item label="IMSI">
{{ formatText(otherInfo.imsi) }}
</el-descriptions-item>
<el-descriptions-item label="充电中">
{{ formatBool(otherInfo.isCharging) }}
</el-descriptions-item>
@@ -818,6 +825,7 @@ const activeTab = ref("deviceInfo");
const deviceData = reactive({
snName: "",
onlineStatus: 0,
screenState: null as number | null,
owner: "",
department: "",
group: "",
@@ -947,6 +955,9 @@ function assignTo<T extends object>(reactiveObj: T, data?: object) {
}
}
/** 首次进入时是否已加载过最后心跳时间 */
let lastHeartbeatLoaded = false;
/** 加载设备头部信息 */
async function loadDeviceDetail(serialno: string) {
try {
@@ -954,7 +965,7 @@ async function loadDeviceDetail(serialno: string) {
if (response) {
Object.assign(deviceData, {
snName: response.snName,
onlineStatus: response.onlineStatus,
onlineStatus: response.online,
deviceSn: response.serialno || serialno,
imei: response.snImei,
department: response.department,
@@ -971,20 +982,38 @@ async function loadDeviceDetail(serialno: string) {
}
// 加载完设备基础信息后,单独查询设备在线状态
loadOnlineStatus(serialno);
// 首次进入时从接口获取最后心跳时间作为最后上线时间
if (!lastHeartbeatLoaded) {
loadLastHeartbeat(serialno);
lastHeartbeatLoaded = true;
}
}
/** 查询单台设备在线状态 */
/** 查询单台设备在线状态(仅更新在线状态和屏幕状态,不更新最后上线时间) */
async function loadOnlineStatus(serialno: string) {
try {
const onlineInfo = await DeviceAPI.getOnlineStatus(serialno);
if (onlineInfo) {
deviceData.onlineStatus = onlineInfo.online ?? 0;
deviceData.screenState = onlineInfo.screenState ?? null;
}
} catch (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) {
loading.basic = true;
@@ -1017,6 +1046,10 @@ async function loadNetworkInfo(serialno: string) {
try {
const data = await DeviceAPI.getNetworkInfo(serialno);
assignTo(networkInfo, data);
// 同步 IMEI 到头部信息
if (data?.mobileImei) {
deviceData.imei = data.mobileImei;
}
} catch (e) {
console.error("加载网络信息失败", e);
} finally {
@@ -1357,6 +1390,10 @@ function handleDeviceOnlineStatus(data: DeviceOnlineVO) {
// 仅当推送的设备 SN 与当前详情页设备匹配时才更新
if (data.serialno === props.serialno) {
deviceData.onlineStatus = data.online ?? 0;
deviceData.screenState = data.screenState ?? null;
if (data.lastHeartbeatTime) {
deviceData.lastOnline = data.lastHeartbeatTime;
}
}
}

View File

@@ -32,8 +32,8 @@
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="在线状态">
<el-tag :type="detailData.onlineStatus === 1 ? 'success' : 'info'" effect="dark" round>
{{ detailData.onlineStatus === 1 ? "在线" : "离线" }}
<el-tag :type="detailData.online === 1 ? 'success' : 'info'" effect="dark" round>
{{ detailData.online === 1 ? "在线" : "离线" }}
</el-tag>
</el-descriptions-item>
</el-descriptions>

View File

@@ -5,16 +5,6 @@ import type { IModalConfig } from "@/components/DeviceSn/types";
import { DeviceEnum } from "@/enums/settings";
import { useAppStore } from "@/stores";
/** 只读输入框配置(展示常用参数,不可编辑) */
const readonlyInput = (placeholder = "系统自动填充") => ({
type: "input" as const,
attrs: {
placeholder,
readonly: true,
disabled: true,
},
});
const modalConfig: IModalConfig<DeviceForm> = {
permPrefix: "sys:device",
component: "drawer",
@@ -23,11 +13,10 @@ const modalConfig: IModalConfig<DeviceForm> = {
size: useAppStore().device === DeviceEnum.MOBILE ? "80%" : 500,
},
pk: "id",
// 编辑只回传可修改字段,其余保持不变
// 编辑只回传可修改字段(仅设备昵称)
beforeSubmit(data) {
return {
id: data.id,
serialno: data.serialno,
snName: data.snName,
status: data.status,
} as DeviceForm;
@@ -43,14 +32,9 @@ const modalConfig: IModalConfig<DeviceForm> = {
attrs: {
placeholder: "设备序列号",
readonly: true,
disabled: true,
},
},
{
label: "设备型号",
prop: "snModel",
rules: [{ required: true, message: "设备型号不能为空", trigger: "blur" }],
...readonlyInput("设备型号"),
},
{
label: "设备昵称",
prop: "snName",
@@ -61,43 +45,6 @@ const modalConfig: IModalConfig<DeviceForm> = {
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: "状态",
prop: "status",