develop-cloud #2

Merged
dashixiong merged 6 commits from develop-cloud into main-cloud 2026-04-01 06:32:13 +00:00
55 changed files with 10573 additions and 2716 deletions
Showing only changes of commit c7c1b416ee - Show all commits

View File

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

View File

@ -74,8 +74,12 @@
<div slot="header">
<span class="card-title">总累计运行数据</span>
<div class="total-count">
<span class="title">总收入</span>
<span class="value">
<span class="title pointer-field" @click="handleTotalRevenueClick">总收入</span>
<span
class="value pointer-field"
:class="{ 'field-disabled': !hasPointId(totalRevenueDisplayItem) }"
@click="handleTotalRevenueClick"
>
<i v-if="isRunningInfoLoading" class="el-icon-loading"></i>
<span v-else>{{ totalRevenueDisplayValue | formatNumber }}</span>
</span>
@ -92,7 +96,11 @@
:key="index + 'sjglData'"
class="sjgl-col"
>
<div class="sjgl-wrapper">
<div
class="sjgl-wrapper pointer-field"
:class="{ 'field-disabled': !hasPointId(item.raw) }"
@click="handleRunningFieldClick(item)"
>
<div class="sjgl-title">{{ item.title }}</div>
<div class="sjgl-value" :style="{color:item.color}">
<i v-if="item.loading" class="el-icon-loading"></i>
@ -111,12 +119,40 @@
<active-chart ref="activeChart" :display-data="runningDisplayData"/>
</el-col>
</el-row>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<script>
import * as echarts from "echarts";
import {getSingleSiteBaseInfo} from "@/api/ems/zddt";
import {getDzjkHomeView, getProjectDisplayData} from "@/api/ems/dzjk";
import {getPointConfigCurve} from "@/api/ems/site";
import WeekChart from "./WeekChart.vue";
import ActiveChart from "./ActiveChart.vue";
import AlarmTable from "./AlarmTable.vue";
@ -135,6 +171,19 @@ export default {
runningInfoLoading: false,
runningUpdateSpinning: false,
runningUpdateTimer: null,
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
fallbackSjglData: [
{
title: "今日充电量kWh",
@ -218,6 +267,7 @@ export default {
value: item.fieldValue,
color: this.getCardColor(index),
loading: this.isRunningInfoLoading,
raw: item,
}));
}
return this.fallbackSjglData.map(item => ({
@ -225,6 +275,7 @@ export default {
value: this.runningInfo[item.attr],
color: item.color,
loading: this.isRunningInfoLoading,
raw: item,
}));
},
},
@ -233,8 +284,172 @@ export default {
clearTimeout(this.runningUpdateTimer);
this.runningUpdateTimer = null;
}
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
methods: {
hasPointId(item) {
return !!String(item?.dataPoint || "").trim();
},
handleTotalRevenueClick() {
const item = this.totalRevenueDisplayItem;
const pointId = String(item?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || "总收入",
});
},
handleRunningFieldClick(card) {
const item = card?.raw || {};
const pointId = String(item?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: card?.title || item?.fieldName || item?.fieldCode || pointId,
});
},
openCurveDialog({pointId, title}) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const pad = (num) => String(num).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
setBaseInfoLoading(loading) {
if (Object.prototype.hasOwnProperty.call(this.$data, "baseInfoLoading")) {
this.baseInfoLoading = loading;
@ -407,6 +622,22 @@ export default {
}
}
.pointer-field {
cursor: pointer;
}
.field-disabled {
cursor: not-allowed;
opacity: 0.75;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.row-container {
& > .el-col {
margin-bottom: 20px;

View File

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

View File

@ -51,15 +51,33 @@
<el-descriptions-item
contentClassName="descriptions-direction work-status"
:span="1" label="工作状态">
{{ CLUSTERWorkStatusOptions[baseInfo.workStatus] || '-' }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'workStatus') }"
@click="handleFieldClick(baseInfo, 'workStatus', '工作状态')"
>
{{ CLUSTERWorkStatusOptions[baseInfo.workStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
:span="1" label="与PCS通信">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'pcsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
:span="1" label="与EMS通信">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'emsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
@ -68,7 +86,11 @@
<el-descriptions-item labelClassName="descriptions-label" contentClassName="descriptions-direction"
v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :span="1"
:label="item.label">
<span class="pointer" @click="showChart(item.pointName || '',baseInfo.deviceId)">
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, item.attr) }"
@click="handleFieldClick(baseInfo, item.attr, item.label)"
>
<i v-if="isPointLoading(baseInfo[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(baseInfo[item.attr]) | formatNumber }}</span>
<span v-if="item.unit" v-html="item.unit"></span>
@ -80,7 +102,11 @@
<div class="process-line-bg">
<div class="process-line" :style="{height:baseInfo.currentSoc+'%'}"></div>
</div>
<div class="process pointer" @click="showChart( '当前SOC',baseInfo.deviceId)">当前SOC :
<div
class="process pointer"
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'currentSoc') }"
@click="handleFieldClick(baseInfo, 'currentSoc', '当前SOC')"
>当前SOC :
{{ baseInfo.currentSoc }}%
</div>
</div>
@ -103,7 +129,8 @@
>
<template slot-scope="scope">
<span class="pointer"
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.avgData
}}</span>
</template>
@ -113,7 +140,8 @@
label="单体最小值">
<template slot-scope="scope">
<span class="pointer"
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.minData
}}</span>
</template>
@ -127,7 +155,8 @@
label="单体最大值">
<template slot-scope="scope">
<span class="pointer "
@click="showChart( tablePointNameMap[scope.row.dataName+scope.column.label],baseInfo.deviceId)">{{
:class="{ 'field-disabled': !hasTableFieldPointId(baseInfo, scope.row.dataName, scope.column.label) }"
@click="handleTableFieldClick(baseInfo, scope.row.dataName, scope.column.label)">{{
scope.row.maxData
}}</span>
</template>
@ -139,24 +168,49 @@
</el-table>
</el-card>
</div>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<script>
import pointChart from "./../PointChart.vue";
import * as echarts from "echarts";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {getProjectDisplayData, getStackNameList, getClusterNameList} from '@/api/ems/dzjk'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve} from "@/api/ems/site";
export default {
name: 'DzjkSbjkBmsdcc',
mixins: [getQuerySiteId, intervalUpdate],
components: {PointTable, pointChart},
components: {PointTable},
computed: {
...mapState({
CLUSTERWorkStatusOptions: state => state?.ems?.CLUSTERWorkStatusOptions || {},
@ -174,6 +228,19 @@ export default {
displayData: [],
clusterDeviceList: [],
selectedClusterId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
unitObj: {
'电压': 'V',
'温度': '&#8451;',
@ -228,8 +295,172 @@ export default {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'CLUSTER'}, dataType)
},
showChart(pointName, deviceId) {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory: 'CLUSTER', deviceId})
hasFieldPointId(baseInfo, fieldName) {
const row = this.getFieldRow(baseInfo, fieldName);
return !!String(row?.dataPoint || "").trim();
},
hasTableFieldPointId(baseInfo, dataName, columnLabel) {
const pointName = this.tablePointNameMap[String(dataName || "") + String(columnLabel || "")];
if (!pointName) {
return false;
}
return this.hasFieldPointId(baseInfo, pointName);
},
getFieldRow(baseInfo, fieldName) {
const key = String(fieldName || "").trim();
const map = baseInfo?._fieldRowMap || {};
return map[key] || null;
},
handleFieldClick(baseInfo, fieldName, title) {
const row = this.getFieldRow(baseInfo, fieldName);
const pointId = String(row?.dataPoint || "").trim();
this.openCurveDialogByPointId(pointId, title || fieldName);
},
handleTableFieldClick(baseInfo, dataName, columnLabel) {
const pointName = this.tablePointNameMap[String(dataName || "") + String(columnLabel || "")];
if (!pointName) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.handleFieldClick(baseInfo, pointName, pointName);
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(deviceId) {
this.selectedClusterId = deviceId || "";
@ -248,13 +479,28 @@ export default {
return index >= 0 ? raw.slice(index + 2) : raw;
},
getFieldMap(rows = [], deviceId = "") {
const rowMap = this.getFieldRowMap(rows, deviceId);
return Object.keys(rowMap).reduce((acc, fieldName) => {
const row = rowMap[fieldName];
if (acc[fieldName] === undefined) {
acc[fieldName] = row?.fieldValue;
}
return acc;
}, {});
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = String(deviceId || "");
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== targetDeviceId) return;
map[this.getFieldName(item.fieldCode)] = item.fieldValue;
const fieldName = this.getFieldName(item.fieldCode);
map[fieldName] = item;
const displayName = String(item.fieldName || "").trim();
if (displayName && !map[displayName]) {
map[displayName] = item;
}
});
rows.forEach(item => {
if (!item || !item.fieldCode) return;
@ -262,7 +508,11 @@ export default {
if (itemDeviceId !== "") return;
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item.fieldValue;
map[fieldName] = item;
}
const displayName = String(item.fieldName || "").trim();
if (displayName && !map[displayName]) {
map[displayName] = item;
}
});
return map;
@ -323,6 +573,11 @@ export default {
pcsCommunicationStatus: statusMap.pcsCommunicationStatus,
emsCommunicationStatus: statusMap.emsCommunicationStatus,
currentSoc: isNaN(currentSoc) ? 0 : currentSoc,
_fieldRowMap: {
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '簇信息'), id),
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '状态'), id),
...this.getFieldRowMap(this.getModuleRows('SBJK_BMSDCC', '单体数据'), id),
},
};
})(),
siteId: this.siteId,
@ -348,7 +603,13 @@ export default {
this.loading = false
})
}
}
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}
</script>

