|
|
@@ -81,16 +81,16 @@
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
</div>
|
|
|
- <div class="attr-section" v-if="hasAttrGroup(systemAttrs, 'Base') || hasAttrGroup(systemAttrs, 'Protocol')">
|
|
|
+ <div class="attr-section" v-if="protocolAttrs.length > 0">
|
|
|
<h4 class="section-title"><i class="el-icon-connection"></i> 协议与基础配置</h4>
|
|
|
- <el-table :data="[...(systemAttrs.Base||[]), ...(systemAttrs.Protocol||[])]" border stripe
|
|
|
+ <el-table :data="protocolAttrs" border stripe
|
|
|
style="width: 100%"
|
|
|
>
|
|
|
<el-table-column prop="attrName" label="属性名称" width="200"></el-table-column>
|
|
|
<el-table-column label="属性值">
|
|
|
<template slot-scope="scope">
|
|
|
- <span v-if="scope.row.attrKey === 'url'" class="url-text">{{ scope.row.attrValue }}</span>
|
|
|
- <span v-else-if="scope.row.attrKey === 'token'" style="display: inline-flex; align-items: center;">
|
|
|
+ <span v-if="scope.row.attrKey === 'url' || scope.row.attrKey === 'addr'" class="url-text">{{ scope.row.attrValue }}</span>
|
|
|
+ <span v-else-if="scope.row.attrKey === 'token' || scope.row.attrKey === 'password'" style="display: inline-flex; align-items: center;">
|
|
|
<span>{{ tokenVisible ? scope.row.attrValue : '••••••••••••••••' }}</span>
|
|
|
<i class="el-icon-view" @click="tokenVisible = !tokenVisible"
|
|
|
style="cursor:pointer;margin-left:10px;color:#409EFF"
|
|
|
@@ -103,6 +103,29 @@
|
|
|
</el-table>
|
|
|
</div>
|
|
|
|
|
|
+ <div class="attr-section" v-if="projectEndpoints.length > 0">
|
|
|
+ <h4 class="section-title"><i class="el-icon-link"></i> 站点端点</h4>
|
|
|
+ <el-table :data="projectEndpoints" border stripe style="width: 100%">
|
|
|
+ <el-table-column prop="siteName" label="站点名称" width="120"></el-table-column>
|
|
|
+ <el-table-column prop="deviceId" label="接入点ID" width="180"></el-table-column>
|
|
|
+ <el-table-column prop="topic" label="订阅主题" min-width="240"></el-table-column>
|
|
|
+ <el-table-column prop="areaCode" label="归属区域" width="160"></el-table-column>
|
|
|
+ <el-table-column label="归属设施" width="120">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ {{ facsNameMap[scope.row.facsCode] || scope.row.facsCode }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="关联设备" min-width="260">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag v-for="d in (scope.row.devices || [])" :key="d.deviceCode" size="mini"
|
|
|
+ style="margin-right:4px;margin-bottom:2px;"
|
|
|
+ >{{ deviceNameMap[d.deviceCode] || d.deviceCode }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="attr-section" v-if="hasAttrGroup(systemAttrs, 'State') || hasAttrGroup(systemAttrs, 'Status')">
|
|
|
<h4 class="section-title"><i class="el-icon-cpu"></i> 运行状态</h4>
|
|
|
<el-row :gutter="15">
|
|
|
@@ -142,6 +165,90 @@
|
|
|
</div>
|
|
|
</el-tab-pane>
|
|
|
|
|
|
+ <el-tab-pane label="系统策略" name="strategy">
|
|
|
+ <div class="tab-content">
|
|
|
+ <el-table :data="strategyList" border stripe>
|
|
|
+ <el-table-column prop="siteName" label="站点" width="120"></el-table-column>
|
|
|
+ <el-table-column prop="deviceId" label="接入点" width="180"></el-table-column>
|
|
|
+ <el-table-column label="类型" width="110" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag size="small" :type="scope.row.type === 'default' ? 'success' : 'warning'">
|
|
|
+ {{ scope.row.type === 'default' ? '默认策略' : '自定义策略' }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="时段数" width="80" align="center">
|
|
|
+ <template slot-scope="scope">{{ (scope.row.slots || []).length }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="updateTime" label="更新时间" width="170"></el-table-column>
|
|
|
+ <el-table-column label="操作" width="200" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button size="mini" icon="el-icon-view" @click="showStrategyDetail(scope.row)">详情</el-button>
|
|
|
+ <el-button size="mini" type="primary" icon="el-icon-s-promotion"
|
|
|
+ :loading="scope.row.sending" @click="sendStrategy(scope.row)"
|
|
|
+ >下发
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+
|
|
|
+ <el-dialog :title="'策略详情 - ' + (strategyEdit.siteName || '')" :visible.sync="strategyDialogVisible"
|
|
|
+ width="860px" append-to-body
|
|
|
+ >
|
|
|
+ <el-table :data="strategyEdit.slots" border size="small">
|
|
|
+ <el-table-column label="峰谷选择" width="110">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-select v-model="scope.row.peakValley" size="mini">
|
|
|
+ <el-option :value="0" label="禁止"></el-option>
|
|
|
+ <el-option :value="1" label="尖"></el-option>
|
|
|
+ <el-option :value="2" label="峰"></el-option>
|
|
|
+ <el-option :value="3" label="平"></el-option>
|
|
|
+ <el-option :value="4" label="谷"></el-option>
|
|
|
+ <el-option :value="5" label="深谷"></el-option>
|
|
|
+ </el-select>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="开始时间" width="140">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-time-select v-model="scope.row.start" size="mini" style="width:110px"
|
|
|
+ :picker-options="{ start: '00:00', step: '00:05', end: '23:55' }"
|
|
|
+ ></el-time-select>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="结束时间" width="140">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-time-select v-model="scope.row.end" size="mini" style="width:110px"
|
|
|
+ :picker-options="{ start: '00:00', step: '00:05', end: '23:55' }"
|
|
|
+ ></el-time-select>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="最大充电功率(kW)" width="160">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-input-number v-model="scope.row.chargeKw" :min="0" size="mini" style="width:130px"></el-input-number>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="最大放电功率(kW)" width="160">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-input-number v-model="scope.row.dischargeKw" :min="0" size="mini" style="width:130px"></el-input-number>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="70" align="center">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button type="text" size="mini" style="color:#F56C6C" @click="removeStrategySlot(scope.$index)">删除</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div style="margin-top:10px;">
|
|
|
+ <el-button size="mini" icon="el-icon-plus" @click="addStrategySlot">添加时段</el-button>
|
|
|
+ </div>
|
|
|
+ <div slot="footer">
|
|
|
+ <el-button @click="strategyDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="strategySaving" @click="saveStrategy">保存</el-button>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
<el-tab-pane
|
|
|
v-for="(config, index) in refDeviceConfigs"
|
|
|
:key="config.modelCode"
|
|
|
@@ -153,12 +260,6 @@
|
|
|
<div class="device-stats">
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="4">
|
|
|
- <div class="stat-item">
|
|
|
- <div class="stat-value">{{ (deviceDataMap[config.modelCode] || []).length }}</div>
|
|
|
- <div class="stat-label">设备总数</div>
|
|
|
- </div>
|
|
|
- </el-col>
|
|
|
- <el-col :span="4">
|
|
|
<div class="stat-item online">
|
|
|
<div class="stat-value">
|
|
|
{{ (deviceDataMap[config.modelCode] || []).filter(d => d.deviceStatus === 1).length }}
|
|
|
@@ -174,6 +275,12 @@
|
|
|
<div class="stat-label">离线设备</div>
|
|
|
</div>
|
|
|
</el-col>
|
|
|
+ <el-col :span="4" v-for="stat in modelExtraStats(config.modelCode)" :key="stat.label">
|
|
|
+ <div class="stat-item">
|
|
|
+ <div class="stat-value">{{ stat.value }} <small>{{ stat.unit }}</small></div>
|
|
|
+ <div class="stat-label">{{ stat.label }}</div>
|
|
|
+ </div>
|
|
|
+ </el-col>
|
|
|
</el-row>
|
|
|
</div>
|
|
|
|
|
|
@@ -191,15 +298,12 @@
|
|
|
<el-tabs v-model="props.row.detailActiveTab">
|
|
|
<el-tab-pane label="监测数据" name="monitor">
|
|
|
|
|
|
- <div v-if="hasDeviceGroup(props.row, 'Measure') || hasDeviceGroup(props.row, 'Status')"
|
|
|
- class="group-section"
|
|
|
+ <div v-for="group in getDetailGroups(props.row)" :key="group.name"
|
|
|
+ class="group-section" style="margin-top: 12px;"
|
|
|
>
|
|
|
- <div class="sub-title"><i class="el-icon-odometer"></i> 实时计量与状态</div>
|
|
|
+ <div class="sub-title"><i class="el-icon-odometer"></i> {{ group.label }}</div>
|
|
|
<el-row :gutter="15">
|
|
|
- <el-col :span="4"
|
|
|
- v-for="attr in [...getDeviceAttrs(props.row, 'Measure'), ...getDeviceAttrs(props.row, 'Status')]"
|
|
|
- :key="attr.attrKey"
|
|
|
- >
|
|
|
+ <el-col :span="4" v-for="attr in group.attrs" :key="attr.attrKey">
|
|
|
<div class="param-card">
|
|
|
<div class="param-label">{{ attr.attrName }}</div>
|
|
|
<div class="param-value">
|
|
|
@@ -288,7 +392,8 @@
|
|
|
range-separator="至"
|
|
|
start-placeholder="开始"
|
|
|
end-placeholder="结束"
|
|
|
- value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ format="yyyy-MM-dd HH:mm"
|
|
|
+ value-format="yyyy-MM-dd HH:mm"
|
|
|
>
|
|
|
</el-date-picker>
|
|
|
</el-form-item>
|
|
|
@@ -332,7 +437,8 @@
|
|
|
<el-date-picker
|
|
|
v-model="eventLogQuery.dateRange"
|
|
|
type="datetimerange"
|
|
|
- value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ format="yyyy-MM-dd HH:mm"
|
|
|
+ value-format="yyyy-MM-dd HH:mm"
|
|
|
>
|
|
|
</el-date-picker>
|
|
|
</el-form-item>
|
|
|
@@ -363,11 +469,47 @@
|
|
|
</el-tabs>
|
|
|
</el-card>
|
|
|
|
|
|
- <el-dialog :visible.sync="callLogDetailDialog" title="日志详情" width="50%">
|
|
|
- <pre v-if="callLogDetailData">{{ JSON.stringify(callLogDetailData, null, 2) }}</pre>
|
|
|
+ <el-dialog :visible.sync="callLogDetailDialog" title="调用日志详情" width="60%">
|
|
|
+ <div v-if="callLogDetailData">
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
+ <el-descriptions-item label="对象代码">{{ callLogDetailData.objCode }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="对象名称">{{ callLogDetailData.objName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="能力标识">{{ callLogDetailData.abilityKey }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="能力名称">{{ callLogDetailData.abilityName }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="调用时间">{{ callLogDetailData.callTime }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="响应时间">{{ callLogDetailData.resTime || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="调用结果">
|
|
|
+ <el-tag :type="getCallStatusType(callLogDetailData.callStatus)">
|
|
|
+ {{ formatCallStatus(callLogDetailData.callStatus) }}
|
|
|
+ </el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div style="margin-top: 20px;">
|
|
|
+ <h4>调用载体</h4>
|
|
|
+ <pre style="white-space: pre-wrap; background-color: #f5f5f5; padding: 10px; border-radius: 4px; max-height: 300px; overflow: auto; font-size: 12px;">{{ formatPayload(callLogDetailData.callPayload) }}</pre>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="margin-top: 20px;">
|
|
|
+ <h4>响应载体</h4>
|
|
|
+ <pre style="white-space: pre-wrap; background-color: #f5f5f5; padding: 10px; border-radius: 4px; max-height: 300px; overflow: auto; font-size: 12px;">{{ formatPayload(callLogDetailData.resPayload) }}</pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</el-dialog>
|
|
|
- <el-dialog :visible.sync="eventLogDetailDialog" title="事件详情" width="50%">
|
|
|
- <pre v-if="eventLogDetailData">{{ JSON.stringify(eventLogDetailData, null, 2) }}</pre>
|
|
|
+ <el-dialog :visible.sync="eventLogDetailDialog" title="事件日志详情" width="60%">
|
|
|
+ <div v-if="eventLogDetailData">
|
|
|
+ <el-descriptions :column="2" border>
|
|
|
+ <el-descriptions-item label="对象编号">{{ eventLogDetailData.objCode }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="模型代码">{{ eventLogDetailData.modelCode }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="事件标识">{{ eventLogDetailData.eventKey }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="事件时间">{{ eventLogDetailData.eventTime }}</el-descriptions-item>
|
|
|
+ </el-descriptions>
|
|
|
+
|
|
|
+ <div style="margin-top: 20px;">
|
|
|
+ <h4>事件详情</h4>
|
|
|
+ <pre style="white-space: pre-wrap; background-color: #f5f5f5; padding: 10px; border-radius: 4px; max-height: 300px; overflow: auto; font-size: 12px;">{{ formatPayload(eventLogDetailData.eventDetail) }}</pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</el-dialog>
|
|
|
|
|
|
<el-dialog :title="abilityDialog.title" :visible.sync="abilityDialog.visible" width="500px">
|
|
|
@@ -405,7 +547,9 @@ import { getSubsystemByCode, updateSubsystem } from '@/api/adapter/subsystem'
|
|
|
import { getModelByCode } from '@/api/basecfg/objModel'
|
|
|
import { getObjAttr, getObjAttrBatch } from '@/api/basecfg/objAttribute'
|
|
|
import { callAbility } from '@/api/basecfg/objAbility'
|
|
|
+import { updateAttrValue } from '@/api/basecfg/objAttributeValue'
|
|
|
import { getByCondition } from '@/api/device/device'
|
|
|
+import { listAllFacs } from '@/api/basecfg/emsfacs'
|
|
|
import { listCallLog, listEventLog, getCallLog, getEventLog } from '@/api/basecfg/objLog'
|
|
|
|
|
|
export default {
|
|
|
@@ -418,14 +562,14 @@ export default {
|
|
|
// 关联设备配置:支持配置多个设备模型Tab
|
|
|
// 只要这里加了配置,页面就会自动生成对应的Tab和表格
|
|
|
refDeviceConfigs: [
|
|
|
- { label: '储能设备', modelCode: 'M_W4_DEV_CN' }
|
|
|
+ { label: '储能电表', modelCode: 'M_W4_DEV_GCC_DB' },
|
|
|
+ { label: '储能一体机', modelCode: 'M_W4_DEV_GCC_YTJ' }
|
|
|
],
|
|
|
|
|
|
sysStaIndexList: [
|
|
|
{
|
|
|
indexKey: 'sumRatedCapacity',
|
|
|
indexName: '额定总容量',
|
|
|
- indexSumFrom: 'M_W4_DEV_CN.ratedCapacity',
|
|
|
unit: 'kWh',
|
|
|
icon: 'el-icon-s-data',
|
|
|
color: '#409EFF'
|
|
|
@@ -433,24 +577,21 @@ export default {
|
|
|
{
|
|
|
indexKey: 'rtCapacity',
|
|
|
indexName: '实时总容量',
|
|
|
- indexSumFrom: 'M_W4_DEV_CN.rtCapacity',
|
|
|
unit: 'kWh',
|
|
|
icon: 'el-icon-pie-chart',
|
|
|
color: '#67C23A'
|
|
|
},
|
|
|
{
|
|
|
- indexKey: 'sumChargingPower',
|
|
|
- indexName: '总充电功率',
|
|
|
- indexSumFrom: 'M_W4_DEV_CN.chargingPower',
|
|
|
- unit: 'W',
|
|
|
+ indexKey: 'sumCumCharge',
|
|
|
+ indexName: '累计充电量',
|
|
|
+ unit: 'kWh',
|
|
|
icon: 'el-icon-lightning',
|
|
|
color: '#E6A23C'
|
|
|
},
|
|
|
{
|
|
|
- indexKey: 'sumDischargePower',
|
|
|
- indexName: '总放电功率',
|
|
|
- indexSumFrom: 'M_W4_DEV_CN.dischargePower',
|
|
|
- unit: 'W',
|
|
|
+ indexKey: 'sumCumDischarge',
|
|
|
+ indexName: '累计放电量',
|
|
|
+ unit: 'kWh',
|
|
|
icon: 'el-icon-upload2',
|
|
|
color: '#F56C6C'
|
|
|
}
|
|
|
@@ -463,15 +604,17 @@ export default {
|
|
|
systemAbilities: [],
|
|
|
|
|
|
// 设备数据 Map结构:key = modelCode
|
|
|
- // deviceDataMap['M_W4_DEV_CN'] = [设备列表]
|
|
|
+ // deviceDataMap['M_W4_DEV_GCC_YTJ'] = [设备列表]
|
|
|
deviceDataMap: {},
|
|
|
// 设备属性值 Map:key = deviceCode
|
|
|
- // deviceAttrMap['GCZR-001'] = { Base:[], Status:[] }
|
|
|
+ // deviceAttrMap['GCC-YTJ-01'] = { Base:[], PCS:[], BMS:[], Fault:[] }
|
|
|
deviceAttrMap: {},
|
|
|
// 设备能力定义 Map: key = modelCode
|
|
|
deviceAbilityMap: {},
|
|
|
// 设备加载状态 Map
|
|
|
deviceLoadingMap: {},
|
|
|
+ // 设施代码->名称映射
|
|
|
+ facsNameMap: {},
|
|
|
|
|
|
// 所有相关的模型Code (用于查询日志)
|
|
|
allModelCodes: [],
|
|
|
@@ -492,6 +635,11 @@ export default {
|
|
|
eventLogDetailDialog: false, eventLogDetailData: null,
|
|
|
|
|
|
abilityDialog: { visible: false, title: '' },
|
|
|
+ // 系统策略编辑
|
|
|
+ strategyDialogVisible: false,
|
|
|
+ strategyEdit: { slots: [] },
|
|
|
+ strategyEditAttrKey: '',
|
|
|
+ strategySaving: false,
|
|
|
abilityForm: {}, abilityParamType: null, abilityInputValue: '', abilitySliderValue: 0, abilityExecuting: false,
|
|
|
currentAbilityObj: null, currentAbilityType: null, currentAbilityModel: null,
|
|
|
|
|
|
@@ -503,46 +651,98 @@ export default {
|
|
|
this.initSystem()
|
|
|
},
|
|
|
computed: {
|
|
|
- // ========== 动态计算统计值 ==========
|
|
|
+ // ========== 动态计算统计值(基于储能一体机属性) ==========
|
|
|
calculatedSystemStats() {
|
|
|
if (!this.sysStaIndexList || this.sysStaIndexList.length === 0) return []
|
|
|
|
|
|
- return this.sysStaIndexList.map(config => {
|
|
|
- // 1. 解析配置: M_W4_DEV_CN.ratedCapacity
|
|
|
- const parts = config.indexSumFrom.split('.')
|
|
|
- if (parts.length !== 2) return { ...config, value: '-' }
|
|
|
-
|
|
|
- const targetModel = parts[0]
|
|
|
- const targetAttrKey = parts[1]
|
|
|
-
|
|
|
- // 2. 获取该模型下的所有设备
|
|
|
- const devices = this.deviceDataMap[targetModel] || []
|
|
|
-
|
|
|
- // 3. 遍历求和
|
|
|
- let sum = 0
|
|
|
- let hasValue = false // 标记是否有有效值参与计算
|
|
|
-
|
|
|
- devices.forEach(dev => {
|
|
|
- // 从 deviceAttrMap 中获取该设备的属性包
|
|
|
- const devAttrs = this.deviceAttrMap[dev.deviceCode]
|
|
|
- if (devAttrs) {
|
|
|
- // 属性可能在 Base, Status, Measure 等任意分组中,需要查找
|
|
|
- const val = this.findAttrValueInAllGroups(devAttrs, targetAttrKey)
|
|
|
- // 确保是数字
|
|
|
- const numVal = parseFloat(val)
|
|
|
- if (!isNaN(numVal)) {
|
|
|
- sum += numVal
|
|
|
- hasValue = true
|
|
|
- }
|
|
|
- }
|
|
|
- })
|
|
|
+ const devices = this.deviceDataMap['M_W4_DEV_GCC_YTJ'] || []
|
|
|
+ let rated = 0
|
|
|
+ let rt = 0
|
|
|
+ let cumCharge = 0
|
|
|
+ let cumDischarge = 0
|
|
|
+
|
|
|
+ devices.forEach(dev => {
|
|
|
+ const devAttrs = this.deviceAttrMap[dev.deviceCode]
|
|
|
+ if (!devAttrs) return
|
|
|
+
|
|
|
+ const num = (key) => {
|
|
|
+ const v = parseFloat(this.findAttrValueInAllGroups(devAttrs, key))
|
|
|
+ return isNaN(v) ? 0 : v
|
|
|
+ }
|
|
|
+
|
|
|
+ const ratedCap = num('ratedCapacity')
|
|
|
+ rated += ratedCap
|
|
|
+ // 当前容量 = 额定容量 * SOC * SOH
|
|
|
+ rt += ratedCap * num('bmsSoc') / 100 * num('bmsSoh') / 100
|
|
|
+
|
|
|
+ // 累计充放电量(BMS上报)
|
|
|
+ cumCharge += num('bmsCumChargeKwh')
|
|
|
+ cumDischarge += num('bmsCumDischargeKwh')
|
|
|
+ })
|
|
|
+
|
|
|
+ const valueMap = {
|
|
|
+ sumRatedCapacity: rated,
|
|
|
+ rtCapacity: rt,
|
|
|
+ sumCumCharge: cumCharge,
|
|
|
+ sumCumDischarge: cumDischarge
|
|
|
+ }
|
|
|
|
|
|
- // 4. 格式化结果 (保留2位小数)
|
|
|
+ return this.sysStaIndexList.map(config => ({
|
|
|
+ ...config,
|
|
|
+ value: Number((valueMap[config.indexKey] || 0).toFixed(2))
|
|
|
+ }))
|
|
|
+ },
|
|
|
+
|
|
|
+ // 系统策略列表(解析Strategy分组属性值,每站点一条)
|
|
|
+ strategyList() {
|
|
|
+ const attrs = this.systemAttrs.Strategy || []
|
|
|
+ return attrs.map(attr => {
|
|
|
+ let strategy = {}
|
|
|
+ try {
|
|
|
+ strategy = JSON.parse(attr.attrValue) || {}
|
|
|
+ } catch (e) {
|
|
|
+ console.error('策略属性解析失败', attr.attrKey, e)
|
|
|
+ }
|
|
|
return {
|
|
|
- ...config,
|
|
|
- value: hasValue ? Number(sum.toFixed(2)) : 0
|
|
|
+ attrKey: attr.attrKey,
|
|
|
+ updateTime: attr.updateTime,
|
|
|
+ deviceId: strategy.deviceId || attr.attrKey.replace('strategy_', ''),
|
|
|
+ siteName: strategy.siteName || attr.attrName || '-',
|
|
|
+ type: strategy.type || 'default',
|
|
|
+ sysFlag: strategy.sysFlag,
|
|
|
+ slots: strategy.slots || [],
|
|
|
+ sending: false
|
|
|
}
|
|
|
})
|
|
|
+ },
|
|
|
+
|
|
|
+ // 站点端点列表(解析projectList属性值)
|
|
|
+ projectEndpoints() {
|
|
|
+ const attr = (this.systemAttrs.Base || []).find(a => a.attrKey === 'projectList')
|
|
|
+ if (!attr || !attr.attrValue) return []
|
|
|
+ try {
|
|
|
+ return JSON.parse(attr.attrValue) || []
|
|
|
+ } catch (e) {
|
|
|
+ console.error('projectList解析失败', e)
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设备代码->名称映射(用于站点端点关联设备展示)
|
|
|
+ deviceNameMap() {
|
|
|
+ const map = {}
|
|
|
+ Object.keys(this.deviceDataMap).forEach(modelCode => {
|
|
|
+ ;(this.deviceDataMap[modelCode] || []).forEach(d => {
|
|
|
+ map[d.deviceCode] = d.deviceName || d.deviceCode
|
|
|
+ })
|
|
|
+ })
|
|
|
+ return map
|
|
|
+ },
|
|
|
+
|
|
|
+ // 协议与基础配置属性(排除projectList大字段)
|
|
|
+ protocolAttrs() {
|
|
|
+ return [...(this.systemAttrs.Base || []), ...(this.systemAttrs.Protocol || [])]
|
|
|
+ .filter(a => a.attrKey !== 'projectList')
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
@@ -550,6 +750,22 @@ export default {
|
|
|
async initSystem() {
|
|
|
await this.loadSystemInfo()
|
|
|
this.loadAllRefDevices()
|
|
|
+ this.loadFacsMap()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 加载设施代码->名称映射
|
|
|
+ async loadFacsMap() {
|
|
|
+ try {
|
|
|
+ const res = await listAllFacs({})
|
|
|
+ const rows = res.rows || res.data || []
|
|
|
+ const map = {}
|
|
|
+ rows.forEach(f => {
|
|
|
+ map[f.facsCode] = f.facsName
|
|
|
+ })
|
|
|
+ this.facsNameMap = map
|
|
|
+ } catch (e) {
|
|
|
+ console.error('加载设施列表失败', e)
|
|
|
+ }
|
|
|
},
|
|
|
|
|
|
// 1. 加载系统层级信息
|
|
|
@@ -653,6 +869,48 @@ export default {
|
|
|
return devAttrs[groupName]
|
|
|
},
|
|
|
|
|
|
+ // 模型扩展统计(储能电表:正向=充电 反向=放电 电能汇总)
|
|
|
+ modelExtraStats(modelCode) {
|
|
|
+ if (modelCode !== 'M_W4_DEV_GCC_DB') return []
|
|
|
+
|
|
|
+ let fwd = 0
|
|
|
+ let rev = 0
|
|
|
+ ;(this.deviceDataMap[modelCode] || []).forEach(dev => {
|
|
|
+ const attrs = this.deviceAttrMap[dev.deviceCode]
|
|
|
+ if (!attrs) return
|
|
|
+ const num = (key) => {
|
|
|
+ const v = parseFloat(this.findAttrValueInAllGroups(attrs, key))
|
|
|
+ return isNaN(v) ? 0 : v
|
|
|
+ }
|
|
|
+ fwd += num('emFwdActiveEnergy')
|
|
|
+ rev += num('emRevActiveEnergy')
|
|
|
+ })
|
|
|
+
|
|
|
+ return [
|
|
|
+ { label: '总充电电能', value: fwd.toFixed(2), unit: 'kWh' },
|
|
|
+ { label: '总放电电能', value: rev.toFixed(2), unit: 'kWh' }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设备详情监测分组(排除档案类分组,兼容PCS/BMS/Fault/Measure/Status等)
|
|
|
+ getDetailGroups(row) {
|
|
|
+ const devAttrs = this.deviceAttrMap[row.deviceCode]
|
|
|
+ if (!devAttrs) return []
|
|
|
+
|
|
|
+ const labels = {
|
|
|
+ PCS: 'PCS监测',
|
|
|
+ BMS: 'BMS监测',
|
|
|
+ Fault: '故障信息',
|
|
|
+ Measure: '实时计量',
|
|
|
+ Status: '状态'
|
|
|
+ }
|
|
|
+ const skip = ['Base', 'Protocol', 'objType', 'objCode', 'modelCode']
|
|
|
+
|
|
|
+ return Object.keys(devAttrs)
|
|
|
+ .filter(k => !skip.includes(k) && Array.isArray(devAttrs[k]) && devAttrs[k].length > 0)
|
|
|
+ .map(k => ({ name: k, label: labels[k] || k, attrs: devAttrs[k] }))
|
|
|
+ },
|
|
|
+
|
|
|
// 获取设备最后更新时间 (从Base属性或Row中找)
|
|
|
getDeviceLastUpdateTime(row) {
|
|
|
return row.updateTime || '-'
|
|
|
@@ -685,6 +943,72 @@ export default {
|
|
|
return text === '正常' || text === '运行中' ? 'success' : 'danger'
|
|
|
},
|
|
|
|
|
|
+ // ========== 系统策略操作 ==========
|
|
|
+ showStrategyDetail(row) {
|
|
|
+ this.strategyEditAttrKey = row.attrKey
|
|
|
+ this.strategyEdit = JSON.parse(JSON.stringify({
|
|
|
+ deviceId: row.deviceId,
|
|
|
+ siteName: row.siteName,
|
|
|
+ type: row.type,
|
|
|
+ sysFlag: row.sysFlag,
|
|
|
+ slots: row.slots || []
|
|
|
+ }))
|
|
|
+ this.strategyDialogVisible = true
|
|
|
+ },
|
|
|
+ addStrategySlot() {
|
|
|
+ this.strategyEdit.slots.push({ peakValley: 0, start: '00:00', end: '01:00', chargeKw: 0, dischargeKw: 0 })
|
|
|
+ },
|
|
|
+ removeStrategySlot(index) {
|
|
|
+ this.strategyEdit.slots.splice(index, 1)
|
|
|
+ },
|
|
|
+ async saveStrategy() {
|
|
|
+ this.strategySaving = true
|
|
|
+ try {
|
|
|
+ const payload = {
|
|
|
+ deviceId: this.strategyEdit.deviceId,
|
|
|
+ siteName: this.strategyEdit.siteName,
|
|
|
+ type: this.strategyEdit.type || 'default',
|
|
|
+ sysFlag: this.strategyEdit.sysFlag,
|
|
|
+ slots: this.strategyEdit.slots
|
|
|
+ }
|
|
|
+ await updateAttrValue({
|
|
|
+ objCode: this.systemInfo.systemCode,
|
|
|
+ modelCode: this.systemInfo.modelCode,
|
|
|
+ attrKey: this.strategyEditAttrKey,
|
|
|
+ attrValue: JSON.stringify(payload)
|
|
|
+ })
|
|
|
+ this.$message.success('策略保存成功')
|
|
|
+ this.strategyDialogVisible = false
|
|
|
+ await this.loadSystemInfo()
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('策略保存失败: ' + (e.message || e))
|
|
|
+ } finally {
|
|
|
+ this.strategySaving = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async sendStrategy(row) {
|
|
|
+ try {
|
|
|
+ await this.$confirm('确认向站点[' + row.siteName + ']下发策略?', '提示', { type: 'warning' })
|
|
|
+ } catch (e) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ row.sending = true
|
|
|
+ try {
|
|
|
+ await callAbility({
|
|
|
+ objCode: this.systemInfo.systemCode,
|
|
|
+ objType: 3,
|
|
|
+ modelCode: this.systemInfo.modelCode,
|
|
|
+ abilityKey: 'applyDefaultStrategy',
|
|
|
+ abilityParam: row.deviceId
|
|
|
+ })
|
|
|
+ this.$message.success('下发指令已发送,请到调用日志查看EMS应答')
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('下发失败: ' + (e.message || e))
|
|
|
+ } finally {
|
|
|
+ row.sending = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
// ========== 5. 能力执行逻辑 (通用) ==========
|
|
|
handleAbilityClick(ability, objInfo, objType, modelCode) {
|
|
|
this.currentAbilityObj = objInfo
|
|
|
@@ -746,15 +1070,22 @@ export default {
|
|
|
// ========== 6. 日志逻辑 (保持原样) ==========
|
|
|
initDateRange() {
|
|
|
const now = new Date()
|
|
|
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
|
|
|
- const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
|
|
|
+ // 默认查询最近1小时,避免数据量大导致查询超时
|
|
|
+ const start = new Date(now.getTime() - 60 * 60 * 1000)
|
|
|
+ const end = new Date(now)
|
|
|
const range = [this.formatDate(start), this.formatDate(end)]
|
|
|
this.callLogQuery.dateRange = range
|
|
|
this.eventLogQuery.dateRange = range
|
|
|
},
|
|
|
formatDate(date) {
|
|
|
- // 简易格式化,建议使用 dayjs 或保持你原有的
|
|
|
- return date.toISOString().replace('T', ' ').substring(0, 19)
|
|
|
+ // 本地时间,格式化到分钟
|
|
|
+ if (!date) return ''
|
|
|
+ const y = date.getFullYear()
|
|
|
+ const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ const d = date.getDate().toString().padStart(2, '0')
|
|
|
+ const h = date.getHours().toString().padStart(2, '0')
|
|
|
+ const mi = date.getMinutes().toString().padStart(2, '0')
|
|
|
+ return `${y}-${m}-${d} ${h}:${mi}`
|
|
|
},
|
|
|
handleTabClick(tab) {
|
|
|
if (tab.name === 'callLogs' && this.callLogList.length === 0) this.queryCallLogs()
|
|
|
@@ -765,7 +1096,7 @@ export default {
|
|
|
this.logLoading = true
|
|
|
try {
|
|
|
const params = {
|
|
|
- modelCodes: this.allModelCodes, // 动态传递所有涉及的模型
|
|
|
+ modelCodes: (this.allModelCodes || []).join(','), // 动态传递所有涉及的模型
|
|
|
pageNum: this.callLogQuery.pageNum,
|
|
|
pageSize: this.callLogQuery.pageSize,
|
|
|
abilityKey: this.callLogQuery.abilityKey
|
|
|
@@ -789,7 +1120,11 @@ export default {
|
|
|
},
|
|
|
async handleCallLogDetail(row) {
|
|
|
const res = await getCallLog(row.id)
|
|
|
- this.callLogDetailData = res.data
|
|
|
+ const data = res.data || {}
|
|
|
+ // payload 为 JSON 字符串时解析为对象,详情弹窗结构化展示(topic + msgBody)
|
|
|
+ data.callPayload = this.tryParseJson(data.callPayload)
|
|
|
+ data.resPayload = this.tryParseJson(data.resPayload)
|
|
|
+ this.callLogDetailData = data
|
|
|
this.callLogDetailDialog = true
|
|
|
},
|
|
|
// 查询事件日志
|
|
|
@@ -797,7 +1132,7 @@ export default {
|
|
|
this.eventLogLoading = true
|
|
|
try {
|
|
|
const params = {
|
|
|
- modelCodes: this.allModelCodes,
|
|
|
+ modelCodes: (this.allModelCodes || []).join(','),
|
|
|
pageNum: this.eventLogQuery.pageNum,
|
|
|
pageSize: this.eventLogQuery.pageSize,
|
|
|
eventKey: this.eventLogQuery.eventKey
|
|
|
@@ -821,10 +1156,44 @@ export default {
|
|
|
},
|
|
|
async handleEventLogDetail(row) {
|
|
|
const res = await getEventLog(row.id)
|
|
|
- this.eventLogDetailData = res.data
|
|
|
+ const data = res.data || {}
|
|
|
+ data.eventDetail = this.tryParseJson(data.eventDetail)
|
|
|
+ this.eventLogDetailData = data
|
|
|
this.eventLogDetailDialog = true
|
|
|
},
|
|
|
|
|
|
+ // 调用结果文案
|
|
|
+ formatCallStatus(status) {
|
|
|
+ const statusMap = { 0: '成功', 1: '进行中', 2: '失败', '-1': '失败' }
|
|
|
+ return statusMap[status] || '未知'
|
|
|
+ },
|
|
|
+ getCallStatusType(status) {
|
|
|
+ const typeMap = { 0: 'success', 1: 'warning', 2: 'danger', '-1': 'danger' }
|
|
|
+ return typeMap[status] || 'info'
|
|
|
+ },
|
|
|
+ // 载体内容格式化:对象/JSON字符串统一美化输出
|
|
|
+ formatPayload(value) {
|
|
|
+ if (value === null || value === undefined || value === '') return '无'
|
|
|
+ if (typeof value === 'string') {
|
|
|
+ try {
|
|
|
+ return JSON.stringify(JSON.parse(value), null, 2)
|
|
|
+ } catch (e) {
|
|
|
+ return value
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return JSON.stringify(value, null, 2)
|
|
|
+ },
|
|
|
+
|
|
|
+ // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
|
|
|
+ tryParseJson(value) {
|
|
|
+ if (!value || typeof value !== 'string') return value
|
|
|
+ try {
|
|
|
+ return JSON.parse(value)
|
|
|
+ } catch (e) {
|
|
|
+ return value
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
// ========== 7. 编辑系统 ==========
|
|
|
handleEditSystem() {
|
|
|
this.editSystemForm = { ...this.systemInfo }
|