fastbee/yack-app/pagesA/home/device/statistic/history.vue

701 lines
21 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>
<view class="device-history">
<view class="tools-wrap">
<view class="time-shortcut">
<uni-data-select v-model="queryParams.timeShortcut" :localdata="timeShortcuts"
@change="handleTimeShortcutChange" :clear="false"></uni-data-select>
</view>
<view class="date-wrap" @click="handleDateTimeClick">
<u--input
customStyle="height: 70rpx; box-sizing: border-box; padding: 0rpx 18rpx; background-color:transparent; border: 1px solid #e5e5e5 !important; pointer-events:none;"
:placeholder="$tt('deviceHistory.inputDate')" :value="formatDate(queryParams.strTime)" disabled>
</u--input>
</view>
<view class="filter-btn">
<u-icon :label="$tt('deviceHistory.filtrate')" labelPos="left" name="hourglass" size="18"
@click="handleFilterClick"></u-icon>
</view>
</view>
<!-- 折线图 -->
<view class="charts-box" v-if="historys.length !== 0">
<qiun-data-charts type="line" :opts="chartOpts" :chartData="chartData" canvasId="multiLineChart" canvas2d />
</view>
<!-- 表格 -->
<view class="table-box" v-if="historys.length !== 0">
<uni-table :border="false" :show="devTotal === 0" stripe :emptyText="$tt('deviceHistory.emptyTable')">
<uni-tr>
<uni-th align="center" width="190">{{ $tt('deviceHistory.updateTime') }}</uni-th>
<uni-th align="center" width="110" v-for="item in tableHeaders"
:key="item.value">{{item.name}}</uni-th>
</uni-tr>
<uni-tr v-for="item in tableCom" :key="item.value">
<uni-td align="center">{{item.time}}</uni-td>
<uni-td align="center" v-for="chil in tableHeaders" :key="chil.value">{{item[chil.value]}}</uni-td>
</uni-tr>
</uni-table>
<view class="pagination-box">
<uni-pagination show-icon :page-size="devPageSize" :current="devPageNum" :total="devTotal"
@change="getTableComsList" />
</view>
</view>
<u-empty mode="data" :show="historys.length === 0" :text="$tt('deviceHistory.emptyData')"
marginTop="30"></u-empty>
<view class="other-wrap">
<!-- 日期选择器 -->
<u-datetime-picker ref="datetimePicker" :show="isDateTime" mode="datetime" :closeOnClickOverlay="true"
:confirmText="dateTimeIndex > 1 ? $tt('deviceHistory.confirm') : $tt('deviceHistory.nextStep')"
:cancelText="$tt('deviceHistory.cancel')" @confirm="handleConfirmDateTime"
@cancel="handleCancelDateTime" @close="handleCloseDateTime">
</u-datetime-picker>
<u-popup :show="isFilter" mode="top" :safeAreaInsetBottom="false" @close="isFilter = false">
<view class="filter-pop">
<view class="content-wrap">
<!-- <view class="title-wrap" v-if="deviceInfo.deviceType === 2">子设备</view>
<view class="check-box-wrap" v-if="deviceInfo.deviceType === 2">
<uni-data-checkbox mode="tag" v-model="queryParams.slaveId"
:localdata="slaveList"></uni-data-checkbox>
</view> -->
<view class="title-wrap">{{ $tt('deviceHistory.variable')}}</view>
<view class="check-box-wrap">
<uni-data-checkbox mode="tag" multiple v-model="queryParams.identifiers"
:localdata="identifierList" @change="handleIdentifierCheck"></uni-data-checkbox>
</view>
</view>
<u-line direction='row'></u-line>
<view class="bottom-wrap">
<u-button customStyle="border: none;" plain :text="$tt('deviceHistory.cancel')"
@click="isFilter = false"></u-button>
<u-line direction='col' length="40"></u-line>
<u-button customStyle="border: none;" type="primary" plain :text="$tt('deviceHistory.confirm')"
@click="handleConfirmFilter"></u-button>
</view>
</view>
</u-popup>
</view>
</view>
</template>
<script>
import moment from 'moment';
import uniDataCheckbox from '@/pagesA/components/uni-data-checkbox/index.vue';
import uniPagination from '@/pagesA/components/uni-pagination/index.vue';
import { getSubGatewayList } from '@/apis/modules/gateway.js';
import { listThingsModel } from '@/apis/modules/device.js';
import { getHistoryList, getSubDeviceComparativeHistory, getDeviceHistoryCalculated } from '@/apis/modules/deviceLog.js';
export default {
name: 'deviceHistory',
components: {
uniDataCheckbox,
uniPagination
},
props: {
device: {
type: Object,
default: null,
require: true,
},
type: Number
},
watch: {
device: function (newVal, oldVal) {
this.deviceInfo = newVal;
},
type (val) {
if (val === 3) {
// const { deviceType } = this.deviceInfo || {};
// if (deviceType === 2) {
// this.getSlaveDatas();
// }
this.getData();
}
}
},
data () {
const beginTime = new Date().getTime() - 3600 * 1000 * 24;
const endTime = new Date().getTime();
return {
deviceInfo: {}, // 设备信息
deviceId: null,
// 时间快捷数据列表(value 为小时数,-1 表示自定义)
timeShortcuts: [{
value: 2,
text: this.$tt('deviceHistory.lastTwoHours')
},
{
value: 4,
text: this.$tt('deviceHistory.lastFourHours')
},
{
value: 8,
text: this.$tt('deviceHistory.lastEightHours')
},
{
value: 12,
text: this.$tt('deviceHistory.lastTwelveHours')
},
{
value: 24,
text: this.$tt('deviceHistory.lastOneDay')
},
{
value: 48,
text: this.$tt('deviceHistory.lastTwoDays')
},
{
value: 72,
text: this.$tt('deviceHistory.lastThreeDays')
},
{
value: 120,
text: this.$tt('deviceHistory.lastFiveDays')
},
{
value: 168,
text: this.$tt('deviceHistory.lastSevenDays')
},
{
value: 360,
text: this.$tt('deviceHistory.lastFifteenDays')
},
{
value: 720,
text: this.$tt('deviceHistory.lastThirtyDay')
},
{
value: -1,
text: this.$tt('deviceHistory.custom')
},
],
// 查询参数
queryParams: {
timeShortcut: 24,
strTime: `${beginTime}-${endTime}`,
beginTime: beginTime,
endTime: endTime,
slaveId: '',
identifiers: [],
},
isDateTime: false, // 是否开启日期选择器
dateTimeIndex: 1, // 次数
isFilter: false, // 是否开启筛选
slaveList: [], // 子设备列表
identifierList: [], // 变量列表
// chart option
chartOpts: {
color: ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4',
'#ea7ccc'
], // 三组数据的颜色
padding: [15, 10, 0, 15], // 图表的内边距
dataLabel: false, // 不显示数据标签
dataPointShape: false, // 显示数据点形状
enableScroll: false, // 禁用图表滚动
legend: {}, // 启用图例
xAxis: {
disableGrid: true, // 禁用X轴网格线
disabled: true, // 禁用x轴的显示
},
yAxis: {
disableGrid: false, // 禁用Y轴网格线
data: [{
// min: 0,
// max: 100 // 设置Y轴的最小值和最大值
}],
},
extra: {
line: {
type: "step", // 直线类型的折线图
width: 1, // 线条宽度
activeType: "hollow", // 激活状态下的线条样式
linearType: "custom" // 自定义线条渐变颜色
},
},
},
// 存储图表数据
chartData: {
categories: [],
series: []
},
historys: [], // 历史数据
tableHeaders: [], // 设备列表table数据
tableComTemp: [],
devPageSize: 10,
devPageNum: 1,
devTotal: 0, // 数据总量
};
},
computed: {
tableCom () {
const start = (this.devPageNum - 1) * this.devPageSize;
const end = start + this.devPageSize;
return this.tableComTemp.slice(start, end);
},
},
methods: {
// 快捷时间选择(val 为小时数,-1 表示自定义)
handleTimeShortcutChange (val) {
if (val === -1) {
return;
}
const endTime = new Date().getTime();
const beginTime = endTime - 3600 * 1000 * val;
this.queryParams.beginTime = beginTime;
this.queryParams.endTime = endTime;
this.queryParams.strTime = beginTime + '-' + endTime;
this.getHistory();
},
// 格式化时间
formatDate (strTime) {
const times = strTime.split('-');
const beginTime = new Date(Number(times[0]));
const endTime = new Date(Number(times[1]));
const beginMonth = (beginTime.getMonth() + 1).toString().padStart(2, '0');
const beginDay = beginTime.getDate().toString().padStart(2, '0');
const beginHours = beginTime.getHours().toString().padStart(2, '0');
const beginMinutes = beginTime.getMinutes().toString().padStart(2, '0');
const endMonth = (endTime.getMonth() + 1).toString().padStart(2, '0');
const endDay = endTime.getDate().toString().padStart(2, '0');
const endHours = endTime.getHours().toString().padStart(2, '0');
const endMinutes = endTime.getMinutes().toString().padStart(2, '0');
return `${beginMonth}-${beginDay} ${beginHours}:${beginMinutes} - ${endMonth}-${endDay} ${endHours}:${endMinutes}`;
},
// 日期选择
handleDateTimeClick () {
const { beginTime, endTime } = this.queryParams;
if (this.dateTimeIndex === 1) {
this.$refs.datetimePicker.innerValue = beginTime;
} else {
this.$refs.datetimePicker.innerValue = endTime;
}
this.isDateTime = true;
},
// 时间选择下一步/确定
handleConfirmDateTime (e) {
const { beginTime, endTime } = this.queryParams;
if (this.dateTimeIndex === 1) {
this.queryParams.beginTime = e.value;
this.dateTimeIndex = this.dateTimeIndex + 1;
this.queryParams.strTime = e.value + '-' + endTime;
this.$refs.datetimePicker.innerValue = endTime;
this.isDateTime = false;
setTimeout(() => {
this.isDateTime = true;
}, 1);
} else {
this.queryParams.endTime = e.value;
this.dateTimeIndex = 1;
this.queryParams.strTime = beginTime + '-' + e.value;
this.getHistory();
this.isDateTime = false;
this.queryParams.timeShortcut = -1;
}
},
// 取消日期选择
handleCancelDateTime () {
this.isDateTime = false;
},
// 点击遮取消日期选择
handleCloseDateTime () {
this.isDateTime = false;
},
// 筛选
handleFilterClick () {
this.isFilter = true;
},
// 变量选择
handleIdentifierCheck (e) {
const value = e.detail.value;
const length = value.length;
if (length > 9) {
uni.showToast({
icon: 'none',
title: this.$tt('deviceHistory.countTip')
});
this.queryParams.identifiers = value.slice(0, 9);
}
},
// 筛选确认
handleConfirmFilter () {
const { identifiers } = this.queryParams;
if (identifiers.length === 0) {
uni.showToast({
icon: 'none',
title: this.$tt('deviceHistory.select')
});
} else {
this.getHistory();
this.isFilter = false;
}
},
// 获取子设备列表数据
getSlaveDatas () {
const { deviceId } = this.deviceInfo;
const params = {
gwDeviceId: deviceId,
pageNum: 1,
pageSize: 9999,
};
getSubGatewayList(params).then(res => {
const { code, rows } = res;
if (code === 200) {
this.slaveList = rows.map(item => ({
text: item.subDeviceName,
value: item.slaveId
}));
this.queryParams.slaveId = this.slaveList[0].value;
}
});
},
// 获取物模型(网关设备按identifier去重)
getIdentifierList () {
const { deviceId, deviceType } = this.deviceInfo;
const params = {
deviceId: deviceId,
pageNum: 1,
pageSize: 9999,
};
listThingsModel(params).then((res) => {
const { code, rows } = res;
if (code === 200) {
this.identifierList = rows.map(item => {
if (item.datatype.type !== 'string') {
return {
text: item.modelName,
value: item.identifier,
type: item.type,
isCalculated: item.isCalculated, // 是否计算物模型
slaveId: item.slaveId, // 子设备从机号(网关自身物模型为空)
}
}
}).filter(item => item !== undefined);
// 网关设备:按identifier去重(多个子设备可能有相同identifier)
if (deviceType === 2) {
const seen = new Set();
this.identifierList = this.identifierList.filter(item => {
if (seen.has(item.value)) {
return false;
}
seen.add(item.value);
return true;
});
}
if (this.identifierList.length > 0) {
this.queryParams.identifiers = [this.identifierList[0].value];
}
}
});
},
// 获取统计数据
getData () {
this.getIdentifierList();
this.getHistory(); // 获取统计数据
},
// 获取统计数据
getHistory () {
const { identifiers, beginTime, endTime } = this.queryParams;
const { deviceId, serialNumber, deviceType } = this.deviceInfo;
if (identifiers.length !== 0) {
this.historys = [];
const beginTimeStr = moment(beginTime).format('YYYY-MM-DD HH:mm:ss');
const endTimeStr = moment(endTime).format('YYYY-MM-DD HH:mm:ss');
if (deviceType === 2) {
this.getGatewayHistory(identifiers, deviceId, serialNumber, beginTimeStr, endTimeStr);
} else {
// 直连设备
const idenList = identifiers.map((item) => {
const iden = this.identifierList.find((chil) => chil.value === item);
return {
identifier: iden.value,
type: iden.type
};
});
const params = {
identifierList: idenList,
beginTime: beginTimeStr,
endTime: endTimeStr,
deviceId: deviceId,
serialNumber: serialNumber,
};
getHistoryList(params).then(res => {
if (res.code === 200) {
this.historys = res.data;
this.chartData = this.getChartDatas();
this.getTableDatas();
}
})
}
}
},
// 网关设备历史查询:普通变量走多设备对比接口,计算物模型走计算接口,结果合并
getGatewayHistory (identifiers, deviceId, serialNumber, beginTimeStr, endTimeStr) {
// 计算物模型:isCalculated=1 且属于网关自身产品(无slaveId)
const calculatedItems = identifiers.filter((id) => {
const iden = this.identifierList.find((chil) => chil.value === id);
return iden && iden.isCalculated === 1 && (iden.slaveId === null || iden.slaveId === undefined || iden.slaveId === '');
});
const normalIdentifiers = identifiers.filter((id) => !calculatedItems.includes(id));
const tasks = [];
if (normalIdentifiers.length > 0) {
// 普通变量:多设备对比接口(数据按子设备SN存储)
tasks.push(getSubDeviceComparativeHistory({
gwDeviceId: deviceId,
identifierList: normalIdentifiers,
type: 1,
beginTime: beginTimeStr,
endTime: endTimeStr,
}).then(res => ({
type: 'compare',
data: res.code === 200 ? (res.data || []) : []
})));
}
if (calculatedItems.length > 0) {
// 计算物模型:查询时按公式计算
const idenList = calculatedItems.map((id) => {
const iden = this.identifierList.find((chil) => chil.value === id);
return {
identifier: iden.value,
type: iden.type
};
});
tasks.push(getDeviceHistoryCalculated({
identifierList: idenList,
beginTime: beginTimeStr,
endTime: endTimeStr,
deviceId: deviceId,
serialNumber: serialNumber,
}).then(res => ({
type: 'calculated',
data: res.code === 200 ? (res.data || []) : []
})));
}
if (tasks.length === 0) {
return;
}
Promise.all(tasks).then((results) => {
// 统一转换为 [{ time, 列名: 值 }] 格式后按时间合并
const rowMap = new Map();
results.forEach((result) => {
if (result.type === 'compare') {
// subDeviceComparativeHistory 格式: [{ "time": "...", "子设备A(从机1)": "25.5" }]
result.data.forEach((row) => {
if (!row.time) {
return;
}
if (!rowMap.has(row.time)) {
rowMap.set(row.time, { time: row.time });
}
const target = rowMap.get(row.time);
Object.keys(row).forEach((k) => {
if (k !== 'time') {
target[k] = row[k];
}
});
});
} else {
// deviceHistoryCalculated 格式: [{ "2026-08-23 10:00:00": [{ "calcVar": "12.3" }] }]
result.data.forEach((item) => {
const time = Object.keys(item)[0];
if (!time) {
return;
}
if (!rowMap.has(time)) {
rowMap.set(time, { time });
}
const target = rowMap.get(time);
Object.values(item)[0].forEach((chil) => {
const id = Object.keys(chil)[0];
const iden = this.identifierList.find((m) => m.value === id);
// 计算物模型列名使用变量名
target[iden ? iden.text : id] = Object.values(chil)[0];
});
});
}
});
// 按时间升序排序
this.historys = Array.from(rowMap.values()).sort((a, b) => (a.time < b.time ? -1 : 1));
this.chartData = this.getChartDatas();
this.getTableDatas();
});
},
// 获取折线图数据(适配两种接口响应格式)
getChartDatas () {
const { deviceType } = this.deviceInfo;
if (deviceType === 2) {
// 网关:合并后的格式 [{ "time": "...", "子设备A(从机1)": "25.5", "计算变量": "12.3" }]
if (this.historys.length === 0) {
return { categories: [], series: [] };
}
const categories = this.historys.map((item) => item.time);
// 从所有行收集列名(避免第一行缺少某些列)
const keys = [];
this.historys.forEach((item) => Object.keys(item).forEach((k) => {
if (k !== 'time' && !keys.includes(k)) {
keys.push(k);
}
}));
const series = keys.map((key) => {
return {
name: key,
data: this.historys.map((d) => Number(d[key]) || 0),
};
});
return { categories, series };
} else {
// deviceHistory 格式: [{ "2026-08-23 10:00:00": [{ "temperature": "25.5" }] }]
const categories = this.historys.map((item) => Object.keys(item)[0]);
const series = this.queryParams.identifiers.map((item) => {
return {
name: this.identifierList.find((chil) => chil.value === item).text,
data: this.historys.map((d) => {
const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item);
return Number(Object.values(ide)[0]);
}),
};
});
return { categories, series };
}
},
// 获取表格数据(适配两种接口响应格式)
getTableDatas () {
const { deviceType } = this.deviceInfo;
if (deviceType === 2) {
// 网关:合并后的格式 [{ "time": "...", "子设备A(从机1)": "25.5", "计算变量": "12.3" }]
// 从所有行收集列名(避免第一行缺少某些列)
const keys = [];
this.historys.forEach((item) => Object.keys(item).forEach((k) => {
if (k !== 'time' && !keys.includes(k)) {
keys.push(k);
}
}));
this.tableHeaders = keys.map((key) => ({
name: key,
value: key,
}));
this.tableComTemp = this.historys.map((item) => {
let obj = {};
keys.forEach((key) => {
obj[key] = item[key];
});
return {
time: item.time,
...obj
};
});
} else {
// deviceHistory 格式
this.tableHeaders = this.queryParams.identifiers.map((item) => ({
name: this.identifierList.find((chil) => chil.value === item).text,
value: item,
}));
this.tableComTemp = this.historys.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.historys.length;
},
// 获取设备列表数据
getTableComsList (e) {
this.devPageNum = e.current;
},
},
options: {
styleIsolation: 'shared'
}
};
</script>
<style lang="scss" scoped>
.device-history {
width: 100%;
.tools-wrap {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx 14rpx;
background: #fff;
.time-shortcut {
width: 190rpx;
::v-deep .uni-select {
height: 70rpx;
}
}
.date-wrap {
flex: 1;
margin-left: 10rpx;
}
.filter-btn {
font-size: 28rpx;
margin-left: 14rpx;
height: 70rpx;
display: flex;
align-items: center;
}
}
.charts-box {
width: 100%;
background-color: #fff;
padding: 20rpx 0;
margin-top: 20rpx;
}
.table-box {
width: 100%;
background-color: #fff;
padding: 20rpx 0;
margin-top: 20rpx;
.pagination-box {
margin: 20rpx 20rpx 0 0;
}
}
.other-wrap {
.filter-pop {
.content-wrap {
padding: 20rpx 20rpx 0;
.title-wrap {
font-size: 28rpx;
font-weight: bold;
margin-top: 10rpx;
}
.check-box-wrap {
margin: 10rpx 0 30rpx 0;
max-height: 200px;
overflow-y: auto;
&:last-child {
margin-bottom: 0;
}
}
}
.bottom-wrap {
display: flex;
flex-direction: row;
align-items: center;
}
}
}
}
</style>