diff --git a/src/api/ems/dzjk.js b/src/api/ems/dzjk.js index f0584d6..0c5e018 100644 --- a/src/api/ems/dzjk.js +++ b/src/api/ems/dzjk.js @@ -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({ diff --git a/src/api/ems/site.js b/src/api/ems/site.js index a7dd588..715efeb 100644 --- a/src/api/ems/site.js +++ b/src/api/ems/site.js @@ -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({ diff --git a/src/views/ems/dzjk/home/WeekChart.vue b/src/views/ems/dzjk/home/WeekChart.vue index ecbf5bd..4967532 100644 --- a/src/views/ems/dzjk/home/WeekChart.vue +++ b/src/views/ems/dzjk/home/WeekChart.vue @@ -1,10 +1,17 @@ @@ -12,11 +19,21 @@ import * as echarts from 'echarts' import resize from '@/mixins/ems/resize' import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue' -import {getPointConfigCurve} from '@/api/ems/site' +import { getPointConfigCurve } from '@/api/ems/site' + +const DAY = 24 * 60 * 60 * 1000 + +function createEmptySummary() { + return { + totalChargedCap: '', + totalDisChargedCap: '', + efficiency: '' + } +} export default { mixins: [resize], - components: {DateRangeSelect}, + components: { DateRangeSelect }, props: { displayData: { type: Array, @@ -27,7 +44,8 @@ export default { return { chart: null, timeRange: [], - siteId: '' + siteId: '', + summary: createEmptySummary() } }, watch: { @@ -50,13 +68,12 @@ export default { this.chart = null }, methods: { - // 更新时间范围 重置图表 updateDate(data) { this.timeRange = data this.getWeekKData() }, getWeekKData() { - const {siteId, timeRange} = this + const { siteId, timeRange } = this const displayData = this.displayData || [] const sectionRows = displayData.filter(item => item && item.sectionName === '一周充放曲线' && item.useFixedDisplay !== 1 && item.dataPoint @@ -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' } - }, - grid: { - containLabel: true + axisPointer: { + type: 'shadow' + }, + 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('
') + }, + 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: [{ - type: 'value', - name: '充电量/放电量kWh', - axisLine: { - lineStyle: { - color: '#333333', - }, - onZero: false + yAxis: [ + { + type: 'value', + name: '充电量/放电量kWh', + axisLine: { + lineStyle: { + 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, - yAxisIndex: 0, - type: 'bar', - data: item.data - })) + dataset: { + source + }, + series: [ + { + name: '充电量', + yAxisIndex: 0, + type: 'bar', + 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 + } + } + ] }) } } } + + diff --git a/src/views/ems/dzjk/sbjk/bmsdcc/index.vue b/src/views/ems/dzjk/sbjk/bmsdcc/index.vue index 2a2d378..979a8d6 100644 --- a/src/views/ems/dzjk/sbjk/bmsdcc/index.vue +++ b/src/views/ems/dzjk/sbjk/bmsdcc/index.vue @@ -56,7 +56,7 @@ :class="{ 'field-disabled': !hasFieldPointId(baseInfo, 'workStatus') }" @click="handleFieldClick(baseInfo, 'workStatus', '工作状态')" > - {{ CLUSTERWorkStatusOptions[baseInfo.workStatus] || '-' }} + {{ formatDictValue(clusterWorkStatusOptions, baseInfo.workStatus) }} - {{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }} + {{ formatDictValue(clusterPcsCommunicationStatusOptions, baseInfo.pcsCommunicationStatus) }} - {{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }} + {{ formatDictValue(clusterEmsCommunicationStatusOptions, baseInfo.emsCommunicationStatus) }} @@ -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(); diff --git a/src/views/ems/dzjk/sbjk/bmszl/index.vue b/src/views/ems/dzjk/sbjk/bmszl/index.vue index 78f3a6d..c5351d9 100644 --- a/src/views/ems/dzjk/sbjk/bmszl/index.vue +++ b/src/views/ems/dzjk/sbjk/bmszl/index.vue @@ -50,19 +50,19 @@ contentClassName="descriptions-direction work-status" label="工作状态" labelClassName="descriptions-label"> - {{ STACKWorkStatusOptions[baseInfo.workStatus] || '-' }} + {{ formatDictValue(stackWorkStatusOptions, baseInfo.workStatus) }} - {{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.pcsCommunicationStatus] || '-' }} + {{ formatDictValue(stackPcsCommunicationStatusOptions, baseInfo.pcsCommunicationStatus) }} - {{ (($store.state.ems && $store.state.ems.communicationStatusOptions) || {})[baseInfo.emsCommunicationStatus] || '-' }} + {{ formatDictValue(stackEmsCommunicationStatusOptions, baseInfo.emsCommunicationStatus) }} @@ -123,7 +123,7 @@ diff --git a/src/views/ems/dzjk/tjbb/dcdqx/index.vue b/src/views/ems/dzjk/tjbb/dcdqx/index.vue index 9e8e9ea..623a13e 100644 --- a/src/views/ems/dzjk/tjbb/dcdqx/index.vue +++ b/src/views/ems/dzjk/tjbb/dcdqx/index.vue @@ -178,6 +178,7 @@ export default { return { type: "line", smooth: true, + connectNulls: true, areaStyle: { opacity: 0.7, }, diff --git a/src/views/ems/dzjk/tjbb/glqx/index.vue b/src/views/ems/dzjk/tjbb/glqx/index.vue index 2863615..e391bca 100644 --- a/src/views/ems/dzjk/tjbb/glqx/index.vue +++ b/src/views/ems/dzjk/tjbb/glqx/index.vue @@ -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, }, diff --git a/src/views/ems/dzjk/tjbb/pcsqx/index.vue b/src/views/ems/dzjk/tjbb/pcsqx/index.vue index 8c67dff..47e71bd 100644 --- a/src/views/ems/dzjk/tjbb/pcsqx/index.vue +++ b/src/views/ems/dzjk/tjbb/pcsqx/index.vue @@ -194,6 +194,7 @@ export default { return { type: "line", smooth: true, + connectNulls: true, areaStyle: { opacity: 0.7, }, diff --git a/src/views/ems/dzjk/tjbb/sybb/index.vue b/src/views/ems/dzjk/tjbb/sybb/index.vue index 2175db0..5b986fa 100644 --- a/src/views/ems/dzjk/tjbb/sybb/index.vue +++ b/src/views/ems/dzjk/tjbb/sybb/index.vue @@ -1,18 +1,17 @@ - diff --git a/src/views/ems/site/pointConfig/index.vue b/src/views/ems/site/pointConfig/index.vue index ea99d0a..8889c8c 100644 --- a/src/views/ems/site/pointConfig/index.vue +++ b/src/views/ems/site/pointConfig/index.vue @@ -85,6 +85,7 @@ +