CMVR-IOT-UI/src/views/device/register/index.vue
lixiaolong 45b190ff02 feat(flow): 优化机器人设备绑定功能
- 将terminalId替换为robotId以统一机器人标识
- 实现机器人设备动态加载和选择功能
- 添加设备类型过滤和状态显示
- 重构设备注册页面为机器人设备管理界面
- 优化API调用参数传递方式
- 添加设备自动匹配和锁定机制
2026-08-05 09:27:55 +08:00

185 lines
8.1 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 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>