1.一周充放曲线改为了时间聚合柱状图。

2.PCS最高温度修复bug展示多PCS设备的数据
3.PCS的状态根据状态枚举映射配置的内容显示
4.BMS的总览,增加工作状态、与PCS通讯、与EMS通讯的配置,及关联展示
5.增加批量导入单体电池点位的功能
6.修复计算点可能会出现id与code在一个池子内的问题,再次计算后数据正常
7.计算点增加小数位限制的功能,实时计算与7天历史接口都已经按照配置的小数位进行限制
8.统计报表中的功率曲线改为了按照分钟显示
9.功率曲线出现断点的问题是因为数据计算太密集了导致的,增加了前端连线不断的显示
10.PCS和电池堆的曲线与配置增加了关联设备显示
11.点位映射中的电池温度,增加了多设备
12.收益报表增加升序排列,合并当月所有合计
13.增加业务报表备注功能,可以根据业务设计开发,目前电表报表与收益报表均有备注列可以修改
This commit is contained in:
xiaoyang
2026-04-12 15:18:00 +08:00
parent fd860597de
commit 20df411925
17 changed files with 1272 additions and 460 deletions

View File

@ -363,27 +363,20 @@ function resolveRangeKind(startDate, endDate) {
const end = new Date(`${normalizeDateInput(endDate || startDate)} 00:00:00`)
if (isNaN(start.getTime()) || isNaN(end.getTime())) return 'day'
const diffDays = Math.floor((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000))
if (diffDays <= 0) return 'minute'
if (diffDays < 30) return 'day'
if (diffDays < 31) return 'minute'
if (diffDays < 180) return 'day'
return 'month'
}
function formatTimeLabelByKind(date, kind = 'day') {
const p = (n) => String(n).padStart(2, '0')
if (kind === 'minute') return `${p(date.getHours())}:${p(date.getMinutes())}`
if (kind === 'minute') return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`
if (kind === 'month') return `${date.getFullYear()}-${p(date.getMonth() + 1)}`
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`
}
function buildSortedLabels(labelSet, kind = 'day') {
const labels = Array.from(labelSet || [])
if (kind === 'minute') {
return labels.sort((a, b) => {
const [ah = 0, am = 0] = String(a || '').split(':').map(v => Number(v) || 0)
const [bh = 0, bm = 0] = String(b || '').split(':').map(v => Number(v) || 0)
return ah * 60 + am - (bh * 60 + bm)
})
}
return labels.sort()
}
@ -768,6 +761,22 @@ export function getAmmeterRevenueData(data) {
})
}
export function batchGetBizRemark(data) {
return request({
url: `/system/bizRemark/batchGet`,
method: 'post',
data
})
}
export function saveBizRemark(data) {
return request({
url: `/system/bizRemark/save`,
method: 'post',
data
})
}
//策略列表
export function strategyRunningList(siteId) {
return request({

View File

@ -260,6 +260,28 @@ export function getPointMatchList(params) {
})
}
// 下载单体电池导入模板
export function downloadSingleBatteryMonitorImportTemplate(siteId) {
return request({
url: `/ems/siteConfig/downloadSingleBatteryMonitorImportTemplate`,
method: 'get',
params: { siteId },
responseType: 'blob'
})
}
// 导入单体电池与监控点位映射
export function importSingleBatteryMonitorMappings(data) {
return request({
url: `/ems/siteConfig/importSingleBatteryMonitorMappings`,
method: 'post',
data,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 点位配置详情
export function getPointMatchDetail(id) {
return request({

View File

@ -1,10 +1,17 @@
<template>
<el-card shadow="always" class="common-card-container common-card-container-body-no-padding time-range-card">
<div slot="header" class="time-range-header">
<span class="card-title">一周充放曲线</span>
<span class="card-title">一周充放柱状图</span>
<date-range-select ref="dateRangeSelect" :showIcon="true" :mini-time-picker="true" @updateDate="updateDate"/>
</div>
<div style="height: 310px" id="weekChart"></div>
<div class="card-main">
<div class="total-data">
<div>总充电量:<span class="point">{{ formatMetricValue(summary.totalChargedCap) }}kWh</span></div>
<div>总放电量:<span class="point">{{ formatMetricValue(summary.totalDisChargedCap) }}kWh</span></div>
<div>综合效率:<span class="point">{{ formatPercentValue(summary.efficiency) }}%</span></div>
</div>
<div ref="weekChartRef" class="week-chart-canvas"></div>
</div>
</el-card>
</template>
@ -14,6 +21,16 @@ import resize from '@/mixins/ems/resize'
import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue'
import { getPointConfigCurve } from '@/api/ems/site'
const DAY = 24 * 60 * 60 * 1000
function createEmptySummary() {
return {
totalChargedCap: '',
totalDisChargedCap: '',
efficiency: ''
}
}
export default {
mixins: [resize],
components: { DateRangeSelect },
@ -27,7 +44,8 @@ export default {
return {
chart: null,
timeRange: [],
siteId: ''
siteId: '',
summary: createEmptySummary()
}
},
watch: {
@ -50,7 +68,6 @@ export default {
this.chart = null
},
methods: {
// 更新时间范围 重置图表
updateDate(data) {
this.timeRange = data
this.getWeekKData()
@ -75,6 +92,7 @@ export default {
const list = curveResponse?.data || []
return {
name: row.fieldName || row.fieldCode || pointId,
fieldCode: row.fieldCode || '',
data: list
.map(item => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter(item => item[0] && !Number.isNaN(item[1]))
@ -86,14 +104,13 @@ export default {
})
},
init(siteId) {
//初始化 清空数据
this.siteId = siteId
this.timeRange = []
this.deviceId = ''
this.summary = createEmptySummary()
this.$refs.dateRangeSelect.init()
},
initChart() {
this.chart = echarts.init(document.querySelector('#weekChart'))
this.chart = echarts.init(this.$refs.weekChartRef)
},
normalizeDateTime(value, endOfDay) {
const raw = String(value || '').trim()
@ -106,41 +123,317 @@ export default {
const t = new Date(value).getTime()
return Number.isNaN(t) ? null : t
},
startOfDay(timestamp) {
const date = new Date(timestamp)
date.setHours(0, 0, 0, 0)
return date.getTime()
},
formatMetricValue(value) {
const num = Number(value)
if (value === '' || value == null || Number.isNaN(num)) return '--'
return this.formatNumber(num)
},
formatPercentValue(value) {
const num = Number(value)
if (value === '' || value == null || Number.isNaN(num)) return '--'
return this.formatNumber(num)
},
formatNumber(value) {
const num = Number(value)
if (Number.isNaN(num)) return '--'
return num.toLocaleString('zh-CN', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
})
},
formatDateLabel(timestamp) {
const date = new Date(timestamp)
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day}`
},
formatTooltipDate(timestamp) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
buildDatasetSource(labels = [], chargeData = [], dischargeData = [], efficiencyData = []) {
const source = [['日期', '充电量', '放电量', '效率']]
labels.forEach((label, index) => {
source.push([
label,
Number(chargeData[index]?.value || 0),
Number(dischargeData[index]?.value || 0),
Number(efficiencyData[index]?.value || 0)
])
})
return source
},
resolveSeriesType(item = {}) {
const text = `${item?.name || ''} ${item?.fieldCode || ''}`.toLowerCase()
if (text.includes('放') || text.includes('discharge') || text.includes('discharged')) {
return 'discharge'
}
if (text.includes('充') || text.includes('charge') || text.includes('charged')) {
return 'charge'
}
return ''
},
buildDailyChartData(seriesData = []) {
const normalizedRange = this.timeRange || []
const startTime = this.parseToTimestamp(this.normalizeDateTime(normalizedRange[0], false))
const endTime = this.parseToTimestamp(this.normalizeDateTime(normalizedRange[1], true))
if (!startTime || !endTime || endTime < startTime) {
return {
labels: [],
chargeData: [],
dischargeData: [],
efficiencyData: [],
summary: createEmptySummary()
}
}
const bucketStarts = []
for (let cursor = this.startOfDay(startTime); cursor <= this.startOfDay(endTime); cursor += DAY) {
bucketStarts.push(cursor)
}
const chargeMap = {}
const dischargeMap = {}
;(seriesData || []).forEach(item => {
const seriesType = this.resolveSeriesType(item)
if (!seriesType) return
;(item?.data || []).forEach(([timestamp, pointValue]) => {
if (!timestamp || Number.isNaN(pointValue)) return
if (timestamp < startTime || timestamp > endTime) return
const bucketStart = this.startOfDay(timestamp)
const normalizedValue = Math.abs(Number(pointValue) || 0)
if (seriesType === 'charge') {
chargeMap[bucketStart] = (chargeMap[bucketStart] || 0) + normalizedValue
} else if (seriesType === 'discharge') {
dischargeMap[bucketStart] = (dischargeMap[bucketStart] || 0) + normalizedValue
}
})
})
const labels = []
const chargeData = []
const dischargeData = []
const efficiencyData = []
bucketStarts.forEach(bucketStart => {
const chargedCap = Number(chargeMap[bucketStart] || 0)
const disChargedCap = Number(dischargeMap[bucketStart] || 0)
const dailyEfficiency = chargedCap > 0
? Number(((disChargedCap / chargedCap) * 100).toFixed(2))
: 0
labels.push(this.formatDateLabel(bucketStart))
chargeData.push({
value: chargedCap,
bucketStart
})
dischargeData.push({
value: disChargedCap,
bucketStart
})
efficiencyData.push({
value: dailyEfficiency,
bucketStart
})
})
const totalChargedCap = chargeData.reduce((sum, item) => sum + Number(item.value || 0), 0)
const totalDisChargedCap = dischargeData.reduce((sum, item) => sum + Number(item.value || 0), 0)
const efficiency = totalChargedCap > 0
? Number(((totalDisChargedCap / totalChargedCap) * 100).toFixed(2))
: 0
return {
labels,
chargeData,
dischargeData,
efficiencyData,
summary: {
totalChargedCap,
totalDisChargedCap,
efficiency
}
}
},
resolveEfficiencyAxisMax(efficiencyData = []) {
const maxValue = efficiencyData.reduce((max, item) => Math.max(max, Number(item?.value || 0)), 0)
if (maxValue <= 100) return 100
return Math.ceil(maxValue / 20) * 20
},
renderEmptyState(message = '暂无数据') {
if (!this.chart) return
this.chart.clear()
this.chart.setOption({
graphic: {
type: 'text',
left: 'center',
top: 'middle',
style: {
text: message,
fill: '#909399',
fontSize: 14
}
}
})
},
setOption(seriesData = []) {
this.chart && this.chart.setOption({
color: ['#4472c4', '#70ad47'],//所有充放电颜色保持统一
if (!this.chart) return
const { labels, chargeData, dischargeData, efficiencyData, summary } = this.buildDailyChartData(seriesData)
const hasValue = chargeData.some(item => Number(item?.value || 0) > 0) || dischargeData.some(item => Number(item?.value || 0) > 0)
if (!labels.length || !hasValue) {
this.summary = createEmptySummary()
this.renderEmptyState()
return
}
this.summary = summary
const efficiencyAxisMax = this.resolveEfficiencyAxisMax(efficiencyData)
const source = this.buildDatasetSource(labels, chargeData, dischargeData, efficiencyData)
this.chart.clear()
this.chart.setOption({
color: ['#4472c4', '#70ad47', '#ffbd00'],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
axisPointer: {
type: 'shadow'
},
grid: {
containLabel: true
formatter: (params = []) => {
if (!params.length) return ''
const dataIndex = Number(params[0]?.dataIndex)
const bucketStart = chargeData[dataIndex]?.bucketStart
const lines = [this.formatTooltipDate(bucketStart)]
params.forEach(item => {
const rawValue = Array.isArray(item?.value) ? item.value[item.seriesIndex + 1] : item?.value
const value = Number(rawValue || 0)
const unit = item.seriesType === 'line' ? '%' : 'kWh'
lines.push(`${item.marker}${item.seriesName}: ${this.formatNumber(value)}${unit}`)
})
return lines.join('<br/>')
},
extraCssText: 'max-width: 420px; white-space: normal;'
},
legend: {
left: 'center',
bottom: '15',
bottom: 15
},
xAxis: {
type: 'time'
grid: {
top: 40,
containLabel: true,
left: 20,
right: 20,
bottom: 60
},
yAxis: [{
yAxis: [
{
type: 'value',
name: '充电量/放电量kWh',
axisLine: {
lineStyle: {
color: '#333333',
color: '#333333'
}
,
onZero: false
}
},
{
type: 'value',
name: '效率%',
max: efficiencyAxisMax,
axisLine: {
lineStyle: {
color: '#333333'
},
onZero: false
}
}
],
xAxis: [{
type: 'category',
name: '单位:日',
nameLocation: 'center',
nameGap: 30,
axisTick: {
show: false
},
axisLabel: {
interval: 0
}
}],
series: seriesData.map(item => ({
name: item.name,
dataset: {
source
},
series: [
{
name: '充电量',
yAxisIndex: 0,
type: 'bar',
data: item.data
}))
color: '#4472c4',
barMaxWidth: 22
},
{
name: '放电量',
yAxisIndex: 0,
type: 'bar',
color: '#70ad47',
barMaxWidth: 22
},
{
name: '效率',
yAxisIndex: 1,
type: 'line',
color: '#ffbd00',
smooth: true,
symbol: 'circle',
symbolSize: 7,
lineStyle: {
width: 2
}
}
]
})
}
}
}
</script>
<style scoped lang="scss">
.card-main {
padding: 0 16px 12px;
}
.total-data {
line-height: 18px;
color: #333333;
font-size: 16px;
padding: 20px 0;
> div {
display: inline-block;
margin-right: 20px;
}
.point {
color: #05AEA3;
font-weight: 500;
font-size: 18px;
}
}
.week-chart-canvas {
height: 310px;
}
</style>

View File

@ -56,7 +56,7 @@
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'workStatus') }"
@click="handleFieldClick(baseInfo, 'workStatus', '工作状态')"
>
{{ CLUSTERWorkStatusOptions[baseInfo.workStatus] || '-' }}
{{ formatDictValue(clusterWorkStatusOptions, baseInfo.workStatus) }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
@ -66,7 +66,7 @@
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'pcsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'pcsCommunicationStatus', '与PCS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }}
{{ formatDictValue(clusterPcsCommunicationStatusOptions, baseInfo.pcsCommunicationStatus) }}
</span>
</el-descriptions-item>
<el-descriptions-item contentClassName="descriptions-direction"
@ -76,7 +76,7 @@
:class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'emsCommunicationStatus') }"
@click="handleFieldClick(baseInfo, 'emsCommunicationStatus', '与EMS通信')"
>
{{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }}
{{ formatDictValue(clusterEmsCommunicationStatusOptions, baseInfo.emsCommunicationStatus) }}
</span>
</el-descriptions-item>
</el-descriptions>
@ -205,7 +205,7 @@ import {getProjectDisplayData, getStackNameList, getClusterNameList} from '@/api
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import {mapState} from "vuex";
import {getPointConfigCurve} from "@/api/ems/site";
import {getPointConfigCurve, getSingleMonitorWorkStatusEnumMappings} from "@/api/ems/site";
export default {
name: 'DzjkSbjkBmsdcc',
@ -215,6 +215,15 @@ export default {
...mapState({
CLUSTERWorkStatusOptions: state => state?.ems?.CLUSTERWorkStatusOptions || {},
}),
clusterWorkStatusOptions() {
return this.getEnumOptions("CLUSTER", "workStatus", this.CLUSTERWorkStatusOptions || {});
},
clusterPcsCommunicationStatusOptions() {
return this.getEnumOptions("CLUSTER", "pcsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
clusterEmsCommunicationStatusOptions() {
return this.getEnumOptions("CLUSTER", "emsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
filteredBaseInfoList() {
if (!this.selectedClusterId) {
return this.baseInfoList || [];
@ -227,6 +236,7 @@ export default {
loading: false,
displayData: [],
clusterDeviceList: [],
siteEnumOptionMap: {},
selectedClusterId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
@ -286,9 +296,69 @@ export default {
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
normalizeDictKey(value) {
const raw = String(value == null ? "" : value).trim();
if (!raw) return "";
if (/^-?\d+(\.0+)?$/.test(raw)) {
return String(parseInt(raw, 10));
}
return raw;
},
formatDictValue(options, value) {
const dict = (options && typeof options === "object") ? options : {};
const key = this.normalizeDictKey(value);
if (!key) return "-";
return dict[key] || key;
},
buildEnumScopeKey(deviceCategory, matchField) {
return `${String(deviceCategory || "").trim()}|${String(matchField || "").trim()}`;
},
buildSiteEnumOptionMap(mappings = []) {
return (mappings || []).reduce((acc, item) => {
const scopeKey = this.buildEnumScopeKey(item?.deviceCategory, item?.matchField);
const dataEnumCode = this.normalizeDictKey(item?.dataEnumCode);
const enumCode = this.normalizeDictKey(item?.enumCode);
const enumName = String(item?.enumName || "").trim();
const optionKey = dataEnumCode || enumCode;
if (!scopeKey || !optionKey || !enumName) {
return acc;
}
if (!acc[scopeKey]) {
acc[scopeKey] = {};
}
acc[scopeKey][optionKey] = enumName;
return acc;
}, {});
},
loadSiteEnumOptions() {
if (!this.siteId) {
this.siteEnumOptionMap = {};
return Promise.resolve({});
}
return getSingleMonitorWorkStatusEnumMappings(this.siteId).then(response => {
const optionMap = this.buildSiteEnumOptionMap(response?.data || []);
this.siteEnumOptionMap = optionMap;
return optionMap;
}).catch(() => {
this.siteEnumOptionMap = {};
return {};
});
},
getEnumOptions(deviceCategory, matchField, fallback = {}) {
const scopeKey = this.buildEnumScopeKey(deviceCategory, matchField);
const siteOptions = this.siteEnumOptionMap[scopeKey];
if (siteOptions && Object.keys(siteOptions).length > 0) {
return siteOptions;
}
return fallback || {};
},
handleCardClass(item) {
const {workStatus = ''} = item
return !(Object.keys(this.CLUSTERWorkStatusOptions).includes(item.workStatus)) ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container'
const workStatus = this.normalizeDictKey((item && item.workStatus) || "");
const statusOptions = (this.clusterWorkStatusOptions && typeof this.clusterWorkStatusOptions === "object")
? this.clusterWorkStatusOptions
: {};
const hasStatus = Object.prototype.hasOwnProperty.call(statusOptions, workStatus);
return !hasStatus ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container';
},
// 查看设备电位表格
pointDetail(row, dataType) {
@ -596,6 +666,7 @@ export default {
Promise.all([
getProjectDisplayData(this.siteId),
this.getClusterDeviceList(),
this.loadSiteEnumOptions(),
]).then(([response]) => {
this.displayData = response?.data || [];
this.buildBaseInfoList();

View File

@ -50,19 +50,19 @@
contentClassName="descriptions-direction work-status"
label="工作状态" labelClassName="descriptions-label">
<span class="pointer" @click="handleStatusFieldClick(baseInfo, 'workStatus', '工作状态')">
{{ STACKWorkStatusOptions[baseInfo.workStatus] || '-' }}
{{ formatDictValue(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] || '-' }}
{{ formatDictValue(stackPcsCommunicationStatusOptions, 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] || '-' }}
{{ formatDictValue(stackEmsCommunicationStatusOptions, baseInfo.emsCommunicationStatus) }}
</span>
</el-descriptions-item>
</el-descriptions>
@ -123,7 +123,7 @@
<script>
import * as echarts from "echarts";
import {getProjectDisplayData, getStackNameList} from '@/api/ems/dzjk'
import {getPointConfigCurve} from "@/api/ems/site";
import {getPointConfigCurve, getSingleMonitorWorkStatusEnumMappings} from "@/api/ems/site";
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import intervalUpdate from "@/mixins/ems/intervalUpdate";
import PointTable from "@/views/ems/site/sblb/PointTable.vue";
@ -137,6 +137,15 @@ export default {
...mapState({
STACKWorkStatusOptions: state => state?.ems?.STACKWorkStatusOptions || {},
}),
stackWorkStatusOptions() {
return this.getEnumOptions("STACK", "workStatus", this.STACKWorkStatusOptions || {});
},
stackPcsCommunicationStatusOptions() {
return this.getEnumOptions("STACK", "pcsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
stackEmsCommunicationStatusOptions() {
return this.getEnumOptions("STACK", "emsCommunicationStatus", (this.$store.state.ems && this.$store.state.ems.communicationStatusOptions) || {});
},
filteredBaseInfoList() {
if (!this.selectedStackId) {
return this.baseInfoList || [];
@ -149,6 +158,7 @@ export default {
loading: false,
displayData: [],
stackDeviceList: [],
siteEnumOptionMap: {},
selectedStackId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
@ -191,9 +201,69 @@ export default {
isPointLoading(value) {
return this.loading && (value === undefined || value === null || value === "" || value === "-");
},
normalizeDictKey(value) {
const raw = String(value == null ? "" : value).trim();
if (!raw) return "";
if (/^-?\d+(\.0+)?$/.test(raw)) {
return String(parseInt(raw, 10));
}
return raw;
},
formatDictValue(options, value) {
const dict = (options && typeof options === "object") ? options : {};
const key = this.normalizeDictKey(value);
if (!key) return "-";
return dict[key] || key;
},
buildEnumScopeKey(deviceCategory, matchField) {
return `${String(deviceCategory || "").trim()}|${String(matchField || "").trim()}`;
},
buildSiteEnumOptionMap(mappings = []) {
return (mappings || []).reduce((acc, item) => {
const scopeKey = this.buildEnumScopeKey(item?.deviceCategory, item?.matchField);
const dataEnumCode = this.normalizeDictKey(item?.dataEnumCode);
const enumCode = this.normalizeDictKey(item?.enumCode);
const enumName = String(item?.enumName || "").trim();
const optionKey = dataEnumCode || enumCode;
if (!scopeKey || !optionKey || !enumName) {
return acc;
}
if (!acc[scopeKey]) {
acc[scopeKey] = {};
}
acc[scopeKey][optionKey] = enumName;
return acc;
}, {});
},
loadSiteEnumOptions() {
if (!this.siteId) {
this.siteEnumOptionMap = {};
return Promise.resolve({});
}
return getSingleMonitorWorkStatusEnumMappings(this.siteId).then(response => {
const optionMap = this.buildSiteEnumOptionMap(response?.data || []);
this.siteEnumOptionMap = optionMap;
return optionMap;
}).catch(() => {
this.siteEnumOptionMap = {};
return {};
});
},
getEnumOptions(deviceCategory, matchField, fallback = {}) {
const scopeKey = this.buildEnumScopeKey(deviceCategory, matchField);
const siteOptions = this.siteEnumOptionMap[scopeKey];
if (siteOptions && Object.keys(siteOptions).length > 0) {
return siteOptions;
}
return fallback || {};
},
handleCardClass(item) {
const {workStatus = ''} = item
return !Object.keys(this.STACKWorkStatusOptions).find(i => i === workStatus) ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container'
const workStatus = this.normalizeDictKey((item && item.workStatus) || "");
const statusOptions = (this.stackWorkStatusOptions && typeof this.stackWorkStatusOptions === "object")
? this.stackWorkStatusOptions
: {};
const hasStatus = Object.prototype.hasOwnProperty.call(statusOptions, workStatus);
return !hasStatus ? "timing-card-container" : workStatus === '9' ? 'warning-card-container' : 'running-card-container';
},
// 查看设备电位表格
@ -487,6 +557,7 @@ export default {
Promise.all([
getProjectDisplayData(this.siteId),
this.getStackDeviceList(),
this.loadSiteEnumOptions(),
]).then(([displayResponse]) => {
this.displayData = displayResponse?.data || [];
this.buildBaseInfoList();

View File

@ -64,7 +64,7 @@
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'workStatus') }"
@click="handlePcsFieldClick(pcsItem, 'workStatus', '工作状态')"
>
{{ formatDictValue((PCSWorkStatusOptions || {}), pcsItem.workStatus) }}
{{ formatDictValue(pcsWorkStatusOptions, pcsItem.workStatus) }}
</span>
</el-descriptions-item
>
@ -79,7 +79,7 @@
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'gridStatus') }"
@click="handlePcsFieldClick(pcsItem, 'gridStatus', '并网状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.gridStatusOptions) || {}), pcsItem.gridStatus) }}
{{ formatDictValue(pcsGridStatusOptions, pcsItem.gridStatus) }}
</span>
</el-descriptions-item
>
@ -96,7 +96,7 @@
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'deviceStatus') }"
@click="handlePcsFieldClick(pcsItem, 'deviceStatus', '设备状态')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.deviceStatusOptions) || {}), pcsItem.deviceStatus) }}
{{ formatDictValue(pcsDeviceStatusOptions, pcsItem.deviceStatus) }}
</span>
</el-descriptions-item
>
@ -111,7 +111,7 @@
:class="{ 'field-disabled': !hasFieldPointId(pcsItem, 'controlMode') }"
@click="handlePcsFieldClick(pcsItem, 'controlMode', '控制模式')"
>
{{ formatDictValue((($store.state.ems && $store.state.ems.controlModeOptions) || {}), pcsItem.controlMode) }}
{{ formatDictValue(pcsControlModeOptions, pcsItem.controlMode) }}
</span>
</el-descriptions-item
>
@ -242,7 +242,7 @@ 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";
import {getPointConfigCurve, getSingleMonitorWorkStatusEnumMappings} from "@/api/ems/site";
export default {
name: "DzjkSbjkPcs",
@ -252,6 +252,18 @@ export default {
...mapState({
PCSWorkStatusOptions: state => state?.ems?.PCSWorkStatusOptions || {},
}),
pcsWorkStatusOptions() {
return this.getEnumOptions("PCS", "workStatus", this.PCSWorkStatusOptions || {});
},
pcsGridStatusOptions() {
return this.getEnumOptions("PCS", "gridStatus", (this.$store.state.ems && this.$store.state.ems.gridStatusOptions) || {});
},
pcsDeviceStatusOptions() {
return this.getEnumOptions("PCS", "deviceStatus", (this.$store.state.ems && this.$store.state.ems.deviceStatusOptions) || {});
},
pcsControlModeOptions() {
return this.getEnumOptions("PCS", "controlMode", (this.$store.state.ems && this.$store.state.ems.controlModeOptions) || {});
},
filteredPcsList() {
if (!this.selectedPcsId) {
return this.pcsList || [];
@ -264,6 +276,7 @@ export default {
loading: false,
displayData: [],
pcsDeviceList: [],
siteEnumOptionMap: {},
selectedPcsId: "",
curveDialogVisible: false,
curveDialogTitle: "点位曲线",
@ -388,10 +401,52 @@ export default {
normalizeDeviceId(value) {
return String(value == null ? "" : value).trim().toUpperCase();
},
buildEnumScopeKey(deviceCategory, matchField) {
return `${String(deviceCategory || "").trim()}|${String(matchField || "").trim()}`;
},
buildSiteEnumOptionMap(mappings = []) {
return (mappings || []).reduce((acc, item) => {
const scopeKey = this.buildEnumScopeKey(item?.deviceCategory, item?.matchField);
const dataEnumCode = this.normalizeDictKey(item?.dataEnumCode);
const enumCode = this.normalizeDictKey(item?.enumCode);
const enumName = String(item?.enumName || "").trim();
const optionKey = dataEnumCode || enumCode;
if (!scopeKey || !optionKey || !enumName) {
return acc;
}
if (!acc[scopeKey]) {
acc[scopeKey] = {};
}
acc[scopeKey][optionKey] = enumName;
return acc;
}, {});
},
loadSiteEnumOptions() {
if (!this.siteId) {
this.siteEnumOptionMap = {};
return Promise.resolve({});
}
return getSingleMonitorWorkStatusEnumMappings(this.siteId).then(response => {
const optionMap = this.buildSiteEnumOptionMap(response?.data || []);
this.siteEnumOptionMap = optionMap;
return optionMap;
}).catch(() => {
this.siteEnumOptionMap = {};
return {};
});
},
getEnumOptions(deviceCategory, matchField, fallback = {}) {
const scopeKey = this.buildEnumScopeKey(deviceCategory, matchField);
const siteOptions = this.siteEnumOptionMap[scopeKey];
if (siteOptions && Object.keys(siteOptions).length > 0) {
return siteOptions;
}
return fallback || {};
},
handleCardClass(item) {
const workStatus = this.normalizeDictKey((item && item.workStatus) || "");
const statusOptions = (this.PCSWorkStatusOptions && typeof this.PCSWorkStatusOptions === 'object')
? this.PCSWorkStatusOptions
const statusOptions = (this.pcsWorkStatusOptions && typeof this.pcsWorkStatusOptions === 'object')
? this.pcsWorkStatusOptions
: {};
const hasStatus = Object.prototype.hasOwnProperty.call(statusOptions, workStatus);
return workStatus === '1' || !hasStatus
@ -651,6 +706,7 @@ export default {
Promise.all([
getProjectDisplayData(this.siteId),
this.getPcsDeviceList(),
this.loadSiteEnumOptions(),
]).then(([displayResponse]) => {
this.displayData = displayResponse?.data || [];
this.buildPcsList();

View File

@ -68,7 +68,7 @@ export default {
.then((response) => {
const list = response?.data || [];
return {
name: row.fieldName || row.fieldCode || pointId,
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId),
data: list
.map((item) => [
this.parseToTimestamp(item.dataTime),

View File

@ -41,32 +41,37 @@ export default {
methods: {
init(siteId,timeRange) {
const [startTime='', endTime=''] = timeRange;
const row = (this.displayData || []).find(
const query = {
siteId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
};
const rows = (this.displayData || []).filter(
(item) =>
item &&
item.fieldCode === "SBJK_SSYX__curveBatteryAveSoc" &&
item.useFixedDisplay !== 1 &&
item.dataPoint
);
const pointId = String(row?.dataPoint || "").trim();
if (!pointId) {
this.setOption([]);
return;
}
getPointConfigCurve({
siteId,
pointId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
})
.then((response) => {
const tasks = rows.map((row) => {
const pointId = String(row.dataPoint || "").trim();
if(!pointId) return Promise.resolve(null);
return getPointConfigCurve({
...query,
pointId
}).then((response) => {
const list = response?.data || [];
this.setOption(
list
return {
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId),
data: list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
);
};
}).catch(() => null);
});
Promise.all(tasks).then((series) => {
this.setOption((series || []).filter(Boolean));
});
},
normalizeDateTime(value, endOfDay) {
@ -80,7 +85,7 @@ export default {
const t = new Date(value).getTime();
return Number.isNaN(t) ? null : t;
},
setOption(data = []) {
setOption(seriesData = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
@ -119,18 +124,16 @@ export default {
end: 100,
},
],
series: [
{
series: seriesData.map(item => ({
type: "line",
name: `平均SOC`,
name: item.name,
showSymbol: false,
smooth: true,
areaStyle: {
opacity: 0.35
},
data,
},
],
data: item.data
})),
},true);
},
},

View File

@ -42,32 +42,37 @@ export default {
methods: {
init(siteId,timeRange) {
const [startTime='', endTime=''] = timeRange;
const row = (this.displayData || []).find(
const query = {
siteId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
};
const rows = (this.displayData || []).filter(
(item) =>
item &&
item.fieldCode === "SBJK_SSYX__curveBatteryAveTemp" &&
item.useFixedDisplay !== 1 &&
item.dataPoint
);
const pointId = String(row?.dataPoint || "").trim();
if (!pointId) {
this.setOption([]);
return;
}
getPointConfigCurve({
siteId,
pointId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
})
.then((response) => {
const tasks = rows.map((row) => {
const pointId = String(row.dataPoint || "").trim();
if(!pointId) return Promise.resolve(null);
return getPointConfigCurve({
...query,
pointId
}).then((response) => {
const list = response?.data || [];
this.setOption(
list
return {
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId),
data: list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
);
};
}).catch(() => null);
});
Promise.all(tasks).then((series) => {
this.setOption((series || []).filter(Boolean));
});
},
normalizeDateTime(value, endOfDay) {
@ -81,7 +86,7 @@ export default {
const t = new Date(value).getTime();
return Number.isNaN(t) ? null : t;
},
setOption(data = []) {
setOption(seriesData = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
@ -120,18 +125,16 @@ export default {
end: 100,
},
],
series: [
{
series: seriesData.map(item => ({
type: "line",
name: `电池平均温度`,
name: item.name,
showSymbol: false,
smooth: true,
areaStyle: {
opacity: 0.35
},
data,
},
],
data: item.data
})),
},true);
},
},

View File

@ -27,7 +27,6 @@ export default {
data() {
return {
chart: null,
seriesName: "PCS最高温度"
};
},
mounted() {
@ -43,33 +42,37 @@ export default {
methods: {
init(siteId,timeRange) {
const [startTime='', endTime=''] = timeRange;
const row = (this.displayData || []).find(
const query = {
siteId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
};
const rows = (this.displayData || []).filter(
(item) =>
item &&
item.fieldCode === "SBJK_SSYX__curvePcsMaxTemp" &&
item.useFixedDisplay !== 1 &&
item.dataPoint
);
const pointId = String(row?.dataPoint || "").trim();
this.seriesName = row?.fieldName || "PCS最高温度";
if (!pointId) {
this.setOption([]);
return;
}
getPointConfigCurve({
siteId,
pointId,
rangeType: "custom",
startTime: this.normalizeDateTime(startTime, false),
endTime: this.normalizeDateTime(endTime, true)
})
.then((response) => {
const tasks = rows.map((row) => {
const pointId = String(row.dataPoint || "").trim();
if(!pointId) return Promise.resolve(null);
return getPointConfigCurve({
...query,
pointId
}).then((response) => {
const list = response?.data || [];
this.setOption(
list
return {
name: (row.deviceName || "") + (row.fieldName || row.fieldCode || pointId),
data: list
.map((item) => [this.parseToTimestamp(item.dataTime), Number(item.pointValue)])
.filter((item) => item[0] && !Number.isNaN(item[1]))
);
};
}).catch(() => null);
});
Promise.all(tasks).then((series) => {
this.setOption((series || []).filter(Boolean));
});
},
normalizeDateTime(value, endOfDay) {
@ -83,7 +86,7 @@ export default {
const t = new Date(value).getTime();
return Number.isNaN(t) ? null : t;
},
setOption(data = []) {
setOption(seriesData = []) {
this.chart && this.chart.setOption({
legend: {
left: "center",
@ -122,18 +125,16 @@ export default {
end: 100,
},
],
series: [
{
series: seriesData.map(item => ({
type: "line",
name: this.seriesName,
name: item.name,
showSymbol: false,
smooth: true,
areaStyle: {
opacity: 0.35
},
data
}
],
data: item.data
})),
},true);
},
},

View File

@ -1,6 +1,5 @@
<template>
<div style="width:100%" v-loading="loading">
<!-- 搜索栏-->
<el-form :inline="true" class="select-container">
<el-form-item label="时间选择">
<el-date-picker
@ -25,84 +24,47 @@
<el-button type="primary" @click="exportTable" native-type="button">导出</el-button>
</el-form-item>
</el-form>
<!--表格-->
<el-table
class="common-table"
:data="tableData"
stripe
style="width: 100%;margin-top:25px;">
<!-- 汇总列-->
style="width: 100%; margin-top: 25px;"
>
<el-table-column label="汇总">
<el-table-column
prop="dataTime"
label="日期"
width="120">
<el-table-column prop="dataTime" label="日期" width="120"></el-table-column>
</el-table-column>
</el-table-column>
<!--充电量列-->
<el-table-column label="充电量" align="center">
<el-table-column
align="center"
prop="activePeakKwh"
label="">
<el-table-column align="center" prop="activePeakKwh" label="尖"></el-table-column>
<el-table-column align="center" prop="activeHighKwh" label="峰"></el-table-column>
<el-table-column align="center" prop="activeFlatKwh" label="平"></el-table-column>
<el-table-column align="center" prop="activeValleyKwh" label=""></el-table-column>
<el-table-column align="center" prop="activeTotalKwh" label="总"></el-table-column>
</el-table-column>
<el-table-column
align="center"
prop="activeHighKwh"
label="峰">
</el-table-column>
<el-table-column
align="center"
prop="activeFlatKwh"
label="平">
</el-table-column>
<el-table-column
align="center"
prop="activeValleyKwh"
label="谷">
</el-table-column>
<el-table-column
align="center"
prop="activeTotalKwh"
label="总">
</el-table-column>
</el-table-column>
<!--充电量列-->
<el-table-column label="放电量" align="center">
<el-table-column
align="center"
prop="reActivePeakKwh"
label="">
<el-table-column align="center" prop="reActivePeakKwh" label="尖"></el-table-column>
<el-table-column align="center" prop="reActiveHighKwh" label="峰"></el-table-column>
<el-table-column align="center" prop="reActiveFlatKwh" label="平"></el-table-column>
<el-table-column align="center" prop="reActiveValleyKwh" label=""></el-table-column>
<el-table-column align="center" prop="reActiveTotalKwh" label="总"></el-table-column>
</el-table-column>
<el-table-column
align="center"
prop="reActiveHighKwh"
label="峰">
</el-table-column>
<el-table-column
align="center"
prop="reActiveFlatKwh"
label="平">
</el-table-column>
<el-table-column
align="center"
prop="reActiveValleyKwh"
label="谷">
</el-table-column>
<el-table-column
align="center"
prop="reActiveTotalKwh"
label="总">
</el-table-column>
</el-table-column>
<!-- 效率-->
<el-table-column label="效率(%)" align="center">
<el-table-column
align="center"
prop="effect">
<el-table-column align="center" prop="effect"></el-table-column>
</el-table-column>
<el-table-column label="备注" align="center" fixed="right" min-width="260">
<template slot-scope="scope">
<div class="remark-cell">
<span class="remark-text">{{ scope.row.remark || "-" }}</span>
<el-button type="text" @click="editRemark(scope.row)">编辑</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length > 0"
background
@ -119,14 +81,16 @@
</div>
</template>
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getAmmeterData} from '@/api/ems/dzjk'
import { batchGetBizRemark, getAmmeterData, saveBizRemark } from "@/api/ems/dzjk";
import { formatDate } from "@/filters/ems";
const BIZ_TYPE = "stats_report";
const REPORT_KEY = "DBBB";
export default {
name: 'DzjkTjbbDbbb',
name: "DzjkTjbbDbbb",
mixins: [getQuerySiteId],
data() {
return {
@ -136,81 +100,141 @@ export default {
return time.getTime() > Date.now();
},
},
defaultDateRange: [],//默认展示的时间
defaultDateRange: [],
dateRange: [],
tableData: [],
pageSize: 10,//分页栏当前每个数据总数
pageNum: 1,//分页栏当前页数
totalSize: 0,//table表格数据总数
}
pageSize: 10,
pageNum: 1,
totalSize: 0,
};
},
methods: {
// 导出表格
buildRemarkKey(dataTime) {
return `${this.siteId}_${dataTime || ""}`;
},
loadRemarks(rows) {
if (!rows.length) return Promise.resolve({});
return batchGetBizRemark({
bizType: BIZ_TYPE,
bizKey1: REPORT_KEY,
bizKey2List: rows.map(row => this.buildRemarkKey(row.dataTime)),
}).then(response => response?.data || {});
},
applyRemarks(rows, remarkMap) {
rows.forEach(row => {
this.$set(row, "remark", remarkMap[this.buildRemarkKey(row.dataTime)] || "");
});
},
exportTable() {
if (!this.dateRange?.length) return
const [startTime, endTime] = this.dateRange
this.download('ems/statsReport/exportAmmeterDataFromDaily', {
if (!this.dateRange?.length) return;
const [startTime, endTime] = this.dateRange;
this.download(
"ems/statsReport/exportAmmeterDataFromDaily",
{
siteId: this.siteId,
startTime,
endTime,
}, `电表报表_${startTime}-${endTime}.xlsx`)
},
// 搜索
`电表报表_${startTime}-${endTime}.xlsx`
);
},
onSearch() {
this.pageNum = 1//每次搜索从1开始搜索
this.getData()
this.pageNum = 1;
this.getData();
},
// 重置
onReset() {
this.dateRange = this.defaultDateRange
this.pageNum = 1//每次搜索从1开始搜索
this.getData()
this.dateRange = this.defaultDateRange;
this.pageNum = 1;
this.getData();
},
// 分页
handleSizeChange(val) {
this.pageSize = val;
this.$nextTick(() => {
this.getData()
})
this.getData();
});
},
handleCurrentChange(val) {
this.pageNum = val
this.pageNum = val;
this.$nextTick(() => {
this.getData()
})
this.getData();
});
},
// 获取数据
getData() {
this.loading = true
const {siteId, pageNum, pageSize} = this
const [startTime = '', endTime = ''] = (this.dateRange || [])
getAmmeterData({siteId: siteId, startTime, endTime, pageSize, pageNum}).then(response => {
this.tableData = response?.rows || [];
this.totalSize = response?.total || 0
}).finally(() => {
this.loading = false
editRemark(row) {
this.$prompt("请输入备注", "编辑备注", {
inputValue: row.remark || "",
inputType: "textarea",
inputPlaceholder: "可输入该日报表备注",
confirmButtonText: "保存",
cancelButtonText: "取消",
})
.then(({ value }) => {
return saveBizRemark({
bizType: BIZ_TYPE,
bizKey1: REPORT_KEY,
bizKey2: this.buildRemarkKey(row.dataTime),
remark: value || "",
}).then(() => {
this.$set(row, "remark", value || "");
this.$message.success("备注保存成功");
});
})
.catch(() => {});
},
getData() {
this.loading = true;
const { siteId, pageNum, pageSize } = this;
const [startTime = "", endTime = ""] = this.dateRange || [];
getAmmeterData({ siteId, startTime, endTime, pageSize, pageNum })
.then(response => {
const rows = response?.rows || [];
this.tableData = rows;
this.totalSize = response?.total || 0;
return this.loadRemarks(rows);
})
.then(remarkMap => {
this.applyRemarks(this.tableData, remarkMap || {});
})
.finally(() => {
this.loading = false;
});
},
init() {
this.dateRange = []
this.tableData = []
this.totalSize = 0
this.pageSize = 10
this.pageNum = 1
let now = new Date(), lastDay = now.getTime(), firstDay = new Date(now.setDate(1)).getTime();
this.dateRange = [];
this.tableData = [];
this.totalSize = 0;
this.pageSize = 10;
this.pageNum = 1;
const now = new Date();
const lastDay = now.getTime();
const firstDay = new Date(new Date().setDate(1)).getTime();
this.defaultDateRange = [formatDate(firstDay), formatDate(lastDay)];
this.dateRange = [formatDate(firstDay), formatDate(lastDay)];
this.getData()
this.getData();
},
},
}
};
</script>
<style scoped lang="scss">
::v-deep {
.common-table.el-table .el-table__header-wrapper th, .common-table.el-table .el-table__fixed-header-wrapper th {
.common-table.el-table .el-table__header-wrapper th,
.common-table.el-table .el-table__fixed-header-wrapper th {
border-bottom: 1px solid #dfe6ec;
}
}
.remark-cell {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.remark-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
</style>

View File

@ -178,6 +178,7 @@ export default {
return {
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},

View File

@ -103,11 +103,11 @@ export default {
dataZoom: [
{
type: "inside",
start: 0,
start: data.length > 500 ? 90 : 0,
end: 100,
},
{
start: 0,
start: data.length > 500 ? 90 : 0,
end: 100,
},
],
@ -115,6 +115,7 @@ export default {
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
@ -122,6 +123,7 @@ export default {
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
@ -129,6 +131,7 @@ export default {
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},
@ -136,6 +139,7 @@ export default {
{
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},

View File

@ -194,6 +194,7 @@ export default {
return {
type: "line",
smooth: true,
connectNulls: true,
areaStyle: {
opacity: 0.7,
},

View File

@ -1,6 +1,5 @@
<template>
<div style="width:100%" v-loading="loading">
<!-- 搜索栏-->
<el-form :inline="true" class="select-container">
<el-form-item label="时间选择">
<el-date-picker
@ -25,97 +24,51 @@
<el-button type="primary" @click="exportTable" native-type="button">导出</el-button>
</el-form-item>
</el-form>
<!--表格-->
<el-table
class="common-table"
:data="tableData"
:summary-method="getSummaries"
show-summary
stripe
style="width: 100%;margin-top:25px;">
<!-- 汇总列-->
style="width: 100%; margin-top: 25px;"
>
<el-table-column label="汇总" min-width="100px" align="center">
<el-table-column
prop="dataTime"
label="日期"
min-width="100px" align="center">
<el-table-column prop="dataTime" label="日期" min-width="100px" align="center"></el-table-column>
<el-table-column prop="dayType" label="日期类型" min-width="100px" align="center"></el-table-column>
<el-table-column prop="weatherDesc" label="天气情况" min-width="180px" align="center"></el-table-column>
</el-table-column>
<el-table-column
prop="dayType"
label="日期类型"
min-width="100px" align="center">
</el-table-column>
<el-table-column
prop="weatherDesc"
label="天气情况"
min-width="180px"
align="center">
</el-table-column>
</el-table-column>
<!--充电量列-->
<el-table-column label="充电价格" align="center">
<el-table-column
align="center"
prop="activePeakPrice"
label="">
<el-table-column align="center" prop="activePeakPrice" label="尖"></el-table-column>
<el-table-column align="center" prop="activeHighPrice" label="峰"></el-table-column>
<el-table-column align="center" prop="activeFlatPrice" label="平"></el-table-column>
<el-table-column align="center" prop="activeValleyPrice" label=""></el-table-column>
<el-table-column align="center" prop="activeTotalPrice" label="总"></el-table-column>
</el-table-column>
<el-table-column
align="center"
prop="activeHighPrice"
label="峰">
</el-table-column>
<el-table-column
align="center"
prop="activeFlatPrice"
label="平">
</el-table-column>
<el-table-column
align="center"
prop="activeValleyPrice"
label="谷">
</el-table-column>
<el-table-column
align="center"
prop="activeTotalPrice"
label="总">
</el-table-column>
</el-table-column>
<!--充电量列-->
<el-table-column label="放电价格" align="center">
<el-table-column
align="center"
prop="reActivePeakPrice"
label="">
<el-table-column align="center" prop="reActivePeakPrice" label="尖"></el-table-column>
<el-table-column align="center" prop="reActiveHighPrice" label="峰"></el-table-column>
<el-table-column align="center" prop="reActiveFlatPrice" label="平"></el-table-column>
<el-table-column align="center" prop="reActiveValleyPrice" label=""></el-table-column>
<el-table-column align="center" prop="reActiveTotalPrice" label="总"></el-table-column>
</el-table-column>
<el-table-column
align="center"
prop="reActiveHighPrice"
label="峰">
</el-table-column>
<el-table-column
align="center"
prop="reActiveFlatPrice"
label="平">
</el-table-column>
<el-table-column
align="center"
prop="reActiveValleyPrice"
label="谷">
</el-table-column>
<el-table-column
align="center"
prop="reActiveTotalPrice"
label="总">
</el-table-column>
</el-table-column>
<!-- 实际收益-->
<el-table-column label="" align="center" fixed="right">
<el-table-column
prop="actualRevenue"
label="实际收益"
align="center">
<el-table-column label="" align="center">
<el-table-column prop="actualRevenue" label="实际收益" align="center"></el-table-column>
</el-table-column>
<el-table-column label="备注" align="center" fixed="right" min-width="260">
<template slot-scope="scope">
<div class="remark-cell">
<span class="remark-text">{{ scope.row.remark || "-" }}</span>
<el-button type="text" @click="editRemark(scope.row)">编辑</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-show="tableData.length > 0"
background
@ -132,14 +85,16 @@
</div>
</template>
<script>
import getQuerySiteId from "@/mixins/ems/getQuerySiteId";
import {getAmmeterRevenueData} from '@/api/ems/dzjk'
import { batchGetBizRemark, getAmmeterRevenueData, saveBizRemark } from "@/api/ems/dzjk";
import { formatDate } from "@/filters/ems";
const BIZ_TYPE = "stats_report";
const REPORT_KEY = "SYBB";
export default {
name: 'DzjkTjbbSybb',
name: "DzjkTjbbSybb",
mixins: [getQuerySiteId],
data() {
return {
@ -149,81 +104,189 @@ export default {
return time.getTime() > Date.now();
},
},
defaultDateRange: [],//默认展示的时间
defaultDateRange: [],
dateRange: [],
tableData: [],
pageSize: 10,//分页栏当前每个数据总数
pageNum: 1,//分页栏当前页数
totalSize: 0,//table表格数据总数
}
summaryTotals: {},
pageSize: 10,
pageNum: 1,
totalSize: 0,
};
},
methods: {
// 导出表格
buildRemarkKey(dataTime) {
return `${this.siteId}_${dataTime || ""}`;
},
loadRemarks(rows) {
if (!rows.length) return Promise.resolve({});
return batchGetBizRemark({
bizType: BIZ_TYPE,
bizKey1: REPORT_KEY,
bizKey2List: rows.map(row => this.buildRemarkKey(row.dataTime)),
}).then(response => response?.data || {});
},
applyRemarks(rows, remarkMap) {
rows.forEach(row => {
this.$set(row, "remark", remarkMap[this.buildRemarkKey(row.dataTime)] || "");
});
},
toScaledInt(value) {
const num = Number(value);
return Number.isFinite(num) ? Math.round(num * 1000) : 0;
},
sumRowsByProp(rows, prop) {
const total = (rows || []).reduce((sum, row) => sum + this.toScaledInt(row?.[prop]), 0);
return total / 1000;
},
formatSummaryNumber(value) {
const num = Number(value);
if (!Number.isFinite(num)) return "";
return num.toFixed(3).replace(/\.?0+$/, "");
},
buildSummaryTotals(rows) {
const numericProps = [
"activePeakPrice",
"activeHighPrice",
"activeFlatPrice",
"activeValleyPrice",
"activeTotalPrice",
"reActivePeakPrice",
"reActiveHighPrice",
"reActiveFlatPrice",
"reActiveValleyPrice",
"reActiveTotalPrice",
"actualRevenue",
];
return numericProps.reduce((result, prop) => {
result[prop] = this.sumRowsByProp(rows, prop);
return result;
}, {});
},
getSummaries({ columns, data }) {
return columns.map((column, index) => {
if (index === 0) return "合计";
const prop = column.property;
if (!prop) return "";
if (Object.prototype.hasOwnProperty.call(this.summaryTotals, prop)) {
return this.formatSummaryNumber(this.summaryTotals[prop]);
}
const hasNumericValue = (data || []).some(item => Number.isFinite(Number(item?.[prop])));
return hasNumericValue ? this.formatSummaryNumber(this.sumRowsByProp(data, prop)) : "";
});
},
exportTable() {
if (!this.dateRange?.length) return
const [startTime, endTime] = this.dateRange
this.download('ems/statsReport/exportAmmeterRevenueData', {
if (!this.dateRange?.length) return;
const [startTime, endTime] = this.dateRange;
this.download(
"ems/statsReport/exportAmmeterRevenueData",
{
siteId: this.siteId,
startTime,
endTime,
}, `收益报表_${startTime}-${endTime}.xlsx`)
},
// 搜索
`收益报表_${startTime}-${endTime}.xlsx`
);
},
onSearch() {
this.pageNum = 1//每次搜索从1开始搜索
this.getData()
this.pageNum = 1;
this.getData();
},
// 重置
onReset() {
this.dateRange = this.defaultDateRange
this.pageNum = 1//每次搜索从1开始搜索
this.getData()
this.dateRange = this.defaultDateRange;
this.pageNum = 1;
this.getData();
},
// 分页
handleSizeChange(val) {
this.pageSize = val;
this.$nextTick(() => {
this.getData()
})
this.getData();
});
},
handleCurrentChange(val) {
this.pageNum = val
this.pageNum = val;
this.$nextTick(() => {
this.getData()
})
this.getData();
});
},
// 获取数据
getData() {
this.loading = true
const {siteId, pageNum, pageSize} = this
const [startTime = '', endTime = ''] = (this.dateRange || [])
getAmmeterRevenueData({siteId: siteId, startTime, endTime, pageSize, pageNum}).then(response => {
const rows = response?.rows || [];
this.totalSize = response?.total || 0
this.tableData = rows
}).finally(() => {
this.loading = false
editRemark(row) {
this.$prompt("请输入备注", "编辑备注", {
inputValue: row.remark || "",
inputType: "textarea",
inputPlaceholder: "可输入该日报表备注",
confirmButtonText: "保存",
cancelButtonText: "取消",
})
.then(({ value }) => {
return saveBizRemark({
bizType: BIZ_TYPE,
bizKey1: REPORT_KEY,
bizKey2: this.buildRemarkKey(row.dataTime),
remark: value || "",
}).then(() => {
this.$set(row, "remark", value || "");
this.$message.success("备注保存成功");
});
})
.catch(() => {});
},
getData() {
this.loading = true;
const { siteId, pageNum, pageSize } = this;
const [startTime = "", endTime = ""] = this.dateRange || [];
getAmmeterRevenueData({ siteId, startTime, endTime, pageSize, pageNum })
.then(pageResponse => {
const rows = pageResponse?.rows || [];
const total = Number(pageResponse?.total || 0);
this.totalSize = total;
this.tableData = rows;
const tasks = [this.loadRemarks(rows)];
if (total > 0) {
tasks.push(
getAmmeterRevenueData({
siteId,
startTime,
endTime,
pageNum: 1,
pageSize: total,
})
);
}
return Promise.all(tasks);
})
.then(([remarkMap, allResponse]) => {
this.applyRemarks(this.tableData, remarkMap || {});
const allRows = allResponse?.rows || allResponse?.data || [];
this.summaryTotals = this.buildSummaryTotals(allRows);
})
.finally(() => {
this.loading = false;
});
},
init() {
this.dateRange = []
this.tableData = []
this.totalSize = 0
this.pageSize = 10
this.pageNum = 1
let now = new Date(), lastDay = now.getTime(), firstDay = new Date(now.setDate(1)).getTime();
this.dateRange = [];
this.tableData = [];
this.summaryTotals = {};
this.totalSize = 0;
this.pageSize = 10;
this.pageNum = 1;
const now = new Date();
const lastDay = now.getTime();
const firstDay = new Date(new Date().setDate(1)).getTime();
this.defaultDateRange = [formatDate(firstDay), formatDate(lastDay)];
this.dateRange = [formatDate(firstDay), formatDate(lastDay)];
this.getData()
this.getData();
},
},
}
};
</script>
<style scoped lang="scss">
::v-deep {
.common-table.el-table {
.el-table__header-wrapper th, .common-table.el-table .el-table__fixed-header-wrapper th {
.el-table__header-wrapper th,
.common-table.el-table .el-table__fixed-header-wrapper th {
border-bottom: 1px solid #dfe6ec;
}
@ -234,7 +297,20 @@ export default {
}
}
}
}
.remark-cell {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.remark-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
}
</style>

View File

@ -85,6 +85,7 @@
</el-table-column>
<el-table-column prop="latestValue" label="最新值" min-width="120" />
<el-table-column prop="dataUnit" label="单位" min-width="80" />
<el-table-column prop="decimalScale" label="小数位数" min-width="80" />
<el-table-column label="操作" width="220" fixed="right">
<template slot-scope="scope">
<el-button type="text" @click="openCurveDialog(scope.row)">曲线</el-button>
@ -192,6 +193,11 @@
<el-input v-model.trim="form.dataUnit" placeholder="请输入单位" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="小数位数">
<el-input-number v-model="form.decimalScale" :min="0" :precision="0" placeholder="不填则不限制" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="24" v-if="form.pointType === 'calc'">
@ -412,6 +418,7 @@ export default {
dataKey: '',
pointDesc: '',
dataUnit: '',
decimalScale: null,
dataA: '',
dataK: '',
dataB: '',
@ -1209,7 +1216,8 @@ export default {
'类型',
'计算表达式',
'最新值',
'单位'
'单位',
'小数位数'
]
const rows = this.tableData.map(item => {
return [
@ -1228,7 +1236,8 @@ export default {
item.pointType === 'calc' ? '计算点' : '数据点',
item.pointType === 'calc' ? (item.calcExpression || '') : '',
item.latestValue === undefined || item.latestValue === null ? '' : item.latestValue,
item.dataUnit || ''
item.dataUnit || '',
item.decimalScale === undefined || item.decimalScale === null ? '' : item.decimalScale
].map(cell => this.escapeCsvCell(cell))
})
const csvText = [headers.map(cell => this.escapeCsvCell(cell)).join(','), ...rows.map(row => row.join(','))].join('\n')
@ -1258,6 +1267,7 @@ export default {
dataKey: '',
pointDesc: '',
dataUnit: '',
decimalScale: null,
dataA: '',
dataK: '',
dataB: '',
@ -1645,6 +1655,7 @@ export default {
pointDesc: this.form.pointDesc,
dataUnit: this.form.dataUnit,
calcExpression: this.form.calcExpression,
decimalScale: this.form.decimalScale,
remark: this.form.remark
}
request(calcData).then(() => {

View File

@ -33,9 +33,13 @@
class="quick-filter-input"
placeholder="按字段名/展示名/设备名筛选"
/>
<div class="filter-actions">
<div class="filter-actions" :class="{ 'single-battery-actions-visible': showSingleBatteryImportActions }">
<el-button size="small" :disabled="!siteId" @click="openImportConfigDialog">导入配置</el-button>
<el-button size="small" type="primary" :disabled="!siteId" @click="exportConfig">导出配置</el-button>
<el-button size="small" :disabled="!siteId || singleBatteryImportLoading" @click="downloadSingleBatteryTemplate">下载单体模板</el-button>
<el-button size="small" type="success" :disabled="!siteId || singleBatteryImportLoading" @click="openSingleBatteryImportDialog">
{{ singleBatteryImportLoading ? '导入中...' : '导入单体电池' }}
</el-button>
<input
ref="configInput"
type="file"
@ -43,6 +47,13 @@
style="display: none"
@change="handleConfigFileChange"
/>
<input
ref="singleBatteryImportInput"
type="file"
accept=".xlsx,.xls"
style="display: none"
@change="handleSingleBatteryImportFileChange"
/>
</div>
</div>
@ -459,17 +470,56 @@
@current-change="handlePointSelectorPageChange"
/>
</el-dialog>
<el-dialog
title="单体电池导入结果"
:visible.sync="singleBatteryImportResultVisible"
width="980px"
append-to-body
class="ems-dialog"
>
<el-alert
:title="singleBatteryImportResult.message || '导入完成'"
:type="singleBatteryImportResult.committed ? 'success' : 'warning'"
:closable="false"
style="margin-bottom: 16px;"
/>
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="总行数">{{ singleBatteryImportResult.totalRows || 0 }}</el-descriptions-item>
<el-descriptions-item label="成功行数">{{ singleBatteryImportResult.successRows || 0 }}</el-descriptions-item>
<el-descriptions-item label="失败行数">{{ singleBatteryImportResult.failureRows || 0 }}</el-descriptions-item>
<el-descriptions-item label="新增单体">{{ singleBatteryImportResult.insertedBatteryCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="更新单体">{{ singleBatteryImportResult.updatedBatteryCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="是否已提交">{{ singleBatteryImportResult.committed ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="新增映射">{{ singleBatteryImportResult.insertedMappingCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="更新映射">{{ singleBatteryImportResult.updatedMappingCount || 0 }}</el-descriptions-item>
</el-descriptions>
<div v-if="(singleBatteryImportResult.failureDetails || []).length > 0" style="margin-top: 16px;">
<div class="import-result-title">失败明细</div>
<el-table class="common-table" :data="singleBatteryImportResult.failureDetails" stripe max-height="360">
<el-table-column prop="rowNum" label="Excel行号" width="100" />
<el-table-column prop="siteId" label="站点ID" width="140" />
<el-table-column prop="stackDeviceId" label="电池堆编号" width="140" />
<el-table-column prop="clusterDeviceId" label="电池簇编号" width="140" />
<el-table-column prop="batteryDeviceId" label="单体编号" width="120" />
<el-table-column prop="errorMessage" label="失败原因" min-width="260" />
</el-table>
</div>
</el-dialog>
</div>
</template>
<script>
import {
downloadSingleBatteryMonitorImportTemplate,
getPointMatchList,
getSingleMonitorProjectPointMapping,
getSingleMonitorWorkStatusEnumMappings,
importSingleBatteryMonitorMappings,
saveSingleMonitorProjectPointMapping,
saveSingleMonitorWorkStatusEnumMappings
} from '@/api/ems/site'
import { saveAs } from 'file-saver'
import { blobValidate } from '@/utils/ems'
export default {
name: 'MonitorPointMapping',
@ -519,6 +569,20 @@ export default {
suppressAutoSave: false,
isSaving: false,
saveStatusText: '自动保存已开启',
singleBatteryImportLoading: false,
singleBatteryImportResultVisible: false,
singleBatteryImportResult: {
committed: false,
totalRows: 0,
successRows: 0,
failureRows: 0,
insertedBatteryCount: 0,
updatedBatteryCount: 0,
insertedMappingCount: 0,
updatedMappingCount: 0,
message: '',
failureDetails: []
},
lastSavedPointSignature: '',
lastSavedEnumSignature: '',
workStatusEnumMappings: []
@ -561,6 +625,19 @@ export default {
this.schedulePointSelectorSearch()
}
},
computed: {
showSingleBatteryImportActions() {
const currentTopMenu = this.topMenuList.find(item => item.code === this.activeTopMenu)
if (!currentTopMenu) {
return false
}
if (currentTopMenu.code === 'SBJK_DTDC') {
return true
}
const activeChildCode = this.activeChildMap[this.activeTopMenu] || ''
return activeChildCode === 'SBJK_DTDC'
}
},
methods: {
async initMenuStructure() {
if (!this.siteId) {
@ -716,6 +793,83 @@ export default {
this.$refs.configInput.click()
}
},
openSingleBatteryImportDialog() {
if (!this.siteId || !this.$refs.singleBatteryImportInput) {
return
}
this.$refs.singleBatteryImportInput.value = ''
this.$refs.singleBatteryImportInput.click()
},
async downloadSingleBatteryTemplate() {
if (!this.siteId) {
this.$message.warning('璇峰厛閫夋嫨绔欑偣')
return
}
try {
const data = await downloadSingleBatteryMonitorImportTemplate(this.siteId)
if (!blobValidate(data)) {
const text = await data.text()
const json = JSON.parse(text)
this.$message.error(json.msg || '模板下载失败')
return
}
saveAs(data, `单体电池导入模板_${this.siteId}.xlsx`)
} catch (error) {
this.$message.error('模板下载失败,请稍后重试')
}
},
handleSingleBatteryImportFileChange(event) {
const file = event && event.target && event.target.files && event.target.files[0]
if (!file) {
return
}
const isExcel = /\.(xlsx|xls)$/i.test(file.name || '')
if (!isExcel) {
this.$message.error('仅支持导入 Excel 文件')
return
}
this.importSingleBatteryFile(file)
},
async importSingleBatteryFile(file) {
if (!this.siteId) {
this.$message.warning('璇峰厛閫夋嫨绔欑偣')
return
}
const formData = new FormData()
formData.append('siteId', this.siteId)
formData.append('file', file)
this.singleBatteryImportLoading = true
try {
const response = await importSingleBatteryMonitorMappings(formData)
this.singleBatteryImportResult = this.normalizeSingleBatteryImportResult(response?.data || {})
this.singleBatteryImportResultVisible = true
if (this.singleBatteryImportResult.committed) {
await this.initMenuStructure()
this.$message.success('单体电池导入成功')
} else {
this.$message.warning(this.singleBatteryImportResult.message || '导入校验未通过')
}
} catch (error) {
this.$message.error('导入失败,请稍后重试')
} finally {
this.singleBatteryImportLoading = false
}
},
normalizeSingleBatteryImportResult(result) {
const source = result || {}
return {
committed: !!source.committed,
totalRows: Number(source.totalRows || 0),
successRows: Number(source.successRows || 0),
failureRows: Number(source.failureRows || 0),
insertedBatteryCount: Number(source.insertedBatteryCount || 0),
updatedBatteryCount: Number(source.updatedBatteryCount || 0),
insertedMappingCount: Number(source.insertedMappingCount || 0),
updatedMappingCount: Number(source.updatedMappingCount || 0),
message: source.message || '',
failureDetails: Array.isArray(source.failureDetails) ? source.failureDetails : []
}
},
handleConfigFileChange(event) {
const file = event && event.target && event.target.files && event.target.files[0]
if (!file) {
@ -1570,6 +1724,18 @@ export default {
gap: 8px;
}
.filter-actions:not(.single-battery-actions-visible) > .el-button:nth-of-type(3),
.filter-actions:not(.single-battery-actions-visible) > .el-button:nth-of-type(4) {
display: none;
}
.import-result-title {
margin-bottom: 10px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.quick-wrapper {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));