| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116 |
- <template>
- <div class="energy-storage-container">
- <el-row :gutter="20">
- <el-col :span="5" :xs="24">
- <div class="head-container">
- <el-input v-model="searchKeyword" placeholder="搜索区域/设施" clearable size="small"
- prefix-icon="el-icon-search" @input="filterTree"
- />
- </div>
- <div class="head-container tree-container">
- <el-tree ref="treeRef" :data="treeData" :props="treeProps" :expand-on-click-node="false"
- :filter-node-method="filterNode" node-key="id" :default-expanded-keys="defaultExpandedKeys"
- highlight-current @node-click="handleNodeClick"
- >
- <span class="custom-tree-node" slot-scope="{ node, data }">
- <span class="tree-label"><i :class="getTreeIcon(data)" class="tree-icon"></i>{{ node.label }}</span>
- <el-tag v-if="data.facsCategory === 'C'" size="mini" effect="plain" class="tree-tag storage-tag"
- >储能</el-tag>
- </span>
- </el-tree>
- </div>
- </el-col>
- <el-col :span="19" :xs="24">
- <div class="main-content">
- <div class="content-header">
- <div class="header-title">
- <h2 class="page-title"><i class="el-icon-data-analysis"></i>储能分析<span class="title-divider"
- >|</span><span class="current-selection">{{ selectedLabel }}</span></h2>
- </div>
- <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-group>
- </div>
- </div>
- <!-- 充放电分析Tab -->
- <div v-show="activeTab === 'charge'" class="tab-content">
- <div class="filter-bar">
- <div class="filter-left">
- <el-radio-group v-model="chargeAnalysis.timeDimension" size="small" @change="handleChargeTimeChange">
- <el-radio-button label="hour">按小时</el-radio-button>
- <el-radio-button label="day">按天</el-radio-button>
- </el-radio-group>
- </div>
- <div class="filter-right">
- <!-- 新增:设施筛选 -->
- <el-select
- v-model="chargeAnalysis.selectedFacs"
- placeholder="全部设施"
- size="small"
- clearable
- @change="loadChargeAnalysisData"
- class="facs-select"
- >
- <el-option label="全部设施" value=""/>
- <el-option
- v-for="facs in filteredFacsList"
- :key="facs.facsCode"
- :label="facs.facsName"
- :value="facs.facsCode"
- />
- </el-select>
- <el-date-picker v-if="chargeAnalysis.timeDimension === 'hour'" v-model="chargeAnalysis.selectedDate"
- type="date" placeholder="选择日期" value-format="yyyy-MM-dd" size="small"
- @change="loadChargeAnalysisData" class="date-picker"
- />
- <el-date-picker v-else v-model="chargeAnalysis.dateRange" type="daterange" range-separator="至"
- start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd"
- size="small" @change="loadChargeAnalysisData" :picker-options="datePickerOptions"
- class="date-picker"
- />
- </div>
- </div>
- <div class="summary-cards" v-loading="chargeAnalysis.summaryLoading">
- <el-row :gutter="16">
- <el-col :span="8">
- <div class="stat-card charge-summary-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(chargeAnalysis.summary.totalCharge) }}<small>kW·h</small></span></div>
- </div>
- </el-col>
- <el-col :span="8">
- <div class="stat-card discharge-summary-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(chargeAnalysis.summary.totalDischarge) }}<small>kW·h</small></span></div>
- </div>
- </el-col>
- <el-col :span="8">
- <div class="stat-card net-card">
- <div class="card-icon"><i class="el-icon-sort"></i></div>
- <div class="card-content"><span class="card-label">净充电量</span><span class="card-value"
- :class="{ 'positive': chargeAnalysis.summary.netCharge >= 0, 'negative': chargeAnalysis.summary.netCharge < 0 }"
- >{{
- chargeAnalysis.summary.netCharge >= 0 ? '+' : ''
- }}{{ formatNumber(chargeAnalysis.summary.netCharge) }}<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 class="chart-controls">
- <el-radio-group v-model="chargeAnalysis.chartType" size="mini">
- <el-radio-button label="bar"><i class="el-icon-s-data"></i></el-radio-button>
- <el-radio-button label="line"><i class="el-icon-data-line"></i></el-radio-button>
- </el-radio-group>
- </div>
- </div>
- <div class="chart-wrapper" v-loading="chargeAnalysis.chartLoading">
- <div ref="chargeChartRef" class="chart-canvas"></div>
- </div>
- </div>
- <div class="table-section">
- <div class="section-header"><h3 class="section-title"><i class="el-icon-document"></i>充放电明细</h3>
- <el-button-group>
- <el-button :type="chargeAnalysis.viewMode === 'table' ? 'primary' : ''" size="mini"
- icon="el-icon-menu" @click="chargeAnalysis.viewMode = 'table'"
- >表格
- </el-button>
- <el-button :type="chargeAnalysis.viewMode === 'chart' ? 'primary' : ''" size="mini"
- icon="el-icon-s-data" @click="chargeAnalysis.viewMode = 'chart'"
- >图表
- </el-button>
- </el-button-group>
- </div>
- <el-table v-if="chargeAnalysis.viewMode === 'table'" v-loading="chargeAnalysis.tableLoading"
- :data="chargeAnalysis.tableData" class="data-table" max-height="400" border show-summary
- :summary-method="getChargeSummaries"
- >
- <el-table-column type="index" label="序号" width="60" align="center"/>
- <el-table-column v-if="chargeAnalysis.timeDimension === 'hour'" label="时间" prop="time" width="100"
- align="center"
- >
- <template slot-scope="scope">{{ formatHourTime(scope.row.time) }}</template>
- </el-table-column>
- <el-table-column v-else label="日期" prop="date" width="120" align="center"/>
- <el-table-column label="区域" min-width="120" show-overflow-tooltip>
- <template slot-scope="scope">
- {{ scope.row.areaShortName || scope.row.areaName || getAreaName(scope.row.areaCode) }}
- </template>
- </el-table-column>
- <el-table-column label="设施" min-width="120" show-overflow-tooltip>
- <template slot-scope="scope">{{ scope.row.facsName || getFacsName(scope.row.facsCode) }}</template>
- </el-table-column>
- <el-table-column label="充电电量(kW·h)" prop="chargeElecQuantity" width="140" align="right">
- <template slot-scope="scope"><span class="charge-value">{{
- formatNumber(scope.row.chargeElecQuantity)
- }}</span></template>
- </el-table-column>
- <el-table-column label="放电电量(kW·h)" prop="dischargeElecQuantity" width="140" align="right">
- <template slot-scope="scope"><span class="discharge-value"
- >{{ formatNumber(scope.row.dischargeElecQuantity) }}</span></template>
- </el-table-column>
- </el-table>
- <div v-else class="detail-chart-wrapper" v-loading="chargeAnalysis.tableLoading">
- <div ref="chargeDetailChartRef" class="detail-chart-canvas"></div>
- </div>
- </div>
- </div>
- </div>
- </el-col>
- </el-row>
- </div>
- </template>
- <script>
- import { listElecStoreHour, listElecStoreDay, getDayStatistics } from '@/api/mgr/elecStoreHour'
- import { areaTreeByFacsCategory } from '@/api/basecfg/area'
- import { listAllFacs } from '@/api/basecfg/emsfacs'
- import * as echarts from 'echarts'
- export default {
- name: 'EnergyStorageAnalysis',
- data() {
- const today = new Date()
- const lastWeek = new Date()
- lastWeek.setDate(lastWeek.getDate() - 7)
- return {
- searchKeyword: '',
- treeData: [],
- treeProps: { children: 'children', label: 'label' },
- defaultExpandedKeys: ['-1'],
- selectedLabel: '全部',
- selectedNode: null,
- queryParams: { areaCode: null, facsCode: null },
- activeTab: 'charge',
- areaMap: {},
- facsMap: {},
- facsList: [], // 新增:设施列表
- chargeAnalysis: {
- timeDimension: 'day',
- selectedDate: this.formatDate(today),
- dateRange: [this.formatDate(lastWeek), this.formatDate(today)],
- chartType: 'bar',
- viewMode: 'table',
- summaryLoading: false,
- chartLoading: false,
- tableLoading: false,
- summary: { totalCharge: 0, totalDischarge: 0, netCharge: 0 },
- tableData: [],
- chartData: [],
- selectedFacs: '' // 新增:选中的设施
- },
- chargeChartInstance: null,
- chargeDetailChartInstance: null,
- datePickerOptions: {
- shortcuts: [{
- text: '最近一周', onClick(picker) {
- const end = new Date()
- const start = new Date()
- start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
- picker.$emit('pick', [start, end])
- }
- }, {
- text: '最近一个月', onClick(picker) {
- const end = new Date()
- const start = new Date()
- start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
- picker.$emit('pick', [start, end])
- }
- }]
- },
- datetimePickerOptions: {
- shortcuts: [{
- text: '最近24小时', onClick(picker) {
- const end = new Date()
- const start = new Date()
- start.setTime(start.getTime() - 24 * 60 * 60 * 1000)
- picker.$emit('pick', [start, end])
- }
- }, {
- text: '最近3天', onClick(picker) {
- const end = new Date()
- const start = new Date()
- start.setTime(start.getTime() - 3 * 24 * 60 * 60 * 1000)
- picker.$emit('pick', [start, end])
- }
- }]
- }
- }
- },
- computed: {
- // 新增:根据选中的区域过滤设施列表
- filteredFacsList() {
- if (!this.queryParams.areaCode) {
- // 未选择区域时,显示所有设施
- return this.facsList
- }
- // 选择了区域时,只显示该区域下的设施
- return this.facsList.filter(facs => facs.refArea === this.queryParams.areaCode)
- }
- },
- watch: {
- 'chargeAnalysis.chartType'() {
- this.renderChargeChart()
- },
- 'chargeAnalysis.viewMode'(val) {
- if (val === 'chart') {
- this.$nextTick(() => {
- this.renderChargeDetailChart()
- })
- }
- }
- },
- created() {
- this.initBaseData()
- },
- mounted() {
- window.addEventListener('resize', this.handleResize)
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.handleResize)
- this.disposeCharts()
- },
- methods: {
- async initBaseData() {
- try {
- await Promise.all([this.loadTreeData(), this.loadFacsList()])
- this.loadChargeAnalysisData()
- } catch (error) {
- console.error('初始化数据失败', error)
- }
- },
- async loadFacsList() {
- try {
- const { data } = await listAllFacs({ facsCategory: 'C' })
- this.facsList = data || [] // 保存设施列表用于下拉筛选
- this.facsMap = {}
- this.facsList.forEach(facs => {
- this.facsMap[facs.facsCode] = facs.facsName
- })
- } catch (error) {
- console.error('加载设施列表失败', error)
- }
- },
- 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 })
- },
- formatDate(date) {
- if (!date) return ''
- const d = new Date(date)
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
- },
- formatTime(time) {
- if (!time) return '-'
- if (typeof time === 'string') {
- return time.includes(' ') ? time.split(' ')[1] : time
- }
- const d = new Date(time)
- return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
- },
- formatHourTime(time) {
- if (!time) return '-'
- return typeof time === 'string' ? time.substring(0, 5) : time
- },
- getAreaName(areaCode) {
- return !areaCode ? '-' : (this.areaMap[areaCode] || areaCode)
- },
- getFacsName(facsCode) {
- return !facsCode ? '-' : (this.facsMap[facsCode] || facsCode)
- },
- async loadTreeData() {
- try {
- const response = await areaTreeByFacsCategory('C', '', false)
- this.treeData = [{ id: '-1', label: '全部', children: response.data || [] }]
- this.defaultExpandedKeys = ['-1']
- this.buildAreaMap(response.data || [])
- this.$nextTick(() => {
- if (this.$refs.treeRef) {
- this.$refs.treeRef.setCurrentKey('-1')
- }
- })
- } catch (error) {
- console.error('加载树形数据失败', 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)
- }
- })
- },
- getTreeIcon(data) {
- if (data.facsCategory === 'C') return 'el-icon-s-grid'
- if (data.id === '-1') return 'el-icon-s-home'
- return 'el-icon-office-building'
- },
- filterTree() {
- if (this.$refs.treeRef) {
- this.$refs.treeRef.filter(this.searchKeyword)
- }
- },
- filterNode(value, data) {
- if (!value) return true
- return data.label && data.label.indexOf(value) !== -1
- },
- handleNodeClick(data) {
- this.selectedNode = data
- // 重置设施筛选
- this.chargeAnalysis.selectedFacs = ''
- if (data.id === '-1') {
- this.queryParams.areaCode = null
- this.queryParams.facsCode = null
- this.selectedLabel = '全部'
- } else if (data.facsCategory === 'C') {
- this.queryParams.facsCode = data.id
- this.queryParams.areaCode = null
- this.selectedLabel = data.label
- // 如果点击的是设施节点,自动选中该设施
- this.chargeAnalysis.selectedFacs = data.id
- } else {
- this.queryParams.areaCode = data.id
- this.queryParams.facsCode = null
- this.selectedLabel = data.label
- }
- this.refreshCurrentTabData()
- },
- handleTabChange() {
- this.refreshCurrentTabData()
- },
- refreshCurrentTabData() {
- if (this.activeTab === 'charge') {
- this.loadChargeAnalysisData()
- }
- },
- handleChargeTimeChange() {
- this.loadChargeAnalysisData()
- },
- async loadChargeAnalysisData() {
- this.chargeAnalysis.summaryLoading = true
- this.chargeAnalysis.tableLoading = true
- this.chargeAnalysis.chartLoading = true
- try {
- if (this.chargeAnalysis.timeDimension === 'hour') {
- await this.loadHourlyChargeData()
- } else {
- await this.loadDailyChargeData()
- }
- this.$nextTick(() => {
- this.renderChargeChart()
- if (this.chargeAnalysis.viewMode === 'chart') {
- this.renderChargeDetailChart()
- }
- })
- } catch (error) {
- console.error('加载充放电分析数据失败', error)
- this.$message.error('加载数据失败')
- } finally {
- this.chargeAnalysis.summaryLoading = false
- this.chargeAnalysis.tableLoading = false
- this.chargeAnalysis.chartLoading = false
- }
- },
- async loadHourlyChargeData() {
- const date = this.chargeAnalysis.selectedDate
- if (!date) return
- const { data: statisticsData } = await getDayStatistics(date)
- if (statisticsData && statisticsData.daySum) {
- this.chargeAnalysis.summary = {
- totalCharge: parseFloat(statisticsData.daySum.chargeElecQuantity) || 0,
- totalDischarge: parseFloat(statisticsData.daySum.dischargeElecQuantity) || 0,
- netCharge: (parseFloat(statisticsData.daySum.chargeElecQuantity) || 0) - (parseFloat(statisticsData.daySum.dischargeElecQuantity) || 0)
- }
- }
- const params = {
- date: date,
- areaCode: this.queryParams.areaCode || undefined,
- facsCode: this.queryParams.facsCode || undefined
- }
- // 新增:如果选择了设施筛选,优先使用筛选的设施
- if (this.chargeAnalysis.selectedFacs) {
- params.facsCode = this.chargeAnalysis.selectedFacs
- }
- const { rows } = await listElecStoreHour(params)
- this.chargeAnalysis.tableData = (rows || []).map(item => ({
- ...item,
- areaName: item.areaName || this.getAreaName(item.areaCode),
- facsName: item.facsName || this.getFacsName(item.facsCode)
- }))
- if (statisticsData && statisticsData.hourSum) {
- this.chargeAnalysis.chartData = statisticsData.hourSum.map(item => ({
- time: item.time,
- chargeElecQuantity: parseFloat(item.chargeElecQuantity) || 0,
- dischargeElecQuantity: parseFloat(item.dischargeElecQuantity) || 0
- }))
- }
- },
- async loadDailyChargeData() {
- const [startDate, endDate] = this.chargeAnalysis.dateRange || []
- if (!startDate || !endDate) return
- const params = {
- startRecTime: startDate,
- endRecTime: endDate,
- areaCode: this.queryParams.areaCode || '-1',
- facsSubCategory: ''
- }
- // 新增:如果选择了设施,添加facsCode参数
- if (this.chargeAnalysis.selectedFacs) {
- params.facsCode = this.chargeAnalysis.selectedFacs
- }
- const { data } = await listElecStoreDay(params)
- const list = data || []
- this.chargeAnalysis.tableData = list.map(item => ({
- ...item,
- areaName: item.areaName || this.getAreaName(item.areaCode),
- facsName: item.facsName || this.getFacsName(item.facsCode)
- }))
- this.chargeAnalysis.chartData = list
- let totalCharge = 0
- let totalDischarge = 0
- list.forEach(item => {
- totalCharge += parseFloat(item.chargeElecQuantity) || 0
- totalDischarge += parseFloat(item.dischargeElecQuantity) || 0
- })
- this.chargeAnalysis.summary = { totalCharge, totalDischarge, netCharge: totalCharge - totalDischarge }
- },
- renderChargeChart() {
- if (!this.$refs.chargeChartRef || this.chargeAnalysis.chartData.length === 0) return
- if (this.chargeChartInstance) {
- this.chargeChartInstance.dispose()
- }
- this.chargeChartInstance = echarts.init(this.$refs.chargeChartRef)
- const isHourly = this.chargeAnalysis.timeDimension === 'hour'
- const xAxisData = this.chargeAnalysis.chartData.map(item => isHourly ? this.formatHourTime(item.time) : item.date)
- const chargeData = this.chargeAnalysis.chartData.map(item => parseFloat(item.chargeElecQuantity) || 0)
- const dischargeData = this.chargeAnalysis.chartData.map(item => parseFloat(item.dischargeElecQuantity) || 0)
- const option = {
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(50, 50, 50, 0.9)',
- borderColor: 'transparent',
- textStyle: { color: '#fff' }
- },
- legend: { data: ['充电电量', '放电电量'], top: 10 },
- grid: { left: '3%', right: '4%', bottom: '15%', top: '15%', containLabel: true },
- xAxis: {
- type: 'category',
- data: xAxisData,
- axisLine: { lineStyle: { color: '#e0e0e0' } },
- axisTick: { show: false },
- axisLabel: { color: '#666', fontSize: 11, rotate: xAxisData.length > 15 ? 45 : 0 }
- },
- yAxis: {
- type: 'value',
- name: 'kW·h',
- nameTextStyle: { color: '#999', fontSize: 11 },
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } }
- },
- dataZoom: xAxisData.length > 20 ? [{ type: 'slider', start: 0, end: 80, height: 20, bottom: '2%' }] : [],
- series: [{
- name: '充电电量',
- type: this.chargeAnalysis.chartType,
- data: chargeData,
- smooth: true,
- itemStyle: { color: '#67c23a', borderRadius: this.chargeAnalysis.chartType === 'bar' ? [4, 4, 0, 0] : 0 },
- lineStyle: this.chargeAnalysis.chartType === 'line' ? { width: 3, color: '#67c23a' } : undefined,
- barWidth: '35%',
- barMaxWidth: 40
- }, {
- name: '放电电量',
- type: this.chargeAnalysis.chartType,
- data: dischargeData,
- smooth: true,
- itemStyle: { color: '#e6a23c', borderRadius: this.chargeAnalysis.chartType === 'bar' ? [4, 4, 0, 0] : 0 },
- lineStyle: this.chargeAnalysis.chartType === 'line' ? { width: 3, color: '#e6a23c' } : undefined,
- barWidth: '35%',
- barMaxWidth: 40
- }]
- }
- this.chargeChartInstance.setOption(option)
- },
- renderChargeDetailChart() {
- if (!this.$refs.chargeDetailChartRef || this.chargeAnalysis.tableData.length === 0) return
- if (this.chargeDetailChartInstance) {
- this.chargeDetailChartInstance.dispose()
- }
- this.chargeDetailChartInstance = echarts.init(this.$refs.chargeDetailChartRef)
- const aggregatedMap = new Map()
- this.chargeAnalysis.tableData.forEach(item => {
- const key = item.facsName || item.areaName || '未知'
- if (aggregatedMap.has(key)) {
- const existing = aggregatedMap.get(key)
- existing.charge += parseFloat(item.chargeElecQuantity) || 0
- existing.discharge += parseFloat(item.dischargeElecQuantity) || 0
- } else {
- aggregatedMap.set(key, {
- name: key,
- charge: parseFloat(item.chargeElecQuantity) || 0,
- discharge: parseFloat(item.dischargeElecQuantity) || 0
- })
- }
- })
- const aggregatedData = Array.from(aggregatedMap.values())
- const option = {
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(50, 50, 50, 0.9)',
- borderColor: 'transparent',
- textStyle: { color: '#fff' }
- },
- legend: { data: ['充电电量', '放电电量'], top: 10 },
- grid: { left: '3%', right: '4%', bottom: '10%', top: '15%', containLabel: true },
- xAxis: {
- type: 'category',
- data: aggregatedData.map(item => item.name),
- axisLine: { lineStyle: { color: '#e0e0e0' } },
- axisTick: { show: false },
- axisLabel: { color: '#666', fontSize: 11 }
- },
- yAxis: {
- type: 'value',
- name: 'kW·h',
- nameTextStyle: { color: '#999', fontSize: 11 },
- axisLine: { show: false },
- axisTick: { show: false },
- splitLine: { lineStyle: { color: '#f0f0f0', type: 'dashed' } }
- },
- series: [{
- name: '充电电量',
- type: 'bar',
- data: aggregatedData.map(item => item.charge),
- itemStyle: { color: '#67c23a', borderRadius: [4, 4, 0, 0] },
- barWidth: '30%'
- }, {
- name: '放电电量',
- type: 'bar',
- data: aggregatedData.map(item => item.discharge),
- itemStyle: { color: '#e6a23c', borderRadius: [4, 4, 0, 0] },
- barWidth: '30%'
- }]
- }
- this.chargeDetailChartInstance.setOption(option)
- },
- getChargeSummaries({ columns, data }) {
- const sums = []
- columns.forEach((column, index) => {
- if (index === 0) {
- sums[index] = '合计'
- return
- }
- if (column.property === 'chargeElecQuantity') {
- sums[index] = this.formatNumber(data.reduce((sum, item) => sum + (parseFloat(item.chargeElecQuantity) || 0), 0))
- } else if (column.property === 'dischargeElecQuantity') {
- sums[index] = this.formatNumber(data.reduce((sum, item) => sum + (parseFloat(item.dischargeElecQuantity) || 0), 0))
- } else {
- sums[index] = ''
- }
- })
- return sums
- },
- handleResize() {
- if (this.chargeChartInstance) this.chargeChartInstance.resize()
- if (this.chargeDetailChartInstance) this.chargeDetailChartInstance.resize()
- },
- disposeCharts() {
- [this.chargeChartInstance, this.chargeDetailChartInstance].forEach(chart => {
- if (chart) {
- chart.dispose()
- }
- })
- this.chargeChartInstance = null
- this.chargeDetailChartInstance = null
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .energy-storage-container {
- padding: 20px;
- background: #f5f7fa;
- min-height: calc(100vh - 84px);
- .head-container {
- background: #fff;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 15px;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
- &.tree-container {
- max-height: calc(100vh - 200px);
- overflow-y: auto;
- &::-webkit-scrollbar {
- width: 6px;
- }
- &::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 3px;
- }
- &::-webkit-scrollbar-thumb {
- background: #c1c1c1;
- border-radius: 3px;
- }
- ::v-deep .el-tree {
- background: transparent;
- .el-tree-node__content {
- height: 40px;
- padding: 0 8px;
- &:hover {
- background-color: #f5f7fa;
- }
- }
- .el-tree-node.is-current > .el-tree-node__content {
- background-color: #ecf5ff;
- color: #409eff;
- .tree-icon {
- color: #409eff !important;
- }
- }
- .custom-tree-node {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 14px;
- .tree-label {
- display: flex;
- align-items: center;
- .tree-icon {
- margin-right: 8px;
- font-size: 16px;
- color: #909399;
- }
- }
- .tree-tag.storage-tag {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #fff;
- border: none;
- }
- }
- }
- }
- }
- .main-content {
- background: #fff;
- border-radius: 12px;
- padding: 24px;
- box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
- min-height: calc(100vh - 124px);
- .content-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24px;
- padding-bottom: 20px;
- border-bottom: 2px solid #f0f2f5;
- .page-title {
- font-size: 22px;
- font-weight: 700;
- color: #303133;
- margin: 0;
- display: flex;
- align-items: center;
- i {
- margin-right: 12px;
- color: #409eff;
- font-size: 26px;
- }
- .title-divider {
- margin: 0 16px;
- color: #dcdfe6;
- font-weight: 300;
- }
- .current-selection {
- color: #409eff;
- font-size: 18px;
- font-weight: 500;
- }
- }
- .header-actions {
- ::v-deep .el-radio-button__inner {
- padding: 10px 20px;
- font-weight: 500;
- i {
- margin-right: 6px;
- }
- }
- ::v-deep .el-radio-button__orig-radio:checked + .el-radio-button__inner {
- background: #409eff;
- border-color: #409eff;
- }
- }
- }
- .tab-content {
- animation: fadeIn 0.3s ease;
- }
- .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;
- }
- }
- }
- .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;
- &.positive {
- color: #67c23a;
- }
- &.negative {
- color: #f56c6c;
- }
- small {
- font-size: 13px;
- font-weight: 400;
- color: #909399;
- margin-left: 4px;
- }
- }
- }
- &.charge-summary-card {
- &::before {
- background: #67c23a;
- }
- .card-icon {
- background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
- }
- }
- &.discharge-summary-card {
- &::before {
- background: #e6a23c;
- }
- .card-icon {
- background: linear-gradient(135deg, #e6a23c 0%, #f5c069 100%);
- }
- }
- &.net-card {
- &::before {
- background: #667eea;
- }
- .card-icon {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- }
- }
- &.avg-card {
- &::before {
- background: #409eff;
- }
- .card-icon {
- background: linear-gradient(135deg, #409eff 0%, #53a8ff 100%);
- }
- }
- &.max-card {
- &::before {
- background: #f56c6c;
- }
- .card-icon {
- background: linear-gradient(135deg, #f56c6c 0%, #f89898 100%);
- }
- }
- &.min-card {
- &::before {
- background: #00d4ff;
- }
- .card-icon {
- background: linear-gradient(135deg, #00d4ff 0%, #00f2fe 100%);
- }
- }
- &.count-card {
- &::before {
- background: #f093fb;
- }
- .card-icon {
- background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
- }
- }
- }
- .summary-cards {
- margin-bottom: 24px;
- }
- .filter-bar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- padding: 16px 20px;
- background: #f5f7fa;
- border-radius: 10px;
- .filter-left {
- display: flex;
- align-items: center;
- .filter-label {
- font-size: 14px;
- color: #606266;
- margin-right: 12px;
- }
- }
- .filter-right {
- display: flex;
- align-items: center;
- gap: 12px;
- .facs-select {
- width: 160px;
- }
- .date-picker {
- width: 240px;
- }
- .datetime-picker {
- width: 360px;
- }
- .device-select {
- width: 180px;
- }
- }
- }
- .chart-section {
- margin-bottom: 24px;
- .chart-wrapper {
- background: #fff;
- border-radius: 8px;
- border: 1px solid #ebeef5;
- padding: 16px;
- .chart-canvas {
- height: 320px;
- width: 100%;
- &.capacity-chart {
- height: 350px;
- }
- }
- }
- .chart-controls {
- ::v-deep .el-radio-button__inner {
- padding: 6px 12px;
- }
- }
- }
- .table-section {
- .data-table {
- border-radius: 8px;
- ::v-deep .el-table__header th {
- background: #f5f7fa;
- color: #606266;
- font-weight: 600;
- }
- .charge-value {
- color: #67c23a;
- font-weight: 600;
- }
- .discharge-value {
- color: #e6a23c;
- font-weight: 600;
- }
- }
- .detail-chart-wrapper {
- background: #fff;
- border-radius: 8px;
- padding: 16px;
- border: 1px solid #ebeef5;
- .detail-chart-canvas {
- height: 350px;
- width: 100%;
- }
- }
- }
- }
- }
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
- @media (max-width: 768px) {
- .energy-storage-container {
- padding: 10px;
- .main-content {
- padding: 16px;
- .content-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 16px;
- }
- .filter-bar {
- flex-direction: column;
- gap: 12px;
- .filter-right {
- flex-wrap: wrap;
- width: 100%;
- .facs-select,
- .date-picker,
- .datetime-picker,
- .device-select {
- width: 100%;
- }
- }
- }
- }
- }
- }
- </style>
|