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

View File

@ -74,8 +74,12 @@
<div slot="header"> <div slot="header">
<span class="card-title">总累计运行数据</span> <span class="card-title">总累计运行数据</span>
<div class="total-count"> <div class="total-count">
<span class="title">总收入</span> <span class="title pointer-field" @click="handleTotalRevenueClick">总收入</span>
<span class="value"> <span
class="value pointer-field"
:class="{ 'field-disabled': !hasPointId(totalRevenueDisplayItem) }"
@click="handleTotalRevenueClick"
>
<i v-if="isRunningInfoLoading" class="el-icon-loading"></i> <i v-if="isRunningInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ totalRevenueDisplayValue | formatNumber }}</span> <span v-else>{{ totalRevenueDisplayValue | formatNumber }}</span>
</span> </span>
@ -92,7 +96,11 @@
:key="index + 'sjglData'" :key="index + 'sjglData'"
class="sjgl-col" 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-title">{{ item.title }}</div>
<div class="sjgl-value" :style="{color:item.color}"> <div class="sjgl-value" :style="{color:item.color}">
<i v-if="item.loading" class="el-icon-loading"></i> <i v-if="item.loading" class="el-icon-loading"></i>
@ -111,12 +119,40 @@
<active-chart ref="activeChart" :display-data="runningDisplayData"/> <active-chart ref="activeChart" :display-data="runningDisplayData"/>
</el-col> </el-col>
</el-row> </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> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts";
import {getSingleSiteBaseInfo} from "@/api/ems/zddt"; import {getSingleSiteBaseInfo} from "@/api/ems/zddt";
import {getDzjkHomeView, getProjectDisplayData} from "@/api/ems/dzjk"; import {getDzjkHomeView, getProjectDisplayData} from "@/api/ems/dzjk";
import {getPointConfigCurve} from "@/api/ems/site";
import WeekChart from "./WeekChart.vue"; import WeekChart from "./WeekChart.vue";
import ActiveChart from "./ActiveChart.vue"; import ActiveChart from "./ActiveChart.vue";
import AlarmTable from "./AlarmTable.vue"; import AlarmTable from "./AlarmTable.vue";
@ -135,6 +171,19 @@ export default {
runningInfoLoading: false, runningInfoLoading: false,
runningUpdateSpinning: false, runningUpdateSpinning: false,
runningUpdateTimer: null, runningUpdateTimer: null,
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
fallbackSjglData: [ fallbackSjglData: [
{ {
title: "今日充电量kWh", title: "今日充电量kWh",
@ -218,6 +267,7 @@ export default {
value: item.fieldValue, value: item.fieldValue,
color: this.getCardColor(index), color: this.getCardColor(index),
loading: this.isRunningInfoLoading, loading: this.isRunningInfoLoading,
raw: item,
})); }));
} }
return this.fallbackSjglData.map(item => ({ return this.fallbackSjglData.map(item => ({
@ -225,6 +275,7 @@ export default {
value: this.runningInfo[item.attr], value: this.runningInfo[item.attr],
color: item.color, color: item.color,
loading: this.isRunningInfoLoading, loading: this.isRunningInfoLoading,
raw: item,
})); }));
}, },
}, },
@ -233,8 +284,172 @@ export default {
clearTimeout(this.runningUpdateTimer); clearTimeout(this.runningUpdateTimer);
this.runningUpdateTimer = null; this.runningUpdateTimer = null;
} }
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
}, },
methods: { 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) { setBaseInfoLoading(loading) {
if (Object.prototype.hasOwnProperty.call(this.$data, "baseInfoLoading")) { if (Object.prototype.hasOwnProperty.call(this.$data, "baseInfoLoading")) {
this.baseInfoLoading = loading; 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 { .row-container {
& > .el-col { & > .el-col {
margin-bottom: 20px; margin-bottom: 20px;

View File

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

View File

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

View File

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

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'dbField'" :key="dataIndex + 'dbField'"
:span="8" :span="8"
class="device-info-col" class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
> >
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span> <span class="left">{{ item.fieldName }}</span>
<span class="right"> <span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i> <i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span> <span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span> </span>
</div>
</el-col> </el-col>
</el-row> </el-row>
</el-card> </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> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate"; import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk"; import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site"; import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default { export default {
name: "DzjkSbjkDb", name: "DzjkSbjkDb",
@ -68,6 +98,19 @@ export default {
displayData: [], displayData: [],
selectedSectionKey: "", selectedSectionKey: "",
ammeterDeviceList: [], ammeterDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
}; };
}, },
computed: { computed: {
@ -122,6 +165,8 @@ export default {
fieldName, fieldName,
fieldValue: row.fieldValue, fieldValue: row.fieldValue,
valueTime: row.valueTime, valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
}; };
}); });
@ -175,6 +220,153 @@ export default {
}, },
}, },
methods: { 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) { handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || ""; this.selectedSectionKey = sectionKey || "";
}, },
@ -234,6 +426,12 @@ export default {
this.updateInterval(this.updateData); this.updateInterval(this.updateData);
}, },
}, },
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}; };
</script> </script>
@ -265,6 +463,26 @@ export default {
cursor: pointer; 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 { .point-loading-icon {
color: #409eff; color: #409eff;
display: inline-block; display: inline-block;

View File

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

View File

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

View File

@ -88,7 +88,6 @@
:totalSize="totalSize" :totalSize="totalSize"
:pointIdList="pointIdList" :pointIdList="pointIdList"
@chart="chartDetail" @chart="chartDetail"
@pointDetail="pointDetail"
></component> ></component>
<el-pagination <el-pagination
v-show="tableData.length > 0" v-show="tableData.length > 0"
@ -104,34 +103,55 @@
> >
</el-pagination> </el-pagination>
<chart-detail ref="chartDetail" /> <chart-detail ref="chartDetail" />
<point-chart ref="pointChart" :site-id="siteId" /> <el-dialog
<point-table ref="pointTable"/> :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> </el-card>
</template> </template>
<script> <script>
import * as echarts from "echarts";
import BarChart from "./BarChart"; import BarChart from "./BarChart";
import { import {
getStackNameList,
getClusterNameList,
getClusterDataInfoList, getClusterDataInfoList,
getClusterNameList,
getStackNameList,
} from "@/api/ems/dzjk"; } from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import ChartDetail from "./ChartDetail.vue"; import ChartDetail from "./ChartDetail.vue";
import Table from "./Table.vue"; import Table from "./Table.vue";
import List from "./List.vue"; import List from "./List.vue";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
export default { export default {
name: "DzjkSbjkDtdc", name: "DzjkSbjkDtdc",
mixins: [getQuerySiteId], mixins: [getQuerySiteId],
components: { components: {
PointTable,
BarChart, BarChart,
ChartDetail, ChartDetail,
DtdcTable: Table, DtdcTable: Table,
DtdcList: List, DtdcList: List,
pointChart,
}, },
computed: { computed: {
pointIdList() { pointIdList() {
@ -166,27 +186,190 @@ export default {
pageNum: 1, //分页栏当前页数 pageNum: 1, //分页栏当前页数
totalSize: 0, //table表格数据总数 totalSize: 0, //table表格数据总数
activeBtn: "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: { methods: {
// 查看设备电位表格 getFieldPointConfig(fieldKey) {
pointDetail(row,dataType){ const pointMap = {
const {deviceId,clusterDeviceId} = row voltage: { pointIdKey: "voltagePointId", title: "电压 (V)" },
this.$refs.pointTable.showTable({siteId:this.siteId,deviceId,deviceCategory:'BATTERY',parentId:clusterDeviceId},dataType) 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) { changeMenu(menu) {
const { activeBtn } = this; const { activeBtn } = this;
activeBtn !== menu && (this.activeBtn = menu); activeBtn !== menu && (this.activeBtn = menu);
}, },
//查看表格行图表 //查看表格行图表
chartDetail({ deviceId, clusterDeviceId, dataType = "" }) { chartDetail(row = {}) {
dataType && const config = this.getFieldPointConfig(row.fieldKey);
this.$refs.pointChart.showChart({ if (!config) return;
pointName: dataType, const pointId = row[config.pointIdKey];
deviceCategory:'BATTERY', this.openCurveDialogByPointId(pointId, config.title);
deviceId: clusterDeviceId,
child: [deviceId],
});
}, },
// 分页 // 分页
handleSizeChange(val) { handleSizeChange(val) {
@ -224,6 +407,9 @@ export default {
); );
this.search.clusterId = ""; this.search.clusterId = "";
this.getClusterList(); this.getClusterList();
} else {
this.search.clusterId = "";
this.clusterOptions = [];
} }
}, },
//表格数据 //表格数据
@ -254,19 +440,26 @@ export default {
}, },
getStackList() { getStackList() {
getStackNameList(this.siteId).then((response) => { 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() { getClusterList() {
const { stackId } = this.search;
if (!stackId) {
this.clusterOptions = [];
return Promise.resolve();
}
this.clusterloading = true; this.clusterloading = true;
getClusterNameList({ const currentStackId = String(stackId);
stackDeviceId: this.search.stackId, return getClusterNameList({
stackDeviceId: stackId,
siteId: this.siteId, siteId: this.siteId,
}) })
.then((response) => { .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(() => { .finally(() => {
this.clusterloading = false; this.clusterloading = false;

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'emsField'" :key="dataIndex + 'emsField'"
:span="6" :span="6"
class="device-info-col" class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
> >
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span> <span class="left">{{ item.fieldName }}</span>
<span class="right"> <span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i> <i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span> <span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span> </span>
</div>
</el-col> </el-col>
</el-row> </el-row>
</el-card> </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> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate"; import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk"; import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site"; import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default { export default {
name: "DzjkSbjkEms", name: "DzjkSbjkEms",
@ -68,6 +98,19 @@ export default {
displayData: [], displayData: [],
selectedSectionKey: "", selectedSectionKey: "",
emsDeviceList: [], emsDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
}; };
}, },
computed: { computed: {
@ -122,6 +165,8 @@ export default {
fieldName, fieldName,
fieldValue: row.fieldValue, fieldValue: row.fieldValue,
valueTime: row.valueTime, valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
}; };
}); });
@ -175,6 +220,153 @@ export default {
}, },
}, },
methods: { 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) { handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || ""; this.selectedSectionKey = sectionKey || "";
}, },
@ -218,6 +410,12 @@ export default {
this.updateInterval(this.updateData); this.updateInterval(this.updateData);
}, },
}, },
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}; };
</script> </script>
@ -249,6 +447,26 @@ export default {
cursor: pointer; 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 { .point-loading-icon {
color: #409eff; color: #409eff;
display: inline-block; display: inline-block;

View File

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

View File

@ -2,7 +2,7 @@
<template> <template>
<div class="ssyx-ems-dashboard-editor-container"> <div class="ssyx-ems-dashboard-editor-container">
<!-- 6个方块--> <!-- 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;"/> <date-range-select ref="dateRangeSelect" @updateDate="updateDate" style="margin-top:20px;"/>
<!-- echart图表--> <!-- echart图表-->
@ -22,13 +22,45 @@
<dcpjwd-chart ref="dcpjwd" :display-data="runningDisplayData"/> <dcpjwd-chart ref="dcpjwd" :display-data="runningDisplayData"/>
</el-col> </el-col>
</el-row> </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> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
</style> </style>
<script> <script>
import * as echarts from "echarts";
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue' import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue"; import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue";
import CnglqxChart from './CnglqxChart.vue' import CnglqxChart from './CnglqxChart.vue'
@ -37,6 +69,7 @@ import DcpjwdChart from './DcpjwdChart.vue'
import DcpjsocChart from './DcpjsocChart.vue' import DcpjsocChart from './DcpjsocChart.vue'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getProjectDisplayData} from '@/api/ems/dzjk' import {getProjectDisplayData} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import intervalUpdate from "@/mixins/ems/intervalUpdate"; import intervalUpdate from "@/mixins/ems/intervalUpdate";
export default { export default {
@ -49,9 +82,172 @@ export default {
timeRange:[], timeRange:[],
isInit:true, isInit:true,
runningHeadLoading: false, 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:{ 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个方块数据 //6个方块数据
getRunningHeadData(){ getRunningHeadData(){
this.runningHeadLoading = true this.runningHeadLoading = true

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'ylField'" :key="dataIndex + 'ylField'"
:span="8" :span="8"
class="device-info-col" class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
> >
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span> <span class="left">{{ item.fieldName }}</span>
<span class="right"> <span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i> <i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span> <span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span> </span>
</div>
</el-col> </el-col>
</el-row> </el-row>
</el-card> </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> </div>
</template> </template>
<script> <script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId"; import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate"; import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk"; import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site"; import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default { export default {
name: "DzjkSbjkYl", name: "DzjkSbjkYl",
@ -68,6 +98,19 @@ export default {
displayData: [], displayData: [],
selectedSectionKey: "", selectedSectionKey: "",
coolingDeviceList: [], coolingDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
}; };
}, },
computed: { computed: {
@ -121,6 +164,8 @@ export default {
fieldName, fieldName,
fieldValue: row.fieldValue, fieldValue: row.fieldValue,
valueTime: row.valueTime, valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
}; };
}); });
@ -165,6 +210,153 @@ export default {
}, },
}, },
methods: { 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) { handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || ""; this.selectedSectionKey = sectionKey || "";
}, },
@ -208,6 +400,12 @@ export default {
this.updateInterval(this.updateData); this.updateInterval(this.updateData);
}, },
}, },
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}; };
</script> </script>
@ -239,6 +437,26 @@ export default {
cursor: pointer; 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 { .point-loading-icon {
color: #409eff; color: #409eff;
display: inline-block; display: inline-block;

View File

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

View File

@ -1,10 +1,10 @@
<template> <template>
<div <div class="ems-dashboard-editor-container" style="background-color: #ffffff">
v-loading="loading"
class="ems-dashboard-editor-container"
style="background-color: #ffffff"
>
<el-form ref="form" :model="form" label-position="top"> <el-form ref="form" :model="form" label-position="top">
<div class="query-groups-toolbar">
<span class="query-groups-count">当前 {{ form.queryGroups.length }} 个点位</span>
<el-button type="primary" size="mini" plain @click="addQueryGroup">新增点位</el-button>
</div>
<div class="query-groups-row"> <div class="query-groups-row">
<div <div
v-for="(group, index) in form.queryGroups" v-for="(group, index) in form.queryGroups"
@ -54,6 +54,14 @@
> >
清空选择 清空选择
</el-button> </el-button>
<el-button
type="text"
size="mini"
:disabled="form.queryGroups.length <= 1"
@click="removeQueryGroup(index)"
>
删除点位
</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -124,17 +132,20 @@ export default {
queryGroups: [], queryGroups: [],
dataUnit: 1, dataUnit: 1,
}, },
loading: false,
debouncedPointSearchMap: {}, debouncedPointSearchMap: {},
queryGroupSeed: 0,
}; };
}, },
created() { created() {
this.form.queryGroups = [1, 2, 3, 4, 5].map((index) => this.createEmptyQueryGroup(index)); this.form.queryGroups = [];
for (let i = 0; i < 5; i += 1) {
this.addQueryGroup();
}
}, },
methods: { methods: {
createEmptyQueryGroup(index) { createEmptyQueryGroup(key) {
return { return {
key: index, key,
pointId: "", pointId: "",
selectedPointName: "", selectedPointName: "",
pointOptions: [], pointOptions: [],
@ -143,6 +154,18 @@ export default {
pointLoading: false, pointLoading: false,
}; };
}, },
addQueryGroup() {
this.queryGroupSeed += 1;
this.form.queryGroups.push(this.createEmptyQueryGroup(this.queryGroupSeed));
},
removeQueryGroup(groupIndex) {
if (this.form.queryGroups.length <= 1) {
this.$message.warning("至少保留1个点位");
return;
}
this.form.queryGroups.splice(groupIndex, 1);
this.debouncedPointSearchMap = {};
},
getQueryGroup(index) { getQueryGroup(index) {
return this.form.queryGroups[index]; return this.form.queryGroups[index];
}, },
@ -494,8 +517,6 @@ export default {
endDate = end + " 00:00:00"; endDate = end + " 00:00:00";
} }
this.loading = true;
const selectedPoints = []; const selectedPoints = [];
const pointIdSet = new Set(); const pointIdSet = new Set();
activeGroups.forEach(({group}) => { activeGroups.forEach(({group}) => {
@ -525,8 +546,6 @@ export default {
return; return;
} }
this.$message.error("查询失败请稍后重试"); this.$message.error("查询失败请稍后重试");
}).finally(() => {
this.loading = false;
}); });
}); });
}, },
@ -539,12 +558,10 @@ export default {
this.chart = null; this.chart = null;
}, },
mounted() { mounted() {
this.loading = true;
this.$nextTick(() => { this.$nextTick(() => {
this.initChart(); this.initChart();
this.$refs.dateTimeSelect.init(); this.$refs.dateTimeSelect.init();
this.syncQuerySiteIds(); this.syncQuerySiteIds();
this.loading = false;
}); });
}, },
}; };
@ -554,18 +571,32 @@ export default {
.query-groups-row { .query-groups-row {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; flex-wrap: wrap;
gap: 8px; gap: 8px;
} }
.query-group { .query-group {
width: 19.2%; flex: 0 0 calc(20% - 8px);
max-width: calc(20% - 8px);
} }
.group-point-item { .group-point-item {
margin-right: 0; margin-right: 0;
} }
.query-groups-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
gap: 10px;
}
.query-groups-count {
color: #909399;
font-size: 12px;
}
.point-select-wrapper { .point-select-wrapper {
width: 100%; width: 100%;
} }
@ -588,13 +619,15 @@ export default {
@media (max-width: 1600px) { @media (max-width: 1600px) {
.query-group { .query-group {
width: 32%; flex-basis: calc(33.3% - 8px);
max-width: calc(33.3% - 8px);
} }
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.query-group { .query-group {
width: 49%; flex-basis: calc(50% - 8px);
max-width: calc(50% - 8px);
} }
} }
@ -603,7 +636,8 @@ export default {
gap: 0; gap: 0;
} }
.query-group { .query-group {
width: 100%; flex-basis: 100%;
max-width: 100%;
} }
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<el-dialog <el-dialog
v-loading="loading" v-loading="loading"
width="92%" width="42%"
:visible.sync="dialogTableVisible" :visible.sync="dialogTableVisible"
class="ems-dialog plan-dialog" class="ems-dialog plan-dialog"
title="保护方案" title="保护方案"
@ -18,7 +18,7 @@
class="plan-form" class="plan-form"
> >
<div class="base-panel"> <div class="base-panel">
<div class="panel-title">基础信息</div> <div class="card-legend">基础信息</div>
<div class="base-grid"> <div class="base-grid">
<el-form-item label="设备保护名称" prop="faultName" class="span-1"> <el-form-item label="设备保护名称" prop="faultName" class="span-1">
<el-input v-model="formData.faultName" placeholder="请输入" clearable></el-input> <el-input v-model="formData.faultName" placeholder="请输入" clearable></el-input>
@ -26,7 +26,7 @@
<el-form-item label="是否告警" prop="isAlert" class="span-1"> <el-form-item label="是否告警" prop="isAlert" class="span-1">
<el-checkbox v-model="formData.isAlert" :true-label="1" :false-label="0">启用告警</el-checkbox> <el-checkbox v-model="formData.isAlert" :true-label="1" :false-label="0">启用告警</el-checkbox>
</el-form-item> </el-form-item>
<el-form-item label="告警等级" prop="faultLevel" class="span-1"> <el-form-item label="告警等级" prop="faultLevel" class="span-2 level-row">
<el-radio-group v-model="formData.faultLevel" :disabled="mode === 'edit'"> <el-radio-group v-model="formData.faultLevel" :disabled="mode === 'edit'">
<el-radio :label="1">等级1</el-radio> <el-radio :label="1">等级1</el-radio>
<el-radio :label="2">等级2</el-radio> <el-radio :label="2">等级2</el-radio>
@ -46,34 +46,43 @@
</div> </div>
<div class="plan-section"> <div class="plan-section">
<div class="card-legend">故障保护</div>
<div class="section-head"> <div class="section-head">
<div> <div>
<div class="section-title">保护前提</div>
<div class="section-desc">配置触发故障的判定条件和关系</div> <div class="section-desc">配置触发故障的判定条件和关系</div>
</div> </div>
<div class="section-actions"> <div class="section-actions">
<el-button @click.native.prevent="addRow('protectionSettings')" type="primary" size="mini"> <el-button @click.native.prevent="addRow('faultProtectionSettings')" type="primary" size="mini">
新增保护前提 新增故障保护
</el-button> </el-button>
</div> </div>
</div> </div>
<div class="row-card" v-for="(item, index) in protectionSettings" :key="'protectionSettings' + index"> <div class="row-card" v-for="(item, index) in faultProtectionSettings" :key="'faultProtectionSettings' + index">
<div class="row-index">{{ index + 1 }}</div> <div class="row-index">{{ index + 1 }}</div>
<div class="row-grid setting-grid"> <div class="row-grid trigger-grid">
<div class="field-block field-point"> <div class="field-block field-point">
<div class="field-label">点位</div> <div class="field-label">点位</div>
<el-autocomplete <el-select
v-model="item.point" v-model="item.point"
placeholder="请输入点位ID/描述" filterable
remote
clearable clearable
:trigger-on-focus="false" reserve-keyword
:debounce="250" placeholder="支持关键字搜索,展开可查看点位列表"
:value-key="'label'" :no-data-text="'暂无匹配点位'"
:fetch-suggestions="(q, c) => querySearchAsync(q, c, index, 'protectionSettings')" :loading="item.pointLoading"
@select="(v) => handleSelect(v, index, 'protectionSettings')" @change="(value) => handlePointChange(index, 'faultProtectionSettings', value)"
@blur="() => fillPointMetaByPoint(item.point, index, 'protectionSettings')" @visible-change="(visible) => handlePointDropdownVisible(index, 'faultProtectionSettings', visible)"
></el-autocomplete> :remote-method="(query) => remotePointSearch(index, 'faultProtectionSettings', query)"
>
<el-option
v-for="pointItem in item.pointOptions"
:key="'fault-point-' + index + '-' + pointItem.value"
:label="pointItem.label"
:value="pointItem.value"
/>
</el-select>
</div> </div>
<div class="field-block"> <div class="field-block">
@ -93,6 +102,68 @@
<el-input placeholder="请输入故障值" v-model="item.faultValue"></el-input> <el-input placeholder="请输入故障值" v-model="item.faultValue"></el-input>
</div> </div>
<div class="field-block">
<div class="field-label">关系</div>
<el-select v-model="item.relationNext" placeholder="请选择">
<el-option
v-for="(value, key) in relationWithPoint"
:key="key + 'relation'"
:label="key"
:value="value"
></el-option>
</el-select>
</div>
</div>
<div class="row-action">
<el-button @click.native.prevent="deleteRow(index, 'faultProtectionSettings')" type="warning" size="mini">
删除
</el-button>
</div>
</div>
<el-empty v-if="faultProtectionSettings.length === 0" description="暂无故障保护请先添加"></el-empty>
</div>
<div class="plan-section">
<div class="card-legend">释放保护</div>
<div class="section-head">
<div>
<div class="section-desc">配置故障释放的判定条件和关系</div>
</div>
<div class="section-actions">
<el-button @click.native.prevent="addRow('releaseProtectionSettings')" type="primary" size="mini">
新增释放保护
</el-button>
</div>
</div>
<div class="row-card" v-for="(item, index) in releaseProtectionSettings" :key="'releaseProtectionSettings' + index">
<div class="row-index">{{ index + 1 }}</div>
<div class="row-grid trigger-grid">
<div class="field-block field-point">
<div class="field-label">点位</div>
<el-select
v-model="item.point"
filterable
remote
clearable
reserve-keyword
placeholder="支持关键字搜索展开可查看点位列表"
:no-data-text="'暂无匹配点位'"
:loading="item.pointLoading"
@change="(value) => handlePointChange(index, 'releaseProtectionSettings', value)"
@visible-change="(visible) => handlePointDropdownVisible(index, 'releaseProtectionSettings', visible)"
:remote-method="(query) => remotePointSearch(index, 'releaseProtectionSettings', query)"
>
<el-option
v-for="pointItem in item.pointOptions"
:key="'release-point-' + index + '-' + pointItem.value"
:label="pointItem.label"
:value="pointItem.value"
/>
</el-select>
</div>
<div class="field-block"> <div class="field-block">
<div class="field-label">释放比较符</div> <div class="field-label">释放比较符</div>
<el-select v-model="item.releaseOperator" placeholder="请选择"> <el-select v-model="item.releaseOperator" placeholder="请选择">
@ -115,7 +186,7 @@
<el-select v-model="item.relationNext" placeholder="请选择"> <el-select v-model="item.relationNext" placeholder="请选择">
<el-option <el-option
v-for="(value, key) in relationWithPoint" v-for="(value, key) in relationWithPoint"
:key="key + 'relation'" :key="key + 'releaseRelation'"
:label="key" :label="key"
:value="value" :value="value"
></el-option> ></el-option>
@ -123,24 +194,24 @@
</div> </div>
</div> </div>
<div class="row-action"> <div class="row-action">
<el-button @click.native.prevent="deleteRow(index, 'protectionSettings')" type="warning" size="mini"> <el-button @click.native.prevent="deleteRow(index, 'releaseProtectionSettings')" type="warning" size="mini">
删除 删除
</el-button> </el-button>
</div> </div>
</div> </div>
<el-empty v-if="protectionSettings.length === 0" description="暂无保护前提请先添加"></el-empty> <el-empty v-if="releaseProtectionSettings.length === 0" description="暂无释放保护请先添加"></el-empty>
</div> </div>
<div class="plan-section"> <div class="plan-section">
<div class="card-legend">执行保护</div>
<div class="section-head"> <div class="section-head">
<div> <div>
<div class="section-title">保护方案</div> <div class="section-desc">设置触发后的执行动作(降功率、停机、禁充放等)</div>
<div class="section-desc">设置触发后的目标写入点位和值</div>
</div> </div>
<div class="section-actions"> <div class="section-actions">
<el-button @click.native.prevent="addRow('protectionPlan')" type="primary" size="mini"> <el-button @click.native.prevent="addRow('protectionPlan')" type="primary" size="mini">
新增保护方案 新增执行保护
</el-button> </el-button>
</div> </div>
</div> </div>
@ -148,29 +219,26 @@
<div class="row-card" v-for="(item, index) in protectionPlan" :key="'protectionPlan' + index"> <div class="row-card" v-for="(item, index) in protectionPlan" :key="'protectionPlan' + index">
<div class="row-index">{{ index + 1 }}</div> <div class="row-index">{{ index + 1 }}</div>
<div class="row-grid plan-grid"> <div class="row-grid plan-grid">
<div class="field-block field-point">
<div class="field-label">点位</div>
<el-autocomplete
v-model="item.point"
placeholder="请输入点位ID/描述"
clearable
:trigger-on-focus="false"
:debounce="250"
:value-key="'label'"
:fetch-suggestions="(q, c) => querySearchAsync(q, c, index, 'protectionPlan')"
@select="(v) => handleSelect(v, index, 'protectionPlan')"
@blur="() => fillPointMetaByPoint(item.point, index, 'protectionPlan')"
></el-autocomplete>
</div>
<div class="field-block eq-block">
<div class="field-label">比较</div>
<div class="eq-text">=</div>
</div>
<div class="field-block"> <div class="field-block">
<div class="field-label">故障值</div> <div class="field-label">执行动作</div>
<el-input placeholder="请输入故障值" v-model="item.value"></el-input> <el-select
v-model="item.action"
clearable
placeholder="请选择执行动作"
@change="(value) => handleProtectionActionChange(index, value)"
>
<el-option
v-for="opt in protectionCapabilityOptions"
:key="'plan-action-' + index + '-' + opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</div>
<div class="field-block" v-if="isDerateAction(item.action)">
<div class="field-label">降功率比例(%)</div>
<el-input placeholder="请输入0-100" v-model="item.value"></el-input>
</div> </div>
</div> </div>
<div class="row-action"> <div class="row-action">
@ -180,7 +248,7 @@
</div> </div>
</div> </div>
<el-empty v-if="protectionPlan.length === 0" description="暂无保护方案请先添加"></el-empty> <el-empty v-if="protectionPlan.length === 0" description="暂无执行保护请先添加"></el-empty>
</div> </div>
</el-form> </el-form>
@ -208,8 +276,19 @@ export default {
return { return {
mode: "", mode: "",
loading: 0, loading: 0,
protectionSettings: [], faultProtectionSettings: [],
releaseProtectionSettings: [],
protectionPlan: [], protectionPlan: [],
protectionCapabilityOptions: [
{ label: "降功率", value: "derate", requireValue: true },
{ label: "关机/停机/切断", value: "shutdown", requireValue: false },
{ label: "禁止充电", value: "forbid_charge", requireValue: false },
{ label: "允许放电", value: "allow_discharge", requireValue: false },
{ label: "禁止放电", value: "forbid_discharge", requireValue: false },
{ label: "允许充电", value: "allow_charge", requireValue: false },
{ label: "禁止充放电", value: "forbid_charge_discharge", requireValue: false },
{ label: "待机", value: "standby", requireValue: false },
],
dialogTableVisible: false, dialogTableVisible: false,
formData: { formData: {
id: "", id: "",
@ -257,140 +336,313 @@ export default {
releaseDelaySeconds: data?.releaseDelaySeconds || "", releaseDelaySeconds: data?.releaseDelaySeconds || "",
description: data?.description || "", description: data?.description || "",
}; };
const plan = JSON.parse(data?.protectionPlan || "[]"); const planRaw = JSON.parse(data?.protectionPlan || "[]");
const settings = JSON.parse(data?.protectionSettings || "[]"); const plan = Array.isArray(planRaw) ? planRaw : planRaw ? [planRaw] : [];
const settingsRaw = JSON.parse(data?.protectionSettings || "[]");
const settings =
Array.isArray(settingsRaw)
? {
faultSettings: settingsRaw.map((item) => Object.assign({}, item)),
releaseSettings: settingsRaw.map((item) => Object.assign({}, item)),
}
: {
faultSettings: Array.isArray(settingsRaw?.faultSettings) ? settingsRaw.faultSettings : [],
releaseSettings: Array.isArray(settingsRaw?.releaseSettings) ? settingsRaw.releaseSettings : [],
};
this.$nextTick(() => { this.$nextTick(() => {
this.protectionPlan.splice(0, 0, ...plan); this.protectionPlan.splice(0, 0, ...plan.map((item) => this.normalizeProtectionPlanItem(item)));
this.protectionSettings.splice(0, 0, ...settings); this.faultProtectionSettings.splice(0, 0, ...settings.faultSettings.map((item) => this.enhancePointRow(item)));
this.releaseProtectionSettings.splice(
0,
0,
...settings.releaseSettings.map((item) => this.enhancePointRow(item))
);
}); });
}); });
} else { } else {
this.mode = "add"; this.mode = "add";
} }
}, },
formatPointLabel(item = {}) {
const pointId = item?.pointId || "";
const pointName = item?.pointName || "";
const pointDesc = item?.pointDesc || "";
return `${pointId || "-"}-${pointName || "-"}(${pointDesc || "-"})`;
},
normalizePointOptions(data = []) {
return (data || []).map((item) => {
const pointId = item?.pointId || item?.point || item?.value || "";
const pointName = item?.pointName || pointId || "";
const pointDesc = item?.pointDesc || "";
return {
value: pointId,
label: this.formatPointLabel({ pointId, pointName, pointDesc }),
pointId,
pointName,
pointDesc,
deviceId: item?.deviceId || "",
deviceName: item?.deviceName || "",
deviceCategory: item?.deviceCategory || "",
categoryName: item?.categoryName || item?.deviceCategory || "",
};
});
},
enhancePointRow(row = {}) {
const nextRow = Object.assign(
{
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
pointOptions: [],
pointOptionsCache: {},
pointRequestId: 0,
pointLoading: false,
},
row || {}
);
const selectedPointId = String(nextRow.point || "").trim();
if (selectedPointId) {
const hasSelected = (nextRow.pointOptions || []).some((item) => item?.value === selectedPointId);
if (!hasSelected) {
const selectedOption = this.normalizePointOptions([
{
pointId: selectedPointId,
pointName: nextRow.pointName || selectedPointId,
pointDesc: "",
deviceId: nextRow.deviceId || "",
deviceName: nextRow.deviceName || "",
deviceCategory: nextRow.deviceCategory || "",
categoryName: nextRow.categoryName || "",
},
])[0];
nextRow.pointOptions = [...(nextRow.pointOptions || []), selectedOption];
}
}
return nextRow;
},
getRow(type, index) {
return (this[type] || [])[index];
},
setRow(type, index, row) {
this[type].splice(index, 1, row);
},
addRow(type) { addRow(type) {
const item = const item =
type === "protectionSettings" type === "protectionPlan"
? { ? this.normalizeProtectionPlanItem({})
deviceId: "", : this.enhancePointRow(this.createProtectionSettingItem(type));
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
faultValue: "",
releaseValue: "",
faultOperator: "",
releaseOperator: "",
relationNext: "",
}
: {
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
value: "",
};
this[type].splice(this[type].length, 0, item); this[type].splice(this[type].length, 0, item);
}, },
getCapabilityLabel(action) {
const target = (this.protectionCapabilityOptions || []).find((item) => item.value === action);
return target?.label || "";
},
isDerateAction(action) {
return action === "derate";
},
inferProtectionAction(raw = {}) {
const marker = `${raw?.actionName || ""} ${raw?.action || ""} ${raw?.pointName || ""} ${raw?.point || ""}`.toLowerCase();
const mapping = [
{ value: "forbid_charge_discharge", markers: ["禁止充放电", "forbid_charge_discharge", "disable_charge_discharge"] },
{ value: "forbid_charge", markers: ["禁止充电", "forbid_charge", "disable_charge"] },
{ value: "allow_discharge", markers: ["允许放电", "allow_discharge"] },
{ value: "forbid_discharge", markers: ["禁止放电", "forbid_discharge", "disable_discharge"] },
{ value: "allow_charge", markers: ["允许充电", "allow_charge"] },
{ value: "shutdown", markers: ["关机", "停机", "切断", "shutdown", "stop"] },
{ value: "standby", markers: ["待机", "standby"] },
{ value: "derate", markers: ["降功率", "derate", "power_limit", "powerlimit"] },
];
for (let i = 0; i < mapping.length; i++) {
const item = mapping[i];
if (item.markers.some((mk) => marker.includes(mk))) {
return item.value;
}
}
return "";
},
normalizeDerateValue(rawValue) {
if (rawValue === null || rawValue === undefined || rawValue === "") return "";
const num = Number(rawValue);
if (Number.isNaN(num)) return rawValue;
if (num > 0 && num <= 1) {
return String((num * 100).toFixed(2)).replace(/\.?0+$/, "");
}
return String(rawValue);
},
normalizeProtectionPlanItem(raw = {}) {
const action = raw?.action || this.inferProtectionAction(raw);
const label = raw?.actionName || this.getCapabilityLabel(action);
const value = this.isDerateAction(action) ? this.normalizeDerateValue(raw?.value) : "";
return {
action,
actionName: label,
point: raw?.point || action || "",
pointName: raw?.pointName || label || "",
value,
};
},
handleProtectionActionChange(index, action) {
const row = this.getRow("protectionPlan", index);
if (!row) return;
const label = this.getCapabilityLabel(action);
this.setRow(
"protectionPlan",
index,
Object.assign({}, row, {
action: action || "",
actionName: label || "",
point: action || "",
pointName: label || "",
value: this.isDerateAction(action) ? row.value : "",
})
);
},
createProtectionSettingItem(type) {
const isFault = type === "faultProtectionSettings";
return this.enhancePointRow({
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
faultValue: isFault ? "" : null,
releaseValue: isFault ? null : "",
faultOperator: isFault ? "" : null,
releaseOperator: isFault ? null : "",
relationNext: "",
});
},
deleteRow(index, type) { deleteRow(index, type) {
this[type].splice(index, 1); this[type].splice(index, 1);
}, },
querySearchAsync(query, cb, index, type) { setPointOptions(index, type, data = []) {
if (!this.formData.siteId) { const row = this.getRow(type, index);
this.$message({ if (!row) return;
type: "warning", const normalized = this.normalizePointOptions(data);
message: "请先选择站点", const selectedPoint = row.point
? this.normalizePointOptions([
{
pointId: row.point,
pointName: row.pointName || row.point,
pointDesc: "",
deviceId: row.deviceId || "",
deviceName: row.deviceName || "",
deviceCategory: row.deviceCategory || "",
categoryName: row.categoryName || "",
},
])
: [];
const merged = [...normalized, ...(row.pointOptions || []), ...selectedPoint];
const optionMap = {};
const uniqueOptions = merged.filter((item) => {
if (!item?.value || optionMap[item.value]) return false;
optionMap[item.value] = true;
return true;
}); });
return cb([]); this.setRow(type, index, Object.assign({}, row, { pointOptions: uniqueOptions }));
},
fetchPointOptions(index, type, query = "", { force = false } = {}) {
const row = this.getRow(type, index);
if (!row || !this.formData.siteId) return Promise.resolve([]);
const normalizedQuery = String(query || "").trim();
const cacheKey = `${this.formData.siteId}_${normalizedQuery}`;
if (!force && row.pointOptionsCache?.[cacheKey]) {
this.setPointOptions(index, type, row.pointOptionsCache[cacheKey]);
return Promise.resolve(row.pointOptionsCache[cacheKey]);
} }
getPointMatchList({ const requestId = Number(row.pointRequestId || 0) + 1;
this.setRow(type, index, Object.assign({}, row, { pointRequestId: requestId, pointLoading: true }));
return getPointMatchList({
siteId: this.formData.siteId, siteId: this.formData.siteId,
pageNum: 1, pageNum: 1,
pageSize: 100, pageSize: 100,
pointId: query || "", pointId: normalizedQuery,
pointDesc: query || "", pointDesc: normalizedQuery,
}) })
.then((response) => { .then((response) => {
const data = response?.rows || []; const latestRow = this.getRow(type, index);
cb( if (!latestRow || latestRow.pointRequestId !== requestId) return [];
data.map((item) => { const result = response?.rows || [];
const pointLabel = item.pointId || item.pointName || item.dataKey || ""; const cache = Object.assign({}, latestRow.pointOptionsCache || {}, { [cacheKey]: result });
const desc = item.pointDesc || ""; this.setRow(type, index, Object.assign({}, latestRow, { pointOptionsCache: cache }));
return { this.setPointOptions(index, type, result);
value: pointLabel, return result;
pointName: item.pointName || pointLabel, })
pointId: item.pointId || pointLabel, .finally(() => {
pointDesc: desc, const latestRow = this.getRow(type, index);
deviceId: item.deviceId || "", if (!latestRow || latestRow.pointRequestId !== requestId) return;
deviceName: item.deviceName || "", this.setRow(type, index, Object.assign({}, latestRow, { pointLoading: false }));
deviceCategory: item.deviceCategory || "", });
categoryName: item.deviceCategory || "", },
label: desc ? `${pointLabel}${desc}` : pointLabel, remotePointSearch(index, type, query) {
}; this.fetchPointOptions(index, type, query);
},
handlePointDropdownVisible(index, type, visible) {
if (!visible) return;
this.fetchPointOptions(index, type, "");
},
handlePointChange(index, type, value) {
const row = this.getRow(type, index);
if (!row) return;
if (!value) {
this.setRow(
type,
index,
Object.assign({}, row, {
point: "",
pointName: "",
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
}) })
); );
})
.catch(() => cb([]));
},
fillPointMetaByPoint(point, index, type) {
if (!this.formData.siteId || !point) {
return; return;
} }
getPointMatchList({ const selectedOption = (row.pointOptions || []).find((item) => item?.value === value) || {};
siteId: this.formData.siteId, this.setRow(
pageNum: 1, type,
pageSize: 1, index,
pointId: point, Object.assign({}, row, {
}).then((response) => { point: selectedOption.pointId || selectedOption.value || value,
const row = (response?.rows || [])[0]; pointName: selectedOption.pointName || selectedOption.pointId || selectedOption.value || value,
if (!row) return; deviceId: selectedOption.deviceId || "",
const line = Object.assign({}, this[type][index], { deviceName: selectedOption.deviceName || "",
point: row.pointId || point, deviceCategory: selectedOption.deviceCategory || "",
pointName: row.pointName || row.pointId || point, categoryName: selectedOption.categoryName || selectedOption.deviceCategory || "",
deviceId: row.deviceId || this[type][index].deviceId, })
deviceName: row.deviceName || this[type][index].deviceName, );
deviceCategory: row.deviceCategory || this[type][index].deviceCategory,
categoryName: row.deviceCategory || this[type][index].categoryName,
});
this[type].splice(index, 1, line);
});
},
handleSelect(data, index, type) {
const line = Object.assign({}, this[type][index], {
point: data.pointId || data.value,
pointName: data.pointName || data.pointId || data.value,
deviceId: data.deviceId || this[type][index].deviceId,
deviceName: data.deviceName || this[type][index].deviceName,
deviceCategory: data.deviceCategory || this[type][index].deviceCategory,
categoryName: data.categoryName || data.deviceCategory || this[type][index].categoryName,
});
this[type].splice(index, 1, line);
}, },
saveDialog() { saveDialog() {
function getToastMsg(name, type, index) { function getToastMsg(name, type, index) {
return { return {
protectionSettings: { faultProtectionSettings: {
deviceId: `请选择保护前提第${index}行的设备`, deviceId: `请选择故障保护第${index}行的设备`,
deviceCategory: `请选择保护前提第${index}行的设备类型`, deviceCategory: `请选择故障保护第${index}行的设备类型`,
categoryName: `请选择保护前提第${index}行的设备类型`, categoryName: `请选择故障保护第${index}行的设备类型`,
point: `请选择保护前提第${index}行的点位`, point: `请选择故障保护第${index}行的点位`,
pointName: `请选择保护前提第${index}行的点位`, pointName: `请选择故障保护第${index}行的点位`,
faultValue: `请输入保护前提第${index}行的故障值`, faultValue: `请输入故障保护第${index}行的故障值`,
releaseValue: `请输入保护前提第${index}行的释放值`, faultOperator: `请选择故障保护第${index}行的故障值比较关系`,
faultOperator: `请选择保护前提第${index}行的故障值比较关系`, relationNext: `请选择故障保护第${index}行与下一个点位的关系`,
releaseOperator: `请选择保护前提第${index}行的释放值比较关系`, },
relationNext: `请选择保护前提第${index}行与下一个点位的关系`, releaseProtectionSettings: {
deviceId: `请选择释放保护第${index}行的设备`,
deviceCategory: `请选择释放保护第${index}行的设备类型`,
categoryName: `请选择释放保护第${index}行的设备类型`,
point: `请选择释放保护第${index}行的点位`,
pointName: `请选择释放保护第${index}行的点位`,
releaseValue: `请输入释放保护第${index}行的释放值`,
releaseOperator: `请选择释放保护第${index}行的释放值比较关系`,
relationNext: `请选择释放保护第${index}行与下一个点位的关系`,
}, },
protectionPlan: { protectionPlan: {
deviceId: `请选择保护方案第${index}行的设备`, action: `请选择执行保护第${index}行的动作`,
deviceCategory: `请选择保护方案第${index}行的设备类型`, value: `请输入执行保护第${index}行的降功率比例(0-100)`,
categoryName: `请选择保护方案第${index}行的设备类型`,
point: `请选择保护方案第${index}行的点位`,
pointName: `请选择保护方案第${index}行的点位`,
value: `请输入保护方案第${index}行的故障值`,
}, },
}[type][name]; }[type][name];
} }
@ -407,32 +659,58 @@ export default {
releaseDelaySeconds = "", releaseDelaySeconds = "",
description = "", description = "",
} = this.formData; } = this.formData;
const { protectionSettings, protectionPlan } = this; const { faultProtectionSettings, releaseProtectionSettings, protectionPlan } = this;
let protectionSettingsValidateStatus = true; let faultSettingsValidateStatus = true;
let releaseSettingsValidateStatus = true;
let protectionPlanValidateStatus = true; let protectionPlanValidateStatus = true;
const settingRequiredFields = ["point", "faultOperator", "faultValue", "releaseOperator", "releaseValue"]; const faultSettingRequiredFields = ["point", "faultOperator", "faultValue"];
const planRequiredFields = ["point", "value"]; const releaseSettingRequiredFields = ["point", "releaseOperator", "releaseValue"];
const planRequiredFields = ["action"];
for (let i = 0; i < protectionSettings.length; i++) { for (let i = 0; i < faultProtectionSettings.length; i++) {
const row = protectionSettings[i] || {}; const row = faultProtectionSettings[i] || {};
for (let inner = 0; inner < settingRequiredFields.length; inner++) { for (let inner = 0; inner < faultSettingRequiredFields.length; inner++) {
const key = settingRequiredFields[inner]; const key = faultSettingRequiredFields[inner];
const value = row[key]; const value = row[key];
if (![0, "0"].includes(value) && !value) { if (![0, "0"].includes(value) && !value) {
this.$message.error(getToastMsg(key, "protectionSettings", i + 1)); this.$message.error(getToastMsg(key, "faultProtectionSettings", i + 1));
protectionSettingsValidateStatus = false; faultSettingsValidateStatus = false;
break; break;
} }
} }
if (!protectionSettingsValidateStatus) break; if (!faultSettingsValidateStatus) break;
if (!row.deviceId) { if (!row.deviceId) {
this.$message.error(`请选择保护前提第${i + 1}行的点位(需从点位列表中选择)`); this.$message.error(`请选择故障保护第${i + 1}行的点位(需从点位列表中选择)`);
protectionSettingsValidateStatus = false; faultSettingsValidateStatus = false;
break; break;
} }
if (protectionSettings[i + 1] && !row.relationNext) { if (faultProtectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "protectionSettings", i + 1)); this.$message.error(getToastMsg("relationNext", "faultProtectionSettings", i + 1));
protectionSettingsValidateStatus = false; faultSettingsValidateStatus = false;
break;
}
}
for (let i = 0; i < releaseProtectionSettings.length; i++) {
const row = releaseProtectionSettings[i] || {};
for (let inner = 0; inner < releaseSettingRequiredFields.length; inner++) {
const key = releaseSettingRequiredFields[inner];
const value = row[key];
if (![0, "0"].includes(value) && !value) {
this.$message.error(getToastMsg(key, "releaseProtectionSettings", i + 1));
releaseSettingsValidateStatus = false;
break;
}
}
if (!releaseSettingsValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择释放保护第${i + 1}行的点位(需从点位列表中选择)`);
releaseSettingsValidateStatus = false;
break;
}
if (releaseProtectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "releaseProtectionSettings", i + 1));
releaseSettingsValidateStatus = false;
break; break;
} }
} }
@ -449,17 +727,35 @@ export default {
} }
} }
if (!protectionPlanValidateStatus) break; if (!protectionPlanValidateStatus) break;
if (!row.deviceId) { if (this.isDerateAction(row.action)) {
this.$message.error(`请选择保护方案第${i + 1}行的点位(需从点位列表中选择)`); const derate = Number(row.value);
if (Number.isNaN(derate) || derate < 0 || derate > 100) {
this.$message.error(getToastMsg("value", "protectionPlan", i + 1));
protectionPlanValidateStatus = false;
break;
}
}
if (!row.pointName) {
this.$message.error(getToastMsg("action", "protectionPlan", i + 1));
protectionPlanValidateStatus = false; protectionPlanValidateStatus = false;
break; break;
} }
} }
if (!protectionSettingsValidateStatus || !protectionPlanValidateStatus) return; if (!faultSettingsValidateStatus || !releaseSettingsValidateStatus || !protectionPlanValidateStatus) return;
const settings = protectionSettings.map((item) => Object.assign({}, item)); const settings = {
const plan = protectionPlan.map((item) => Object.assign({}, item)); faultSettings: faultProtectionSettings.map((item) => Object.assign({}, item)),
releaseSettings: releaseProtectionSettings.map((item) => Object.assign({}, item)),
};
const plan = protectionPlan.map((item) =>
Object.assign({}, item, {
actionName: this.getCapabilityLabel(item.action),
point: item.action || "",
pointName: this.getCapabilityLabel(item.action) || "",
value: this.isDerateAction(item.action) ? item.value : null,
})
);
this.loading += 1; this.loading += 1;
const params = { const params = {
@ -506,7 +802,8 @@ export default {
this.formData[key] = key === "isAlert" ? 0 : key === "faultLevel" ? 1 : ""; this.formData[key] = key === "isAlert" ? 0 : key === "faultLevel" ? 1 : "";
} }
this.$refs.addTempForm.resetFields(); this.$refs.addTempForm.resetFields();
this.$set(this, "protectionSettings", []); this.$set(this, "faultProtectionSettings", []);
this.$set(this, "releaseProtectionSettings", []);
this.$set(this, "protectionPlan", []); this.$set(this, "protectionPlan", []);
this.dialogTableVisible = false; this.dialogTableVisible = false;
}, },
@ -516,25 +813,28 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.plan-form { .plan-form {
background: #f5f8ff; padding: 16px 0;
border: 1px solid #e8edf7;
border-radius: 14px;
padding: 16px;
} }
.base-panel { .base-panel {
position: relative;
background: #fff; background: #fff;
border: 1px solid #e7ecf6; border: 1px solid #e7ecf6;
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
margin-bottom: 16px; margin: 14px 0 16px;
} }
.panel-title { .card-legend {
position: absolute;
top: 0;
left: 14px;
transform: translateY(-50%);
padding: 0 10px;
background: #fff;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #1f2d3d; color: #1f2d3d;
margin-bottom: 14px;
} }
.base-grid { .base-grid {
@ -551,12 +851,34 @@ export default {
} }
} }
.level-row {
::v-deep .el-form-item__content {
min-height: 36px;
display: flex;
align-items: center;
}
::v-deep .el-radio-group {
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 36px;
}
::v-deep .el-radio {
display: inline-flex;
align-items: center;
margin-right: 16px;
}
}
.plan-section { .plan-section {
position: relative;
background: #fff; background: #fff;
border: 1px solid #e7ecf6; border: 1px solid #e7ecf6;
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
margin-bottom: 14px; margin: 14px 0;
} }
.section-head { .section-head {
@ -567,14 +889,6 @@ export default {
margin-bottom: 12px; margin-bottom: 12px;
} }
.section-title {
font-size: 15px;
font-weight: 600;
color: #1f2d3d;
line-height: 1;
margin-bottom: 6px;
}
.section-desc { .section-desc {
color: #7b8999; color: #7b8999;
font-size: 12px; font-size: 12px;
@ -622,8 +936,8 @@ export default {
gap: 10px; gap: 10px;
} }
.setting-grid { .trigger-grid {
grid-template-columns: 2fr repeat(5, minmax(120px, 1fr)); grid-template-columns: 2fr repeat(3, minmax(120px, 1fr));
} }
.plan-grid { .plan-grid {
@ -672,7 +986,7 @@ export default {
} }
@media (max-width: 1366px) { @media (max-width: 1366px) {
.setting-grid { .trigger-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
@ -704,7 +1018,7 @@ export default {
flex-direction: column; flex-direction: column;
} }
.setting-grid, .trigger-grid,
.plan-grid { .plan-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }

View File

@ -31,13 +31,13 @@
<template slot-scope="scope">{{ scope.row.isAlert === 1 ? '是' : '否' }}</template> <template slot-scope="scope">{{ scope.row.isAlert === 1 ? '是' : '否' }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="description" label="处理方案描述" min-width="180" show-overflow-tooltip /> <el-table-column prop="description" label="处理方案描述" min-width="180" show-overflow-tooltip />
<el-table-column prop="protectionSettings" label="保护前提" min-width="360" show-overflow-tooltip> <el-table-column prop="protectionSettings" label="故障/释放保护" min-width="360" show-overflow-tooltip>
<template slot-scope="scope"> <template slot-scope="scope">
<div class="rich-lines" v-html="handleProtectionSettings(scope.row.protectionSettings)"></div> <div class="rich-lines" v-html="handleProtectionSettings(scope.row.protectionSettings)"></div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="faultDelaySeconds" label="前提延时(s)" width="110" /> <el-table-column prop="faultDelaySeconds" label="前提延时(s)" width="110" />
<el-table-column prop="protectionPlan" label="保护方案" min-width="260" show-overflow-tooltip> <el-table-column prop="protectionPlan" label="执行保护" min-width="260" show-overflow-tooltip>
<template slot-scope="scope"> <template slot-scope="scope">
<div class="rich-lines" v-html="handleProtectionPlan(scope.row.protectionPlan)"></div> <div class="rich-lines" v-html="handleProtectionPlan(scope.row.protectionPlan)"></div>
</template> </template>
@ -96,37 +96,76 @@ export default {
this.getData(); this.getData();
}, },
handleProtectionSettings(data) { handleProtectionSettings(data) {
if (!data || !JSON.parse(data)) return; if (!data) return;
const arr = JSON.parse(data); let parsed = null;
const str = arr.map((item, index) => { try {
parsed = JSON.parse(data);
} catch (e) {
return "";
}
const faultSettings = Array.isArray(parsed) ? parsed : parsed?.faultSettings || [];
const releaseSettings = Array.isArray(parsed) ? parsed : parsed?.releaseSettings || [];
const buildLine = (item, index, total, key, value, relationKey) => {
const { const {
categoryName = "", categoryName = "",
deviceId = "", deviceId = "",
point = "", point = "",
faultOperator = "", [key]: operator = "",
faultValue = "", [value]: val = "",
releaseOperator = "",
releaseValue = "",
relationNext = "", relationNext = "",
} = item; } = item;
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${ return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
deviceId ? deviceId + "-" : "" deviceId ? deviceId + "-" : ""
}${point || ""}</span> <span>故障:${faultOperator || ""}${faultValue || ""}</span> <span>释放:${ }${point || ""}</span> <span>${relationKey}:${operator || ""}${val || ""}</span> ${
releaseOperator || "" total[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
}${releaseValue || ""}</span> ${
arr[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
}</div>`; }</div>`;
}); };
return str.join("");
const faultStr = faultSettings.map((item, index) =>
buildLine(item, index, faultSettings, "faultOperator", "faultValue", "故障")
);
const releaseStr = releaseSettings.map((item, index) =>
buildLine(item, index, releaseSettings, "releaseOperator", "releaseValue", "释放")
);
const groups = [];
if (faultStr.length) {
groups.push(`<div><strong>故障保护</strong></div>${faultStr.join("")}`);
}
if (releaseStr.length) {
groups.push(`<div><strong>释放保护</strong></div>${releaseStr.join("")}`);
}
return groups.join("");
}, },
handleProtectionPlan(data) { handleProtectionPlan(data) {
if (!data || !JSON.parse(data)) return; if (!data) return;
const arr = JSON.parse(data); let arr = [];
try {
const parsed = JSON.parse(data);
arr = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
} catch (e) {
return "";
}
const actionLabelMap = {
derate: "降功率",
shutdown: "关机/停机/切断",
forbid_charge: "禁止充电",
allow_discharge: "允许放电",
forbid_discharge: "禁止放电",
allow_charge: "允许充电",
forbid_charge_discharge: "禁止充放电",
standby: "待机",
};
const str = arr.map((item, index) => { const str = arr.map((item, index) => {
const { categoryName = "", deviceId = "", point = "", value = "" } = item; const action = item?.action || "";
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${ const point = item?.point || "";
deviceId ? deviceId + "-" : "" const pointName = item?.pointName || "";
}${point || ""}</span> <span>故障:=${value || ""}</span> </div>`; const actionName = item?.actionName || actionLabelMap[action] || pointName || point || "未配置";
const value = item?.value;
if ((action === "derate" || actionName.includes("降功率")) && value !== null && value !== undefined && value !== "") {
return `<div>${index + 1}、 <span>动作:${actionName}</span> <span>比例:${value}%</span></div>`;
}
return `<div>${index + 1}、 <span>动作:${actionName}</span></div>`;
}); });
return str.join(""); return str.join("");
}, },