diff --git a/src/api/ems/dzjk.js b/src/api/ems/dzjk.js index bbb7104..f0584d6 100644 --- a/src/api/ems/dzjk.js +++ b/src/api/ems/dzjk.js @@ -8,6 +8,14 @@ export function getDzjkHomeView(siteId) { }) } +//获取单个站点总累计运行数据(基于日表) +export function getDzjkHomeTotalView(siteId) { + return request({ + url: `/ems/siteMonitor/homeTotalView?siteId=${siteId}`, + method: 'get' + }) +} + // 单站监控项目点位配置(供单站监控功能查询) export function getProjectPointMapping(siteId) { return request({ @@ -738,88 +746,25 @@ export function getLoadNameList(siteId) { // 电表报表 export function getAmmeterData({siteId, startTime, endTime, pageSize, pageNum}) { - const kind = 'day' - const aliasMap = { - activePeakKwh: 'activePeakKwh', - activeHighKwh: 'activeHighKwh', - activeFlatKwh: 'activeFlatKwh', - activeValleyKwh: 'activeValleyKwh', - activeTotalKwh: 'activeTotalKwh', - reActivePeakKwh: 'reActivePeakKwh', - reActiveHighKwh: 'reActiveHighKwh', - reActiveFlatKwh: 'reActiveFlatKwh', - reActiveValleyKwh: 'reActiveValleyKwh', - reActiveTotalKwh: 'reActiveTotalKwh', - effect: 'effect', - } - return queryMenuPointCurves({ - siteId, - menuCode: 'TJBB_DBBB', - startDate: startTime, - endDate: endTime, - }).then((records) => { - const rowMap = new Map() - records.forEach((record) => { - const alias = 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, {dataTime: 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, + return request({ + url: `/ems/statsReport/getAmmeterDataFromDaily`, + method: 'get', + params: { + siteId, + startTime, + endTime, + pageSize, + pageNum, } }) } // 电价报表 export function getAmmeterRevenueData(data) { - const {siteId, startTime, endTime, pageNum, pageSize} = data || {} - const kind = 'day' - const aliasMap = { - activePeakPrice: 'activePeakPrice', - activeHighPrice: 'activeHighPrice', - activeFlatPrice: 'activeFlatPrice', - activeValleyPrice: 'activeValleyPrice', - activeTotalPrice: 'activeTotalPrice', - reActivePeakPrice: 'reActivePeakPrice', - reActiveHighPrice: 'reActiveHighPrice', - reActiveFlatPrice: 'reActiveFlatPrice', - reActiveValleyPrice: 'reActiveValleyPrice', - reActiveTotalPrice: 'reActiveTotalPrice', - actualRevenue: 'actualRevenue', - } - return queryMenuPointCurves({ - siteId, - menuCode: 'TJBB_SYBB', - startDate: startTime, - endDate: endTime, - }).then((records) => { - const rowMap = new Map() - records.forEach((record) => { - const alias = 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, {dataTime: 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, - } + return request({ + url: `/ems/statsReport/getAmmeterRevenueData`, + method: 'get', + params: data }) } diff --git a/src/api/ems/site.js b/src/api/ems/site.js index 77ec512..a7dd588 100644 --- a/src/api/ems/site.js +++ b/src/api/ems/site.js @@ -8,6 +8,15 @@ 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({ @@ -309,6 +318,18 @@ export function getPointConfigCurve(data) { }) } +// 点位配置-生成最近7天数据 +export function generatePointConfigRecent7Days(data) { + return request({ + url: `/ems/pointConfig/generateRecent7Days`, + method: 'post', + data, + headers: { + repeatSubmit: false + } + }) +} + // 计算点配置列表 export function getPointCalcConfigList(params) { return request({ @@ -352,6 +373,92 @@ export function deletePointCalcConfig(ids) { }) } +// 数据修正列表(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}) { return request({ diff --git a/src/components/Ems/SingleSquareBox/index.vue b/src/components/Ems/SingleSquareBox/index.vue index 2402d4d..568d7a7 100644 --- a/src/components/Ems/SingleSquareBox/index.vue +++ b/src/components/Ems/SingleSquareBox/index.vue @@ -17,13 +17,13 @@ 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; } .point-loading-icon{ @@ -33,7 +33,7 @@ animation: pointLoadingSpinPulse 1.1s linear infinite; } ::v-deep .el-card__body{ - padding: 12px 10px; + padding: 8px 7px; } } @keyframes pointLoadingSpinPulse { diff --git a/src/views/ems/dzjk/home/index.vue b/src/views/ems/dzjk/home/index.vue index 93f8035..96cc194 100644 --- a/src/views/ems/dzjk/home/index.vue +++ b/src/views/ems/dzjk/home/index.vue @@ -42,7 +42,7 @@ class="sjgl-col power-col" >
-
装机功率(MW)
+
装机功率(MWh)
{{ info.installPower | formatNumber }} @@ -54,7 +54,7 @@ class="sjgl-col power-col" >
-
装机容量(MW)
+
装机容量(MWh)
{{ info.installCapacity | formatNumber }} @@ -151,7 +151,7 @@ + + diff --git a/src/views/ems/site/dataCorrection/AddDataCorrection.vue b/src/views/ems/site/dataCorrection/AddDataCorrection.vue new file mode 100644 index 0000000..a34c848 --- /dev/null +++ b/src/views/ems/site/dataCorrection/AddDataCorrection.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/src/views/ems/site/dataCorrection/DailyChargeDataTab.vue b/src/views/ems/site/dataCorrection/DailyChargeDataTab.vue new file mode 100644 index 0000000..7be3f36 --- /dev/null +++ b/src/views/ems/site/dataCorrection/DailyChargeDataTab.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/src/views/ems/site/dataCorrection/DailyEnergyDataTab.vue b/src/views/ems/site/dataCorrection/DailyEnergyDataTab.vue new file mode 100644 index 0000000..6e012d6 --- /dev/null +++ b/src/views/ems/site/dataCorrection/DailyEnergyDataTab.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/src/views/ems/site/dataCorrection/index.vue b/src/views/ems/site/dataCorrection/index.vue new file mode 100644 index 0000000..ff5b5a8 --- /dev/null +++ b/src/views/ems/site/dataCorrection/index.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/src/views/ems/site/pointConfig/index.vue b/src/views/ems/site/pointConfig/index.vue index 5e3cd81..ea99d0a 100644 --- a/src/views/ems/site/pointConfig/index.vue +++ b/src/views/ems/site/pointConfig/index.vue @@ -49,7 +49,7 @@ 新增{{ activePointTab === 'calc' ? '计算点' : '数据点' }} 导入点位 - 覆盖已存在点位数据 + 导出点位
- 示例:A + B * 2;(A + B) / C;voltageA * currentA + powerLoss + 示例:A + B * 2;IF(A > B, A, B);DAY_DIFF(POINT4092);MONTH_DIFF(POINT4092);HOUR_DIFF(POINT4092)
@@ -271,6 +271,13 @@
+ + 生成最近7天数据 + 取消 确定
@@ -343,7 +350,8 @@ import { deletePointMatch, getDeviceListBySiteAndCategory, getPointConfigLatestValues, - getPointConfigCurve + getPointConfigCurve, + generatePointConfigRecent7Days } from '@/api/ems/site' export default { @@ -354,7 +362,6 @@ export default { deviceCategoryList: [], tableData: [], total: 0, - overwrite: false, activePointTab: 'data', queryParams: { pageNum: 1, @@ -371,6 +378,7 @@ export default { curveDialogVisible: false, curveDialogTitle: '曲线', curveLoading: false, + generateRecentLoading: false, curveChart: null, curveCustomRange: [], curveQuery: { @@ -523,80 +531,29 @@ export default { return } const points = this.tableData - .filter(item => item.pointType === 'data' && item.siteId && item.deviceId && item.dataKey) + .filter(item => item.siteId && item.pointId) .map(item => ({ siteId: item.siteId, - deviceId: item.deviceId, - dataKey: item.dataKey + pointId: item.pointId })) if (!points.length) { - const calcRows = this.tableData.filter(item => item.pointType !== 'data') - if (!calcRows.length) { - this.tableData = this.applyCalcLatestValues(this.tableData) - return - } - this.loadCalcDependencyRows(calcRows).then(depRows => { - if (!depRows.length) { - this.tableData = this.applyCalcLatestValues(this.tableData) - return - } - const depPoints = depRows.map(item => ({ - siteId: item.siteId, - deviceId: item.deviceId, - dataKey: item.dataKey - })) - return getPointConfigLatestValues({ points: depPoints }).then(response => { - const latestList = response?.data || [] - const latestMap = latestList.reduce((acc, item) => { - const key = `${item.siteId || ''}__${item.deviceId || ''}__${item.dataKey || ''}` - acc[key] = item.pointValue - return acc - }, {}) - const depRowsWithLatest = depRows.map(row => { - const key = `${row.siteId || ''}__${row.deviceId || ''}__${row.dataKey || ''}` - const latestValue = latestMap[key] - return { - ...row, - latestValue: (latestValue === null || latestValue === undefined || latestValue === '') ? '-' : latestValue - } - }) - const mergedRows = this.applyCalcLatestValues([...depRowsWithLatest, ...this.tableData]) - const calcLatestMap = mergedRows - .filter(item => item.pointType !== 'data') - .reduce((acc, item) => { - acc[this.getCalcRowKey(item)] = item.latestValue - return acc - }, {}) - this.tableData = this.tableData.map(row => { - if (row.pointType === 'data') return row - const nextLatest = calcLatestMap[this.getCalcRowKey(row)] - return { - ...row, - latestValue: (nextLatest === null || nextLatest === undefined || nextLatest === '') ? '-' : nextLatest - } - }) - }) - }).catch(() => { - this.tableData = this.applyCalcLatestValues(this.tableData) - }) return } getPointConfigLatestValues({ points }).then(response => { const latestList = response?.data || [] const latestMap = latestList.reduce((acc, item) => { - const key = `${item.siteId || ''}__${item.deviceId || ''}__${item.dataKey || ''}` + const key = `${item.siteId || ''}__${item.pointId || ''}` acc[key] = item.pointValue return acc }, {}) - const withDataLatestValue = this.tableData.map(row => { - const key = `${row.siteId || ''}__${row.deviceId || ''}__${row.dataKey || ''}` + this.tableData = this.tableData.map(row => { + const key = `${row.siteId || ''}__${row.pointId || ''}` const latestValue = latestMap[key] return { ...row, latestValue: (latestValue === null || latestValue === undefined || latestValue === '') ? '-' : latestValue } }) - this.tableData = this.applyCalcLatestValues(withDataLatestValue) }).catch(() => {}) }, getCalcRowKey(row) { @@ -607,8 +564,8 @@ export default { if (!expr) { return [] } - if (!/^[0-9A-Za-z_+\-*/().\s]+$/.test(expr)) { - throw new Error('计算表达式仅支持四则运算和括号') + if (!/^[0-9A-Za-z_+\-*/().,?:<>=!&|\s]+$/.test(expr)) { + throw new Error('计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法') } const tokens = [] let index = 0 @@ -657,7 +614,20 @@ export default { index += 1 continue } - if (['+', '-', '*', '/'].includes(ch)) { + if (ch === ',' || ch === '?' || ch === ':') { + tokens.push({ type: ch, value: ch }) + index += 1 + continue + } + if (index + 1 < expr.length) { + const twoChars = expr.slice(index, index + 2) + if (['&&', '||', '>=', '<=', '==', '!='].includes(twoChars)) { + tokens.push({ type: 'operator', value: twoChars }) + index += 2 + continue + } + } + if (['+', '-', '*', '/', '>', '<', '!'].includes(ch)) { tokens.push({ type: 'operator', value: ch }) index += 1 continue @@ -668,7 +638,7 @@ export default { return tokens }, extractExpressionTokens(expression) { - const reserved = new Set(['IF']) + const reserved = new Set(['IF', 'DAY_DIFF', 'MONTH_DIFF', 'HOUR_DIFF']) try { const tokens = this.tokenizeCalcExpression(expression) const identifiers = tokens @@ -874,6 +844,24 @@ export default { ? evaluateNode(node.trueNode) : evaluateNode(node.falseNode) } + if (node.type === 'function') { + const fnName = String(node.name || '').toUpperCase() + if (fnName === 'DAY_DIFF') { + if (!Array.isArray(node.args) || node.args.length !== 1) { + throw new Error('DAY_DIFF函数参数数量错误,需1个参数') + } + // 单参数模式依赖后端历史基线,前端仅做语法校验 + throw new Error('DAY_DIFF单参数仅支持后端计算') + } + if (fnName === 'MONTH_DIFF' || fnName === 'HOUR_DIFF') { + if (!Array.isArray(node.args) || node.args.length !== 1) { + throw new Error(`${fnName}函数参数数量错误,需1个参数`) + } + // 单参数模式依赖后端历史基线,前端仅做语法校验 + throw new Error(`${fnName}单参数仅支持后端计算`) + } + throw new Error(`不支持的函数: ${node.name}`) + } throw new Error(`不支持的节点类型: ${node.type}`) } @@ -1009,21 +997,85 @@ export default { if (matchType('identifier')) { const identifier = String(token.value || '') if (matchType('(')) { - if (identifier.toUpperCase() !== 'IF') { - throw new Error(`不支持的函数: ${identifier}`) + const functionName = identifier.toUpperCase() + if (functionName === 'IF') { + const condition = parseExpression() + expectType(',', 'IF函数缺少第1个逗号') + const trueValue = parseExpression() + expectType(',', 'IF函数缺少第2个逗号') + const falseValue = parseExpression() + expectType(')', 'IF函数缺少右括号') + return { + type: 'ternary', + condition, + trueNode: trueValue, + falseNode: falseValue + } } - const condition = parseExpression() - expectType(',', 'IF函数缺少第1个逗号') - const trueValue = parseExpression() - expectType(',', 'IF函数缺少第2个逗号') - const falseValue = parseExpression() - expectType(')', 'IF函数缺少右括号') - return { - type: 'ternary', - condition, - trueNode: trueValue, - falseNode: falseValue + if (functionName === 'DAY_DIFF') { + const args = [] + if (!matchType(')')) { + while (true) { + args.push(parseExpression()) + if (matchType(',')) { + continue + } + expectType(')', 'DAY_DIFF函数缺少右括号') + break + } + } + if (args.length !== 1) { + throw new Error('DAY_DIFF函数参数数量错误,需1个参数') + } + return { + type: 'function', + name: functionName, + args + } } + if (functionName === 'MONTH_DIFF') { + const args = [] + if (!matchType(')')) { + while (true) { + args.push(parseExpression()) + if (matchType(',')) { + continue + } + expectType(')', 'MONTH_DIFF函数缺少右括号') + break + } + } + if (args.length !== 1) { + throw new Error('MONTH_DIFF函数参数数量错误,需1个参数') + } + return { + type: 'function', + name: functionName, + args + } + } + if (functionName === 'HOUR_DIFF') { + const args = [] + if (!matchType(')')) { + while (true) { + args.push(parseExpression()) + if (matchType(',')) { + continue + } + expectType(')', 'HOUR_DIFF函数缺少右括号') + break + } + } + if (args.length !== 1) { + throw new Error('HOUR_DIFF函数参数数量错误,需1个参数') + } + return { + type: 'function', + name: functionName, + args + } + } + throw new Error(`不支持的函数: ${identifier}`) } return { type: 'variable', name: identifier } } @@ -1115,7 +1167,6 @@ export default { const formData = new FormData() formData.append('file', file) formData.append('siteId', siteId) - formData.append('overwrite', String(!!this.overwrite)) this.loading = true return importPointConfigCsv(formData) }).then(response => { @@ -1127,6 +1178,72 @@ export default { this.loading = false }) }, + escapeCsvCell(value) { + if (value === null || value === undefined) { + return '' + } + const text = String(value) + if (/[",\n\r]/.test(text)) { + return `"${text.replace(/"/g, '""')}"` + } + return text + }, + handleExport() { + if (!this.tableData.length) { + this.$message.warning('暂无可导出数据') + return + } + const headers = [ + '站点ID', + '点位ID', + '点位名', + '设备类型', + '设备ID', + '数据键', + '点位描述', + '寄存器地址', + 'A系数', + 'K系数', + 'B系数', + '位偏移', + '类型', + '计算表达式', + '最新值', + '单位' + ] + const rows = this.tableData.map(item => { + return [ + item.siteId || '', + item.pointId || '', + item.pointName || '', + item.deviceCategory || '', + item.deviceId || '', + item.dataKey || '', + item.pointDesc || '', + item.registerAddress || '', + item.dataA === undefined || item.dataA === null ? '' : item.dataA, + item.dataK === undefined || item.dataK === null ? '' : item.dataK, + item.dataB === undefined || item.dataB === null ? '' : item.dataB, + item.dataBit === undefined || item.dataBit === null ? '' : item.dataBit, + item.pointType === 'calc' ? '计算点' : '数据点', + item.pointType === 'calc' ? (item.calcExpression || '') : '', + item.latestValue === undefined || item.latestValue === null ? '' : item.latestValue, + item.dataUnit || '' + ].map(cell => this.escapeCsvCell(cell)) + }) + const csvText = [headers.map(cell => this.escapeCsvCell(cell)).join(','), ...rows.map(row => row.join(','))].join('\n') + const blob = new Blob([`\uFEFF${csvText}`], { type: 'text/csv;charset=utf-8;' }) + const fileName = `point_list_${new Date().getTime()}.csv` + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = fileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + this.$message.success('导出成功') + }, resetForm() { const querySiteId = this.hasValidSiteId(this.queryParams.siteId) ? String(this.queryParams.siteId).trim() : '' this.form = { @@ -1444,6 +1561,33 @@ export default { } this.curveLoading = false }, + handleGenerateRecent7Days() { + if (this.form.pointType !== 'calc') { + return + } + const siteId = String(this.form.siteId || '').trim() + const pointId = String(this.form.pointId || '').trim() + if (!siteId) { + this.$message.warning('站点ID不能为空') + return + } + if (!pointId) { + this.$message.warning('请先输入点位ID') + return + } + this.generateRecentLoading = true + generatePointConfigRecent7Days({ + siteId, + pointId, + deviceId: '' + }).then(response => { + this.$message.success(response?.msg || '最近7天数据生成成功') + }).catch(err => { + this.$message.error(err?.message || '最近7天数据生成失败') + }).finally(() => { + this.generateRecentLoading = false + }) + }, submitForm() { this.$refs.pointForm.validate(valid => { if (!valid) return diff --git a/src/views/ems/site/sbbh/AddPlan.vue b/src/views/ems/site/sbbh/AddPlan.vue index 15fe34e..1e8f241 100644 --- a/src/views/ems/site/sbbh/AddPlan.vue +++ b/src/views/ems/site/sbbh/AddPlan.vue @@ -262,7 +262,8 @@