View File

@ -49,15 +49,21 @@
<el-descriptions-item
contentClassName="descriptions-direction work-status"
label="工作状态" labelClassName="descriptions-label">
{{ STACKWorkStatusOptions[baseInfo.workStatus] || '-' }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'workStatus', '工作状态')">
{{ STACKWorkStatusOptions[baseInfo.workStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与PCS通信"
labelClassName="descriptions-label">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
<el-descriptions-item :span="1" contentClassName="descriptions-direction" label="与EMS通信"
labelClassName="descriptions-label">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')">
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
</span>
</el-descriptions-item>
</el-descriptions>
</div>
@ -66,7 +72,7 @@
<el-descriptions-item v-for="(item,index) in infoData" :key="index+'pcsInfoData'" :label="item.label"
:span="1" contentClassName="descriptions-direction"
labelClassName="descriptions-label">
<span class="pointer" @click="showChart(item.pointName || '',baseInfo.deviceId)">
<span class="pointer" @click="handleStackFieldClick(baseInfo, item)">
<i v-if="isPointLoading(baseInfo[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(baseInfo[item.attr]) | formatNumber }}</span>
<span v-if="item.unit" v-html="item.unit"></span>
@ -78,7 +84,7 @@
<div class="process-line-bg">
<div :style="{height:baseInfo.stackSoc+'%'}" class="process-line"></div>
</div>
<div class="process pointer" @click="showChart('当前SOC',baseInfo.deviceId)">当前SOC :
<div class="process pointer" @click="handleStackSocClick(baseInfo)">当前SOC :
{{ baseInfo.stackSoc }}%
</div>
</div>
@ -98,21 +104,21 @@
>
<template slot-scope="scope">
<span class="pointer"
@click="showChart('簇电压',scope.row.clusterId,'CLUSTER')">{{ scope.row.clusterVoltage }} V</span>
@click="handleClusterFieldClick(scope.row, 'clusterVoltage', '簇电压')">{{ scope.row.clusterVoltage }} V</span>
</template>
</el-table-column>
<el-table-column
label="簇电流">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('簇电流',scope.row.clusterId,'CLUSTER')">{{ scope.row.clusterCurrent }} A</span>
@click="handleClusterFieldClick(scope.row, 'clusterCurrent', '簇电流')">{{ scope.row.clusterCurrent }} A</span>
</template>
</el-table-column>
<el-table-column
label="簇SOC">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('当前SOC',scope.row.clusterId,'CLUSTER')">{{ scope.row.currentSoc }} %</span>
@click="handleClusterFieldClick(scope.row, 'currentSoc', '当前SOC')">{{ scope.row.currentSoc }} %</span>
</template>
</el-table-column>
<el-table-column
@ -120,7 +126,7 @@
prop="maxVoltage">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最高单体电压',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'maxCellVoltage', '最高单体电压')">{{
scope.row.maxCellVoltage
}} V</span>
</template>
@ -134,7 +140,7 @@
prop="minVoltage">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最低单体电压',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'minCellVoltage', '最低单体电压')">{{
scope.row.minCellVoltage
}} V</span>
</template>
@ -147,7 +153,7 @@
label="单体最高温度">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最高单体温度',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'maxCellTemp', '最高单体温度')">{{
scope.row.maxCellTemp
}} &#8451;</span>
</template>
@ -161,7 +167,7 @@
prop="minTemperature">
<template slot-scope="scope">
<span class="pointer"
@click="showChart('最低单体温度',scope.row.clusterId,'CLUSTER')">{{
@click="handleClusterFieldClick(scope.row, 'minCellTemp', '最低单体温度')">{{
scope.row.minCellTemp
}} &#8451;</span>
</template>
@ -174,22 +180,47 @@
</el-table>
</el-card>
</div>
<point-chart ref="pointChart" :site-id="siteId"/>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
<point-table ref="pointTable"/>
</div>
</template>
<script>
import * as echarts from "echarts";
import {getProjectDisplayData, getStackNameList} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import {mapState} from "vuex";
export default {
name: 'DzjkSbjkBmszl',
components: {pointChart, PointTable},
components: {PointTable},
mixins: [getQuerySiteId, intervalUpdate],
computed: {
...mapState({
@ -208,6 +239,19 @@ export default {
displayData: [],
stackDeviceList: [],
selectedStackId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
baseInfoList: [{
siteId: "",
deviceId: "",
@ -217,15 +261,15 @@ export default {
batteryDataList: [],
}],
infoData: [
{label: '电池堆总电压', attr: 'stackVoltage', unit: 'V', pointName: '电池堆电压'},
{label: '可充电量', attr: 'availableChargeCapacity', unit: 'kWh', pointName: '可充电量'},
{label: '累计充电量', attr: 'totalChargeCapacity', unit: 'kWh', pointName: '累计充电量'},
{label: '电池堆总电流', attr: 'stackCurrent', unit: 'A', pointName: '电池堆总电流'},
{label: '可放电量', attr: 'availableDischargeCapacity', unit: 'kWh', pointName: '可放电量'},
{label: '累计放电量', attr: 'totalDischargeCapacity', unit: 'kWh', pointName: '累计放电量'},
{label: 'SOH', attr: 'stackSoh', unit: '%', pointName: 'SOH'},
{label: '平均温度', attr: 'operatingTemp', unit: '&#8451;', pointName: '平均温度'},
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: '&Omega;', pointName: '绝缘电阻'},
{label: '电池堆总电压', attr: 'stackVoltage', unit: 'V'},
{label: '可充电量', attr: 'availableChargeCapacity', unit: 'kWh'},
{label: '累计充电量', attr: 'totalChargeCapacity', unit: 'kWh'},
{label: '电池堆总电流', attr: 'stackCurrent', unit: 'A'},
{label: '可放电量', attr: 'availableDischargeCapacity', unit: 'kWh'},
{label: '累计放电量', attr: 'totalDischargeCapacity', unit: 'kWh'},
{label: 'SOH', attr: 'stackSoh', unit: '%'},
{label: '平均温度', attr: 'operatingTemp', unit: '&#8451;'},
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: '&Omega;'},
]
}
},
@ -246,8 +290,181 @@ export default {
const {siteId, deviceId} = row
this.$refs.pointTable.showTable({siteId, deviceId, deviceCategory: 'STACK'}, dataType)
},
showChart(pointName, deviceId, deviceCategory = 'STACK') {
pointName && this.$refs.pointChart.showChart({pointName, deviceCategory, deviceId})
handleStatusFieldClick(baseInfo, fieldKey, title) {
const pointId = this.resolvePointId(baseInfo, fieldKey, "status");
this.openCurveDialogByPointId(pointId, title || fieldKey);
},
handleStackFieldClick(baseInfo, item) {
const fieldKey = item?.attr || "";
const pointId = this.resolvePointId(baseInfo, fieldKey, "info");
this.openCurveDialogByPointId(pointId, item?.label || fieldKey);
},
handleStackSocClick(baseInfo) {
const pointId = this.resolvePointId(baseInfo, "stackSoc", "info");
this.openCurveDialogByPointId(pointId, "当前SOC");
},
handleClusterFieldClick(row = {}, fieldKey = "", title = "") {
const directKeys = [
"pointId",
"dataPoint",
`${fieldKey}PointId`,
`${fieldKey}DataPoint`,
];
let pointId = "";
directKeys.some((key) => {
const value = String(row?.[key] || "").trim();
if (value) {
pointId = value;
return true;
}
return false;
});
if (!pointId && row?.pointIdMap && fieldKey) {
pointId = String(row.pointIdMap[fieldKey] || "").trim();
}
this.openCurveDialogByPointId(pointId, title || fieldKey);
},
resolvePointId(baseInfo = {}, fieldKey = "", source = "info") {
const mapKey = source === "status" ? "statusPointIdMap" : "pointIdMap";
return String(baseInfo?.[mapKey]?.[fieldKey] || "").trim();
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
init() {
this.updateData()
@ -262,26 +479,44 @@ export default {
const index = raw.lastIndexOf("__");
return index >= 0 ? raw.slice(index + 2) : raw;
},
getFieldMap(rows = [], deviceId = "") {
isEmptyValue(value) {
return value === undefined || value === null || value === "";
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = String(deviceId || "");
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== targetDeviceId) return;
map[this.getFieldName(item.fieldCode)] = item.fieldValue;
map[this.getFieldName(item.fieldCode)] = item;
});
rows.forEach(item => {
if (!item || !item.fieldCode) return;
const itemDeviceId = String(item.deviceId || "");
if (itemDeviceId !== "") return;
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item.fieldValue;
const existRow = map[fieldName];
if (!existRow || this.isEmptyValue(existRow.fieldValue)) {
map[fieldName] = item;
}
});
return map;
},
getFieldMap(rowMap = {}) {
const map = {};
Object.keys(rowMap || {}).forEach((fieldName) => {
map[fieldName] = rowMap[fieldName]?.fieldValue;
});
return map;
},
getPointIdMap(rowMap = {}) {
const map = {};
Object.keys(rowMap || {}).forEach((fieldName) => {
map[fieldName] = String(rowMap[fieldName]?.dataPoint || "").trim();
});
return map;
},
getLatestTime(menuCode) {
const times = (this.displayData || [])
.filter(item => item.menuCode === menuCode && item.valueTime)
@ -311,8 +546,10 @@ export default {
this.baseInfoList = devices.map(device => ({
...(() => {
const id = device.deviceId || device.id || this.siteId;
const infoMap = this.getFieldMap(this.getModuleRows('SBJK_BMSZL', '堆信息'), id);
const statusMap = this.getFieldMap(this.getModuleRows('SBJK_BMSZL', '状态'), id);
const infoRowMap = this.getFieldRowMap(this.getModuleRows('SBJK_BMSZL', '堆信息'), id);
const statusRowMap = this.getFieldRowMap(this.getModuleRows('SBJK_BMSZL', '状态'), id);
const infoMap = this.getFieldMap(infoRowMap);
const statusMap = this.getFieldMap(statusRowMap);
const stackSoc = Number(infoMap.stackSoc);
return {
...infoMap,
@ -320,6 +557,8 @@ export default {
pcsCommunicationStatus: statusMap.pcsCommunicationStatus,
emsCommunicationStatus: statusMap.emsCommunicationStatus,
stackSoc: isNaN(stackSoc) ? 0 : stackSoc,
pointIdMap: this.getPointIdMap(infoRowMap),
statusPointIdMap: this.getPointIdMap(statusRowMap),
};
})(),
siteId: this.siteId,
@ -346,6 +585,13 @@ export default {
},
}
,
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
}
</script>
@ -363,6 +609,13 @@ export default {
cursor: pointer;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
::v-deep {
//描述列表样式
.descriptions-main {

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'dbField'"
:span="8"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkDb",
@ -68,6 +98,19 @@ export default {
displayData: [],
selectedSectionKey: "",
ammeterDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
@ -122,6 +165,8 @@ export default {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
@ -175,6 +220,153 @@ export default {
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
@ -234,6 +426,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
@ -265,6 +463,26 @@ export default {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

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

View File

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

View File

@ -88,7 +88,6 @@
:totalSize="totalSize"
:pointIdList="pointIdList"
@chart="chartDetail"
@pointDetail="pointDetail"
></component>
<el-pagination
v-show="tableData.length > 0"
@ -104,34 +103,55 @@
>
</el-pagination>
<chart-detail ref="chartDetail" />
<point-chart ref="pointChart" :site-id="siteId" />
<point-table ref="pointTable"/>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</el-card>
</template>
<script>
import * as echarts from "echarts";
import BarChart from "./BarChart";
import {
getStackNameList,
getClusterNameList,
getClusterDataInfoList,
getClusterNameList,
getStackNameList,
} from "@/api/ems/dzjk";
import { getPointConfigCurve } from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import ChartDetail from "./ChartDetail.vue";
import Table from "./Table.vue";
import List from "./List.vue";
import pointChart from "./../PointChart.vue";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
export default {
name: "DzjkSbjkDtdc",
mixins: [getQuerySiteId],
components: {
PointTable,
BarChart,
ChartDetail,
DtdcTable: Table,
DtdcList: List,
pointChart,
},
computed: {
pointIdList() {
@ -166,27 +186,190 @@ export default {
pageNum: 1, //分页栏当前页数
totalSize: 0, //table表格数据总数
activeBtn: "table",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
methods: {
// 查看设备电位表格
pointDetail(row,dataType){
const {deviceId,clusterDeviceId} = row
this.$refs.pointTable.showTable({siteId:this.siteId,deviceId,deviceCategory:'BATTERY',parentId:clusterDeviceId},dataType)
getFieldPointConfig(fieldKey) {
const pointMap = {
voltage: { pointIdKey: "voltagePointId", title: "电压 (V)" },
temperature: { pointIdKey: "temperaturePointId", title: "温度 ()" },
soc: { pointIdKey: "socPointId", title: "SOC (%)" },
soh: { pointIdKey: "sohPointId", title: "SOH (%)" },
};
return pointMap[String(fieldKey || "").trim()] || null;
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(
d.getHours()
)}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
changeMenu(menu) {
const { activeBtn } = this;
activeBtn !== menu && (this.activeBtn = menu);
},
//查看表格行图表
chartDetail({ deviceId, clusterDeviceId, dataType = "" }) {
dataType &&
this.$refs.pointChart.showChart({
pointName: dataType,
deviceCategory:'BATTERY',
deviceId: clusterDeviceId,
child: [deviceId],
});
chartDetail(row = {}) {
const config = this.getFieldPointConfig(row.fieldKey);
if (!config) return;
const pointId = row[config.pointIdKey];
this.openCurveDialogByPointId(pointId, config.title);
},
// 分页
handleSizeChange(val) {
@ -224,6 +407,9 @@ export default {
);
this.search.clusterId = "";
this.getClusterList();
} else {
this.search.clusterId = "";
this.clusterOptions = [];
}
},
//表格数据
@ -254,19 +440,26 @@ export default {
},
getStackList() {
getStackNameList(this.siteId).then((response) => {
this.stackOptions = JSON.parse(JSON.stringify(response?.data || []));
const list = JSON.parse(JSON.stringify(response?.data || []));
this.stackOptions = list;
});
},
getClusterList() {
const { stackId } = this.search;
if (!stackId) {
this.clusterOptions = [];
return Promise.resolve();
}
this.clusterloading = true;
getClusterNameList({
stackDeviceId: this.search.stackId,
const currentStackId = String(stackId);
return getClusterNameList({
stackDeviceId: stackId,
siteId: this.siteId,
})
.then((response) => {
this.clusterOptions = JSON.parse(
JSON.stringify(response?.data || [])
);
// 避免用户快速切换电池堆时旧请求覆盖新数据
if (String(this.search.stackId || "") !== currentStackId) return;
this.clusterOptions = JSON.parse(JSON.stringify(response?.data || []));
})
.finally(() => {
this.clusterloading = false;

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'emsField'"
:span="6"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkEms",
@ -68,6 +98,19 @@ export default {
displayData: [],
selectedSectionKey: "",
emsDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
@ -122,6 +165,8 @@ export default {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
@ -175,6 +220,153 @@ export default {
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
@ -218,6 +410,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
@ -249,6 +447,26 @@ export default {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

@ -58,7 +58,14 @@
:span="1"
label="工作状态"
labelClassName="descriptions-label"
>{{ formatDictValue((PCSWorkStatusOptions || {}), pcsItem.workStatus) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'workStatus') }"
@click="handlePcsFieldClick(pcsItem, 'workStatus', '工作状态')"
>
{{ formatDictValue((PCSWorkStatusOptions || {}), pcsItem.workStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -66,7 +73,14 @@
contentClassName="descriptions-direction"
label="并网状态"
labelClassName="descriptions-label"
>{{ formatDictValue((($store.state.ems && $store.state.ems.gridStatusOptions) || {}), pcsItem.gridStatus) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'gridStatus') }"
@click="handlePcsFieldClick(pcsItem, 'gridStatus', '并网状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.gridStatusOptions) || {}), pcsItem.gridStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -76,7 +90,14 @@
:span="1"
label="设备状态"
labelClassName="descriptions-label"
>{{ formatDictValue((($store.state.ems && $store.state.ems.deviceStatusOptions) || {}), pcsItem.deviceStatus) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'deviceStatus') }"
@click="handlePcsFieldClick(pcsItem, 'deviceStatus', '设备状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.deviceStatusOptions) || {}), pcsItem.deviceStatus) }}
</span>
</el-descriptions-item
>
<el-descriptions-item
@ -84,7 +105,14 @@
contentClassName="descriptions-direction"
label="控制模式"
labelClassName="descriptions-label"
>{{ formatDictValue((($store.state.ems && $store.state.ems.controlModeOptions) || {}), pcsItem.controlMode) }}
>
<span
class="pointer"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'controlMode') }"
@click="handlePcsFieldClick(pcsItem, 'controlMode', '控制模式')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.controlModeOptions) || {}), pcsItem.controlMode) }}
</span>
</el-descriptions-item
>
</el-descriptions>
@ -105,9 +133,8 @@
>
<span
class="pointer"
@click="
showChart(item.pointName || '', pcsItem.deviceId)
"
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, item.attr) }"
@click="handlePcsFieldClick(pcsItem, item.attr, item.label)"
>
<i v-if="isPointLoading(pcsItem[item.attr])" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(pcsItem[item.attr]) | formatNumber }}</span>
@ -144,7 +171,8 @@
>
<span
class="pointer"
@click="showChart('直流功率', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcPowerPointId }"
@click="openCurveDialogByPointId(item.dcPowerPointId, '直流功率')"
>{{ item.dcPower }}kW</span
>
</el-descriptions-item>
@ -156,7 +184,8 @@
>
<span
class="pointer"
@click="showChart('直流电压', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcVoltagePointId }"
@click="openCurveDialogByPointId(item.dcVoltagePointId, '直流电压')"
>{{ item.dcVoltage }}V</span
>
</el-descriptions-item>
@ -168,7 +197,8 @@
>
<span
class="pointer"
@click="showChart('直流电流', item.deviceId,true)"
:class="{ 'field-disabled': !item.dcCurrentPointId }"
@click="openCurveDialogByPointId(item.dcCurrentPointId, '直流电流')"
>{{ item.dcCurrent }}A</span
>
</el-descriptions-item>
@ -176,22 +206,47 @@
</div>
</el-card>
</div>
<point-chart ref="pointChart" :site-id="siteId"/>
<point-table ref="pointTable"/>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<script>
import pointChart from "./../PointChart.vue";
import * as echarts from "echarts";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getPcsNameList, getProjectDisplayData} from "@/api/ems/dzjk";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve} from "@/api/ems/site";
export default {
name: "DzjkSbjkPcs",
components: {pointChart, PointTable},
components: {PointTable},
mixins: [getQuerySiteId, intervalUpdate],
computed: {
...mapState({
@ -210,6 +265,19 @@ export default {
displayData: [],
pcsDeviceList: [],
selectedPcsId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
pcsList: [{
deviceId: "",
deviceName: "PCS",
@ -337,9 +405,157 @@ export default {
const {deviceId} = row
this.$refs.pointTable.showTable({siteId: this.siteId, deviceId, deviceCategory: 'PCS'}, dataType)
},
showChart(pointName, deviceId, isBranch = false) {
pointName &&
this.$refs.pointChart.showChart({pointName, deviceCategory: isBranch ? 'BRANCH' : 'PCS', deviceId});
hasFieldPointId(pcsItem, fieldName) {
const row = this.getFieldRow(pcsItem, fieldName);
return !!String(row?.dataPoint || "").trim();
},
getFieldRow(pcsItem, fieldName) {
const key = String(fieldName || "").trim();
const map = pcsItem?._fieldRowMap || {};
return map[key] || null;
},
handlePcsFieldClick(pcsItem, fieldName, title) {
const row = this.getFieldRow(pcsItem, fieldName);
const pointId = String(row?.dataPoint || "").trim();
this.openCurveDialogByPointId(pointId, title || fieldName);
},
openCurveDialogByPointId(pointId, title) {
const normalizedPointId = String(pointId || "").trim();
if (!normalizedPointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || normalizedPointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId: normalizedPointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(deviceId) {
this.selectedPcsId = deviceId || "";
@ -355,6 +571,14 @@ export default {
return index >= 0 ? fieldCode.slice(index + 2) : fieldCode;
},
getFieldMap(rows = [], deviceId = "") {
const rowMap = this.getFieldRowMap(rows, deviceId);
return Object.keys(rowMap).reduce((acc, fieldName) => {
const row = rowMap[fieldName] || {};
acc[fieldName] = row.fieldValue;
return acc;
}, {});
},
getFieldRowMap(rows = [], deviceId = "") {
const map = {};
const targetDeviceId = this.normalizeDeviceId(deviceId || "");
// 设备维度优先:先吃 device_id 对应值,再用默认值(空 device_id)补齐
@ -366,7 +590,7 @@ export default {
if (itemDeviceId !== targetDeviceId) {
return;
}
map[this.getFieldName(item.fieldCode)] = item.fieldValue;
map[this.getFieldName(item.fieldCode)] = item;
});
rows.forEach(item => {
if (!item || !item.fieldCode) {
@ -378,7 +602,7 @@ export default {
}
const fieldName = this.getFieldName(item.fieldCode);
if (map[fieldName] === undefined || map[fieldName] === null || map[fieldName] === "") {
map[fieldName] = item.fieldValue;
map[fieldName] = item;
}
});
return map;
@ -411,6 +635,10 @@ export default {
deviceId: device.deviceId || device.id || this.siteId,
deviceName: device.deviceName || device.name || device.deviceId || device.id || 'PCS',
...this.getFieldMap(this.getModuleRows('SBJK_PCS', '状态'), device.deviceId || device.id || this.siteId),
_fieldRowMap: {
...this.getFieldRowMap(this.getModuleRows('SBJK_PCS', '电参量'), device.deviceId || device.id || this.siteId),
...this.getFieldRowMap(this.getModuleRows('SBJK_PCS', '状态'), device.deviceId || device.id || this.siteId),
},
dataUpdateTime: this.getLatestTime('SBJK_PCS'),
alarmNum: 0,
pcsBranchInfoList: [],
@ -433,6 +661,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
<style lang="scss" scoped>
@ -449,6 +683,18 @@ export default {
cursor: pointer;
}
.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

@ -2,7 +2,7 @@
<template>
<div class="ssyx-ems-dashboard-editor-container">
<!-- 6个方块-->
<real-time-base-info :display-data="runningDisplayData" :loading="runningHeadLoading"/>
<real-time-base-info :display-data="runningDisplayData" :loading="runningHeadLoading" @field-click="handleHeadFieldClick"/>
<!-- 时间选择 -->
<date-range-select ref="dateRangeSelect" @updateDate="updateDate" style="margin-top:20px;"/>
<!-- echart图表-->
@ -22,13 +22,45 @@
<dcpjwd-chart ref="dcpjwd" :display-data="runningDisplayData"/>
</el-col>
</el-row>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
</style>
<script>
import * as echarts from "echarts";
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import RealTimeBaseInfo from "./../RealTimeBaseInfo.vue";
import CnglqxChart from './CnglqxChart.vue'
@ -37,6 +69,7 @@ import DcpjwdChart from './DcpjwdChart.vue'
import DcpjsocChart from './DcpjsocChart.vue'
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getProjectDisplayData} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
export default {
@ -49,9 +82,172 @@ export default {
timeRange:[],
isInit:true,
runningHeadLoading: false,
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
}
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
methods:{
handleHeadFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.title || item?.raw?.fieldName || pointId,
});
},
openCurveDialog({pointId, title}) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const pad = (num) => String(num).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query).then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
}).catch(() => {
this.renderCurveChart([]);
}).finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map(item => this.formatCurveTime(item.dataTime));
const yData = rows.map(item => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
//6个方块数据
getRunningHeadData(){
this.runningHeadLoading = true

View File

@ -41,23 +41,53 @@
:key="dataIndex + 'ylField'"
:span="8"
class="device-info-col"
:class="{ 'field-disabled': !item.pointId }"
>
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
<div class="field-click-wrapper" @click="handleFieldClick(item)">
<span class="left">{{ item.fieldName }}</span>
<span class="right">
<i v-if="isPointLoading(item.fieldValue)" class="el-icon-loading point-loading-icon"></i>
<span v-else>{{ displayValue(item.fieldValue) | formatNumber }}</span>
</span>
</div>
</el-col>
</el-row>
</el-card>
<el-dialog
:visible.sync="curveDialogVisible"
:title="curveDialogTitle"
width="1000px"
append-to-body
class="ems-dialog"
:close-on-click-modal="false"
destroy-on-close
@opened="handleCurveDialogOpened"
@closed="handleCurveDialogClosed"
>
<div class="curve-tools">
<el-date-picker
v-model="curveCustomRange"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
style="width: 440px"
/>
<el-button type="primary" size="mini" :loading="curveLoading" @click="loadCurveData">查询</el-button>
</div>
<div v-loading="curveLoading" ref="curveChartRef" style="height: 380px;"></div>
</el-dialog>
</div>
</template>
<script>
import * as echarts from "echarts";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import { getProjectDisplayData } from "@/api/ems/dzjk";
import { getDeviceList } from "@/api/ems/site";
import { getDeviceList, getPointConfigCurve } from "@/api/ems/site";
export default {
name: "DzjkSbjkYl",
@ -68,6 +98,19 @@ export default {
displayData: [],
selectedSectionKey: "",
coolingDeviceList: [],
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
curveChart: null,
curveLoading: false,
curveCustomRange: [],
curveQuery: {
siteId: "",
pointId: "",
pointType: "data",
rangeType: "custom",
startTime: "",
endTime: "",
},
};
},
computed: {
@ -121,6 +164,8 @@ export default {
fieldName,
fieldValue: row.fieldValue,
valueTime: row.valueTime,
pointId: String(row?.dataPoint || "").trim(),
raw: row,
};
});
@ -165,6 +210,153 @@ export default {
},
},
methods: {
handleFieldClick(item) {
const pointId = String(item?.pointId || item?.raw?.dataPoint || "").trim();
if (!pointId) {
this.$message.warning("该字段未配置点位,无法查询曲线");
return;
}
this.openCurveDialog({
pointId,
title: item?.fieldName || pointId,
});
},
openCurveDialog({ pointId, title }) {
const range = this.getDefaultCurveRange();
this.curveCustomRange = range;
this.curveDialogTitle = `点位曲线 - ${title || pointId}`;
this.curveQuery = {
siteId: this.siteId,
pointId,
pointType: "data",
rangeType: "custom",
startTime: range[0],
endTime: range[1],
};
this.curveDialogVisible = true;
},
handleCurveDialogOpened() {
if (!this.curveChart && this.$refs.curveChartRef) {
this.curveChart = echarts.init(this.$refs.curveChartRef);
}
this.loadCurveData();
},
handleCurveDialogClosed() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
this.curveLoading = false;
},
getDefaultCurveRange() {
const end = new Date();
const start = new Date(end.getTime() - 24 * 60 * 60 * 1000);
return [this.formatDateTime(start), this.formatDateTime(end)];
},
formatDateTime(date) {
const d = new Date(date);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
formatCurveTime(value) {
if (value === undefined || value === null || value === "") {
return "";
}
const raw = String(value).trim();
const normalized = raw
.replace("T", " ")
.replace(/\.\d+/, "")
.replace(/Z$/, "")
.replace(/([+-]\d{2}:?\d{2})$/, "")
.trim();
const matched = normalized.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})/);
if (matched) {
return `${matched[1]} ${matched[2]}`;
}
return normalized.slice(0, 16);
},
loadCurveData() {
if (!this.curveQuery.siteId || !this.curveQuery.pointId) {
this.$message.warning("点位信息不完整,无法查询曲线");
return;
}
if (!this.curveCustomRange || this.curveCustomRange.length !== 2) {
this.$message.warning("请选择查询时间范围");
return;
}
this.curveQuery.startTime = this.curveCustomRange[0];
this.curveQuery.endTime = this.curveCustomRange[1];
const query = {
siteId: this.curveQuery.siteId,
pointId: this.curveQuery.pointId,
pointType: "data",
rangeType: "custom",
startTime: this.curveQuery.startTime,
endTime: this.curveQuery.endTime,
};
this.curveLoading = true;
getPointConfigCurve(query)
.then((response) => {
const rows = response?.data || [];
this.renderCurveChart(rows);
})
.catch(() => {
this.renderCurveChart([]);
})
.finally(() => {
this.curveLoading = false;
});
},
renderCurveChart(rows = []) {
if (!this.curveChart) return;
const xData = rows.map((item) => this.formatCurveTime(item.dataTime));
const yData = rows.map((item) => item.pointValue);
this.curveChart.clear();
this.curveChart.setOption({
legend: {},
grid: {
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
},
},
textStyle: {
color: "#333333",
},
xAxis: {
type: "category",
data: xData,
},
yAxis: {
type: "value",
},
dataZoom: [
{
type: "inside",
start: 0,
end: 100,
},
{
start: 0,
end: 100,
},
],
series: [
{
name: this.curveDialogTitle,
type: "line",
data: yData,
connectNulls: true,
},
],
});
if (!rows.length) {
this.$message.warning("当前时间范围暂无曲线数据");
}
},
handleTagClick(sectionKey) {
this.selectedSectionKey = sectionKey || "";
},
@ -208,6 +400,12 @@ export default {
this.updateInterval(this.updateData);
},
},
beforeDestroy() {
if (this.curveChart) {
this.curveChart.dispose();
this.curveChart = null;
}
},
};
</script>
@ -239,6 +437,26 @@ export default {
cursor: pointer;
}
.device-info-col {
cursor: pointer;
}
.field-click-wrapper {
width: 100%;
}
.device-info-col.field-disabled {
cursor: not-allowed;
opacity: 0.8;
}
.curve-tools {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.point-loading-icon {
color: #409eff;
display: inline-block;

View File

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

View File

@ -1,10 +1,10 @@
<template>
<div
v-loading="loading"
class="ems-dashboard-editor-container"
style="background-color: #ffffff"
>
<div class="ems-dashboard-editor-container" style="background-color: #ffffff">
<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
v-for="(group, index) in form.queryGroups"
@ -54,6 +54,14 @@
>
清空选择
</el-button>
<el-button
type="text"
size="mini"
:disabled="form.queryGroups.length <= 1"
@click="removeQueryGroup(index)"
>
删除点位
</el-button>
</div>
</div>
</div>
@ -124,17 +132,20 @@ export default {
queryGroups: [],
dataUnit: 1,
},
loading: false,
debouncedPointSearchMap: {},
queryGroupSeed: 0,
};
},
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: {
createEmptyQueryGroup(index) {
createEmptyQueryGroup(key) {
return {
key: index,
key,
pointId: "",
selectedPointName: "",
pointOptions: [],
@ -143,6 +154,18 @@ export default {
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) {
return this.form.queryGroups[index];
},
@ -494,8 +517,6 @@ export default {
endDate = end + " 00:00:00";
}
this.loading = true;
const selectedPoints = [];
const pointIdSet = new Set();
activeGroups.forEach(({group}) => {
@ -525,8 +546,6 @@ export default {
return;
}
this.$message.error("查询失败请稍后重试");
}).finally(() => {
this.loading = false;
});
});
},
@ -539,12 +558,10 @@ export default {
this.chart = null;
},
mounted() {
this.loading = true;
this.$nextTick(() => {
this.initChart();
this.$refs.dateTimeSelect.init();
this.syncQuerySiteIds();
this.loading = false;
});
},
};
@ -554,18 +571,32 @@ export default {
.query-groups-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.query-group {
width: 19.2%;
flex: 0 0 calc(20% - 8px);
max-width: calc(20% - 8px);
}
.group-point-item {
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 {
width: 100%;
}
@ -588,13 +619,15 @@ export default {
@media (max-width: 1600px) {
.query-group {
width: 32%;
flex-basis: calc(33.3% - 8px);
max-width: calc(33.3% - 8px);
}
}
@media (max-width: 1200px) {
.query-group {
width: 49%;
flex-basis: calc(50% - 8px);
max-width: calc(50% - 8px);
}
}
@ -603,7 +636,8 @@ export default {
gap: 0;
}
.query-group {
width: 100%;
flex-basis: 100%;
max-width: 100%;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<el-dialog
v-loading="loading"
width="92%"
width="42%"
:visible.sync="dialogTableVisible"
class="ems-dialog plan-dialog"
title="保护方案"
@ -18,7 +18,7 @@
class="plan-form"
>
<div class="base-panel">
<div class="panel-title">基础信息</div>
<div class="card-legend">基础信息</div>
<div class="base-grid">
<el-form-item label="设备保护名称" prop="faultName" class="span-1">
<el-input v-model="formData.faultName" placeholder="请输入" clearable></el-input>
@ -26,7 +26,7 @@
<el-form-item label="是否告警" prop="isAlert" class="span-1">
<el-checkbox v-model="formData.isAlert" :true-label="1" :false-label="0">启用告警</el-checkbox>
</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 :label="1">等级1</el-radio>
<el-radio :label="2">等级2</el-radio>
@ -46,34 +46,43 @@
</div>
<div class="plan-section">
<div class="card-legend">故障保护</div>
<div class="section-head">
<div>
<div class="section-title">保护前提</div>
<div class="section-desc">配置触发故障的判定条件和关系</div>
</div>
<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>
</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-grid setting-grid">
<div class="row-grid trigger-grid">
<div class="field-block field-point">
<div class="field-label">点位</div>
<el-autocomplete
<el-select
v-model="item.point"
placeholder="请输入点位ID/描述"
filterable
remote
clearable
:trigger-on-focus="false"
:debounce="250"
:value-key="'label'"
:fetch-suggestions="(q, c) => querySearchAsync(q, c, index, 'protectionSettings')"
@select="(v) => handleSelect(v, index, 'protectionSettings')"
@blur="() => fillPointMetaByPoint(item.point, index, 'protectionSettings')"
></el-autocomplete>
reserve-keyword
placeholder="支持关键字搜索,展开可查看点位列表"
:no-data-text="'暂无匹配点位'"
:loading="item.pointLoading"
@change="(value) => handlePointChange(index, 'faultProtectionSettings', value)"
@visible-change="(visible) => handlePointDropdownVisible(index, 'faultProtectionSettings', visible)"
: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 class="field-block">
@ -93,6 +102,68 @@
<el-input placeholder="请输入故障值" v-model="item.faultValue"></el-input>
</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-label">释放比较符</div>
<el-select v-model="item.releaseOperator" placeholder="请选择">
@ -115,7 +186,7 @@
<el-select v-model="item.relationNext" placeholder="请选择">
<el-option
v-for="(value, key) in relationWithPoint"
:key="key + 'relation'"
:key="key + 'releaseRelation'"
:label="key"
:value="value"
></el-option>
@ -123,24 +194,24 @@
</div>
</div>
<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>
</div>
</div>
<el-empty v-if="protectionSettings.length === 0" description="暂无保护前提请先添加"></el-empty>
<el-empty v-if="releaseProtectionSettings.length === 0" description="暂无释放保护请先添加"></el-empty>
</div>
<div class="plan-section">
<div class="card-legend">执行保护</div>
<div class="section-head">
<div>
<div class="section-title">保护方案</div>
<div class="section-desc">设置触发后的目标写入点位和值</div>
<div class="section-desc">设置触发后的执行动作(降功率、停机、禁充放等)</div>
</div>
<div class="section-actions">
<el-button @click.native.prevent="addRow('protectionPlan')" type="primary" size="mini">
新增保护方案
新增执行保护
</el-button>
</div>
</div>
@ -148,29 +219,26 @@
<div class="row-card" v-for="(item, index) in protectionPlan" :key="'protectionPlan' + index">
<div class="row-index">{{ index + 1 }}</div>
<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-label">故障值</div>
<el-input placeholder="请输入故障值" v-model="item.value"></el-input>
<div class="field-label">执行动作</div>
<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 class="row-action">
@ -180,7 +248,7 @@
</div>
</div>
<el-empty v-if="protectionPlan.length === 0" description="暂无保护方案请先添加"></el-empty>
<el-empty v-if="protectionPlan.length === 0" description="暂无执行保护请先添加"></el-empty>
</div>
</el-form>
@ -208,8 +276,19 @@ export default {
return {
mode: "",
loading: 0,
protectionSettings: [],
faultProtectionSettings: [],
releaseProtectionSettings: [],
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,
formData: {
id: "",
@ -257,140 +336,313 @@ export default {
releaseDelaySeconds: data?.releaseDelaySeconds || "",
description: data?.description || "",
};
const plan = JSON.parse(data?.protectionPlan || "[]");
const settings = JSON.parse(data?.protectionSettings || "[]");
const planRaw = JSON.parse(data?.protectionPlan || "[]");
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.protectionPlan.splice(0, 0, ...plan);
this.protectionSettings.splice(0, 0, ...settings);
this.protectionPlan.splice(0, 0, ...plan.map((item) => this.normalizeProtectionPlanItem(item)));
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 {
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) {
const item =
type === "protectionSettings"
? {
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
faultValue: "",
releaseValue: "",
faultOperator: "",
releaseOperator: "",
relationNext: "",
}
: {
deviceId: "",
deviceName: "",
deviceCategory: "",
categoryName: "",
point: "",
pointName: "",
value: "",
};
type === "protectionPlan"
? this.normalizeProtectionPlanItem({})
: this.enhancePointRow(this.createProtectionSettingItem(type));
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) {
this[type].splice(index, 1);
},
querySearchAsync(query, cb, index, type) {
if (!this.formData.siteId) {
this.$message({
type: "warning",
message: "请先选择站点",
});
return cb([]);
setPointOptions(index, type, data = []) {
const row = this.getRow(type, index);
if (!row) return;
const normalized = this.normalizePointOptions(data);
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;
});
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,
pageNum: 1,
pageSize: 100,
pointId: query || "",
pointDesc: query || "",
pointId: normalizedQuery,
pointDesc: normalizedQuery,
})
.then((response) => {
const data = response?.rows || [];
cb(
data.map((item) => {
const pointLabel = item.pointId || item.pointName || item.dataKey || "";
const desc = item.pointDesc || "";
return {
value: pointLabel,
pointName: item.pointName || pointLabel,
pointId: item.pointId || pointLabel,
pointDesc: desc,
deviceId: item.deviceId || "",
deviceName: item.deviceName || "",
deviceCategory: item.deviceCategory || "",
categoryName: item.deviceCategory || "",
label: desc ? `${pointLabel}${desc}` : pointLabel,
};
})
);
const latestRow = this.getRow(type, index);
if (!latestRow || latestRow.pointRequestId !== requestId) return [];
const result = response?.rows || [];
const cache = Object.assign({}, latestRow.pointOptionsCache || {}, { [cacheKey]: result });
this.setRow(type, index, Object.assign({}, latestRow, { pointOptionsCache: cache }));
this.setPointOptions(index, type, result);
return result;
})
.catch(() => cb([]));
.finally(() => {
const latestRow = this.getRow(type, index);
if (!latestRow || latestRow.pointRequestId !== requestId) return;
this.setRow(type, index, Object.assign({}, latestRow, { pointLoading: false }));
});
},
fillPointMetaByPoint(point, index, type) {
if (!this.formData.siteId || !point) {
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: "",
})
);
return;
}
getPointMatchList({
siteId: this.formData.siteId,
pageNum: 1,
pageSize: 1,
pointId: point,
}).then((response) => {
const row = (response?.rows || [])[0];
if (!row) return;
const line = Object.assign({}, this[type][index], {
point: row.pointId || point,
pointName: row.pointName || row.pointId || point,
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);
const selectedOption = (row.pointOptions || []).find((item) => item?.value === value) || {};
this.setRow(
type,
index,
Object.assign({}, row, {
point: selectedOption.pointId || selectedOption.value || value,
pointName: selectedOption.pointName || selectedOption.pointId || selectedOption.value || value,
deviceId: selectedOption.deviceId || "",
deviceName: selectedOption.deviceName || "",
deviceCategory: selectedOption.deviceCategory || "",
categoryName: selectedOption.categoryName || selectedOption.deviceCategory || "",
})
);
},
saveDialog() {
function getToastMsg(name, type, index) {
return {
protectionSettings: {
deviceId: `请选择保护前提第${index}行的设备`,
deviceCategory: `请选择保护前提第${index}行的设备类型`,
categoryName: `请选择保护前提第${index}行的设备类型`,
point: `请选择保护前提第${index}行的点位`,
pointName: `请选择保护前提第${index}行的点位`,
faultValue: `请输入保护前提第${index}行的故障值`,
releaseValue: `请输入保护前提第${index}行的释放值`,
faultOperator: `请选择保护前提第${index}行的故障值比较关系`,
releaseOperator: `请选择保护前提第${index}行的释放值比较关系`,
relationNext: `请选择保护前提第${index}行与下一个点位的关系`,
faultProtectionSettings: {
deviceId: `请选择故障保护第${index}行的设备`,
deviceCategory: `请选择故障保护第${index}行的设备类型`,
categoryName: `请选择故障保护第${index}行的设备类型`,
point: `请选择故障保护第${index}行的点位`,
pointName: `请选择故障保护第${index}行的点位`,
faultValue: `请输入故障保护第${index}行的故障值`,
faultOperator: `请选择故障保护第${index}行的故障值比较关系`,
relationNext: `请选择故障保护第${index}行与下一个点位的关系`,
},
releaseProtectionSettings: {
deviceId: `请选择释放保护第${index}行的设备`,
deviceCategory: `请选择释放保护第${index}行的设备类型`,
categoryName: `请选择释放保护第${index}行的设备类型`,
point: `请选择释放保护第${index}行的点位`,
pointName: `请选择释放保护第${index}行的点位`,
releaseValue: `请输入释放保护第${index}行的释放值`,
releaseOperator: `请选择释放保护第${index}行的释放值比较关系`,
relationNext: `请选择释放保护第${index}行与下一个点位的关系`,
},
protectionPlan: {
deviceId: `请选择保护方案第${index}行的设备`,
deviceCategory: `请选择保护方案第${index}行的设备类型`,
categoryName: `请选择保护方案第${index}行的设备类型`,
point: `请选择保护方案第${index}行的点位`,
pointName: `请选择保护方案第${index}行的点位`,
value: `请输入保护方案第${index}行的故障值`,
action: `请选择执行保护第${index}行的动作`,
value: `请输入执行保护第${index}行的降功率比例(0-100)`,
},
}[type][name];
}
@ -407,32 +659,58 @@ export default {
releaseDelaySeconds = "",
description = "",
} = this.formData;
const { protectionSettings, protectionPlan } = this;
let protectionSettingsValidateStatus = true;
const { faultProtectionSettings, releaseProtectionSettings, protectionPlan } = this;
let faultSettingsValidateStatus = true;
let releaseSettingsValidateStatus = true;
let protectionPlanValidateStatus = true;
const settingRequiredFields = ["point", "faultOperator", "faultValue", "releaseOperator", "releaseValue"];
const planRequiredFields = ["point", "value"];
const faultSettingRequiredFields = ["point", "faultOperator", "faultValue"];
const releaseSettingRequiredFields = ["point", "releaseOperator", "releaseValue"];
const planRequiredFields = ["action"];
for (let i = 0; i < protectionSettings.length; i++) {
const row = protectionSettings[i] || {};
for (let inner = 0; inner < settingRequiredFields.length; inner++) {
const key = settingRequiredFields[inner];
for (let i = 0; i < faultProtectionSettings.length; i++) {
const row = faultProtectionSettings[i] || {};
for (let inner = 0; inner < faultSettingRequiredFields.length; inner++) {
const key = faultSettingRequiredFields[inner];
const value = row[key];
if (![0, "0"].includes(value) && !value) {
this.$message.error(getToastMsg(key, "protectionSettings", i + 1));
protectionSettingsValidateStatus = false;
this.$message.error(getToastMsg(key, "faultProtectionSettings", i + 1));
faultSettingsValidateStatus = false;
break;
}
}
if (!protectionSettingsValidateStatus) break;
if (!faultSettingsValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择保护前提第${i + 1}行的点位(需从点位列表中选择)`);
protectionSettingsValidateStatus = false;
this.$message.error(`请选择故障保护第${i + 1}行的点位(需从点位列表中选择)`);
faultSettingsValidateStatus = false;
break;
}
if (protectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "protectionSettings", i + 1));
protectionSettingsValidateStatus = false;
if (faultProtectionSettings[i + 1] && !row.relationNext) {
this.$message.error(getToastMsg("relationNext", "faultProtectionSettings", i + 1));
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;
}
}
@ -449,17 +727,35 @@ export default {
}
}
if (!protectionPlanValidateStatus) break;
if (!row.deviceId) {
this.$message.error(`请选择保护方案第${i + 1}行的点位(需从点位列表中选择)`);
if (this.isDerateAction(row.action)) {
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;
break;
}
}
if (!protectionSettingsValidateStatus || !protectionPlanValidateStatus) return;
if (!faultSettingsValidateStatus || !releaseSettingsValidateStatus || !protectionPlanValidateStatus) return;
const settings = protectionSettings.map((item) => Object.assign({}, item));
const plan = protectionPlan.map((item) => Object.assign({}, item));
const settings = {
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;
const params = {
@ -506,7 +802,8 @@ export default {
this.formData[key] = key === "isAlert" ? 0 : key === "faultLevel" ? 1 : "";
}
this.$refs.addTempForm.resetFields();
this.$set(this, "protectionSettings", []);
this.$set(this, "faultProtectionSettings", []);
this.$set(this, "releaseProtectionSettings", []);
this.$set(this, "protectionPlan", []);
this.dialogTableVisible = false;
},
@ -516,25 +813,28 @@ export default {
<style scoped lang="scss">
.plan-form {
background: #f5f8ff;
border: 1px solid #e8edf7;
border-radius: 14px;
padding: 16px;
padding: 16px 0;
}
.base-panel {
position: relative;
background: #fff;
border: 1px solid #e7ecf6;
border-radius: 12px;
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-weight: 600;
color: #1f2d3d;
margin-bottom: 14px;
}
.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 {
position: relative;
background: #fff;
border: 1px solid #e7ecf6;
border-radius: 12px;
padding: 16px;
margin-bottom: 14px;
margin: 14px 0;
}
.section-head {
@ -567,14 +889,6 @@ export default {
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1f2d3d;
line-height: 1;
margin-bottom: 6px;
}
.section-desc {
color: #7b8999;
font-size: 12px;
@ -622,8 +936,8 @@ export default {
gap: 10px;
}
.setting-grid {
grid-template-columns: 2fr repeat(5, minmax(120px, 1fr));
.trigger-grid {
grid-template-columns: 2fr repeat(3, minmax(120px, 1fr));
}
.plan-grid {
@ -672,7 +986,7 @@ export default {
}
@media (max-width: 1366px) {
.setting-grid {
.trigger-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@ -704,7 +1018,7 @@ export default {
flex-direction: column;
}
.setting-grid,
.trigger-grid,
.plan-grid {
grid-template-columns: 1fr;
}

View File

@ -31,13 +31,13 @@
<template slot-scope="scope">{{ scope.row.isAlert === 1 ? '是' : '否' }}</template>
</el-table-column>
<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">
<div class="rich-lines" v-html="handleProtectionSettings(scope.row.protectionSettings)"></div>
</template>
</el-table-column>
<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">
<div class="rich-lines" v-html="handleProtectionPlan(scope.row.protectionPlan)"></div>
</template>
@ -96,37 +96,76 @@ export default {
this.getData();
},
handleProtectionSettings(data) {
if (!data || !JSON.parse(data)) return;
const arr = JSON.parse(data);
const str = arr.map((item, index) => {
if (!data) return;
let parsed = null;
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 {
categoryName = "",
deviceId = "",
point = "",
faultOperator = "",
faultValue = "",
releaseOperator = "",
releaseValue = "",
[key]: operator = "",
[value]: val = "",
relationNext = "",
} = item;
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
deviceId ? deviceId + "-" : ""
}${point || ""}</span> <span>故障:${faultOperator || ""}${faultValue || ""}</span> <span>释放:${
releaseOperator || ""
}${releaseValue || ""}</span> ${
arr[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
}${point || ""}</span> <span>${relationKey}:${operator || ""}${val || ""}</span> ${
total[index + 1] ? "<span>关系:" + (relationNext || "") + "</span>" : ""
}</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) {
if (!data || !JSON.parse(data)) return;
const arr = JSON.parse(data);
if (!data) return;
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 { categoryName = "", deviceId = "", point = "", value = "" } = item;
return `<div>${index + 1}、 <span>${categoryName ? categoryName + "-" : ""}${
deviceId ? deviceId + "-" : ""
}${point || ""}</span> <span>故障:=${value || ""}</span> </div>`;
const action = item?.action || "";
const point = item?.point || "";
const pointName = item?.pointName || "";
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("");
},