fastbee/yack-web/src/views/iot/device/running-status.vue
2026-03-20 08:11:55 +08:00

1335 lines
72 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="running-status">
<el-row :gutter="120">
<el-col :xs="24" :sm="24" :md="24" :lg="14" :xl="10" style="margin-bottom: 50px">
<el-descriptions :column="1" border style="margin-bottom: 50px">
<!-- 设备模式-->
<el-descriptions-item :labelStyle="statusColor">
<template slot="label">
<i class="el-icon-menu"></i>
{{ $t('device.running-status.866086-0') }}
</template>
<el-link :underline="false" style="line-height: 28px; font-size: 16px; padding-right: 10px">{{ title }}</el-link>
</el-descriptions-item>
<!-- 设备升级-->
<el-descriptions-item :labelStyle="statusColor">
<template slot="label">
<svg-icon icon-class="ota" />
{{ $t('device.running-status.866086-1') }}
</template>
<el-link :underline="false" style="line-height: 28px; font-size: 16px; padding-right: 10px">Version {{ deviceInfo.firmwareVersion }}</el-link>
<el-button type="success" size="mini" style="float: right" @click="getLatestFirmware(deviceInfo.deviceId)" :disabled="deviceInfo.status != 3">{{ $t('device.running-status.866086-2') }}</el-button>
</el-descriptions-item>
<!-- IMEI / ICCID(蜂窝信息) -->
<el-descriptions-item :labelStyle="statusColor">
<template slot="label">
<i class="el-icon-postcard"></i>
IMEI
</template>
<div class="cell-flex">
<el-input v-model="deviceInfo.imei" size="mini" placeholder="请输入IMEI" style="max-width: 260px;" @keyup.enter.native="saveImeiIccid('imei')" />
<el-popover placement="bottom" width="360" trigger="click" popper-class="atcmd-popover">
<div class="atcmd-title">编辑下发指令</div>
<el-input v-model="imeiCmd" size="mini" placeholder="例如:AT+IMEI\\r\\n" />
<el-button slot="reference" size="mini" icon="el-icon-edit" title="编辑指令"></el-button>
</el-popover>
<el-button type="primary" size="mini" :disabled="deviceInfo.status != 3" @click="sendAt('imei')" :loading="imeiLoading" title="默认指令:AT+IMEI\r\n">从设备读取</el-button>
</div>
</el-descriptions-item>
<el-descriptions-item :labelStyle="statusColor">
<template slot="label">
<i class="el-icon-postcard"></i>
ICCID
</template>
<div class="cell-flex">
<el-input v-model="deviceInfo.iccid" size="mini" placeholder="请输入ICCID" style="max-width: 260px;" @keyup.enter.native="saveImeiIccid('iccid')"/>
<el-popover placement="bottom" width="360" trigger="click" popper-class="atcmd-popover">
<div class="atcmd-title">编辑下发指令</div>
<el-input v-model="iccidCmd" size="mini" placeholder="例如:AT+ICCID\\r\\n" />
<el-button slot="reference" size="mini" icon="el-icon-edit" title="编辑指令"></el-button>
</el-popover>
<el-button type="primary" size="mini" :disabled="deviceInfo.status != 3" @click="sendAt('iccid')" :loading="iccidLoading" title="默认指令:AT+ICCID\r\n">从设备读取</el-button>
</div>
</el-descriptions-item>
<!-- 设备物模型-->
<el-descriptions-item v-for="(item, index) in deviceInfo.thingsModels" :key="index" :labelStyle="statusColor">
<template slot="label">
<i class="el-icon-open"></i>
{{ item.name }}
</template>
<div v-if="item.datatype.type == 'bool'">
<el-switch
v-model="item.shadow"
@change="mqttPublish(deviceInfo, item)"
active-text=""
inactive-text=""
active-value="1"
inactive-value="0"
style="min-width: 100px"
:disabled="shadowUnEnable || item.isReadonly == 1"
/>
</div>
<div v-if="item.datatype.type == 'enum'">
<div v-if="item.datatype.showWay && item.datatype.showWay == 'button'">
<el-button
style="margin: 5px"
size="mini"
@click="enumButtonClick(deviceInfo, item, subItem.value)"
v-for="subItem in item.datatype.enumList"
:key="subItem.value"
:disabled="shadowUnEnable || item.isReadonly == 1"
:class="{ 'is-active-btn': subItem.value === item.shadow }"
>
{{ subItem.text }}
</el-button>
</div>
<el-select v-else v-model="item.shadow" :placeholder="$t('device.running-status.866086-3')" @change="mqttPublish(deviceInfo, item)" :disabled="shadowUnEnable || item.isReadonly == 1">
<el-option v-for="subItem in item.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
</el-select>
</div>
<div v-if="item.datatype.type == 'string'">
<el-input
v-model="item.shadow"
:placeholder="item.datatype.unit ? $t('device.running-status.866086-5', [item.datatype.unit]) : $t('device.running-status.866086-4')"
:disabled="shadowUnEnable || item.isReadonly == 1"
>
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, item)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && item.isReadonly == 0"
></el-button>
</el-input>
</div>
<div v-if="item.datatype.type == 'decimal'">
<div style="width: 80%; float: left">
<el-slider
v-model="item.shadow"
:min="item.datatype.min"
:max="item.datatype.max"
:step="item.datatype.step"
:format-tooltip="(x) => x + ' ' + item.datatype.unit"
:disabled="shadowUnEnable || item.isReadonly == 1"
></el-slider>
</div>
<div style="width: 20%; float: left">
<el-button
icon="el-icon-s-promotion"
type="info"
@click="mqttPublish(deviceInfo, item)"
style="font-size: 16px; padding: 1px 8px; margin: 2px 0 0 5px; border-radius: 3px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && item.isReadonly == 0"
></el-button>
</div>
</div>
<div v-if="item.datatype.type == 'integer'">
<div style="width: 80%; float: left">
<el-slider
v-model="item.shadow"
:min="item.datatype.min"
:max="item.datatype.max"
:step="item.datatype.step"
:format-tooltip="(x) => x + ' ' + item.datatype.unit"
:disabled="shadowUnEnable || item.isReadonly == 1"
></el-slider>
</div>
<div style="width: 20%; float: left">
<el-button
icon="el-icon-s-promotion"
type="info"
@click="mqttPublish(deviceInfo, item)"
style="font-size: 16px; padding: 1px 8px; margin: 4px 0 0 10px; border-radius: 3px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && item.isReadonly == 0"
></el-button>
</div>
</div>
<div v-if="item.datatype.type == 'object'">
<el-descriptions :column="1" size="mini" border>
<el-descriptions-item v-for="(param, index) in item.datatype.params" :key="index" :label="param.name">
<div v-if="param.datatype.type == 'bool'">
<el-switch
v-model="param.shadow"
@change="mqttPublish(deviceInfo, param)"
active-text=""
inactive-text=""
active-value="1"
inactive-value="0"
style="min-width: 100px"
:disabled="shadowUnEnable || param.isReadonly == 1"
/>
</div>
<div v-if="param.datatype.type == 'enum'">
<div v-if="param.datatype.showWay && param.datatype.showWay == 'button'">
<el-button
style="margin: 5px"
size="mini"
@click="enumButtonClick(deviceInfo, param, subItem.value)"
v-for="subItem in param.datatype.enumList"
:key="subItem.value"
:disabled="shadowUnEnable || param.isReadonly == 1"
:class="{ 'is-active-btn': subItem.value === param.shadow }"
>
{{ subItem.text }}
</el-button>
</div>
<el-select
size="small"
v-else
v-model="param.shadow"
:placeholder="$t('device.running-status.866086-3')"
@change="mqttPublish(deviceInfo, param)"
:disabled="shadowUnEnable || param.isReadonly == 1"
>
<el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
</el-select>
</div>
<div v-if="param.datatype.type == 'string'">
<el-input v-model="param.shadow" :placeholder="$t('device.running-status.866086-4')" :disabled="shadowUnEnable || param.isReadonly == 1">
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, param)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && param.isReadonly == 0"
></el-button>
</el-input>
</div>
<div v-if="param.datatype.type == 'decimal'">
<el-input v-model="param.shadow" type="number" :placeholder="$t('device.running-status.866086-7')" :disabled="shadowUnEnable || param.isReadonly == 1">
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, param)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && param.isReadonly == 0"
></el-button>
</el-input>
</div>
<div v-if="param.datatype.type == 'integer'">
<el-input v-model="param.shadow" type="integer" :placeholder="$t('device.running-status.866086-8')" :disabled="shadowUnEnable || param.isReadonly == 1">
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, param)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && param.isReadonly == 0"
></el-button>
</el-input>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="item.datatype.type == 'array'">
<el-descriptions :column="1" size="mini" border v-if="item.datatype.arrayType != 'object'">
<el-descriptions-item v-for="(model, index) in item.datatype.arrayModel" :key="index" :label="item.name + (index + 1)">
<div v-if="item.datatype.arrayType == 'string'">
<el-input
:placeholder="$t('device.running-status.866086-4')"
size="mini"
v-model="model.shadow"
:disabled="shadowUnEnable || item.isReadonly == 1"
@input="arrayItemChange($event, item)"
>
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, model)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable || item.isReadonly == 0"
></el-button>
</el-input>
</div>
<div v-if="item.datatype.arrayType == 'decimal'">
<el-input
type="number"
:placeholder="$t('device.running-status.866086-7')"
size="mini"
v-model="model.shadow"
:disabled="shadowUnEnable || item.isReadonly == 1"
@input="arrayItemChange($event, item)"
>
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, model)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable || item.isReadonly == 0"
></el-button>
</el-input>
</div>
<div v-if="item.datatype.arrayType == 'integer'">
<el-input
type="integer"
:placeholder="$t('device.running-status.866086-8')"
size="mini"
v-model="model.shadow"
:disabled="shadowUnEnable || item.isReadonly == 1"
@input="arrayItemChange($event, item)"
>
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, model)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable || item.isReadonly == 0"
></el-button>
</el-input>
</div>
</el-descriptions-item>
</el-descriptions>
<el-collapse v-if="item.datatype.arrayType == 'object'">
<el-collapse-item v-for="(arrayParam, index) in item.datatype.arrayParams" :key="index">
<template slot="title">
<span style="color: #666">
<i class="el-icon-tickets"></i>
{{ item.name + (index + 1) }}
</span>
</template>
<el-descriptions :column="1" size="mini" border>
<el-descriptions-item v-for="(param, index) in arrayParam" :key="index" :label="param.name">
<div v-if="param.datatype.type == 'bool'">
<el-switch
v-model="param.shadow"
@change="mqttPublish(deviceInfo, param)"
active-text=""
inactive-text=""
active-value="1"
inactive-value="0"
style="min-width: 100px"
:disabled="shadowUnEnable || param.isReadonly == 1"
/>
</div>
<div v-if="param.datatype.type == 'enum'">
<div v-if="param.datatype.showWay && param.datatype.showWay == 'button'">
<el-button
style="margin: 5px"
size="mini"
@click="enumButtonClick(deviceInfo, param, subItem.value)"
v-for="subItem in param.datatype.enumList"
:key="subItem.value"
:disabled="shadowUnEnable || param.isReadonly == 1"
:class="{ 'is-active-btn': subItem.value === param.shadow }"
>
{{ subItem.text }}
</el-button>
</div>
<el-select
v-else
v-model="param.shadow"
:placeholder="$t('device.running-status.866086-3')"
size="small"
@change="mqttPublish(deviceInfo, param)"
:disabled="shadowUnEnable || param.isReadonly == 1"
>
<el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
</el-select>
</div>
<div v-if="param.datatype.type == 'string'">
<el-input v-model="param.shadow" :placeholder="$t('device.running-status.866086-4')" :disabled="shadowUnEnable || param.isReadonly == 1">
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, param)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && param.isReadonly == 0"
></el-button>
</el-input>
</div>
<div v-if="param.datatype.type == 'decimal'">
<el-input v-model="param.shadow" type="number" :placeholder="$t('device.running-status.866086-7')" :disabled="shadowUnEnable || param.isReadonly == 1">
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, param)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && param.isReadonly == 0"
></el-button>
</el-input>
</div>
<div v-if="param.datatype.type == 'integer'">
<el-input v-model="param.shadow" type="integer" :placeholder="$t('device.running-status.866086-8')" :disabled="shadowUnEnable || param.isReadonly == 1">
<el-button
slot="append"
icon="el-icon-s-promotion"
@click="mqttPublish(deviceInfo, param)"
style="font-size: 20px"
:title="$t('device.running-status.866086-6')"
v-if="!shadowUnEnable && param.isReadonly == 0"
></el-button>
</el-input>
</div>
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</div>
</el-descriptions-item>
</el-descriptions>
<!---设备状态(影子模式,value值不会更新)-->
<el-descriptions :column="1" border size="mini" v-if="deviceInfo.isShadow == 1 && deviceInfo.status != 3">
<template slot="title">
<span style="font-size: 14px; color: #606266">{{ $t('device.running-status.866086-9') }}</span>
</template>
<!-- 设备物模型-->
<el-descriptions-item v-for="(item, index) in deviceInfo.thingsModels" :key="index">
<template slot="label">
<i class="el-icon-open"></i>
{{ item.name }}
</template>
<div v-if="item.datatype.type == 'bool'">
<el-switch v-model="item.value" @change="mqttPublish(deviceInfo, item)" active-text="" inactive-text="" active-value="1" inactive-value="0" style="min-width: 100px" disabled />
</div>
<div v-if="item.datatype.type == 'enum'">
<div v-if="item.datatype.showWay && item.datatype.showWay == 'button'">
<el-button style="margin: 5px" size="mini" disabled v-for="subItem in item.datatype.enumList" :key="subItem.value">{{ subItem.text }}</el-button>
</div>
<el-select v-else v-model="item.value" :placeholder="$t('device.running-status.866086-3')" @change="mqttPublish(deviceInfo, item)" disabled size="mini">
<el-option v-for="subItem in item.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
</el-select>
</div>
<div v-if="item.datatype.type == 'string'">
<el-input v-model="item.value" :placeholder="$t('device.running-status.866086-4')" disabled size="mini"></el-input>
</div>
<div v-if="item.datatype.type == 'decimal'">
<el-input v-model="item.value" type="number" :placeholder="$t('device.running-status.866086-7')" disabled size="mini"></el-input>
</div>
<div v-if="item.datatype.type == 'integer'">
<el-input v-model="item.value" type="integer" :placeholder="$t('device.running-status.866086-8')" disabled size="mini"></el-input>
</div>
<div v-if="item.datatype.type == 'object'">
<el-descriptions :column="1" size="mini" border>
<el-descriptions-item v-for="(param, index) in item.datatype.params" :key="index" :label="param.name">
<div v-if="param.datatype.type == 'bool'">
<el-switch
v-model="param.value"
size="mini"
@change="mqttPublish(deviceInfo, param)"
active-text=""
inactive-text=""
active-value="1"
inactive-value="0"
style="min-width: 100px"
disabled
/>
</div>
<div v-if="param.datatype.type == 'enum'">
<el-select v-model="param.value" :placeholder="$t('device.running-status.866086-3')" @change="mqttPublish(deviceInfo, param)" disabled size="mini">
<el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
</el-select>
</div>
<div v-if="param.datatype.type == 'string'">
<el-input v-model="param.value" :placeholder="$t('device.running-status.866086-4')" disabled size="mini"></el-input>
</div>
<div v-if="param.datatype.type == 'decimal'">
<el-input v-model="param.value" type="number" :placeholder="$t('device.running-status.866086-7')" disabled size="mini"></el-input>
</div>
<div v-if="param.datatype.type == 'integer'">
<el-input v-model="param.value" type="integer" :placeholder="$t('device.running-status.866086-8')" disabled size="mini"></el-input>
</div>
</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="item.datatype.type == 'array'">
<el-descriptions :column="1" size="mini" border v-if="item.datatype.arrayType != 'object'">
<el-descriptions-item v-for="(model, index) in item.datatype.arrayModel" :key="index" :label="item.name + (index + 1)">
<div v-if="item.datatype.arrayType == 'string'">
<el-input v-model="model.value" :placeholder="$t('device.running-status.866086-4')" size="mini" disabled></el-input>
</div>
<div v-if="item.datatype.arrayType == 'decimal'">
<el-input v-model="model.value" type="number" :placeholder="$t('device.running-status.866086-7')" size="mini" disabled></el-input>
</div>
<div v-if="item.datatype.arrayType == 'integer'">
<el-input v-model="model.value" type="integer" :placeholder="$t('device.running-status.866086-8')" size="mini" disabled></el-input>
</div>
</el-descriptions-item>
</el-descriptions>
<el-collapse v-if="item.datatype.arrayType == 'object'">
<el-collapse-item v-for="(arrayParam, index) in item.datatype.arrayParams" :key="index">
<template slot="title">
<span style="color: #666">
<i class="el-icon-tickets"></i>
{{ item.name + (index + 1) }}
</span>
</template>
<el-descriptions :column="1" size="mini" border>
<el-descriptions-item v-for="(param, index) in arrayParam" :key="index" :label="param.name">
<div v-if="param.datatype.type == 'bool'">
<el-switch
v-model="param.value"
@change="mqttPublish(deviceInfo, param)"
active-text=""
inactive-text=""
active-value="1"
inactive-value="0"
style="min-width: 100px"
disabled
/>
</div>
<div v-if="param.datatype.type == 'enum'">
<el-select v-model="param.value" :placeholder="$t('device.running-status.866086-3')" @change="mqttPublish(deviceInfo, param)" disabled size="mini">
<el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
</el-select>
</div>
<div v-if="param.datatype.type == 'string'">
<el-input v-model="param.value" :placeholder="$t('device.running-status.866086-4')" disabled size="mini"></el-input>
</div>
<div v-if="param.datatype.type == 'decimal'">
<el-input v-model="param.value" type="number" :placeholder="$t('device.running-status.866086-7')" disabled size="mini"></el-input>
</div>
<div v-if="param.datatype.type == 'integer'">
<el-input v-model="param.value" type="integer" :placeholder="$t('device.running-status.866086-8')" disabled size="mini"></el-input>
</div>
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
</el-collapse>
</div>
</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
<!-- 设备监测图表(全宽响应式矩阵布局,尽量一屏看完,减少长滚动)-->
<div class="monitor-grid-wrap" v-if="deviceInfo.chartList.length > 0">
<div class="monitor-grid">
<el-card v-for="(item, index) in deviceInfo.chartList" :key="index" shadow="hover" class="monitor-card">
<div ref="map" class="monitor-chart"></div>
</el-card>
</div>
</div>
<!-- 添加或修改产品固件对话框 -->
<el-dialog :title="$t('device.running-status.866086-10')" :visible.sync="openFirmware" width="600px" append-to-body>
<div v-if="firmware == null" style="text-align: center; font-size: 16px">
<i class="el-icon-success" style="color: #67c23a"></i>
{{ $t('device.running-status.866086-11') }}
</div>
<el-descriptions :column="1" border size="large" v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version" :labelStyle="{ width: '150px', 'font-weight': 'bold' }">
<template slot="title">
<el-link icon="el-icon-success" type="success" :underline="false">{{ $t('device.running-status.866086-12') }}</el-link>
</template>
<el-descriptions-item :label="$t('device.running-status.866086-13')">{{ firmware.firmwareName }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-4')">{{ firmware.productName }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.device-edit.148398-12')">Version {{ firmware.version }}</el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-16')">
<el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{ getDownloadUrl(firmware.filePath) }}</el-link>
</el-descriptions-item>
<el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark }}</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button type="success" @click="otaUpgrade" v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version">{{ $t('device.running-status.866086-18') }}</el-button>
<el-button @click="cancel">{{ $t('cancel') }}</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getLatestFirmware } from '@/api/iot/firmware';
import { serviceInvoke } from '@/api/iot/runstatus';
import { messagePost } from '@/api/iot/mqttTest';
import { getDevice, updateDevice } from '@/api/iot/device';
export default {
name: 'running-status',
props: {
device: {
type: Object,
default: null,
},
},
watch: {
// 获取到父组件传递的device后,刷新列表
device: {
handler(newVal) {
if (newVal && newVal.deviceId != 0) {
if (newVal && newVal.deviceId != 0) {
this.deviceInfo = newVal;
this.updateDeviceStatus(this.deviceInfo);
this.$nextTick(function () {
this.MonitorChart();
});
// 添加message事件监听器
// this.mqttCallback();
}
}
},
},
},
data() {
return {
// 控制模块标题
title: '设备控制 ',
// 未启用设备影子
shadowUnEnable: false,
// 控制项标题背景
statusColor: {
background: '#67C23A',
color: '#fff',
minWidth: '100px',
},
// 最新固件信息
firmware: {},
// 打开固件对话框
openFirmware: false,
// 遮罩层
loading: true,
// 设备信息
deviceInfo: {
boolList: [],
enumList: [],
stringList: [],
integerList: [],
decimalList: [],
arrayList: [],
thingsModels: [],
chartList: [],
},
// 监测图表
monitorChart: [
{
chart: {},
data: {
id: '',
name: '',
value: '',
},
},
],
resizeObserver: null,
remoteCommand: {},
imeiCmd: 'AT+IMEI\\r\\n',
iccidCmd: 'AT+ICCID\\r\\n',
imeiLoading: false,
iccidLoading: false,
saveLoadingImei: false,
saveLoadingIccid: false,
};
},
mounted() {
const { deviceId, serialNumber } = this.device;
if (deviceId) {
this.initDataStatus();
this.initData();
}
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
methods: {
async refreshDeviceBaseInfo() {
if (!this.deviceInfo || !this.deviceInfo.deviceId) return;
const res = await getDevice(this.deviceInfo.deviceId);
if (res && res.code === 200 && res.data) {
// 只刷新这两个字段,避免影响运行态数据
this.deviceInfo.imei = res.data.imei;
this.deviceInfo.iccid = res.data.iccid;
}
},
async sendAt(type) {
try {
const cmd = type === 'imei' ? this.imeiCmd : this.iccidCmd;
if (!cmd) {
this.$message({ type: 'warning', message: '指令不能为空' });
return;
}
if (type === 'imei') this.imeiLoading = true;
if (type === 'iccid') this.iccidLoading = true;
// FUNCTION_GET 下发链路最终会走 ByteUtils.convertToHexByteArray(command)。
// 因此这里必须传“十六进制字节串”,而不是原始 ASCII。
// 同时兼容两种输入:
// 1) 用户输入 AT+IMEI\\r\\n 这类带转义的 ASCII:会先还原成真实 \r\n,再转 hex(不带空格)
// 2) 用户直接输入 41 54 ... 或 41542b...:会清理空格后直接使用(后端也会再次清理)
const trimmed = String(cmd).trim();
const hexMessage = this.isHexLike(trimmed)
? this.normalizeHexString(trimmed)
: this.asciiCommandToHex(this.normalizeAtEscapes(trimmed));
await messagePost({
message: hexMessage,
serialNumber: this.deviceInfo.serialNumber,
topicName: '/function/get',
dataType: 'hex',
});
this.$message({ type: 'success', message: '已下发指令,等待设备回复解析…' });
// 后端收到回复后会自动解析并写入数据库,这里做一次轻量轮询刷新
setTimeout(() => this.refreshDeviceBaseInfo(), 1200);
setTimeout(() => this.refreshDeviceBaseInfo(), 2500);
} catch (e) {
this.$message({ type: 'error', message: e && e.message ? e.message : '指令下发失败' });
} finally {
this.imeiLoading = false;
this.iccidLoading = false;
}
},
normalizeAtEscapes(str) {
// 输入框里用户通常会键入:AT+IMEI\\r\\n
// 但你当前的设备/链路期望的不是“真实 CR/LF(0d0a)”,而是把转义串当作占位符:
// 把 \\r\\n 期望转成 "/r/n"(即字符 '/','r','/','n',十六进制为 2f 72 2f 6e)。
if (!str) return '';
return String(str)
.replace(/\\r\\n/g, '/r/n')
.replace(/\\r/g, '/r')
.replace(/\\n/g, '/n');
},
isHexLike(hexStr) {
if (!hexStr) return false;
const cleaned = this.normalizeHexString(hexStr);
if (!cleaned) return false;
return /^[0-9a-fA-F]+$/.test(cleaned);
},
normalizeHexString(hexStr) {
if (!hexStr) return '';
return String(hexStr)
.trim()
.replace(/\s+/g, '')
.replace(/^0x/i, '');
},
asciiCommandToHex(asciiStr) {
// 把字符串按字节(低8位)转成纯16进制(无空格)
if (!asciiStr) return '';
let hex = '';
for (let i = 0; i < asciiStr.length; i++) {
const c = asciiStr.charCodeAt(i) & 0xff;
let hexByte = c.toString(16).toUpperCase();
if (hexByte.length === 1) hexByte = `0${hexByte}`;
hex += hexByte;
}
return hex;
},
async saveImeiIccid(type) {
try {
if (!this.deviceInfo || !this.deviceInfo.deviceId) return;
if (type === 'imei') this.saveLoadingImei = true;
if (type === 'iccid') this.saveLoadingIccid = true;
const payload = {
deviceId: this.deviceInfo.deviceId,
serialNumber: this.deviceInfo.serialNumber,
};
if (type === 'imei') payload.imei = this.deviceInfo.imei;
if (type === 'iccid') payload.iccid = this.deviceInfo.iccid;
const res = await updateDevice(payload);
if (res && res.code === 200) {
this.$message({ type: 'success', message: '保存成功' });
await this.refreshDeviceBaseInfo();
} else {
this.$message({ type: 'error', message: (res && res.msg) || '保存失败' });
}
} catch (e) {
this.$message({ type: 'error', message: e && e.message ? e.message : '保存失败' });
} finally {
this.saveLoadingImei = false;
this.saveLoadingIccid = false;
}
},
initResizeObserver() {
if (this.resizeObserver) return;
if (typeof window.ResizeObserver !== 'function') return;
const el = this.$el && this.$el.querySelector ? this.$el.querySelector('.monitor-grid-wrap') : null;
if (!el) return;
this.resizeObserver = new ResizeObserver(() => {
this.handleResize();
});
this.resizeObserver.observe(el);
},
handleResize() {
this.$nextTick(() => {
if (!this.monitorChart || !this.monitorChart.length) return;
this.monitorChart.forEach((item) => {
if (item && item.chart && typeof item.chart.resize === 'function') {
item.chart.resize();
}
});
});
},
scheduleChartResize() {
// grid / card 可能会在首次渲染后继续“撑开”,多次兜底 resize,确保不缩在左上角
this.handleResize();
setTimeout(() => this.handleResize(), 50);
setTimeout(() => this.handleResize(), 200);
setTimeout(() => this.handleResize(), 500);
},
initData() {
// 监听值的实时更新
this.$busEvent.$on('updateData', (params) => {
this.updateParam(params);
});
},
initDataStatus() {
// 监听值的实时更新
this.$busEvent.$on('updateStatus', (status) => {
this.updateStatus(status);
});
},
updateStatus(status) {
let { serialNumber, productId, data } = status;
if (data) {
if (this.deviceInfo.serialNumber == status.serialNumber) {
this.deviceInfo.status = data.status;
this.deviceInfo.isShadow = data.isShadow;
this.deviceInfo.rssi = data.rssi;
this.updateDeviceStatus(this.deviceInfo);
}
}
},
//更新参数值
updateParam(params) {
let { serialNumber, productId, data } = params;
if (data) {
data = data.message;
data.forEach((msg) => {
this.deviceInfo.thingsModels.some((item, index) => {
if (msg.id === item.id) {
const variable = this.deviceInfo.thingsModels[index];
// 普通类型(小数/整数/字符串/布尔/枚举)
if (item.datatype.type == 'decimal' || item.datatype.type == 'integer') {
variable.shadow = Number(msg.value);
} else {
variable.shadow = msg.value;
}
this.$set(this.deviceInfo.thingsModels, index, variable);
return true;
}
});
let isComplete = false;
for (let j = 0; j < data.length; j++) {
// 设备状态
for (let k = 0; k < this.deviceInfo.thingsModels.length && !isComplete; k++) {
if (this.deviceInfo.thingsModels[k].datatype.type == 'object') {
// 对象类型
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.params.length; n++) {
if (this.deviceInfo.thingsModels[k].datatype.params[n].id == data[j].id) {
this.deviceInfo.thingsModels[k].datatype.params[n].shadow = data[j].value;
isComplete = true;
break;
}
}
} else if (this.deviceInfo.thingsModels[k].datatype.type == 'array') {
// 数组类型
if (this.deviceInfo.thingsModels[k].datatype.arrayType == 'object') {
// 1.对象类型数组,id为数组中一个元素,例如:array_01_gateway_temperature
if (String(data[j].id).indexOf('array_') == 0) {
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == data[j].id) {
this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = data[j].value;
isComplete = true;
break;
}
}
if (isComplete) {
break;
}
}
} else {
// 2.对象类型数组,例如:gateway_temperature,消息ID添加前缀后匹配
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
let index = n > 9 ? String(n) : '0' + k;
let prefix = 'array_' + index + '_';
if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == prefix + data[j].id) {
this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = data[j].value;
}
}
}
}
} else {
// 整数、小数和字符串类型数组
for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayModel.length; n++) {
if (this.deviceInfo.thingsModels[k].datatype.arrayModel[n].id == data[j].id) {
this.deviceInfo.thingsModels[k].datatype.arrayModel[n].shadow = data[j].value;
break;
}
}
}
}
}
// 图表数据
for (let k = 0; k < this.deviceInfo.chartList.length; k++) {
if (this.deviceInfo.chartList[k].id.indexOf('array_') == 0) {
// 数组类型匹配,例如:array_00_gateway_temperature
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (data[j].id == this.monitorChart[m].data.id) {
// 更新数据对象
this.monitorChart[m].data.value = data[j].value;
// 确保数值是数字类型
const numValue = Number(data[j].value) || 0;
// 构建图表数据
const chartData = [
{
value: numValue,
name: this.monitorChart[m].data.name,
},
];
// 更新图表配置,包括detail部分
this.monitorChart[m].chart.setOption({
series: [
{
data: chartData,
detail: {
formatter: '{value}' + ' ' + (this.deviceInfo.chartList[k].datatype.unit || ''),
},
},
],
}, false); // false表示不合并,完全替换
// 强制刷新图表
this.$nextTick(() => {
if (this.monitorChart[m] && this.monitorChart[m].chart) {
this.monitorChart[m].chart.resize();
}
});
break;
}
}
}
} else {
// 普通类型匹配
if (this.deviceInfo.chartList[k].id == data[j].id) {
this.deviceInfo.chartList[k].shadow = data[j].value;
// 更新图表
for (let m = 0; m < this.monitorChart.length; m++) {
if (data[j].id == this.monitorChart[m].data.id) {
// 更新数据对象
this.monitorChart[m].data.value = data[j].value;
// 确保数值是数字类型
const numValue = Number(data[j].value) || 0;
// 构建图表数据
const chartData = [
{
value: numValue,
name: this.monitorChart[m].data.name,
},
];
// 更新图表配置,包括detail部分
this.monitorChart[m].chart.setOption({
series: [
{
data: chartData,
detail: {
formatter: '{value}' + ' ' + (this.deviceInfo.chartList[k].datatype.unit || ''),
},
},
],
}, false); // false表示不合并,完全替换
// 强制刷新图表
this.$nextTick(() => {
if (this.monitorChart[m] && this.monitorChart[m].chart) {
this.monitorChart[m].chart.resize();
}
});
break;
}
}
}
}
if (isComplete) {
break;
}
}
}
// }
});
}
},
//发送指令
mqttPublish(device, model) {
const command = {};
command[model.id] = model.shadow;
const data = {
serialNumber: device.serialNumber,
productId: device.productId,
remoteCommand: command,
identifier: model.id,
modelName: model.name,
isShadow: device.status != 3,
type: model.type,
};
serviceInvoke(data).then((response) => {
if (response.code === 200) {
this.$message({
type: 'success',
message: this.$t('device.running-status.866086-25'),
});
}
});
},
/**
* Mqtt发布消息
* @device 设备
* @model 物模型(id/name/type/name/isReadonly/value/shadow),type 类型(1=属性,2=功能,3=OTA升级,4=实时监测)
* */
// mqttPublish(device, model) {
// let topic = "";
// let message = ""
// if (model.type == 1) {
// if (device.status == 3) {
// // 属性,在线模式
// topic = "/" + device.productId + "/" + device.serialNumber + "/property-online/get";
// } else if (device.isShadow) {
// // 属性,离线模式
// topic = "/" + device.productId + "/" + device.serialNumber + "/property-offline/post";
// }
// message = '[{"id":"' + model.id + '","value":"' + model.shadow + '"}]';
// } else if (model.type == 2) {
// if (device.status == 3) {
// // 功能,在线模式
// topic = "/" + device.productId + "/" + device.serialNumber + "/function-online/get";
//
// } else if (device.isShadow) {
// // 功能,离线模式
// topic = "/" + device.productId + "/" + device.serialNumber + "/function-offline/post";
// }
// message = '[{"id":"' + model.id + '","value":"' + model.shadow + '"}]';
// } else if (model.type == 3) {
// // OTA升级
// topic = "/" + device.productId + "/" + device.serialNumber + "/ota/get";
// message = '{"version":' + this.firmware.version + ',"downloadUrl":"' + this.getDownloadUrl(this.firmware.filePath) + '"}';
// } else {
// return;
// }
// if (topic != "") {
// // 发布
// this.$mqttTool.publish(topic, message, model.name).then(res => {
// this.$modal.notifySuccess(res);
// }).catch(res => {
// this.$modal.notifyError(res);
// });
// }
// },
/** 枚举类型按钮单击 */
enumButtonClick(device, model, value) {
model.shadow = value;
this.mqttPublish(device, model);
},
/** 更新设备状态 */
updateDeviceStatus(device) {
if (device.status == 3) {
this.statusColor.background = '#12d09f';
this.title = this.$t('device.running-status.866086-26');
this.shadowUnEnable = false;
} else {
if (device.isShadow == 1) {
this.statusColor.background = '#409EFF';
this.title = this.$t('device.running-status.866086-27');
} else {
this.statusColor.background = '#909399';
this.title = this.$t('device.running-status.866086-28');
this.shadowUnEnable = true;
}
}
this.$emit('statusEvent', this.deviceInfo.status);
},
/** 物模型数组元素值改变事件 */
arrayItemChange(value, thingsModel) {
let shadow = '';
for (let i = 0; i < thingsModel.datatype.arrayCount; i++) {
shadow += thingsModel.datatype.arrayModel[i].shadow + ',';
}
shadow = shadow.substring(0, shadow.length - 1);
thingsModel.shadow = shadow;
},
/** 物模型中数组值改变事件 */
arrayInputChange(value, thingsModel) {
let arrayModels = value.split(',');
if (arrayModels.length != thingsModel.datatype.arrayCount) {
this.$modal.alertWarning(this.$t('device.running-status.866086-29') + thingsModel.datatype.arrayCount + this.$t('device.running-status.866086-30'));
} else {
for (let i = 0; i < thingsModel.datatype.arrayCount; i++) {
thingsModel.datatype.arrayModel[i].shadow = arrayModels[i];
}
}
},
/**用户是否拥有分享设备权限*/
// hasShrarePerm(permission) {
// if (this.deviceInfo.isOwner == 0) {
// // 分享设备权限
// if (this.deviceInfo.userPerms.indexOf(permission) == -1) {
// return false;
// }
// }
// return true;
// },
/** 设备升级 */
otaUpgrade() {
// OTA升级
let topic = '/' + this.deviceInfo.productId + '/' + this.deviceInfo.serialNumber + '/ota/get';
let message = '{"version":' + this.firmware.version + ',"downloadUrl":"' + this.getDownloadUrl(this.firmware.filePath) + '"}';
// 发布
this.$mqttTool
.publish(topic, message, this.$t('device.running-status.866086-31'))
.then((res) => {
this.$modal.notifySuccess(res);
})
.catch((res) => {
this.$modal.notifyError(res);
});
this.openFirmware = false;
},
/** 获取最新固件 */
getLatestFirmware(deviceId) {
getLatestFirmware(deviceId).then((response) => {
this.firmware = response.data;
this.openFirmware = true;
});
},
// 取消按钮
cancel() {
this.openFirmware = false;
},
// 获取下载路径前缀
getDownloadUrl(path) {
return window.location.origin + process.env.VUE_APP_BASE_API + path;
},
/**图表展示*/
MonitorChart() {
// 释放旧图表实例,避免重复 init 导致内存泄漏
if (this.monitorChart && this.monitorChart.length) {
this.monitorChart.forEach((item) => {
if (item && item.chart && typeof item.chart.dispose === 'function') {
item.chart.dispose();
}
});
}
for (let i = 0; i < this.deviceInfo.chartList.length; i++) {
this.monitorChart[i] = {
chart: this.$echarts.init(this.$refs.map[i]),
data: {
id: this.deviceInfo.chartList[i].id,
name: this.deviceInfo.chartList[i].name,
value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
},
};
var option;
option = {
tooltip: {
formatter: ' {b} <br/> {c}' + this.deviceInfo.chartList[i].datatype.unit,
},
series: [
{
name: this.deviceInfo.chartList[i].datatype.type,
type: 'gauge',
min: this.deviceInfo.chartList[i].datatype.min,
max: this.deviceInfo.chartList[i].datatype.max,
colorBy: 'data',
splitNumber: 10,
// 留一点空间给数值和标题,视觉上更“占满”
radius: '95%',
center: ['50%', '55%'],
// 分割线
splitLine: {
distance: 4,
},
axisLabel: {
fontSize: 10,
distance: 10,
},
// 刻度线
axisTick: {
distance: 4,
},
// 仪表盘轴线
axisLine: {
lineStyle: {
width: 8,
color: [
[0.2, '#409EFF'], // 0~20%
[0.8, '#12d09f'], // 40~60%
[1, '#F56C6C'], // 80~100%
],
opacity: 0.3,
},
},
pointer: {
icon: 'triangle',
length: '60%',
width: 7,
},
progress: {
show: true,
width: 8,
},
detail: {
valueAnimation: true,
formatter: '{value}' + ' ' + this.deviceInfo.chartList[i].datatype.unit,
offsetCenter: [0, '68%'],
fontSize: 22,
},
data: [
{
value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
name: this.deviceInfo.chartList[i].name,
},
],
title: {
offsetCenter: [0, '88%'],
fontSize: 16,
},
},
],
};
option && this.monitorChart[i].chart.setOption(option);
// grid/card 尺寸变化时,强制适配容器尺寸,避免表盘“缩在左上角”
this.monitorChart[i].chart.resize();
}
// 监听容器尺寸变化 + 多次兜底 resize:确保布局稳定后都 resize 到位
this.$nextTick(() => {
this.initResizeObserver();
this.scheduleChartResize();
});
},
},
};
</script>
<style>
.running-status {
padding-left: 20px;
}
.cell-flex {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.atcmd-title {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
}
.monitor-grid-wrap {
background-color: #f5f7fa;
padding: 20px 10px;
border-radius: 15px;
margin-right: 5px;
}
.monitor-grid {
display: grid;
/* 默认:中等屏幕 3 列(避免媒体查询未命中时“没效果”) */
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
}
/* 让宽屏更“和谐”:优先 4 列(内容区一般达不到 1600px,这里下调阈值) */
@media (min-width: 1400px) {
.monitor-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (min-width: 1100px) and (max-width: 1399px) {
.monitor-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (min-width: 800px) and (max-width: 1099px) {
.monitor-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 799px) {
.monitor-grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
.monitor-card {
border-radius: 18px;
}
.monitor-card .el-card__body {
padding: 8px;
}
.monitor-chart {
height: 320px;
width: 100%;
}
/* 重写滑动块样式 */
.el-slider__bar {
height: 18px;
}
.el-slider__runway {
height: 18px;
margin: 5px 0;
}
.el-slider__button {
height: 18px;
width: 18px;
border-radius: 10%;
}
.el-slider__button-wrapper {
top: -9px;
}
.is-active-btn {
color: #1890ff;
border-color: #badeff;
background-color: #e8f4ff;
}
</style>