This commit is contained in:
2026-02-16 13:41:41 +08:00
parent 41a3ab45b3
commit c7c1b416ee
17 changed files with 2821 additions and 375 deletions

View File

@ -1,6 +1,6 @@
<template>
<div class="ems-dashboard-editor-container" v-loading="loading">
<el-card shadow="always" class="common-card-container">
<el-card shadow="never" class="common-card-container">
<div slot="header" class="clearfix">
<span class="card-title">运行参数配置</span>
<span class="site-tag">站点{{ siteId || '-' }}</span>
@ -208,6 +208,11 @@ export default {
font-size: 13px;
}
.common-card-container {
border: none;
box-shadow: none !important;
}
.runtime-grid {
max-width: 1180px;
display: flex;
@ -219,7 +224,7 @@ export default {
}
.group-card {
border: 1px solid #ebeef5;
border: none;
border-radius: 4px;
padding: 14px 14px 2px;
min-height: 330px;

View File

@ -74,8 +74,12 @@
<div slot="header">
<span class="card-title">总累计运行数据</span>
<div class="total-count">
<span class="title">总收入</span>
<span class="value">
<span class="title pointer-field" @click="handleTotalRevenueClick">总收入</span>
<span
class="value pointer-field"
:class="{ 'field-disabled': !hasPointId(totalRevenueDisplayItem) }"
@click="handleTotalRevenueClick"
>
<i v-if="isRunningInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ totalRevenueDisplayValue | formatNumber }}</span>
</span>
@ -92,7 +96,11 @@
:key="index + 'sjglData'"
class="sjgl-col"
>
<div class="sjgl-wrapper">
<div
class="sjgl-wrapper pointer-field"
:class="{ 'field-disabled': !hasPointId(item.raw) }"
@click="handleRunningFieldClick(item)"
>
<div class="sjgl-title">{{ item.title }}</div>
<div class="sjgl-value" :style="{color:item.color}">
<i v-if="item.loading" class="el-icon-loading"></i>
@ -111,12 +119,40 @@
<active-chart ref="activeChart" :display-data="runningDisplayData"/>
</el-col>
</el-row>
<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>
</div>
</template>
<script>
import * as echarts from "echarts";
import {getSingleSiteBaseInfo} from "@/api/ems/zddt";
import {getDzjkHomeView, getProjectDisplayData} from "@/api/ems/dzjk";
import {getPointConfigCurve} from "@/api/ems/site";
import WeekChart from "./WeekChart.vue";
import ActiveChart from "./ActiveChart.vue";
import AlarmTable from "./AlarmTable.vue";
@ -135,6 +171,19 @@ export default {
runningInfoLoading: false,
runningUpdateSpinning: false,
runningUpdateTimer: null,
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
fallbackSjglData: [
{
title: "今日充电量kWh",
@ -218,6 +267,7 @@ export default {
value: item.fieldValue,
color: this.getCardColor(index),
loading: this.isRunningInfoLoading,
raw: item,
}));
}
return this.fallbackSjglData.map(item => ({
@ -225,6 +275,7 @@ export default {
value: this.runningInfo[item.attr],
color: item.color,
loading: this.isRunningInfoLoading,
raw: item,
}));
},
},
@ -233,8 +284,172 @@ export default {
clearTimeout(this.runningUpdateTimer);
this.runningUpdateTimer = null;
}
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
methods: {
hasPointId(item) {
return !!String(item?.dataPoint || "").trim();
},
handleTotalRevenueClick() {
const item = this.totalRevenueDisplayItem;
const pointId = String(item?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || "总收入",
});
},
handleRunningFieldClick(card) {
const item = card?.raw || {};
const pointId = String(item?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: card?.title || item?.fieldName || item?.fieldCode || pointId,
});
},
openCurveDialog({pointId, title}) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
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 pad = (num) => String(num).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(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("当前时间范围暂无曲线数据");
}
},
setBaseInfoLoading(loading) {
if (Object.prototype.hasOwnProperty.call(this.$data, "baseInfoLoading")) {
this.baseInfoLoading = loading;
@ -407,6 +622,22 @@ export default {
}
}
.pointer-field {
cursor: pointer;
}
.field-disabled {
cursor: not-allowed;
opacity: 0.75;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.row-container {
& > .el-col {
margin-bottom: 20px;

View File

@ -3,7 +3,13 @@
<!-- 6个方块-->
<el-row :gutter="10">
<el-col :xs="12" :sm="8" :lg="4" style="margin-bottom: 10px;" class="single-square-box-container" v-for="(item,index) in displaySquares" :key="index+'singleSquareBox'">
<single-square-box :data="{...item,value:item.value,loading:item.valueLoading}" ></single-square-box>
<div
class="square-click-wrapper"
:class="{ 'field-disabled': !item.pointId }"
@click="handleSquareClick(item)"
>
<single-square-box :data="{...item,value:item.value,loading:item.valueLoading}" ></single-square-box>
</div>
</el-col>
</el-row>
</template>
@ -49,16 +55,23 @@ export default {
];
return defaults.map((def, index) => {
const row = sourceMap[def.fieldCode] || {};
const pointId = String(row.dataPoint || "").trim();
return {
title: row.fieldName || def.fieldName,
value: row.fieldValue,
valueLoading: this.loading && this.isEmptyValue(row.fieldValue),
bgColor: this.getBgColor(index),
pointId,
fieldCode: row.fieldCode || def.fieldCode,
raw: row,
};
});
},
},
methods: {
handleSquareClick(item) {
this.$emit("field-click", item || {});
},
getFieldName(fieldCode) {
const raw = String(fieldCode || "").trim();
if (!raw) return "";
@ -85,4 +98,13 @@ export default {
width: fit-content;
}
}
.square-click-wrapper {
cursor: pointer;
}
.square-click-wrapper.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
</style>

View File

@ -51,15 +51,33 @@
<el-descriptions-item
contentClassName="descriptions-direction work-status"
:span="1" label="工作状态">
{{ CLUSTERWorkStatusOptions[baseInfo.workStatus] || '-' }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'workStatus') }"
@click="handleFieldClick(baseInfo, 'workStatus', '工作状态')"
>
{{ CLUSTERWorkStatusOptions[baseInfo.workStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
:span="1" label="与PCS通信">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'pcsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
:span="1" label="与EMS通信">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'emsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
@ -68,7 +86,11 @@
<el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction"
v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :span="1"
:label="item.label">
<span class="pointer" @click="showChart(item.pointName || '',baseInfo.deviceId)">
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, item.attr) }"
@click="handleFieldClick(baseInfo, item.attr, item.label)"
>
<i v-if="isPointLoading(baseInfo[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(baseInfo[item.attr]) | formatNumber }}</span>
<span v-if="item.unit" v-html="item.unit"></span>
@ -80,7 +102,11 @@
<div class="process-line-bg">
<div class="process-line" :style="{height:baseInfo.currentSoc+'%'}"></div>
</div>
<div class="process pointer" @click="showChart( '当前SOC',baseInfo.deviceId)">当前SOC :
<div
class="process pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'currentSoc') }"
@click="handleFieldClick(baseInfo, 'currentSoc', '当前SOC')"
>当前SOC :
{{ baseInfo.currentSoc }}%
</div>
</div>
@ -103,7 +129,8 @@
>
<template slot-scope="scope">
<span class="pointer"
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.avgData
}}</span>
</template>
@ -113,7 +140,8 @@
label="单体最小值">
<template slot-scope="scope">
<span class="pointer"
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.minData
}}</span>
</template>
@ -127,7 +155,8 @@
label="单体最大值">
<template slot-scope="scope">
<span class="pointer "
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.maxData
}}</span>
</template>
@ -139,24 +168,49 @@
</el-table>
</el-card>
</div>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<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>
</div>
</template>
<script>
import pointChart from "./../PointChart.vue";
import * as echarts from "echarts";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {getProjectDisplayData, getStackNameList, getClusterNameList} from '@/api/ems/dzjk'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve} from "@/api/ems/site";
export default {
name: 'DzjkSbjkBmsdcc',
mixins: [getQuerySiteId, intervalUpdate],
components: {PointTable, pointChart},
components: {PointTable},
computed: {
...mapState({
CLUSTERWorkStatusOptions: state => state?.ems?.CLUSTERWorkStatusOptions || {},
@ -174,6 +228,19 @@ export default {
displayData: [],
clusterDeviceList: [],
selectedClusterId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
unitObj: {
'电压': 'V',
'温度': '&#8451;',
@ -228,8 +295,172 @@ export default {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'CLUSTER'}, dataType)
},
showChart(pointName, deviceId) {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory: 'CLUSTER', deviceId})
hasFieldPointId(baseInfo, fieldName) {
const row = this.getFieldRow(baseInfo, fieldName);
return !!String(row?.dataPoint || "").trim();
},
hasTableFieldPointId(baseInfo, dataName, columnLabel) {
const pointName = this.tablePointNameMap[String(dataName || "") + String(columnLabel || "")];
if (!pointName) {
return false;
}
return this.hasFieldPointId(baseInfo, pointName);
},
getFieldRow(baseInfo, fieldName) {
const key = String(fieldName || "").trim();
const map = baseInfo?._fieldRowMap || {};
return map[key] || null;
},
handleFieldClick(baseInfo, fieldName, title) {
const row = this.getFieldRow(baseInfo, fieldName);
const pointId = String(row?.dataPoint || "").trim();
this.openCurveDialogByPointId(pointId, title || fieldName);
},
handleTableFieldClick(baseInfo, dataName, columnLabel) {
const pointName = this.tablePointNameMap[String(dataName || "") + String(columnLabel || "")];
if (!pointName) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.handleFieldClick(baseInfo, pointName, pointName);
},
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("当前时间范围暂无曲线数据");
}
},
handleTagClick(deviceId) {
this.selectedClusterId = deviceId || "";
@ -248,13 +479,28 @@ export default {
return index >= 0 ? raw.slice(index + 2) : raw;
},
getFieldMap(rows = [], deviceId = "") {
const rowMap = this.getFieldRowMap(rows, deviceId);
return Object.keys(rowMap).reduce((acc, fieldName) => {
const row = rowMap[fieldName];
if (acc[fieldName] === undefined) {
acc[fieldName] = row?.fieldValue;
}
return acc;
}, {});
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = String(deviceId || "");
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== targetDeviceId) return;
map[this.getFieldName(item.fieldCode)] = item.fieldValue;
const fieldName = this.getFieldName(item.fieldCode);
map[fieldName] = item;
const displayName = String(item.fieldName || "").trim();
if (displayName && !map[displayName]) {
map[displayName] = item;
}
});
rows.forEach(item => {
if (!item || !item.fieldCode) return;
@ -262,7 +508,11 @@ export default {
if (itemDeviceId !== "") return;
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item.fieldValue;
map[fieldName] = item;
}
const displayName = String(item.fieldName || "").trim();
if (displayName && !map[displayName]) {
map[displayName] = item;
}
});
return map;
@ -323,6 +573,11 @@ export default {
pcsCommunicationStatus: statusMap.pcsCommunicationStatus,
emsCommunicationStatus: statusMap.emsCommunicationStatus,
currentSoc: isNaN(currentSoc) ? 0 : currentSoc,
_fieldRowMap: {
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '簇信息'), id),
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '状态'), id),
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '单体数据'), id),
},
};
})(),
siteId: this.siteId,
@ -348,7 +603,13 @@ export default {
this.loading = false
})
}
}
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}
</script>

