You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
469 lines
12 KiB
469 lines
12 KiB
<template>
|
|
<div class="title">
|
|
设备监控
|
|
<el-button size="small" :icon="renderIcon('Refresh')" @click="refreshData"
|
|
style="margin-left: 5px;"
|
|
/>
|
|
<el-button size="small" :icon="renderIcon('fa Simplybuilt')" @click="simBootAll"
|
|
style="margin-left: 2px;" link
|
|
v-if="worldModelState.runState.isRunning && worldModelState.runState.isVirtual"
|
|
>全上线
|
|
</el-button>
|
|
<el-input v-model="searchKeyword" size="small" style="width: 240px" placeholder="Search">
|
|
<template #prefix>
|
|
<component :is="renderIcon('element Search')"></component>
|
|
</template>
|
|
</el-input>
|
|
<span class="close" @click="closeMe">
|
|
<component :is="renderIcon('element Close')" />
|
|
</span>
|
|
</div>
|
|
<div class="calc-left-panel">
|
|
<el-empty v-if="!isActivated || errorDescription" :description="errorDescription" style="width:100%;">
|
|
</el-empty>
|
|
<div v-else class="monitor-tool-wrap">
|
|
<div class="infor-row">
|
|
<div class="infor-item">
|
|
总数<span class="num">{{ total }}</span>
|
|
</div>
|
|
<div class="infor-item">
|
|
上线<span class="num num-green">{{ online }}</span>
|
|
</div>
|
|
<div class="infor-item">
|
|
下线<span class="num num-red">{{ offline }}</span>
|
|
</div>
|
|
<div class="infor-item">
|
|
空闲<span class="num num-orange">{{ idle }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="list">
|
|
<div v-for="deviceInfo in deviceList" class="item" :class="selectedId === deviceInfo.id" @mousedown="()=>{ selectedId = deviceInfo.id }">
|
|
<div class="content">
|
|
<div class="row">
|
|
<div class="row-icon">
|
|
<component :is="renderIcon('antd DeploymentUnitOutlined')"></component>
|
|
</div>
|
|
<div class="row-content">
|
|
<div class="data-infor">
|
|
<el-button link style="margin-right: 2px; color:WHITE;" @click="copyDeviceId(deviceInfo)">ID:{{ deviceInfo.id }}</el-button>
|
|
<span :class="deviceInfo.isOnline?'green':'red'">[{{ getDeviceModeDesc(deviceInfo) }}]</span>
|
|
<div class="infor-right">
|
|
<span class="num-red" v-if="deviceInfo.isBlocked">阻挡</span>
|
|
<span class="num-red" v-if="deviceInfo.taskStatus === 'PAUSED'">暂停</span>
|
|
<span class="blue">{{ deviceInfo.logicX + '_' + deviceInfo.logicY + '(' + deviceInfo.direction + ')' }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="data-infor">
|
|
<div class="progress">
|
|
<span :style="{width:getTaskProcessPercent(deviceInfo)+'%'}"></span>
|
|
<div class="text"> {{ getTaskProcessPercent(deviceInfo) }} %</div>
|
|
</div>
|
|
</div>
|
|
<div class="data-infor">
|
|
{{ getDeviceTaskTypeDesc(deviceInfo) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import IWidgets from '../IWidgets.js'
|
|
import { worldModel } from '@/core/manager/WorldModel.js'
|
|
|
|
export default {
|
|
name: 'MonitorView',
|
|
mixins: [IWidgets],
|
|
data() {
|
|
return {
|
|
searchKeyword: '',
|
|
selectedId: '',
|
|
stopSubscribe: [],
|
|
/**
|
|
* 设备列表数据
|
|
* @type {Array<Partial<AgvStatusVo>>}
|
|
*/
|
|
deviceList: []
|
|
}
|
|
},
|
|
mounted() {
|
|
window['MonitorView'] = this
|
|
},
|
|
unmounted() {
|
|
window['MonitorView'] = null
|
|
this.undescribe()
|
|
},
|
|
methods: {
|
|
simBootAll() {
|
|
const viewport = window['viewport']
|
|
for (const deviceInfo of this.deviceList) {
|
|
if (deviceInfo.isOnline) {
|
|
continue
|
|
}
|
|
const view3DObject = Model.find3D(deviceInfo.id)
|
|
if (typeof view3DObject.bootForMonitor === 'function') {
|
|
view3DObject.bootForMonitor()
|
|
}
|
|
}
|
|
},
|
|
copyDeviceId(deviceInfo) {
|
|
navigator.clipboard.writeText(deviceInfo.id).then(() => {
|
|
system.msg('设备ID已复制到剪贴板', 'success')
|
|
}).catch(err => {
|
|
system.msg('无法复制设备ID:', 'error')
|
|
})
|
|
},
|
|
/**
|
|
* 获取任务进度百分比
|
|
*/
|
|
getTaskProcessPercent(deviceInfo) {
|
|
if (deviceInfo == null || !deviceInfo.taskTotalCount || !deviceInfo.taskCompleted) {
|
|
return 0
|
|
}
|
|
return Math.round((deviceInfo.taskCompleted / deviceInfo.taskTotalCount) * 100)
|
|
},
|
|
/**
|
|
* 处理设备存活消息
|
|
* @type {(type: BackendTopicType, topic: string, body: {
|
|
* id: string
|
|
* type: string
|
|
* online: boolean
|
|
* }) => void}
|
|
*/
|
|
onDeviceAliveMessage(type, topic, body) {
|
|
const deviceInfo = _.find(this.deviceList, device => device.id === body.id)
|
|
if (!deviceInfo) {
|
|
// 如果设备信息不存在,则添加新设备
|
|
this.deviceList.push({
|
|
id: body.id,
|
|
type: body.type,
|
|
isOnline: body.online
|
|
})
|
|
|
|
} else {
|
|
// 如果设备信息已存在,则更新在线状态
|
|
deviceInfo.isOnline = body.online
|
|
}
|
|
},
|
|
/**
|
|
* 处理设备状态消息
|
|
* @type {DeviceStatusFn}
|
|
*/
|
|
onDeviceStatusMessage(type, topic, body) {
|
|
const deviceInfo = _.find(this.deviceList, device => device.id === body.id)
|
|
if (!deviceInfo) {
|
|
return
|
|
}
|
|
_.assign(deviceInfo, body)
|
|
},
|
|
async subscribe() {
|
|
await this.refreshData()
|
|
|
|
// 订阅设备状态消息
|
|
this.stopSubscribe.push(
|
|
worldModel.backendMessageReceiver.subscribe('DeviceAlive', this.onDeviceAliveMessage.bind(this))
|
|
)
|
|
this.stopSubscribe.push(
|
|
worldModel.backendMessageReceiver.subscribe('DeviceStatus', this.onDeviceStatusMessage.bind(this))
|
|
)
|
|
},
|
|
async refreshData() {
|
|
this.deviceList = []
|
|
const res = await LCC.queryDeviceInfoList()
|
|
if (!res.success) {
|
|
return
|
|
}
|
|
this.deviceList = res.data
|
|
},
|
|
undescribe() {
|
|
// 取消订阅设备状态消息
|
|
for (const stopFn of this.stopSubscribe) {
|
|
stopFn()
|
|
}
|
|
this.stopSubscribe = []
|
|
},
|
|
getDeviceModeDesc(deviceInfo) {
|
|
if (!deviceInfo.isOnline) {
|
|
return '离线'
|
|
}
|
|
if (!deviceInfo.mode) {
|
|
return '在线'
|
|
}
|
|
switch (deviceInfo.mode) {
|
|
case 'AMR_FREE_MODE':
|
|
return '空闲模式'
|
|
case 'AMR_INIT_MODE':
|
|
return '初始化模式'
|
|
case 'AMR_TASK_MODE':
|
|
return '任务模式'
|
|
case 'AMR_SINGLE_ACTION_MODE':
|
|
return '单动作模式'
|
|
case 'AMR_MANUAL_MODE':
|
|
return '手动模式'
|
|
case 'AMR_HANDSET_MODE':
|
|
return '遥控器模式'
|
|
case 'AMR_CHARGE_MODE':
|
|
return '充电模式'
|
|
case 'AMR_TASK_INTERRUPT_MODE':
|
|
return '任务被中断模式'
|
|
case 'AMR_CUSTOMIZE_MODE':
|
|
return '自定义模式'
|
|
}
|
|
return deviceInfo.mode
|
|
},
|
|
getDeviceTaskTypeDesc(deviceInfo) {
|
|
switch (deviceInfo.bizTaskType) {
|
|
case 'MOVE':
|
|
return '移动 ' + deviceInfo.bizTaskFrom + ' -> ' + deviceInfo.bizTaskTo
|
|
case 'CHARGE':
|
|
return '充电' + deviceInfo.bizTaskFrom + ' -> ' + deviceInfo.bizTaskTo
|
|
case 'CARRY':
|
|
return '搬运 [' + deviceInfo.bizLpn + '] ' + deviceInfo.bizTaskFrom + ' -> ' + deviceInfo.bizTaskTo
|
|
case 'LOAD':
|
|
return '装载 [' + deviceInfo.bizLpn + '] ' + deviceInfo.bizTaskFrom + ' -> ' + deviceInfo.bizTaskTo
|
|
case 'UNLOAD':
|
|
return '卸载 [' + deviceInfo.bizLpn + '] ' + deviceInfo.bizTaskFrom + ' -> ' + deviceInfo.bizTaskTo
|
|
default:
|
|
return '无任务'
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
/*
|
|
total: 0,
|
|
online: 0,
|
|
offline: 0,
|
|
running: 0,
|
|
idle: 0,
|
|
*/
|
|
total() {
|
|
return this.deviceList.length
|
|
},
|
|
online() {
|
|
return this.deviceList.filter(device => device.isOnline).length
|
|
},
|
|
offline() {
|
|
return this.deviceList.filter(device => !device.isOnline).length
|
|
},
|
|
idle() {
|
|
return this.deviceList.filter(device => device.mode === 'AMR_FREE_MODE').length
|
|
},
|
|
worldModelState() {
|
|
return worldModel.state
|
|
}
|
|
},
|
|
watch: {
|
|
errorDescription: {
|
|
handler(newVal) {
|
|
if (newVal) {
|
|
this.undescribe()
|
|
} else {
|
|
this.subscribe()
|
|
}
|
|
},
|
|
immediate: true
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style lang="less">
|
|
.monitor-tool-wrap {
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.infor-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
//background: #cdcccc;
|
|
padding: 3px 0;
|
|
|
|
.infor-item {
|
|
margin: 2px 8px;
|
|
font-size: 12px;
|
|
background: #f3e9c0;
|
|
border-radius: 3px;
|
|
padding: 0 5px;
|
|
}
|
|
|
|
.num {
|
|
display: inline-block;
|
|
padding: 0 3px;
|
|
font-weight: bold;
|
|
color: #333;
|
|
|
|
&:first-child {
|
|
margin-left: 0;
|
|
}
|
|
|
|
&:last-child {
|
|
margin-right: 0;
|
|
}
|
|
|
|
&.num-red {
|
|
color: red;
|
|
}
|
|
|
|
&.num-green {
|
|
color: green;
|
|
}
|
|
|
|
&.num-blue {
|
|
color: blue;
|
|
}
|
|
|
|
&.num-orange {
|
|
color: #d78f0b;
|
|
}
|
|
}
|
|
}
|
|
|
|
.list {
|
|
flex: 1;
|
|
overflow: auto;
|
|
font-size: 12px;
|
|
color: #fff;
|
|
|
|
.item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 2px solid #808080;
|
|
margin: 5px 5px;
|
|
border-radius: 5px;
|
|
background: #747a80;
|
|
|
|
&.selected {
|
|
border-color: #fa9d12;
|
|
}
|
|
|
|
.title {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 3px 10px;
|
|
border-bottom: 1px solid #81888e;
|
|
margin-bottom: 5px;
|
|
|
|
.num {
|
|
font-weight: bold;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.el-icon {
|
|
margin: 0 5px;
|
|
font-size: 15px;
|
|
}
|
|
}
|
|
|
|
.content {
|
|
flex: 1;
|
|
padding: 5px 0;
|
|
|
|
.row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
margin: 0 5px;
|
|
|
|
.row-icon {
|
|
width: 75px;
|
|
height: 75px;
|
|
background: #3e454c;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
.el-icon {
|
|
font-size: 70px;
|
|
color: #fff;
|
|
}
|
|
}
|
|
|
|
.row-content {
|
|
flex: 1;
|
|
margin-left: 5px;
|
|
}
|
|
}
|
|
|
|
.data-infor {
|
|
margin: 3px 0;
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
|
|
.infor-right {
|
|
flex: 1;
|
|
flex-shrink: 0;
|
|
|
|
span {
|
|
&.blue {
|
|
color: #3dc4ff;
|
|
}
|
|
|
|
&.name {
|
|
margin-right: 15px;
|
|
}
|
|
}
|
|
}
|
|
|
|
& > span {
|
|
margin-right: 3px;
|
|
|
|
&.green {
|
|
color: #00ff00;
|
|
}
|
|
|
|
&.red {
|
|
color: red;
|
|
}
|
|
|
|
& > svg {
|
|
position: relative;
|
|
top: 3px;
|
|
}
|
|
}
|
|
|
|
.progress {
|
|
flex: 1;
|
|
min-width: 80px;
|
|
border: 1px solid #dcdcdc;
|
|
height: 20px;
|
|
margin-left: 5px;
|
|
position: relative;
|
|
|
|
span {
|
|
font-size: 12px;
|
|
background: #7ac678;
|
|
display: inline-block;
|
|
width: 0;
|
|
text-align: center;
|
|
height: 18px;
|
|
}
|
|
|
|
.text {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
left: 0;
|
|
top: 0;
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
& > svg {
|
|
margin-right: 5px;
|
|
position: relative;
|
|
top: 1px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
|