255 lines
14 KiB
Vue
255 lines
14 KiB
Vue
<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>
|