548 lines
15 KiB
Vue
548 lines
15 KiB
Vue
<template>
|
||
<el-card
|
||
v-loading="loading"
|
||
shadow="always"
|
||
class="sbjk-card-container common-card-container-no-title-bg running-card-container"
|
||
>
|
||
<div slot="header">
|
||
<span class="large-title">单体电池实时数据</span>
|
||
</div>
|
||
<!-- 搜索栏-->
|
||
<el-form :inline="true" class="select-container">
|
||
<el-form-item label="编号">
|
||
<el-input
|
||
v-model="search.batteryId"
|
||
placeholder="请输入"
|
||
clearable
|
||
style="width: 150px"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="电池堆">
|
||
<el-select
|
||
v-model="search.stackId"
|
||
placeholder="请选择"
|
||
@change="changeStackId"
|
||
>
|
||
<el-option
|
||
:label="item.deviceName"
|
||
:value="item.id"
|
||
v-for="(item, index) in stackOptions"
|
||
:key="index + 'stackOptions'"
|
||
></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="电池簇">
|
||
<el-select
|
||
v-model="search.clusterId"
|
||
:no-data-text="
|
||
!search.stackId && stackOptions.length > 0
|
||
? '请先选择电池堆'
|
||
: '无数据'
|
||
"
|
||
placeholder="请选择"
|
||
:loading="clusterloading"
|
||
loading-text="正在加载数据"
|
||
>
|
||
<el-option
|
||
:label="item.deviceName"
|
||
:value="item.id"
|
||
v-for="(item, index) in clusterOptions"
|
||
:key="index + 'clusterOptions'"
|
||
></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="onSearch" native-type="button"
|
||
>搜索</el-button
|
||
>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button @click="onReset" native-type="button">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<!-- 切换 -->
|
||
<div class="tip-container">
|
||
<div class="color-tip" v-show="activeBtn === 'list'">
|
||
单体信息
|
||
<span class="tip minwd">最低单体温度</span>
|
||
<span class="tip maxwd">最高单体温度</span>
|
||
<span class="tip mindy">单体最低电压</span>
|
||
<span class="tip maxdy">单体最高电压</span>
|
||
</div>
|
||
<el-button-group class="ems-btns-group">
|
||
<el-button
|
||
:class="{ activeBtn: activeBtn === 'table' }"
|
||
@click="changeMenu('table')"
|
||
>图表</el-button
|
||
>
|
||
<el-button
|
||
:class="{ activeBtn: activeBtn === 'list' }"
|
||
@click="changeMenu('list')"
|
||
>图形</el-button
|
||
>
|
||
</el-button-group>
|
||
</div>
|
||
<component
|
||
:is="activeBtn === 'table' ? 'DtdcTable' : 'DtdcList'"
|
||
:tableData="tableData"
|
||
:totalSize="totalSize"
|
||
:pointIdList="pointIdList"
|
||
@chart="chartDetail"
|
||
></component>
|
||
<el-pagination
|
||
v-show="tableData.length > 0"
|
||
background
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
:current-page="pageNum"
|
||
:page-size="pageSize"
|
||
:page-sizes="[10, 20, 30, 40]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="totalSize"
|
||
style="margin-top: 15px; text-align: center"
|
||
>
|
||
</el-pagination>
|
||
<chart-detail ref="chartDetail" />
|
||
<el-dialog
|
||
:visible.sync="curveDialogVisible"
|
||
:title="curveDialogTitle"
|
||
width="1000px"
|
||
append-to-body
|
||
class="ems-dialog"
|
||
:close-on-click-modal="false"
|
||
destroy-on-close
|
||
@opened="handleCurveDialogOpened"
|
||
@closed="handleCurveDialogClosed"
|
||
>
|
||
<div class="curve-tools">
|
||
<el-date-picker
|
||
v-model="curveCustomRange"
|
||
type="datetimerange"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
range-separator="至"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
style="width: 440px"
|
||
/>
|
||
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
|
||
</div>
|
||
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
|
||
</el-dialog>
|
||
</el-card>
|
||
</template>
|
||
|
||
<script>
|
||
import * as echarts from "echarts";
|
||
import BarChart from "./BarChart";
|
||
import {
|
||
getClusterDataInfoList,
|
||
getClusterNameList,
|
||
getStackNameList,
|
||
} from "@/api/ems/dzjk";
|
||
import { getPointConfigCurve } from "@/api/ems/site";
|
||
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
|
||
import ChartDetail from "./ChartDetail.vue";
|
||
import Table from "./Table.vue";
|
||
import List from "./List.vue";
|
||
export default {
|
||
name: "DzjkSbjkDtdc",
|
||
mixins: [getQuerySiteId],
|
||
components: {
|
||
BarChart,
|
||
ChartDetail,
|
||
DtdcTable: Table,
|
||
DtdcList: List,
|
||
},
|
||
computed: {
|
||
pointIdList() {
|
||
let obj = {};
|
||
this.pointData.forEach((item) => {
|
||
const {
|
||
maxCellTempId,
|
||
maxCellVoltageId,
|
||
minCellTempId,
|
||
minCellVoltageId,
|
||
} = item;
|
||
obj[item.clusterId] = [
|
||
parseInt(minCellTempId || 0),
|
||
parseInt(maxCellTempId || 0),
|
||
parseInt(minCellVoltageId || 0),
|
||
parseInt(maxCellVoltageId || 0),
|
||
]; //最低单体温度 最高温度 最低电压 最高电压 todo 这里的顺序需要和图形组件里的顺序保持一致,
|
||
});
|
||
return obj;
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
clusterloading: false,
|
||
search: { stackId: "", clusterId: "", batteryId: "" },
|
||
stackOptions: [], //{id:'',deviceName:''}
|
||
clusterOptions: [], //{id:'',deviceName:''}
|
||
tableData: [],
|
||
pointData: [],
|
||
pageSize: 40, //分页栏当前每个数据总数
|
||
pageNum: 1, //分页栏当前页数
|
||
totalSize: 0, //table表格数据总数
|
||
activeBtn: "table",
|
||
curveDialogVisible: false,
|
||
curveDialogTitle: "点位曲线",
|
||
curveChart: null,
|
||
curveLoading: false,
|
||
curveCustomRange: [],
|
||
curveQuery: {
|
||
siteId: "",
|
||
pointId: "",
|
||
pointType: "data",
|
||
rangeType: "custom",
|
||
startTime: "",
|
||
endTime: "",
|
||
},
|
||
};
|
||
},
|
||
beforeDestroy() {
|
||
if (this.curveChart) {
|
||
this.curveChart.dispose();
|
||
this.curveChart = null;
|
||
}
|
||
},
|
||
methods: {
|
||
getFieldPointConfig(fieldKey) {
|
||
const pointMap = {
|
||
voltage: { pointIdKey: "voltagePointId", title: "电压 (V)" },
|
||
temperature: { pointIdKey: "temperaturePointId", title: "温度 (℃)" },
|
||
soc: { pointIdKey: "socPointId", title: "SOC (%)" },
|
||
soh: { pointIdKey: "sohPointId", title: "SOH (%)" },
|
||
};
|
||
return pointMap[String(fieldKey || "").trim()] || null;
|
||
},
|
||
openCurveDialogByPointId(pointId, title) {
|
||
const normalizedPointId = String(pointId || "").trim();
|
||
if (!normalizedPointId) {
|
||
this.$message.warning("该字段未配置点位,无法查询曲线");
|
||
return;
|
||
}
|
||
const range = this.getDefaultCurveRange();
|
||
this.curveCustomRange = range;
|
||
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
|
||
this.curveQuery = {
|
||
siteId: this.siteId,
|
||
pointId: normalizedPointId,
|
||
pointType: "data",
|
||
rangeType: "custom",
|
||
startTime: range[0],
|
||
endTime: range[1],
|
||
};
|
||
this.curveDialogVisible = true;
|
||
},
|
||
handleCurveDialogOpened() {
|
||
if (!this.curveChart && this.$refs.curveChartRef) {
|
||
this.curveChart = echarts.init(this.$refs.curveChartRef);
|
||
}
|
||
this.loadCurveData();
|
||
},
|
||
handleCurveDialogClosed() {
|
||
if (this.curveChart) {
|
||
this.curveChart.dispose();
|
||
this.curveChart = null;
|
||
}
|
||
this.curveLoading = false;
|
||
},
|
||
getDefaultCurveRange() {
|
||
const end = new Date();
|
||
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
||
return [this.formatDateTime(start), this.formatDateTime(end)];
|
||
},
|
||
formatDateTime(date) {
|
||
const d = new Date(date);
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
|
||
d.getHours()
|
||
)}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
||
},
|
||
formatCurveTime(value) {
|
||
if (value === undefined || value === null || value === "") {
|
||
return "";
|
||
}
|
||
const raw = String(value).trim();
|
||
const normalized = raw
|
||
.replace("T", " ")
|
||
.replace(/\.\d+/, "")
|
||
.replace(/Z$/, "")
|
||
.replace(/([+-]\d{2}:?\d{2})$/, "")
|
||
.trim();
|
||
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
|
||
if (matched) {
|
||
return `${matched[1]} ${matched[2]}`;
|
||
}
|
||
return normalized.slice(0, 16);
|
||
},
|
||
loadCurveData() {
|
||
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
|
||
this.$message.warning("点位信息不完整,无法查询曲线");
|
||
return;
|
||
}
|
||
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
|
||
this.$message.warning("请选择查询时间范围");
|
||
return;
|
||
}
|
||
this.curveQuery.startTime = this.curveCustomRange[0];
|
||
this.curveQuery.endTime = this.curveCustomRange[1];
|
||
const query = {
|
||
siteId: this.curveQuery.siteId,
|
||
pointId: this.curveQuery.pointId,
|
||
pointType: "data",
|
||
rangeType: "custom",
|
||
startTime: this.curveQuery.startTime,
|
||
endTime: this.curveQuery.endTime,
|
||
};
|
||
this.curveLoading = true;
|
||
getPointConfigCurve(query)
|
||
.then((response) => {
|
||
const rows = response?.data || [];
|
||
this.renderCurveChart(rows);
|
||
})
|
||
.catch(() => {
|
||
this.renderCurveChart([]);
|
||
})
|
||
.finally(() => {
|
||
this.curveLoading = false;
|
||
});
|
||
},
|
||
renderCurveChart(rows = []) {
|
||
if (!this.curveChart) return;
|
||
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
|
||
const yData = rows.map((item) => item.pointValue);
|
||
this.curveChart.clear();
|
||
this.curveChart.setOption({
|
||
legend: {},
|
||
grid: {
|
||
containLabel: true,
|
||
},
|
||
tooltip: {
|
||
trigger: "axis",
|
||
axisPointer: {
|
||
type: "cross",
|
||
},
|
||
},
|
||
textStyle: {
|
||
color: "#333333",
|
||
},
|
||
xAxis: {
|
||
type: "category",
|
||
data: xData,
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
},
|
||
dataZoom: [
|
||
{
|
||
type: "inside",
|
||
start: 0,
|
||
end: 100,
|
||
},
|
||
{
|
||
start: 0,
|
||
end: 100,
|
||
},
|
||
],
|
||
series: [
|
||
{
|
||
name: this.curveDialogTitle,
|
||
type: "line",
|
||
data: yData,
|
||
connectNulls: true,
|
||
},
|
||
],
|
||
});
|
||
if (!rows.length) {
|
||
this.$message.warning("当前时间范围暂无曲线数据");
|
||
}
|
||
},
|
||
changeMenu(menu) {
|
||
const { activeBtn } = this;
|
||
activeBtn !== menu && (this.activeBtn = menu);
|
||
},
|
||
//查看表格行图表
|
||
chartDetail(row = {}) {
|
||
const config = this.getFieldPointConfig(row.fieldKey);
|
||
if (!config) return;
|
||
const pointId = row[config.pointIdKey];
|
||
this.openCurveDialogByPointId(pointId, config.title);
|
||
},
|
||
// 分页
|
||
handleSizeChange(val) {
|
||
this.pageSize = val;
|
||
this.$nextTick(() => {
|
||
this.getTableData();
|
||
});
|
||
},
|
||
handleCurrentChange(val) {
|
||
this.pageNum = val;
|
||
this.$nextTick(() => {
|
||
this.getTableData();
|
||
});
|
||
},
|
||
// 搜索
|
||
onSearch() {
|
||
this.pageNum = 1; //每次搜索从1开始搜索
|
||
this.getTableData();
|
||
},
|
||
// 重置
|
||
// 清空搜索栏选中数据
|
||
// 清空电池簇列表,保留电池堆列表
|
||
onReset() {
|
||
this.search = { stackId: "", clusterId: "", batteryId: "" };
|
||
this.clusterOptions = [];
|
||
this.pageNum = 1;
|
||
this.getTableData();
|
||
},
|
||
changeStackId(val) {
|
||
if (val) {
|
||
console.log(
|
||
"选择了电池堆,需要获取对应的电池簇",
|
||
val,
|
||
this.search.stackId
|
||
);
|
||
this.search.clusterId = "";
|
||
this.getClusterList();
|
||
} else {
|
||
this.search.clusterId = "";
|
||
this.clusterOptions = [];
|
||
}
|
||
},
|
||
//表格数据
|
||
getTableData() {
|
||
this.loading = true;
|
||
const {
|
||
stackId: stackDeviceId,
|
||
clusterId: clusterDeviceId,
|
||
batteryId,
|
||
} = this.search;
|
||
const { siteId, pageNum, pageSize } = this;
|
||
getClusterDataInfoList({
|
||
stackDeviceId,
|
||
clusterDeviceId,
|
||
siteId,
|
||
batteryId,
|
||
pageNum,
|
||
pageSize,
|
||
})
|
||
.then((response) => {
|
||
this.tableData = response?.rows?.[0]?.batteryList || []; //todo check
|
||
this.pointData = response?.rows?.[0]?.clusterList || []; //todo check
|
||
this.totalSize = response?.total || 0;
|
||
})
|
||
.finally(() => {
|
||
this.loading = false;
|
||
});
|
||
},
|
||
getStackList() {
|
||
getStackNameList(this.siteId).then((response) => {
|
||
const list = JSON.parse(JSON.stringify(response?.data || []));
|
||
this.stackOptions = list;
|
||
});
|
||
},
|
||
getClusterList() {
|
||
const { stackId } = this.search;
|
||
if (!stackId) {
|
||
this.clusterOptions = [];
|
||
return Promise.resolve();
|
||
}
|
||
this.clusterloading = true;
|
||
const currentStackId = String(stackId);
|
||
return getClusterNameList({
|
||
stackDeviceId: stackId,
|
||
siteId: this.siteId,
|
||
})
|
||
.then((response) => {
|
||
// 避免用户快速切换电池堆时旧请求覆盖新数据
|
||
if (String(this.search.stackId || "") !== currentStackId) return;
|
||
this.clusterOptions = JSON.parse(JSON.stringify(response?.data || []));
|
||
})
|
||
.finally(() => {
|
||
this.clusterloading = false;
|
||
});
|
||
},
|
||
init() {
|
||
// 只有页面初次加载或切换站点的时候调用电池堆列表,其他情况不需要
|
||
this.search = { stackId: "", clusterId: "", batteryId: "" }; //保证切换站点时,清空选择项
|
||
this.clusterOptions = [];
|
||
this.pageNum = 1;
|
||
this.totalSize = 0;
|
||
this.getStackList();
|
||
this.getTableData();
|
||
},
|
||
},
|
||
mounted() {},
|
||
};
|
||
</script>
|
||
<style scoped lang="scss">
|
||
.tip-container {
|
||
text-align: right;
|
||
position: relative;
|
||
.color-tip {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
transform: translateY(-50%);
|
||
font-size: 11px;
|
||
line-height: 12px;
|
||
color: #333;
|
||
.tip {
|
||
padding-left: 30px;
|
||
position: relative;
|
||
&::before {
|
||
display: block;
|
||
content: "";
|
||
position: absolute;
|
||
left: 14px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
}
|
||
&.minwd {
|
||
color: #3794ff;
|
||
&::before {
|
||
background: #3794ff;
|
||
}
|
||
}
|
||
&.maxwd {
|
||
color: #ff3a3b;
|
||
&::before {
|
||
background: #ff3a3b;
|
||
}
|
||
}
|
||
&.mindy {
|
||
color: #de6902;
|
||
&::before {
|
||
background: #de6902;
|
||
}
|
||
}
|
||
&.maxdy {
|
||
color: #ffb521;
|
||
&::before {
|
||
background: #ffb521;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
::v-deep {
|
||
.el-button-group.ems-btns-group {
|
||
& > .el-button {
|
||
padding: 5px 30px !important;
|
||
font-size: 11px;
|
||
line-height: 16px;
|
||
// padding-left: 50px;
|
||
// padding-right: 50px;
|
||
// font-size: 16px;
|
||
// line-height: 24px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|