- 将terminalId替换为robotId以统一机器人标识 - 实现机器人设备动态加载和选择功能 - 添加设备类型过滤和状态显示 - 重构设备注册页面为机器人设备管理界面 - 优化API调用参数传递方式 - 添加设备自动匹配和锁定机制
185 lines
8.1 KiB
Vue
185 lines
8.1 KiB
Vue
<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>
|
||
</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>
|
||
</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>
|
||
</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'
|
||
|
||
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
|
||
})
|
||
})
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
const loadDevices = async () => {
|
||
devices.value = []
|
||
if (!selectedRobotId.value) return
|
||
loading.value = true
|
||
try {
|
||
const response = await getRobotDevicesByRobotId(selectedRobotId.value, { onlineOnly: false })
|
||
devices.value = Array.isArray(response.data) ? response.data : []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const controlRoute = (kind) => ({
|
||
2: 'mechanical_arm', 4: 'head', 5: 'camera', 7: 'hand', 9: 'microphone', 13: 'speaker'
|
||
})[kind]
|
||
|
||
const openControl = (device) => {
|
||
router.push({
|
||
path: `/device/${controlRoute(device.deviceKind)}/${device.id}`,
|
||
query: { robotId: selectedRobotId.value, deviceId: device.deviceId }
|
||
})
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await loadRobots()
|
||
await loadDevices()
|
||
})
|
||
</script>
|
||
|
||
<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%; }
|
||
}
|
||
</style>
|