feat(device): 新增设备在线状态查询与实时更新功能
- 新增设备在线状态视图对象类型定义 - 新增设备在线状态批量查询接口与详情页查询接口 - 表格数据加载完成后批量查询并更新设备在线状态 - 详情页独立查询在线状态并展示状态指示点 - 通过 SSE 推送实时更新表格与详情页的设备在线状态
This commit is contained in:
@@ -4,6 +4,7 @@ import type {
|
|||||||
DeviceForm,
|
DeviceForm,
|
||||||
DeviceQueryParams,
|
DeviceQueryParams,
|
||||||
DeviceItem,
|
DeviceItem,
|
||||||
|
DeviceOnlineVO,
|
||||||
DeviceProfileDetail,
|
DeviceProfileDetail,
|
||||||
DeviceProfileForm,
|
DeviceProfileForm,
|
||||||
DevicePasswordChangeForm,
|
DevicePasswordChangeForm,
|
||||||
@@ -456,6 +457,37 @@ const DeviceAPI = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询设备在线状态
|
||||||
|
*
|
||||||
|
* @param sns 设备序列号列表
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 查询单个设备在线状态
|
||||||
|
*
|
||||||
|
* @param sn 设备序列号
|
||||||
|
*/
|
||||||
|
getOnlineStatus(sn: string) {
|
||||||
|
return request<any, DeviceOnlineVO>({
|
||||||
|
url: `${DEVICE_BASE_URL}/online/status`,
|
||||||
|
method: "get",
|
||||||
|
params: { sn },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询设备在线状态
|
||||||
|
*
|
||||||
|
* @param sns 设备序列号列表
|
||||||
|
*/
|
||||||
|
getOnlineStatusBatch(sns: string[]) {
|
||||||
|
return request<any, Record<string, DeviceOnlineVO>>({
|
||||||
|
url: `${DEVICE_BASE_URL}/online/status/batch`,
|
||||||
|
method: "post",
|
||||||
|
data: sns,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传应用图标(全局图标库)
|
* 上传应用图标(全局图标库)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -58,6 +58,28 @@ export interface DeviceItem {
|
|||||||
status?: number;
|
status?: number;
|
||||||
/** 用户名 */
|
/** 用户名 */
|
||||||
Devicename?: string;
|
Devicename?: string;
|
||||||
|
/** 设备序列号 */
|
||||||
|
serialno?: string;
|
||||||
|
/** 在线状态(1:在线;0:离线) */
|
||||||
|
online?: number;
|
||||||
|
/** 最后心跳时间 */
|
||||||
|
lastHeartbeatTime?: string;
|
||||||
|
/** 推送ID */
|
||||||
|
pushId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设备在线状态视图对象 */
|
||||||
|
export interface DeviceOnlineVO {
|
||||||
|
/** 设备序列号 */
|
||||||
|
serialno?: string;
|
||||||
|
/** 在线状态(1:在线;0:离线) */
|
||||||
|
online?: number;
|
||||||
|
/** 最后心跳时间 */
|
||||||
|
lastHeartbeatTime?: string;
|
||||||
|
/** 设备 IP */
|
||||||
|
ip?: string;
|
||||||
|
/** 屏幕状态(1:亮屏;0:熄屏;null:未知) */
|
||||||
|
screenState?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设备表单对象 */
|
/** 设备表单对象 */
|
||||||
|
|||||||
@@ -388,6 +388,7 @@ const emit = defineEmits<{
|
|||||||
editClick: [row: IObject];
|
editClick: [row: IObject];
|
||||||
filterChange: [data: IObject];
|
filterChange: [data: IObject];
|
||||||
operateClick: [data: IOperateData];
|
operateClick: [data: IOperateData];
|
||||||
|
dataLoaded: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// 表格工具栏按钮配置
|
// 表格工具栏按钮配置
|
||||||
@@ -929,6 +930,8 @@ function fetchPageData(formData: IObject = {}, isRestart = false) {
|
|||||||
} else {
|
} else {
|
||||||
pageData.value = Array.isArray(data) ? data : (data?.list ?? (data as IObject)?.data ?? []);
|
pageData.value = Array.isArray(data) ? data : (data?.list ?? (data as IObject)?.data ?? []);
|
||||||
}
|
}
|
||||||
|
// 数据加载完成后通知父组件,供父组件做后处理(如批量查询设备在线状态)
|
||||||
|
emit("dataLoaded");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -971,7 +974,14 @@ function saveXlsx(fileData: BlobPart, fileName: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 暴露的属性和方法
|
// 暴露的属性和方法
|
||||||
defineExpose({ fetchPageData, exportPageData, getFilterParams, getSelectionData, handleRefresh });
|
defineExpose({
|
||||||
|
fetchPageData,
|
||||||
|
exportPageData,
|
||||||
|
getFilterParams,
|
||||||
|
getSelectionData,
|
||||||
|
handleRefresh,
|
||||||
|
pageData,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
<div class="info-content">
|
<div class="info-content">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="device-name">{{ deviceData.snName }}</span>
|
<span class="device-name">{{ deviceData.snName }}</span>
|
||||||
<el-tag type="success" size="small">
|
<span
|
||||||
|
class="status-dot"
|
||||||
|
:style="{ backgroundColor: deviceData.onlineStatus == 1 ? '#67c23a' : '#f56c6c' }"
|
||||||
|
/>
|
||||||
|
<el-tag :type="deviceData.onlineStatus == 1 ? 'success' : 'danger'" size="small">
|
||||||
{{ deviceData.onlineStatus == 1 ? "在线" : "离线" }}
|
{{ deviceData.onlineStatus == 1 ? "在线" : "离线" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
@@ -747,12 +751,14 @@ import type {
|
|||||||
DeviceBasicInfo,
|
DeviceBasicInfo,
|
||||||
DeviceHardwareInfo,
|
DeviceHardwareInfo,
|
||||||
DeviceNetworkInfo,
|
DeviceNetworkInfo,
|
||||||
|
DeviceOnlineVO,
|
||||||
DeviceOtherInfo,
|
DeviceOtherInfo,
|
||||||
DeviceSecurityInfo,
|
DeviceSecurityInfo,
|
||||||
DeviceLocationInfo,
|
DeviceLocationInfo,
|
||||||
DeviceScreenshot,
|
DeviceScreenshot,
|
||||||
} from "@/api/system/device/types";
|
} from "@/api/system/device/types";
|
||||||
import { nextTick, reactive, ref, watch } from "vue";
|
import { nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
import { useSse } from "@/composables";
|
||||||
import {
|
import {
|
||||||
Back,
|
Back,
|
||||||
Edit,
|
Edit,
|
||||||
@@ -960,6 +966,20 @@ async function loadDeviceDetail(serialno: string) {
|
|||||||
ElMessage.error("加载设备详情失败");
|
ElMessage.error("加载设备详情失败");
|
||||||
console.error("Error loading device detail:", error);
|
console.error("Error loading device detail:", error);
|
||||||
}
|
}
|
||||||
|
// 加载完设备基础信息后,单独查询设备在线状态
|
||||||
|
loadOnlineStatus(serialno);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 查询单台设备在线状态 */
|
||||||
|
async function loadOnlineStatus(serialno: string) {
|
||||||
|
try {
|
||||||
|
const onlineInfo = await DeviceAPI.getOnlineStatus(serialno);
|
||||||
|
if (onlineInfo) {
|
||||||
|
deviceData.onlineStatus = onlineInfo.online ?? 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("查询设备在线状态失败:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 基本信息 */
|
/** 基本信息 */
|
||||||
@@ -1319,6 +1339,41 @@ const handleStopApp = (row: ApkInstallInfo) => {
|
|||||||
|
|
||||||
defineExpose({ loadAll });
|
defineExpose({ loadAll });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 设备在线状态事件监听取消函数
|
||||||
|
*/
|
||||||
|
let unsubscribeDeviceOnlineStatus: (() => void) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 SSE 推送的设备在线状态更新详情页的在线状态
|
||||||
|
*/
|
||||||
|
function handleDeviceOnlineStatus(data: DeviceOnlineVO) {
|
||||||
|
if (!data || !data.serialno) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 仅当推送的设备 SN 与当前详情页设备匹配时才更新
|
||||||
|
if (data.serialno === props.serialno) {
|
||||||
|
deviceData.onlineStatus = data.online ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 监听 SSE 推送的设备在线状态,实时更新详情页
|
||||||
|
const sse = useSse();
|
||||||
|
unsubscribeDeviceOnlineStatus = sse.on<DeviceOnlineVO>(
|
||||||
|
"device-online-status",
|
||||||
|
handleDeviceOnlineStatus
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 组件销毁时取消 SSE 事件监听
|
||||||
|
if (unsubscribeDeviceOnlineStatus) {
|
||||||
|
unsubscribeDeviceOnlineStatus();
|
||||||
|
unsubscribeDeviceOnlineStatus = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/** 操作按钮事件 */
|
/** 操作按钮事件 */
|
||||||
const handleReturn = () => {
|
const handleReturn = () => {
|
||||||
emit("update:modelValue", false);
|
emit("update:modelValue", false);
|
||||||
@@ -1806,4 +1861,13 @@ const handleMoreActions = (command: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -58,7 +58,15 @@ const contentConfig: IContentConfig<DeviceQueryParams, DeviceItem> = {
|
|||||||
prop: "serialno",
|
prop: "serialno",
|
||||||
templet: "custom",
|
templet: "custom",
|
||||||
slotName: "serialno",
|
slotName: "serialno",
|
||||||
width: 150,
|
width: 220,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "在线状态",
|
||||||
|
align: "center",
|
||||||
|
prop: "online",
|
||||||
|
templet: "custom",
|
||||||
|
slotName: "onlineStatus",
|
||||||
|
width: 100,
|
||||||
},
|
},
|
||||||
{ label: "设备型号", align: "center", prop: "snModel", width: 200 },
|
{ label: "设备型号", align: "center", prop: "snModel", width: 200 },
|
||||||
{ label: "设备昵称", align: "center", prop: "snName", width: 200 },
|
{ label: "设备昵称", align: "center", prop: "snName", width: 200 },
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
@toolbar-click="handleToolbarClick"
|
@toolbar-click="handleToolbarClick"
|
||||||
@operate-click="handleOperateClick"
|
@operate-click="handleOperateClick"
|
||||||
@filter-change="handleFilterChange"
|
@filter-change="handleFilterChange"
|
||||||
|
@data-loaded="fetchDeviceOnlineStatus"
|
||||||
>
|
>
|
||||||
<template #status="scope">
|
<template #status="scope">
|
||||||
<el-tag :type="scope.row[scope.prop as string] == 1 ? 'success' : 'info'">
|
<el-tag :type="scope.row[scope.prop as string] == 1 ? 'success' : 'info'">
|
||||||
@@ -48,6 +49,15 @@
|
|||||||
:style="{ marginLeft: '2px' }"
|
:style="{ marginLeft: '2px' }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<template #onlineStatus="scope">
|
||||||
|
<span
|
||||||
|
class="status-dot"
|
||||||
|
:style="{ backgroundColor: scope.row.online == 1 ? '#67c23a' : '#f56c6c' }"
|
||||||
|
/>
|
||||||
|
<el-tag :type="scope.row.online == 1 ? 'success' : 'info'" size="small">
|
||||||
|
{{ scope.row.online == 1 ? "在线" : "离线" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
</device-page-content>
|
</device-page-content>
|
||||||
|
|
||||||
<!-- 新增 -->
|
<!-- 新增 -->
|
||||||
@@ -92,8 +102,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, ref } from "vue";
|
||||||
import DeviceAPI from "@/api/system/device";
|
import DeviceAPI from "@/api/system/device";
|
||||||
|
import type { DeviceOnlineVO } from "@/api/system/device/types";
|
||||||
import type { IObject, PageModalInstance } from "@/components/DeviceSn/types";
|
import type { IObject, PageModalInstance } from "@/components/DeviceSn/types";
|
||||||
|
import { useSse } from "@/composables";
|
||||||
import DeviceSnOverView from "@/components/DeviceSn/DeviceSnOverView.vue";
|
import DeviceSnOverView from "@/components/DeviceSn/DeviceSnOverView.vue";
|
||||||
import usePage from "@/components/DeviceSn/usePage";
|
import usePage from "@/components/DeviceSn/usePage";
|
||||||
import addModalConfig from "./config/add";
|
import addModalConfig from "./config/add";
|
||||||
@@ -222,7 +235,88 @@ const dialogVisible = ref(false);
|
|||||||
const overviewVisible = ref(false);
|
const overviewVisible = ref(false);
|
||||||
const selectedRow = ref<IObject>({});
|
const selectedRow = ref<IObject>({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询设备在线状态并更新表格数据
|
||||||
|
*/
|
||||||
|
async function fetchDeviceOnlineStatus() {
|
||||||
|
try {
|
||||||
|
// 从 contentRef 获取当前表格 pageData
|
||||||
|
const pageData = contentRef.value?.pageData as IObject[] | undefined;
|
||||||
|
if (!pageData || pageData.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 收集所有设备序列号
|
||||||
|
const sns = pageData.map((item) => item.serialno as string).filter((sn) => !!sn);
|
||||||
|
if (sns.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 批量查询在线状态
|
||||||
|
const onlineMap = await DeviceAPI.getOnlineStatusBatch(sns);
|
||||||
|
if (onlineMap) {
|
||||||
|
// 更新表格数据中的在线状态
|
||||||
|
pageData.forEach((item) => {
|
||||||
|
const sn = item.serialno as string;
|
||||||
|
const onlineInfo = onlineMap[sn];
|
||||||
|
if (onlineInfo) {
|
||||||
|
item.online = onlineInfo.online;
|
||||||
|
item.lastHeartbeatTime = onlineInfo.lastHeartbeatTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("批量查询设备在线状态失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SSE 设备在线状态事件监听取消函数
|
||||||
|
*/
|
||||||
|
let unsubscribeDeviceOnlineStatus: (() => void) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 SSE 推送的设备在线状态更新单条表格数据
|
||||||
|
*/
|
||||||
|
function handleDeviceOnlineStatus(data: DeviceOnlineVO) {
|
||||||
|
if (!data || !data.serialno) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pageData = contentRef.value?.pageData as IObject[] | undefined;
|
||||||
|
if (!pageData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const row = pageData.find((item) => item.serialno === data.serialno);
|
||||||
|
if (row) {
|
||||||
|
row.online = data.online;
|
||||||
|
row.lastHeartbeatTime = data.lastHeartbeatTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initOptions();
|
initOptions();
|
||||||
|
// 监听 SSE 推送的设备在线状态,实时更新表格对应行
|
||||||
|
const sse = useSse();
|
||||||
|
unsubscribeDeviceOnlineStatus = sse.on<DeviceOnlineVO>(
|
||||||
|
"device-online-status",
|
||||||
|
handleDeviceOnlineStatus
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
// 组件销毁时取消 SSE 事件监听
|
||||||
|
if (unsubscribeDeviceOnlineStatus) {
|
||||||
|
unsubscribeDeviceOnlineStatus();
|
||||||
|
unsubscribeDeviceOnlineStatus = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user