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

604 lines
18 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 } 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,
}
}
}).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 = [];
if (deviceType === 2) {
// 网关设备:调用多设备对比接口,数据按子设备SN存储
const params = {
gwDeviceId: deviceId,
identifierList: identifiers,
type: 1,
beginTime: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
};
getSubDeviceComparativeHistory(params).then(res => {
if (res.code === 200) {
this.historys = res.data || [];
this.chartData = this.getChartDatas();
this.getTableDatas();
}
})
} 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: moment(beginTime).format('YYYY-MM-DD HH:mm:ss'),
endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss'),
deviceId: deviceId,
serialNumber: serialNumber,
};
getHistoryList(params).then(res => {
if (res.code === 200) {
this.historys = res.data;
this.chartData = this.getChartDatas();
this.getTableDatas();
}
})
}
}
},
// 获取折线图数据(适配两种接口响应格式)
getChartDatas () {
const { deviceType } = this.deviceInfo;
if (deviceType === 2) {
// subDeviceComparativeHistory 格式: [{ "time": "2026-08-23 10:00:00", "子设备A": "25.5", "子设备B": "26.0" }]
const categories = this.historys.map((item) => item.time);
// 获取除 time 外的所有列名作为系列
const keys = Object.keys(this.historys[0] || {}).filter(k => k !== 'time');
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) {
// subDeviceComparativeHistory 格式
const keys = Object.keys(this.historys[0] || {}).filter(k => k !== 'time');
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>