develop-cloud #2
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'">
|
||||
<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>
|
||||
|
||||
@ -51,15 +51,33 @@
|
||||
<el-descriptions-item
|
||||
contentClassName="descriptions-direction work-status"
|
||||
:span="1" label="工作状态">
|
||||
<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通信">
|
||||
<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通信">
|
||||
<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',
|
||||
'温度': '℃',
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -49,15 +49,21 @@
|
||||
<el-descriptions-item
|
||||
contentClassName="descriptions-direction work-status"
|
||||
label="工作状态" labelClassName="descriptions-label">
|
||||
<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">
|
||||
<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">
|
||||
<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
|
||||
}} ℃</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
|
||||
}} ℃</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: '℃', pointName: '平均温度'},
|
||||
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: 'Ω', 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: '℃'},
|
||||
{label: '绝缘电阻', attr: 'stackInsulationResistance', unit: 'Ω'},
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -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 {
|
||||
|
||||
@ -41,23 +41,53 @@
|
||||
:key="dataIndex + 'dbField'"
|
||||
:span="8"
|
||||
class="device-info-col"
|
||||
:class="{ 'field-disabled': !item.pointId }"
|
||||
>
|
||||
<div class="field-click-wrapper" @click="handleFieldClick(item)">
|
||||
<span class="left">{{ item.fieldName }}</span>
|
||||
<span class="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;
|
||||
|
||||
@ -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 });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -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});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -41,23 +41,53 @@
|
||||
:key="dataIndex + 'emsField'"
|
||||
:span="6"
|
||||
class="device-info-col"
|
||||
:class="{ 'field-disabled': !item.pointId }"
|
||||
>
|
||||
<div class="field-click-wrapper" @click="handleFieldClick(item)">
|
||||
<span class="left">{{ item.fieldName }}</span>
|
||||
<span class="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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -41,23 +41,53 @@
|
||||
:key="dataIndex + 'ylField'"
|
||||
:span="8"
|
||||
class="device-info-col"
|
||||
:class="{ 'field-disabled': !item.pointId }"
|
||||
>
|
||||
<div class="field-click-wrapper" @click="handleFieldClick(item)">
|
||||
<span class="left">{{ item.fieldName }}</span>
|
||||
<span class="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;
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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: "请先选择站点",
|
||||
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;
|
||||
});
|
||||
return cb([]);
|
||||
this.setRow(type, index, Object.assign({}, row, { pointOptions: uniqueOptions }));
|
||||
},
|
||||
fetchPointOptions(index, type, query = "", { force = false } = {}) {
|
||||
const row = this.getRow(type, index);
|
||||
if (!row || !this.formData.siteId) return Promise.resolve([]);
|
||||
const normalizedQuery = String(query || "").trim();
|
||||
const cacheKey = `${this.formData.siteId}_${normalizedQuery}`;
|
||||
if (!force && row.pointOptionsCache?.[cacheKey]) {
|
||||
this.setPointOptions(index, type, row.pointOptionsCache[cacheKey]);
|
||||
return Promise.resolve(row.pointOptionsCache[cacheKey]);
|
||||
}
|
||||
getPointMatchList({
|
||||
const requestId = Number(row.pointRequestId || 0) + 1;
|
||||
this.setRow(type, index, Object.assign({}, row, { pointRequestId: requestId, pointLoading: true }));
|
||||
return getPointMatchList({
|
||||
siteId: this.formData.siteId,
|
||||
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;
|
||||
})
|
||||
.finally(() => {
|
||||
const latestRow = this.getRow(type, index);
|
||||
if (!latestRow || latestRow.pointRequestId !== requestId) return;
|
||||
this.setRow(type, index, Object.assign({}, latestRow, { pointLoading: false }));
|
||||
});
|
||||
},
|
||||
remotePointSearch(index, type, query) {
|
||||
this.fetchPointOptions(index, type, query);
|
||||
},
|
||||
handlePointDropdownVisible(index, type, visible) {
|
||||
if (!visible) return;
|
||||
this.fetchPointOptions(index, type, "");
|
||||
},
|
||||
handlePointChange(index, type, value) {
|
||||
const row = this.getRow(type, index);
|
||||
if (!row) return;
|
||||
if (!value) {
|
||||
this.setRow(
|
||||
type,
|
||||
index,
|
||||
Object.assign({}, row, {
|
||||
point: "",
|
||||
pointName: "",
|
||||
deviceId: "",
|
||||
deviceName: "",
|
||||
deviceCategory: "",
|
||||
categoryName: "",
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(() => cb([]));
|
||||
},
|
||||
fillPointMetaByPoint(point, index, type) {
|
||||
if (!this.formData.siteId || !point) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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("");
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user