CMVR-IOT-UI/src/views/device/register/index.vue

185 lines
8.1 KiB
Vue
Raw Normal View History

2025-08-21 14:11:58 +08:00
<template>
<div class="app-container robot-device-page">
<section class="page-heading">
<div>
<h2>机器人设备</h2>
<p>设备由机器人在线上报,切换机器人后自动刷新。</p>
</div>
<div class="robot-selector">
<span>当前机器人</span>
<el-select v-model="selectedRobotId" filterable placeholder="请选择机器人"
:loading="robotLoading" @change="loadDevices">
<el-option v-for="robot in robotOptions" :key="robot.value"
:label="robot.label" :value="robot.value" />
</el-select>
<el-button :icon="Refresh" circle title="刷新设备" :loading="loading" @click="loadDevices" />
</div>
</section>
<section class="status-strip">
<div><strong>{{ devices.length }}</strong><span>设备总数</span></div>
<div><strong class="online">{{ onlineCount }}</strong><span>在线设备</span></div>
<div><strong class="fault">{{ faultCount }}</strong><span>故障设备</span></div>
<div class="filters">
<el-input v-model="keyword" clearable :prefix-icon="Search" placeholder="搜索名称或设备 ID" />
<el-select v-model="deviceKind" clearable placeholder="全部类型">
<el-option v-for="item in deviceKinds" :key="item.value"
:label="item.label" :value="item.value" />
</el-select>
</div>
</section>
<el-table v-loading="loading" :data="filteredDevices" height="calc(100vh - 285px)"
empty-text="当前机器人尚未上报设备">
<el-table-column label="设备" min-width="220">
<template #default="{ row }">
<div class="device-name">
<span class="device-icon"><el-icon><Cpu /></el-icon></span>
<div><b>{{ row.deviceName || row.deviceId }}</b><small>{{ row.deviceId }}</small></div>
</div>
2025-08-21 14:11:58 +08:00
</template>
</el-table-column>
<el-table-column label="类型" width="130">
<template #default="{ row }">{{ kindLabel(row.deviceKind, row.typeName) }}</template>
</el-table-column>
<el-table-column label="运行状态" width="120" align="center">
<template #default="{ row }">
<el-tag :type="row.onlineStatus === '1' ? 'success' : 'info'" effect="light">
{{ row.onlineStatus === '1' ? '在线' : '离线' }}
</el-tag>
2025-08-21 14:11:58 +08:00
</template>
</el-table-column>
<el-table-column label="健康状态" width="120" align="center">
<template #default="{ row }">
<el-tag :type="isFault(row) ? 'danger' : 'success'" effect="plain">
{{ isFault(row) ? '故障' : '正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="errorMessage" label="状态信息" min-width="180" show-overflow-tooltip>
<template #default="{ row }">{{ row.errorMessage || '运行正常' }}</template>
</el-table-column>
<el-table-column prop="lastSeenTime" label="最后上报" width="180" />
<el-table-column label="操作" width="100" fixed="right" align="center">
<template #default="{ row }">
<el-button v-if="controlRoute(row.deviceKind)" link type="primary"
:disabled="row.onlineStatus !== '1'" @click="openControl(row)">示教</el-button>
<span v-else class="muted">-</span>
2025-08-21 14:11:58 +08:00
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Cpu, Refresh, Search } from '@element-plus/icons-vue'
import { getRobotList, getRobotDevicesByRobotId } from '@/api/inspection/robot'
2025-10-11 17:28:22 +08:00
2025-08-21 14:11:58 +08:00
const router = useRouter()
const robotLoading = ref(false)
const loading = ref(false)
const selectedRobotId = ref('')
const robotOptions = ref([])
const devices = ref([])
const keyword = ref('')
const deviceKind = ref()
const deviceKinds = [
{ value: 1, label: '移动底盘' }, { value: 2, label: '机械臂' },
{ value: 3, label: '电池' }, { value: 4, label: '仿生头' },
{ value: 5, label: '摄像头' }, { value: 6, label: 'CAN 总线' },
{ value: 7, label: '灵巧手' }, { value: 8, label: '夹爪' },
{ value: 9, label: '麦克风' }, { value: 10, label: '电机' },
{ value: 11, label: '电机系统' }, { value: 12, label: '机器人控制器' },
{ value: 13, label: '扬声器' }
]
const kindLabel = (kind, fallback) => deviceKinds.find(item => item.value === kind)?.label || fallback || '未知设备'
const isFault = (device) => device.hasError === '1' || Number(device.healthStatus) === 3
const onlineCount = computed(() => devices.value.filter(item => item.onlineStatus === '1').length)
const faultCount = computed(() => devices.value.filter(isFault).length)
const filteredDevices = computed(() => {
const search = keyword.value.trim().toLowerCase()
return devices.value.filter(item => {
const matchesKind = deviceKind.value == null || item.deviceKind === deviceKind.value
const matchesSearch = !search || `${item.deviceName || ''} ${item.deviceId || ''}`.toLowerCase().includes(search)
return matchesKind && matchesSearch
})
2025-08-21 14:11:58 +08:00
})
const loadRobots = async () => {
robotLoading.value = true
try {
const response = await getRobotList({ pageNum: 1, pageSize: 1000 })
robotOptions.value = (response.rows || []).map(robot => ({
value: robot.robotId,
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
online: robot.connectStatus === '1'
})).filter(item => item.value)
const preferred = robotOptions.value.find(item => item.online) || robotOptions.value[0]
selectedRobotId.value = preferred?.value || ''
} finally {
robotLoading.value = false
}
}
2025-08-21 14:11:58 +08:00
const loadDevices = async () => {
devices.value = []
if (!selectedRobotId.value) return
2025-08-21 14:11:58 +08:00
loading.value = true
try {
const response = await getRobotDevicesByRobotId(selectedRobotId.value, { onlineOnly: false })
devices.value = Array.isArray(response.data) ? response.data : []
} finally {
2025-08-21 14:11:58 +08:00
loading.value = false
}
}
const controlRoute = (kind) => ({
2: 'mechanical_arm', 4: 'head', 5: 'camera', 7: 'hand', 9: 'microphone', 13: 'speaker'
})[kind]
2025-08-21 14:11:58 +08:00
const openControl = (device) => {
2025-08-21 14:11:58 +08:00
router.push({
path: `/device/${controlRoute(device.deviceKind)}/${device.id}`,
query: { robotId: selectedRobotId.value, deviceId: device.deviceId }
2025-08-21 14:11:58 +08:00
})
}
onMounted(async () => {
await loadRobots()
await loadDevices()
})
</script>
2025-08-21 14:11:58 +08:00
<style lang="scss" scoped>
.robot-device-page { min-height: 100%; background: #f5f7fa; }
.page-heading { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 4px 0 20px; }
.page-heading h2 { margin: 0 0 5px; color: #1f2937; font-size: 22px; letter-spacing: 0; }
.page-heading p { margin: 0; color: #8492a6; font-size: 13px; }
.robot-selector { display: flex; align-items: center; gap: 10px; color: #606266; font-size: 13px; }
.robot-selector .el-select { width: 330px; }
.status-strip { display: flex; align-items: center; gap: 34px; margin-bottom: 14px; padding: 14px 18px; border: 1px solid #e4e7ed; border-radius: 6px; background: #fff; }
.status-strip > div:not(.filters) { display: grid; min-width: 74px; }
.status-strip strong { color: #303133; font-size: 24px; line-height: 28px; }
.status-strip strong.online { color: #16a34a; }
.status-strip strong.fault { color: #dc2626; }
.status-strip span { color: #909399; font-size: 12px; }
.filters { display: flex; flex: 1; justify-content: flex-end; gap: 10px; }
.filters .el-input { width: 240px; }
.filters .el-select { width: 150px; }
.device-name { display: flex; align-items: center; gap: 12px; }
.device-icon { display: grid; width: 34px; height: 34px; place-items: center; border-radius: 6px; background: #ecf5ff; color: #409eff; }
.device-name div { display: grid; min-width: 0; }
.device-name b { color: #303133; font-weight: 600; }
.device-name small { overflow: hidden; color: #909399; text-overflow: ellipsis; }
.muted { color: #c0c4cc; }
@media (max-width: 900px) {
.page-heading, .status-strip { align-items: stretch; flex-direction: column; }
.robot-selector, .filters { justify-content: flex-start; flex-wrap: wrap; }
.robot-selector .el-select, .filters .el-input { width: 100%; }
2025-08-21 14:11:58 +08:00
}
</style>