Files
vue3-element-admin/src/views/devices/sn/index.vue
TongTongStudio a0c620404a feat(device): 新增设备在线状态查询与实时更新功能
- 新增设备在线状态视图对象类型定义
- 新增设备在线状态批量查询接口与详情页查询接口
- 表格数据加载完成后批量查询并更新设备在线状态
- 详情页独立查询在线状态并展示状态指示点
- 通过 SSE 推送实时更新表格与详情页的设备在线状态
2026-08-09 20:03:18 +08:00

323 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-container h-full flex flex-1 flex-col">
<!-- 列表 -->
<div v-show="!dialogVisible" class="flex flex-col h-full">
<!-- 搜索 -->
<device-page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<device-page-content
ref="contentRef"
:content-config="contentConfig"
class="flex-1 overflow-hidden"
@add-click="handleAddClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operate-click="handleOperateClick"
@filter-change="handleFilterChange"
@data-loaded="fetchDeviceOnlineStatus"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop as string] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop as string] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<template #gender="scope">
<DictTag v-model="scope.row[scope.prop as string]" code="gender" />
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop as string] }}</el-text>
<copy-button
v-if="scope.row[scope.prop as string]"
:text="scope.row[scope.prop as string]"
:style="{ marginLeft: '2px' }"
/>
</template>
<template #serialno="scope">
<el-link type="primary" @click="handleSerialnoClick(scope.row)">
{{ scope.row[scope.prop as string] }}
</el-link>
<copy-button
v-if="scope.row[scope.prop as string]"
:text="scope.row[scope.prop as string]"
:style="{ marginLeft: '2px' }"
/>
</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-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<DictSelect v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template>
<template #openModal>
<el-button type="primary" @click="openSecondModal">打开二级弹窗</el-button>
</template>
</device-page-modal>
<!-- 编辑 -->
<device-page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #gender="scope">
<DictSelect v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template>
</device-page-modal>
</div>
<div v-show="dialogVisible">
<device-sn-detail
v-model="dialogVisible"
:serialno="selectedRow?.serialno"
@return-click="handleReturnClick"
/>
</div>
<!-- 操作列详情 - 概览抽屉样式与编辑抽屉一致 -->
<el-drawer v-model="overviewVisible" title="查看" size="500px" destroy-on-close>
<DeviceSnOverView :serialno="selectedRow?.serialno" />
<template #footer>
<el-button @click="overviewVisible = false">关闭</el-button>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, ref } from "vue";
import DeviceAPI from "@/api/system/device";
import type { DeviceOnlineVO } from "@/api/system/device/types";
import type { IObject, PageModalInstance } from "@/components/DeviceSn/types";
import { useSse } from "@/composables";
import DeviceSnOverView from "@/components/DeviceSn/DeviceSnOverView.vue";
import usePage from "@/components/DeviceSn/usePage";
import addModalConfig from "./config/add";
import contentConfig from "./config/content";
import editModalConfig from "./config/edit";
import searchConfig from "./config/search";
import { initOptions } from "./config/options";
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
handleAddClick,
handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 其他工具
function handleToolbarClick(name: string) {
console.warn(name);
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
function handleReturnClick() {
dialogVisible.value = false;
overviewVisible.value = false;
}
// 表格工具
const handleOperateClick = (data: IObject) => {
if (data.name === "detail") {
selectedRow.value = data.row;
overviewVisible.value = true;
} else if (data.name === "edit") {
editModalConfig.drawer = { ...editModalConfig.drawer, title: "修改" };
handleEditClick(data.row, async () => {
return await DeviceAPI.getFormData(data.row.serialno as string); // 根据SN获取详情
});
} else if (data.name === "reset_pwd") {
ElMessageBox.prompt("请输入用户名" + data.row.username + "」的新密码", "重置密码", {
confirmButtonText: "确定",
cancelButtonText: "取消",
}).then(
(result) => {
const value = result.value;
if (!value || value.length < 6) {
ElMessage.warning("密码至少需6位字符请重新输入");
return false;
}
DeviceAPI.resetPassword(data.row.id, value).then(() => {
ElMessage.success("密码重置成功,新密码是:" + value);
});
},
() => {
// 用户取消
}
);
} else if (data.name === "more") {
// 处理新增的自定义操作
ElMessage.info(`执行了自定义操作设备ID: ${data.row.id}`);
} else if (data.name === "refresh") {
DeviceAPI.deviceRefresh(data.row.serialno as string).then(() => {
ElMessage.success(`刷新设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "screenshot") {
DeviceAPI.deviceScreenshot(data.row.serialno as string).then(() => {
ElMessage.success(`截图设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "reboot") {
ElMessageBox.confirm("确定要重启该设备吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
DeviceAPI.deviceReboot(data.row.serialno as string).then(() => {
ElMessage.success(`重启设备设备SN: ${data.row.serialno as string}`);
});
})
.catch(() => {});
} else if (data.name === "shutdown") {
DeviceAPI.deviceShutdown(data.row.serialno as string).then(() => {
ElMessage.success(`关机设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "locate") {
DeviceAPI.deviceLocate(data.row.serialno as string).then(() => {
ElMessage.success(`定位设备设备SN: ${data.row.serialno as string}`);
});
} else if (data.name === "refactory") {
ElMessageBox.confirm("确定要重置该设备吗?此操作将清除设备所有数据,是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "error",
})
.then(() => {
DeviceAPI.deviceRestore(data.row.serialno as string).then(() => {
ElMessage.success(`重置设备设备SN: ${data.row.serialno as string}`);
});
})
.catch(() => {});
}
};
// 打开二级弹窗
const addModalRef2 = ref();
const openSecondModal = () => {
handleAddClick(addModalRef2 as Ref<PageModalInstance>);
};
// SN点击事件 - 打开详情弹窗
const handleSerialnoClick = async (row: IObject) => {
// 将当前行的数据传递给详情组件
selectedRow.value = row;
dialogVisible.value = true;
};
const dialogVisible = ref(false);
const overviewVisible = ref(false);
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(() => {
initOptions();
// 监听 SSE 推送的设备在线状态,实时更新表格对应行
const sse = useSse();
unsubscribeDeviceOnlineStatus = sse.on<DeviceOnlineVO>(
"device-online-status",
handleDeviceOnlineStatus
);
});
onBeforeUnmount(() => {
// 组件销毁时取消 SSE 事件监听
if (unsubscribeDeviceOnlineStatus) {
unsubscribeDeviceOnlineStatus();
unsubscribeDeviceOnlineStatus = null;
}
});
</script>
<style scoped>
.status-dot {
display: inline-block;
flex-shrink: 0;
width: 8px;
height: 8px;
margin-right: 4px;
border-radius: 50%;
}
</style>