|
@@ -16,6 +16,7 @@
|
|
|
</el-card>
|
|
</el-card>
|
|
|
|
|
|
|
|
<el-tabs v-model="activeName" @tab-click="tabClick" class="main-tabs">
|
|
<el-tabs v-model="activeName" @tab-click="tabClick" class="main-tabs">
|
|
|
|
|
+ <!-- 总览Tab -->
|
|
|
<el-tab-pane label="总览" name="summary">
|
|
<el-tab-pane label="总览" name="summary">
|
|
|
<div class="chartGroup">
|
|
<div class="chartGroup">
|
|
|
<div class="chart-card">
|
|
<div class="chart-card">
|
|
@@ -48,6 +49,161 @@
|
|
|
</div>
|
|
</div>
|
|
|
</el-tab-pane>
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
+ <!-- 实时分析Tab -->
|
|
|
|
|
+ <el-tab-pane label="实时监测" name="realtime">
|
|
|
|
|
+ <!-- 设备状态概览卡片 -->
|
|
|
|
|
+ <div class="overview-section">
|
|
|
|
|
+ <div class="section-header">
|
|
|
|
|
+ <h3 class="section-title">
|
|
|
|
|
+ <i class="el-icon-data-board"></i>
|
|
|
|
|
+ 设备状态概览
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <el-button type="text" icon="el-icon-refresh" @click="refreshRealtimeData" :loading="realtimeLoading">
|
|
|
|
|
+ 刷新数据
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <div class="stat-card total-card">
|
|
|
|
|
+ <div class="card-icon">
|
|
|
|
|
+ <i class="el-icon-s-platform"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-content">
|
|
|
|
|
+ <span class="card-label">设备总数</span>
|
|
|
|
|
+ <span class="card-value">{{ deviceSummary.totalCount }}<small>台</small></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <div class="stat-card charge-card">
|
|
|
|
|
+ <div class="card-icon">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <div class="stat-card discharge-card">
|
|
|
|
|
+ <div class="card-icon">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="6">
|
|
|
|
|
+ <div class="stat-card capacity-card">
|
|
|
|
|
+ <div class="card-icon">
|
|
|
|
|
+ <i class="el-icon-odometer"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="card-content">
|
|
|
|
|
+ <span class="card-label">总当前容量</span>
|
|
|
|
|
+ <span class="card-value">{{ formatNumber(deviceSummary.totalCapacity) }}<small>kW·h</small></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 设备实时状态列表 -->
|
|
|
|
|
+ <div class="device-realtime-section" v-loading="realtimeLoading">
|
|
|
|
|
+ <div class="section-header">
|
|
|
|
|
+ <h3 class="section-title">
|
|
|
|
|
+ <i class="el-icon-cpu"></i>
|
|
|
|
|
+ 设备实时状态
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ <span class="update-time" v-if="lastUpdateTime">
|
|
|
|
|
+ <i class="el-icon-time"></i>
|
|
|
|
|
+ 更新时间: {{ lastUpdateTime }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
|
|
+ <el-col
|
|
|
|
|
+ v-for="device in deviceRealtimeList"
|
|
|
|
|
+ :key="device.deviceCode"
|
|
|
|
|
+ :span="12"
|
|
|
|
|
+ :lg="8"
|
|
|
|
|
+ :xl="6"
|
|
|
|
|
+ class="device-card-col"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="device-card" :class="getDeviceStatusClass(device)">
|
|
|
|
|
+ <div class="device-header">
|
|
|
|
|
+ <div class="device-title">
|
|
|
|
|
+ <i class="el-icon-cpu device-icon"></i>
|
|
|
|
|
+ <span class="device-name">{{ device.deviceName || device.deviceCode }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-tag :type="getDeviceStatusType(device)" size="mini" effect="dark">
|
|
|
|
|
+ {{ getDeviceStatusText(device) }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="device-body">
|
|
|
|
|
+ <div class="device-info">
|
|
|
|
|
+ <div class="info-row">
|
|
|
|
|
+ <span class="info-label">设备编码:</span>
|
|
|
|
|
+ <span class="info-value code">{{ device.deviceCode }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="info-row">
|
|
|
|
|
+ <span class="info-label">安装位置:</span>
|
|
|
|
|
+ <span class="info-value">{{ device.location || '-' }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="device-metrics">
|
|
|
|
|
+ <div class="metric-item charge">
|
|
|
|
|
+ <div class="metric-icon">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="metric-item discharge">
|
|
|
|
|
+ <div class="metric-icon">
|
|
|
|
|
+ <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>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="capacity-bar">
|
|
|
|
|
+ <div class="capacity-label">
|
|
|
|
|
+ <span>当前容量</span>
|
|
|
|
|
+ <span class="capacity-value">{{ formatNumber(device.currentCapacity) }} kW·h</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-progress
|
|
|
|
|
+ :percentage="getCapacityPercentage(device)"
|
|
|
|
|
+ :stroke-width="12"
|
|
|
|
|
+ :color="getCapacityColor(device)"
|
|
|
|
|
+ :show-text="false"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="capacity-percent">{{ getCapacityPercentage(device).toFixed(1) }}%</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="device-footer">
|
|
|
|
|
+ <span class="device-area">
|
|
|
|
|
+ <i class="el-icon-location-outline"></i>
|
|
|
|
|
+ {{ getAreaName(device.areaCode) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="device-time" v-if="device.indexTime">
|
|
|
|
|
+ <i class="el-icon-time"></i>
|
|
|
|
|
+ {{ formatTime(device.indexTime) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ <el-empty v-if="deviceRealtimeList.length === 0 && !realtimeLoading" description="暂无设备数据" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 区块储能Tab -->
|
|
|
<el-tab-pane label="区块储能" name="area">
|
|
<el-tab-pane label="区块储能" name="area">
|
|
|
<el-row :gutter="20">
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="5" :xs="24">
|
|
<el-col :span="5" :xs="24">
|
|
@@ -142,14 +298,17 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
-import { dayStatistics, listElecStoreH } from '@/api/mgr/elecStoreH'
|
|
|
|
|
|
|
+// 新的API引用
|
|
|
|
|
+import { getLatestByDevice } from '@/api/mgr/elecStoreIndex'
|
|
|
|
|
+import { listElecStoreHour, getDayStatistics } from '@/api/mgr/elecStoreHour'
|
|
|
|
|
+import { getByCondition } from '@/api/device/device'
|
|
|
import { areaTreeByFacsCategory } from '@/api/basecfg/area'
|
|
import { areaTreeByFacsCategory } from '@/api/basecfg/area'
|
|
|
import { dateFormat, numToStr } from '@/utils/index.js'
|
|
import { dateFormat, numToStr } from '@/utils/index.js'
|
|
|
import dayjs from 'dayjs'
|
|
import dayjs from 'dayjs'
|
|
|
import { DateTool } from '@/utils/DateTool'
|
|
import { DateTool } from '@/utils/DateTool'
|
|
|
import BaseChart from '@/components/BaseChart'
|
|
import BaseChart from '@/components/BaseChart'
|
|
|
import { listConfig } from '@/api/system/config'
|
|
import { listConfig } from '@/api/system/config'
|
|
|
-import SubTitle from '@/components/SubTitle' // 确保引入SubTitle
|
|
|
|
|
|
|
+import SubTitle from '@/components/SubTitle'
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
name: 'ElecStoreH',
|
|
name: 'ElecStoreH',
|
|
@@ -225,7 +384,23 @@ export default {
|
|
|
dateRange: [
|
|
dateRange: [
|
|
|
dayjs().format(DateTool.DateFormat.YYYY_MM_DD_00_00_00),
|
|
dayjs().format(DateTool.DateFormat.YYYY_MM_DD_00_00_00),
|
|
|
dayjs().format(DateTool.DateFormat.YYYY_MM_DD_23_59_59)
|
|
dayjs().format(DateTool.DateFormat.YYYY_MM_DD_23_59_59)
|
|
|
- ]
|
|
|
|
|
|
|
+ ],
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 实时分析相关数据 ==========
|
|
|
|
|
+ realtimeLoading: false,
|
|
|
|
|
+ lastUpdateTime: '',
|
|
|
|
|
+ deviceList: [], // 设备基础信息列表
|
|
|
|
|
+ deviceRealtimeList: [], // 设备+指标合并后的列表
|
|
|
|
|
+ deviceSummary: {
|
|
|
|
|
+ totalCount: 0,
|
|
|
|
|
+ totalChargePower: 0,
|
|
|
|
|
+ totalDischargePower: 0,
|
|
|
|
|
+ totalCapacity: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ // 假设最大容量为100kW·h,实际应从设备配置获取
|
|
|
|
|
+ maxCapacityConfig: 100,
|
|
|
|
|
+ // 区域映射
|
|
|
|
|
+ areaMap: {}
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
@@ -350,6 +525,51 @@ export default {
|
|
|
methods: {
|
|
methods: {
|
|
|
numToStr,
|
|
numToStr,
|
|
|
|
|
|
|
|
|
|
+ // ========== 工具方法 ==========
|
|
|
|
|
+ formatNumber(value) {
|
|
|
|
|
+ if (value === null || value === undefined) return '0.00'
|
|
|
|
|
+ const num = parseFloat(value)
|
|
|
|
|
+ if (isNaN(num)) return '0.00'
|
|
|
|
|
+ return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ formatTime(time) {
|
|
|
|
|
+ if (!time) return '-'
|
|
|
|
|
+ if (typeof time === 'string') {
|
|
|
|
|
+ // 如果是完整的日期时间字符串,只取时间部分
|
|
|
|
|
+ if (time.includes(' ')) {
|
|
|
|
|
+ return time.split(' ')[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ return time
|
|
|
|
|
+ }
|
|
|
|
|
+ const d = new Date(time)
|
|
|
|
|
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ formatDateTime(date) {
|
|
|
|
|
+ if (!date) return ''
|
|
|
|
|
+ const d = new Date(date)
|
|
|
|
|
+ const year = d.getFullYear()
|
|
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
+ const day = String(d.getDate()).padStart(2, '0')
|
|
|
|
|
+ const hours = String(d.getHours()).padStart(2, '0')
|
|
|
|
|
+ const minutes = String(d.getMinutes()).padStart(2, '0')
|
|
|
|
|
+ const seconds = String(d.getSeconds()).padStart(2, '0')
|
|
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ getAreaName(areaCode) {
|
|
|
|
|
+ if (!areaCode) return '-'
|
|
|
|
|
+ // 优先从映射中获取
|
|
|
|
|
+ if (this.areaMap[areaCode]) {
|
|
|
|
|
+ return this.areaMap[areaCode]
|
|
|
|
|
+ }
|
|
|
|
|
+ // 根据已知的区域代码返回名称
|
|
|
|
|
+ if (areaCode === '321283124S3001') return '常泰北区'
|
|
|
|
|
+ if (areaCode === '321283124S3002') return '常泰南区'
|
|
|
|
|
+ return areaCode
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
// 获取树节点图标
|
|
// 获取树节点图标
|
|
|
getTreeIcon(data) {
|
|
getTreeIcon(data) {
|
|
|
if (data.facsCategory === 'C') {
|
|
if (data.facsCategory === 'C') {
|
|
@@ -398,12 +618,26 @@ export default {
|
|
|
const response = await areaTreeByFacsCategory(this.facsCategory, this.facsSubCategory, false)
|
|
const response = await areaTreeByFacsCategory(this.facsCategory, this.facsSubCategory, false)
|
|
|
this.areaOptions = [{ id: '-1', label: '全部', children: response.data || [] }]
|
|
this.areaOptions = [{ id: '-1', label: '全部', children: response.data || [] }]
|
|
|
|
|
|
|
|
|
|
+ // 构建区域映射
|
|
|
|
|
+ this.buildAreaMap(response.data || [])
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
this.$message.error('获取区域列表失败')
|
|
this.$message.error('获取区域列表失败')
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- /** 查询储能计量-小时列表 */
|
|
|
|
|
|
|
+ // 构建区域映射
|
|
|
|
|
+ buildAreaMap(nodes) {
|
|
|
|
|
+ nodes.forEach(node => {
|
|
|
|
|
+ if (node.id && node.label) {
|
|
|
|
|
+ this.areaMap[node.id] = node.label
|
|
|
|
|
+ }
|
|
|
|
|
+ if (node.children && node.children.length > 0) {
|
|
|
|
|
+ this.buildAreaMap(node.children)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 查询储能计量-小时列表 (使用新API) */
|
|
|
getList() {
|
|
getList() {
|
|
|
this.loading = true
|
|
this.loading = true
|
|
|
const { areaCode } = this.queryParams
|
|
const { areaCode } = this.queryParams
|
|
@@ -414,7 +648,7 @@ export default {
|
|
|
startRecTime = start
|
|
startRecTime = start
|
|
|
endRecTime = end
|
|
endRecTime = end
|
|
|
}
|
|
}
|
|
|
- listElecStoreH({
|
|
|
|
|
|
|
+ listElecStoreHour({
|
|
|
areaCode,
|
|
areaCode,
|
|
|
pageNum: 1,
|
|
pageNum: 1,
|
|
|
pageSize: 999,
|
|
pageSize: 999,
|
|
@@ -433,7 +667,7 @@ export default {
|
|
|
getTodayChart() {
|
|
getTodayChart() {
|
|
|
const { areaCode } = this.queryParams
|
|
const { areaCode } = this.queryParams
|
|
|
const nowDay = dateFormat(new Date(), 'yyyy-MM-dd')
|
|
const nowDay = dateFormat(new Date(), 'yyyy-MM-dd')
|
|
|
- listElecStoreH({
|
|
|
|
|
|
|
+ listElecStoreHour({
|
|
|
areaCode,
|
|
areaCode,
|
|
|
pageNum: 1,
|
|
pageNum: 1,
|
|
|
pageSize: 999,
|
|
pageSize: 999,
|
|
@@ -448,7 +682,7 @@ export default {
|
|
|
|
|
|
|
|
getSummary() {
|
|
getSummary() {
|
|
|
const date = dateFormat(new Date(), 'yyyy-MM-dd')
|
|
const date = dateFormat(new Date(), 'yyyy-MM-dd')
|
|
|
- dayStatistics({ date }).then(({ code, data }) => {
|
|
|
|
|
|
|
+ getDayStatistics(date).then(({ code, data }) => {
|
|
|
if (code === 200) {
|
|
if (code === 200) {
|
|
|
this.hourSum = data.hourSum || []
|
|
this.hourSum = data.hourSum || []
|
|
|
this.daySum = data.daySum || {}
|
|
this.daySum = data.daySum || {}
|
|
@@ -472,11 +706,15 @@ export default {
|
|
|
this.getTodayChart()
|
|
this.getTodayChart()
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- /** Tab 切换逻辑 - 修复树高亮问题 */
|
|
|
|
|
|
|
+ /** Tab 切换逻辑 */
|
|
|
tabClick() {
|
|
tabClick() {
|
|
|
if (this.activeName === 'summary') {
|
|
if (this.activeName === 'summary') {
|
|
|
this.getSummary()
|
|
this.getSummary()
|
|
|
|
|
+ } else if (this.activeName === 'realtime') {
|
|
|
|
|
+ // 实时分析Tab
|
|
|
|
|
+ this.loadRealtimeData()
|
|
|
} else {
|
|
} else {
|
|
|
|
|
+ // 区块储能Tab
|
|
|
this.selectedLabel = '全部'
|
|
this.selectedLabel = '全部'
|
|
|
this.queryParams.areaCode = '-1'
|
|
this.queryParams.areaCode = '-1'
|
|
|
|
|
|
|
@@ -492,6 +730,136 @@ export default {
|
|
|
this.getTodayChart()
|
|
this.getTodayChart()
|
|
|
this.getList()
|
|
this.getList()
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ========== 实时分析相关方法 ==========
|
|
|
|
|
+
|
|
|
|
|
+ /** 加载实时数据 - 先获取设备列表,再匹配指标数据 */
|
|
|
|
|
+ async loadRealtimeData() {
|
|
|
|
|
+ this.realtimeLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 1. 先获取设备列表
|
|
|
|
|
+ const deviceResponse = await getByCondition({ subsystemCode: 'SYS_GCC' })
|
|
|
|
|
+ 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)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 合并设备信息和指标数据
|
|
|
|
|
+ this.deviceRealtimeList = this.deviceList.map(device => {
|
|
|
|
|
+ const indexData = indexMap.get(device.deviceCode) || {}
|
|
|
|
|
+ return {
|
|
|
|
|
+ // 设备基础信息
|
|
|
|
|
+ deviceCode: device.deviceCode,
|
|
|
|
|
+ deviceName: device.deviceName,
|
|
|
|
|
+ deviceBrand: device.deviceBrand,
|
|
|
|
|
+ deviceSpec: device.deviceSpec,
|
|
|
|
|
+ deviceStatus: device.deviceStatus,
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 计算汇总数据
|
|
|
|
|
+ this.calculateDeviceSummary()
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 更新时间
|
|
|
|
|
+ this.lastUpdateTime = this.formatDateTime(new Date())
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载实时数据失败', error)
|
|
|
|
|
+ this.$message.error('加载实时数据失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.realtimeLoading = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 刷新实时数据 */
|
|
|
|
|
+ refreshRealtimeData() {
|
|
|
|
|
+ this.loadRealtimeData()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 计算设备汇总数据 */
|
|
|
|
|
+ calculateDeviceSummary() {
|
|
|
|
|
+ const summary = {
|
|
|
|
|
+ totalCount: this.deviceRealtimeList.length,
|
|
|
|
|
+ totalChargePower: 0,
|
|
|
|
|
+ totalDischargePower: 0,
|
|
|
|
|
+ totalCapacity: 0
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.deviceRealtimeList.forEach(device => {
|
|
|
|
|
+ summary.totalChargePower += parseFloat(device.chargeVoltage) || 0
|
|
|
|
|
+ summary.totalDischargePower += parseFloat(device.dischargePower) || 0
|
|
|
|
|
+ summary.totalCapacity += parseFloat(device.currentCapacity) || 0
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ this.deviceSummary = summary
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 获取设备状态样式类 */
|
|
|
|
|
+ getDeviceStatusClass(device) {
|
|
|
|
|
+ const chargePower = parseFloat(device.chargeVoltage) || 0
|
|
|
|
|
+ const dischargePower = parseFloat(device.dischargePower) || 0
|
|
|
|
|
+
|
|
|
|
|
+ if (chargePower > dischargePower) {
|
|
|
|
|
+ return 'charging'
|
|
|
|
|
+ } else if (dischargePower > chargePower) {
|
|
|
|
|
+ return 'discharging'
|
|
|
|
|
+ }
|
|
|
|
|
+ return 'idle'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 获取设备状态标签类型 */
|
|
|
|
|
+ getDeviceStatusType(device) {
|
|
|
|
|
+ const chargePower = parseFloat(device.chargeVoltage) || 0
|
|
|
|
|
+ const dischargePower = parseFloat(device.dischargePower) || 0
|
|
|
|
|
+
|
|
|
|
|
+ if (chargePower > dischargePower) {
|
|
|
|
|
+ return 'success'
|
|
|
|
|
+ } else if (dischargePower > chargePower) {
|
|
|
|
|
+ return 'warning'
|
|
|
|
|
+ }
|
|
|
|
|
+ return 'info'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 获取设备状态文本 */
|
|
|
|
|
+ getDeviceStatusText(device) {
|
|
|
|
|
+ const chargePower = parseFloat(device.chargeVoltage) || 0
|
|
|
|
|
+ const dischargePower = parseFloat(device.dischargePower) || 0
|
|
|
|
|
+
|
|
|
|
|
+ if (chargePower > dischargePower) {
|
|
|
|
|
+ return '充电中'
|
|
|
|
|
+ } else if (dischargePower > chargePower) {
|
|
|
|
|
+ return '放电中'
|
|
|
|
|
+ }
|
|
|
|
|
+ return '待机'
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 获取容量百分比 */
|
|
|
|
|
+ getCapacityPercentage(device) {
|
|
|
|
|
+ const capacity = parseFloat(device.currentCapacity) || 0
|
|
|
|
|
+ return Math.min(100, Math.max(0, (capacity / this.maxCapacityConfig) * 100))
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /** 获取容量进度条颜色 */
|
|
|
|
|
+ getCapacityColor(device) {
|
|
|
|
|
+ const percentage = this.getCapacityPercentage(device)
|
|
|
|
|
+ if (percentage >= 60) return '#67c23a'
|
|
|
|
|
+ if (percentage >= 30) return '#e6a23c'
|
|
|
|
|
+ return '#f56c6c'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -617,6 +985,338 @@ export default {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ========== 实时分析Tab样式 ==========
|
|
|
|
|
+.overview-section,
|
|
|
|
|
+.device-realtime-section {
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ .section-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .update-time {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ margin-right: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 统计卡片
|
|
|
|
|
+.stat-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ transform: translateY(-4px);
|
|
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-icon {
|
|
|
|
|
+ width: 56px;
|
|
|
|
|
+ height: 56px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-right: 16px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ font-size: 26px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+
|
|
|
|
|
+ .card-label {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .card-value {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+
|
|
|
|
|
+ small {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-left: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 卡片颜色变体
|
|
|
|
|
+ &.total-card {
|
|
|
|
|
+ &::before { background: #409eff; }
|
|
|
|
|
+ .card-icon { background: linear-gradient(135deg, #409eff 0%, #53a8ff 100%); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.charge-card {
|
|
|
|
|
+ &::before { background: #67c23a; }
|
|
|
|
|
+ .card-icon { background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.discharge-card {
|
|
|
|
|
+ &::before { background: #e6a23c; }
|
|
|
|
|
+ .card-icon { background: linear-gradient(135deg, #e6a23c 0%, #f5c069 100%); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.capacity-card {
|
|
|
|
|
+ &::before { background: #909399; }
|
|
|
|
|
+ .card-icon { background: linear-gradient(135deg, #909399 0%, #b4b4b4 100%); }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 设备卡片
|
|
|
|
|
+.device-card-col {
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.device-card {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ &::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.charging::before {
|
|
|
|
|
+ background: #67c23a;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.discharging::before {
|
|
|
|
|
+ background: #e6a23c;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .device-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .device-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+
|
|
|
|
|
+ .device-icon {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .device-name {
|
|
|
|
|
+ font-size: 15px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .device-body {
|
|
|
|
|
+ .device-info {
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ padding-bottom: 12px;
|
|
|
|
|
+ border-bottom: 1px dashed #ebeef5;
|
|
|
|
|
+
|
|
|
|
|
+ .info-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-label {
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ width: 70px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .info-value {
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+
|
|
|
|
|
+ &.code {
|
|
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
|
|
+ background: #f4f4f5;
|
|
|
|
|
+ padding: 2px 6px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .device-metrics {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .metric-item {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+
|
|
|
|
|
+ .metric-icon {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.charge .metric-icon {
|
|
|
|
|
+ background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.discharge .metric-icon {
|
|
|
|
|
+ background: linear-gradient(135deg, #e6a23c 0%, #f5c069 100%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .metric-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+
|
|
|
|
|
+ .metric-label {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .metric-value {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+
|
|
|
|
|
+ small {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 400;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .capacity-bar {
|
|
|
|
|
+ .capacity-label {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+
|
|
|
|
|
+ .capacity-value {
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .capacity-percent {
|
|
|
|
|
+ text-align: right;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .el-progress-bar__outer {
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ::v-deep .el-progress-bar__inner {
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .device-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-top: 12px;
|
|
|
|
|
+ padding-top: 12px;
|
|
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+
|
|
|
|
|
+ i {
|
|
|
|
|
+ margin-right: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ========== 区块储能Tab样式 ==========
|
|
|
.head-container {
|
|
.head-container {
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
padding: 15px;
|
|
padding: 15px;
|
|
@@ -757,6 +1457,13 @@ export default {
|
|
|
margin-bottom: 20px;
|
|
margin-bottom: 20px;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .device-card {
|
|
|
|
|
+ .device-metrics {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 8px !important;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
@media (max-width: 768px) {
|