View File

@ -49,15 +49,21 @@
<el-descriptions-item
contentClassName="descriptions-direction work-status"
label="工作状态" labelClassName="descriptions-label">
{{ STACKWorkStatusOptions[baseInfo.workStatus] || '-' }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'workStatus', '工作状态')">
{{ STACKWorkStatusOptions[baseInfo.workStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与PCS通信"
labelClassName="descriptions-label">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与EMS通信"
labelClassName="descriptions-label">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
@ -66,7 +72,7 @@
<el-descriptions-item v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :label="item.label"
:span="1" contentClassName="descriptions-direction"
labelClassName="descriptions-label">
<span class="pointer" @click="showChart(item.pointName || '',baseInfo.deviceId)">
<span class="pointer" @click="handleStackFieldClick(baseInfo, item)">
<i v-if="isPointLoading(baseInfo[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(baseInfo[item.attr]) | formatNumber }}</span>
<span v-if="item.unit" v-html="item.unit"></span>
@ -78,7 +84,7 @@
<div class="process-line-bg">
<div :style="{height:baseInfo.stackSoc+'%'}" class="process-line"></div>
</div>
<div class="process pointer" @click="showChart('当前SOC',baseInfo.deviceId)">当前SOC :
<div class="process pointer" @click="handleStackSocClick(baseInfo)">当前SOC :
{{ baseInfo.stackSoc }}%
</div>
</div>
@ -98,21 +104,21 @@
>
<template slot-scope="scope">
<span class="pointer"
@click="showChart('簇电压',scope.row.clusterId,'CLUSTER')">{{ scope.row.clusterVoltage }} V</span>
@click="handleClusterFieldClick(scope.row, 'clusterVoltage', '簇电压')">{{ scope.row.clusterVoltage }} V</span>
</template>
</el-table-column>
<el-table-column
label="簇电流">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('簇电流',scope.row.clusterId,'CLUSTER')">{{ scope.row.clusterCurrent }} A</span>
@click="handleClusterFieldClick(scope.row, 'clusterCurrent', '簇电流')">{{ scope.row.clusterCurrent }} A</span>
</template>
</el-table-column>
<el-table-column
label="簇SOC">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('当前SOC',scope.row.clusterId,'CLUSTER')">{{ scope.row.currentSoc }} %</span>
@click="handleClusterFieldClick(scope.row, 'currentSoc', '当前SOC')">{{ scope.row.currentSoc }} %</span>
</template>
</el-table-column>
<el-table-column
@ -120,7 +126,7 @@
prop="maxVoltage">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最高单体电压',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'maxCellVoltage', '最高单体电压')">{{
scope.row.maxCellVoltage
}} V</span>
</template>
@ -134,7 +140,7 @@
prop="minVoltage">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最低单体电压',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'minCellVoltage', '最低单体电压')">{{
scope.row.minCellVoltage
}} V</span>
</template>
@ -147,7 +153,7 @@
label="单体最高温度">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最高单体温度',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'maxCellTemp', '最高单体温度')">{{
scope.row.maxCellTemp
}} &#8451;</span>
</template>
@ -161,7 +167,7 @@
prop="minTemperature">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最低单体温度',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'minCellTemp', '最低单体温度')">{{
scope.row.minCellTemp
}} &#8451;</span>
</template>
@ -174,22 +180,47 @@
</el-table>
</el-card>
</div>
<point-chart ref="pointChart" :site-id="siteId"/>
<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>
<point-table ref="pointTable"/>
</div>
</template>
<script>
import * as echarts from "echarts";
import {getProjectDisplayData, getStackNameList} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {mapState} from "vuex";
export default {
name: 'DzjkSbjkBmszl',
components: {pointChart, PointTable},
components: {PointTable},
mixins: [getQuerySiteId, intervalUpdate],
computed: {
...mapState({
@ -208,6 +239,19 @@ export default {
displayData: [],
stackDeviceList: [],
selectedStackId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
baseInfoList: [{
siteId: "",
deviceId: "",
@ -217,15 +261,15 @@ export default {
batteryDataList: [],
}],
infoData: [
{label: '电池堆总电压', attr: 'stackVoltage', unit: 'V', pointName: '电池堆电压'},
{label: '可充电量', attr: 'availableChargeCapacity', unit: 'kWh', pointName: '可充电量'},
{label: '累计充电量', attr: 'totalChargeCapacity', unit: 'kWh', pointName: '累计充电量'},
{label: '电池堆总电流', attr: 'stackCurrent', unit: 'A', pointName: '电池堆总电流'},
{label: '可放电量', attr: 'availableDischargeCapacity', unit: 'kWh', pointName: '可放电量'},
{label: '累计放电量', attr: 'totalDischargeCapacity', unit: 'kWh', pointName: '累计放电量'},
{label: 'SOH', attr: 'stackSoh', unit: '%', pointName: 'SOH'},
{label: '平均温度', attr: 'operatingTemp', unit: '&#8451;', pointName: '平均温度'},
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: '&Omega;', pointName: '绝缘电阻'},
{label: '电池堆总电压', attr: 'stackVoltage', unit: 'V'},
{label: '可充电量', attr: 'availableChargeCapacity', unit: 'kWh'},
{label: '累计充电量', attr: 'totalChargeCapacity', unit: 'kWh'},
{label: '电池堆总电流', attr: 'stackCurrent', unit: 'A'},
{label: '可放电量', attr: 'availableDischargeCapacity', unit: 'kWh'},
{label: '累计放电量', attr: 'totalDischargeCapacity', unit: 'kWh'},
{label: 'SOH', attr: 'stackSoh', unit: '%'},
{label: '平均温度', attr: 'operatingTemp', unit: '&#8451;'},
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: '&Omega;'},
]
}
},
@ -246,8 +290,181 @@ export default {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'STACK'}, dataType)
},
showChart(pointName, deviceId, deviceCategory = 'STACK') {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory, deviceId})
handleStatusFieldClick(baseInfo, fieldKey, title) {
const pointId = this.resolvePointId(baseInfo, fieldKey, "status");
this.openCurveDialogByPointId(pointId, title || fieldKey);
},
handleStackFieldClick(baseInfo, item) {
const fieldKey = item?.attr || "";
const pointId = this.resolvePointId(baseInfo, fieldKey, "info");
this.openCurveDialogByPointId(pointId, item?.label || fieldKey);
},
handleStackSocClick(baseInfo) {
const pointId = this.resolvePointId(baseInfo, "stackSoc", "info");
this.openCurveDialogByPointId(pointId, "当前SOC");
},
handleClusterFieldClick(row = {}, fieldKey = "", title = "") {
const directKeys = [
"pointId",
"dataPoint",
`${fieldKey}PointId`,
`${fieldKey}DataPoint`,
];
let pointId = "";
directKeys.some((key) => {
const value = String(row?.[key] || "").trim();
if (value) {
pointId = value;
return true;
}
return false;
});
if (!pointId && row?.pointIdMap && fieldKey) {
pointId = String(row.pointIdMap[fieldKey] || "").trim();
}
this.openCurveDialogByPointId(pointId, title || fieldKey);
},
resolvePointId(baseInfo = {}, fieldKey = "", source = "info") {
const mapKey = source === "status" ? "statusPointIdMap" : "pointIdMap";
return String(baseInfo?.[mapKey]?.[fieldKey] || "").trim();
},
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("当前时间范围暂无曲线数据");
}
},
init() {
this.updateData()
@ -262,26 +479,44 @@ export default {
const index = raw.lastIndexOf("__");
return index >= 0 ? raw.slice(index + 2) : raw;
},
getFieldMap(rows = [], deviceId = "") {
isEmptyValue(value) {
return value === undefined || value === null || value === "";
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = String(deviceId || "");
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== targetDeviceId) return;
map[this.getFieldName(item.fieldCode)] = item.fieldValue;
map[this.getFieldName(item.fieldCode)] = item;
});
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== "") return;
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item.fieldValue;
const existRow = map[fieldName];
if (!existRow || this.isEmptyValue(existRow.fieldValue)) {
map[fieldName] = item;
}
});
return map;
},
getFieldMap(rowMap = {}) {
const map = {};
Object.keys(rowMap || {}).forEach((fieldName) => {
map[fieldName] = rowMap[fieldName]?.fieldValue;
});
return map;
},
getPointIdMap(rowMap = {}) {
const map = {};
Object.keys(rowMap || {}).forEach((fieldName) => {
map[fieldName] = String(rowMap[fieldName]?.dataPoint || "").trim();
});
return map;
},
getLatestTime(menuCode) {
const times = (this.displayData || [])
.filter(item => item.menuCode === menuCode && item.valueTime)
@ -311,8 +546,10 @@ export default {
this.baseInfoList = devices.map(device => ({
...(() => {
const id = device.deviceId || device.id || this.siteId;
const infoMap = this.getFieldMap(this.getModuleRows('SBJK_BMSZL', '堆信息'), id);
const statusMap = this.getFieldMap(this.getModuleRows('SBJK_BMSZL', '状态'), id);
const infoRowMap = this.getFieldRowMap(this.getModuleRows('SBJK_BMSZL', '堆信息'), id);
const statusRowMap = this.getFieldRowMap(this.getModuleRows('SBJK_BMSZL', '状态'), id);
const infoMap = this.getFieldMap(infoRowMap);
const statusMap = this.getFieldMap(statusRowMap);
const stackSoc = Number(infoMap.stackSoc);
return {
...infoMap,
@ -320,6 +557,8 @@ export default {
pcsCommunicationStatus: statusMap.pcsCommunicationStatus,
emsCommunicationStatus: statusMap.emsCommunicationStatus,
stackSoc: isNaN(stackSoc) ? 0 : stackSoc,
pointIdMap: this.getPointIdMap(infoRowMap),
statusPointIdMap: this.getPointIdMap(statusRowMap),
};
})(),
siteId: this.siteId,
@ -346,6 +585,13 @@ export default {
},
}
,
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}
</script>
@ -363,6 +609,13 @@ export default {
cursor: pointer;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
::v-deep {
//描述列表样式
.descriptions-main {

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'dbField'"
:span="8"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<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>
</div>
</template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkDb",
@ -68,6 +98,19 @@ export default {
displayData: [],
selectedSectionKey: "",
ammeterDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
@ -122,6 +165,8 @@ export default {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
@ -175,6 +220,153 @@ export default {
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
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("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
@ -234,6 +426,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
@ -265,6 +463,26 @@ export default {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

@ -15,10 +15,10 @@
{{ item.clusterDeviceId }}
</div>
<div>#{{ item.deviceId }}</div>
<div class="dy pointer" @click="chartDetail(item, '电压 (V)')">
<div class="dy pointer" @click="chartDetail(item, 'voltage')">
{{ item.voltage }}V
</div>
<div class="wd pointer" @click="chartDetail(item, '温度 (℃)')">
<div class="wd pointer" @click="chartDetail(item, 'temperature')">
{{ item.temperature }}
</div>
</div>
@ -102,9 +102,8 @@ export default {
return className;
},
//查看表格行图表
chartDetail(row, dataType = "") {
const { clusterDeviceId, deviceId } = row;
this.$emit("chart", { ...row, dataType });
chartDetail(row, fieldKey = "") {
this.$emit("chart", { ...row, fieldKey });
},
},
};

View File

@ -11,7 +11,7 @@
<el-table-column prop="voltage" label="电压 (V)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, '电压 (V)')"
@click="chartDetail(scope.row, 'voltage')"
type="text"
size="small"
>
@ -22,7 +22,7 @@
<el-table-column prop="temperature" label="温度 (℃)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, '温度 (℃)')"
@click="chartDetail(scope.row, 'temperature')"
type="text"
size="small"
>
@ -33,7 +33,7 @@
<el-table-column prop="soc" label="SOC (%)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'SOC (%)')"
@click="chartDetail(scope.row, 'soc')"
type="text"
size="small"
>
@ -44,7 +44,7 @@
<el-table-column prop="soh" label="SOH (%)">
<template slot-scope="scope">
<el-button
@click="chartDetail(scope.row, 'SOH (%)')"
@click="chartDetail(scope.row, 'soh')"
type="text"
size="small"
>
@ -52,16 +52,6 @@
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template slot-scope="scope">
<el-button @click="$emit('pointDetail',scope.row,'point')" type="text" size="small">
详细
</el-button>
<el-button @click="$emit('pointDetail',scope.row,'alarmPoint')" type="text" size="small">
报警点位详细
</el-button>
</template>
</el-table-column>
</el-table>
<!-- <el-pagination
v-show="tableData.length > 0"
@ -117,8 +107,8 @@ export default {
},
methods: {
//查看表格行图表
chartDetail(row, dataType = "") {
this.$emit("chart", {...row, dataType});
chartDetail(row, fieldKey = "") {
this.$emit("chart", {...row, fieldKey});
},
},
};

View File

@ -88,7 +88,6 @@
:totalSize="totalSize"
:pointIdList="pointIdList"
@chart="chartDetail"
@pointDetail="pointDetail"
></component>
<el-pagination
v-show="tableData.length > 0"
@ -104,34 +103,55 @@
>
</el-pagination>
<chart-detail ref="chartDetail" />
<point-chart ref="pointChart" :site-id="siteId" />
<point-table ref="pointTable"/>
<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 {
getStackNameList,
getClusterNameList,
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";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
export default {
name: "DzjkSbjkDtdc",
mixins: [getQuerySiteId],
components: {
PointTable,
BarChart,
ChartDetail,
DtdcTable: Table,
DtdcList: List,
pointChart,
},
computed: {
pointIdList() {
@ -166,27 +186,190 @@ export default {
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: {
// 查看设备电位表格
pointDetail(row,dataType){
const {deviceId,clusterDeviceId} = row
this.$refs.pointTable.showTable({siteId:this.siteId,deviceId,deviceCategory:'BATTERY',parentId:clusterDeviceId},dataType)
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({ deviceId, clusterDeviceId, dataType = "" }) {
dataType &&
this.$refs.pointChart.showChart({
pointName: dataType,
deviceCategory:'BATTERY',
deviceId: clusterDeviceId,
child: [deviceId],
});
chartDetail(row = {}) {
const config = this.getFieldPointConfig(row.fieldKey);
if (!config) return;
const pointId = row[config.pointIdKey];
this.openCurveDialogByPointId(pointId, config.title);
},
// 分页
handleSizeChange(val) {
@ -224,6 +407,9 @@ export default {
);
this.search.clusterId = "";
this.getClusterList();
} else {
this.search.clusterId = "";
this.clusterOptions = [];
}
},
//表格数据
@ -254,19 +440,26 @@ export default {
},
getStackList() {
getStackNameList(this.siteId).then((response) => {
this.stackOptions = JSON.parse(JSON.stringify(response?.data || []));
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;
getClusterNameList({
stackDeviceId: this.search.stackId,
const currentStackId = String(stackId);
return getClusterNameList({
stackDeviceId: stackId,
siteId: this.siteId,
})
.then((response) => {
this.clusterOptions = JSON.parse(
JSON.stringify(response?.data || [])
);
// 避免用户快速切换电池堆时旧请求覆盖新数据
if (String(this.search.stackId || "") !== currentStackId) return;
this.clusterOptions = JSON.parse(JSON.stringify(response?.data || []));
})
.finally(() => {
this.clusterloading = false;

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'emsField'"
:span="6"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<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>
</div>
</template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkEms",
@ -68,6 +98,19 @@ export default {
displayData: [],
selectedSectionKey: "",
emsDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
@ -122,6 +165,8 @@ export default {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
@ -175,6 +220,153 @@ export default {
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
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("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
@ -218,6 +410,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
@ -249,6 +447,26 @@ export default {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

@ -58,7 +58,14 @@
:span="1"
label="工作状态"
labelClassName="descriptions-label"
>{{ formatDictValue((PCSWorkStatusOptions || {}), pcsItem.workStatus) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'workStatus') }"
@click="handlePcsFieldClick(pcsItem, 'workStatus', '工作状态')"
>
{{ formatDictValue((PCSWorkStatusOptions || {}), pcsItem.workStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -66,7 +73,14 @@
contentClassName="descriptions-direction"
label="并网状态"
labelClassName="descriptions-label"
>{{ formatDictValue((($store.state.ems && $store.state.ems.gridStatusOptions) || {}), pcsItem.gridStatus) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'gridStatus') }"
@click="handlePcsFieldClick(pcsItem, 'gridStatus', '并网状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.gridStatusOptions) || {}), pcsItem.gridStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -76,7 +90,14 @@
:span="1"
label="设备状态"
labelClassName="descriptions-label"
>{{ formatDictValue((($store.state.ems && $store.state.ems.deviceStatusOptions) || {}), pcsItem.deviceStatus) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'deviceStatus') }"
@click="handlePcsFieldClick(pcsItem, 'deviceStatus', '设备状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.deviceStatusOptions) || {}), pcsItem.deviceStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -84,7 +105,14 @@
contentClassName="descriptions-direction"
label="控制模式"
labelClassName="descriptions-label"
>{{ formatDictValue((($store.state.ems && $store.state.ems.controlModeOptions) || {}), pcsItem.controlMode) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'controlMode') }"
@click="handlePcsFieldClick(pcsItem, 'controlMode', '控制模式')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.controlModeOptions) || {}), pcsItem.controlMode) }}
</span>
</el-descriptions-item
>
</el-descriptions>
@ -105,9 +133,8 @@
>
<span
class="pointer"
@click="
showChart(item.pointName || '', pcsItem.deviceId)
"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, item.attr) }"
@click="handlePcsFieldClick(pcsItem, item.attr, item.label)"
>
<i v-if="isPointLoading(pcsItem[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(pcsItem[item.attr]) | formatNumber }}</span>
@ -144,7 +171,8 @@
>
<span
class="pointer"
@click="showChart('直流功率', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcPowerPointId }"
@click="openCurveDialogByPointId(item.dcPowerPointId, '直流功率')"
>{{ item.dcPower }}kW</span
>
</el-descriptions-item>
@ -156,7 +184,8 @@
>
<span
class="pointer"
@click="showChart('直流电压', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcVoltagePointId }"
@click="openCurveDialogByPointId(item.dcVoltagePointId, '直流电压')"
>{{ item.dcVoltage }}V</span
>
</el-descriptions-item>
@ -168,7 +197,8 @@
>
<span
class="pointer"
@click="showChart('直流电流', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcCurrentPointId }"
@click="openCurveDialogByPointId(item.dcCurrentPointId, '直流电流')"
>{{ item.dcCurrent }}A</span
>
</el-descriptions-item>
@ -176,22 +206,47 @@
</div>
</el-card>
</div>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<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>
</div>
</template>
<script>
import pointChart from "./../PointChart.vue";
import * as echarts from "echarts";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getPcsNameList, getProjectDisplayData} from "@/api/ems/dzjk";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve} from "@/api/ems/site";
export default {
name: "DzjkSbjkPcs",
components: {pointChart, PointTable},
components: {PointTable},
mixins: [getQuerySiteId, intervalUpdate],
computed: {
...mapState({
@ -210,6 +265,19 @@ export default {
displayData: [],
pcsDeviceList: [],
selectedPcsId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
pcsList: [{
deviceId: "",
deviceName: "PCS",
@ -337,9 +405,157 @@ export default {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'PCS'}, dataType)
},
showChart(pointName, deviceId, isBranch = false) {
pointName &&
this.$refs.pointChart.showChart({pointName, deviceCategory: isBranch ? 'BRANCH' : 'PCS', deviceId});
hasFieldPointId(pcsItem, fieldName) {
const row = this.getFieldRow(pcsItem, fieldName);
return !!String(row?.dataPoint || "").trim();
},
getFieldRow(pcsItem, fieldName) {
const key = String(fieldName || "").trim();
const map = pcsItem?._fieldRowMap || {};
return map[key] || null;
},
handlePcsFieldClick(pcsItem, fieldName, title) {
const row = this.getFieldRow(pcsItem, fieldName);
const pointId = String(row?.dataPoint || "").trim();
this.openCurveDialogByPointId(pointId, title || fieldName);
},
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("当前时间范围暂无曲线数据");
}
},
handleTagClick(deviceId) {
this.selectedPcsId = deviceId || "";
@ -355,6 +571,14 @@ export default {
return index >= 0 ? fieldCode.slice(index + 2) : fieldCode;
},
getFieldMap(rows = [], deviceId = "") {
const rowMap = this.getFieldRowMap(rows, deviceId);
return Object.keys(rowMap).reduce((acc, fieldName) => {
const row = rowMap[fieldName] || {};
acc[fieldName] = row.fieldValue;
return acc;
}, {});
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = this.normalizeDeviceId(deviceId || "");
// 设备维度优先:先吃 device_id 对应值,再用默认值(空 device_id)补齐
@ -366,7 +590,7 @@ export default {
if (itemDeviceId !== targetDeviceId) {
return;
}
map[this.getFieldName(item.fieldCode)] = item.fieldValue;
map[this.getFieldName(item.fieldCode)] = item;
});
rows.forEach(item => {
if (!item || !item.fieldCode) {
@ -378,7 +602,7 @@ export default {
}
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item.fieldValue;
map[fieldName] = item;
}
});
return map;
@ -411,6 +635,10 @@ export default {
deviceId: device.deviceId || device.id || this.siteId,
deviceName: device.deviceName || device.name || device.deviceId || device.id || 'PCS',
...this.getFieldMap(this.getModuleRows('SBJK_PCS', '状态'), device.deviceId || device.id || this.siteId),
_fieldRowMap: {
...this.getFieldRowMap(this.getModuleRows('SBJK_PCS', '电参量'), device.deviceId || device.id || this.siteId),
...this.getFieldRowMap(this.getModuleRows('SBJK_PCS', '状态'), device.deviceId || device.id || this.siteId),
},
dataUpdateTime: this.getLatestTime('SBJK_PCS'),
alarmNum: 0,
pcsBranchInfoList: [],
@ -433,6 +661,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
<style lang="scss" scoped>
@ -449,6 +683,18 @@ export default {
cursor: pointer;
}
.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

@ -2,7 +2,7 @@
<template>
<div class="ssyx-ems-dashboard-editor-container">
<!-- 6个方块-->
<real-time-base-info :display-data="runningDisplayData" :loading="runningHeadLoading"/>
<real-time-base-info :display-data="runningDisplayData" :loading="runningHeadLoading" @field-click="handleHeadFieldClick"/>
<!-- 时间选择 -->
<date-range-select ref="dateRangeSelect" @updateDate="updateDate" style="margin-top:20px;"/>
<!-- echart图表-->
@ -22,13 +22,45 @@
<dcpjwd-chart ref="dcpjwd" :display-data="runningDisplayData"/>
</el-col>
</el-row>
<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>
</div>
</template>
<style scoped lang="scss">
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
</style>
<script>
import * as echarts from "echarts";
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue";
import CnglqxChart from './CnglqxChart.vue'
@ -37,6 +69,7 @@ import DcpjwdChart from './DcpjwdChart.vue'
import DcpjsocChart from './DcpjsocChart.vue'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getProjectDisplayData} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
export default {
@ -49,9 +82,172 @@ export default {
timeRange:[],
isInit:true,
runningHeadLoading: false,
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:{
handleHeadFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.title || item?.raw?.fieldName || pointId,
});
},
openCurveDialog({pointId, title}) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
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 pad = (num) => String(num).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(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("当前时间范围暂无曲线数据");
}
},
//6个方块数据
getRunningHeadData(){
this.runningHeadLoading = true

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'ylField'"
:span="8"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<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>
</div>
</template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkYl",
@ -68,6 +98,19 @@ export default {
displayData: [],
selectedSectionKey: "",
coolingDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
@ -121,6 +164,8 @@ export default {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
@ -165,6 +210,153 @@ export default {
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
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("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
@ -208,6 +400,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
@ -239,6 +437,26 @@ export default {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

@ -147,6 +147,9 @@ export default {
if(val){
this.search.clusterId=''
this.getClusterList()
} else {
this.search.clusterId=''
this.clusterOptions=[]
}
},
//表格数据
@ -170,8 +173,15 @@ export default {
})
},
async getClusterList(){
const currentStackId = String(this.search.stackId || '')
if (!currentStackId) {
this.clusterOptions = []
this.search.clusterId = ''
return
}
this.clusterloading =true
await getClusterNameList({stackDeviceId: this.search.stackId, siteId: this.siteId}).then(response => {
if (String(this.search.stackId || '') !== currentStackId) return
const data = JSON.parse(JSON.stringify(response?.data || []))
this.clusterOptions = data
this.search.clusterId = data.length > 0 ? data[0].id : ''
@ -200,4 +210,3 @@ export default {
}
}
</script>