Parcourir la source

储能接入 前端

learshaw il y a 2 jours
Parent
commit
e423ce0bf1

+ 11 - 0
ems-ui-cloud/src/api/basecfg/objAttributeValue.js

@@ -21,6 +21,17 @@ export function getAttrValue(id) {
   })
 }
 
+/**
+ * 修改(合并)能源对象属性值:按 objCode+modelCode+attrKey 唯一定位
+ */
+export function updateAttrValue(data) {
+  return request({
+    url: '/ems/object/attr/value',
+    method: 'put',
+    data: data
+  })
+}
+
 // 新增能源对象属性
 export function addAttrValueBatch(data) {
   return request({

+ 0 - 85
ems-ui-cloud/src/api/mgr/elecStoreIndex.js

@@ -1,85 +0,0 @@
-import request from '@/utils/request'
-
-/**
- * 储能设施指标API - 对应 ElecStoreIndexController
- * 数据表: adm_ems_elec_store_index (10min实时充放电功率和容量信息)
- */
-
-/**
- * 查询储能设施指标列表(分页)
- * @param {Object} query - 查询参数
- * @param {string} query.areaCode - 园区代码
- * @param {string} query.facsCode - 设施代码
- * @param {string} query.deviceCode - 设备代码
- * @param {string} query.date - 日期 yyyy-MM-dd
- * @param {string} query.startRecTime - 开始时间
- * @param {string} query.endRecTime - 结束时间
- */
-export function listElecStoreIndex(query) {
-  return request({
-    url: '/ems/elec/store/index/list',
-    method: 'get',
-    params: query
-  })
-}
-
-/**
- * 查询储能设施指标列表(不分页)
- * @param {Object} query - 查询参数
- */
-export function listAllElecStoreIndex(query) {
-  return request({
-    url: '/ems/elec/store/index/listAll',
-    method: 'get',
-    params: query
-  })
-}
-
-/**
- * 查询每个设备最新一条记录
- * @param {Object} query - 查询参数
- * @param {string} query.areaCode - 园区代码(可选)
- * @param {string} query.facsCode - 设施代码(可选)
- */
-export function getLatestByDevice(query) {
-  return request({
-    url: '/ems/elec/store/index/latest',
-    method: 'get',
-    params: query
-  })
-}
-
-/**
- * 新增储能设施指标
- * @param {Object} data - 储能设施指标数据
- */
-export function addElecStoreIndex(data) {
-  return request({
-    url: '/ems/elec/store/index',
-    method: 'post',
-    data: data
-  })
-}
-
-/**
- * 修改储能设施指标
- * @param {Object} data - 储能设施指标数据
- */
-export function updateElecStoreIndex(data) {
-  return request({
-    url: '/ems/elec/store/index',
-    method: 'put',
-    data: data
-  })
-}
-
-/**
- * 删除储能设施指标
- * @param {Array} ids - 主键数组
- */
-export function delElecStoreIndex(ids) {
-  return request({
-    url: '/ems/elec/store/index/' + ids,
-    method: 'delete'
-  })
-}

+ 19 - 0
ems-ui-cloud/src/utils/ComponentHandle.js

@@ -5,6 +5,24 @@ import Vue from 'vue';
  * 组件处理对象,用于创建和管理Vue组件。
  */
 const ComponentHandle = {
+  // 已创建的组件实例(大屏3D气泡内的Vue实例,需随大屏销毁,否则轮询请求累积)
+  instances: [],
+  /**
+   * 销毁全部已创建组件实例
+   */
+  destroyAll() {
+    ComponentHandle.instances.forEach(vm => {
+      try {
+        vm.$destroy();
+        if (vm.$el && vm.$el.parentNode) {
+          vm.$el.parentNode.removeChild(vm.$el);
+        }
+      } catch (e) {
+        // ignore
+      }
+    });
+    ComponentHandle.instances = [];
+  },
   /**
    * 创建一个组件实例。
    *
@@ -46,6 +64,7 @@ const ComponentHandle = {
       },
     });
     comp = new Content({ store }).$mount();
+    ComponentHandle.instances.push(comp);
     return comp.$el;
   },
 };

+ 32 - 12
ems-ui-cloud/src/views/adapter/cdz/index.vue

@@ -545,7 +545,8 @@
               <el-form-item label="时间范围">
                 <el-date-picker v-model="callLogQuery.dateRange" type="datetimerange" 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>
               <el-form-item label="能力标识">
@@ -594,7 +595,8 @@
               <el-form-item label="时间范围">
                 <el-date-picker v-model="eventLogQuery.dateRange" type="datetimerange" 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>
               <el-form-item label="事件标识">
@@ -1016,17 +1018,21 @@ export default {
     },
     initDateRange() {
       const now = new Date()
-      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
-      const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      // 默认查询最近1小时,避免数据量大导致查询超时
+      const startOfDay = new Date(now.getTime() - 60 * 60 * 1000)
+      const endOfDay = new Date(now)
       this.callLogQuery.dateRange = [this.formatDate(startOfDay), this.formatDate(endOfDay)]
       this.eventLogQuery.dateRange = [this.formatDate(startOfDay), this.formatDate(endOfDay)]
     },
     formatDate(date) {
+      // 本地时间,格式化到分钟
       if (!date) return ''
-      const y = date.getFullYear(), m = (date.getMonth() + 1).toString().padStart(2, '0')
-      const d = date.getDate().toString().padStart(2, '0'), h = date.getHours().toString().padStart(2, '0')
-      const mi = date.getMinutes().toString().padStart(2, '0'), s = date.getSeconds().toString().padStart(2, '0')
-      return `${y}-${m}-${d} ${h}:${mi}:${s}`
+      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}`
     },
     handleEdit() {
       this.editForm = { ...this.systemInfo }
@@ -1548,7 +1554,7 @@ export default {
       this.logLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.callLogQuery.pageNum,
           pageSize: this.callLogQuery.pageSize
         }
@@ -1578,7 +1584,7 @@ export default {
       this.eventLogLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.eventLogQuery.pageNum,
           pageSize: this.eventLogQuery.pageSize
         }
@@ -1610,10 +1616,22 @@ export default {
       const map = { 0: 'success', 1: 'warning', 2: 'danger' }
       return map[status] || 'info'
     },
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       try {
         const res = await getCallLog(row.id)
-        this.callLogDetailData = res.data
+        const callData = res.data || {}
+        callData.callPayload = this.tryParseJson(callData.callPayload)
+        callData.resPayload = this.tryParseJson(callData.resPayload)
+        this.callLogDetailData = callData
         this.callLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取调用日志详情失败')
@@ -1622,7 +1640,9 @@ export default {
     async handleEventLogDetail(row) {
       try {
         const res = await getEventLog(row.id)
-        this.eventLogDetailData = res.data
+        const eventData = res.data || {}
+        eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+        this.eventLogDetailData = eventData
         this.eventLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取事件日志详情失败')

+ 30 - 13
ems-ui-cloud/src/views/adapter/devc/index.vue

@@ -327,7 +327,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>
               <el-form-item label="能力标识">
@@ -400,7 +401,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>
               <el-form-item label="事件标识">
@@ -1047,8 +1049,9 @@ export default {
     // 初始化时间范围
     initDateRange() {
       const now = new Date()
-      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
-      const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      // 默认查询最近1小时,避免数据量大导致查询超时
+      const startOfDay = new Date(now.getTime() - 60 * 60 * 1000)
+      const endOfDay = new Date(now)
 
       this.callLogQuery.dateRange = [
         this.formatDate(startOfDay),
@@ -1062,14 +1065,14 @@ export default {
 
     // 格式化日期
     formatDate(date) {
+      // 本地时间,格式化到分钟
       if (!date) return ''
-      const year = date.getFullYear()
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      const day = date.getDate().toString().padStart(2, '0')
-      const hours = date.getHours().toString().padStart(2, '0')
-      const minutes = date.getMinutes().toString().padStart(2, '0')
-      const seconds = date.getSeconds().toString().padStart(2, '0')
-      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+      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}`
     },
 
     // 加载系统信息
@@ -1625,10 +1628,22 @@ export default {
     },
 
     // 查看调用日志详情
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       try {
         const res = await getCallLog(row.id)
-        this.callLogDetailData = res.data
+        const callData = res.data || {}
+        callData.callPayload = this.tryParseJson(callData.callPayload)
+        callData.resPayload = this.tryParseJson(callData.resPayload)
+        this.callLogDetailData = callData
         this.callLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取调用日志详情失败')
@@ -1639,7 +1654,9 @@ export default {
     async handleEventLogDetail(row) {
       try {
         const res = await getEventLog(row.id)
-        this.eventLogDetailData = res.data
+        const eventData = res.data || {}
+        eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+        this.eventLogDetailData = eventData
         this.eventLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取事件日志详情失败')

+ 32 - 15
ems-ui-cloud/src/views/adapter/dljk/index.vue

@@ -1953,7 +1953,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>
               <el-form-item label="能力标识">
@@ -2011,7 +2012,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>
               <el-form-item label="事件标识">
@@ -3071,8 +3073,9 @@ export default {
     // 初始化时间范围
     initDateRange() {
       const now = new Date()
-      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
-      const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      // 默认查询最近1小时,避免数据量大导致查询超时
+      const startOfDay = new Date(now.getTime() - 60 * 60 * 1000)
+      const endOfDay = new Date(now)
 
       this.callLogQuery.dateRange = [
         this.formatDate(startOfDay),
@@ -3086,14 +3089,14 @@ export default {
 
     // 格式化日期
     formatDate(date) {
+      // 本地时间,格式化到分钟
       if (!date) return ''
-      const year = date.getFullYear()
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      const day = date.getDate().toString().padStart(2, '0')
-      const hours = date.getHours().toString().padStart(2, '0')
-      const minutes = date.getMinutes().toString().padStart(2, '0')
-      const seconds = date.getSeconds().toString().padStart(2, '0')
-      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+      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}`
     },
 
     // 查询调用日志
@@ -3101,7 +3104,7 @@ export default {
       this.logLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.callLogQuery.pageNum,
           pageSize: this.callLogQuery.pageSize
         }
@@ -3143,7 +3146,7 @@ export default {
       this.eventLogLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.eventLogQuery.pageNum,
           pageSize: this.eventLogQuery.pageSize
         }
@@ -3196,10 +3199,22 @@ export default {
     },
 
     // 查看调用日志详情
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       try {
         const res = await getCallLog(row.id)
-        this.callLogDetailData = res.data
+        const callData = res.data || {}
+        callData.callPayload = this.tryParseJson(callData.callPayload)
+        callData.resPayload = this.tryParseJson(callData.resPayload)
+        this.callLogDetailData = callData
         this.callLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取调用日志详情失败')
@@ -3210,7 +3225,9 @@ export default {
     async handleEventLogDetail(row) {
       try {
         const res = await getEventLog(row.id)
-        this.eventLogDetailData = res.data
+        const eventData = res.data || {}
+        eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+        this.eventLogDetailData = eventData
         this.eventLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取事件日志详情失败')

+ 446 - 77
ems-ui-cloud/src/views/adapter/gcc/index.vue

@@ -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 }

+ 32 - 15
ems-ui-cloud/src/views/adapter/gczr/index.vue

@@ -610,7 +610,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>
               <el-form-item label="能力标识">
@@ -669,7 +670,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>
               <el-form-item label="事件标识">
@@ -1049,8 +1051,9 @@ export default {
     // 初始化时间范围
     initDateRange() {
       const now = new Date()
-      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
-      const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      // 默认查询最近1小时,避免数据量大导致查询超时
+      const startOfDay = new Date(now.getTime() - 60 * 60 * 1000)
+      const endOfDay = new Date(now)
 
       this.callLogQuery.dateRange = [
         this.formatDate(startOfDay),
@@ -1064,14 +1067,14 @@ export default {
 
     // 格式化日期
     formatDate(date) {
+      // 本地时间,格式化到分钟
       if (!date) return ''
-      const year = date.getFullYear()
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      const day = date.getDate().toString().padStart(2, '0')
-      const hours = date.getHours().toString().padStart(2, '0')
-      const minutes = date.getMinutes().toString().padStart(2, '0')
-      const seconds = date.getSeconds().toString().padStart(2, '0')
-      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+      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}`
     },
 
     // 加载系统信息
@@ -1782,7 +1785,7 @@ export default {
       this.logLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.callLogQuery.pageNum,
           pageSize: this.callLogQuery.pageSize
         }
@@ -1824,7 +1827,7 @@ export default {
       this.eventLogLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.eventLogQuery.pageNum,
           pageSize: this.eventLogQuery.pageSize
         }
@@ -1877,10 +1880,22 @@ export default {
     },
 
     // 查看调用日志详情
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       try {
         const res = await getCallLog(row.id)
-        this.callLogDetailData = res.data
+        const callData = res.data || {}
+        callData.callPayload = this.tryParseJson(callData.callPayload)
+        callData.resPayload = this.tryParseJson(callData.resPayload)
+        this.callLogDetailData = callData
         this.callLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取调用日志详情失败')
@@ -1891,7 +1906,9 @@ export default {
     async handleEventLogDetail(row) {
       try {
         const res = await getEventLog(row.id)
-        this.eventLogDetailData = res.data
+        const eventData = res.data || {}
+        eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+        this.eventLogDetailData = eventData
         this.eventLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取事件日志详情失败')

+ 33 - 10
ems-ui-cloud/src/views/adapter/hm/index.vue

@@ -288,7 +288,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 +333,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>
@@ -730,15 +732,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()
@@ -749,7 +758,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
@@ -771,9 +780,21 @@ export default {
       this.callLogQuery.pageNum = 1
       this.queryCallLogs()
     },
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       const res = await getCallLog(row.id)
-      this.callLogDetailData = res.data
+      const callData = res.data || {}
+      callData.callPayload = this.tryParseJson(callData.callPayload)
+      callData.resPayload = this.tryParseJson(callData.resPayload)
+      this.callLogDetailData = callData
       this.callLogDetailDialog = true
     },
     // 查询事件日志
@@ -781,7 +802,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
@@ -805,7 +826,9 @@ export default {
     },
     async handleEventLogDetail(row) {
       const res = await getEventLog(row.id)
-      this.eventLogDetailData = res.data
+      const eventData = res.data || {}
+      eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+      this.eventLogDetailData = eventData
       this.eventLogDetailDialog = true
     },
 

+ 33 - 10
ems-ui-cloud/src/views/adapter/ljcy/index.vue

@@ -288,7 +288,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 +333,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>
@@ -738,15 +740,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()
@@ -757,7 +766,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
@@ -779,9 +788,21 @@ export default {
       this.callLogQuery.pageNum = 1
       this.queryCallLogs()
     },
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       const res = await getCallLog(row.id)
-      this.callLogDetailData = res.data
+      const callData = res.data || {}
+      callData.callPayload = this.tryParseJson(callData.callPayload)
+      callData.resPayload = this.tryParseJson(callData.resPayload)
+      this.callLogDetailData = callData
       this.callLogDetailDialog = true
     },
     // 查询事件日志
@@ -789,7 +810,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
@@ -813,7 +834,9 @@ export default {
     },
     async handleEventLogDetail(row) {
       const res = await getEventLog(row.id)
-      this.eventLogDetailData = res.data
+      const eventData = res.data || {}
+      eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+      this.eventLogDetailData = eventData
       this.eventLogDetailDialog = true
     },
 

+ 32 - 15
ems-ui-cloud/src/views/adapter/nhjc/index.vue

@@ -337,7 +337,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>
               <el-form-item label="能力标识">
@@ -410,7 +411,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>
               <el-form-item label="事件标识">
@@ -1059,8 +1061,9 @@ export default {
     // 初始化时间范围
     initDateRange() {
       const now = new Date()
-      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
-      const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      // 默认查询最近1小时,避免数据量大导致查询超时
+      const startOfDay = new Date(now.getTime() - 60 * 60 * 1000)
+      const endOfDay = new Date(now)
 
       this.callLogQuery.dateRange = [
         this.formatDate(startOfDay),
@@ -1074,14 +1077,14 @@ export default {
 
     // 格式化日期
     formatDate(date) {
+      // 本地时间,格式化到分钟
       if (!date) return ''
-      const year = date.getFullYear()
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      const day = date.getDate().toString().padStart(2, '0')
-      const hours = date.getHours().toString().padStart(2, '0')
-      const minutes = date.getMinutes().toString().padStart(2, '0')
-      const seconds = date.getSeconds().toString().padStart(2, '0')
-      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+      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}`
     },
 
     // 加载系统信息
@@ -1539,7 +1542,7 @@ export default {
         const modelCodes = this.getQueryModelCodes(this.selectedLogModel)
 
         const params = {
-          modelCodes: modelCodes,
+          modelCodes: (modelCodes || []).join(','),
           pageNum: this.callLogQuery.pageNum,
           pageSize: this.callLogQuery.pageSize
         }
@@ -1585,7 +1588,7 @@ export default {
         const modelCodes = this.getQueryModelCodes(this.selectedEventLogModel)
 
         const params = {
-          modelCodes: modelCodes,
+          modelCodes: (modelCodes || []).join(','),
           pageNum: this.eventLogQuery.pageNum,
           pageSize: this.eventLogQuery.pageSize
         }
@@ -1639,10 +1642,22 @@ export default {
     },
 
     // 查看调用日志详情
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       try {
         const res = await getCallLog(row.id)
-        this.callLogDetailData = res.data
+        const callData = res.data || {}
+        callData.callPayload = this.tryParseJson(callData.callPayload)
+        callData.resPayload = this.tryParseJson(callData.resPayload)
+        this.callLogDetailData = callData
         this.callLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取调用日志详情失败')
@@ -1653,7 +1668,9 @@ export default {
     async handleEventLogDetail(row) {
       try {
         const res = await getEventLog(row.id)
-        this.eventLogDetailData = res.data
+        const eventData = res.data || {}
+        eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+        this.eventLogDetailData = eventData
         this.eventLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取事件日志详情失败')

+ 32 - 15
ems-ui-cloud/src/views/adapter/pv/index.vue

@@ -835,7 +835,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>
               <el-form-item label="能力标识">
@@ -894,7 +895,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>
               <el-form-item label="事件标识">
@@ -1274,8 +1276,9 @@ export default {
     // 初始化时间范围
     initDateRange() {
       const now = new Date()
-      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
-      const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      // 默认查询最近1小时,避免数据量大导致查询超时
+      const startOfDay = new Date(now.getTime() - 60 * 60 * 1000)
+      const endOfDay = new Date(now)
 
       this.callLogQuery.dateRange = [
         this.formatDate(startOfDay),
@@ -1289,14 +1292,14 @@ export default {
 
     // 格式化日期
     formatDate(date) {
+      // 本地时间,格式化到分钟
       if (!date) return ''
-      const year = date.getFullYear()
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      const day = date.getDate().toString().padStart(2, '0')
-      const hours = date.getHours().toString().padStart(2, '0')
-      const minutes = date.getMinutes().toString().padStart(2, '0')
-      const seconds = date.getSeconds().toString().padStart(2, '0')
-      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+      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}`
     },
 
     // 加载系统信息
@@ -2007,7 +2010,7 @@ export default {
       this.logLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.callLogQuery.pageNum,
           pageSize: this.callLogQuery.pageSize
         }
@@ -2049,7 +2052,7 @@ export default {
       this.eventLogLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.eventLogQuery.pageNum,
           pageSize: this.eventLogQuery.pageSize
         }
@@ -2102,10 +2105,22 @@ export default {
     },
 
     // 查看调用日志详情
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       try {
         const res = await getCallLog(row.id)
-        this.callLogDetailData = res.data
+        const callData = res.data || {}
+        callData.callPayload = this.tryParseJson(callData.callPayload)
+        callData.resPayload = this.tryParseJson(callData.resPayload)
+        this.callLogDetailData = callData
         this.callLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取调用日志详情失败')
@@ -2116,7 +2131,9 @@ export default {
     async handleEventLogDetail(row) {
       try {
         const res = await getEventLog(row.id)
-        this.eventLogDetailData = res.data
+        const eventData = res.data || {}
+        eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+        this.eventLogDetailData = eventData
         this.eventLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取事件日志详情失败')

+ 32 - 15
ems-ui-cloud/src/views/adapter/zm/index.vue

@@ -856,7 +856,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>
               <el-form-item label="能力标识">
@@ -915,7 +916,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>
               <el-form-item label="事件标识">
@@ -1771,8 +1773,9 @@ export default {
     // 初始化时间范围(默认今天)
     initDateRange() {
       const now = new Date()
-      const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
-      const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      // 默认查询最近1小时,避免数据量大导致查询超时
+      const startOfDay = new Date(now.getTime() - 60 * 60 * 1000)
+      const endOfDay = new Date(now)
 
       this.callLogQuery.dateRange = [
         this.formatDate(startOfDay),
@@ -1786,14 +1789,14 @@ export default {
 
     // 格式化日期
     formatDate(date) {
+      // 本地时间,格式化到分钟
       if (!date) return ''
-      const year = date.getFullYear()
-      const month = (date.getMonth() + 1).toString().padStart(2, '0')
-      const day = date.getDate().toString().padStart(2, '0')
-      const hours = date.getHours().toString().padStart(2, '0')
-      const minutes = date.getMinutes().toString().padStart(2, '0')
-      const seconds = date.getSeconds().toString().padStart(2, '0')
-      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+      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}`
     },
 
     // 加载系统信息
@@ -2477,7 +2480,7 @@ export default {
       this.logLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.callLogQuery.pageNum,
           pageSize: this.callLogQuery.pageSize
         }
@@ -2519,7 +2522,7 @@ export default {
       this.eventLogLoading = true
       try {
         const params = {
-          modelCodes: this.allModelCodes,
+          modelCodes: (this.allModelCodes || []).join(','),
           pageNum: this.eventLogQuery.pageNum,
           pageSize: this.eventLogQuery.pageSize
         }
@@ -2572,10 +2575,22 @@ export default {
     },
 
     // 查看调用日志详情
+    // JSON 字符串解析为对象(解析失败保持原样),用于日志详情结构化展示
+    tryParseJson(value) {
+      if (!value || typeof value !== 'string') return value
+      try {
+        return JSON.parse(value)
+      } catch (e) {
+        return value
+      }
+    },
     async handleCallLogDetail(row) {
       try {
         const res = await getCallLog(row.id)
-        this.callLogDetailData = res.data
+        const callData = res.data || {}
+        callData.callPayload = this.tryParseJson(callData.callPayload)
+        callData.resPayload = this.tryParseJson(callData.resPayload)
+        this.callLogDetailData = callData
         this.callLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取调用日志详情失败')
@@ -2586,7 +2601,9 @@ export default {
     async handleEventLogDetail(row) {
       try {
         const res = await getEventLog(row.id)
-        this.eventLogDetailData = res.data
+        const eventData = res.data || {}
+        eventData.eventDetail = this.tryParseJson(eventData.eventDetail)
+        this.eventLogDetailData = eventData
         this.eventLogDetailDialog = true
       } catch (error) {
         this.$message.error('获取事件日志详情失败')

+ 2 - 267
ems-ui-cloud/src/views/analysis/power/store.vue

@@ -30,7 +30,6 @@
             <div class="header-actions">
               <el-radio-group v-model="activeTab" size="small" @change="handleTabChange">
                 <el-radio-button label="charge"><i class="el-icon-s-data"></i> 充放电分析</el-radio-button>
-                <el-radio-button label="capacity"><i class="el-icon-pie-chart"></i> 容量分析</el-radio-button>
               </el-radio-group>
             </div>
           </div>
@@ -161,67 +160,6 @@
               </div>
             </div>
           </div>
-          <!-- 容量分析Tab -->
-          <div v-show="activeTab === 'capacity'" class="tab-content">
-            <div class="filter-bar">
-              <div class="filter-left"><span class="filter-label">数据范围:</span></div>
-              <div class="filter-right">
-                <el-date-picker v-model="capacityAnalysis.dateRange" type="datetimerange" range-separator="至"
-                                start-placeholder="开始时间" end-placeholder="结束时间"
-                                value-format="yyyy-MM-dd HH:mm:ss" size="small" @change="loadCapacityAnalysisData"
-                                :picker-options="datetimePickerOptions" class="datetime-picker"
-                />
-                <el-select v-model="capacityAnalysis.selectedDevice" placeholder="选择设备" size="small" clearable
-                           @change="loadCapacityAnalysisData" class="device-select"
-                >
-                  <el-option label="全部设备" value=""/>
-                  <el-option v-for="device in deviceList" :key="device.deviceCode"
-                             :label="device.deviceName || device.deviceCode" :value="device.deviceCode"
-                  />
-                </el-select>
-              </div>
-            </div>
-            <div class="summary-cards" v-loading="capacityAnalysis.loading">
-              <el-row :gutter="16">
-                <el-col :span="8">
-                  <div class="stat-card avg-card">
-                    <div class="card-icon"><i class="el-icon-data-analysis"></i></div>
-                    <div class="card-content"><span class="card-label">平均容量</span><span class="card-value"
-                    >{{ formatNumber(capacityAnalysis.summary.avgCapacity) }}<small>kW·h</small></span></div>
-                  </div>
-                </el-col>
-                <el-col :span="8">
-                  <div class="stat-card max-card">
-                    <div class="card-icon"><i class="el-icon-top-right"></i></div>
-                    <div class="card-content"><span class="card-label">最大容量</span><span class="card-value"
-                    >{{ formatNumber(capacityAnalysis.summary.maxCapacity) }}<small>kW·h</small></span></div>
-                  </div>
-                </el-col>
-                <el-col :span="8">
-                  <div class="stat-card min-card">
-                    <div class="card-icon"><i class="el-icon-bottom-right"></i></div>
-                    <div class="card-content"><span class="card-label">最小容量</span><span class="card-value"
-                    >{{ formatNumber(capacityAnalysis.summary.minCapacity) }}<small>kW·h</small></span></div>
-                  </div>
-                </el-col>
-
-              </el-row>
-            </div>
-            <div class="chart-section">
-              <div class="section-header"><h3 class="section-title"><i class="el-icon-data-line"></i>容量变化趋势</h3>
-              </div>
-              <div class="chart-wrapper" v-loading="capacityAnalysis.loading">
-                <div ref="capacityChartRef" class="chart-canvas capacity-chart"></div>
-              </div>
-            </div>
-            <div class="chart-section">
-              <div class="section-header"><h3 class="section-title"><i class="el-icon-pie-chart"></i>设备容量分布</h3>
-              </div>
-              <div class="chart-wrapper" v-loading="capacityAnalysis.loading">
-                <div ref="capacityDistributionChartRef" class="chart-canvas"></div>
-              </div>
-            </div>
-          </div>
         </div>
       </el-col>
     </el-row>
@@ -229,9 +167,7 @@
 </template>
 
 <script>
-import { listAllElecStoreIndex } from '@/api/mgr/elecStoreIndex'
 import { listElecStoreHour, listElecStoreDay, getDayStatistics } from '@/api/mgr/elecStoreHour'
-import { getByCondition } from '@/api/device/device'
 import { areaTreeByFacsCategory } from '@/api/basecfg/area'
 import { listAllFacs } from '@/api/basecfg/emsfacs'
 import * as echarts from 'echarts'
@@ -254,8 +190,6 @@ export default {
       areaMap: {},
       facsMap: {},
       facsList: [],  // 新增:设施列表
-      deviceList: [],
-      deviceMap: {},
       chargeAnalysis: {
         timeDimension: 'day',
         selectedDate: this.formatDate(today),
@@ -270,17 +204,8 @@ export default {
         chartData: [],
         selectedFacs: ''  // 新增:选中的设施
       },
-      capacityAnalysis: {
-        dateRange: [this.formatDateTime(new Date(today.getTime() - 24 * 60 * 60 * 1000)), this.formatDateTime(today)],
-        selectedDevice: '',
-        loading: false,
-        summary: { avgCapacity: 0, maxCapacity: 0, minCapacity: 0, recordCount: 0 },
-        chartData: []
-      },
       chargeChartInstance: null,
       chargeDetailChartInstance: null,
-      capacityChartInstance: null,
-      capacityDistributionChartInstance: null,
       datePickerOptions: {
         shortcuts: [{
           text: '最近一周', onClick(picker) {
@@ -353,24 +278,12 @@ export default {
   methods: {
     async initBaseData() {
       try {
-        await Promise.all([this.loadTreeData(), this.loadDeviceList(), this.loadFacsList()])
+        await Promise.all([this.loadTreeData(), this.loadFacsList()])
         this.loadChargeAnalysisData()
       } catch (error) {
         console.error('初始化数据失败', error)
       }
     },
-    async loadDeviceList() {
-      try {
-        const { data } = await getByCondition({ subsystemCode: 'SYS_GCC' })
-        this.deviceList = data || []
-        this.deviceMap = {}
-        this.deviceList.forEach(device => {
-          this.deviceMap[device.deviceCode] = device
-        })
-      } catch (error) {
-        console.error('加载设备列表失败', error)
-      }
-    },
     async loadFacsList() {
       try {
         const { data } = await listAllFacs({ facsCategory: 'C' })
@@ -394,11 +307,6 @@ export default {
       const d = new Date(date)
       return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
     },
-    formatDateTime(date) {
-      if (!date) return ''
-      const d = new Date(date)
-      return `${this.formatDate(d)} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
-    },
     formatTime(time) {
       if (!time) return '-'
       if (typeof time === 'string') {
@@ -417,11 +325,6 @@ export default {
     getFacsName(facsCode) {
       return !facsCode ? '-' : (this.facsMap[facsCode] || facsCode)
     },
-    getDeviceName(deviceCode) {
-      if (!deviceCode) return '-'
-      const device = this.deviceMap[deviceCode]
-      return device ? (device.deviceName || deviceCode) : deviceCode
-    },
     async loadTreeData() {
       try {
         const response = await areaTreeByFacsCategory('C', '', false)
@@ -490,8 +393,6 @@ export default {
     refreshCurrentTabData() {
       if (this.activeTab === 'charge') {
         this.loadChargeAnalysisData()
-      } else {
-        this.loadCapacityAnalysisData()
       }
     },
     handleChargeTimeChange() {
@@ -731,184 +632,18 @@ export default {
       })
       return sums
     },
-    async loadCapacityAnalysisData() {
-      this.capacityAnalysis.loading = true
-      try {
-        const [startTime, endTime] = this.capacityAnalysis.dateRange || []
-        if (!startTime || !endTime) return
-        const params = {
-          startRecTime: startTime,
-          endRecTime: endTime,
-          areaCode: this.queryParams.areaCode || undefined,
-          facsCode: this.queryParams.facsCode || undefined,
-          deviceCode: this.capacityAnalysis.selectedDevice || undefined
-        }
-        const { data } = await listAllElecStoreIndex(params)
-        const list = data || []
-        this.capacityAnalysis.chartData = list.map(item => ({
-          ...item,
-          deviceName: this.getDeviceName(item.deviceCode)
-        }))
-        if (list.length > 0) {
-          const capacities = list.map(item => parseFloat(item.currentCapacity) || 0)
-          this.capacityAnalysis.summary = {
-            avgCapacity: capacities.reduce((a, b) => a + b, 0) / capacities.length,
-            maxCapacity: Math.max(...capacities),
-            minCapacity: Math.min(...capacities),
-            recordCount: list.length
-          }
-        } else {
-          this.capacityAnalysis.summary = { avgCapacity: 0, maxCapacity: 0, minCapacity: 0, recordCount: 0 }
-        }
-        this.$nextTick(() => {
-          this.renderCapacityChart()
-          this.renderCapacityDistributionChart()
-        })
-      } catch (error) {
-        console.error('加载容量分析数据失败', error)
-        this.$message.error('加载数据失败')
-      } finally {
-        this.capacityAnalysis.loading = false
-      }
-    },
-    renderCapacityChart() {
-      if (!this.$refs.capacityChartRef || this.capacityAnalysis.chartData.length === 0) return
-      if (this.capacityChartInstance) {
-        this.capacityChartInstance.dispose()
-      }
-      this.capacityChartInstance = echarts.init(this.$refs.capacityChartRef)
-      const deviceGroups = new Map()
-      this.capacityAnalysis.chartData.forEach(item => {
-        if (!deviceGroups.has(item.deviceCode)) {
-          deviceGroups.set(item.deviceCode, [])
-        }
-        deviceGroups.get(item.deviceCode).push(item)
-      })
-      const allTimes = [...new Set(this.capacityAnalysis.chartData.map(item => item.time))].sort()
-      const series = []
-      const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#00d4ff', '#ff6b6b', '#4ecdc4']
-      let colorIndex = 0
-      deviceGroups.forEach((items, deviceCode) => {
-        const dataMap = new Map(items.map(item => [item.time, parseFloat(item.currentCapacity) || 0]))
-        const seriesData = allTimes.map(time => dataMap.get(time) || null)
-        const deviceName = items[0].deviceName || deviceCode
-        series.push({
-          name: deviceName,
-          type: 'line',
-          data: seriesData,
-          smooth: true,
-          symbol: 'circle',
-          symbolSize: 4,
-          connectNulls: true,
-          lineStyle: { width: 2, color: colors[colorIndex % colors.length] },
-          itemStyle: { color: colors[colorIndex % colors.length] }
-        })
-        colorIndex++
-      })
-      const option = {
-        tooltip: {
-          trigger: 'axis',
-          backgroundColor: 'rgba(50, 50, 50, 0.9)',
-          borderColor: 'transparent',
-          textStyle: { color: '#fff' }
-        },
-        legend: { data: series.map(s => s.name), top: 10, type: 'scroll' },
-        grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
-        xAxis: {
-          type: 'category',
-          data: allTimes.map(t => this.formatTime(t)),
-          axisLine: { lineStyle: { color: '#e0e0e0' } },
-          axisTick: { show: false },
-          axisLabel: { color: '#666', fontSize: 10, rotate: 45 }
-        },
-        yAxis: {
-          type: 'value',
-          name: 'kW·h',
-          nameTextStyle: { color: '#999', fontSize: 11 },
-          axisLine: { show: false },
-          axisTick: { show: false },
-          splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } }
-        },
-        dataZoom: allTimes.length > 20 ? [{ type: 'slider', start: 0, end: 80, height: 20, bottom: '2%' }] : [],
-        series
-      }
-      this.capacityChartInstance.setOption(option)
-    },
-    renderCapacityDistributionChart() {
-      if (!this.$refs.capacityDistributionChartRef || this.capacityAnalysis.chartData.length === 0) return
-      if (this.capacityDistributionChartInstance) {
-        this.capacityDistributionChartInstance.dispose()
-      }
-      this.capacityDistributionChartInstance = echarts.init(this.$refs.capacityDistributionChartRef)
-      const deviceCapacity = new Map()
-      this.capacityAnalysis.chartData.forEach(item => {
-        const existing = deviceCapacity.get(item.deviceCode)
-        if (!existing || new Date(item.time) > new Date(existing.time)) {
-          deviceCapacity.set(item.deviceCode, {
-            name: item.deviceName || item.deviceCode,
-            value: parseFloat(item.currentCapacity) || 0,
-            time: item.time
-          })
-        }
-      })
-      const pieData = Array.from(deviceCapacity.values())
-      const total = pieData.reduce((sum, item) => sum + item.value, 0)
-      const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#00d4ff', '#ff6b6b', '#4ecdc4', '#f39c12', '#9b59b6']
-      const option = {
-        tooltip: {
-          trigger: 'item',
-          backgroundColor: 'rgba(50, 50, 50, 0.9)',
-          borderColor: 'transparent',
-          textStyle: { color: '#fff' },
-          formatter: (params) => {
-            const percent = total > 0 ? ((params.value / total) * 100).toFixed(1) : 0
-            return `<div style="padding: 8px;"><div style="margin-bottom: 6px;">${params.marker} ${params.name}</div><div style="font-size: 14px; font-weight: bold;">${this.formatNumber(params.value)} kW·h</div><div style="color: #aaa; font-size: 12px;">占比: ${percent}%</div></div>`
-          }
-        },
-        legend: {
-          type: 'scroll',
-          orient: 'vertical',
-          right: '5%',
-          top: 'middle',
-          textStyle: { color: '#666', fontSize: 12 }
-        },
-        series: [{
-          name: '容量分布',
-          type: 'pie',
-          radius: ['40%', '70%'],
-          center: ['40%', '50%'],
-          avoidLabelOverlap: true,
-          itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
-          label: { show: true, formatter: '{b}: {d}%', fontSize: 11 },
-          emphasis: {
-            label: { show: true, fontSize: 14, fontWeight: 'bold' },
-            itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.3)' }
-          },
-          data: pieData.map((item, index) => ({
-            name: item.name,
-            value: item.value,
-            itemStyle: { color: colors[index % colors.length] }
-          }))
-        }]
-      }
-      this.capacityDistributionChartInstance.setOption(option)
-    },
     handleResize() {
       if (this.chargeChartInstance) this.chargeChartInstance.resize()
       if (this.chargeDetailChartInstance) this.chargeDetailChartInstance.resize()
-      if (this.capacityChartInstance) this.capacityChartInstance.resize()
-      if (this.capacityDistributionChartInstance) this.capacityDistributionChartInstance.resize()
     },
     disposeCharts() {
-      [this.chargeChartInstance, this.chargeDetailChartInstance, this.capacityChartInstance, this.capacityDistributionChartInstance].forEach(chart => {
+      [this.chargeChartInstance, this.chargeDetailChartInstance].forEach(chart => {
         if (chart) {
           chart.dispose()
         }
       })
       this.chargeChartInstance = null
       this.chargeDetailChartInstance = null
-      this.capacityChartInstance = null
-      this.capacityDistributionChartInstance = null
     }
   }
 }

+ 8 - 0
ems-ui-cloud/src/views/largeScreen/center.vue

@@ -45,6 +45,14 @@ export default {
   mounted () {
     this.initThree();
   },
+  beforeDestroy () {
+    // 释放three.js资源并销毁气泡内Vue实例,避免重复打开大屏导致资源累积卡顿
+    if (this.modelApi) {
+      this.modelApi.dispose();
+      this.modelApi = null;
+    }
+    ComponentHandle.destroyAll();
+  },
   methods: {
     ...mapMutations('userState', ['setAreaType']),
     roomClick (value) {

+ 61 - 19
ems-ui-cloud/src/views/largeScreen/dialog/storage-real.vue

@@ -14,7 +14,9 @@
 
 <script>
 import threejsBaseDialog from './base.vue'
-import { getLatestByDevice } from "@/api/mgr/elecStoreIndex";
+import { getByCondition } from "@/api/device/device";
+import { getObjAttrBatch } from "@/api/basecfg/objAttribute";
+import { getStoreHByDataRange } from "@/api/mgr/elecStoreHour";
 import { numToStr } from "@/utils";
 
 let timer = null;
@@ -40,14 +42,14 @@ export default {
           unit: "kW·h",
         },
         {
-          name: "充电功率",
+          name: "当日充电量",
           value: 0,
-          unit: "kW",
+          unit: "kW·h",
         },
         {
-          name: "放电功率",
+          name: "当日放电量",
           value: 0,
-          unit: "kW",
+          unit: "kW·h",
         },
       ],
     };
@@ -65,19 +67,24 @@ export default {
     },
     async getStorageData() {
       try {
-        const params = {};
+        // 从储能一体机物模型属性值读取(替代adm_ems_elec_store_index)
+        const params = {
+          subsystemCode: 'SYS_GCC',
+          deviceModel: 'M_W4_DEV_GCC_YTJ'
+        };
 
         if (this.propData.areaCode && this.propData.areaCode !== '-1') {
           params.areaCode = this.propData.areaCode;
         }
 
         if (this.propData.facsCode) {
-          params.facsCode = this.propData.facsCode;
+          params.refFacs = this.propData.facsCode;
         }
 
-        const { data } = await getLatestByDevice(params);
+        const { data: devices } = await getByCondition(params);
+        const deviceList = devices || [];
 
-        if (!data || !Array.isArray(data) || data.length === 0) {
+        if (deviceList.length === 0) {
           const [capacity, charge, discharge] = this.storageList;
           capacity.value = 0;
           charge.value = 0;
@@ -86,27 +93,62 @@ export default {
           return;
         }
 
-        // 汇总所有设备的数据
-        let totalCapacity = 0;      // 当前容量总和(单位:kW·h)
-        let totalChargePower = 0;   // 充电功率总和(单位:W -> kW)
-        let totalDischargePower = 0; // 放电功率总和(单位:W -> kW)
+        const attrRes = await getObjAttrBatch(2, 'M_W4_DEV_GCC_YTJ');
+        const attrMap = attrRes.data || {};
 
-        data.forEach(device => {
-          totalCapacity += device.currentCapacity || 0;
-          totalChargePower += (device.chargeVoltage || 0) / 1000; // W转kW
-          totalDischargePower += (device.dischargePower || 0) / 1000; // W转kW
+        // 当前容量总和(额定*SOC*SOH)
+        let totalCapacity = 0;
+        deviceList.forEach(device => {
+          const attrs = attrMap[device.deviceCode] || {};
+          const num = (key) => {
+            const v = parseFloat(this.findAttrValue(attrs, key));
+            return isNaN(v) ? 0 : v;
+          };
+          totalCapacity += num('ratedCapacity') * num('bmsSoc') / 100 * num('bmsSoh') / 100;
         });
 
+        // 当日充放电量:小时计量汇总表(adm_ems_elec_store_h)今日增量
+        let dayCharge = 0;
+        let dayDischarge = 0;
+        const rangeParams = { startRecTime: this.formatToday() };
+        if (this.propData.areaCode && this.propData.areaCode !== '-1') {
+          rangeParams.areaCode = this.propData.areaCode;
+        }
+        const { data: dayData } = await getStoreHByDataRange(rangeParams);
+        if (dayData) {
+          dayCharge = parseFloat(dayData.chargeElecQuantity) || 0;
+          dayDischarge = parseFloat(dayData.dischargeElecQuantity) || 0;
+        }
+
         const [capacity, charge, discharge] = this.storageList;
         capacity.value = numToStr(totalCapacity.toFixed(2));
-        charge.value = numToStr(totalChargePower.toFixed(2));
-        discharge.value = numToStr(totalDischargePower.toFixed(2));
+        charge.value = numToStr(dayCharge.toFixed(2));
+        discharge.value = numToStr(dayDischarge.toFixed(2));
         this.storageList = [capacity, charge, discharge];
 
       } catch (error) {
         console.error('获取储能数据失败:', error);
       }
     },
+    // 今日日期 yyyy-MM-dd
+    formatToday() {
+      const d = new Date();
+      const m = String(d.getMonth() + 1).padStart(2, '0');
+      const day = String(d.getDate()).padStart(2, '0');
+      return `${d.getFullYear()}-${m}-${day}`;
+    },
+    // 从设备属性包(按分组)中取指定属性值
+    findAttrValue(attrObj, attrKey) {
+      if (!attrObj) return null;
+      for (const group in attrObj) {
+        const list = attrObj[group];
+        if (Array.isArray(list)) {
+          const target = list.find(a => a.attrKey === attrKey);
+          if (target) return target.attrValue;
+        }
+      }
+      return null;
+    },
   },
 };
 </script>

+ 70 - 27
ems-ui-cloud/src/views/largeScreen/home/left.vue

@@ -23,7 +23,7 @@
           <span class="value">{{ currentCapacity }}</span><span class="unit">kW·h</span>
         </div>
         <div class="charge-status">
-          当前容量
+          当前存电容量({{ capacityPercent }}%)
         </div>
       </div>
       <div class="storage-grid">
@@ -51,8 +51,8 @@ import {mapState} from 'vuex';
 import {numToStr} from "@/utils";
 import {DateTool} from "@/utils/DateTool";
 import { listFacsMeterHourSta } from '@/api/device/energyConsumption'
-import { getStoreHByDataRange } from "@/api/mgr/elecStoreHour";
-import { getLatestByDevice } from "@/api/mgr/elecStoreIndex";
+import { getByCondition } from "@/api/device/device";
+import { getObjAttrBatch } from "@/api/basecfg/objAttribute";
 import { getPvSupplySummary } from '@/api/mgr/pvSupply'
 
 export default {
@@ -88,8 +88,10 @@ export default {
       buildingType: '1',
       buildingTabs: [{name: '按建筑', value: '1'}, {name: '按设施', value: '2'}],
       buildingData: [],
-      // 当前容量(所有设备current_capacity之和
+      // 当前存电容量(Σ额定*SOC*SOH
       currentCapacity: '0.00',
+      // 存电占比(当前存电容量/总实际容量)
+      capacityPercent: '0.00',
       storageData: [
         {
           name: "今日充电",
@@ -329,7 +331,7 @@ export default {
 
     /**
      * 获取储能数据
-     * - 当前容量:从 elecStoreIndex 获取各设备最新记录的 currentCapacity 之和
+     * - 当前容量:从储能一体机物模型属性值计算(额定*SOC*SOH)之和
      * - 今日充电/放电:从 elecStoreH 获取今日汇总
      * - 累计充电/放电:从 elecStoreH 获取历史累计汇总
      */
@@ -338,34 +340,62 @@ export default {
         // 构建区域查询参数
         const areaParam = this.areaType && this.areaType !== '-1' ? { areaCode: this.areaType } : {}
 
-        // 1. 获取当前容量 - 查询各设备最新记录
-        const { data: latestList = [] } = await getLatestByDevice(areaParam)
+        // 1. 设备列表(一体机 + 储能电表)
+        const { data: ytjDevices = [] } = await getByCondition({
+          ...areaParam,
+          subsystemCode: 'SYS_GCC',
+          deviceModel: 'M_W4_DEV_GCC_YTJ'
+        })
+        const { data: dbDevices = [] } = await getByCondition({
+          ...areaParam,
+          subsystemCode: 'SYS_GCC',
+          deviceModel: 'M_W4_DEV_GCC_DB'
+        })
 
-        // 计算所有设备的当前容量之和
-        let totalCurrentCapacity = 0
-        if (latestList && latestList.length > 0) {
-          totalCurrentCapacity = latestList.reduce((sum, item) => {
-            return sum + (item.currentCapacity || 0)
-          }, 0)
+        // 2. 物模型属性值
+        const ytjAttrMap = (await getObjAttrBatch(2, 'M_W4_DEV_GCC_YTJ')).data || {}
+        const dbAttrMap = (await getObjAttrBatch(2, 'M_W4_DEV_GCC_DB')).data || {}
+        const num = (attrs, key) => {
+          const v = parseFloat(this.findStoreAttrValue(attrs, key))
+          return isNaN(v) ? 0 : v
         }
-        this.currentCapacity = numToStr(totalCurrentCapacity)
 
-        // 2. 获取今日充放电数据
-        const todayDate = DateTool.now(DateTool.DateFormat.YYYY_MM_DD)
-        const { data: todayData = {} } = await getStoreHByDataRange({
-          ...areaParam,
-          startRecTime: todayDate
+        // 3. 当前存电容量(Σ额定*SOC*SOH)、总实际容量(Σ额定*SOH)与累计充放电(BMS累计值)- 一体机汇总
+        let totalCurrentCapacity = 0
+        let totalUsableCapacity = 0
+        let totalCumCharge = 0
+        let totalCumDischarge = 0
+        ;(ytjDevices || []).forEach(device => {
+          const attrs = ytjAttrMap[device.deviceCode] || {}
+          const rated = num(attrs, 'ratedCapacity')
+          const soc = num(attrs, 'bmsSoc')
+          const soh = num(attrs, 'bmsSoh')
+          totalCurrentCapacity += rated * soc / 100 * soh / 100
+          totalUsableCapacity += rated * soh / 100
+          totalCumCharge += num(attrs, 'bmsCumChargeKwh')
+          totalCumDischarge += num(attrs, 'bmsCumDischargeKwh')
+        })
+        this.currentCapacity = numToStr(totalCurrentCapacity)
+        // 总百分比 = 当前存电容量 / 总实际容量
+        this.capacityPercent = totalUsableCapacity > 0
+          ? (totalCurrentCapacity / totalUsableCapacity * 100).toFixed(2)
+          : '0.00'
+
+        // 4. 今日充放电(电表当日正向/反向电能,adapter按当日零点基线推算)- 电表汇总
+        let dayCharge = 0
+        let dayDischarge = 0
+        ;(dbDevices || []).forEach(device => {
+          const attrs = dbAttrMap[device.deviceCode] || {}
+          dayCharge += num(attrs, 'dayFwdEnergy')
+          dayDischarge += num(attrs, 'dayRevEnergy')
         })
 
-        // 3. 获取累计充放电数据(不传日期参数,查询所有历史数据)
-        const { data: totalData = {} } = await getStoreHByDataRange(areaParam)
-
-        // 4. 更新储能数据
+        // 5. 更新储能数据
         const [todayCharge, todayDischarge, totalCharge, totalDischarge] = this.storageData
-        todayCharge.value = numToStr(todayData.chargeElecQuantity || 0)
-        todayDischarge.value = numToStr(todayData.dischargeElecQuantity || 0)
-        totalCharge.value = numToStr(totalData.chargeElecQuantity || 0)
-        totalDischarge.value = numToStr(totalData.dischargeElecQuantity || 0)
+        todayCharge.value = numToStr(dayCharge)
+        todayDischarge.value = numToStr(dayDischarge)
+        totalCharge.value = numToStr(totalCumCharge)
+        totalDischarge.value = numToStr(totalCumDischarge)
 
         this.storageData = [todayCharge, todayDischarge, totalCharge, totalDischarge]
 
@@ -373,6 +403,19 @@ export default {
         console.error('获取储能数据失败:', error)
       }
     },
+
+    // 从设备属性包(按分组)中取指定属性值
+    findStoreAttrValue(attrObj, attrKey) {
+      if (!attrObj) return null
+      for (const group in attrObj) {
+        const list = attrObj[group]
+        if (Array.isArray(list)) {
+          const target = list.find(a => a.attrKey === attrKey)
+          if (target) return target.attrValue
+        }
+      }
+      return null
+    },
   }
 }
 </script>

+ 6 - 0
ems-ui-cloud/src/views/largeScreen/index.vue

@@ -94,6 +94,12 @@ export default {
   beforeDestroy() {
     clearInterval(this.timer)
     this.timer = null
+    // 卸载自适应缩放,避免重复进入大屏累积resize监听
+    try {
+      autofit.off()
+    } catch (e) {
+      // ignore
+    }
   },
   methods: {
     ...mapMutations('userState', ["setRegionFactor", "setElec2CFactor"]),

+ 8 - 0
ems-ui-cloud/src/views/largeScreen/pv-road/center.vue

@@ -34,6 +34,14 @@ export default {
     this.setAreaType(AREA_CODE)
     this.initThree();
   },
+  beforeDestroy () {
+    // 释放three.js资源并销毁气泡内Vue实例,避免重复打开导致资源累积卡顿
+    if (this.modelApi) {
+      this.modelApi.dispose();
+      this.modelApi = null;
+    }
+    ComponentHandle.destroyAll();
+  },
   methods: {
     ...mapMutations('userState', ['setAreaType']),
 

+ 65 - 3
ems-ui-cloud/src/views/largeScreen/three/renderModel.js

@@ -149,11 +149,13 @@ class renderModel {
     this.setFBXModel(loaded);
     this.setRoomModel();
     //监听场景大小改变,跳转渲染尺寸
-    window.addEventListener('resize', this.onWindowResizes.bind(this));
+    this._boundResize = this.onWindowResizes.bind(this);
+    window.addEventListener('resize', this._boundResize);
     //场景渲染
     this.sceneAnimation();
     // 添加鼠标点击事件
-    window.addEventListener('click', this.onMouseClick.bind(this));
+    this._boundClick = this.onMouseClick.bind(this);
+    window.addEventListener('click', this._boundClick);
   }
 
   initGltfModel(loaded) {
@@ -173,7 +175,8 @@ class renderModel {
     //场景渲染
     this.sceneAnimation();
     // 添加鼠标点击事件
-    window.addEventListener('click', this.onMouseClick.bind(this));
+    this._boundClick = this._boundClick || this.onMouseClick.bind(this);
+    window.addEventListener('click', this._boundClick);
   }
 
   onMouseClick(event) {
@@ -305,6 +308,65 @@ class renderModel {
   sceneAnimation() {
     this.renderer.setAnimationLoop(this.animate.bind(this));
   }
+
+  /**
+   * 释放全部资源(大屏组件销毁时调用,避免重复打开导致
+   * WebGL上下文/动画循环/全局监听累积,引发卡顿与接口超时)
+   */
+  dispose() {
+    // 停止渲染循环
+    if (this.renderer) {
+      this.renderer.setAnimationLoop(null);
+    }
+    // 移除全局监听
+    if (this._boundResize) {
+      window.removeEventListener('resize', this._boundResize);
+      this._boundResize = null;
+    }
+    if (this._boundClick) {
+      window.removeEventListener('click', this._boundClick);
+      this._boundClick = null;
+    }
+    // 释放场景内几何体/材质/贴图
+    if (this.scene) {
+      this.scene.traverse(obj => {
+        if (obj.geometry) {
+          obj.geometry.dispose();
+        }
+        if (obj.material) {
+          const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
+          materials.forEach(mat => {
+            Object.keys(mat).forEach(key => {
+              const value = mat[key];
+              if (value && value.isTexture) {
+                value.dispose();
+              }
+            });
+            mat.dispose();
+          });
+        }
+      });
+    }
+    // 控制器与渲染器
+    if (this.controls) {
+      this.controls.dispose();
+      this.controls = null;
+    }
+    if (this.css3DRenderer && this.css3DRenderer.domElement && this.css3DRenderer.domElement.parentNode) {
+      this.css3DRenderer.domElement.parentNode.removeChild(this.css3DRenderer.domElement);
+    }
+    if (this.renderer) {
+      if (this.renderer.domElement && this.renderer.domElement.parentNode) {
+        this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
+      }
+      this.renderer.dispose();
+      this.renderer.forceContextLoss();
+      this.renderer = null;
+    }
+    this.labels = [];
+    this.outlineObjs = [];
+    this.scene = null;
+  }
   flyTo(target, scale = 1, duration = 1000) {
     const targetPosition = new THREE.Vector3().copy(target.position);
     const targetFocus = new THREE.Vector3().copy(target.targetContent);

+ 195 - 100
ems-ui-cloud/src/views/mgr/powerstore.vue

@@ -4,51 +4,18 @@
       <div class="tips">
         <el-statistic title="总装机">
           <template slot="formatter">
-            {{ pvCfg.total }}kW·h
+            {{ Math.round(pvCfg.total) }}kW·h
           </template>
         </el-statistic>
         <el-statistic :title="`储能${item.name}装机`" v-for="(item,index) in pvCfg.areas" :key="`store_${index}`">
           <template slot="formatter">
-            {{ numToStr(item.store) }}kW·h
+            {{ Math.round(item.store) }}kW·h
           </template>
         </el-statistic>
       </div>
     </el-card>
 
     <el-tabs v-model="activeName" @tab-click="tabClick" class="main-tabs">
-      <!-- 总览Tab -->
-      <el-tab-pane label="总览" name="summary">
-        <div class="chartGroup">
-          <div class="chart-card">
-            <div class="chart-header">
-              <i class="el-icon-s-data"></i>
-              <span>当日充电量【单位:kW·h】</span>
-            </div>
-            <div class="chart-body">
-              <div class="center-label">
-                <div class="label-title">总充电量</div>
-                <div class="label-value">{{ daySum.chargeElecQuantity || 0 }}kW·h</div>
-              </div>
-              <BaseChart width="100%" height="450px" :option="chargePieOptions"/>
-            </div>
-          </div>
-
-          <div class="chart-card">
-            <div class="chart-header">
-              <i class="el-icon-s-marketing"></i>
-              <span>当日放电量【单位:kW·h】</span>
-            </div>
-            <div class="chart-body">
-              <div class="center-label">
-                <div class="label-title">总放电量</div>
-                <div class="label-value">{{ daySum.dischargeElecQuantity || 0 }}kW·h</div>
-              </div>
-              <BaseChart width="100%" height="450px" :option="disChargePieOptions"/>
-            </div>
-          </div>
-        </div>
-      </el-tab-pane>
-
       <!-- 实时分析Tab -->
       <el-tab-pane label="实时监测" name="realtime">
         <!-- 设备状态概览卡片 -->
@@ -80,8 +47,9 @@
                   <i class="el-icon-upload2"></i>
                 </div>
                 <div class="card-content">
-                  <span class="card-label">总充电功率</span>
-                  <span class="card-value">{{ formatNumber(deviceSummary.totalChargePower) }}<small>W</small></span>
+                  <span class="card-label">今日总充电量</span>
+                  <span class="card-value">{{ formatNumber(deviceSummary.totalDayCharge) }}<small>kW·h</small></span>
+                  <span class="card-sub">累计 {{ formatNumber(deviceSummary.totalCumCharge) }} kW·h</span>
                 </div>
               </div>
             </el-col>
@@ -91,8 +59,9 @@
                   <i class="el-icon-download"></i>
                 </div>
                 <div class="card-content">
-                  <span class="card-label">总放电功率</span>
-                  <span class="card-value">{{ formatNumber(deviceSummary.totalDischargePower) }}<small>W</small></span>
+                  <span class="card-label">今日总放电量</span>
+                  <span class="card-value">{{ formatNumber(deviceSummary.totalDayDischarge) }}<small>kW·h</small></span>
+                  <span class="card-sub">累计 {{ formatNumber(deviceSummary.totalCumDischarge) }} kW·h</span>
                 </div>
               </div>
             </el-col>
@@ -151,6 +120,10 @@
                       <span class="info-label">安装位置:</span>
                       <span class="info-value">{{ device.location || '-' }}</span>
                     </div>
+                    <div class="info-row">
+                      <span class="info-label">电池健康:</span>
+                      <span class="info-value">{{ device.bmsSoh || 0 }}%</span>
+                    </div>
                   </div>
                   <div class="device-metrics">
                     <div class="metric-item charge">
@@ -158,8 +131,9 @@
                         <i class="el-icon-top"></i>
                       </div>
                       <div class="metric-info">
-                        <span class="metric-label">充电功率</span>
-                        <span class="metric-value">{{ formatNumber(device.chargeVoltage) }} <small>W</small></span>
+                        <span class="metric-label">今日充电量</span>
+                        <span class="metric-value">{{ formatNumber(device.dayChargeKwh) }} <small>kW·h</small></span>
+                        <span class="metric-sub">累计 {{ formatNumber(device.cumChargeKwh) }}</span>
                       </div>
                     </div>
                     <div class="metric-item discharge">
@@ -167,15 +141,16 @@
                         <i class="el-icon-bottom"></i>
                       </div>
                       <div class="metric-info">
-                        <span class="metric-label">放电功率</span>
-                        <span class="metric-value">{{ formatNumber(device.dischargePower) }} <small>W</small></span>
+                        <span class="metric-label">今日放电量</span>
+                        <span class="metric-value">{{ formatNumber(device.dayDischargeKwh) }} <small>kW·h</small></span>
+                        <span class="metric-sub">累计 {{ formatNumber(device.cumDischargeKwh) }}</span>
                       </div>
                     </div>
                   </div>
                   <div class="capacity-bar">
                     <div class="capacity-label">
-                      <span>当前容量</span>
-                      <span class="capacity-value">{{ formatNumber(device.currentCapacity) }} kW·h</span>
+                      <span>当前容量 / 可用容量</span>
+                      <span class="capacity-value">{{ formatNumber(device.currentCapacity) }} / {{ formatNumber(device.usableCapacity) }} kW·h</span>
                     </div>
                     <el-progress
                       :percentage="getCapacityPercentage(device)"
@@ -203,8 +178,41 @@
         </div>
       </el-tab-pane>
 
+      <!-- 总览Tab -->
+      <el-tab-pane label="当日计量" name="summary">
+        <div class="chartGroup">
+          <div class="chart-card">
+            <div class="chart-header">
+              <i class="el-icon-s-data"></i>
+              <span>当日充电量【单位:kW·h】</span>
+            </div>
+            <div class="chart-body">
+              <div class="center-label">
+                <div class="label-title">总充电量</div>
+                <div class="label-value">{{ daySum.chargeElecQuantity || 0 }}kW·h</div>
+              </div>
+              <BaseChart width="100%" height="450px" :option="chargePieOptions"/>
+            </div>
+          </div>
+
+          <div class="chart-card">
+            <div class="chart-header">
+              <i class="el-icon-s-marketing"></i>
+              <span>当日放电量【单位:kW·h】</span>
+            </div>
+            <div class="chart-body">
+              <div class="center-label">
+                <div class="label-title">总放电量</div>
+                <div class="label-value">{{ daySum.dischargeElecQuantity || 0 }}kW·h</div>
+              </div>
+              <BaseChart width="100%" height="450px" :option="disChargePieOptions"/>
+            </div>
+          </div>
+        </div>
+      </el-tab-pane>
+
       <!-- 区块储能Tab -->
-      <el-tab-pane label="区块储能" name="area">
+      <el-tab-pane label="区块计量" name="area">
         <el-row :gutter="20">
           <el-col :span="5" :xs="24">
             <div class="head-container">
@@ -298,8 +306,8 @@
 </template>
 
 <script>
-// 新的API引用
-import { getLatestByDevice } from '@/api/mgr/elecStoreIndex'
+// API引用
+import { getObjAttrBatch } from '@/api/basecfg/objAttribute'
 import { listElecStoreHour, getDayStatistics } from '@/api/mgr/elecStoreHour'
 import { getByCondition } from '@/api/device/device'
 import { areaTreeByFacsCategory } from '@/api/basecfg/area'
@@ -307,7 +315,6 @@ import { dateFormat, numToStr } from '@/utils/index.js'
 import dayjs from 'dayjs'
 import { DateTool } from '@/utils/DateTool'
 import BaseChart from '@/components/BaseChart'
-import { listConfig } from '@/api/system/config'
 import SubTitle from '@/components/SubTitle'
 
 export default {
@@ -318,7 +325,7 @@ export default {
   },
   data() {
     return {
-      activeName: 'summary',
+      activeName: 'realtime',
       // 遮罩层
       loading: true,
       facsCategory: 'C', // 储能分类
@@ -393,12 +400,12 @@ export default {
       deviceRealtimeList: [], // 设备+指标合并后的列表
       deviceSummary: {
         totalCount: 0,
-        totalChargePower: 0,
-        totalDischargePower: 0,
+        totalDayCharge: 0,
+        totalDayDischarge: 0,
+        totalCumCharge: 0,
+        totalCumDischarge: 0,
         totalCapacity: 0
       },
-      // 假设最大容量为100kW·h,实际应从设备配置获取
-      maxCapacityConfig: 100,
       // 区域映射
       areaMap: {}
     }
@@ -520,7 +527,7 @@ export default {
     } else {
       this.tabClick()
     }
-    this.getPvConfig()
+    this.loadInstalledCapacity()
   },
   methods: {
     numToStr,
@@ -583,30 +590,78 @@ export default {
       this.$refs.areaTree.filter(this.areaName)
     },
 
-    async getPvConfig() {
+    /** 装机容量:从储能一体机(M_W4_DEV_GCC_YTJ)额定容量属性汇总 */
+    async loadInstalledCapacity() {
       try {
-        const { rows } = await listConfig({
-          configKey: 'storage-equipped-capacity'
+        const deviceResponse = await getByCondition({
+          subsystemCode: 'SYS_GCC',
+          deviceModel: 'M_W4_DEV_GCC_YTJ'
         })
-        if (!rows || !rows.length) {
-          return
-        }
-        const cfg = JSON.parse(rows[0].remark)
-        const areas = []
-        let equipedStorage = cfg[this.areaType] ? cfg[this.areaType].store : 0
-        if (!this.areaType || this.areaType === '-1') {
-          for (let item in cfg) {
-            equipedStorage += cfg[item].store
-            areas.push(cfg[item])
-          }
-        }
+        const devices = deviceResponse.data || []
+        if (!devices.length) return
+
+        const attrRes = await getObjAttrBatch(2, 'M_W4_DEV_GCC_YTJ')
+        const attrMap = attrRes.data || {}
+
+        const zoneNames = {}
+        let total = 0
+        devices.forEach(device => {
+          const rated = parseFloat(this.findAttrValue(attrMap[device.deviceCode], 'ratedCapacity')) || 0
+          total += rated
+          const zone = this.getZoneName(device.areaCode)
+          zoneNames[zone] = (zoneNames[zone] || 0) + rated
+        })
+
         this.pvCfg = {
-          total: numToStr(equipedStorage),
-          areas: areas
+          total: numToStr(total),
+          areas: Object.keys(zoneNames).map(name => ({ name, store: zoneNames[name] }))
         }
       } catch (error) {
-        console.error('获取配置失败', error)
+        console.error('获取装机容量失败', error)
+      }
+    },
+
+    /** 区域代码转南北区名称 */
+    getZoneName(areaCode) {
+      if (this.areaMap[areaCode]) return this.areaMap[areaCode]
+      if (areaCode && areaCode.indexOf('S3001') !== -1) return '北区'
+      if (areaCode && areaCode.indexOf('S3002') !== -1) return '南区'
+      return areaCode || '-'
+    },
+
+    /** 判断设备属性包(按分组)中是否存在指定属性 */
+    hasAttr(attrObj, attrKey) {
+      return this.findAttrValue(attrObj, attrKey) !== null
+    },
+
+    /** 从设备属性包(按分组)中取指定属性值 */
+    findAttrValue(attrObj, attrKey) {
+      if (!attrObj) return null
+      for (const group in attrObj) {
+        const list = attrObj[group]
+        if (Array.isArray(list)) {
+          const target = list.find(a => a.attrKey === attrKey)
+          if (target) return target.attrValue
+        }
+      }
+      return null
+    },
+
+    /** 从设备属性包中取最新更新时间 */
+    findAttrUpdateTime(attrObj) {
+      if (!attrObj) return null
+      let latest = null
+      for (const group in attrObj) {
+        const list = attrObj[group]
+        if (Array.isArray(list)) {
+          list.forEach(a => {
+            if (a.updateTime && (!latest || new Date(a.updateTime) > new Date(latest))) {
+              latest = a.updateTime
+            }
+          })
+        }
       }
+      return latest
     },
 
     // 查询区域列表 - 返回Promise以供tabClick使用
@@ -731,27 +786,39 @@ export default {
 
     // ========== 实时分析相关方法 ==========
 
-    /** 加载实时数据 - 先获取设备列表,再匹配指标数据 */
+    /** 加载实时数据 - 从储能一体机物模型属性值读取 */
     async loadRealtimeData() {
       this.realtimeLoading = true
       try {
-        // 1. 先获取设备列表
-        const deviceResponse = await getByCondition({ subsystemCode: 'SYS_GCC' })
+        // 1. 获取储能一体机设备列表
+        const deviceResponse = await getByCondition({
+          subsystemCode: 'SYS_GCC',
+          deviceModel: 'M_W4_DEV_GCC_YTJ'
+        })
         this.deviceList = deviceResponse.data || []
 
-        // 2. 获取每个设备的最新指标数据
-        const indexResponse = await getLatestByDevice({})
-        const indexList = indexResponse.data || []
-
-        // 3. 将指标数据映射为 Map,以 deviceCode 为 key
-        const indexMap = new Map()
-        indexList.forEach(item => {
-          indexMap.set(item.deviceCode, item)
-        })
+        // 2. 批量获取设备属性值
+        const attrRes = await getObjAttrBatch(2, 'M_W4_DEV_GCC_YTJ')
+        const attrMap = attrRes.data || {}
 
-        // 4. 合并设备信息和指标数据
+        // 3. 合并设备信息与属性指标
         this.deviceRealtimeList = this.deviceList.map(device => {
-          const indexData = indexMap.get(device.deviceCode) || {}
+          const attrs = attrMap[device.deviceCode] || {}
+          const num = (key) => {
+            const v = parseFloat(this.findAttrValue(attrs, key))
+            return isNaN(v) ? 0 : v
+          }
+
+          const ratedCapacity = num('ratedCapacity')
+          const soc = num('bmsSoc')
+          const soh = num('bmsSoh')
+          // PCS有功功率(kW)
+          const powerKw = num('pcsActivePower')
+          // 充放电方向:优先按协议充放电状态(170-充电 153-放电),待机/未知时按有功功率符号兜底(正-充电 负-放电)
+          const chargeStatus = String(this.findAttrValue(attrs, 'pcsChargeStatus') || '')
+          const isCharging = chargeStatus === '170' || (chargeStatus !== '153' && powerKw > 0)
+          const isDischarging = chargeStatus === '153' || (chargeStatus !== '170' && powerKw < 0)
+
           return {
             // 设备基础信息
             deviceCode: device.deviceCode,
@@ -762,18 +829,28 @@ export default {
             location: device.location,
             areaCode: device.areaCode,
             refFacs: device.refFacs,
-            // 指标数据
-            chargeVoltage: indexData.chargeVoltage || 0,
-            dischargePower: indexData.dischargePower || 0,
-            currentCapacity: indexData.currentCapacity || 0,
-            indexTime: indexData.time || null
+            // 指标数据(功率换算为W,保留用于状态判定)
+            chargeVoltage: isCharging ? Math.abs(powerKw) * 1000 : 0,
+            dischargePower: isDischarging ? Math.abs(powerKw) * 1000 : 0,
+            // 充放电电量:adapter按"BMS累计值-当日零点基线"推算的当日增量(厂家PCS日电量字段不可信)
+            dayChargeKwh: num('dayChargeKwh'),
+            dayDischargeKwh: num('dayDischargeKwh'),
+            cumChargeKwh: num('bmsCumChargeKwh'),
+            cumDischargeKwh: num('bmsCumDischargeKwh'),
+            // 当前容量 = 额定容量 * SOC * SOH
+            // 当前容量 = 额定容量 * SOC * SOH;可用容量 = 额定容量 * SOH(老化后实际最大容量)
+            currentCapacity: ratedCapacity * soc / 100 * soh / 100,
+            ratedCapacity: ratedCapacity,
+            bmsSoh: soh,
+            usableCapacity: ratedCapacity * soh / 100,
+            indexTime: this.findAttrUpdateTime(attrs)
           }
         })
 
-        // 5. 计算汇总数据
+        // 4. 计算汇总数据
         this.calculateDeviceSummary()
 
-        // 6. 更新时间
+        // 5. 更新时间
         this.lastUpdateTime = this.formatDateTime(new Date())
       } catch (error) {
         console.error('加载实时数据失败', error)
@@ -792,14 +869,18 @@ export default {
     calculateDeviceSummary() {
       const summary = {
         totalCount: this.deviceRealtimeList.length,
-        totalChargePower: 0,
-        totalDischargePower: 0,
+        totalDayCharge: 0,
+        totalDayDischarge: 0,
+        totalCumCharge: 0,
+        totalCumDischarge: 0,
         totalCapacity: 0
       }
 
       this.deviceRealtimeList.forEach(device => {
-        summary.totalChargePower += parseFloat(device.chargeVoltage) || 0
-        summary.totalDischargePower += parseFloat(device.dischargePower) || 0
+        summary.totalDayCharge += parseFloat(device.dayChargeKwh) || 0
+        summary.totalDayDischarge += parseFloat(device.dayDischargeKwh) || 0
+        summary.totalCumCharge += parseFloat(device.cumChargeKwh) || 0
+        summary.totalCumDischarge += parseFloat(device.cumDischargeKwh) || 0
         summary.totalCapacity += parseFloat(device.currentCapacity) || 0
       })
 
@@ -845,10 +926,12 @@ export default {
       return '待机'
     },
 
-    /** 获取容量百分比 */
+    /** 获取容量百分比(按可用容量 = 额定容量*SOH,即 SOC) */
     getCapacityPercentage(device) {
       const capacity = parseFloat(device.currentCapacity) || 0
-      return Math.min(100, Math.max(0, (capacity / this.maxCapacityConfig) * 100))
+      const usable = parseFloat(device.usableCapacity) || 0
+      if (usable <= 0) return 0
+      return Math.min(100, Math.max(0, (capacity / usable) * 100))
     },
 
     /** 获取容量进度条颜色 */
@@ -1073,6 +1156,12 @@ export default {
       font-weight: 500;
     }
 
+    .card-sub {
+      font-size: 12px;
+      color: #b1b3b8;
+      margin-top: 4px;
+    }
+
     .card-value {
       font-size: 24px;
       font-weight: 700;
@@ -1252,6 +1341,12 @@ export default {
             color: #909399;
           }
 
+          .metric-sub {
+            font-size: 11px;
+            color: #b1b3b8;
+            margin-top: 2px;
+          }
+
           .metric-value {
             font-size: 14px;
             font-weight: 600;

+ 2 - 2
ems-ui-cloud/vue.config.js

@@ -36,8 +36,8 @@ module.exports = {
     proxy: {
       // detail: https://cli.vuejs.org/config/#devserver-proxy
       [process.env.VUE_APP_BASE_API]: {
-        target: 'http://172.192.13.95:9100/',
-        // target: 'http://127.0.0.1:9100/',
+        target: 'http://192.168.20.210:9100/',
+        //target: 'http://127.0.0.1:9100/',
         changeOrigin: true,
         pathRewrite: {
           ['^' + process.env.VUE_APP_BASE_API + '/ems']: process.env.EMS_REWRITE_URL,