fastbee/yack-web/src/views/dataCenter/history.vue

1842 lines
92 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="data-center-history">
<el-card>
<div class="history-device-filter">
<el-form size="mini" :inline="true" label-width="80px" @submit.native.prevent>
<el-form-item :label="$t('dataCenter.history.filterDept')" prop="deptId" class="history-device-filter__item">
<el-cascader
v-model="historyDeviceFilter.deptId"
:options="deptOptions"
:props="deptCascaderProps"
:placeholder="$t('dataCenter.history.filterDeptPlaceholder')"
clearable
filterable
size="mini"
:show-all-levels="false"
class="history-filter-control"
popper-class="history-dept-cascader-dropdown"
@visible-change="onHistoryDeptCascaderVisible"
@change="handleHistoryDeviceFilterChange"
/>
</el-form-item>
<el-form-item :label="$t('dataCenter.history.filterGroup')" class="history-device-filter__item">
<el-select
v-model="historyDeviceFilter.groupId"
class="history-filter-control"
:placeholder="$t('dataCenter.history.filterGroupPlaceholder')"
clearable
size="mini"
@change="handleHistoryDeviceFilterChange"
>
<el-option v-for="group in myGroupList" :key="group.groupId" :label="group.groupName" :value="group.groupId" />
</el-select>
</el-form-item>
<el-form-item class="history-device-filter__item history-device-filter__item--checkbox">
<el-checkbox v-model="historyDeviceFilter.showChild" @change="handleHistoryDeviceFilterChange">
{{ $t('device.index.105953-18') }}
</el-checkbox>
<el-tooltip :content="$t('device.index.105953-19')" placement="top">
<i class="el-icon-question history-device-filter__help" />
</el-tooltip>
</el-form-item>
</el-form>
</div>
<el-tabs v-model="activeTab">
<el-tab-pane :label="$t('dataCenter.history.384934-0')" name="device">
<div class="device-wrap">
<el-form @submit.native.prevent :model="devQueryParams" ref="devQueryForm" size="mini" :inline="true" label-width="68px">
<el-form-item :label="$t('dataCenter.history.384934-1')" prop="deviceId">
<el-select style="width: 100%" v-model="devQueryParams.deviceId" :placeholder="$t('dataCenter.history.384934-2')" filterable @change="handleDevDeviceChange">
<el-option v-for="(item, index) in devDeviceListNonGw" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="devSubDeviceList.length > 0" label="子设备" prop="subDeviceId">
<el-select style="width: 100%" v-model="devQueryParams.subDeviceId" placeholder="请选择子设备" filterable @change="handleDevSubDeviceChange">
<el-option v-for="item in devSubDeviceList" :key="item.subDeviceId" :label="item.subDeviceName + ' (从机' + item.slaveId + ')'" :value="item.subDeviceId"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('dataCenter.history.384934-3')" prop="identifiers">
<el-select style="width: 100%" v-model="devQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" filterable multiple collapse-tags @change="handleDevIdentifierChange">
<el-option label="全部勾选/反选" value="__selectAll__">
<span style="font-weight: bold; color: #409EFF;">{{ devIsAllSelected ? '全部反选' : '全部勾选' }}</span>
</el-option>
<el-option v-for="(item, index) in devIdentifierList" :key="index" :label="item.modelName" :value="item.identifier"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('dataCenter.history.384934-5')" prop="dayDaterange">
<el-date-picker
style="width: 315px"
v-model="devQueryParams.dayDaterange"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator="-"
:start-placeholder="$t('dataCenter.history.384934-6')"
:end-placeholder="$t('dataCenter.history.384934-7')"
:picker-options="pickerOptions"
></el-date-picker>
</el-form-item>
<el-form-item style="float: right; margin-right: 0">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleDevQuery">{{ $t('dataCenter.history.384934-8') }}</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="handleDevResetQuery">{{ $t('dataCenter.history.384934-9') }}</el-button>
<el-button type="success" icon="el-icon-download" size="mini" :disabled="devDatas.length === 0" @click="handleDevExport" v-loading="exportLoading">{{ $t('dataCenter.history.384934-24') }}</el-button>
</el-form-item>
</el-form>
<el-row>
<el-col :span="24">
<el-card v-loading="loading" shadow="never">
<div slot="header" style="display: flex; align-items: center; justify-content: space-between;">
<span>{{ $t('dataCenter.history.384934-10') }}</span>
<el-switch v-model="independentYAxis" active-text="独立Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div>
<div class="el-table--enable-row-hover el-table--medium">
<div v-show="devDatas.length !== 0" ref="devLineChart" :style="{ width: '100%', height: devChartHeight + 'px', background: '#fff' }"></div>
<el-empty v-if="devDatas.length === 0" style="height: 480px" :description="$t('dataCenter.history.384934-12')"></el-empty>
<el-table v-show="devTotal > 0" style="margin-top: 50px" :data="devTableList" :border="false">
<el-table-column :label="$t('dataCenter.history.384934-13')" prop="time" width="200" />
<el-table-column v-for="item in this.devTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
</el-table>
<pagination
style="margin-bottom: 20px"
v-show="devTotal > 0"
:autoScroll="false"
:total="devTotal"
:page.sync="devPageNum"
:limit.sync="devPageSize"
@pagination="getDevTableList"
/>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="网关设备" name="gateway">
<div class="gateway-wrap">
<!-- 统一 4 步:1 选网关 2 选物模型 3 选子设备(可选) 4 选时间 -->
<el-form @submit.native.prevent :model="gwQueryParams" ref="gwQueryForm" size="mini" :inline="true" label-width="90px">
<el-form-item label="网关设备" prop="gwDeviceId">
<el-select style="width: 200px" v-model="gwQueryParams.gwDeviceId" placeholder="请选择网关设备" filterable @change="handleGwDeviceChange">
<el-option v-for="item in gwDeviceList" :key="item.deviceId" :label="item.deviceName" :value="item.deviceId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="物模型" prop="identifiers">
<el-select style="width: 320px" v-model="gwQueryParams.identifiers" placeholder="请选择变量(可多选)" filterable multiple collapse-tags @change="handleGwIdentifierChange">
<el-option label="全部勾选/反选" value="__selectAll__">
<span style="font-weight: bold; color: #409EFF;">{{ gwGwIsAllSelected ? '全部反选' : '全部勾选' }}</span>
</el-option>
<el-option-group label="计算量(网关纯计算,无需选子设备)" v-if="gwVariableListCalcUnique.length">
<el-option v-for="item in gwVariableListCalcUnique" :key="'calc-' + item.identifier" :label="item.modelName + ' (计算)'" :value="item.identifier"></el-option>
</el-option-group>
<el-option-group label="子设备变量" v-if="gwVariableListSubUnique.length">
<el-option v-for="item in gwVariableListSubUnique" :key="'sub-' + item.identifier" :label="item.modelName" :value="item.identifier"></el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item label="子设备" prop="subDeviceIds" v-if="gwShowSubDeviceSelect">
<el-select
style="width: 280px"
v-model="gwSubDeviceSelectValue"
placeholder="不选则全部子设备对比,可多选指定子设备"
filterable
multiple
collapse-tags
clearable
>
<el-option label="全部子设备(多设备对比)" value="__all__"></el-option>
<el-option v-for="item in gwSubDeviceList" :key="item.subDeviceId" :label="item.subDeviceName + ' (从机' + item.slaveId + ')'" :value="item.subDeviceId"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="dayDaterange">
<el-date-picker
style="width: 315px"
v-model="gwQueryParams.dayDaterange"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator="-"
start-placeholder="开始时间"
end-placeholder="结束时间"
:picker-options="pickerOptions"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleGwQuery">查询</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="handleGwReset">重置</el-button>
<el-button type="success" icon="el-icon-download" size="mini" :disabled="!gwHasResult" @click="handleGwExport" v-loading="exportLoading">导出Excel</el-button>
</el-form-item>
</el-form>
<el-card v-loading="loading" shadow="never">
<div slot="header" style="display: flex; align-items: center; justify-content: space-between;">
<span>{{ gwResultTitle }}</span>
<el-switch v-model="independentYAxis" active-text="独立Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div>
<!-- 计算量结果 -->
<template v-if="gwResultMode === 'calc'">
<div v-show="gwCalcDatas.length !== 0" ref="gwCalcLineChart" :style="{ width: '100%', height: gwCalcChartHeight + 'px' }"></div>
<el-empty v-if="gwCalcDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(计算量)和时间范围后查询"></el-empty>
<el-table v-show="gwCalcTotal > 0" style="margin-top: 20px" :data="gwCalcTableList" border>
<el-table-column label="更新时间" prop="time" width="180" />
<el-table-column v-for="item in gwCalcTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
</el-table>
<pagination v-show="gwCalcTotal > 0" :total="gwCalcTotal" :page.sync="gwCalcPageNum" :limit.sync="gwCalcPageSize" @pagination="getGwCalcTableList" />
</template>
<!-- 多设备对比结果 -->
<template v-else-if="gwResultMode === 'compare'">
<div v-show="subCompareDatas.length !== 0" ref="subCompareLineChart" :style="{ width: '100%', height: subCompareChartHeight + 'px' }"></div>
<el-empty v-if="subCompareDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型(子设备变量)和时间范围,子设备可不选(多设备对比)"></el-empty>
<el-table v-show="subCompareTotal > 0" style="margin-top: 20px" :data="subCompareTableList" border>
<el-table-column v-for="col in subCompareTableColumns" :key="col.prop" :label="col.label" :prop="col.prop" :min-width="col.prop === 'time' ? 180 : 100" />
</el-table>
<pagination v-show="subCompareTotal > 0" :total="subCompareTotal" :page.sync="subComparePageNum" :limit.sync="subComparePageSize" @pagination="getSubCompareTableList" />
</template>
<!-- 单子设备历史结果 -->
<template v-else-if="gwResultMode === 'single'">
<div v-show="gwSingleDatas.length !== 0" ref="gwSingleLineChart" :style="{ width: '100%', height: gwSingleChartHeight + 'px' }"></div>
<el-empty v-if="gwSingleDatas.length === 0 && !loading" style="height: 480px" description="请选择网关、物模型、子设备和时间范围后查询"></el-empty>
<el-table v-show="gwSingleTotal > 0" style="margin-top: 20px" :data="gwSingleTableList" border>
<el-table-column label="更新时间" prop="time" width="180" />
<el-table-column v-for="item in gwSingleTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
</el-table>
<pagination v-show="gwSingleTotal > 0" :total="gwSingleTotal" :page.sync="gwSinglePageNum" :limit.sync="gwSinglePageSize" @pagination="getGwSingleTableList" />
</template>
<el-empty v-else style="height: 480px" description="请按顺序选择:1.网关设备 2.物模型 3.子设备(可选) 4.时间范围,然后查询"></el-empty>
</el-card>
</div>
</el-tab-pane>
<el-tab-pane :label="$t('dataCenter.history.384934-14')" name="scene">
<div class="scene-wrap">
<el-form @submit.native.prevent :model="sceneQueryParams" ref="sceneQueryForm" size="mini" :inline="true" label-width="68px">
<el-form-item :label="$t('dataCenter.history.384934-15')" prop="sceneModelId">
<el-select style="width: 100%" v-model="sceneQueryParams.sceneModelId" :placeholder="$t('dataCenter.history.384934-16')" @change="handleSceneModelChange">
<el-option v-for="(item, index) in sceneModelList" :key="index" :label="item.sceneModelName" :value="item.sceneModelId"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('dataCenter.history.384934-17')" prop="sceneModelDeviceId">
<el-select style="width: 100%" v-model="sceneQueryParams.sceneModelDeviceId" :placeholder="$t('dataCenter.history.384934-18')" @change="handleSceneDeviceChange">
<el-option v-for="(item, index) in sceneDeviceList" :key="index" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('dataCenter.history.384934-3')" prop="identifiers">
<el-select style="width: 100%" v-model="sceneQueryParams.identifiers" :placeholder="$t('dataCenter.history.384934-4')" multiple collapse-tags @change="handleSceneIdentifierChange">
<el-option label="全部勾选/反选" value="__selectAll__">
<span style="font-weight: bold; color: #409EFF;">{{ sceneIsAllSelected ? '全部反选' : '全部勾选' }}</span>
</el-option>
<el-option v-for="(item, index) in sceneIdentifierList" :key="index" :label="item.sourceName" :value="item.identifier"></el-option>
</el-select>
</el-form-item>
<template v-if="sceneSearchShow">
<el-form-item :label="$t('dataCenter.history.384934-5')" prop="dayDaterange">
<el-date-picker
style="width: 315px"
v-model="sceneQueryParams.dayDaterange"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetimerange"
range-separator="-"
:start-placeholder="$t('dataCenter.history.384934-6')"
:end-placeholder="$t('dataCenter.history.384934-7')"
:picker-options="pickerOptions"
></el-date-picker>
</el-form-item>
</template>
<el-form-item style="float: right; margin-right: 0">
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleSceneQuery">{{ $t('dataCenter.history.384934-8') }}</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="handleSceneResetQuery">{{ $t('dataCenter.history.384934-9') }}</el-button>
<el-button type="text" @click="handleSceneSearchChange">
{{ sceneSearchShow ? $t('template.index.891112-113') : $t('template.index.891112-112') }}
<i :class="{ 'el-icon-arrow-down': !sceneSearchShow, 'el-icon-arrow-up': sceneSearchShow }"></i>
</el-button>
</el-form-item>
</el-form>
<el-row>
<el-col :span="24">
<el-card v-loading="loading" shadow="never">
<div slot="header" style="display: flex; align-items: center; justify-content: space-between;">
<span>{{ $t('dataCenter.history.384934-10') }}</span>
<el-switch v-model="independentYAxis" active-text="独立Y轴" inactive-text="共享Y轴" @change="handleYAxisModeChange" />
</div>
<div class="el-table--enable-row-hover el-table--medium">
<div v-show="sceneDatas.length !== 0" ref="sceneLineChart" :style="{ width: '100%', height: sceneChartHeight + 'px', background: '#fff' }"></div>
<el-empty v-if="sceneDatas.length === 0" style="height: 480px" :description="$t('dataCenter.history.384934-12')"></el-empty>
<el-table v-show="sceneTotal > 0" style="margin-top: 50px" :data="sceneTableList" :border="false">
<el-table-column :label="$t('dataCenter.history.384934-13')" prop="time" width="200" />
<el-table-column v-for="item in this.sceneTableHeaderTemp" :key="item.value" :label="item.name" :prop="item.value" />
</el-table>
<pagination
style="margin-bottom: 20px"
v-show="sceneTotal > 0"
:autoScroll="false"
:total="sceneTotal"
:page.sync="scenePageNum"
:limit.sync="scenePageSize"
@pagination="getSceneTableList"
/>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</template>
<script>
import moment from 'moment';
import { listDeviceShort, listThingsModel } from '@/api/iot/device.js';
import { listGroup } from '@/api/iot/group';
import { deptsTreeSelect } from '@/api/system/user';
import { listGateway } from '@/api/iot/gateway.js';
import { getSceneModelList, getSceneModelDetail, getSceneModelDataList } from '@/api/scene/list.js';
import {
getDataCenterDeviceHistory,
getDataCenterSceneHistory,
exportDeviceHistory,
getGatewayCalculatedModels,
getDeviceHistoryCalculated,
exportDeviceHistoryCalculated,
getSubDeviceComparativeHistory,
exportSubDeviceComparativeHistory,
} from '@/api/data/center.js';
import { saveAs } from 'file-saver';
import { blobValidate } from '@/utils/ruoyi';
export default {
name: 'dataCenterHistory',
data() {
return {
activeTab: 'device',
/** 与 /system/user/deptTree 返回的 TreeSelect(id/label/children)一致,任意层级可选 */
deptCascaderProps: {
value: 'id',
label: 'label',
children: 'children',
checkStrictly: true,
emitPath: false,
},
/** 设备/网关下拉共用:机构、分组、含下级(与设备管理 shortList 参数一致) */
historyDeviceFilter: {
deptId: null,
showChild: true,
groupId: null,
},
deptOptions: [],
myGroupList: [],
devDeviceList: [], // 设备-设备列表
devSubDeviceList: [], // 网关子设备列表
devIdentifierList: [],
pickerOptions: {
shortcuts: [
{
text: this.$t('dataCenter.history.384934-19'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 2);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-26'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 4);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-27'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 8);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-28'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 12);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-20'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.analysis.384934-25'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 2);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-29'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 3);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-30'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 5);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-21'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-31'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 15);
picker.$emit('pick', [start, end]);
},
},
{
text: this.$t('dataCenter.history.384934-22'),
onClick(picker) {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
picker.$emit('pick', [start, end]);
},
},
],
},
// 查询参数
devQueryParams: {
deviceId: null,
subDeviceId: null,
identifiers: [],
dayDaterange: [new Date(new Date().getTime() - 3600 * 1000 * 2), new Date()],
},
loading: false,
devDatas: [], // 设备列表数据
devTableComTemp: [], // 设备列表table数据
devTableHeaderTemp: [], // 设备列表table数据
devPageNum: 1,
devPageSize: 10,
devTotal: 0,
sceneSearchShow: false, //搜索展开
sceneModelList: [], // 场景列表
sceneDeviceList: [],
sceneIdentifierList: [], // 场景变量
sceneQueryParams: {
sceneModelId: null,
sceneModelDeviceId: null,
identifiers: [],
dayDaterange: [new Date(new Date().getTime() - 3600 * 1000 * 2), new Date()],
},
sceneDatas: [], // 场景列表数据
sceneTableComTemp: [], // 设备列表table数据
sceneTableHeaderTemp: [], // 设备列表table数据
scenePageNum: 1,
scenePageSize: 10,
sceneTotal: 0,
exportLoading: false,
// 网关设备 tab 统一 4 步(物模型支持多选)
gwQueryParams: {
gwDeviceId: null,
identifiers: [],
/** 子设备:null=全部,[id]=单设备,[id1,id2,...]=多设备对比指定 */
subDeviceIds: null,
dayDaterange: [new Date(Date.now() - 3600 * 1000 * 2), new Date()],
},
gwSubDeviceList: [],
gwCalcIdentifierList: [],
subCompareIdentifierList: [],
gwResultMode: '', // 'calc' | 'compare' | 'single'
// 计算量结果(复用原结构)
gwCalcDatas: [],
gwCalcTableHeaderTemp: [],
gwCalcTableComTemp: [],
gwCalcTotal: 0,
gwCalcPageNum: 1,
gwCalcPageSize: 10,
// 多设备对比结果
subCompareDatas: [],
subCompareTableColumns: [],
subCompareTableComTemp: [],
subCompareTotal: 0,
subComparePageNum: 1,
subComparePageSize: 10,
// 单子设备历史结果
gwSingleDatas: [],
gwSingleTableHeaderTemp: [],
gwSingleTableComTemp: [],
gwSingleTotal: 0,
gwSinglePageNum: 1,
gwSinglePageSize: 10,
independentYAxis: true,
devChartHeight: 480,
gwCalcChartHeight: 480,
gwSingleChartHeight: 480,
subCompareChartHeight: 480,
sceneChartHeight: 480,
};
},
beforeDestroy() {
this.teardownHistoryDeptCascaderLabelClick();
},
mounted() {
this.getDeptTree();
this.getGroupList();
this.getDevDeviceList();
this.getSceneModelListDatas();
const { activeName, deviceId, identifier, sceneModelId, sceneModelDeviceId } = this.$route.query;
if (activeName) {
this.activeTab = activeName || 'device';
// 设置默认值
if (activeName === 'device') {
this.devQueryParams.deviceId = Number(deviceId);
this.getDevIdentifierList(Number(deviceId));
this.devQueryParams.identifiers = [identifier];
setTimeout(() => {
this.handleDevQuery();
}, 500);
} else {
this.sceneQueryParams.sceneModelId = Number(sceneModelId);
this.getSceneModelDetailDatas(Number(sceneModelId));
this.sceneQueryParams.sceneModelDeviceId = Number(sceneModelDeviceId);
this.getSceneIdentifierList(Number(sceneModelDeviceId));
this.sceneQueryParams.identifiers = [identifier];
setTimeout(() => {
this.handleSceneQuery();
}, 500);
}
}
},
computed: {
devTableList() {
const start = (this.devPageNum - 1) * this.devPageSize;
const end = start + this.devPageSize;
return this.devTableComTemp.slice(start, end);
},
sceneTableList() {
const start = (this.scenePageNum - 1) * this.scenePageSize;
const end = start + this.scenePageSize;
return this.sceneTableComTemp.slice(start, end);
},
// 判断设备变量是否全部选中
devIsAllSelected() {
if (this.devIdentifierList.length === 0) return false;
const allIdentifiers = this.devIdentifierList.map(item => item.identifier);
return allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.devQueryParams.identifiers.includes(id));
},
// 判断场景变量是否全部选中
sceneIsAllSelected() {
if (this.sceneIdentifierList.length === 0) return false;
const allIdentifiers = this.sceneIdentifierList.map(item => item.identifier);
return allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
},
/** 设备 tab 仅展示非网关设备 */
devDeviceListNonGw() {
return (this.devDeviceList || []).filter((d) => d.deviceType !== 2 && d.deviceType !== 4);
},
/** 网关设备 tab 仅展示网关设备 */
gwDeviceList() {
return (this.devDeviceList || []).filter((d) => d.deviceType === 2);
},
gwCalcTableList() {
const start = (this.gwCalcPageNum - 1) * this.gwCalcPageSize;
return (this.gwCalcTableComTemp || []).slice(start, start + this.gwCalcPageSize);
},
subCompareTableList() {
const start = (this.subComparePageNum - 1) * this.subComparePageSize;
return (this.subCompareTableComTemp || []).slice(start, start + this.subComparePageSize);
},
// 网关统一:计算量变量列表(用于下拉分组)
gwVariableListCalc() {
return this.gwCalcIdentifierList || [];
},
// 计算量按 identifier 去重,避免重复 key
gwVariableListCalcUnique() {
const list = this.gwCalcIdentifierList || [];
const seen = new Set();
return list.filter((i) => {
if (seen.has(i.identifier)) return false;
seen.add(i.identifier);
return true;
});
},
// 网关统一:子设备变量列表
gwVariableListSub() {
return this.subCompareIdentifierList || [];
},
// 子设备变量按 identifier 去重,且排除计算量(跨设备计算量不展示在子设备变量中)
gwVariableListSubUnique() {
const list = this.subCompareIdentifierList || [];
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
const seen = new Set();
return list.filter((i) => {
if (seen.has(i.identifier) || calcSet.has(i.identifier)) return false;
seen.add(i.identifier);
return true;
});
},
// 当前选中的物模型是否全部为计算量(计算量时不展示子设备选择)
gwSelectedIsCalculated() {
const ids = this.gwQueryParams.identifiers || [];
if (ids.length === 0) return false;
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
return ids.every((id) => calcSet.has(id));
},
// 选中的计算量 identifier 列表
gwSelectedCalcIdentifiers() {
const ids = this.gwQueryParams.identifiers || [];
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
return ids.filter((id) => calcSet.has(id));
},
// 选中的子设备变量 identifier 列表(排除同时属于计算量的,避免仅选计算量时报混合选择)
gwSelectedSubIdentifiers() {
const ids = this.gwQueryParams.identifiers || [];
const subSet = new Set((this.subCompareIdentifierList || []).map((i) => i.identifier));
const calcSet = new Set((this.gwCalcIdentifierList || []).map((i) => i.identifier));
return ids.filter((id) => subSet.has(id) && !calcSet.has(id));
},
// 网关物模型“全部勾选”仅针对子设备变量(不包含计算量)
gwGwIsAllSelected() {
const ids = this.gwQueryParams.identifiers || [];
const allSub = this.gwVariableListSubUnique.map((i) => i.identifier);
return allSub.length > 0 && allSub.every((id) => ids.includes(id));
},
gwShowSubDeviceSelect() {
return (this.gwQueryParams.identifiers || []).length > 0 && !this.gwSelectedIsCalculated;
},
/** 子设备多选绑定:null 显示为“全部”,选具体设备时去掉“全部” */
gwSubDeviceSelectValue: {
get() {
const ids = this.gwQueryParams.subDeviceIds;
return ids === null || (Array.isArray(ids) && ids.length === 0) ? ['__all__'] : ids;
},
set(val) {
const ids = (val || []).filter((v) => v !== '__all__');
this.gwQueryParams.subDeviceIds = ids.length === 0 ? null : ids;
},
},
gwResultTitle() {
if (this.gwResultMode === 'calc') return '曲线趋势图(计算量)';
if (this.gwResultMode === 'compare') return '多设备同一变量对比(仅显示有数据的时间点)';
if (this.gwResultMode === 'single') return '单子设备历史';
return '曲线趋势图';
},
gwHasResult() {
if (this.gwResultMode === 'calc') return this.gwCalcDatas.length > 0;
if (this.gwResultMode === 'compare') return this.subCompareDatas.length > 0;
if (this.gwResultMode === 'single') return this.gwSingleDatas.length > 0;
return false;
},
gwSingleTableList() {
const start = (this.gwSinglePageNum - 1) * this.gwSinglePageSize;
return (this.gwSingleTableComTemp || []).slice(start, start + this.gwSinglePageSize);
},
},
methods: {
getDeptTree() {
deptsTreeSelect().then((response) => {
this.deptOptions = response.data || [];
});
},
getGroupList() {
listGroup({
pageSize: 30,
pageNum: 1,
userId: this.$store.state.user.userId,
}).then((response) => {
this.myGroupList = response.rows || [];
});
},
handleHistoryDeviceFilterChange() {
this.getDevDeviceList();
},
/**
* Element UI Cascader 在 checkStrictly 下仅 radio 会触发选中,行点击会走展开。
* 在面板捕获阶段对「文字区域」模拟 radio 点击,右侧展开箭头仍走原逻辑。
*/
onHistoryDeptCascaderVisible(visible) {
if (!visible) {
this.teardownHistoryDeptCascaderLabelClick();
return;
}
this.$nextTick(() => {
setTimeout(() => this.setupHistoryDeptCascaderLabelClick(), 0);
});
},
setupHistoryDeptCascaderLabelClick() {
this.teardownHistoryDeptCascaderLabelClick();
const drop = document.querySelector('.history-dept-cascader-dropdown');
const panel = drop && drop.querySelector('.el-cascader-panel');
if (!panel) return;
this._historyDeptCascPanel = panel;
this._historyDeptCascPanelHandler = (e) => {
const li = e.target.closest('.el-cascader-node.is-selectable');
if (!li || !panel.contains(li)) return;
if (e.target.closest('.el-cascader-node__postfix')) return;
if (e.target.closest('.el-radio')) return;
const radio = li.querySelector('.el-radio__original');
if (radio && !radio.disabled) {
e.stopPropagation();
e.preventDefault();
radio.click();
}
};
panel.addEventListener('click', this._historyDeptCascPanelHandler, true);
},
teardownHistoryDeptCascaderLabelClick() {
if (this._historyDeptCascPanel && this._historyDeptCascPanelHandler) {
this._historyDeptCascPanel.removeEventListener('click', this._historyDeptCascPanelHandler, true);
}
this._historyDeptCascPanel = null;
this._historyDeptCascPanelHandler = null;
},
/** 筛选后若当前选中设备不在列表中,清空下游状态 */
clearStaleSelectionsAfterDeviceListLoad() {
const ids = new Set((this.devDeviceList || []).map((d) => d.deviceId));
if (this.devQueryParams.deviceId && !ids.has(this.devQueryParams.deviceId)) {
this.devQueryParams.deviceId = null;
this.devQueryParams.subDeviceId = null;
this.devSubDeviceList = [];
this.devIdentifierList = [];
this.devQueryParams.identifiers = [];
this.devDatas = [];
this.devTableComTemp = [];
this.devTotal = 0;
}
if (this.gwQueryParams.gwDeviceId && !ids.has(this.gwQueryParams.gwDeviceId)) {
this.handleGwReset();
}
},
// 获取设备列表
getDevDeviceList() {
const params = {
showChild: this.historyDeviceFilter.showChild,
entrance: 2,
pageNum: 1,
pageSize: 9999,
};
if (this.historyDeviceFilter.deptId != null && this.historyDeviceFilter.deptId !== undefined) {
params.deptId = this.historyDeviceFilter.deptId;
}
if (this.historyDeviceFilter.groupId) {
params.groupId = this.historyDeviceFilter.groupId;
}
listDeviceShort(params).then((res) => {
if (res.code === 200) {
this.devDeviceList = res.rows;
this.$nextTick(() => this.clearStaleSelectionsAfterDeviceListLoad());
}
});
},
// 选择设备后(设备 tab 仅非网关,直接拉取物模型)
handleDevDeviceChange(val) {
this.devQueryParams.identifiers = [];
this.devQueryParams.subDeviceId = null;
this.devSubDeviceList = [];
this.devIdentifierList = [];
if (val) this.getDevIdentifierList(val);
},
// 选择子设备后
handleDevSubDeviceChange(val) {
this.devQueryParams.identifiers = [];
this.devIdentifierList = [];
if (val) {
this.getDevIdentifierList(val);
}
},
// 获取网关子设备列表
getDevSubDeviceList(gwDeviceId) {
listGateway({ gwDeviceId: gwDeviceId, pageNum: 1, pageSize: 9999 }).then((res) => {
if (res.code === 200) {
this.devSubDeviceList = res.rows || [];
if (this.devSubDeviceList.length === 0) {
this.getDevIdentifierList(gwDeviceId);
}
}
});
},
// 设备变量选择变化
handleDevIdentifierChange(val) {
// 如果选择了"全部勾选",则切换全选/全不选
const index = val.indexOf('__selectAll__');
if (index > -1) {
const allIdentifiers = this.devIdentifierList.map(item => item.identifier);
// 判断当前是否已经全部选中(基于当前值,不是新值)
const isAllSelected = allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.devQueryParams.identifiers.includes(id));
this.$nextTick(() => {
if (isAllSelected) {
// 如果已经全部选中,则清空选择(反选)
this.devQueryParams.identifiers = [];
} else {
// 如果没有全部选中,则选中所有(勾选)
this.devQueryParams.identifiers = [...allIdentifiers];
}
});
}
},
getDevIdentifierList(deviceId) {
const params = {
deviceId: deviceId,
pageNum: 1,
pageSize: 9999,
};
listThingsModel(params).then((res) => {
if (res.code === 200) {
this.devIdentifierList = res.rows || [];
}
});
},
// 获取实际查询用的设备信息(子设备优先)
getQueryDevice() {
const device = this.devDeviceList.find((item) => item.deviceId === this.devQueryParams.deviceId);
if (!device) return null;
if (this.devSubDeviceList.length > 0 && this.devQueryParams.subDeviceId) {
const subDevice = this.devSubDeviceList.find((item) => item.subDeviceId === this.devQueryParams.subDeviceId);
if (subDevice) {
return { deviceId: subDevice.subDeviceId, serialNumber: subDevice.subDeviceNo };
}
}
return { deviceId: device.deviceId, serialNumber: device.serialNumber };
},
// 获取设备图形数据
getDevChartDatas() {
this.loading = true;
const queryDevice = this.getQueryDevice();
const identifierList = this.devQueryParams.identifiers.map((item) => {
const identifiers = this.devIdentifierList.find((chil) => chil.identifier === item);
return { identifier: identifiers.identifier, type: identifiers.type };
});
const params = {
deviceId: queryDevice.deviceId,
serialNumber: queryDevice.serialNumber,
identifierList: identifierList,
beginTime: moment(this.devQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(this.devQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
getDataCenterDeviceHistory(params).then((res) => {
if (res.code === 200) {
this.devDatas = res.data;
this.formatDevTableDatas();
if (this.devDatas.length !== 0) {
setTimeout(() => {
this.drawDevLine();
}, 500);
}
}
this.loading = false;
});
},
// 搜索按钮操作
handleDevQuery() {
if (!this.devQueryParams.deviceId || this.devQueryParams.identifiers.length === 0 || !this.devQueryParams.dayDaterange || this.devQueryParams.dayDaterange.length === 0) {
return;
}
if (this.devSubDeviceList.length > 0 && !this.devQueryParams.subDeviceId) {
return;
}
this.getDevChartDatas();
},
// 重置按钮操作
handleDevResetQuery() {
this.resetForm('devQueryForm');
this.devQueryParams.identifiers = [];
this.devQueryParams.subDeviceId = null;
this.devSubDeviceList = [];
this.devIdentifierList = [];
this.devDatas = [];
this.devTableComTemp = [];
this.devTotal = 0;
},
// 获取设备列表数据
getDevTableList(e) {
this.devPageNum = e.page;
this.devPageSize = e.limit;
},
// 搜索展开隐藏
handleSceneSearchChange() {
this.sceneSearchShow = !this.sceneSearchShow;
},
// 获取场景列表
getSceneModelListDatas() {
const params = {
pageNum: 1,
pageSize: 9999,
};
getSceneModelList(params).then((res) => {
if (res.code === 200) {
this.sceneModelList = res.rows;
}
});
},
// 场景选择后
handleSceneModelChange(val) {
this.sceneQueryParams.sceneModelDeviceId = null;
this.sceneQueryParams.identifiers = [];
this.getSceneModelDetailDatas(val);
},
getSceneModelDetailDatas(sceneModelId) {
getSceneModelDetail(sceneModelId).then((res) => {
if (res.code === 200) {
this.sceneDeviceList = res.data.sceneModelDeviceList;
}
});
},
// 场景-设备选择后
handleSceneDeviceChange(val) {
this.sceneQueryParams.identifiers = [];
this.getSceneIdentifierList(val);
},
// 场景变量选择变化
handleSceneIdentifierChange(val) {
// 如果选择了"全部勾选",则切换全选/全不选
const index = val.indexOf('__selectAll__');
if (index > -1) {
const allIdentifiers = this.sceneIdentifierList.map(item => item.identifier);
// 判断当前是否已经全部选中(基于当前值,不是新值)
const isAllSelected = allIdentifiers.length > 0 &&
allIdentifiers.every(id => this.sceneQueryParams.identifiers.includes(id));
this.$nextTick(() => {
if (isAllSelected) {
// 如果已经全部选中,则清空选择(反选)
this.sceneQueryParams.identifiers = [];
} else {
// 如果没有全部选中,则选中所有(勾选)
this.sceneQueryParams.identifiers = [...allIdentifiers];
}
});
}
},
// 获取变量情况列表
getSceneIdentifierList(sceneModelDeviceId) {
const params = {
sceneModelId: this.sceneQueryParams.sceneModelId,
sceneModelDeviceId: sceneModelDeviceId,
pageNum: 1,
pageSize: 9999,
};
getSceneModelDataList(params).then((res) => {
if (res.code === 200) {
this.sceneIdentifierList = res.rows;
}
});
},
// 获取设备图形数据
getSceneChartDatas() {
this.loading = true;
const variableType = this.sceneDeviceList.find((item) => item.id === this.sceneQueryParams.sceneModelDeviceId).variableType;
const ids = this.sceneQueryParams.identifiers.map((item) => this.sceneIdentifierList.find((chil) => chil.identifier === item).id).join(',');
const params = {
sceneModelId: this.sceneQueryParams.sceneModelId,
sceneModelDeviceId: this.sceneQueryParams.sceneModelDeviceId,
variableType: variableType,
ids: ids,
beginTime: moment(this.sceneQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(this.sceneQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
getDataCenterSceneHistory(params).then((res) => {
if (res.code === 200) {
this.sceneDatas = res.data;
this.formatSceneTableDatas();
if (this.sceneDatas.length !== 0) {
setTimeout(() => {
this.drawSceneLine();
}, 500);
}
}
this.loading = false;
});
},
// 搜索按钮操作
handleSceneQuery() {
const isField = this.areAllFields(this.sceneQueryParams);
if (isField) {
this.getSceneChartDatas();
}
},
// 重置按钮操作
handleSceneResetQuery() {
this.resetForm('sceneQueryForm');
this.sceneQueryParams.identifiers = [];
this.sceneDatas = [];
this.sceneTableComTemp = [];
this.sceneTotal = 0;
this.handleSceneQuery();
},
// 获取场景列表数据
getSceneTableList(e) {
this.scenePageNum = e.page;
this.scenePageSize = e.limit;
},
// 判断对象是否都有值
areAllFields(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (!obj[key] || obj[key] === '' || obj[key].length === 0) {
return false;
}
}
}
return true;
},
calcChartHeight(seriesCount) {
return Math.min(1200, Math.max(480, seriesCount * 14 + 360));
},
applyIndependentScale(series) {
series.forEach((s) => {
const nums = (s.data || []).filter(v => v != null && v !== '').map(Number).filter(n => !isNaN(n));
if (nums.length === 0) return;
const min = Math.min(...nums);
const max = Math.max(...nums);
const range = max - min;
s.data = s.data.map(v => {
if (v == null || v === '') return null;
const num = Number(v);
if (isNaN(num)) return null;
// 映射到 -100 ~ 100,range=0 时固定为 0(居中水平线)
return {
value: range === 0 ? 0 : ((num - min) / range) * 200 - 100,
_original: v,
};
});
});
return {
type: 'value',
min: -100,
max: 100,
splitNumber: 4,
axisLine: { show: true },
axisTick: { show: true },
axisLabel: {
formatter: (val) => val + '%',
},
splitLine: {
show: true,
lineStyle: { type: 'dashed', opacity: 0.5 },
},
};
},
independentScaleTooltip() {
return {
trigger: 'axis',
formatter: (params) => {
if (!params || !params.length) return '';
let html = '<div style="font-size:12px">' + params[0].axisValueLabel + '</div>';
params.forEach((p) => {
if (p.data == null) return;
const val = (typeof p.data === 'object' && p.data._original != null)
? p.data._original : p.value;
html += p.marker + ' ' + p.seriesName + ': <b>' + val + '</b><br/>';
});
return html;
},
};
},
handleYAxisModeChange() {
this.$nextTick(() => {
if (this.activeTab === 'device') {
if (this.devDatas.length) this.drawDevLine();
} else if (this.activeTab === 'gateway') {
if (this.gwResultMode === 'calc' && this.gwCalcDatas.length) this.drawGwCalcLine();
if (this.gwResultMode === 'compare' && this.subCompareDatas.length) this.drawSubCompareLine();
if (this.gwResultMode === 'single' && this.gwSingleDatas.length) this.drawGwSingleLine();
} else if (this.activeTab === 'scene') {
if (this.sceneDatas.length) this.drawSceneLine();
}
});
},
drawDevLine() {
const series = this.getDevSeries();
this.devChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
this.charts = this.$echarts.init(this.$refs.devLineChart);
this.charts.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
this.charts.setOption({
tooltip,
legend: {
align: 'right',
left: '3%',
top: '15%',
},
grid: {
top: '30%',
left: '5%',
right: '5%',
bottom: '5%',
containLabel: true,
},
toolbox: {
feature: {
dataView: {},
restore: {},
saveAsImage: {},
},
},
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
data: this.devDatas.length !== 0 && this.devDatas.map((item) => Object.keys(item)[0]),
},
yAxis,
series,
});
this.charts.resize();
});
},
// 获取设备的series
getDevSeries() {
return this.devQueryParams.identifiers.map((item) => {
return {
name: this.devIdentifierList.find((chil) => chil.identifier === item).modelName,
type: 'line',
smooth: true,
data: this.devDatas.map((d) => {
const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item);
return Object.values(ide)[0];
}),
};
});
},
// 网关统一:选网关后拉取计算量变量、子设备变量、子设备列表
handleGwDeviceChange(val) {
this.gwQueryParams.identifiers = [];
this.gwQueryParams.subDeviceIds = null;
this.gwCalcIdentifierList = [];
this.subCompareIdentifierList = [];
this.gwSubDeviceList = [];
this.gwResultMode = '';
if (!val) return;
Promise.all([
getGatewayCalculatedModels(val).then((res) => (res.code === 200 ? res.data || [] : [])),
listThingsModel({ deviceId: val, pageNum: 1, pageSize: 9999 }).then((res) => {
if (res.code === 200) {
const rows = res.rows || [];
return rows.filter((r) => r.type === 1);
}
return [];
}),
listGateway({ gwDeviceId: val, pageNum: 1, pageSize: 9999 }).then((res) => (res.code === 200 ? res.rows || [] : [])),
]).then(([calcList, subList, subDevices]) => {
this.gwCalcIdentifierList = calcList;
this.subCompareIdentifierList = subList;
this.gwSubDeviceList = subDevices;
});
},
handleGwIdentifierChange(val) {
const raw = (val || []).filter((v) => v !== '__selectAll__');
const index = (val || []).indexOf('__selectAll__');
if (index > -1) {
// 全部勾选/反选:仅作用于子设备变量,不勾选计算量
const allSub = this.gwVariableListSubUnique.map((i) => i.identifier);
this.$nextTick(() => {
this.gwQueryParams.identifiers = this.gwGwIsAllSelected ? [] : [...allSub];
});
return;
}
// 计算量与子设备变量均支持多选,保留用户选择的全部项(混合选择在查询时校验)
this.gwQueryParams.identifiers = raw;
if (this.gwSelectedIsCalculated) this.gwQueryParams.subDeviceIds = null;
},
handleGwQuery() {
const { gwDeviceId, identifiers, subDeviceIds, dayDaterange } = this.gwQueryParams;
if (!gwDeviceId || !identifiers || identifiers.length === 0 || !dayDaterange || !dayDaterange.length) {
this.$message.warning('请选择网关设备、物模型和时间范围');
return;
}
const calcIds = this.gwSelectedCalcIdentifiers;
const subIds = this.gwSelectedSubIdentifiers;
if (calcIds.length > 0 && subIds.length > 0) {
this.$message.warning('不能混合选择计算量与子设备变量,请只选其中一类');
return;
}
const device = this.devDeviceList.find((d) => d.deviceId === gwDeviceId);
if (!device) return;
const isCalc = this.gwSelectedIsCalculated;
const singleDeviceId = Array.isArray(subDeviceIds) && subDeviceIds.length === 1 ? subDeviceIds[0] : null;
if (!isCalc && this.gwSubDeviceList.length > 0 && singleDeviceId) {
// 单子设备历史(多变量)
const sub = this.gwSubDeviceList.find((s) => s.subDeviceId === singleDeviceId);
if (!sub) {
this.$message.warning('请选择子设备');
return;
}
this.loading = true;
this.gwResultMode = 'single';
const identifierList = subIds.map((id) => {
const item = this.subCompareIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const params = {
deviceId: sub.subDeviceId,
serialNumber: sub.subDeviceNo,
identifierList,
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
getDataCenterDeviceHistory(params).then((res) => {
if (res.code === 200) {
this.gwSingleDatas = res.data || [];
this.formatGwSingleTableDatas();
this.$nextTick(() => {
if (this.gwSingleDatas.length) this.drawGwSingleLine();
});
}
this.loading = false;
});
return;
}
if (!isCalc) {
// 多设备对比(支持多变量、指定子设备或全部)
this.loading = true;
this.gwResultMode = 'compare';
const params = {
gwDeviceId,
identifierList: subIds,
type: 1,
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
if (Array.isArray(subDeviceIds) && subDeviceIds.length > 0) {
params.subDeviceIds = subDeviceIds;
}
getSubDeviceComparativeHistory(params).then((res) => {
if (res.code === 200) {
this.subCompareDatas = res.data || [];
this.formatSubCompareTableDatas();
this.$nextTick(() => {
if (this.subCompareDatas.length) this.drawSubCompareLine();
});
}
this.loading = false;
});
return;
}
// 计算量(多变量)
this.loading = true;
this.gwResultMode = 'calc';
const identifierList = calcIds.map((id) => {
const item = this.gwCalcIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const calcParams = {
deviceId: device.deviceId,
serialNumber: device.serialNumber,
identifierList,
beginTime: moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
getDeviceHistoryCalculated(calcParams).then((res) => {
if (res.code === 200) {
this.gwCalcDatas = res.data || [];
this.gwCalcTableHeaderTemp = calcIds.map((id) => {
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
return { name: (m && m.modelName) || id, value: id };
});
this.formatGwCalcTableDatas();
this.$nextTick(() => {
if (this.gwCalcDatas.length) this.drawGwCalcLine();
});
}
this.loading = false;
});
},
handleGwReset() {
this.gwQueryParams.gwDeviceId = null;
this.gwQueryParams.identifiers = [];
this.gwQueryParams.subDeviceIds = null;
this.gwQueryParams.dayDaterange = [new Date(Date.now() - 3600 * 1000 * 2), new Date()];
this.gwCalcIdentifierList = [];
this.subCompareIdentifierList = [];
this.gwSubDeviceList = [];
this.gwResultMode = '';
this.gwCalcDatas = [];
this.gwCalcTableComTemp = [];
this.gwCalcTotal = 0;
this.subCompareDatas = [];
this.subCompareTableComTemp = [];
this.subCompareTotal = 0;
this.gwSingleDatas = [];
this.gwSingleTableComTemp = [];
this.gwSingleTotal = 0;
},
async handleGwExport() {
const { gwDeviceId, identifiers, subDeviceIds, dayDaterange } = this.gwQueryParams;
if (!gwDeviceId || !identifiers || identifiers.length === 0 || !dayDaterange?.length) {
this.$message.warning('请先选择网关、物模型和时间范围');
return;
}
const device = this.devDeviceList.find((d) => d.deviceId === gwDeviceId);
if (!device) return;
const calcIds = this.gwSelectedCalcIdentifiers;
const subIds = this.gwSelectedSubIdentifiers;
if (calcIds.length > 0 && subIds.length > 0) {
this.$message.warning('不能混合选择计算量与子设备变量');
return;
}
try {
this.exportLoading = true;
const beginTime = moment(dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss');
const endTime = moment(dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss');
const singleId = Array.isArray(subDeviceIds) && subDeviceIds.length === 1 ? subDeviceIds[0] : null;
if (this.gwResultMode === 'single' && singleId) {
const sub = this.gwSubDeviceList.find((s) => s.subDeviceId === singleId);
if (!sub || this.gwSingleDatas.length === 0) {
this.$message.warning('没有数据可导出');
return;
}
const identifierList = subIds.map((id) => {
const item = this.subCompareIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const params = { deviceId: sub.subDeviceId, serialNumber: sub.subDeviceNo, identifierList, beginTime, endTime };
const res = await exportDeviceHistory(params);
if (res instanceof Blob) {
const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) {
try {
const text = await res.text();
const err = JSON.parse(text);
this.$message.error(err.msg || '导出失败');
return;
} catch (e) {
this.$message.error('导出失败');
return;
}
}
saveAs(res, `${sub.subDeviceNo}_${subIds.join('-')}_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
this.$message.success('导出成功');
}
return;
}
if (this.gwResultMode === 'compare' && this.subCompareDatas.length > 0) {
const params = { gwDeviceId, identifierList: subIds, type: 1, beginTime, endTime };
if (Array.isArray(subDeviceIds) && subDeviceIds.length > 0) params.subDeviceIds = subDeviceIds;
const res = await exportSubDeviceComparativeHistory(params);
if (res instanceof Blob) {
const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) {
try {
const text = await res.text();
const err = JSON.parse(text);
this.$message.error(err.msg || '导出失败');
return;
} catch (e) {
this.$message.error('导出失败');
return;
}
}
const idPart = subIds.length ? subIds.join('-') : 'compare';
saveAs(res, `sub_compare_${idPart}_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
this.$message.success('导出成功');
}
return;
}
if (this.gwResultMode === 'calc' && this.gwCalcDatas.length > 0) {
const identifierList = calcIds.map((id) => {
const item = this.gwCalcIdentifierList.find((i) => i.identifier === id) || {};
return { identifier: id, type: item.type || 1 };
});
const params = { deviceId: device.deviceId, serialNumber: device.serialNumber, identifierList, beginTime, endTime };
const res = await exportDeviceHistoryCalculated(params);
if (res instanceof Blob) {
const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) {
try {
const text = await res.text();
const err = JSON.parse(text);
this.$message.error(err.msg || '导出失败');
return;
} catch (e) {
this.$message.error('导出失败');
return;
}
}
saveAs(res, `${device.serialNumber}_calculated_${moment().format('YYYYMMDDHHmmss')}.xlsx`);
this.$message.success('导出成功');
}
return;
}
this.$message.warning('没有数据可导出');
} catch (e) {
this.$message.error(e.message || '导出失败');
} finally {
this.exportLoading = false;
}
},
getGwCalcTableList(e) {
if (e) {
this.gwCalcPageNum = e.page;
this.gwCalcPageSize = e.limit;
}
},
formatGwCalcTableDatas() {
this.gwCalcTableComTemp = (this.gwCalcDatas || []).map((item) => {
const time = Object.keys(item)[0];
const vals = Object.values(item)[0] || [];
const obj = { time };
vals.forEach((v) => {
const k = Object.keys(v)[0];
obj[k] = Object.values(v)[0];
});
return obj;
});
this.gwCalcTotal = this.gwCalcTableComTemp.length;
const ids = this.gwSelectedCalcIdentifiers;
this.gwCalcTableHeaderTemp = ids.map((id) => {
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
return { name: (m && m.modelName) || id, value: id };
});
},
drawGwCalcLine() {
if (!this.$refs.gwCalcLineChart) return;
const ids = this.gwSelectedCalcIdentifiers;
const times = this.gwCalcDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => {
const m = this.gwCalcIdentifierList.find((i) => i.identifier === id);
return {
name: (m && m.modelName) || id,
type: 'line',
smooth: true,
data: this.gwCalcDatas.map((d) => {
const arr = Object.values(d)[0] || [];
const o = arr.find((x) => Object.keys(x)[0] === id);
return o ? Object.values(o)[0] : null;
}),
};
});
this.gwCalcChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
const chart = this.$echarts.init(this.$refs.gwCalcLineChart);
chart.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
chart.setOption({
tooltip,
legend: { align: 'right', left: '3%', top: '3%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
axisLabel: { rotate: 30 },
data: times,
},
yAxis,
series,
});
chart.resize();
});
},
getGwSingleTableList(e) {
if (e) {
this.gwSinglePageNum = e.page;
this.gwSinglePageSize = e.limit;
}
},
formatGwSingleTableDatas() {
const ids = this.gwSelectedSubIdentifiers;
this.gwSingleTableComTemp = (this.gwSingleDatas || []).map((item) => {
const time = Object.keys(item)[0];
const vals = Object.values(item)[0] || [];
const obj = { time };
vals.forEach((v) => {
const k = Object.keys(v)[0];
obj[k] = Object.values(v)[0];
});
return obj;
});
this.gwSingleTotal = this.gwSingleTableComTemp.length;
this.gwSingleTableHeaderTemp = ids.map((id) => {
const m = this.subCompareIdentifierList.find((i) => i.identifier === id);
return { name: (m && m.modelName) || id, value: id };
});
},
drawGwSingleLine() {
if (!this.$refs.gwSingleLineChart || !this.gwSingleDatas.length) return;
const ids = this.gwSelectedSubIdentifiers;
const times = this.gwSingleDatas.map((d) => Object.keys(d)[0]);
const series = ids.map((id) => {
const m = this.subCompareIdentifierList.find((i) => i.identifier === id);
return {
name: (m && m.modelName) || id,
type: 'line',
smooth: true,
data: this.gwSingleDatas.map((d) => {
const arr = Object.values(d)[0] || [];
const o = arr.find((x) => Object.keys(x)[0] === id);
return o ? Object.values(o)[0] : null;
}),
};
});
this.gwSingleChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
const chart = this.$echarts.init(this.$refs.gwSingleLineChart);
chart.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
chart.setOption({
tooltip,
legend: { align: 'right', left: '3%', top: '3%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
axisLabel: { rotate: 30 },
data: times,
},
yAxis,
series,
});
chart.resize();
});
},
getSubCompareTableList(e) {
if (e) {
this.subComparePageNum = e.page;
this.subComparePageSize = e.limit;
}
},
/** 多设备对比列名分隔符,与后端一致 */
subCompareColSep() {
return ':::';
},
/** 多设备对比表格/图例列展示名:后端返回 "设备标签:::identifier" 时解析为 "设备标签 - 变量名" */
getSubCompareColumnLabel(key) {
if (key === 'time') return '更新时间';
const sep = this.subCompareColSep();
if (key && key.includes(sep)) {
const idx = key.indexOf(sep);
const deviceLabel = key.substring(0, idx);
const identifier = key.substring(idx + sep.length);
const model = (this.subCompareIdentifierList || []).find((i) => i.identifier === identifier);
return `${deviceLabel} - ${(model && model.modelName) || identifier}`;
}
return key;
},
formatSubCompareTableDatas() {
this.subCompareTableComTemp = this.subCompareDatas || [];
this.subCompareTotal = this.subCompareTableComTemp.length;
const cols = [];
if (this.subCompareDatas.length) {
Object.keys(this.subCompareDatas[0]).forEach((k) => {
cols.push({ prop: k, label: this.getSubCompareColumnLabel(k) });
});
}
this.subCompareTableColumns = cols;
},
drawSubCompareLine() {
if (!this.$refs.subCompareLineChart || !this.subCompareDatas.length) return;
const times = this.subCompareDatas.map((d) => d.time);
const keys = Object.keys(this.subCompareDatas[0]).filter((k) => k !== 'time');
const series = keys.map((k) => ({
name: this.getSubCompareColumnLabel(k),
type: 'line',
smooth: true,
data: this.subCompareDatas.map((d) => d[k]),
}));
this.subCompareChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
const chart = this.$echarts.init(this.$refs.subCompareLineChart);
chart.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
chart.setOption({
tooltip,
legend: { align: 'right', left: '3%', top: '3%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '15%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
axisLabel: { rotate: 30 },
data: times,
},
yAxis,
series,
});
chart.resize();
});
},
// 格式化设备列表数据
formatDevTableDatas() {
this.devTableComTemp = this.devDatas.map((item) => {
const time = Object.keys(item)[0];
let obj = {};
Object.values(item)[0].forEach((chil) => {
obj[Object.keys(chil)[0]] = Object.values(chil)[0];
});
return { time, ...obj };
});
this.devTotal = this.devDatas.length;
this.devTableHeaderTemp = this.devQueryParams.identifiers.map((item) => ({
name: this.devIdentifierList.find((chil) => chil.identifier === item).modelName,
value: item,
}));
},
// 导出设备历史数据
async handleDevExport() {
if (!this.devQueryParams.deviceId || this.devQueryParams.identifiers.length === 0 || !this.devQueryParams.dayDaterange || this.devQueryParams.dayDaterange.length === 0) {
this.$message.warning('请先选择设备、变量和时间范围');
return;
}
if (this.devSubDeviceList.length > 0 && !this.devQueryParams.subDeviceId) {
this.$message.warning('请选择子设备');
return;
}
if (this.devDatas.length === 0) {
this.$message.warning('没有数据可导出');
return;
}
try {
this.exportLoading = true;
const queryDevice = this.getQueryDevice();
const identifierList = this.devQueryParams.identifiers.map((item) => {
const identifiers = this.devIdentifierList.find((chil) => chil.identifier === item);
return { identifier: identifiers.identifier, type: identifiers.type };
});
const params = {
deviceId: queryDevice.deviceId,
serialNumber: queryDevice.serialNumber,
identifierList: identifierList,
beginTime: moment(this.devQueryParams.dayDaterange[0]).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(this.devQueryParams.dayDaterange[1]).format('YYYY-MM-DD HH:mm:ss'),
};
const res = await exportDeviceHistory(params);
if (res instanceof Blob) {
const clonedBlob = res.slice();
const isValidBlob = await blobValidate(clonedBlob);
if (!isValidBlob) {
try {
const text = await res.text();
const errorObj = JSON.parse(text);
this.$message.error(errorObj.msg || '导出失败');
return;
} catch (e) {
this.$message.error('导出失败,请稍后重试');
return;
}
}
const deviceSerialNumber = queryDevice.serialNumber || '设备数据';
const fileName = `${deviceSerialNumber}_${moment().format('YYYYMMDDHHmmss')}.xlsx`;
saveAs(res, fileName);
this.$message.success('导出成功');
} else {
this.$message.error('导出失败:服务器返回格式错误');
}
} catch (err) {
console.error('导出失败:', err);
this.$message.error(err.message || '导出失败');
} finally {
this.exportLoading = false;
}
},
drawSceneLine() {
const series = this.getSceneSeries();
this.sceneChartHeight = this.calcChartHeight(series.length);
this.$nextTick(() => {
this.charts = this.$echarts.init(this.$refs.sceneLineChart);
this.charts.clear();
const yAxis = this.independentYAxis
? this.applyIndependentScale(series)
: { type: 'value', scale: true };
const tooltip = this.independentYAxis
? this.independentScaleTooltip()
: { trigger: 'axis' };
this.charts.setOption({
tooltip,
legend: { align: 'right', left: '3%', top: '15%' },
grid: { top: '30%', left: '5%', right: '5%', bottom: '5%', containLabel: true },
toolbox: { feature: { dataView: {}, restore: {}, saveAsImage: {} } },
dataZoom: [
{ type: 'inside', start: 0, end: 100 },
{ start: 0, end: 100 },
],
xAxis: {
type: 'category',
boundaryGap: true,
axisTick: { alignWithLabel: true },
data: this.sceneDatas.length !== 0 && this.sceneDatas.map((item) => Object.keys(item)[0]),
},
yAxis,
series,
});
this.charts.resize();
});
},
// 获取设备的series
getSceneSeries() {
return this.sceneQueryParams.identifiers.map((item) => {
return {
name: this.sceneIdentifierList.find((chil) => chil.identifier === item).sourceName,
type: 'line',
smooth: true,
data: this.sceneDatas.map((d) => {
const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item);
return Object.values(ide)[0];
}),
};
});
},
// 格式化设备列表数据
formatSceneTableDatas() {
this.sceneTableComTemp = this.sceneDatas.map((item) => {
const time = Object.keys(item)[0];
let obj = {};
Object.values(item)[0].forEach((chil) => {
obj[Object.keys(chil)[0]] = Object.values(chil)[0];
});
return { time, ...obj };
});
this.sceneTotal = this.sceneDatas.length;
this.sceneTableHeaderTemp = this.sceneQueryParams.identifiers.map((item) => ({
name: this.sceneIdentifierList.find((chil) => chil.identifier === item).sourceName,
value: item,
}));
},
},
};
</script>
<style lang="scss" scoped>
.data-center-history {
padding: 20px;
.history-device-filter {
margin-bottom: 12px;
padding: 12px 14px 4px;
background: #f5f7fa;
border-radius: 4px;
border: 1px solid #ebeef5;
&__item {
margin-right: 20px;
margin-bottom: 10px;
vertical-align: middle;
&--checkbox {
margin-left: 4px;
}
}
&__help {
margin-left: 6px;
color: #909399;
cursor: help;
vertical-align: middle;
}
/* 与 Tab 内 el-select 同为 Element mini,统一高度与边框 */
.history-filter-control {
width: 340px;
min-width: 280px;
vertical-align: middle;
}
::v-deep .history-filter-control.el-cascader {
line-height: normal;
}
::v-deep .history-filter-control .el-input__inner {
height: 28px;
line-height: 28px;
font-size: 12px;
}
::v-deep .history-filter-control .el-input__icon {
line-height: 28px;
}
}
.device-wrap {
margin-top: 5px;
}
.scene-wrap {
margin-top: 5px;
}
.gateway-wrap {
margin-top: 5px;
}
}
</style>
<style lang="scss">
/* 下拉层挂到 body,需单独写:提示整段文字可点选 */
.history-dept-cascader-dropdown .el-cascader-node.is-selectable .el-cascader-node__label {
cursor: pointer;
user-select: none;
}
</style>