feat(device): 在线状态每 30 秒批量刷新

抽离 refreshOnlineStatus 复用批量接口,页面定时刷新当前页 SN 在线状态,SSE 不可用时仍能保持准确。
This commit is contained in:
2026-11-07 11:25:17 +08:00
parent daca252cb9
commit d40b8984cd
3 changed files with 40 additions and 11 deletions

View File

@@ -1,19 +1,14 @@
import DeviceAPI from "@/api/device";
import type { DeviceItem, DeviceQueryParams } from "@/api/device/types";
import type { CrudTableConfig } from "@/components/Crud/types";
import { syncOnlineStatus, updateOnlineStatus } from "./online";
import { refreshOnlineStatus, syncOnlineStatus } from "./online";
/** 列表接口不保证带在线状态,加载后批量补齐 */
async function fetchDevices(params: DeviceQueryParams) {
const page = await DeviceAPI.getPage(params);
syncOnlineStatus(page.list);
const sns = page.list.map((item) => item.serialno).filter((sn): sn is string => !!sn);
if (sns.length) {
// 在线状态后补,不阻塞列表首屏
void DeviceAPI.getOnlineStatusBatch(sns)
.then((onlineMap) => Object.values(onlineMap ?? {}).forEach(updateOnlineStatus))
.catch(() => undefined);
}
// 在线状态后补,不阻塞列表首屏
void refreshOnlineStatus();
return page;
}

View File

@@ -1,14 +1,40 @@
import DeviceAPI from "@/api/device";
import type { DeviceItem, DeviceOnlineVO } from "@/api/device/types";
/** 设备在线状态:分页接口提供当前页初值,SSE 推送实时覆盖 */
/** 设备在线状态:分页接口提供当前页初值,批量查询与 SSE 推送覆盖 */
export const onlineStatus = reactive<Record<string, number>>({});
/** 当前页设备 SN,供定时批量刷新使用 */
let pageSns: string[] = [];
export function syncOnlineStatus(list: DeviceItem[]) {
pageSns = list.map((item) => item.serialno).filter((sn): sn is string => !!sn);
list.forEach((item) => {
if (item.serialno) onlineStatus[item.serialno] = item.online ?? 0;
// 列表接口未返回 online 时保留已有值,避免把批量/SSE 结果覆盖成离线
if (item.serialno && item.online !== undefined && item.online !== null) {
onlineStatus[item.serialno] = item.online;
}
});
}
export function updateOnlineStatus(data: DeviceOnlineVO) {
if (data?.serialno) onlineStatus[data.serialno] = data.online ?? 0;
}
/**
* 批量刷新当前页在线状态。
* 设备每 30 秒上报心跳,页面按同一频率刷新,不依赖列表接口是否返回 online。
*/
export async function refreshOnlineStatus(): Promise<void> {
const sns = pageSns;
if (!sns.length) return;
try {
const onlineMap = await DeviceAPI.getOnlineStatusBatch(sns);
// 以返回的 SN 作为键,兼容 value 中缺失 serialno 的情况
Object.entries(onlineMap ?? {}).forEach(([sn, info]) => {
if (info) onlineStatus[sn] = info.online ?? 0;
});
} catch {
// 刷新失败保留上一次状态,不打扰用户
}
}

View File

@@ -75,7 +75,7 @@ import DeviceSnOverView from "./components/DeviceSnOverView.vue";
import addFormConfig from "./config/add";
import contentConfig from "./config/content";
import editFormConfig from "./config/edit";
import { onlineStatus, updateOnlineStatus } from "./config/online";
import { onlineStatus, refreshOnlineStatus, updateOnlineStatus } from "./config/online";
import { initOptions } from "./config/options";
import searchConfig from "./config/search";
@@ -198,6 +198,9 @@ async function handleRowAction({ key, row }: { key: string; row: CrudRecord }) {
*/
let unsubscribeDeviceOnlineStatus: (() => void) | null = null;
/** 定时刷新在线状态(与设备 30s 心跳同频),SSE 不可用时仍能保持状态准确 */
let onlineStatusTimer: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
initOptions();
// 监听 SSE 推送的设备在线状态,实时更新表格对应行
@@ -206,6 +209,7 @@ onMounted(() => {
"device-online-status",
updateOnlineStatus,
);
onlineStatusTimer = setInterval(refreshOnlineStatus, 30000);
});
onBeforeUnmount(() => {
@@ -214,6 +218,10 @@ onBeforeUnmount(() => {
unsubscribeDeviceOnlineStatus();
unsubscribeDeviceOnlineStatus = null;
}
if (onlineStatusTimer) {
clearInterval(onlineStatusTimer);
onlineStatusTimer = null;
}
});
</script>