diff --git a/src/api/ems/dzjk.js b/src/api/ems/dzjk.js index 56a36b0..f0584d6 100644 --- a/src/api/ems/dzjk.js +++ b/src/api/ems/dzjk.js @@ -8,6 +8,39 @@ export function getDzjkHomeView(siteId) { }) } +//获取单个站点总累计运行数据(基于日表) +export function getDzjkHomeTotalView(siteId) { + return request({ + url: `/ems/siteMonitor/homeTotalView?siteId=${siteId}`, + method: 'get' + }) +} + +// 单站监控项目点位配置(供单站监控功能查询) +export function getProjectPointMapping(siteId) { + return request({ + url: `/ems/siteMonitor/getProjectPointMapping?siteId=${siteId}`, + method: 'get' + }) +} + +// 单站监控项目展示数据(字段配置 + 最新值) +export function getProjectDisplayData(siteId) { + return request({ + url: `/ems/siteMonitor/getProjectDisplayData?siteId=${siteId}`, + method: 'get' + }) +} + +// 单站监控项目展示数据写入(批量) +export function saveProjectDisplayData(data) { + return request({ + url: `/ems/siteMonitor/saveProjectDisplayData`, + method: 'post', + data + }) +} + //站点首页 冲放曲线 export function getSevenChargeData({siteId, startDate, endDate}) { return request({ @@ -156,12 +189,328 @@ export function createTicketNo(data) { }) } -// 概率统计 -//获取概率统计 电量指标接口 -export function getElectricData({siteId, startDate, endDate}) { +// 告警确认关闭 +export function closeAlarm(data) { return request({ - url: `/ems/statsReport/getElectricData?siteId=${siteId}&startDate=${startDate}&endDate=${endDate}`, - method: 'get' + url: `/ems/siteAlarm/closeAlarm`, + method: 'post', + data + }) +} + +function getFieldNameByCode(fieldCode) { + const raw = String(fieldCode || '').trim() + if (!raw) return '' + const idx = raw.lastIndexOf('__') + return idx >= 0 ? raw.slice(idx + 2) : raw +} + +function normalizeRows(displayResponse) { + const rows = displayResponse?.data || [] + return Array.isArray(rows) ? rows : [] +} + +function filterByMenu(rows, menuCode) { + return (rows || []).filter(item => item && item.menuCode === menuCode) +} + +function toDateLabel(valueTime) { + if (!valueTime) return '' + const date = new Date(valueTime) + if (isNaN(date.getTime())) return '' + const p = (n) => String(n).padStart(2, '0') + return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}` +} + +function toDateTimeLabel(valueTime) { + if (!valueTime) return '' + const date = new Date(valueTime) + if (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())}` +} + +function toValueMap(rows) { + const map = {} + ;(rows || []).forEach(item => { + const fieldName = getFieldNameByCode(item?.fieldCode) + if (fieldName) { + map[fieldName] = item?.fieldValue + } + }) + return map +} + +function groupRowsByDevice(rows) { + const map = new Map() + ;(rows || []).forEach(item => { + const deviceId = String(item?.deviceId || '').trim() || 'DEFAULT' + if (!map.has(deviceId)) { + map.set(deviceId, []) + } + map.get(deviceId).push(item) + }) + return map +} + +function paginateRows(rows, pageNum = 1, pageSize = 10) { + const safePageNum = Number(pageNum) > 0 ? Number(pageNum) : 1 + const safePageSize = Number(pageSize) > 0 ? Number(pageSize) : 10 + const start = (safePageNum - 1) * safePageSize + return (rows || []).slice(start, start + safePageSize) +} + +function toNumber(value) { + const num = Number(value) + return Number.isFinite(num) ? num : null +} + +function normalizeDateInput(dateStr) { + if (dateStr) return dateStr + return toDateLabel(new Date()) +} + +function resolveElectricUnit(startDate, endDate) { + const start = new Date(`${normalizeDateInput(startDate)} 00:00:00`) + const end = new Date(`${normalizeDateInput(endDate)} 00:00:00`) + if (isNaN(start.getTime()) || isNaN(end.getTime())) return '日' + const diffDays = Math.floor((end.getTime() - start.getTime()) / (24 * 60 * 60 * 1000)) + if (diffDays <= 0) return '时' + if (diffDays < 30) return '日' + return '月' +} + +function formatByUnit(date, unit) { + const p = (n) => String(n).padStart(2, '0') + if (unit === '时') return `${p(date.getHours())}:${p(date.getMinutes())}` + if (unit === '月') return `${date.getFullYear()}-${p(date.getMonth() + 1)}` + return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}` +} + +function aggregateCurveByUnit(curveRows, unit) { + const result = new Map() + ;(curveRows || []).forEach(item => { + const time = item?.dataTime ? new Date(item.dataTime) : null + if (!time || isNaN(time.getTime())) return + const value = toNumber(item?.pointValue) + if (value == null) return + const label = formatByUnit(time, unit) + result.set(label, value) + }) + return result +} + +function getLatestCurveValue(curveRows) { + let latestTime = null + let latestValue = null + ;(curveRows || []).forEach(item => { + const time = item?.dataTime ? new Date(item.dataTime) : null + if (!time || isNaN(time.getTime())) return + const value = toNumber(item?.pointValue) + if (value == null) return + if (latestTime == null || time.getTime() > latestTime) { + latestTime = time.getTime() + latestValue = value + } + }) + return latestValue +} + +function findMappingByField(rows, fieldNames = []) { + const targetSet = new Set((fieldNames || []).map(name => String(name || '').trim())) + return (rows || []).find(item => targetSet.has(getFieldNameByCode(item?.fieldCode))) +} + +function getDataPointFromMapping(mapping) { + if (!mapping) return '' + const useFixedDisplay = Number(mapping?.useFixedDisplay) === 1 + const fixedDataPoint = String(mapping?.fixedDataPoint || '').trim() + const dataPoint = String(mapping?.dataPoint || '').trim() + if (useFixedDisplay && fixedDataPoint) return fixedDataPoint + return dataPoint || fixedDataPoint +} + +function queryPointCurveByPointId({siteId, pointId, startDate, endDate}) { + if (!siteId || !pointId) return Promise.resolve([]) + const start = `${normalizeDateInput(startDate)} 00:00:00` + const end = `${normalizeDateInput(endDate)} 23:59:59` + return request({ + url: `/ems/pointConfig/curve`, + method: 'post', + headers: { + repeatSubmit: false + }, + data: { + siteId, + pointId, + rangeType: 'custom', + startTime: start, + endTime: end + } + }).then(resp => (Array.isArray(resp?.data) ? resp.data : [])) +} + +function sortCurveRows(curveRows = []) { + return [...(curveRows || [])].sort((a, b) => { + const ta = new Date(a?.dataTime).getTime() + const tb = new Date(b?.dataTime).getTime() + return (isNaN(ta) ? 0 : ta) - (isNaN(tb) ? 0 : tb) + }) +} + +function resolveRangeKind(startDate, endDate) { + const start = new Date(`${normalizeDateInput(startDate)} 00:00:00`) + 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' + 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 === '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() +} + +function normalizePointValue(raw) { + const num = toNumber(raw) + if (num != null) return num + return raw == null ? '' : raw +} + +function resolveAliasByField(aliasMap, fieldName) { + const raw = String(fieldName || '').trim() + if (!raw) return '' + if (aliasMap[raw]) return aliasMap[raw] + const withoutStat = raw.replace(/_stat$/, '') + return aliasMap[withoutStat] || '' +} + +function queryMenuPointCurves({siteId, menuCode, startDate, endDate, mappingFilter}) { + return getProjectPointMapping(siteId).then((mappingResp) => { + const allMappings = Array.isArray(mappingResp?.data) ? mappingResp.data : [] + let menuMappings = allMappings.filter(item => item?.menuCode === menuCode) + if (typeof mappingFilter === 'function') { + menuMappings = menuMappings.filter(mappingFilter) + } + const tasks = menuMappings.map((mapping) => { + const pointId = getDataPointFromMapping(mapping) + if (!pointId) return Promise.resolve(null) + return queryPointCurveByPointId({siteId, pointId, startDate, endDate}) + .then(curve => ({ + deviceId: String(mapping?.deviceId || '').trim(), + fieldName: getFieldNameByCode(mapping?.fieldCode), + curve: sortCurveRows(curve || []), + })) + .catch(() => ({ + deviceId: String(mapping?.deviceId || '').trim(), + fieldName: getFieldNameByCode(mapping?.fieldCode), + curve: [], + })) + }) + return Promise.all(tasks).then(rows => rows.filter(Boolean)) + }) +} + +// 电量指标 +export function getElectricData({siteId, startDate, endDate}) { + return getProjectPointMapping(siteId).then((mappingResp) => { + const allMappings = Array.isArray(mappingResp?.data) ? mappingResp.data : [] + const gltjMappings = allMappings.filter(item => item?.menuCode === 'TJBB_GLTJ') + + const chargedMap = findMappingByField(gltjMappings, ['chargedCap_stat', 'chargedCap']) + const disChargedMap = findMappingByField(gltjMappings, ['disChargedCap_stat', 'disChargedCap']) + const dailyEfficiencyMap = findMappingByField(gltjMappings, ['dailyEfficiency']) + const totalChargedMap = findMappingByField(gltjMappings, ['totalChargedCap_stat', 'totalChargedCap']) + const totalDisChargedMap = findMappingByField(gltjMappings, ['totalDisChargedCap_stat', 'totalDisChargedCap']) + const totalEfficiencyMap = findMappingByField(gltjMappings, ['efficiency']) + + const pointMap = { + charged: getDataPointFromMapping(chargedMap), + disCharged: getDataPointFromMapping(disChargedMap), + dailyEfficiency: getDataPointFromMapping(dailyEfficiencyMap), + totalCharged: getDataPointFromMapping(totalChargedMap), + totalDisCharged: getDataPointFromMapping(totalDisChargedMap), + totalEfficiency: getDataPointFromMapping(totalEfficiencyMap), + } + + const queryTasks = Object.keys(pointMap).map((key) => { + const pointId = pointMap[key] + return queryPointCurveByPointId({siteId, pointId, startDate, endDate}) + .then(curve => ({key, curve})) + .catch(() => ({key, curve: []})) + }) + + return Promise.all(queryTasks).then((queryResult) => { + const curveMap = {} + queryResult.forEach(item => { + curveMap[item.key] = item.curve || [] + }) + + const unit = resolveElectricUnit(startDate, endDate) + const chargedSeries = aggregateCurveByUnit(curveMap.charged, unit) + const disChargedSeries = aggregateCurveByUnit(curveMap.disCharged, unit) + const efficiencySeries = aggregateCurveByUnit(curveMap.dailyEfficiency, unit) + + const labels = Array.from(new Set([ + ...chargedSeries.keys(), + ...disChargedSeries.keys(), + ...efficiencySeries.keys(), + ])).sort() + + const sevenDayDisChargeStats = labels.map((label) => { + const chargedCap = chargedSeries.get(label) + const disChargedCap = disChargedSeries.get(label) + let dailyEfficiency = efficiencySeries.get(label) + if (dailyEfficiency == null && chargedCap != null && chargedCap !== 0 && disChargedCap != null) { + dailyEfficiency = Number(((disChargedCap / chargedCap) * 100).toFixed(2)) + } + return { + ammeterDate: label, + chargedCap: chargedCap == null ? '' : chargedCap, + disChargedCap: disChargedCap == null ? '' : disChargedCap, + dailyEfficiency: dailyEfficiency == null ? '' : dailyEfficiency, + } + }) + + const fallbackTotalCharged = sevenDayDisChargeStats.reduce((acc, item) => acc + (toNumber(item.chargedCap) || 0), 0) + const fallbackTotalDisCharged = sevenDayDisChargeStats.reduce((acc, item) => acc + (toNumber(item.disChargedCap) || 0), 0) + + const totalChargedCap = getLatestCurveValue(curveMap.totalCharged) + const totalDisChargedCap = getLatestCurveValue(curveMap.totalDisCharged) + const totalEfficiency = getLatestCurveValue(curveMap.totalEfficiency) + + const resultTotalCharged = totalChargedCap == null ? fallbackTotalCharged : totalChargedCap + const resultTotalDisCharged = totalDisChargedCap == null ? fallbackTotalDisCharged : totalDisChargedCap + const resultEfficiency = totalEfficiency == null + ? (resultTotalCharged > 0 ? Number(((resultTotalDisCharged / resultTotalCharged) * 100).toFixed(2)) : 0) + : totalEfficiency + + return { + data: { + totalChargedCap: resultTotalCharged, + totalDisChargedCap: resultTotalDisCharged, + efficiency: resultEfficiency, + unit, + sevenDayDisChargeStats, + } + } + }) }) } @@ -173,27 +522,147 @@ export function getPcsNameList(siteId) { }) } -//pcs曲线 -export function getPCSData({siteId, startTime, endTime, dataType}) { - return request({ - url: `/ems/statsReport/getPCSData?siteId=${siteId}&startDate=${startTime}&endDate=${endTime}&dataType=${dataType}`, - method: 'get' +// pcs曲线 +export function getPCSData({siteId, startTime, endTime}) { + const kind = resolveRangeKind(startTime, endTime) + const aliasMap = { + activePower_stat: 'activePower', + activePower: 'activePower', + reactivePower_stat: 'reactivePower', + reactivePower: 'reactivePower', + uCurrent: 'uCurrent', + vCurrent: 'vCurrent', + wCurrent: 'wCurrent', + } + return queryMenuPointCurves({ + siteId, + menuCode: 'TJBB_PCSQX', + startDate: startTime, + endDate: endTime, + }).then((records) => { + const byDevice = new Map() + records.forEach((record) => { + const alias = resolveAliasByField(aliasMap, record.fieldName) + if (!alias) return + const deviceId = record.deviceId || '' + if (!byDevice.has(deviceId)) byDevice.set(deviceId, new Map()) + const rowMap = byDevice.get(deviceId) + ;(record.curve || []).forEach((point) => { + const time = point?.dataTime ? new Date(point.dataTime) : null + if (!time || isNaN(time.getTime())) return + const label = formatTimeLabelByKind(time, kind) + if (!rowMap.has(label)) rowMap.set(label, {statisDate: label}) + rowMap.get(label)[alias] = normalizePointValue(point?.pointValue) + }) + }) + const data = [] + byDevice.forEach((rowMap, deviceId) => { + const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind) + data.push({ + deviceId, + dataList: labels.map(label => rowMap.get(label)), + }) + }) + return {data} }) } -//电池堆曲线 -export function getStackData({siteId, startTime, endTime, dataType}) { - return request({ - url: `/ems/statsReport/getStackData?siteId=${siteId}&startDate=${startTime}&endDate=${endTime}&dataType=${dataType}`, - method: 'get' +// 电池堆曲线 +export function getStackData({siteId, startTime, endTime}) { + const kind = resolveRangeKind(startTime, endTime) + const aliasMap = { + temp: 'temp', + voltage_stat: 'voltage', + voltage: 'voltage', + current: 'current', + soc_stat: 'soc', + soc: 'soc', + } + return queryMenuPointCurves({ + siteId, + menuCode: 'TJBB_DCDQX', + startDate: startTime, + endDate: endTime, + }).then((records) => { + const byDevice = new Map() + records.forEach((record) => { + const alias = resolveAliasByField(aliasMap, record.fieldName) + if (!alias) return + const deviceId = record.deviceId || '' + if (!byDevice.has(deviceId)) byDevice.set(deviceId, new Map()) + const rowMap = byDevice.get(deviceId) + ;(record.curve || []).forEach((point) => { + const time = point?.dataTime ? new Date(point.dataTime) : null + if (!time || isNaN(time.getTime())) return + const label = formatTimeLabelByKind(time, kind) + if (!rowMap.has(label)) rowMap.set(label, {statisDate: label}) + rowMap.get(label)[alias] = normalizePointValue(point?.pointValue) + }) + }) + const data = [] + byDevice.forEach((rowMap, deviceId) => { + const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind) + data.push({ + deviceId, + dataList: labels.map(label => rowMap.get(label)), + }) + }) + return {data} }) } -//电池温度 +// 电池温度 export function getClusterData({siteId, stackId, clusterId, dateTime, pageNum, pageSize}) { - return request({ - url: `/ems/statsReport/getClusterData?siteId=${siteId}&stackId=${stackId}&clusterId=${clusterId}&dateTime=${dateTime}&pageNum=${pageNum}&pageSize=${pageSize}`, - method: 'get' + const startDate = dateTime || normalizeDateInput('') + const endDate = dateTime || normalizeDateInput('') + const kind = 'minute' + const aliasMap = { + maxTemp: 'maxTemp', + maxTempId: 'maxTempId', + minTemp: 'minTemp', + minTempId: 'minTempId', + maxVoltage: 'maxVoltage', + maxVoltageId: 'maxVoltageId', + minVoltage: 'minVoltage', + minVoltageId: 'minVoltageId', + } + const queryClusterCurves = (withClusterFilter) => queryMenuPointCurves({ + siteId, + menuCode: 'TJBB_DCWD', + startDate, + endDate, + mappingFilter: withClusterFilter + ? (item) => { + if (!clusterId) return true + return String(item?.deviceId || '').trim() === String(clusterId || '').trim() + } + : undefined + }) + + return queryClusterCurves(true).then((records) => { + if (clusterId && (!records || records.length === 0)) { + return queryClusterCurves(false) + } + return records + }).then((records) => { + const rowMap = new Map() + records.forEach((record) => { + const alias = resolveAliasByField(aliasMap, record.fieldName) + if (!alias) return + ;(record.curve || []).forEach((point) => { + const time = point?.dataTime ? new Date(point.dataTime) : null + if (!time || isNaN(time.getTime())) return + const label = formatTimeLabelByKind(time, kind) + if (!rowMap.has(label)) rowMap.set(label, {statisDate: label}) + rowMap.get(label)[alias] = normalizePointValue(point?.pointValue) + }) + }) + const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind) + const fullRows = labels.map(label => rowMap.get(label)) + return { + rows: paginateRows(fullRows, pageNum, pageSize), + total: fullRows.length, + } }) } @@ -233,9 +702,37 @@ export function batteryAveTemp(siteId, startTime, endTime) { // 功率曲线 export function getPowerData({siteId, startDate, endDate}) { - return request({ - url: `/ems/statsReport/getPowerData?siteId=${siteId}&startDate=${startDate}&endDate=${endDate}`, - method: 'get' + const kind = resolveRangeKind(startDate, endDate) + const aliasMap = { + gridPower_stat: 'gridPower', + gridPower: 'gridPower', + loadPower_stat: 'loadPower', + loadPower: 'loadPower', + storagePower_stat: 'storagePower', + storagePower: 'storagePower', + pvPower_stat: 'pvPower', + pvPower: 'pvPower', + } + return queryMenuPointCurves({ + siteId, + menuCode: 'TJBB_GLQX', + startDate, + endDate, + }).then((records) => { + const rowMap = new Map() + records.forEach((record) => { + const alias = resolveAliasByField(aliasMap, record.fieldName) + if (!alias) return + ;(record.curve || []).forEach((point) => { + const time = point?.dataTime ? new Date(point.dataTime) : null + if (!time || isNaN(time.getTime())) return + const label = formatTimeLabelByKind(time, kind) + if (!rowMap.has(label)) rowMap.set(label, {statisDate: label}) + rowMap.get(label)[alias] = normalizePointValue(point?.pointValue) + }) + }) + const labels = buildSortedLabels(new Set(Array.from(rowMap.keys())), kind) + return {data: labels.map(label => rowMap.get(label))} }) } @@ -250,8 +747,15 @@ export function getLoadNameList(siteId) { // 电表报表 export function getAmmeterData({siteId, startTime, endTime, pageSize, pageNum}) { return request({ - url: `/ems/statsReport/getAmmeterData?siteId=${siteId}&startTime=${startTime}&endTime=${endTime}&pageSize=${pageSize}&pageNum=${pageNum}`, - method: 'get' + url: `/ems/statsReport/getAmmeterDataFromDaily`, + method: 'get', + params: { + siteId, + startTime, + endTime, + pageSize, + pageNum, + } }) } @@ -305,6 +809,23 @@ export function configStrategy(data) { }) } +// 获取策略运行参数配置(按站点) +export function getStrategyRuntimeConfig(siteId) { + return request({ + url: `/system/strategyRuntimeConfig/getBySiteId?siteId=${siteId}`, + method: 'get' + }) +} + +// 保存策略运行参数配置(按站点) +export function saveStrategyRuntimeConfig(data) { + return request({ + url: `/system/strategyRuntimeConfig/save`, + method: 'post', + data + }) +} + //http://localhost:8089/strategy/temp/getTempNameList?strategyId=1&siteId=021_FXX_01 //获取策略下的所有模板列表 export function getTempNameList({siteId, strategyId}) { diff --git a/src/api/ems/search.js b/src/api/ems/search.js index 32bd1ea..e1cf73d 100644 --- a/src/api/ems/search.js +++ b/src/api/ems/search.js @@ -29,4 +29,12 @@ export function getAllBatteryIdsBySites(data) { url: `/ems/generalQuery/getAllBatteryIdsBySites/${data}`, method: 'get', }) -} \ No newline at end of file +} + +// 综合查询-按站点获取配置设备列表 +export function getGeneralQueryDeviceList(siteId) { + return request({ + url: `/ems/siteConfig/getDeviceList?siteId=${siteId}`, + method: 'get', + }) +} diff --git a/src/api/ems/site.js b/src/api/ems/site.js index 2f25903..a7dd588 100644 --- a/src/api/ems/site.js +++ b/src/api/ems/site.js @@ -8,6 +8,33 @@ export function getSiteInfoList({siteName, startTime, endTime, pageSize, pageNum }) } +// 手动同步站点天气(收益报表) +export function syncSiteWeatherByDateRange({siteId, startTime, endTime}) { + return request({ + url: `/ems/statsReport/syncWeatherByDateRange`, + method: 'post', + params: {siteId, startTime, endTime} + }) +} + +// 新增站点 +export function addSite(data) { + return request({ + url: `/ems/siteConfig/addSite`, + method: 'post', + data + }) +} + +// 编辑站点 +export function updateSite(data) { + return request({ + url: `/ems/siteConfig/updateSite`, + method: 'post', + data + }) +} + // 设备列表 export function getDeviceInfoList(data) { return request({ @@ -102,6 +129,46 @@ export function getDeviceListBySiteAndCategory({siteId, deviceCategory}) { }) } +// 获取单站监控项目点位映射 +export function getSingleMonitorProjectPointMapping(siteId) { + return request({ + url: `/ems/siteConfig/getSingleMonitorProjectPointMapping?siteId=${siteId}`, + method: 'get', + }) +} + +// 保存单站监控项目点位映射 +export function saveSingleMonitorProjectPointMapping(data) { + return request({ + url: `/ems/siteConfig/saveSingleMonitorProjectPointMapping`, + method: 'post', + data, + headers: { + repeatSubmit: false + } + }) +} + +// 获取单站监控工作状态枚举映射(PCS) +export function getSingleMonitorWorkStatusEnumMappings(siteId) { + return request({ + url: `/ems/siteConfig/getSingleMonitorWorkStatusEnumMappings?siteId=${siteId}`, + method: 'get', + }) +} + +// 保存单站监控工作状态枚举映射(PCS) +export function saveSingleMonitorWorkStatusEnumMappings(data) { + return request({ + url: `/ems/siteConfig/saveSingleMonitorWorkStatusEnumMappings`, + method: 'post', + data, + headers: { + repeatSubmit: false + } + }) +} + //新增设备保护 export function addProtectPlan(data) { return request({ @@ -163,6 +230,234 @@ export function importPointList(data) { }) } +// 按站点导入模板点位配置 +export function importPointTemplateBySite(data) { + return request({ + url: `/ems/pointConfig/importTemplateBySite`, + method: 'post', + data + }) +} + +// CSV导入点位配置 +export function importPointConfigCsv(data) { + return request({ + url: `/ems/pointConfig/importCsv`, + method: 'post', + data, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +// 点位配置列表 +export function getPointMatchList(params) { + return request({ + url: `/ems/pointConfig/list`, + method: 'get', + params + }) +} + +// 点位配置详情 +export function getPointMatchDetail(id) { + return request({ + url: `/ems/pointConfig/${id}`, + method: 'get', + }) +} + +// 新增点位配置 +export function addPointMatch(data) { + return request({ + url: `/ems/pointConfig`, + method: 'post', + data + }) +} + +// 编辑点位配置 +export function updatePointMatch(data) { + return request({ + url: `/ems/pointConfig`, + method: 'put', + data + }) +} + +// 删除点位配置 +export function deletePointMatch(ids) { + return request({ + url: `/ems/pointConfig/${ids}`, + method: 'delete', + }) +} + +// 点位配置-批量获取最新值(新接口) +export function getPointConfigLatestValues(data) { + return request({ + url: `/ems/pointConfig/latestValues`, + method: 'post', + data, + headers: { + repeatSubmit: false + } + }) +} + +// 点位配置-曲线数据(新接口) +export function getPointConfigCurve(data) { + return request({ + url: `/ems/pointConfig/curve`, + method: 'post', + data, + headers: { + repeatSubmit: false + } + }) +} + +// 点位配置-生成最近7天数据 +export function generatePointConfigRecent7Days(data) { + return request({ + url: `/ems/pointConfig/generateRecent7Days`, + method: 'post', + data, + headers: { + repeatSubmit: false + } + }) +} + +// 计算点配置列表 +export function getPointCalcConfigList(params) { + return request({ + url: `/ems/pointCalcConfig/list`, + method: 'get', + params + }) +} + +// 计算点配置详情 +export function getPointCalcConfigDetail(id) { + return request({ + url: `/ems/pointCalcConfig/${id}`, + method: 'get', + }) +} + +// 新增计算点配置 +export function addPointCalcConfig(data) { + return request({ + url: `/ems/pointCalcConfig`, + method: 'post', + data + }) +} + +// 编辑计算点配置 +export function updatePointCalcConfig(data) { + return request({ + url: `/ems/pointCalcConfig`, + method: 'put', + data + }) +} + +// 删除计算点配置 +export function deletePointCalcConfig(ids) { + return request({ + url: `/ems/pointCalcConfig/${ids}`, + method: 'delete', + }) +} + +// 数据修正列表(ems_daily_energy_data) +export function getDailyEnergyDataList(params) { + return request({ + url: `/ems/dailyEnergyData/list`, + method: 'get', + params + }) +} + +// 数据修正详情 +export function getDailyEnergyDataDetail(id) { + return request({ + url: `/ems/dailyEnergyData/${id}`, + method: 'get', + }) +} + +// 新增数据修正 +export function addDailyEnergyData(data) { + return request({ + url: `/ems/dailyEnergyData`, + method: 'post', + data + }) +} + +// 编辑数据修正 +export function updateDailyEnergyData(data) { + return request({ + url: `/ems/dailyEnergyData`, + method: 'put', + data + }) +} + +// 删除数据修正 +export function deleteDailyEnergyData(ids) { + return request({ + url: `/ems/dailyEnergyData/${ids}`, + method: 'delete', + }) +} + +// 充放电修正列表(ems_daily_charge_data) +export function getDailyChargeDataList(params) { + return request({ + url: `/ems/dailyChargeData/list`, + method: 'get', + params + }) +} + +// 充放电修正详情 +export function getDailyChargeDataDetail(id) { + return request({ + url: `/ems/dailyChargeData/${id}`, + method: 'get', + }) +} + +// 新增充放电修正 +export function addDailyChargeData(data) { + return request({ + url: `/ems/dailyChargeData`, + method: 'post', + data + }) +} + +// 编辑充放电修正 +export function updateDailyChargeData(data) { + return request({ + url: `/ems/dailyChargeData`, + method: 'put', + data + }) +} + +// 删除充放电修正 +export function deleteDailyChargeData(ids) { + return request({ + url: `/ems/dailyChargeData/${ids}`, + method: 'delete', + }) +} //mqtt export function getMqttList({pageSize, pageNum, mqttTopic, topicName, siteId}) { diff --git a/src/components/Ems/SingleSquareBox/index.vue b/src/components/Ems/SingleSquareBox/index.vue index 0a39ca2..568d7a7 100644 --- a/src/components/Ems/SingleSquareBox/index.vue +++ b/src/components/Ems/SingleSquareBox/index.vue @@ -2,7 +2,10 @@ @@ -14,18 +17,29 @@ color:#666666; text-align: left; .single-square-box-title{ - font-size: 12px; - line-height: 12px; - padding-bottom: 12px; + font-size: 10px; + line-height: 10px; + padding-bottom: 8px; } .single-square-box-value{ - font-size: 26px; - line-height: 26px; + font-size: 18px; + line-height: 18px; font-weight: 500; } - ::v-deep .el-card__body{ - padding: 12px 10px; + .point-loading-icon{ + color: #409eff; + display: inline-block; + transform-origin: center; + animation: pointLoadingSpinPulse 1.1s linear infinite; } + ::v-deep .el-card__body{ + padding: 8px 7px; + } + } + @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); } } + + diff --git a/src/views/ems/dzjk/clpz/xftg/AddTemplate.vue b/src/views/ems/dzjk/clpz/xftg/AddTemplate.vue index c8f8dd1..9387e5e 100644 --- a/src/views/ems/dzjk/clpz/xftg/AddTemplate.vue +++ b/src/views/ems/dzjk/clpz/xftg/AddTemplate.vue @@ -9,14 +9,6 @@ - - - - - - - - 新增 @@ -64,6 +56,12 @@ + + + + + + + + + + + + + + + { const data = JSON.parse(JSON.stringify(response?.data || [])); if (data.length > 0) { - const {templateName, sdcLimit, sdcDown, sdcUp} = JSON.parse(JSON.stringify(data[0])); + const {templateName, sdcLimit} = JSON.parse(JSON.stringify(data[0])); this.formData.templateName = templateName this.formData.sdcLimit = sdcLimit - this.formData.sdcDown = sdcDown - this.formData.sdcUp = sdcUp } if (data.length === 1) { - const {startTime, endTime} = data; + const {startTime, endTime} = data[0]; if (!startTime || !endTime) { this.tableData = [] } else { @@ -242,15 +316,15 @@ export default { cancelAddTime() { this.$refs.addTimeForm.resetFields() this.showAddTime = false - this.formInline = {timeRange: this.secondRange, chargeDischargePower: '', chargeStatus: ''}//startTime: '', endTime: '', + this.formInline = {timeRange: this.secondRange, chargeDischargePower: '', sdcDown: '', sdcUp: '', chargeStatus: ''}//startTime: '', endTime: '', }, saveTime() { //表单校验,校验成功,添加到tableData里 this.$refs.addTimeForm.validate(valid => { if (!valid) return - const {timeRange: [startTime, endTime], chargeDischargePower, chargeStatus} = this.formInline + const {timeRange: [startTime, endTime], chargeDischargePower, sdcDown, sdcUp, chargeStatus} = this.formInline - this.tableData.push({startTime, endTime, chargeDischargePower, chargeStatus}) + this.tableData.push({startTime, endTime, chargeDischargePower, sdcDown, sdcUp, chargeStatus}) this.$nextTick(() => { this.cancelAddTime() }) @@ -262,9 +336,14 @@ export default { saveDialog() { this.$refs.addTempForm.validate(valid => { if (!valid) return - const {templateName, sdcLimit, sdcDown, sdcUp} = this.formData + const {templateName, sdcLimit} = this.formData const {siteId, updateStrategyId} = this.$home - const {tableData} = this + const tableData = this.tableData.map(item => ({ + ...item, + sdcDown: this.normalizeSocValue(item.sdcDown), + sdcUp: this.normalizeSocValue(item.sdcUp) + })) + if (!this.validateTableData(tableData)) return if (this.mode === 'edit') { editStrategyTemp({ siteId, @@ -272,8 +351,6 @@ export default { templateId: this.editTempId, templateName, sdcLimit, - sdcDown, - sdcUp, timeConfigList: tableData }).then(response => { if (response?.code === 200) { @@ -288,8 +365,6 @@ export default { strategyId: updateStrategyId, templateName, sdcLimit, - sdcDown, - sdcUp, timeConfigList: tableData }).then(response => { if (response?.code === 200) { @@ -300,14 +375,64 @@ export default { } }) }, + normalizeSocValue(value) { + if (value === null || value === undefined) return null + const normalized = String(value).replace('%', '').trim() + return normalized === '' ? null : normalized + }, + toMinutes(timeValue) { + if (!timeValue || String(timeValue).indexOf(':') < 0) return -1 + const [h, m] = String(timeValue).split(':') + const hour = Number(h), minute = Number(m) + if (!Number.isInteger(hour) || !Number.isInteger(minute)) return -1 + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return -1 + return hour * 60 + minute + }, + validateTableData(list = []) { + const numberPattern = /^-?\d+(\.\d+)?$/ + const socPattern = /^(0|[1-9]\d*)(\.\d+)?$/ + for (let i = 0; i < list.length; i++) { + const row = list[i] + const rowNo = i + 1 + if (!row.startTime || !row.endTime) { + this.$message.error(`第${rowNo}行:开始时间和结束时间不能为空`) + return false + } + const startMinute = this.toMinutes(row.startTime) + const endMinute = this.toMinutes(row.endTime) + if (startMinute < 0 || endMinute < 0 || startMinute >= endMinute) { + this.$message.error(`第${rowNo}行:时间范围不合法`) + return false + } + if (!numberPattern.test(String(row.chargeDischargePower ?? '').trim())) { + this.$message.error(`第${rowNo}行:充放功率格式不正确`) + return false + } + if (!socPattern.test(String(row.sdcDown ?? '').trim())) { + this.$message.error(`第${rowNo}行:SOC下限格式不正确`) + return false + } + if (!socPattern.test(String(row.sdcUp ?? '').trim())) { + this.$message.error(`第${rowNo}行:SOC上限格式不正确`) + return false + } + if (Number(row.sdcDown) > Number(row.sdcUp)) { + this.$message.error(`第${rowNo}行:SOC下限不能大于SOC上限`) + return false + } + if (row.chargeStatus === undefined || row.chargeStatus === null || row.chargeStatus === '') { + this.$message.error(`第${rowNo}行:请选择充电状态`) + return false + } + } + return true + }, closeDialog() { // 清空所有数据 this.$refs.addTempForm.resetFields() this.formData = { templateName: '', sdcLimit: 0, - sdcDown: '', - sdcUp: '', } this.tableData = [] this.cancelAddTime() @@ -322,4 +447,4 @@ export default { max-height: 90vh; overflow-y: auto; } - \ No newline at end of file + diff --git a/src/views/ems/dzjk/clpz/xftg/TempTable.vue b/src/views/ems/dzjk/clpz/xftg/TempTable.vue index f79cf9b..3ad3043 100644 --- a/src/views/ems/dzjk/clpz/xftg/TempTable.vue +++ b/src/views/ems/dzjk/clpz/xftg/TempTable.vue @@ -39,14 +39,14 @@ prop="sdcDown" label="SOC下限"> @@ -123,7 +131,7 @@ - diff --git a/src/views/ems/dzjk/home/ActiveChart.vue b/src/views/ems/dzjk/home/ActiveChart.vue index 1df41b1..2375d0d 100644 --- a/src/views/ems/dzjk/home/ActiveChart.vue +++ b/src/views/ems/dzjk/home/ActiveChart.vue @@ -12,12 +12,18 @@ import * as echarts from 'echarts' import resize from '@/mixins/ems/resize' import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue' -import {getPointData} from '@/api/ems/dzjk' +import {getPointConfigCurve} from '@/api/ems/site' import intervalUpdate from "@/mixins/ems/intervalUpdate"; export default { mixins: [resize, intervalUpdate], components: {DateRangeSelect}, + props: { + displayData: { + type: Array, + default: () => [] + } + }, data() { return { chart: null, @@ -26,6 +32,13 @@ export default { isInit: true } }, + watch: { + displayData() { + if (this.siteId && this.timeRange.length === 2) { + this.getGVQXData() + } + } + }, mounted() { this.$nextTick(() => { this.initChart() @@ -46,11 +59,34 @@ export default { this.isInit = false }, getGVQXData() { - this.showLoading() const {siteId, timeRange} = this - getPointData({siteId, startDate: timeRange[0], endDate: timeRange[1]}).then(response => { - this.setOption(response?.data || []) - }).finally(() => this.hideLoading()) + const displayData = this.displayData || [] + const sectionRows = displayData.filter(item => + item && item.sectionName === '当日功率曲线' && item.useFixedDisplay !== 1 && item.dataPoint + ) + const tasks = sectionRows.map(row => { + const pointId = String(row.dataPoint || '').trim() + if (!pointId) return Promise.resolve(null) + return getPointConfigCurve({ + siteId, + pointId, + pointType: 'data', + rangeType: 'custom', + startTime: this.normalizeDateTime(timeRange[0], false), + endTime: this.normalizeDateTime(timeRange[1], true) + }).then(curveResponse => { + const list = curveResponse?.data || [] + return { + name: 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)) + }) }, init(siteId) { //初始化 清空数据 @@ -64,18 +100,18 @@ export default { initChart() { this.chart = echarts.init(document.querySelector('#activeChart')) }, - showLoading() { - this.chart && this.chart.showLoading() + normalizeDateTime(value, endOfDay) { + const raw = String(value || '').trim() + if (!raw) return '' + if (raw.includes(' ')) return raw + return `${raw} ${endOfDay ? '23:59:59' : '00:00:00'}` }, - hideLoading() { - this.chart && this.chart.hideLoading() + parseToTimestamp(value) { + if (!value) return null + const t = new Date(value).getTime() + return Number.isNaN(t) ? null : t }, - setOption(data) { - const source = [['日期', '电网功率', '负载功率', '储能功率', '光伏功率', 'soc平均值', 'soh平均值', '电池平均温度平均值']] - console.log('source.slice(1)', source[0].slice(1)) - this.chart && data.forEach((item) => { - source.push([item.statisDate, item.gridPower, item.loadPower, item.storagePower, item.pvPower, item.avgSoc, item.avgSoh, item.avgTemp]) - }) + setOption(seriesData = []) { this.chart.setOption({ grid: { containLabel: true @@ -86,35 +122,28 @@ export default { }, tooltip: { trigger: 'axis', - axisPointer: { // 坐标轴指示器,坐标轴触发有效 - type: 'shadow' // 默认为直线,可选为:'line' | 'shadow' - } + axisPointer: { type: 'cross' } }, textStyle: { color: "#333333", }, xAxis: { - type: 'category', + type: 'time', }, - yAxis: [ - { - type: 'value', - }, - { - type: 'value', - }, - ], - dataset: {source}, - series: source[0].slice(1).map((item, index) => { + yAxis: [{ + type: 'value', + }], + series: seriesData.map((item) => { return { - type: 'line',//index === 5 ? 'bar' : 'line', + name: item.name, + type: 'line', showSymbol: false, symbolSize: 2, smooth: true, areaStyle: { opacity: 0.5, }, - yAxisIndex: index <= 4 ? 0 : 1 + data: item.data } }) }) @@ -124,4 +153,3 @@ export default { } - diff --git a/src/views/ems/dzjk/home/WeekChart.vue b/src/views/ems/dzjk/home/WeekChart.vue index 592be06..ecbf5bd 100644 --- a/src/views/ems/dzjk/home/WeekChart.vue +++ b/src/views/ems/dzjk/home/WeekChart.vue @@ -12,16 +12,29 @@ import * as echarts from 'echarts' import resize from '@/mixins/ems/resize' import DateRangeSelect from '@/components/Ems/DateRangeSelect/index.vue' -import {getSevenChargeData} from '@/api/ems/dzjk' +import {getPointConfigCurve} from '@/api/ems/site' export default { mixins: [resize], components: {DateRangeSelect}, + props: { + displayData: { + type: Array, + default: () => [] + } + }, data() { return { chart: null, timeRange: [], - siteId: '', + siteId: '' + } + }, + watch: { + displayData() { + if (this.siteId && this.timeRange.length === 2) { + this.getWeekKData() + } } }, mounted() { @@ -43,11 +56,34 @@ export default { this.getWeekKData() }, getWeekKData() { - this.showLoading() const {siteId, timeRange} = this - getSevenChargeData({siteId, startDate: timeRange[0], endDate: timeRange[1]}).then(response => { - this.setOption(response?.data || []) - }).finally(() => this.hideLoading()) + const displayData = this.displayData || [] + const sectionRows = displayData.filter(item => + item && item.sectionName === '一周充放曲线' && item.useFixedDisplay !== 1 && item.dataPoint + ) + const tasks = sectionRows.map(row => { + const pointId = String(row.dataPoint || '').trim() + if (!pointId) return Promise.resolve(null) + return getPointConfigCurve({ + siteId, + pointId, + pointType: 'data', + rangeType: 'custom', + startTime: this.normalizeDateTime(timeRange[0], false), + endTime: this.normalizeDateTime(timeRange[1], true) + }).then(curveResponse => { + const list = curveResponse?.data || [] + return { + name: 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)) + }) }, init(siteId) { //初始化 清空数据 @@ -59,24 +95,23 @@ export default { initChart() { this.chart = echarts.init(document.querySelector('#weekChart')) }, - showLoading() { - this.chart && this.chart.showLoading() + normalizeDateTime(value, endOfDay) { + const raw = String(value || '').trim() + if (!raw) return '' + if (raw.includes(' ')) return raw + return `${raw} ${endOfDay ? '23:59:59' : '00:00:00'}` }, - hideLoading() { - this.chart && this.chart.hideLoading() + parseToTimestamp(value) { + if (!value) return null + const t = new Date(value).getTime() + return Number.isNaN(t) ? null : t }, - setOption(data, unit) { - const source = [['日期', '充电量', '放电量']] - data.forEach(item => { - source.push([item.ammeterDate, item.chargedCap, item.disChargedCap]) - }) + setOption(seriesData = []) { this.chart && this.chart.setOption({ color: ['#4472c4', '#70ad47'],//所有充放电颜色保持统一 tooltip: { trigger: 'axis', - axisPointer: { // 坐标轴指示器,坐标轴触发有效 - type: 'shadow' // 默认为直线,可选为:'line' | 'shadow' - } + axisPointer: { type: 'cross' } }, grid: { containLabel: true @@ -86,9 +121,7 @@ export default { bottom: '15', }, xAxis: { - type: 'category', - name: unit, - nameLocation: 'center' + type: 'time' }, yAxis: [{ type: 'value', @@ -100,19 +133,12 @@ export default { onZero: false } }], - dataset: { - source - }, - series: [ - { - yAxisIndex: 0, - type: 'bar', - }, - { - yAxisIndex: 0, - type: 'bar', - }, - ] + series: seriesData.map(item => ({ + name: item.name, + yAxisIndex: 0, + type: 'bar', + data: item.data + })) }) } } diff --git a/src/views/ems/dzjk/home/index.vue b/src/views/ems/dzjk/home/index.vue index b2f9b1b..96cc194 100644 --- a/src/views/ems/dzjk/home/index.vue +++ b/src/views/ems/dzjk/home/index.vue @@ -1,5 +1,5 @@ diff --git a/src/views/ems/dzjk/sbjk/bmsdcc/index.vue b/src/views/ems/dzjk/sbjk/bmsdcc/index.vue index bee446e..2a2d378 100644 --- a/src/views/ems/dzjk/sbjk/bmsdcc/index.vue +++ b/src/views/ems/dzjk/sbjk/bmsdcc/index.vue @@ -1,6 +1,28 @@ diff --git a/src/views/ems/dzjk/sbjk/bmszl/index.vue b/src/views/ems/dzjk/sbjk/bmszl/index.vue index c120058..78f3a6d 100644 --- a/src/views/ems/dzjk/sbjk/bmszl/index.vue +++ b/src/views/ems/dzjk/sbjk/bmszl/index.vue @@ -1,6 +1,28 @@ diff --git a/src/views/ems/dzjk/sbjk/db/index.vue b/src/views/ems/dzjk/sbjk/db/index.vue index fe5d411..9103933 100644 --- a/src/views/ems/dzjk/sbjk/db/index.vue +++ b/src/views/ems/dzjk/sbjk/db/index.vue @@ -1,228 +1,436 @@ @@ -232,6 +440,58 @@ export default { &.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); } +} diff --git a/src/views/ems/dzjk/sbjk/dh/index.vue b/src/views/ems/dzjk/sbjk/dh/index.vue index 0e424df..5d6ec8f 100644 --- a/src/views/ems/dzjk/sbjk/dh/index.vue +++ b/src/views/ems/dzjk/sbjk/dh/index.vue @@ -1,5 +1,5 @@