fastbee/yack-web/src/views/iot/device/running-status.vue

1575 lines
82 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 nowrap">
<el-input v-model="deviceInfo.imei" size="mini" placeholder="请输入IMEI" style="flex: 1; min-width: 160px" @keyup.enter.native="saveImeiIccid('imei')" />
<el-popover v-model="imeiPopoverVisible" 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 (回车下发)" @keyup.enter.native="handleEnterSend('imei')" />
<el-button slot="reference" size="mini" type="default">编辑指令</el-button>
</el-popover>
<el-button type="primary" size="mini" @click="sendAt('imei')" :loading="imeiLoading" title="默认指令:AT+IMEI\r\n">从设备读取</el-button>
</div>
</el-descriptions-item>
<!-- ICCID 区域(同理) -->
<el-descriptions-item :labelStyle="statusColor">
<template slot="label">
<i class="el-icon-postcard"></i>
ICCID
</template>
<div class="cell-flex nowrap">
<el-input v-model="deviceInfo.iccid" size="mini" placeholder="请输入ICCID" style="flex: 1; min-width: 160px" @keyup.enter.native="saveImeiIccid('iccid')" />
<el-popover v-model="iccidPopoverVisible" 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 (回车下发)" @keyup.enter.native="handleEnterSend('iccid')" />
<el-button slot="reference" size="mini" type="default">编辑指令</el-button>
</el-popover>
<el-button type="primary" size="mini" @click="sendAt('iccid')" :loading="iccidLoading" title="默认指令:AT+ICCID\r\n">从设备读取</el-button>
</div>
</el-descriptions-item>
<!-- LBS 获取经纬度(AT+LBS,回复由后端解析写入 latitude/longitude) -->
<el-descriptions-item :labelStyle="statusColor">
<template slot="label">
<i class="el-icon-location-outline"></i>
LBS获取经纬度
</template>
<div class="cell-flex nowrap">
<el-input v-model="lbsInput" size="mini" placeholder="经度,纬度(设备回复后自动刷新,可手动修改后回车保存)" style="flex: 1; min-width: 160px" @keyup.enter.native="saveLbsCoords" />
<el-popover v-model="lbsPopoverVisible" placement="bottom" width="360" trigger="click" popper-class="atcmd-popover">
<div class="atcmd-title">编辑下发指令,回车即可发送</div>
<el-input v-model="lbsCmd" size="mini" placeholder="例如:AT+LBS\\r\\n 或 Hex:41 54 2b 4c 42 53 0d 0a (回车下发)" @keyup.enter.native="handleEnterSend('lbs')" />
<el-button slot="reference" size="mini" type="default">编辑指令</el-button>
</el-popover>
<el-button type="primary" size="mini" @click="sendAt('lbs')" :loading="lbsLoading" title="默认指令:AT+LBS\r\n(ASCII 转 Hex 为 41542B4C42530D0A)">
从设备读取
</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>
<!-- 便捷指令:一键下发,仅发送、不回显、不保存 -->
<div class="quick-command-panel">
<div class="quick-command-title">
<i class="el-icon-s-promotion"></i>
便捷指令
<span class="quick-command-tip">点击按钮一键下发指令</span>
</div>
<div class="quick-command-list">
<el-button
v-for="item in quickCommands"
:key="item.key"
class="quick-command-btn"
:type="item.danger ? 'danger' : 'primary'"
size="small"
:plain="!item.danger"
:loading="item.loading"
:title="'默认指令:' + item.cmd"
@click="sendQuickCommand(item)"
>
<i :class="item.icon"></i>
{{ item.name }}
</el-button>
</div>
<div class="quick-command-custom">
<span class="quick-command-custom-label">自定义指令</span>
<el-input
v-model="quickCustomCmd"
size="small"
placeholder="如:AT+CSQ\\r\\n 或 Hex:41 54 2b 43 53 51 0d 0a"
style="flex: 1"
clearable
@keyup.enter.native="sendQuickCustom"
/>
<el-button type="primary" size="small" :loading="quickCustomLoading" @click="sendQuickCustom">发送</el-button>
</div>
</div>
<!---设备状态(影子模式,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.syncLbsInputFromDevice();
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',
lbsCmd: 'AT+LBS\\r\\n',
lbsInput: '',
imeiLoading: false,
iccidLoading: false,
lbsLoading: false,
saveLoadingImei: false,
saveLoadingIccid: false,
// 便捷指令:一键下发,仅发送不回显、不保存
quickCommands: [
{
key: 'reset',
name: '重启设备',
cmd: 'AT+RESET\\r\\n',
icon: 'el-icon-refresh',
danger: true,
loading: false,
},
],
quickCustomCmd: '',
quickCustomLoading: false,
imeiPopoverVisible: false, // 控制IMEI指令编辑弹窗
iccidPopoverVisible: false, // 控制ICCID指令编辑弹窗
lbsPopoverVisible: 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: {
// 新增方法:处理弹窗内回车下发指令
handleEnterSend(type) {
// 关闭对应的弹窗
if (type === 'imei') {
this.imeiPopoverVisible = false;
} else if (type === 'iccid') {
this.iccidPopoverVisible = false;
} else if (type === 'lbs') {
this.lbsPopoverVisible = false;
}
// 调用原有的 sendAt 方法下发指令
this.sendAt(type);
},
async refreshDeviceBaseInfo() {
if (!this.deviceInfo || !this.deviceInfo.deviceId) return;
const res = await getDevice(this.deviceInfo.deviceId);
if (res && res.code === 200 && res.data) {
// 只刷新这些字段,避免影响运行态数据/使用 $set 确保响应式更新
this.$set(this.deviceInfo, 'imei', res.data.imei);
this.$set(this.deviceInfo, 'iccid', res.data.iccid);
this.$set(this.deviceInfo, 'latitude', res.data.latitude);
this.$set(this.deviceInfo, 'longitude', res.data.longitude);
this.syncLbsInputFromDevice();
}
},
syncLbsInputFromDevice() {
const lat = this.deviceInfo.latitude;
const lng = this.deviceInfo.longitude;
if (lat != null && lat !== '' && lng != null && lng !== '') {
this.lbsInput = `${lng},${lat}`;
} else {
this.lbsInput = '';
}
},
async sendAt(type) {
const cmd = type === 'imei' ? this.imeiCmd : type === 'iccid' ? this.iccidCmd : type === 'lbs' ? this.lbsCmd : '';
const loadingSetter = type === 'imei'
? (v) => (this.imeiLoading = v)
: type === 'iccid'
? (v) => (this.iccidLoading = v)
: type === 'lbs'
? (v) => (this.lbsLoading = v)
: null;
// 读取型指令:下发后轮询刷新设备信息,等待回显
await this.sendRawCommand(cmd, { loadingSetter, refreshAfter: true });
},
// 便捷指令:一键下发,仅发送、不回显、不保存
async sendQuickCommand(item) {
if (!item || !item.cmd) return;
await this.sendRawCommand(item.cmd, {
loadingSetter: (v) => (item.loading = v),
refreshAfter: false,
});
},
// 便捷指令:自定义指令发送(仅下发,不保存)
async sendQuickCustom() {
const cmd = String(this.quickCustomCmd || '').trim();
if (!cmd) {
this.$message({ type: 'warning', message: '请输入要下发的指令' });
return;
}
await this.sendRawCommand(cmd, {
loadingSetter: (v) => (this.quickCustomLoading = v),
refreshAfter: false,
});
},
async sendRawCommand(cmd, { loadingSetter = null, refreshAfter = true } = {}) {
try {
if (!cmd) {
this.$message({ type: 'warning', message: '指令不能为空' });
return;
}
if (loadingSetter) loadingSetter(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(trimmed);
const res = await messagePost({
message: hexMessage,
serialNumber: this.deviceInfo.serialNumber,
topicName: '/function/get',
dataType: 'hex',
deferIfOffline: true,
});
if (res && res.deferred) {
this.$message({ type: 'warning', message: res.msg || '设备离线,已加入上线后自动下发队列' });
} else {
this.$message({ type: 'success', message: '已下发指令,等待设备回复解析…' });
}
// 读取型指令:后端收到回复后会自动解析并写入数据库,这里做轻量轮询刷新(3秒、8秒各一次)
if (refreshAfter) {
setTimeout(() => this.refreshDeviceBaseInfo(), 3000);
setTimeout(() => this.refreshDeviceBaseInfo(), 8000);
}
} catch (e) {
this.$message({ type: 'error', message: e && e.message ? e.message : '指令下发失败' });
} finally {
if (loadingSetter) loadingSetter(false);
}
},
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;
}
},
async saveLbsCoords() {
try {
if (!this.deviceInfo || !this.deviceInfo.deviceId) return;
const raw = String(this.lbsInput || '').trim();
if (!raw) {
this.$message({ type: 'warning', message: '请输入 经度,纬度' });
return;
}
const parts = raw
.split(',')
.map((s) => s.trim())
.filter(Boolean);
if (parts.length !== 2) {
this.$message({ type: 'warning', message: '格式应为:经度,纬度' });
return;
}
const lng = parseFloat(parts[0]);
const lat = parseFloat(parts[1]);
if (Number.isNaN(lat) || Number.isNaN(lng)) {
this.$message({ type: 'warning', message: '经度、纬度必须为数字' });
return;
}
const res = await updateDevice({
deviceId: this.deviceInfo.deviceId,
serialNumber: this.deviceInfo.serialNumber,
longitude: lng,
latitude: lat,
});
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 : '保存失败' });
}
},
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%;
}
/* 便捷指令:一键下发,仅发送不回显、不保存 */
.quick-command-panel {
background-color: #f5f7fa;
border: 1px solid #ebeef5;
border-radius: 15px;
padding: 16px 20px;
margin-bottom: 50px;
}
.quick-command-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.quick-command-title i {
font-size: 16px;
color: #409eff;
}
.quick-command-tip {
font-size: 12px;
font-weight: 400;
color: #909399;
}
.quick-command-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 12px;
}
.quick-command-btn i {
margin-right: 4px;
}
.quick-command-custom {
display: flex;
align-items: center;
gap: 10px;
}
.quick-command-custom-label {
font-size: 12px;
color: #909399;
white-space: nowrap;
}
/* 重写滑动块样式 */
.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;
}
/* IMEI/ICCID样式 */
.cell-flex {
display: flex;
align-items: center;
gap: 10px;
}
.cell-flex.nowrap {
flex-wrap: nowrap;
}
@media (max-width: 768px) {
.cell-flex.nowrap .el-input {
min-width: 120px;
}
}
</style>