Files
vue3-element-admin/src/views/devices/sn/index.vue
tongtongstudio 2bb36bc895 fix(device): 设备示意图改为模块导入并清理残留死代码
- 设备示意图改用 import 引入,避免生产构建后 /src/assets 路径 404
- SSE 改为直接引用 @/utils/sse,还原上游 composables 入口
- 移除已失效的 /static 代理与未被调用的 DeviceAPI.getInfo
2026-11-05 09:20:51 +08:00

355 lines
12 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">
<!-- 搜索 -->
<DevicePageSearch
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<DevicePageContent
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>
</DevicePageContent>
<!-- 新增 -->
<DevicePageModal
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>
</DevicePageModal>
<!-- 编辑 -->
<DevicePageModal
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>
</DevicePageModal>
</div>
<div v-show="dialogVisible">
<DeviceSnDetail
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/device";
import type { DeviceOnlineVO } from "@/api/device/types";
import type { IObject, PageModalInstance } from "./components/types";
import { useSse } from "@/utils/sse";
import DevicePageContent from "./components/DevicePageContent.vue";
import DevicePageModal from "./components/DevicePageModal.vue";
import DevicePageSearch from "./components/DevicePageSearch.vue";
import DeviceSnDetail from "./components/DeviceSnDetail.vue";
import DeviceSnOverView from "./components/DeviceSnOverView.vue";
import usePage from "./components/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 === "mirror") {
// 屏幕镜像:通知设备端启动屏幕采集,由家属端 client 进行 WebRTC 实时远程观看/控制
DeviceAPI.deviceMirror(data.row.serialno as string)
.then(() => {
ElMessage.success(`已发送屏幕镜像指令,设备SN: ${data.row.serialno as string}`);
})
.catch(() => {});
} 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(() => {});
} else if (data.name === "unbind") {
// 解除设备手机号绑定(管理员端):确认后清除 snMobile 并推送通知设备端标记解绑
const sn = data.row.serialno as string;
const mobile = (data.row.snMobile as string) || "";
ElMessageBox.confirm(
mobile
? `设备「${sn}」当前绑定手机号:${mobile}\n确定要解除该绑定吗?解除后需由家属端重新绑定。`
: `设备「${sn}」当前未绑定手机号,无需解除绑定。`,
"解除绑定",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
},
)
.then(() => {
DeviceAPI.unbindDeviceMobile(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>