2025-09-28 14:38:41 +08:00
|
|
|
<template>
|
2026-04-30 13:06:14 +08:00
|
|
|
<div class="ems-dashboard-container" v-loading="loading">
|
|
|
|
|
<!-- 顶部筛选区域 -->
|
|
|
|
|
<div class="filter-panel">
|
|
|
|
|
<el-form :inline="true" :model="queryParams" class="filter-form">
|
|
|
|
|
<el-form-item label="时间范围">
|
2026-05-07 11:07:49 +08:00
|
|
|
<time-range-picker v-model="timeRangeParams" start-key="recordStartDate" end-key="recordEndDate"
|
|
|
|
|
:default-start-time="defaultStartTime" :default-end-time="defaultEndTime" @quick-select="loadData" />
|
2026-04-30 13:06:14 +08:00
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button type="primary" icon="el-icon-refresh" @click="loadData">刷新</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 图表区域 -->
|
|
|
|
|
<div class="charts-section">
|
2026-05-07 11:07:49 +08:00
|
|
|
<!-- 产线能源类型二维表 -->
|
2026-04-30 13:06:14 +08:00
|
|
|
<el-row :gutter="20">
|
2026-05-07 11:07:49 +08:00
|
|
|
<el-col :span="24">
|
2026-04-30 13:06:14 +08:00
|
|
|
<div class="chart-card">
|
|
|
|
|
<div class="chart-header">
|
2026-05-07 11:07:49 +08:00
|
|
|
<span class="chart-title">产线能源类型二维表</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<el-table :data="tableData" border style="width: 100%" :summary-method="getSummaries" show-summary>
|
|
|
|
|
<el-table-column prop="lineName" label="产线"></el-table-column>
|
|
|
|
|
<el-table-column v-for="type in energyTypeList" :key="type.energyTypeId"
|
|
|
|
|
:prop="'type_' + type.energyTypeId" :label="type.name">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row['type_' + type.energyTypeId] ? Number(scope.row['type_' +
|
|
|
|
|
type.energyTypeId]).toFixed(2)
|
|
|
|
|
: 0 }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
2026-05-07 11:07:49 +08:00
|
|
|
</el-row>
|
2026-04-30 13:06:14 +08:00
|
|
|
|
2026-05-07 11:07:49 +08:00
|
|
|
<el-row :gutter="20" style="margin-top: 20px">
|
|
|
|
|
<!-- 产线能源时序分析 -->
|
|
|
|
|
<el-col :span="24">
|
2026-04-30 13:06:14 +08:00
|
|
|
<div class="chart-card">
|
2026-05-07 11:07:49 +08:00
|
|
|
<div class="chart-header" style="flex-wrap: wrap; gap: 12px;">
|
|
|
|
|
<span class="chart-title">产线能源时序分析</span>
|
|
|
|
|
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
|
|
|
|
<el-select v-model="selectedLineAnalysisEnergyTypeId" placeholder="请选择能源类型"
|
|
|
|
|
@change="updateLineEnergyTrendChart" style="width: 150px;">
|
|
|
|
|
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name"
|
|
|
|
|
:value="item.energyTypeId">
|
|
|
|
|
</el-option>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-select v-model="selectedLines" multiple placeholder="请选择产线" @change="updateLineEnergyTrendChart"
|
|
|
|
|
style="width: 200px;">
|
|
|
|
|
<el-option v-for="line in productionLines" :key="line.value" :label="line.label" :value="line.value">
|
|
|
|
|
</el-option>
|
|
|
|
|
</el-select>
|
|
|
|
|
</div>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
2026-05-07 11:07:49 +08:00
|
|
|
<div ref="lineEnergyTrendChart" class="chart-container"></div>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 20px">
|
2026-05-07 11:07:49 +08:00
|
|
|
<!-- 能源消耗趋势图 -->
|
|
|
|
|
<el-col :span="16">
|
2026-04-30 13:06:14 +08:00
|
|
|
<div class="chart-card">
|
|
|
|
|
<div class="chart-header">
|
2026-05-07 11:07:49 +08:00
|
|
|
<span class="chart-title">能源消耗趋势</span>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
2026-05-07 11:07:49 +08:00
|
|
|
<div ref="trendChart" class="chart-container"></div>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
|
|
|
|
</el-col>
|
2025-09-28 14:38:41 +08:00
|
|
|
|
2026-05-07 11:07:49 +08:00
|
|
|
<!-- 能源产线占比 -->
|
|
|
|
|
<el-col :span="8">
|
2026-04-30 13:06:14 +08:00
|
|
|
<div class="chart-card">
|
|
|
|
|
<div class="chart-header">
|
2026-05-07 11:07:49 +08:00
|
|
|
<span class="chart-title">能源产线占比</span>
|
|
|
|
|
<el-select v-model="selectedEnergyTypeId" placeholder="请选择能源" @change="updateEnergyTypeChart"
|
|
|
|
|
style="width: 150px;">
|
|
|
|
|
<el-option v-for="item in energyTypeList" :key="item.energyTypeId" :label="item.name"
|
|
|
|
|
:value="item.energyTypeId">
|
|
|
|
|
</el-option>
|
|
|
|
|
</el-select>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
2026-05-07 11:07:49 +08:00
|
|
|
<div ref="energyTypeChart" class="chart-container"></div>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
2026-05-07 11:07:49 +08:00
|
|
|
<!-- 能源流向桑基图 -->
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 20px;">
|
2026-04-30 13:06:14 +08:00
|
|
|
<el-col :span="24">
|
|
|
|
|
<div class="chart-card">
|
2026-05-07 11:07:49 +08:00
|
|
|
<div class="chart-header" style="flex-wrap: wrap; gap: 12px;">
|
|
|
|
|
<span class="chart-title">能源流向</span>
|
|
|
|
|
<el-select v-model="selectedSankeyEnergyTypeId" placeholder="请选择能源类型" @change="updateSankeyChart"
|
|
|
|
|
style="width: 200px;">
|
|
|
|
|
<el-option v-for="type in getValidSankeyEnergyTypes()" :key="type.energyTypeId" :label="type.name"
|
|
|
|
|
:value="type.energyTypeId">
|
|
|
|
|
</el-option>
|
|
|
|
|
</el-select>
|
|
|
|
|
<el-alert v-if="getValidSankeyEnergyTypes().length === 0" title="暂无可用能源类型(需要能源类型有且仅有一个主表)" type="warning"
|
|
|
|
|
:closable="false" size="small" style="flex: 1; max-width: 500px; margin-left: auto;"></el-alert>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
2026-05-07 11:07:49 +08:00
|
|
|
<div ref="sankeyChart" class="chart-container"></div>
|
2026-04-30 13:06:14 +08:00
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-09-28 14:38:41 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-04-30 13:06:14 +08:00
|
|
|
import { listEnergyType } from '@/api/ems/energyType'
|
|
|
|
|
import { listMeter } from '@/api/ems/meter'
|
|
|
|
|
import { listEnergyRecord } from '@/api/ems/energyRecord'
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import * as echarts from 'echarts'
|
|
|
|
|
import TimeRangePicker from '@/views/wms/report/components/timeRangePicker.vue'
|
2025-09-28 14:38:41 +08:00
|
|
|
|
|
|
|
|
export default {
|
2026-04-30 13:06:14 +08:00
|
|
|
name: 'EmsDashboard',
|
|
|
|
|
dicts: ['sys_lines'],
|
2025-09-28 14:38:41 +08:00
|
|
|
components: {
|
2026-04-30 13:06:14 +08:00
|
|
|
TimeRangePicker
|
2025-09-28 14:38:41 +08:00
|
|
|
},
|
|
|
|
|
data() {
|
2026-04-30 13:06:14 +08:00
|
|
|
const now = dayjs()
|
2025-09-28 14:38:41 +08:00
|
|
|
return {
|
2026-04-30 13:06:14 +08:00
|
|
|
timeRangeParams: {
|
|
|
|
|
recordStartDate: now.startOf('month').format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
recordEndDate: now.endOf('month').format('YYYY-MM-DD HH:mm:ss')
|
2025-09-28 14:38:41 +08:00
|
|
|
},
|
2026-04-30 13:06:14 +08:00
|
|
|
defaultStartTime: now.startOf('month').format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
defaultEndTime: now.endOf('month').format('YYYY-MM-DD HH:mm:ss'),
|
|
|
|
|
queryParams: {
|
|
|
|
|
energyTypeId: null,
|
|
|
|
|
productionLines: []
|
|
|
|
|
},
|
|
|
|
|
energyTypeList: [],
|
2026-05-07 11:07:49 +08:00
|
|
|
productionLines: [],
|
|
|
|
|
selectedEnergyTypeId: null,
|
|
|
|
|
selectedLineAnalysisEnergyTypeId: null,
|
|
|
|
|
selectedSankeyEnergyTypeId: null,
|
|
|
|
|
selectedLines: [],
|
2026-04-30 13:06:14 +08:00
|
|
|
meterList: [],
|
|
|
|
|
energyRecords: [],
|
|
|
|
|
statistics: [],
|
|
|
|
|
tableData: [],
|
|
|
|
|
trendChartType: 'day',
|
|
|
|
|
trendChart: null,
|
|
|
|
|
energyTypeChart: null,
|
2026-05-07 11:07:49 +08:00
|
|
|
lineEnergyTrendChart: null,
|
|
|
|
|
sankeyChart: null,
|
2026-04-30 13:06:14 +08:00
|
|
|
loading: false,
|
2025-09-28 14:38:41 +08:00
|
|
|
}
|
|
|
|
|
},
|
2026-04-30 13:06:14 +08:00
|
|
|
mounted() {
|
|
|
|
|
this.initCharts()
|
|
|
|
|
this.loadBasicData()
|
|
|
|
|
window.addEventListener('resize', this.handleResize)
|
2025-09-28 14:38:41 +08:00
|
|
|
},
|
2026-04-30 13:06:14 +08:00
|
|
|
beforeDestroy() {
|
|
|
|
|
window.removeEventListener('resize', this.handleResize)
|
|
|
|
|
if (this.trendChart) this.trendChart.dispose()
|
|
|
|
|
if (this.energyTypeChart) this.energyTypeChart.dispose()
|
2026-05-07 11:07:49 +08:00
|
|
|
if (this.lineEnergyTrendChart) this.lineEnergyTrendChart.dispose()
|
|
|
|
|
if (this.sankeyChart) this.sankeyChart.dispose()
|
2025-09-28 14:38:41 +08:00
|
|
|
},
|
|
|
|
|
methods: {
|
2026-04-30 13:06:14 +08:00
|
|
|
initCharts() {
|
|
|
|
|
this.trendChart = echarts.init(this.$refs.trendChart)
|
|
|
|
|
this.energyTypeChart = echarts.init(this.$refs.energyTypeChart)
|
2026-05-07 11:07:49 +08:00
|
|
|
this.lineEnergyTrendChart = echarts.init(this.$refs.lineEnergyTrendChart)
|
|
|
|
|
this.sankeyChart = echarts.init(this.$refs.sankeyChart)
|
2026-04-30 13:06:14 +08:00
|
|
|
},
|
|
|
|
|
handleResize() {
|
|
|
|
|
this.trendChart && this.trendChart.resize()
|
|
|
|
|
this.energyTypeChart && this.energyTypeChart.resize()
|
2026-05-07 11:07:49 +08:00
|
|
|
this.lineEnergyTrendChart && this.lineEnergyTrendChart.resize()
|
|
|
|
|
this.sankeyChart && this.sankeyChart.resize()
|
2026-04-30 13:06:14 +08:00
|
|
|
},
|
|
|
|
|
async loadBasicData() {
|
|
|
|
|
try {
|
|
|
|
|
this.loading = true;
|
|
|
|
|
const [energyTypeRes, meterRes] = await Promise.all([
|
|
|
|
|
listEnergyType({ pageSize: 999 }),
|
2026-05-07 11:07:49 +08:00
|
|
|
listMeter({ pageSize: 999 })
|
2026-04-30 13:06:14 +08:00
|
|
|
])
|
|
|
|
|
this.energyTypeList = energyTypeRes.rows || []
|
2026-05-07 11:07:49 +08:00
|
|
|
if (this.energyTypeList.length > 0) {
|
|
|
|
|
this.selectedEnergyTypeId = this.energyTypeList[0].energyTypeId
|
|
|
|
|
this.selectedLineAnalysisEnergyTypeId = this.energyTypeList[0].energyTypeId
|
|
|
|
|
}
|
2026-04-30 13:06:14 +08:00
|
|
|
this.meterList = meterRes.rows || []
|
2026-05-07 11:07:49 +08:00
|
|
|
this.productionLines = this.dict.type.sys_lines || []
|
|
|
|
|
if (this.productionLines.length > 0) {
|
|
|
|
|
this.selectedLines = this.productionLines.slice(0, 3).map(l => l.value)
|
|
|
|
|
}
|
|
|
|
|
// 设置默认的桑基图能源类型(筛选有且只有一个主表的能源)
|
|
|
|
|
const validSankeyEnergyTypes = this.getValidSankeyEnergyTypes()
|
|
|
|
|
if (validSankeyEnergyTypes.length > 0) {
|
|
|
|
|
this.selectedSankeyEnergyTypeId = validSankeyEnergyTypes[0].energyTypeId
|
|
|
|
|
}
|
2026-04-30 13:06:14 +08:00
|
|
|
await this.loadData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载基础数据失败', error)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async loadData() {
|
|
|
|
|
try {
|
|
|
|
|
const query = {
|
|
|
|
|
recordStartDate: this.timeRangeParams.recordStartDate,
|
|
|
|
|
recordEndDate: this.timeRangeParams.recordEndDate
|
|
|
|
|
}
|
|
|
|
|
if (this.queryParams.energyTypeId) {
|
|
|
|
|
query.energyTypeId = this.queryParams.energyTypeId
|
|
|
|
|
}
|
|
|
|
|
this.loading = true;
|
|
|
|
|
const res = await listEnergyRecord(query)
|
|
|
|
|
this.energyRecords = res.rows || []
|
|
|
|
|
this.processData()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载数据失败', error)
|
|
|
|
|
} finally {
|
|
|
|
|
this.loading = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
processData() {
|
|
|
|
|
const validMeterIds = new Set(this.meterList.map(m => m.meterId))
|
|
|
|
|
this.energyRecords = this.energyRecords.filter(record => validMeterIds.has(record.meterId))
|
|
|
|
|
this.calculateStatistics()
|
|
|
|
|
this.updateTrendChart()
|
|
|
|
|
this.updateEnergyTypeChart()
|
2026-05-07 11:07:49 +08:00
|
|
|
this.updateLineEnergyTrendChart()
|
|
|
|
|
this.updateSankeyChart()
|
2026-04-30 13:06:14 +08:00
|
|
|
this.updateTableData()
|
|
|
|
|
},
|
|
|
|
|
calculateStatistics() {
|
|
|
|
|
const totalConsumption = this.energyRecords.reduce((sum, record) => sum + (Number(record.consumption) || 0), 0)
|
|
|
|
|
const avgDaily = this.getAvgDailyConsumption()
|
|
|
|
|
const maxConsumption = this.getMaxConsumption()
|
|
|
|
|
const deviceCount = new Set(this.energyRecords.map(r => r.meterId)).size
|
|
|
|
|
|
|
|
|
|
this.statistics = [
|
|
|
|
|
{
|
|
|
|
|
label: '总能耗',
|
|
|
|
|
value: Number(totalConsumption).toFixed(2),
|
|
|
|
|
icon: 'el-icon-data-line',
|
|
|
|
|
trend: 12.5
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '日均能耗',
|
|
|
|
|
value: Number(avgDaily).toFixed(2),
|
|
|
|
|
icon: 'el-icon-data-analysis',
|
|
|
|
|
trend: -3.2
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '最高能耗',
|
|
|
|
|
value: Number(maxConsumption).toFixed(2),
|
|
|
|
|
icon: 'el-icon-s-data',
|
|
|
|
|
trend: 5.1
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: '活跃设备',
|
|
|
|
|
value: deviceCount,
|
|
|
|
|
icon: 'el-icon-s-tools',
|
|
|
|
|
trend: 8.3
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
getAvgDailyConsumption() {
|
|
|
|
|
if (this.energyRecords.length === 0) return 0
|
|
|
|
|
const dateMap = new Map()
|
|
|
|
|
this.energyRecords.forEach(record => {
|
|
|
|
|
const date = record.recordDate
|
|
|
|
|
if (!dateMap.has(date)) {
|
|
|
|
|
dateMap.set(date, 0)
|
|
|
|
|
}
|
|
|
|
|
dateMap.set(date, dateMap.get(date) + (Number(record.consumption) || 0))
|
|
|
|
|
})
|
|
|
|
|
const total = Array.from(dateMap.values()).reduce((sum, val) => sum + val, 0)
|
|
|
|
|
return total / dateMap.size
|
|
|
|
|
},
|
|
|
|
|
getMaxConsumption() {
|
|
|
|
|
if (this.energyRecords.length === 0) return 0
|
|
|
|
|
return Math.max(...this.energyRecords.map(r => Number(r.consumption) || 0))
|
|
|
|
|
},
|
|
|
|
|
updateTrendChart() {
|
|
|
|
|
const dateFormat = this.trendChartType === 'day' ? 'YYYY-MM-DD' :
|
2026-05-07 11:07:49 +08:00
|
|
|
this.trendChartType === 'week' ? 'YYYY-WW' : 'YYYY-MM'
|
|
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
const groupedData = new Map()
|
|
|
|
|
this.energyRecords.forEach(record => {
|
|
|
|
|
const dateKey = dayjs(record.recordDate).format(dateFormat)
|
|
|
|
|
const energyType = this.getEnergyTypeName(record.energyId)
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
if (!groupedData.has(dateKey)) {
|
|
|
|
|
groupedData.set(dateKey, {})
|
|
|
|
|
}
|
|
|
|
|
if (!groupedData.get(dateKey)[energyType]) {
|
|
|
|
|
groupedData.get(dateKey)[energyType] = 0
|
|
|
|
|
}
|
|
|
|
|
groupedData.get(dateKey)[energyType] += Number(record.consumption) || 0
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const dates = Array.from(groupedData.keys()).sort()
|
|
|
|
|
const energyTypes = Array.from(new Set(this.energyRecords.map(r => this.getEnergyTypeName(r.energyId))))
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#00CED1']
|
|
|
|
|
const series = energyTypes.map((type, index) => ({
|
|
|
|
|
name: type,
|
|
|
|
|
type: 'line',
|
|
|
|
|
smooth: true,
|
|
|
|
|
data: dates.map(date => groupedData.get(date)[type] || 0),
|
|
|
|
|
itemStyle: { color: colors[index % colors.length] },
|
|
|
|
|
areaStyle: {
|
|
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
{ offset: 0, color: colors[index % colors.length] + '80' },
|
|
|
|
|
{ offset: 1, color: colors[index % colors.length] + '10' }
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const option = {
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'axis'
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
data: energyTypes
|
|
|
|
|
},
|
|
|
|
|
grid: {
|
|
|
|
|
left: '3%',
|
|
|
|
|
right: '4%',
|
|
|
|
|
bottom: '3%',
|
|
|
|
|
containLabel: true
|
|
|
|
|
},
|
|
|
|
|
xAxis: {
|
|
|
|
|
type: 'category',
|
|
|
|
|
boundaryGap: false,
|
|
|
|
|
data: dates
|
|
|
|
|
},
|
|
|
|
|
yAxis: {
|
|
|
|
|
type: 'value',
|
|
|
|
|
name: '能耗'
|
|
|
|
|
},
|
|
|
|
|
series
|
|
|
|
|
}
|
|
|
|
|
this.trendChart.setOption(option)
|
|
|
|
|
},
|
|
|
|
|
updateEnergyTypeChart() {
|
2026-05-07 11:07:49 +08:00
|
|
|
const lineMap = new Map()
|
|
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
this.energyRecords.forEach(record => {
|
2026-05-07 11:07:49 +08:00
|
|
|
// 只统计选中的能源类型
|
|
|
|
|
if (this.selectedEnergyTypeId && record.energyId !== this.selectedEnergyTypeId) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const meter = this.meterList.find(m => m.meterId === record.meterId)
|
|
|
|
|
if (meter && meter.productionLine) {
|
|
|
|
|
const lines = Array.isArray(meter.productionLine) ? meter.productionLine : [meter.productionLine]
|
|
|
|
|
lines.forEach(lineId => {
|
|
|
|
|
const lineName = this.getLineName(lineId)
|
|
|
|
|
lineMap.set(lineName, (lineMap.get(lineName) || 0) + (Number(record.consumption) || 0))
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-30 13:06:14 +08:00
|
|
|
})
|
|
|
|
|
|
2026-05-07 11:07:49 +08:00
|
|
|
const data = Array.from(lineMap.entries()).map(([name, value]) => ({ name, value }))
|
|
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
const option = {
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'item',
|
|
|
|
|
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
orient: 'vertical',
|
|
|
|
|
right: 10,
|
|
|
|
|
top: 'center'
|
|
|
|
|
},
|
|
|
|
|
series: [
|
|
|
|
|
{
|
2026-05-07 11:07:49 +08:00
|
|
|
name: '产线消耗',
|
2026-04-30 13:06:14 +08:00
|
|
|
type: 'pie',
|
|
|
|
|
radius: ['40%', '70%'],
|
|
|
|
|
avoidLabelOverlap: false,
|
|
|
|
|
itemStyle: {
|
|
|
|
|
borderRadius: 10,
|
|
|
|
|
borderColor: '#fff',
|
|
|
|
|
borderWidth: 2
|
|
|
|
|
},
|
|
|
|
|
label: {
|
|
|
|
|
show: false,
|
|
|
|
|
position: 'center'
|
|
|
|
|
},
|
|
|
|
|
emphasis: {
|
|
|
|
|
label: {
|
|
|
|
|
show: true,
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
fontWeight: 'bold'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
labelLine: {
|
|
|
|
|
show: false
|
|
|
|
|
},
|
|
|
|
|
data
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
this.energyTypeChart.setOption(option)
|
|
|
|
|
},
|
2026-05-07 11:07:49 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
getEnergyTypeName(energyTypeId) {
|
|
|
|
|
const type = this.energyTypeList.find(t => t.energyTypeId === energyTypeId)
|
|
|
|
|
return type ? type.name : '未知'
|
|
|
|
|
},
|
|
|
|
|
getLineName(lineId) {
|
|
|
|
|
const line = this.dict.type.sys_lines?.find(l => l.value === lineId)
|
|
|
|
|
return line ? line.label : lineId
|
|
|
|
|
},
|
|
|
|
|
// 获取有且仅有一个主表的能源类型列表
|
|
|
|
|
getValidSankeyEnergyTypes() {
|
|
|
|
|
const energyTypeMeterMap = {}
|
|
|
|
|
// 按能源类型分组所有表计
|
|
|
|
|
this.meterList.forEach(meter => {
|
|
|
|
|
if (!energyTypeMeterMap[meter.energyTypeId]) {
|
|
|
|
|
energyTypeMeterMap[meter.energyTypeId] = []
|
|
|
|
|
}
|
|
|
|
|
energyTypeMeterMap[meter.energyTypeId].push(meter)
|
|
|
|
|
})
|
|
|
|
|
// 筛选出有且仅有一个主表的能源类型
|
|
|
|
|
return this.energyTypeList.filter(type => {
|
|
|
|
|
const meters = energyTypeMeterMap[type.energyTypeId] || []
|
|
|
|
|
const totalMeters = meters.filter(m => m.isTotalMeter === 1)
|
|
|
|
|
return totalMeters.length === 1
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
// 更新桑基图
|
|
|
|
|
updateSankeyChart() {
|
|
|
|
|
if (!this.selectedSankeyEnergyTypeId) {
|
|
|
|
|
// 如果没有选择能源类型,显示空图表
|
|
|
|
|
this.sankeyChart.setOption({
|
|
|
|
|
title: { text: '请选择能源类型' },
|
|
|
|
|
series: []
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取当前能源类型的所有表计
|
|
|
|
|
const currentEnergyMeters = this.meterList.filter(m => m.energyTypeId === this.selectedSankeyEnergyTypeId)
|
|
|
|
|
const totalMeter = currentEnergyMeters.find(m => m.isTotalMeter === 1)
|
|
|
|
|
const subMeters = currentEnergyMeters.filter(m => m.isTotalMeter !== 1)
|
|
|
|
|
|
|
|
|
|
if (!totalMeter) {
|
|
|
|
|
this.sankeyChart.setOption({
|
|
|
|
|
title: { text: '当前能源类型没有主表' },
|
|
|
|
|
series: []
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算各节点的能耗
|
|
|
|
|
const nodeMap = {}
|
|
|
|
|
const links = []
|
|
|
|
|
const nodes = []
|
|
|
|
|
|
|
|
|
|
// 获取能源类型名称
|
|
|
|
|
const energyType = this.energyTypeList.find(t => t.energyTypeId === this.selectedSankeyEnergyTypeId)
|
|
|
|
|
const energyTypeName = energyType ? energyType.name : '能源'
|
|
|
|
|
|
|
|
|
|
// 1. 能源节点 -> 总表节点
|
|
|
|
|
let totalConsumption = 0
|
|
|
|
|
// 先计算总表的总消耗
|
2026-04-30 13:06:14 +08:00
|
|
|
this.energyRecords.forEach(record => {
|
2026-05-07 11:07:49 +08:00
|
|
|
if (record.meterId === totalMeter.meterId) {
|
|
|
|
|
totalConsumption += Number(record.consumption) || 0
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 添加能源节点
|
|
|
|
|
const energyNodeName = energyTypeName
|
|
|
|
|
nodes.push({ name: energyNodeName })
|
|
|
|
|
nodeMap[energyNodeName] = true
|
|
|
|
|
|
|
|
|
|
// 添加总表节点
|
|
|
|
|
const totalMeterName = `总表:${totalMeter.meterCode}`
|
|
|
|
|
nodes.push({ name: totalMeterName })
|
|
|
|
|
nodeMap[totalMeterName] = true
|
|
|
|
|
|
|
|
|
|
// 能源 -> 总表 连接
|
|
|
|
|
links.push({
|
|
|
|
|
source: energyNodeName,
|
|
|
|
|
target: totalMeterName,
|
|
|
|
|
value: totalConsumption
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 2. 总表 -> 分表/产线节点
|
|
|
|
|
// 按产线分组分表
|
|
|
|
|
const lineSubMeterMap = {}
|
|
|
|
|
subMeters.forEach(meter => {
|
|
|
|
|
const lineName = meter.productionLine ? this.getLineName(meter.productionLine) : '未分配产线'
|
|
|
|
|
if (!lineSubMeterMap[lineName]) {
|
|
|
|
|
lineSubMeterMap[lineName] = []
|
|
|
|
|
}
|
|
|
|
|
lineSubMeterMap[lineName].push(meter)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为每条产线创建节点
|
|
|
|
|
Object.keys(lineSubMeterMap).forEach(lineName => {
|
|
|
|
|
const lineMeters = lineSubMeterMap[lineName]
|
|
|
|
|
let lineConsumption = 0
|
|
|
|
|
|
|
|
|
|
// 计算产线总消耗
|
|
|
|
|
lineMeters.forEach(meter => {
|
|
|
|
|
this.energyRecords.forEach(record => {
|
|
|
|
|
if (record.meterId === meter.meterId) {
|
|
|
|
|
lineConsumption += Number(record.consumption) || 0
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (lineConsumption > 0) {
|
|
|
|
|
// 产线节点
|
|
|
|
|
nodes.push({ name: `产线:${lineName}` })
|
|
|
|
|
nodeMap[`产线:${lineName}`] = true
|
|
|
|
|
links.push({
|
|
|
|
|
source: totalMeterName,
|
|
|
|
|
target: `产线:${lineName}`,
|
|
|
|
|
value: lineConsumption
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为产线下的每个分表创建节点
|
|
|
|
|
lineMeters.forEach(meter => {
|
|
|
|
|
let meterConsumption = 0
|
|
|
|
|
this.energyRecords.forEach(record => {
|
|
|
|
|
if (record.meterId === meter.meterId) {
|
|
|
|
|
meterConsumption += Number(record.consumption) || 0
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
if (meterConsumption > 0) {
|
|
|
|
|
const meterNodeName = `设备:${meter.meterCode}`
|
|
|
|
|
if (!nodeMap[meterNodeName]) {
|
|
|
|
|
nodes.push({ name: meterNodeName })
|
|
|
|
|
nodeMap[meterNodeName] = true
|
|
|
|
|
}
|
|
|
|
|
links.push({
|
|
|
|
|
source: `产线:${lineName}`,
|
|
|
|
|
target: meterNodeName,
|
|
|
|
|
value: meterConsumption
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-30 13:06:14 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-07 11:07:49 +08:00
|
|
|
// 渲染桑基图
|
2026-04-30 13:06:14 +08:00
|
|
|
const option = {
|
|
|
|
|
tooltip: {
|
2026-05-07 11:07:49 +08:00
|
|
|
trigger: 'item',
|
|
|
|
|
formatter: function (params) {
|
|
|
|
|
if (params.dataType === 'edge') {
|
|
|
|
|
return `${params.data.source} → ${params.data.target}<br/>能耗: ${params.data.value.toFixed(2)}`
|
|
|
|
|
} else {
|
|
|
|
|
return `${params.name}<br/>能耗: ${params.value ? params.value.toFixed(2) : 0}`
|
|
|
|
|
}
|
2026-04-30 13:06:14 +08:00
|
|
|
}
|
|
|
|
|
},
|
2026-05-07 11:07:49 +08:00
|
|
|
series: [{
|
|
|
|
|
type: 'sankey',
|
|
|
|
|
layout: 'none',
|
|
|
|
|
emphasis: {
|
|
|
|
|
focus: 'adjacency'
|
|
|
|
|
},
|
|
|
|
|
data: nodes,
|
|
|
|
|
links: links,
|
|
|
|
|
lineStyle: {
|
|
|
|
|
color: 'gradient',
|
|
|
|
|
curveness: 0.5
|
|
|
|
|
},
|
|
|
|
|
label: {
|
|
|
|
|
color: '#333'
|
2026-04-30 13:06:14 +08:00
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
}]
|
2026-04-30 13:06:14 +08:00
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
|
|
|
|
|
this.sankeyChart.setOption(option)
|
2026-04-30 13:06:14 +08:00
|
|
|
},
|
2026-05-07 11:07:49 +08:00
|
|
|
updateLineEnergyTrendChart() {
|
|
|
|
|
const dateFormat = 'YYYY-MM-DD'
|
|
|
|
|
const groupedData = new Map()
|
|
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
this.energyRecords.forEach(record => {
|
2026-05-07 11:07:49 +08:00
|
|
|
if (this.selectedLineAnalysisEnergyTypeId && record.energyId !== this.selectedLineAnalysisEnergyTypeId) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
const meter = this.meterList.find(m => m.meterId === record.meterId)
|
2026-05-07 11:07:49 +08:00
|
|
|
if (!meter || !meter.productionLine) return
|
|
|
|
|
|
|
|
|
|
const lines = Array.isArray(meter.productionLine) ? meter.productionLine : [meter.productionLine]
|
|
|
|
|
const dateKey = dayjs(record.recordDate).format(dateFormat)
|
|
|
|
|
|
|
|
|
|
lines.forEach(lineId => {
|
|
|
|
|
if (this.selectedLines.length > 0 && !this.selectedLines.includes(lineId)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lineName = this.getLineName(lineId)
|
|
|
|
|
if (!groupedData.has(dateKey)) {
|
|
|
|
|
groupedData.set(dateKey, {})
|
|
|
|
|
}
|
|
|
|
|
if (!groupedData.get(dateKey)[lineName]) {
|
|
|
|
|
groupedData.get(dateKey)[lineName] = 0
|
|
|
|
|
}
|
|
|
|
|
groupedData.get(dateKey)[lineName] += Number(record.consumption) || 0
|
|
|
|
|
})
|
2026-04-30 13:06:14 +08:00
|
|
|
})
|
|
|
|
|
|
2026-05-07 11:07:49 +08:00
|
|
|
const dates = Array.from(groupedData.keys()).sort()
|
|
|
|
|
const lineNames = Array.from(new Set(
|
|
|
|
|
Array.from(groupedData.values()).flatMap(obj => Object.keys(obj))
|
|
|
|
|
)).sort()
|
|
|
|
|
|
|
|
|
|
const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#00CED1', '#00BFFF', '#32CD32']
|
|
|
|
|
const series = lineNames.map((lineName, index) => ({
|
|
|
|
|
name: lineName,
|
|
|
|
|
type: 'line',
|
|
|
|
|
smooth: true,
|
|
|
|
|
data: dates.map(date => groupedData.get(date)[lineName] || 0),
|
|
|
|
|
itemStyle: { color: colors[index % colors.length] },
|
|
|
|
|
areaStyle: {
|
|
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
|
{ offset: 0, color: colors[index % colors.length] + '80' },
|
|
|
|
|
{ offset: 1, color: colors[index % colors.length] + '10' }
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const selectedEnergyType = this.energyTypeList.find(t => t.energyTypeId === this.selectedLineAnalysisEnergyTypeId)
|
|
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
const option = {
|
|
|
|
|
tooltip: {
|
2026-05-07 11:07:49 +08:00
|
|
|
trigger: 'axis'
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
data: lineNames,
|
|
|
|
|
top: 10
|
2026-04-30 13:06:14 +08:00
|
|
|
},
|
|
|
|
|
grid: {
|
|
|
|
|
left: '3%',
|
|
|
|
|
right: '4%',
|
|
|
|
|
bottom: '3%',
|
2026-05-07 11:07:49 +08:00
|
|
|
top: 80,
|
2026-04-30 13:06:14 +08:00
|
|
|
containLabel: true
|
|
|
|
|
},
|
|
|
|
|
xAxis: {
|
2026-05-07 11:07:49 +08:00
|
|
|
type: 'category',
|
|
|
|
|
boundaryGap: false,
|
|
|
|
|
data: dates
|
2026-04-30 13:06:14 +08:00
|
|
|
},
|
|
|
|
|
yAxis: {
|
2026-05-07 11:07:49 +08:00
|
|
|
type: 'value',
|
|
|
|
|
name: selectedEnergyType ? selectedEnergyType.name : '能耗'
|
2026-04-30 13:06:14 +08:00
|
|
|
},
|
2026-05-07 11:07:49 +08:00
|
|
|
series
|
2026-04-30 13:06:14 +08:00
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
this.lineEnergyTrendChart.setOption(option)
|
2025-09-28 14:38:41 +08:00
|
|
|
},
|
2026-04-30 13:06:14 +08:00
|
|
|
updateTableData() {
|
|
|
|
|
const lineEnergyMap = new Map()
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
this.energyRecords.forEach(record => {
|
|
|
|
|
const meter = this.meterList.find(m => m.meterId === record.meterId)
|
|
|
|
|
if (meter && meter.productionLine) {
|
|
|
|
|
const lines = Array.isArray(meter.productionLine) ? meter.productionLine : [meter.productionLine]
|
|
|
|
|
lines.forEach(lineId => {
|
|
|
|
|
const lineName = this.getLineName(lineId)
|
|
|
|
|
const energyTypeId = record.energyId
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
if (!lineEnergyMap.has(lineName)) {
|
|
|
|
|
lineEnergyMap.set(lineName, {})
|
|
|
|
|
}
|
|
|
|
|
const lineData = lineEnergyMap.get(lineName)
|
|
|
|
|
if (!lineData['type_' + energyTypeId]) {
|
|
|
|
|
lineData['type_' + energyTypeId] = 0
|
|
|
|
|
}
|
|
|
|
|
lineData['type_' + energyTypeId] += Number(record.consumption) || 0
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.tableData = Array.from(lineEnergyMap.entries()).map(([lineName, data]) => {
|
|
|
|
|
const row = { lineName, ...data }
|
|
|
|
|
let total = 0
|
|
|
|
|
this.energyTypeList.forEach(type => {
|
|
|
|
|
const val = data['type_' + type.energyTypeId] || 0
|
|
|
|
|
total += val
|
|
|
|
|
})
|
|
|
|
|
row.total = total
|
|
|
|
|
return row
|
|
|
|
|
})
|
2025-09-30 10:01:16 +08:00
|
|
|
},
|
2026-04-30 13:06:14 +08:00
|
|
|
getSummaries(param) {
|
|
|
|
|
const { columns, data } = param
|
|
|
|
|
const sums = []
|
|
|
|
|
columns.forEach((column, index) => {
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
sums[index] = '合计'
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (column.property === 'total') {
|
|
|
|
|
const values = data.map(item => Number(item[column.property]) || 0)
|
|
|
|
|
sums[index] = values.reduce((prev, curr) => prev + curr, 0).toFixed(2)
|
|
|
|
|
} else if (column.property && column.property.startsWith('type_')) {
|
|
|
|
|
const values = data.map(item => Number(item[column.property]) || 0)
|
|
|
|
|
sums[index] = values.reduce((prev, curr) => prev + curr, 0).toFixed(2)
|
|
|
|
|
} else {
|
|
|
|
|
sums[index] = ''
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return sums
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.ems-dashboard-container {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
background: #f6f7fb;
|
|
|
|
|
min-height: calc(100vh - 100px);
|
|
|
|
|
|
|
|
|
|
.filter-panel {
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
|
|
|
|
.filter-form {
|
|
|
|
|
margin: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-cards {
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-4px);
|
|
|
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&.stat-0 {
|
|
|
|
|
border-left: 4px solid #409EFF;
|
2025-09-30 10:01:16 +08:00
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
&.stat-1 {
|
|
|
|
|
border-left: 4px solid #67C23A;
|
2025-09-30 10:01:16 +08:00
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
&.stat-2 {
|
|
|
|
|
border-left: 4px solid #E6A23C;
|
2025-09-30 10:01:16 +08:00
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
&.stat-3 {
|
|
|
|
|
border-left: 4px solid #F56C6C;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
width: 60px;
|
|
|
|
|
height: 60px;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin-right: 16px;
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
|
|
|
|
.stat-0 & {
|
|
|
|
|
background: linear-gradient(135deg, #409EFF, #00CED1);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
.stat-1 & {
|
|
|
|
|
background: linear-gradient(135deg, #67C23A, #95E67D);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
.stat-2 & {
|
|
|
|
|
background: linear-gradient(135deg, #E6A23C, #F7D94C);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
2026-05-07 11:07:49 +08:00
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
.stat-3 & {
|
|
|
|
|
background: linear-gradient(135deg, #F56C6C, #FC9494);
|
|
|
|
|
color: #ffffff;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #303133;
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #909399;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-trend {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #67C23A;
|
|
|
|
|
|
|
|
|
|
&:has(.el-icon-bottom) {
|
|
|
|
|
color: #F56C6C;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.charts-section {
|
2026-05-07 11:07:49 +08:00
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
2026-04-30 13:06:14 +08:00
|
|
|
.chart-card {
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
|
|
|
|
|
|
.chart-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
|
|
|
|
.chart-title {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #303133;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 350px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-container {
|
|
|
|
|
width: 100%;
|
2025-09-30 10:01:16 +08:00
|
|
|
}
|
2025-09-28 14:38:41 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-30 13:06:14 +08:00
|
|
|
</style>
|