CMVR-IOT-UI/src/views/inspection/robot/index.vue

255 lines
14 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-page">
<el-form v-show="showSearch" :model="queryParams" inline class="query-form">
<el-form-item label="机器人ID">
<el-input v-model="queryParams.robotId" placeholder="永久身份ID" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="queryParams.robotName" placeholder="机器人名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="连接状态">
<el-select v-model="queryParams.connectStatus" placeholder="全部" clearable style="width: 120px">
<el-option label="在线" value="1" />
<el-option label="离线" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增机器人</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="robotList" row-key="id">
<el-table-column label="机器人" min-width="190">
<template #default="{ row }">
<div class="robot-name">{{ row.robotName || '未命名机器人' }}</div>
<div class="secondary">{{ row.robotCode || '未设置业务编号' }}</div>
</template>
</el-table-column>
<el-table-column label="永久机器人ID" prop="robotId" min-width="210" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.robotId" class="identity-text">{{ row.robotId }}</span>
<el-tag v-else type="info" effect="plain">旧数据</el-tag>
</template>
</el-table-column>
<el-table-column label="连接" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.connectStatus === '1' ? 'success' : 'info'" effect="light">
{{ row.connectStatus === '1' ? '在线' : '等待接入' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="当前端点" min-width="175">
<template #default="{ row }">
<span v-if="row.ipAddress && row.port">{{ row.ipAddress }}:{{ row.port }}</span>
<span v-else class="secondary">连接后自动获取</span>
</template>
</el-table-column>
<el-table-column label="设备" width="150" align="center">
<template #default="{ row }">
<el-button v-if="row.robotId" link type="primary" @click="openDevices(row)">
{{ row.onlineDeviceCount || 0 }}/{{ row.deviceTotal || 0 }} 在线
</el-button>
<span v-else>-</span>
<el-tag v-if="row.faultDeviceCount" type="danger" size="small" class="fault-tag">
{{ row.faultDeviceCount }} 故障
</el-tag>
</template>
</el-table-column>
<el-table-column label="最后心跳" prop="lastHeartbeatTime" width="175">
<template #default="{ row }">{{ row.lastHeartbeatTime || '-' }}</template>
</el-table-column>
<el-table-column label="类型" prop="robotType" width="120">
<template #default="{ row }">{{ robotTypeLabel(row.robotType) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" icon="View" :disabled="!row.robotId" @click="openDevices(row)">设备</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleArchive(row)">归档</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize" @pagination="getList" />
<el-dialog v-model="formOpen" :title="form.id ? '编辑机器人' : '登记机器人'" width="620px" append-to-body>
<el-alert v-if="!form.id" title="登记永久机器人ID后IP、端口和设备将在QUIC连接时自动同步。" type="info" show-icon :closable="false" />
<el-form ref="robotRef" :model="form" :rules="rules" label-width="110px" class="robot-form">
<el-form-item label="机器人ID" prop="robotId">
<el-input v-model="form.robotId" :disabled="Boolean(form.id)" maxlength="128" placeholder="与边缘端 robot_id 完全一致">
<template #prefix><el-icon><Lock /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item label="机器人名称" prop="robotName">
<el-input v-model="form.robotName" placeholder="请输入机器人名称" />
</el-form-item>
<el-form-item label="业务编号" prop="robotCode">
<el-input v-model="form.robotCode" placeholder="用于业务展示,可后续修改" />
</el-form-item>
<el-form-item label="机器人型号">
<el-input v-model="form.robotModel" placeholder="选填" />
</el-form-item>
<el-form-item label="机器人类型" prop="robotType">
<el-select v-model="form.robotType" placeholder="请选择" clearable style="width: 100%">
<el-option v-for="type in robot_type" :key="type.value" :label="type.label" :value="type.value" />
</el-select>
</el-form-item>
<el-form-item label="当前地图">
<el-select v-model="form.currentMapId" placeholder="可稍后绑定" clearable style="width: 100%">
<el-option v-for="map in mapList" :key="map.value" :label="map.label" :value="map.value" />
</el-select>
</el-form-item>
<template v-if="form.id">
<el-divider content-position="left">连接信息</el-divider>
<el-form-item label="连接状态">
<el-tag :type="form.connectStatus === '1' ? 'success' : 'info'">{{ form.connectStatus === '1' ? '在线' : '等待接入' }}</el-tag>
</el-form-item>
<el-form-item label="当前端点"><span>{{ endpointText(form) }}</span></el-form-item>
<el-form-item label="边缘节点"><span>{{ form.quicNodeId || '-' }}</span></el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="formOpen = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm">保存</el-button>
</template>
</el-dialog>
<el-drawer v-model="deviceOpen" size="min(960px, 92vw)" destroy-on-close>
<template #header>
<div>
<div class="drawer-title">{{ activeRobot.robotName || activeRobot.robotId }}</div>
<div class="secondary">{{ activeRobot.robotId }} · {{ endpointText(activeRobot) }}</div>
</div>
</template>
<div class="device-toolbar">
<el-input v-model="deviceKeyword" placeholder="搜索设备ID或类型" clearable prefix-icon="Search" />
<el-select v-model="deviceOnline" placeholder="全部状态" clearable>
<el-option label="在线" value="1" /><el-option label="离线" value="0" />
</el-select>
<el-button icon="Refresh" :loading="deviceLoading" @click="loadDevices">刷新</el-button>
</div>
<el-table v-loading="deviceLoading" :data="filteredDevices" empty-text="机器人尚未上报设备">
<el-table-column label="设备" min-width="180">
<template #default="{ row }"><strong>{{ row.deviceName || row.deviceId }}</strong><div class="secondary">{{ row.deviceId }}</div></template>
</el-table-column>
<el-table-column label="类型" width="120"><template #default="{ row }">{{ deviceKindLabel(row.deviceKind) }}</template></el-table-column>
<el-table-column label="实现" prop="typeName" min-width="150" show-overflow-tooltip />
<el-table-column label="运行状态" width="110"><template #default="{ row }">{{ managerStateLabel(row.managerState) }}</template></el-table-column>
<el-table-column label="健康" width="100"><template #default="{ row }"><el-tag :type="healthTag(row)">{{ healthLabel(row) }}</el-tag></template></el-table-column>
<el-table-column label="在线" width="90"><template #default="{ row }"><el-tag :type="row.onlineStatus === '1' ? 'success' : 'info'">{{ row.onlineStatus === '1' ? '在线' : '离线' }}</el-tag></template></el-table-column>
<el-table-column label="最后发现" prop="lastSeenTime" width="175" />
<el-table-column label="错误" prop="errorMessage" min-width="180" show-overflow-tooltip />
</el-table>
</el-drawer>
</div>
</template>
<script setup name="InspectionRobot">
import { computed, getCurrentInstance, onBeforeUnmount, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { addRobot, deleteRobot, getRobotDevices, getRobotList, updateRobot } from '@/api/inspection/robot'
import { getMapList } from '@/api/inspection/map'
const { proxy } = getCurrentInstance()
const { robot_type } = proxy.useDict('robot_type')
const robotRef = ref()
const loading = ref(false)
const submitting = ref(false)
const showSearch = ref(true)
const total = ref(0)
const robotList = ref([])
const mapList = ref([])
const formOpen = ref(false)
const deviceOpen = ref(false)
const deviceLoading = ref(false)
const devices = ref([])
const deviceKeyword = ref('')
const deviceOnline = ref('')
const activeRobot = ref({})
let refreshTimer
const queryParams = reactive({ pageNum: 1, pageSize: 10, robotId: '', robotName: '', connectStatus: '' })
const emptyForm = () => ({ id: '', robotId: '', robotName: '', robotCode: '', robotModel: '', robotType: '', currentMapId: '' })
const form = reactive(emptyForm())
const rules = {
robotId: [{ required: true, message: '请输入永久机器人ID', trigger: 'blur' }],
robotName: [{ required: true, message: '请输入机器人名称', trigger: 'blur' }],
robotCode: [{ required: true, message: '请输入业务编号', trigger: 'blur' }],
robotType: [{ required: true, message: '请选择机器人类型', trigger: 'change' }]
}
const filteredDevices = computed(() => devices.value.filter(item => {
const keyword = deviceKeyword.value.trim().toLowerCase()
const text = `${item.deviceId || ''} ${item.deviceName || ''} ${item.typeName || ''}`.toLowerCase()
return (!keyword || text.includes(keyword)) && (!deviceOnline.value || item.onlineStatus === deviceOnline.value)
}))
async function getList() {
loading.value = true
try { const res = await getRobotList(queryParams); robotList.value = res.rows || []; total.value = res.total || 0 } finally { loading.value = false }
}
function handleQuery() { queryParams.pageNum = 1; getList() }
function resetQuery() { Object.assign(queryParams, { pageNum: 1, robotId: '', robotName: '', connectStatus: '' }); getList() }
function handleAdd() { Object.assign(form, emptyForm()); formOpen.value = true }
function handleUpdate(row) { Object.assign(form, emptyForm(), row); formOpen.value = true }
function submitForm() {
robotRef.value.validate(async valid => {
if (!valid) return
submitting.value = true
try {
if (form.id) await updateRobot(form); else await addRobot(form)
ElMessage.success(form.id ? '机器人信息已更新' : '机器人已登记等待QUIC接入')
formOpen.value = false
getList()
} finally { submitting.value = false }
})
}
async function handleArchive(row) {
await ElMessageBox.confirm(`归档后机器人ID“${row.robotId || row.robotName}”不可再次使用,是否继续?`, '归档机器人', { type: 'warning' })
await deleteRobot(row.id); ElMessage.success('机器人已归档'); getList()
}
async function openDevices(row) {
activeRobot.value = row; deviceOpen.value = true; deviceKeyword.value = ''; deviceOnline.value = ''; await loadDevices()
clearInterval(refreshTimer); refreshTimer = setInterval(() => { if (deviceOpen.value) loadDevices(false) }, 5000)
}
async function loadDevices(showLoading = true) {
if (!activeRobot.value.id) return
if (showLoading) deviceLoading.value = true
try { const res = await getRobotDevices(activeRobot.value.id); devices.value = res.data || [] } finally { deviceLoading.value = false }
}
function endpointText(row) { return row.ipAddress && row.port ? `${row.ipAddress}:${row.port}` : '等待连接后自动获取' }
function robotTypeLabel(value) { return robot_type.value?.find(item => String(item.value) === String(value))?.label || value || '-' }
const kindNames = { 1: 'AGV', 2: '机械臂', 3: '电池', 4: '仿生头', 5: '摄像头', 6: 'CAN总线', 7: '灵巧手', 8: '夹爪', 9: '麦克风', 10: '电机', 11: '电机系统', 12: '机器人', 13: '扬声器' }
const stateNames = { 1: '停用', 2: '初始化', 3: '已注册', 4: '就绪', 5: '运行中', 6: '已停止', 7: '错误' }
function deviceKindLabel(value) { return kindNames[value] || `未知(${value ?? '-'})` }
function managerStateLabel(value) { return stateNames[value] || '-' }
function healthLabel(row) { if (row.hasError === '1') return '故障'; return ({ 1: '健康', 2: '降级', 3: '故障' })[row.healthStatus] || '未知' }
function healthTag(row) { return row.hasError === '1' || row.healthStatus === 3 ? 'danger' : row.healthStatus === 2 ? 'warning' : row.healthStatus === 1 ? 'success' : 'info' }
onBeforeUnmount(() => clearInterval(refreshTimer))
getList()
getMapList({ pageNum: 1, pageSize: 10000 }).then(res => { mapList.value = (res.rows || []).map(item => ({ label: item.mapName, value: item.id })) })
</script>
<style scoped lang="scss">
.robot-page { min-width: 0; }
.query-form :deep(.el-input) { width: 190px; }
.robot-name, .drawer-title { font-weight: 600; color: var(--el-text-color-primary); }
.drawer-title { font-size: 16px; margin-bottom: 4px; }
.secondary { color: var(--el-text-color-secondary); font-size: 12px; line-height: 20px; }
.identity-text { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
.fault-tag { margin-left: 6px; }
.robot-form { margin-top: 22px; }
.device-toolbar { display: grid; grid-template-columns: minmax(220px, 1fr) 150px auto; gap: 10px; margin-bottom: 16px; }
@media (max-width: 720px) { .device-toolbar { grid-template-columns: 1fr; } }
</style>