498 lines
14 KiB
Vue
498 lines
14 KiB
Vue
<template>
|
||
<div>
|
||
<div class="pcs-tags">
|
||
<el-tag
|
||
size="small"
|
||
:type="selectedSectionKey ? 'info' : 'primary'"
|
||
:effect="selectedSectionKey ? 'plain' : 'dark'"
|
||
class="pcs-tag-item"
|
||
@click="handleTagClick('')"
|
||
>
|
||
全部
|
||
</el-tag>
|
||
<el-tag
|
||
v-for="(group, index) in sectionGroups"
|
||
:key="index + 'dbTag'"
|
||
size="small"
|
||
:type="selectedSectionKey === group.sectionKey ? 'primary' : 'info'"
|
||
:effect="selectedSectionKey === group.sectionKey ? 'dark' : 'plain'"
|
||
class="pcs-tag-item"
|
||
@click="handleTagClick(group.sectionKey)"
|
||
>
|
||
{{ group.displayName || "电表" }}
|
||
</el-tag>
|
||
</div>
|
||
<el-card
|
||
v-for="(group, index) in filteredSectionGroups"
|
||
:key="index + 'dbSection'"
|
||
class="sbjk-card-container list running-card-container"
|
||
shadow="always"
|
||
>
|
||
<div slot="header">
|
||
<span class="large-title">{{ group.displayName || "电表" }}</span>
|
||
<div class="info">
|
||
<div>状态:{{ group.statusText }}</div>
|
||
<div>数据更新时间:{{ group.updateTimeText }}</div>
|
||
</div>
|
||
</div>
|
||
<el-row class="device-info-row">
|
||
<el-col
|
||
v-for="(item, dataIndex) in group.items"
|
||
: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, getPointConfigCurve } from "@/api/ems/site";
|
||
|
||
export default {
|
||
name: "DzjkSbjkDb",
|
||
mixins: [getQuerySiteId, intervalUpdate],
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
displayData: [],
|
||
selectedSectionKey: "",
|
||
ammeterDeviceList: [],
|
||
curveDialogVisible: false,
|
||
curveDialogTitle: "点位曲线",
|
||
curveChart: null,
|
||
curveLoading: false,
|
||
curveCustomRange: [],
|
||
curveQuery: {
|
||
siteId: "",
|
||
pointId: "",
|
||
pointType: "data",
|
||
rangeType: "custom",
|
||
startTime: "",
|
||
endTime: "",
|
||
},
|
||
};
|
||
},
|
||
computed: {
|
||
moduleDisplayData() {
|
||
return (this.displayData || []).filter((item) => item.menuCode === "SBJK_DB");
|
||
},
|
||
dbTemplateFields() {
|
||
const source = this.moduleDisplayData || [];
|
||
const result = [];
|
||
const seen = new Set();
|
||
source.forEach((item) => {
|
||
const fieldName = String(item?.fieldName || "").trim();
|
||
if (!fieldName || seen.has(fieldName)) {
|
||
return;
|
||
}
|
||
seen.add(fieldName);
|
||
result.push(fieldName);
|
||
});
|
||
return result.length > 0 ? result : this.fallbackFields;
|
||
},
|
||
sectionGroups() {
|
||
const source = this.moduleDisplayData || [];
|
||
const devices = (this.ammeterDeviceList || []).length > 0
|
||
? this.ammeterDeviceList
|
||
: [{ deviceId: "", deviceName: "电表" }];
|
||
|
||
return devices.map((device, index) => {
|
||
const deviceId = String(device?.deviceId || device?.id || "").trim();
|
||
const sectionKey = deviceId || `AMMETER_${index}`;
|
||
const displayName = String(device?.deviceName || device?.name || deviceId || `电表${index + 1}`).trim();
|
||
const exactRows = source.filter((item) => String(item?.deviceId || "").trim() === deviceId);
|
||
const fallbackRows = source.filter((item) => !String(item?.deviceId || "").trim());
|
||
|
||
const exactValueMap = {};
|
||
exactRows.forEach((item) => {
|
||
const key = String(item?.fieldName || "").trim();
|
||
if (key) {
|
||
exactValueMap[key] = item;
|
||
}
|
||
});
|
||
const fallbackValueMap = {};
|
||
fallbackRows.forEach((item) => {
|
||
const key = String(item?.fieldName || "").trim();
|
||
if (key && fallbackValueMap[key] === undefined) {
|
||
fallbackValueMap[key] = item;
|
||
}
|
||
});
|
||
|
||
const items = (this.dbTemplateFields || []).map((fieldName) => {
|
||
const row = exactValueMap[fieldName] || fallbackValueMap[fieldName] || {};
|
||
return {
|
||
fieldName,
|
||
fieldValue: row.fieldValue,
|
||
valueTime: row.valueTime,
|
||
pointId: String(row?.dataPoint || "").trim(),
|
||
raw: row,
|
||
};
|
||
});
|
||
|
||
const statusItem = (items || []).find((it) => String(it.fieldName || "").includes("状态"));
|
||
const timestamps = [...exactRows, ...fallbackRows]
|
||
.map((it) => new Date(it?.valueTime).getTime())
|
||
.filter((ts) => !isNaN(ts));
|
||
|
||
return {
|
||
sectionName: displayName,
|
||
sectionKey,
|
||
displayName,
|
||
deviceId,
|
||
items,
|
||
statusText: this.displayValue(statusItem ? statusItem.fieldValue : "-"),
|
||
updateTimeText: timestamps.length > 0 ? this.formatDate(new Date(Math.max(...timestamps))) : "-",
|
||
};
|
||
});
|
||
},
|
||
displaySectionGroups() {
|
||
if (this.sectionGroups.length > 0) {
|
||
return this.sectionGroups;
|
||
}
|
||
return [
|
||
{
|
||
sectionName: "电参量",
|
||
sectionKey: "电参量",
|
||
displayName: "电表",
|
||
items: this.fallbackFields.map((fieldName) => ({ fieldName, fieldValue: "-" })),
|
||
statusText: "-",
|
||
updateTimeText: "-",
|
||
},
|
||
];
|
||
},
|
||
filteredSectionGroups() {
|
||
const groups = this.displaySectionGroups || [];
|
||
if (!this.selectedSectionKey) {
|
||
return groups;
|
||
}
|
||
return groups.filter((group) => group.sectionKey === this.selectedSectionKey);
|
||
},
|
||
fallbackFields() {
|
||
return [
|
||
"正向有功电能",
|
||
"反向有功电能",
|
||
"正向无功电能",
|
||
"反向无功电能",
|
||
"有功功率",
|
||
"无功功率",
|
||
];
|
||
},
|
||
},
|
||
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 || "";
|
||
},
|
||
displayValue(value) {
|
||
return value === undefined || value === null || value === "" ? "-" : value;
|
||
},
|
||
isPointLoading(value) {
|
||
return this.loading && (value === undefined || value === null || value === "" || value === "-");
|
||
},
|
||
formatDate(date) {
|
||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||
return "-";
|
||
}
|
||
const p = (n) => String(n).padStart(2, "0");
|
||
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(
|
||
date.getMinutes()
|
||
)}:${p(date.getSeconds())}`;
|
||
},
|
||
resolveDbDisplayName(sectionName) {
|
||
const key = String(sectionName || "").trim();
|
||
if (!key) {
|
||
return "电表";
|
||
}
|
||
const list = this.ammeterDeviceList || [];
|
||
const matched = list.find((item) => {
|
||
const deviceId = String(item.deviceId || item.id || "").trim();
|
||
const deviceName = String(item.deviceName || item.name || "").trim();
|
||
return key === deviceId || key === deviceName;
|
||
});
|
||
if (matched) {
|
||
return matched.deviceName || matched.name || key;
|
||
}
|
||
return key;
|
||
},
|
||
getAmmeterDeviceList() {
|
||
return getDeviceList(this.siteId)
|
||
.then((response) => {
|
||
const list = response?.data || [];
|
||
this.ammeterDeviceList = list.filter((item) => item.deviceCategory === "AMMETER");
|
||
})
|
||
.catch(() => {
|
||
this.ammeterDeviceList = [];
|
||
});
|
||
},
|
||
updateData() {
|
||
this.loading = true;
|
||
Promise.all([getProjectDisplayData(this.siteId), this.getAmmeterDeviceList()])
|
||
.then(([response]) => {
|
||
this.displayData = response?.data || [];
|
||
})
|
||
.finally(() => {
|
||
this.loading = false;
|
||
});
|
||
},
|
||
init() {
|
||
this.updateData();
|
||
this.updateInterval(this.updateData);
|
||
},
|
||
},
|
||
beforeDestroy() {
|
||
if (this.curveChart) {
|
||
this.curveChart.dispose();
|
||
this.curveChart = null;
|
||
}
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.sbjk-card-container {
|
||
&.list:not(:last-child) {
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.info {
|
||
float: right;
|
||
text-align: right;
|
||
font-size: 12px;
|
||
color: #666;
|
||
line-height: 18px;
|
||
}
|
||
}
|
||
|
||
.pcs-tags {
|
||
margin: 0 0 12px;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
justify-content: flex-start;
|
||
align-items: center;
|
||
}
|
||
|
||
.pcs-tag-item {
|
||
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;
|
||
transform-origin: center;
|
||
animation: pointLoadingSpinPulse 1.1s linear infinite;
|
||
}
|
||
@keyframes pointLoadingSpinPulse {
|
||
0% { opacity: 0.45; transform: rotate(0deg) scale(0.9); }
|
||
50% { opacity: 1; transform: rotate(180deg) scale(1.08); }
|
||
100% { opacity: 0.45; transform: rotate(360deg) scale(0.9); }
|
||
}
|
||
</style>
|